Subprocess management: use a decorator
Use a decorator instead of a mixin to handle subprocess spawning and monitoring
This commit is contained in:
@@ -1,13 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# This file is for code dealing specifically with blender
|
||||
|
||||
import logging
|
||||
import bpy
|
||||
from bpy.props import CollectionProperty
|
||||
from bpy.types import PropertyGroup, Panel, UIList, AddonPreferences, Operator
|
||||
from .subprocess_adapter import SubprocessOperatorMixin
|
||||
from .subprocess_adapter import subprocess_operator
|
||||
from . import bpackage as bpkg
|
||||
|
||||
class RepositoryProperty(PropertyGroup):
|
||||
url = bpy.props.StringProperty(name="URL")
|
||||
# status = bpy.props.EnumProperty(name="Status")
|
||||
|
||||
|
||||
class PACKAGE_UL_repositories(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
@@ -32,23 +36,37 @@ class PackagePreferences(AddonPreferences):
|
||||
col.operator("package.add_repository", icon="ZOOMIN", text="")
|
||||
col.operator("package.remove_repository", icon="ZOOMOUT", text="")
|
||||
|
||||
class PACKAGE_OT_fetch(SubprocessOperatorMixin, bpy.types.Operator):
|
||||
bl_idname = "package.fetch"
|
||||
row = layout.row()
|
||||
row.operator("package.refresh")
|
||||
|
||||
|
||||
@subprocess_operator
|
||||
class PACKAGE_OT_refresh(Operator):
|
||||
bl_idname = "package.refresh"
|
||||
bl_label = "Update package list(s)"
|
||||
|
||||
last_response = None
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
settings = bpy.context.window_manager.package_manager_settings
|
||||
self.subprocess = Process(target=blenderpack.fetch, args=(settings.url, self.pipe))
|
||||
def invoke(self, context, event):
|
||||
prefs = context.user_preferences.addons[__package__].preferences
|
||||
if 'repositories' not in prefs:
|
||||
return {'FINISHED'}
|
||||
# HACK: just do the active repo for now
|
||||
repo = bpkg.Repository(prefs['repositories'][prefs.active_repository].to_dict())
|
||||
self.proc_args = []
|
||||
self.proc_kwargs = {'target': repo.refresh}
|
||||
|
||||
def execute(self, context, event):
|
||||
pass
|
||||
|
||||
def modal(self, context, event):
|
||||
# try:
|
||||
self.poll_subprocess()
|
||||
# except:
|
||||
|
||||
def handle_response(self, resp):
|
||||
self.__class__.last_response = resp
|
||||
self.report({'INFO'}, "Request returned %s" % self.__class__.last_response)
|
||||
self.report({'INFO'}, "Request returned %s" % resp)
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
class PACKAGE_OT_add_repository(bpy.types.Operator):
|
||||
bl_idname = "package.add_repository"
|
||||
@@ -73,7 +91,7 @@ def register():
|
||||
bpy.utils.register_class(RepositoryProperty)
|
||||
bpy.utils.register_class(PackagePreferences)
|
||||
|
||||
bpy.utils.register_class(PACKAGE_OT_fetch)
|
||||
bpy.utils.register_class(PACKAGE_OT_refresh)
|
||||
|
||||
bpy.utils.register_class(PACKAGE_OT_add_repository)
|
||||
bpy.utils.register_class(PACKAGE_OT_remove_repository)
|
||||
@@ -83,7 +101,7 @@ def unregister():
|
||||
bpy.utils.unregister_class(RepositoryProperty)
|
||||
bpy.utils.unregister_class(PackagePreferences)
|
||||
|
||||
bpy.utils.unregister_class(PACKAGE_OT_fetch)
|
||||
bpy.utils.unregister_class(PACKAGE_OT_refresh)
|
||||
|
||||
bpy.utils.unregister_class(PACKAGE_OT_add_repository)
|
||||
bpy.utils.unregister_class(PACKAGE_OT_remove_repository)
|
||||
|
@@ -1,3 +1,3 @@
|
||||
import bpackage.exceptions
|
||||
from . import exceptions
|
||||
from .package import Package
|
||||
from .repository import Repository
|
||||
|
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# HACK: seems 'requests' module bundled with blender isn't bundled with 'idna' module. So force system python for now
|
||||
# import sys
|
||||
# sys.path.insert(0, '/usr/lib/python3.6/site-packages')
|
||||
import sys
|
||||
sys.path.insert(0, '/usr/lib/python3.6/site-packages')
|
||||
|
||||
import logging
|
||||
|
||||
|
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# HACK: seems 'requests' module bundled with blender isn't bundled with 'idna' module. So force system python for now
|
||||
# import sys
|
||||
# sys.path.insert(0, '/usr/lib/python3.6/site-packages')
|
||||
import sys
|
||||
sys.path.insert(0, '/usr/lib/python3.6/site-packages')
|
||||
|
||||
from pathlib import Path
|
||||
import requests
|
||||
@@ -56,10 +56,15 @@ class Repository:
|
||||
req_headers = {}
|
||||
|
||||
# Do things this way to avoid adding empty objects/None to the req_headers dict
|
||||
if 'etag' in self._headers:
|
||||
req_headers['If-None-Match'] = self._headers['etag']
|
||||
if 'last-modified' in self._headers:
|
||||
req_headers['If-Modified-Since'] = self._headers['last-modified']
|
||||
if self._headers:
|
||||
try:
|
||||
req_headers['If-None-Match'] = self._headers['etag']
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
req_headers['If-Modified-Since'] = self._headers['last-modified']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
#try
|
||||
resp = requests.get(self.url, headers=req_headers)
|
||||
|
@@ -1,39 +1,59 @@
|
||||
import logging
|
||||
from multiprocessing import Process, Pipe
|
||||
from bpy.types import Operator
|
||||
|
||||
class SubprocessOperatorMixin:
|
||||
timer = None
|
||||
log = logging.getLogger("%s.SubprocessOperatorMixin" % __name__)
|
||||
def subprocess_operator(cls: Operator, polling_interval=.01) -> Operator:
|
||||
"""
|
||||
Class decorator which wraps Operator methods with setup code for running a subprocess.
|
||||
|
||||
# run once in invoke
|
||||
def setup(self):
|
||||
pass
|
||||
# run on receipt of data from subprocess
|
||||
def handle_response(self, resp):
|
||||
pass
|
||||
Expects args for Process() to defined in cls.proc_args and cls.proc_kwargs. For example,
|
||||
setting cls.proc_kwargs = {'target:' some_func} can be used to run 'some_func' in
|
||||
a subprocess.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.pipe = Pipe()
|
||||
self.subprocess = None
|
||||
def decoratify(cls, methodname, decorator):
|
||||
"""
|
||||
Decorate `cls.methodname` with `decorator` if method `methodname` exists in cls
|
||||
else just call the decorator with an empty function
|
||||
"""
|
||||
orig_method = getattr(cls, methodname, None)
|
||||
if orig_method:
|
||||
setattr(cls, methodname, decorator(orig_method))
|
||||
else:
|
||||
# HACK: need a no-op which accepts any arguments
|
||||
setattr(cls, methodname, decorator(lambda *args, **kwargs: None))
|
||||
|
||||
def execute(self, context):
|
||||
return self.invoke(context, None)
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.subprocess.start()
|
||||
# we have to explicitly close the end of the pipe we are NOT using,
|
||||
# otherwise no exception will be generated when the other process closes its end.
|
||||
self.pipe[1].close()
|
||||
def decorate_execute(orig_execute):
|
||||
def execute(self, context):
|
||||
orig_execute(self, context)
|
||||
call_copy_of_method_if_exist(cls, 'execute', self, context)
|
||||
return self.invoke(context, None)
|
||||
return execute
|
||||
|
||||
|
||||
wm = context.window_manager
|
||||
wm.modal_handler_add(self)
|
||||
self.timer = wm.event_timer_add(.01, context.window)
|
||||
def decorate_invoke(orig_invoke):
|
||||
def invoke(self, context, event):
|
||||
orig_invoke(self, context, event)
|
||||
|
||||
self.setup()
|
||||
self.pipe = Pipe()
|
||||
self.proc = Process(
|
||||
*getattr(self, 'proc_args', []),
|
||||
**getattr(self, 'proc_kwargs', [])
|
||||
)
|
||||
self.proc.start()
|
||||
# we have to explicitly close the end of the pipe we are NOT using,
|
||||
# otherwise no exception will be generated when the other process closes its end.
|
||||
self.pipe[1].close()
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
wm = context.window_manager
|
||||
wm.modal_handler_add(self)
|
||||
self.timer = wm.event_timer_add(polling_interval, context.window)
|
||||
|
||||
def modal(self, context, event):
|
||||
return {'RUNNING_MODAL'}
|
||||
return invoke
|
||||
|
||||
def poll_subprocess(self):
|
||||
self.log.debug("polling")
|
||||
try:
|
||||
if self.pipe[0].poll():
|
||||
@@ -45,6 +65,13 @@ class SubprocessOperatorMixin:
|
||||
return {'FINISHED'}
|
||||
|
||||
if newdata is not None:
|
||||
self.handle_response(newdata)
|
||||
self.handle_response(newdata) #TODO: make this a customizable callback
|
||||
# this should allow chaining of multiple subprocess in a single operator
|
||||
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
decoratify(cls, 'execute', decorate_execute)
|
||||
decoratify(cls, 'invoke', decorate_invoke)
|
||||
setattr(cls, 'poll_subprocess', poll_subprocess)
|
||||
|
||||
return cls
|
||||
|
Reference in New Issue
Block a user