diff --git a/__init__.py b/__init__.py deleted file mode 100644 index ec0fd92..0000000 --- a/__init__.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- - -bl_info = { - 'name': 'Package Manager', - 'description': 'Testing package management', - 'category': 'System', - 'support': 'TESTING', -} - -# from bpy.utils import register_class, unregister_class -import importlib -import sys -import logging - -log = logging.getLogger(__name__) - -def register(): - - # Reload support - if '%s.blender_common' % __name__ in sys.modules: - - def reload_mod(name): - log.debug("Reloading %s", name) - modname = '%s.%s' % (__name__, name) - try: - old_module = sys.modules[modname] - except KeyError: - # Wasn't loaded before -- can happen after an upgrade. - new_module = importlib.import_module(modname) - else: - new_module = importlib.reload(old_module) - - sys.modules[modname] = new_module - return new_module - - blender_common = reload_mod('blender_common') - subprocess_adapter = reload_mod('subprocess_adapter') - bpackage = reload_mod('bpackage') - - else: - from . import blender_common - - blender_common.register() - -def unregister(): - blender_common.unregister() - diff --git a/blender_common.py b/blender_common.py deleted file mode 100644 index 1b124f9..0000000 --- a/blender_common.py +++ /dev/null @@ -1,122 +0,0 @@ -# -*- 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 subprocess_operator, subprocess_function -from . import bpackage as bpkg - -class RepositoryProperty(PropertyGroup): - url = bpy.props.StringProperty(name="URL") - status = bpy.props.EnumProperty(name="Status", items=[ - ("OK", "Okay", "FILE_TICK"), - ("NOTFOUND", "Not found", "ERROR"), - ("NOCONNECT", "Could not connect", "QUESTION"), - ]) - - -class PACKAGE_UL_repositories(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - split = layout.split(0.3) - split.label(item.status) - split.prop(item, "name") - split.prop(item, "url") - - def invoke(self, onctext, event): - pass - -class PackagePreferences(AddonPreferences): - bl_idname = __package__ - - repositories = CollectionProperty(type=RepositoryProperty) - active_repository = bpy.props.IntProperty() - - def draw(self, context): - layout = self.layout - row = layout.row() - row.template_list("PACKAGE_UL_repositories", "", self, "repositories", self, "active_repository") - col = row.column(align=True) - col.operator("package.add_repository", icon="ZOOMIN", text="") - col.operator("package.remove_repository", icon="ZOOMOUT", text="") - - row = layout.row() - row.operator("package.refresh") - - -@subprocess_operator -class PACKAGE_OT_refresh(Operator): - """Check for new and updated packages""" - bl_idname = "package.refresh" - bl_label = "Refresh Packages" - - log = logging.getLogger(__name__) - - def invoke(self, context, event): - prefs = context.user_preferences.addons[__package__].preferences - - if 'repositories' not in prefs or len(prefs['repositories']) <= 0: - self.log.debug(prefs) - self.report({'WARNING'}, "No respositories to refresh") - return {'FINISHED'} - - # HACK: just use the active repo until we do multi-repo support - repo = bpkg.Repository(prefs['repositories'][prefs.active_repository].to_dict()) - - self.proc_kwargs = {'target': subprocess_function(repo.refresh, pipe=self.pipe)} - - return {'RUNNING_MODAL'} - - def modal(self, context, event): - try: - self.poll_subprocess() - except bpkg.repository.MissingURLError: - self.report({'WARNING'}, "No URL specified") - - - return {'RUNNING_MODAL'} - - def handle_response(self, resp): - self.report({'INFO'}, "Request returned %s" % resp) - - -class PACKAGE_OT_add_repository(bpy.types.Operator): - bl_idname = "package.add_repository" - bl_label = "Add Repository" - - def execute(self, context): - prefs = context.user_preferences.addons[__package__].preferences - prefs.repositories.add() - return {'FINISHED'} - -class PACKAGE_OT_remove_repository(bpy.types.Operator): - bl_idname = "package.remove_repository" - bl_label = "Remove Repository" - - def execute(self, context): - prefs = context.user_preferences.addons[__package__].preferences - prefs.repositories.remove(prefs.active_repository) - return {'FINISHED'} - - -def register(): - bpy.utils.register_class(RepositoryProperty) - bpy.utils.register_class(PackagePreferences) - - bpy.utils.register_class(PACKAGE_OT_refresh) - - bpy.utils.register_class(PACKAGE_OT_add_repository) - bpy.utils.register_class(PACKAGE_OT_remove_repository) - bpy.utils.register_class(PACKAGE_UL_repositories) - -def unregister(): - bpy.utils.unregister_class(RepositoryProperty) - bpy.utils.unregister_class(PackagePreferences) - - bpy.utils.unregister_class(PACKAGE_OT_refresh) - - bpy.utils.unregister_class(PACKAGE_OT_add_repository) - bpy.utils.unregister_class(PACKAGE_OT_remove_repository) - bpy.utils.unregister_class(PACKAGE_UL_repositories) - diff --git a/bpackage/__init__.py b/bpackage/__init__.py deleted file mode 100644 index cdb22b2..0000000 --- a/bpackage/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from . import exceptions -from .package import Package -from .repository import Repository diff --git a/bpackage/exceptions.py b/bpackage/exceptions.py deleted file mode 100644 index e677624..0000000 --- a/bpackage/exceptions.py +++ /dev/null @@ -1,14 +0,0 @@ -class BadAddon(Exception): - """ - Raised when something expected to be an addon turns out not to be one after all - """ - -class RepositoryListError(ValueError): - """ - Raised when something is amiss with the repo.json file - """ - -class RepositoryNotFoundError(RepositoryListError): - """ - Raised when looking for a repository which isn't there - """ diff --git a/bpackage/package.py b/bpackage/package.py deleted file mode 100644 index ae76121..0000000 --- a/bpackage/package.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/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 logging - -log = logging.getLogger(__name__) - - -class Package: - """ - Stores package methods and metadata - """ - - def __init__(self, package_dict:dict = None): - self.from_dict(package_dict) - - def to_dict(self) -> dict: - """ - Return a dict representation of the package - """ - return { - 'bl_info': self.bl_info, - 'url': self.url, - } - - def from_dict(self, package_dict: dict): - """ - Get attributes from a dict such as produced by `to_dict` - """ - if package_dict is None: - package_dict = {} - - for attr in ('name', 'url', 'bl_info'): - setattr(self, attr, package_dict.get(attr)) - - diff --git a/bpackage/repository.py b/bpackage/repository.py deleted file mode 100644 index 1a6a7e7..0000000 --- a/bpackage/repository.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/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') - -from pathlib import Path -import requests -import json -import logging - -log = logging.getLogger(__name__) -# log.level = logging.DEBUG - -class RepositoryError(Exception): - """ - Superclass for repository-related exceptions - """ - -class MissingURLError(RepositoryError): - """ - Thrown when the repository needs a URL but doesn't have one - """ - -# class RepositoryNotFound( - -class Package: - """ - Stores package methods and metadata - """ - - def __init__(self, package_dict:dict = None): - self.from_dict(package_dict) - - def to_dict(self) -> dict: - """ - Return a dict representation of the package - """ - return { - 'bl_info': self.bl_info, - 'url': self.url, - } - - def from_dict(self, package_dict: dict): - """ - Get attributes from a dict such as produced by `to_dict` - """ - if package_dict is None: - package_dict = {} - - for attr in ('name', 'url', 'bl_info'): - setattr(self, attr, package_dict.get(attr)) - - -class Repository: - """ - Stores repository metadata (including packages) - """ - - def __init__(self, repo_dict:dict = None): - self.from_dict(repo_dict) - - def refresh(self): - """ - Requests repo.json from URL and embeds etag/last-modification headers - """ - - log.debug(self.url) - if self.url is None: - raise MissingURLError("Cannot refresh repository without a URL") - - - log.debug("Refreshing repository from %s", self.url) - req_headers = {} - - # Do things this way to avoid adding empty objects/None to the req_headers dict - 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) - - repodict = json.loads(resp.json()) - repodict['etag'] = resp.headers.get('etag') - repodict['last-modified'] = resp.headers.get('last-modified') - - self.from_dict(repodict) - - - def to_dict(self) -> dict: - """ - Return a dict representation of the repository - """ - return { - 'name': self.name, - 'packages': [p.to_dict() for p in self.packages], - 'url': self.url, - '_headers': self._headers, - } - - def from_dict(self, repodict: dict): - """ - Get attributes from a dict such as produced by `to_dict` - """ - if repodict is None: - repodict = {} - - for attr in ('name', 'url', 'packages', '_headers'): - if attr == 'package': - value = set(Package(pkg) for pkg in repodict.get('packages', [])) - else: - value = repodict.get(attr) - - setattr(self, attr, value) - - def dump(self, path: Path): - """ - Dump repository as a repo.json file in 'path' - """ - with (path / 'repo.json').open('w', encoding='utf-8') as repo_file: - json.dump(self.to_dict(), repo_file, indent=4, sort_keys=True) - log.info("repo.json written to %s" % path) - - diff --git a/subprocess_adapter.py b/subprocess_adapter.py deleted file mode 100644 index 6720a33..0000000 --- a/subprocess_adapter.py +++ /dev/null @@ -1,127 +0,0 @@ -import logging -from multiprocessing import Process, Pipe -from bpy.types import Operator - -log = logging.getLogger(__name__) - -def subprocess_operator(cls: Operator, polling_interval=.01) -> Operator: - """ - Class decorator which wraps Operator methods with setup code for running a subprocess. - - 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 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 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 - - - def decorate_invoke(orig_invoke): - """ - Create pipe and modal timer, start subprocess - """ - def invoke(self, context, event): - self.pipe = Pipe() #HACK: do this first so operator-defined invoke can access it - # this is needed because the operator is currently responsible for setting up the Process, - # and the pipe is needed for the subprocess_function decorator. - # TODO: Perhaps this responsibility should be handled here instead (simplifies things but looses some flexibility) - - orig_ret = orig_invoke(self, context, event) - - # HACK to allow early exits. - # Perhaps subprocess_operators should define separately named methods, to avoid confusing mixing semantics in return types - if not orig_ret.issubset({'RUNNING_MODAL'}): - log.debug('Exiting early') - return orig_ret - - 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() - - wm = context.window_manager - wm.modal_handler_add(self) - self.timer = wm.event_timer_add(polling_interval, context.window) - - return {'RUNNING_MODAL'} - return invoke - - def poll_subprocess(self): - """ - Check the pipe for new messages (nonblocking). If there is a new message, call the callback - """ - log.debug("polling") - try: - if self.pipe[0].poll(): - resp = self.pipe[0].recv() - # assert(isinstance(resp, SubprocessMessage)) - else: - resp = None - except EOFError: - log.debug("done polling") - return {'FINISHED'} - - if resp is not None: - if resp.exception is not None: - raise resp.exception - elif resp.data is not None: - self.handle_response(resp.data) #TODO: make this a customizable callback - # this should allow chaining of multiple subprocess in a single operator - - decoratify(cls, 'execute', decorate_execute) - decoratify(cls, 'invoke', decorate_invoke) - setattr(cls, 'poll_subprocess', poll_subprocess) - - return cls - -def subprocess_function(func, *args, pipe, **kwargs): - """ - Wrapper which feeds the return val of `func` into the latter end of a pipe - """ - def wrapper(*args, **kwargs): - # 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. - pipe[0].close() - - try: - return_val = func(*args, **kwargs) - pipe[1].send(SubprocessMessage(data=return_val)) - except Exception as err: - log.debug("Caught exception from subprocess: %s", err) - pipe[1].send(SubprocessMessage(exception=err)) - raise - finally: - pipe[1].close() - - return wrapper - -class SubprocessMessage: - """ - Container for communications between child processes and the parent process - """ - #TODO: currently only used for child -> parent, might need something different for parent -> child? - def __init__(self, exception=None, data=None): - self.exception = exception - self.data = data diff --git a/utils.py b/utils.py deleted file mode 100644 index fd2f6b2..0000000 --- a/utils.py +++ /dev/null @@ -1,6 +0,0 @@ -from pathlib import Path -import json - -def dump_repo(repo_path: Path, repo_data: dict): - with (repo_path / 'repo.json').open('w', encoding='utf-8') as repo_file: - json.dump(repo_data, repo_file, indent=4, sort_keys=True)