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:
@@ -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()
|
||||||
|
@@ -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)
|
Reference in New Issue
Block a user