From 1cce8cda8108ba727cec61bd3cc1fae69fd78222 Mon Sep 17 00:00:00 2001 From: Peter Cassetta Date: Sun, 26 Jun 2016 22:00:03 -0500 Subject: [PATCH] Documentation: Documenting every class and function. Added a docstring to every class and function, documenting arguments and return values. Also made a couple cleanup changes. Next cleanup-related change will be moving from urllib.requests to the requests module. --- package_manager/__init__.py | 24 ++++- package_manager/networking.py | 171 +++++++++++++++++++++++----------- 2 files changed, 139 insertions(+), 56 deletions(-) diff --git a/package_manager/__init__.py b/package_manager/__init__.py index 1326bd4..0bbfd82 100644 --- a/package_manager/__init__.py +++ b/package_manager/__init__.py @@ -31,9 +31,10 @@ bl_info = { import bpy import addon_utils -from bpy.types import Operator, AddonPreferences -from bpy.props import StringProperty, BoolProperty, IntProperty, CollectionProperty +from bpy.types import AddonPreferences +from bpy.props import StringProperty, IntProperty, CollectionProperty +# Support reloading if "bpy" in locals(): import imp try: @@ -43,7 +44,10 @@ if "bpy" in locals(): else: from . import networking + class PackageManagerAddon(bpy.types.PropertyGroup): + """PropertyGroup representing an add-on available for download.""" + source = StringProperty() name = StringProperty() description = StringProperty() @@ -59,15 +63,20 @@ class PackageManagerAddon(bpy.types.PropertyGroup): module_name = StringProperty() download_url = StringProperty() + class PackageManagerPreferences(AddonPreferences): - # this must match the addon name, use '__package__' - # when defining this in a submodule of a python package. + """Package Manager's add-on preferences. + + Entire add-on functionality is available from its preferences panel. + """ bl_idname = __name__ pm_addons = CollectionProperty(type=PackageManagerAddon) pm_addons_index = IntProperty() def draw(self, context): + """Draw preferences UI.""" + layout = self.layout split = layout.split(percentage=1.0/3) @@ -151,15 +160,22 @@ class PackageManagerPreferences(AddonPreferences): for i in range(4 - tot_row): split.separator() + def register(): + """Register classes, operators, and preferences.""" + networking.register() bpy.utils.register_class(PackageManagerAddon) bpy.utils.register_class(PackageManagerPreferences) + def unregister(): + """Unregister classes, operators, and preferences.""" + networking.unregister() bpy.utils.unregister_class(PackageManagerAddon) bpy.utils.unregister_class(PackageManagerPreferences) + if __name__ == "__main__": register() diff --git a/package_manager/networking.py b/package_manager/networking.py index fb1e637..23dde33 100644 --- a/package_manager/networking.py +++ b/package_manager/networking.py @@ -42,12 +42,16 @@ class WM_OT_update_index(bpy.types.Operator): @classmethod def poll(self, context): + """Run operator only if an asynchronous download is not in progress.""" + global download_install_status return (not download_install_status or download_install_status == "Install successful" or "failed" in download_install_status) def __init__ (self): + """Init some variables and ensure proper states on run.""" + global download_install_status download_install_status = "" @@ -57,11 +61,10 @@ class WM_OT_update_index(bpy.types.Operator): self.loop.stop() def execute(self, context): - # Update add-on index + """Begin asynchronous execution and modal timer.""" + self.loop.stop() - self.status("Starting") - self.loop.run_in_executor(None, self.update_index) wm = context.window_manager @@ -71,6 +74,8 @@ class WM_OT_update_index(bpy.types.Operator): return {'RUNNING_MODAL'} def modal(self, context, event): + """Check status of list update and terminate operator when complete.""" + global download_install_status if self._redraw: @@ -88,18 +93,28 @@ class WM_OT_update_index(bpy.types.Operator): return {'PASS_THROUGH'} def cancel(self, context): + """Ensure timer and loop are stopped before operator ends.""" + self.loop.stop() wm = context.window_manager wm.event_timer_remove(self._timer) - def status(self, text): + def status(self, text: str): + """Change list update status for access from main thread, and redraw UI. + + Keyword arguments: + text -- new status + """ + global download_install_status download_install_status = text self._redraw = True def update_index(self): + """Download index.json and update add-on list.""" + self.status("Downloading update") index_file = self.download_index() @@ -116,7 +131,10 @@ class WM_OT_update_index(bpy.types.Operator): self.status("Update failed") def download_index(self): - # Download the index.json file + """Download index.json and return it as a str, or return False if an + error occurs. + """ + try: req = urllib.request.urlopen(INDEX_DOWNLOAD_URL) index_file = req.read().decode('utf-8') @@ -130,7 +148,14 @@ class WM_OT_update_index(bpy.types.Operator): return index_file - def parse_json(self, index_file): + def parse_json(self, index_file: str) -> bool: + """Parse JSON text and create add-on entries from data, return True on + success or False when an error occurs during parsing. + + Keyword arguments: + index_file -- JSON text + """ + # Parse downloaded file try: addon_list = json.loads(index_file) @@ -165,42 +190,31 @@ class WM_OT_update_index(bpy.types.Operator): return True - def load_addon_data(self, addon, module_name, content): + def load_addon_data(self, addon: PackageManagerAddon, module_name: str, + content: dict): + """Load interpreted JSON data into a PackageManagerAddon object + + Keyword arguments: + addon -- PackageManagerAddon to populate + module_name -- module name of add-on + content -- dict with parsed JSON data + """ + addon.name = content["name"] addon.blender = '.'.join(map(str, content["blender"])) addon.module_name = module_name addon.download_url = content["download_url"] - if "author" in content: - addon.author = content["author"] + optional_keys = ["author", "category", "description", "location", + "source", "support", "tracker_url", "warning", "wiki_url"] - if "category" in content: - addon.category = content["category"] - - if "description" in content: - addon.description = content["description"] - - if "location" in content: - addon.location = content["location"] - - if "source" in content: - addon.source = content["source"] - - if "support" in content: - addon.support = content["support"] - - if "tracker_url" in content: - addon.tracker_url = content["tracker_url"] + for key in optional_keys: + if key in content: + addon[key] = content[key] if "version" in content: # TODO: add multi-version functionality addon.version = '.'.join(map(str, content["version"])) - - if "warning" in content: - addon.warning = content["warning"] - - if "wiki_url" in content: - addon.wiki_url = content["wiki_url"] class WM_OT_addon_download_install(bpy.types.Operator): @@ -215,12 +229,16 @@ class WM_OT_addon_download_install(bpy.types.Operator): @classmethod def poll(self, context): + """Run operator only if an asynchronous download is not in progress.""" + global download_install_status return (not download_install_status or download_install_status == "Install successful" or "failed" in download_install_status) def __init__ (self): + """Init some variables and ensure proper states on run.""" + global download_install_status download_install_status = "" @@ -230,6 +248,10 @@ class WM_OT_addon_download_install(bpy.types.Operator): self.loop.stop() def execute(self, context): + """Perform verification on selected addon, then begin asynchronous + execution and modal timer. + """ + if self.addon is None: return {'CANCELLED'} @@ -262,6 +284,8 @@ class WM_OT_addon_download_install(bpy.types.Operator): return {'RUNNING_MODAL'} def modal(self, context, event): + """Check status of download/install, and exit operator when complete.""" + global download_install_status if self._redraw: @@ -279,21 +303,38 @@ class WM_OT_addon_download_install(bpy.types.Operator): return {'PASS_THROUGH'} def cancel(self, context): + """Ensure timer and loop are stopped before operator ends.""" + self.loop.stop() wm = context.window_manager wm.event_timer_remove(self._timer) - def status(self, text): + def status(self, text: str): + """Change install/download status for access from main thread, and + redraw UI. + + Keyword arguments: + text -- new status + """ + global download_install_status download_install_status = text self._redraw = True - def download_and_install (self, addon, download_url, filetype): - # Perform download; if successful, install + def download_and_install (self, addon: str, download_url: str, + filetype: str): + """Download add-on of filetype from download_url and install it. + + Keyword arguments: + addon -- module name of the add-on to install + download_url -- the url to download add-on from + filetype -- the filetype of the add-on, .py or .zip + """ + self.status("Downloading") - if self.download(addon, download_url): + if self.download(download_url): self.status("Installing") if self.install(addon, filetype): self.status("Install successful") @@ -304,11 +345,19 @@ class WM_OT_addon_download_install(bpy.types.Operator): self.status("Download failed") self._redraw = True - def download(self, addon, download_url): + def download(self, download_url: str): + """Download add-on from download_url and save it to disk. Return False + on download error, or True if successful. + + Keyword arguments: + download_url -- the url to download add-on from + """ + filetype = os.path.splitext(download_url)[1] - download_path = bpy.utils.user_resource('SCRIPTS', path="addons/package_manager/" - "download%s" % filetype) + download_path = bpy.utils.user_resource('SCRIPTS', path="addons/" + "package_manager/download%s" + % filetype) # Download add-on and save to disk try: @@ -317,7 +366,8 @@ class WM_OT_addon_download_install(bpy.types.Operator): shutil.copyfileobj(req, download_file) req.close() except urllib.error.HTTPError as err: - log.warning("Download failed with HTTPError: %s %s", str(err.code), err.reason) + log.warning("Download failed with HTTPError: %s %s", str(err.code), + err.reason) return False except urllib.error.URLError as err: log.warning("Download failed with URLError: %s", err) @@ -325,32 +375,49 @@ class WM_OT_addon_download_install(bpy.types.Operator): return True - def install(self, addon, filetype): + def install(self, addon: str, filetype: str): + """Install downloaded add-on on disk to USER add-ons path. Return False + on installation error, or True if successful. + + Keyword arguments: + addon -- module name of the add-on to install + filetype -- the filetype of the add-on, .py or .zip + """ + filename = addon + (filetype if filetype == ".py" else "") - download_path = bpy.utils.user_resource('SCRIPTS', path="addons/package_manager/" - "download%s" % filetype) + download_path = bpy.utils.user_resource('SCRIPTS', path="addons/" + "package_manager/download%s" + % filetype) addon_path = bpy.utils.user_resource('SCRIPTS', path="addons/%s" % filename) # Copy downloaded add-on to USER scripts path - if filetype == ".py": - shutil.move(download_path, addon_path) - elif filetype == ".zip": - # Remove existing add-on - if os.path.exists(addon_path): - shutil.rmtree(addon_path) - with zipfile.ZipFile(download_path,"r") as zipped_addon: - zipped_addon.extractall(bpy.utils.user_resource('SCRIPTS', path="addons")) - else: + try: + if filetype == ".py": + shutil.move(download_path, addon_path) + elif filetype == ".zip": + # Remove existing add-on + if os.path.exists(addon_path): + shutil.rmtree(addon_path) + with zipfile.ZipFile(download_path,"r") as zipped_addon: + zipped_addon.extractall(bpy.utils.user_resource('SCRIPTS', path="addons")) + else: + return False + except Exception as err: + log.warning("Install failed: %s", err) return False return True def register(): + """Register operators.""" + bpy.utils.register_class(WM_OT_update_index) bpy.utils.register_class(WM_OT_addon_download_install) def unregister(): + """Unregister operators.""" + bpy.utils.unregister_class(WM_OT_update_index) bpy.utils.unregister_class(WM_OT_addon_download_install) \ No newline at end of file