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.
This commit is contained in:
2016-06-26 22:00:03 -05:00
parent f6897e6401
commit 1cce8cda81
2 changed files with 139 additions and 56 deletions

View File

@@ -31,9 +31,10 @@ bl_info = {
import bpy import bpy
import addon_utils import addon_utils
from bpy.types import Operator, AddonPreferences from bpy.types import AddonPreferences
from bpy.props import StringProperty, BoolProperty, IntProperty, CollectionProperty from bpy.props import StringProperty, IntProperty, CollectionProperty
# Support reloading
if "bpy" in locals(): if "bpy" in locals():
import imp import imp
try: try:
@@ -43,7 +44,10 @@ if "bpy" in locals():
else: else:
from . import networking from . import networking
class PackageManagerAddon(bpy.types.PropertyGroup): class PackageManagerAddon(bpy.types.PropertyGroup):
"""PropertyGroup representing an add-on available for download."""
source = StringProperty() source = StringProperty()
name = StringProperty() name = StringProperty()
description = StringProperty() description = StringProperty()
@@ -59,15 +63,20 @@ class PackageManagerAddon(bpy.types.PropertyGroup):
module_name = StringProperty() module_name = StringProperty()
download_url = StringProperty() download_url = StringProperty()
class PackageManagerPreferences(AddonPreferences): class PackageManagerPreferences(AddonPreferences):
# this must match the addon name, use '__package__' """Package Manager's add-on preferences.
# when defining this in a submodule of a python package.
Entire add-on functionality is available from its preferences panel.
"""
bl_idname = __name__ bl_idname = __name__
pm_addons = CollectionProperty(type=PackageManagerAddon) pm_addons = CollectionProperty(type=PackageManagerAddon)
pm_addons_index = IntProperty() pm_addons_index = IntProperty()
def draw(self, context): def draw(self, context):
"""Draw preferences UI."""
layout = self.layout layout = self.layout
split = layout.split(percentage=1.0/3) split = layout.split(percentage=1.0/3)
@@ -151,15 +160,22 @@ class PackageManagerPreferences(AddonPreferences):
for i in range(4 - tot_row): for i in range(4 - tot_row):
split.separator() split.separator()
def register(): def register():
"""Register classes, operators, and preferences."""
networking.register() networking.register()
bpy.utils.register_class(PackageManagerAddon) bpy.utils.register_class(PackageManagerAddon)
bpy.utils.register_class(PackageManagerPreferences) bpy.utils.register_class(PackageManagerPreferences)
def unregister(): def unregister():
"""Unregister classes, operators, and preferences."""
networking.unregister() networking.unregister()
bpy.utils.unregister_class(PackageManagerAddon) bpy.utils.unregister_class(PackageManagerAddon)
bpy.utils.unregister_class(PackageManagerPreferences) bpy.utils.unregister_class(PackageManagerPreferences)
if __name__ == "__main__": if __name__ == "__main__":
register() register()

View File

@@ -42,12 +42,16 @@ class WM_OT_update_index(bpy.types.Operator):
@classmethod @classmethod
def poll(self, context): def poll(self, context):
"""Run operator only if an asynchronous download is not in progress."""
global download_install_status global download_install_status
return (not download_install_status return (not download_install_status
or download_install_status == "Install successful" or download_install_status == "Install successful"
or "failed" in download_install_status) or "failed" in download_install_status)
def __init__ (self): def __init__ (self):
"""Init some variables and ensure proper states on run."""
global download_install_status global download_install_status
download_install_status = "" download_install_status = ""
@@ -57,11 +61,10 @@ class WM_OT_update_index(bpy.types.Operator):
self.loop.stop() self.loop.stop()
def execute(self, context): def execute(self, context):
# Update add-on index """Begin asynchronous execution and modal timer."""
self.loop.stop() self.loop.stop()
self.status("Starting") self.status("Starting")
self.loop.run_in_executor(None, self.update_index) self.loop.run_in_executor(None, self.update_index)
wm = context.window_manager wm = context.window_manager
@@ -71,6 +74,8 @@ class WM_OT_update_index(bpy.types.Operator):
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
def modal(self, context, event): def modal(self, context, event):
"""Check status of list update and terminate operator when complete."""
global download_install_status global download_install_status
if self._redraw: if self._redraw:
@@ -88,18 +93,28 @@ class WM_OT_update_index(bpy.types.Operator):
return {'PASS_THROUGH'} return {'PASS_THROUGH'}
def cancel(self, context): def cancel(self, context):
"""Ensure timer and loop are stopped before operator ends."""
self.loop.stop() self.loop.stop()
wm = context.window_manager wm = context.window_manager
wm.event_timer_remove(self._timer) 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 global download_install_status
download_install_status = text download_install_status = text
self._redraw = True self._redraw = True
def update_index(self): def update_index(self):
"""Download index.json and update add-on list."""
self.status("Downloading update") self.status("Downloading update")
index_file = self.download_index() index_file = self.download_index()
@@ -116,7 +131,10 @@ class WM_OT_update_index(bpy.types.Operator):
self.status("Update failed") self.status("Update failed")
def download_index(self): 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: try:
req = urllib.request.urlopen(INDEX_DOWNLOAD_URL) req = urllib.request.urlopen(INDEX_DOWNLOAD_URL)
index_file = req.read().decode('utf-8') index_file = req.read().decode('utf-8')
@@ -130,7 +148,14 @@ class WM_OT_update_index(bpy.types.Operator):
return index_file 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 # Parse downloaded file
try: try:
addon_list = json.loads(index_file) addon_list = json.loads(index_file)
@@ -165,43 +190,32 @@ class WM_OT_update_index(bpy.types.Operator):
return True 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.name = content["name"]
addon.blender = '.'.join(map(str, content["blender"])) addon.blender = '.'.join(map(str, content["blender"]))
addon.module_name = module_name addon.module_name = module_name
addon.download_url = content["download_url"] addon.download_url = content["download_url"]
if "author" in content: optional_keys = ["author", "category", "description", "location",
addon.author = content["author"] "source", "support", "tracker_url", "warning", "wiki_url"]
if "category" in content: for key in optional_keys:
addon.category = content["category"] if key in content:
addon[key] = content[key]
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"]
if "version" in content: if "version" in content:
# TODO: add multi-version functionality # TODO: add multi-version functionality
addon.version = '.'.join(map(str, content["version"])) 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): class WM_OT_addon_download_install(bpy.types.Operator):
"""Download and install add-on""" """Download and install add-on"""
@@ -215,12 +229,16 @@ class WM_OT_addon_download_install(bpy.types.Operator):
@classmethod @classmethod
def poll(self, context): def poll(self, context):
"""Run operator only if an asynchronous download is not in progress."""
global download_install_status global download_install_status
return (not download_install_status return (not download_install_status
or download_install_status == "Install successful" or download_install_status == "Install successful"
or "failed" in download_install_status) or "failed" in download_install_status)
def __init__ (self): def __init__ (self):
"""Init some variables and ensure proper states on run."""
global download_install_status global download_install_status
download_install_status = "" download_install_status = ""
@@ -230,6 +248,10 @@ class WM_OT_addon_download_install(bpy.types.Operator):
self.loop.stop() self.loop.stop()
def execute(self, context): def execute(self, context):
"""Perform verification on selected addon, then begin asynchronous
execution and modal timer.
"""
if self.addon is None: if self.addon is None:
return {'CANCELLED'} return {'CANCELLED'}
@@ -262,6 +284,8 @@ class WM_OT_addon_download_install(bpy.types.Operator):
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
def modal(self, context, event): def modal(self, context, event):
"""Check status of download/install, and exit operator when complete."""
global download_install_status global download_install_status
if self._redraw: if self._redraw:
@@ -279,21 +303,38 @@ class WM_OT_addon_download_install(bpy.types.Operator):
return {'PASS_THROUGH'} return {'PASS_THROUGH'}
def cancel(self, context): def cancel(self, context):
"""Ensure timer and loop are stopped before operator ends."""
self.loop.stop() self.loop.stop()
wm = context.window_manager wm = context.window_manager
wm.event_timer_remove(self._timer) 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 global download_install_status
download_install_status = text download_install_status = text
self._redraw = True self._redraw = True
def download_and_install (self, addon, download_url, filetype): def download_and_install (self, addon: str, download_url: str,
# Perform download; if successful, install 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") self.status("Downloading")
if self.download(addon, download_url): if self.download(download_url):
self.status("Installing") self.status("Installing")
if self.install(addon, filetype): if self.install(addon, filetype):
self.status("Install successful") self.status("Install successful")
@@ -304,11 +345,19 @@ class WM_OT_addon_download_install(bpy.types.Operator):
self.status("Download failed") self.status("Download failed")
self._redraw = True 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] filetype = os.path.splitext(download_url)[1]
download_path = bpy.utils.user_resource('SCRIPTS', path="addons/package_manager/" download_path = bpy.utils.user_resource('SCRIPTS', path="addons/"
"download%s" % filetype) "package_manager/download%s"
% filetype)
# Download add-on and save to disk # Download add-on and save to disk
try: try:
@@ -317,7 +366,8 @@ class WM_OT_addon_download_install(bpy.types.Operator):
shutil.copyfileobj(req, download_file) shutil.copyfileobj(req, download_file)
req.close() req.close()
except urllib.error.HTTPError as err: 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 return False
except urllib.error.URLError as err: except urllib.error.URLError as err:
log.warning("Download failed with URLError: %s", err) log.warning("Download failed with URLError: %s", err)
@@ -325,32 +375,49 @@ class WM_OT_addon_download_install(bpy.types.Operator):
return True 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 "") filename = addon + (filetype if filetype == ".py" else "")
download_path = bpy.utils.user_resource('SCRIPTS', path="addons/package_manager/" download_path = bpy.utils.user_resource('SCRIPTS', path="addons/"
"download%s" % filetype) "package_manager/download%s"
% filetype)
addon_path = bpy.utils.user_resource('SCRIPTS', path="addons/%s" % filename) addon_path = bpy.utils.user_resource('SCRIPTS', path="addons/%s" % filename)
# Copy downloaded add-on to USER scripts path # Copy downloaded add-on to USER scripts path
if filetype == ".py": try:
shutil.move(download_path, addon_path) if filetype == ".py":
elif filetype == ".zip": shutil.move(download_path, addon_path)
# Remove existing add-on elif filetype == ".zip":
if os.path.exists(addon_path): # Remove existing add-on
shutil.rmtree(addon_path) if os.path.exists(addon_path):
with zipfile.ZipFile(download_path,"r") as zipped_addon: shutil.rmtree(addon_path)
zipped_addon.extractall(bpy.utils.user_resource('SCRIPTS', path="addons")) with zipfile.ZipFile(download_path,"r") as zipped_addon:
else: 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 False
return True return True
def register(): def register():
"""Register operators."""
bpy.utils.register_class(WM_OT_update_index) bpy.utils.register_class(WM_OT_update_index)
bpy.utils.register_class(WM_OT_addon_download_install) bpy.utils.register_class(WM_OT_addon_download_install)
def unregister(): def unregister():
"""Unregister operators."""
bpy.utils.unregister_class(WM_OT_update_index) bpy.utils.unregister_class(WM_OT_update_index)
bpy.utils.unregister_class(WM_OT_addon_download_install) bpy.utils.unregister_class(WM_OT_addon_download_install)