From 1188f91b7b98d49485d36a78f205799efd49bbfb Mon Sep 17 00:00:00 2001 From: gandalf3 Date: Sat, 8 Jul 2017 17:51:41 -0700 Subject: [PATCH] Subprocess management: use a decorator Use a decorator instead of a mixin to handle subprocess spawning and monitoring --- blender_common.py | 46 ++++++++++++++++-------- bpackage/__init__.py | 2 +- bpackage/package.py | 4 +-- bpackage/repository.py | 17 +++++---- subprocess_adapter.py | 79 ++++++++++++++++++++++++++++-------------- 5 files changed, 99 insertions(+), 49 deletions(-) diff --git a/blender_common.py b/blender_common.py index f5fe58a..23aa3c0 100644 --- a/blender_common.py +++ b/blender_common.py @@ -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) diff --git a/bpackage/__init__.py b/bpackage/__init__.py index 4e67d97..cdb22b2 100644 --- a/bpackage/__init__.py +++ b/bpackage/__init__.py @@ -1,3 +1,3 @@ -import bpackage.exceptions +from . import exceptions from .package import Package from .repository import Repository diff --git a/bpackage/package.py b/bpackage/package.py index da0db4e..ae76121 100644 --- a/bpackage/package.py +++ b/bpackage/package.py @@ -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 diff --git a/bpackage/repository.py b/bpackage/repository.py index e03f025..a978c7d 100644 --- a/bpackage/repository.py +++ b/bpackage/repository.py @@ -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) diff --git a/subprocess_adapter.py b/subprocess_adapter.py index 2af0ba0..904d4ec 100644 --- a/subprocess_adapter.py +++ b/subprocess_adapter.py @@ -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