From 3b19c16c08e46c6d84cc22b13b5c0a06f3d3c133 Mon Sep 17 00:00:00 2001 From: Jake Dube Date: Sun, 7 May 2017 17:43:59 -0400 Subject: [PATCH 1/3] Simplify logging setup --- __init__.py | 8 ++++---- logging_setup.py | 2 ++ maze_tools.py | 6 ++---- prep_manager.py | 7 ++----- render_kit.py | 5 +---- trees.py | 10 ++++------ txt_img_converter.py | 6 ++---- 7 files changed, 17 insertions(+), 27 deletions(-) diff --git a/__init__.py b/__init__.py index 0f42f20..6320dd2 100644 --- a/__init__.py +++ b/__init__.py @@ -25,7 +25,6 @@ import os import sys import subprocess -import logging from os.path import basename, dirname import bpy @@ -47,6 +46,8 @@ from . import addon_updater_ops from . import bug_reporter +logger = setup_logger(__name__) + bl_info = { "name": "UltiMaze [PRO]", "author": "Jake Dube", @@ -85,7 +86,6 @@ def enum_previews_from_directory(self, context): """EnumProperty callback for building a list of enum items""" addon_prefs = context.user_preferences.addons[get_addon_name()].preferences - logger = logging.getLogger(__name__) logger.debug("beginning to build the enum_previews_from_directory") enum_items = [] @@ -822,7 +822,7 @@ def execute(self, context): "\nin the add-on preferences for UltiMaze to automatically render " "\nout and save a nice preview image.") - logging.getLogger(__name__).warning("There are no available blends linked w/ pngs for importing!") + logger.warning("There are no available blends linked w/ pngs for importing!") return {'CANCELLED'} path = os.path.join(addon_prefs.tiles_path, mg.tiles + ".blend") @@ -844,7 +844,7 @@ class EnableLayerMG(Operator): bl_options = {'REGISTER', 'UNDO'} def execute(self, context): - logging.getLogger(__name__).warning("Enabling the first layer...this shouldn't be a problem, but also not spectacularly awesome either!") + logger.warning("Enabling the first layer...this shouldn't be a problem, but also not spectacularly awesome either!") context.scene.layers[0] = True return {'FINISHED'} diff --git a/logging_setup.py b/logging_setup.py index 2056cec..a0965b3 100644 --- a/logging_setup.py +++ b/logging_setup.py @@ -41,3 +41,5 @@ def setup_logger(name): logger.addHandler(handler) logger.setLevel(logging.DEBUG) logger.propagate = False + + return logging.getLogger(name) diff --git a/maze_tools.py b/maze_tools.py index 6830dfb..e8fd1ca 100644 --- a/maze_tools.py +++ b/maze_tools.py @@ -20,7 +20,6 @@ IN_BLENDER = True import random -import logging if IN_BLENDER: from . import weira @@ -33,7 +32,7 @@ from time import sleep from logging_setup import setup_logger -setup_logger(__name__) +logger = setup_logger(__name__) def round_avg(x1, x2): @@ -229,7 +228,6 @@ def __init__(self, debug, width=10, height=10): self.IN_BLENDER = IN_BLENDER if not width & 1 or not height & 1: - logger = logging.getLogger(__name__) logger.critical("Even maze dimension(s) w={}, h={}! Will likely crash!".format(width, height)) self.debug = debug @@ -306,7 +304,7 @@ def dir_to_ordered_pair(x, y, direction, dist=2): try: return a[direction] except KeyError: - logging.getLogger(__name__).error("Error! Invalid direction!") + logger.error("Error! Invalid direction!") def loop_update(self, sleep_time=0.0): """Updates progress reports.""" diff --git a/prep_manager.py b/prep_manager.py index c0b514c..58f1e89 100644 --- a/prep_manager.py +++ b/prep_manager.py @@ -26,15 +26,13 @@ - always_save: Saves .blend file and referenced images/texts. """ -import logging - import bpy from .logging_setup import setup_logger from .addon_name import get_addon_name -setup_logger(__name__) +logger = setup_logger(__name__) def check_tiles_exist(): @@ -122,7 +120,7 @@ def save_text(text): # write to file with open(text_path, "w") as d: d.write(str(text_as_string)) - logging.getLogger(__name__).debug("Wrote {} to file {}".format(text.name, text.filepath)) + logger.debug("Wrote {} to file {}".format(text.name, text.filepath)) def always_save(): @@ -138,7 +136,6 @@ def always_save(): addon_prefs = bpy.context.user_preferences.addons[get_addon_name()].preferences debug = addon_prefs.debug_mode - logger = logging.getLogger(__name__) # save file if addon_prefs.always_save_prior: diff --git a/render_kit.py b/render_kit.py index 8c9e129..dc232cd 100644 --- a/render_kit.py +++ b/render_kit.py @@ -18,7 +18,6 @@ # ##### END GPL LICENSE BLOCK ##### import os -import logging import bpy from bpy.types import Operator @@ -30,12 +29,11 @@ from .addon_name import get_addon_name -setup_logger(__name__) +logger = setup_logger(__name__) @persistent def render_and_leave(dummy): - logger = logging.getLogger(__name__) # get data that was transferred with open(os.path.join(os.path.dirname(__file__), "tile_renderer_data.txt"), 'r') as f: @@ -109,7 +107,6 @@ def invoke(self, context, event): def execute(self, context): addon_prefs = context.user_preferences.addons[get_addon_name()].preferences - logger = logging.getLogger(__name__) mg = context.scene.mg logger.debug("Rendering:{}".format(self.filename)) diff --git a/trees.py b/trees.py index 4741283..f4c7110 100644 --- a/trees.py +++ b/trees.py @@ -19,14 +19,12 @@ IN_BLENDER = True -import logging - if IN_BLENDER: from .logging_setup import setup_logger else: from logging_setup import setup_logger -setup_logger(__name__) +logger = setup_logger(__name__) class RebelChildError(Exception): @@ -149,7 +147,7 @@ def unparent(self, child): self.nodes[parent]['children'].remove(child) self.nodes[child]['parent'] = None else: - logging.getLogger(__name__).warning("Node {} is a root! Cannot unparent root!".format(child)) + logger.warning("Node {} is a root! Cannot unparent root!".format(child)) def insert_parent(self, parent, child): print("Only a stub") @@ -170,7 +168,7 @@ def child_shift_detach(self, node): self.nodes[parent]['children'].remove(node) self.nodes[node]['parent'] = None else: - logging.getLogger(__name__).info("Node {} is a root already".format(node)) + logger.info("Node {} is a root already".format(node)) def replacement_child_shift_detach(self, node): children = self.nodes[node]['children'] @@ -201,7 +199,7 @@ def prune_leaves(self, iterations): try: self.nodes[parent]['children'].remove(leaf_node) except KeyError: - logging.getLogger(__name__).warning("Removing root from its parent's children list failed: roots don't have parents!") + logger.warning("Removing root from its parent's children list failed: roots don't have parents!") del self.nodes[leaf_node] def prune_roots(self, iterations): diff --git a/txt_img_converter.py b/txt_img_converter.py index fca0d10..4cbcfeb 100644 --- a/txt_img_converter.py +++ b/txt_img_converter.py @@ -29,7 +29,6 @@ str_list_maze - Converts a python maze into a text block convert_list_maze - Convert text maze into a Python list maze """ -import logging import bpy @@ -40,7 +39,7 @@ from .logging_setup import setup_logger from .addon_name import get_addon_name -setup_logger(__name__) +logger = setup_logger(__name__) def write_to_text(text): @@ -143,7 +142,7 @@ def convert_list_maze(): if str_maze[index] == "1": maze.make_path(x, y) except IndexError: - logging.getLogger(__name__).warning("IndexError when trying to access a text file's string for " + logger.warning("IndexError when trying to access a text file's string for " "converting to a list maze..." "index={}, maze.width={}, maze.height={}".format(index, maze.width, @@ -160,7 +159,6 @@ class ConvertMazeImageMG(bpy.types.Operator): def execute(self, context): mg = context.scene.mg - logger = logging.getLogger(__name__) # check if image is assigned if not mg.maze_image: From 58f86fb72ad3598f2962e2ad53ce8fc89908de6a Mon Sep 17 00:00:00 2001 From: Jake Dube Date: Sat, 8 Jul 2017 22:18:14 -0400 Subject: [PATCH 2/3] Update third party modules --- addon_updater.py | 1670 +++++++++++++++++++++++++++++++++++------- addon_updater_ops.py | 352 ++++++--- 2 files changed, 1691 insertions(+), 331 deletions(-) diff --git a/addon_updater.py b/addon_updater.py index 3e9afa7..64f9d00 100755 --- a/addon_updater.py +++ b/addon_updater.py @@ -20,7 +20,1128 @@ """ See documentation for usage https://github.com/CGCookie/blender-addon-updater +""" + +import urllib.request +import urllib +import os +import json +import zipfile +import shutil +import asyncio # for async processing +import threading +import time +from datetime import datetime, timedelta + +# blender imports, used in limited cases +import bpy +import addon_utils + +# ----------------------------------------------------------------------------- +# Define error messages/notices & hard coded globals +# ----------------------------------------------------------------------------- + +DEFAULT_API_URL = "https://api.github.com" # plausibly could be some other system +DEFAULT_TIMEOUT = 10 +DEFAULT_PER_PAGE = 30 + + +# ----------------------------------------------------------------------------- +# The main class +# ----------------------------------------------------------------------------- + +class Singleton_updater(object): + """ + This is the singleton class to reference a copy from, + it is the shared module level class + """ + + def __init__(self): + """ + #UPDATE + :param user: string # name of the user owning the repository + :param repo: string # name of the repository + :param api_url: string # should just be the github api link + :param timeout: integer # request timeout + :param current_version: tuple # typically 3 values meaning the version # + """ + + self._user = None + self._repo = None + self._website = None + self._api_url = DEFAULT_API_URL + self._current_version = None + self._tags = [] + self._tag_latest = None + self._tag_names = [] + self._latest_release = None + self._include_branches = False + self._include_branch_list = ['master'] + self._include_branch_autocheck = False + self._manual_only = False + self._version_min_update = None + self._version_max_update = None + + # by default, backup current addon if new is being loaded + self._backup_current = True + + # by default, enable/disable the addon.. but less safe. + self._auto_reload_post_update = False + + self._check_interval_enable = False + self._check_interval_months = 0 + self._check_interval_days = 7 + self._check_interval_hours = 0 + self._check_interval_minutes = 0 + + # runtime variables, initial conditions + self._verbose = False + self._fake_install = False + self._async_checking = False # only true when async daemon started + self._update_ready = None + self._update_link = None + self._update_version = None + self._source_zip = None + self._check_thread = None + self._skip_tag = None + + # get from module data + self._addon = __package__.lower() + self._addon_package = __package__ # must not change + self._updater_path = os.path.join(os.path.dirname(__file__), + self._addon + "_updater") + self._addon_root = os.path.dirname(__file__) + self._json = {} + self._error = None + self._error_msg = None + self._prefiltered_tag_count = 0 + + # to verify a valid import, in place of placeholder import + self.invalidupdater = False + + # ------------------------------------------------------------------------- + # Getters and setters + # ------------------------------------------------------------------------- + + @property + def addon(self): + return self._addon + + @addon.setter + def addon(self, value): + self._addon = str(value) + + @property + def verbose(self): + return self._verbose + + @verbose.setter + def verbose(self, value): + try: + self._verbose = bool(value) + if self._verbose == True: + print(self._addon + " updater verbose is enabled") + except: + raise ValueError("Verbose must be a boolean value") + + @property + def include_branches(self): + return self._include_branches + + @include_branches.setter + def include_branches(self, value): + try: + self._include_branches = bool(value) + except: + raise ValueError("include_branches must be a boolean value") + + @property + def include_branch_list(self): + return self._include_branch_list + + @include_branch_list.setter + def include_branch_list(self, value): + try: + if value == None: + self._include_branch_list = ['master'] + elif type(value) != type(['master']): + raise ValueError("include_branch_list should be a list of valid branches") + else: + self._include_branch_list = value + except: + raise ValueError("include_branch_list should be a list of valid branches") + + # not currently used + @property + def include_branch_autocheck(self): + return self._include_branch_autocheck + + @include_branch_autocheck.setter + def include_branch_autocheck(self, value): + try: + self._include_branch_autocheck = bool(value) + except: + raise ValueError("include_branch_autocheck must be a boolean value") + + @property + def manual_only(self): + return self._manual_only + + @manual_only.setter + def manual_only(self, value): + try: + self._manual_only = bool(value) + except: + raise ValueError("manual_only must be a boolean value") + + @property + def auto_reload_post_update(self): + return self._auto_reload_post_update + + @auto_reload_post_update.setter + def auto_reload_post_update(self, value): + try: + self._auto_reload_post_update = bool(value) + except: + raise ValueError("Must be a boolean value") + + @property + def fake_install(self): + return self._fake_install + + @fake_install.setter + def fake_install(self, value): + if type(value) != type(False): + raise ValueError("fake_install must be a boolean value") + self._fake_install = bool(value) + + @property + def user(self): + return self._user + + @user.setter + def user(self, value): + try: + self._user = str(value) + except: + raise ValueError("User must be a string value") + + @property + def json(self): + if self._json == {}: + self.set_updater_json() + return self._json + + @property + def repo(self): + return self._repo + + @repo.setter + def repo(self, value): + try: + self._repo = str(value) + except: + raise ValueError("User must be a string") + + @property + def website(self): + return self._website + + @website.setter + def website(self, value): + if self.check_is_url(value) == False: + raise ValueError("Not a valid URL: " + value) + self._website = value + + @property + def async_checking(self): + return self._async_checking + + @property + def api_url(self): + return self._api_url + + @api_url.setter + def api_url(self, value): + if self.check_is_url(value) == False: + raise ValueError("Not a valid URL: " + value) + self._api_url = value + + @property + def stage_path(self): + return self._updater_path + + @stage_path.setter + def stage_path(self, value): + if value == None: + if self._verbose: print("Aborting assigning stage_path, it's null") + return + elif value != None and not os.path.exists(value): + try: + os.makedirs(value) + except: + if self._verbose: print("Error trying to staging path") + return + self._updater_path = value + + @property + def tags(self): + if self._tags == []: + return [] + tag_names = [] + for tag in self._tags: + tag_names.append(tag["name"]) + + return tag_names + + @property + def tag_latest(self): + if self._tag_latest == None: + return None + return self._tag_latest["name"] + + @property + def latest_release(self): + if self._releases_latest == None: + return None + return self._latest_release + + @property + def current_version(self): + return self._current_version + + @property + def update_ready(self): + return self._update_ready + + @property + def update_version(self): + return self._update_version + + @property + def update_link(self): + return self._update_link + + @current_version.setter + def current_version(self, tuple_values): + if type(tuple_values) is not tuple: + raise ValueError( \ + "Not a tuple! current_version must be a tuple of integers") + for i in tuple_values: + if type(i) is not int: + raise ValueError( \ + "Not an integer! current_version must be a tuple of integers") + self._current_version = tuple_values + + def set_check_interval(self, enable=False, months=0, days=14, hours=0, minutes=0): + # enabled = False, default initially will not check against frequency + # if enabled, default is then 2 weeks + + if type(enable) is not bool: + raise ValueError("Enable must be a boolean value") + if type(months) is not int: + raise ValueError("Months must be an integer value") + if type(days) is not int: + raise ValueError("Days must be an integer value") + if type(hours) is not int: + raise ValueError("Hours must be an integer value") + if type(minutes) is not int: + raise ValueError("Minutes must be an integer value") + + if enable == False: + self._check_interval_enable = False + else: + self._check_interval_enable = True + + self._check_interval_months = months + self._check_interval_days = days + self._check_interval_hours = hours + self._check_interval_minutes = minutes + + @property + def check_interval(self): + return (self._check_interval_enable, + self._check_interval_months, + self._check_interval_days, + self._check_interval_hours, + self._check_interval_minutes) + + @property + def error(self): + return self._error + + @property + def error_msg(self): + return self._error_msg + + @property + def version_min_update(self): + return self._version_min_update + + @version_min_update.setter + def version_min_update(self, value): + if value == None: + self._version_min_update = None + return + if type(value) != type((1, 2, 3)): + raise ValueError("Version minimum must be a tuple") + else: + # potentially check entries are integers + self._version_min_update = value + + @property + def version_max_update(self): + return self._version_max_update + + @version_max_update.setter + def version_max_update(self, value): + if value == None: + self._version_max_update = None + return + if type(value) != type((1, 2, 3)): + raise ValueError("Version maximum must be a tuple") + else: + # potentially check entries are integers + self._version_max_update = value + + # ------------------------------------------------------------------------- + # Parameter validation related functions + # ------------------------------------------------------------------------- + + + def check_is_url(self, url): + if not ("http://" in url or "https://" in url): + return False + if "." not in url: + return False + return True + + def get_tag_names(self): + tag_names = [] + self.get_tags(self) + for tag in self._tags: + tag_names.append(tag["name"]) + return tag_names + + # declare how the class gets printed + + def __repr__(self): + return "".format(a=__file__) + + def __str__(self): + return "Updater, with user: {a}, repository: {b}, url: {c}".format( + a=self._user, + b=self._repo, c=self.form_repo_url()) + + # ------------------------------------------------------------------------- + # API-related functions + # ------------------------------------------------------------------------- + + def form_repo_url(self): + return self._api_url + "/repos/" + self.user + "/" + self.repo + + def get_tags(self): + request = "/repos/" + self.user + "/" + self.repo + "/tags" + if self.verbose: print("Getting tags from server") + + # get all tags, internet call + all_tags = self.get_api(request) + self._prefiltered_tag_count = len(all_tags) + + # pre-process to skip tags + if self.skip_tag != None: + self._tags = [tg for tg in all_tags if self.skip_tag(tg) == False] + else: + self._tags = all_tags + + # get additional branches too, if needed, and place in front + # does NO checking here whether branch is valid + if self._include_branches == True: + temp_branches = self._include_branch_list.copy() + temp_branches.reverse() + for branch in temp_branches: + request = self._api_url + "/repos/" \ + + self.user + "/" + self.repo + "/zipball/" + branch + include = { + "name": branch.title(), + "zipball_url": request + } + self._tags = [include] + self._tags # append to front + + if self._tags == None: + # some error occurred + self._tag_latest = None + self._tags = [] + return + elif self._prefiltered_tag_count == 0 and self._include_branches == False: + self._tag_latest = None + self._error = "No releases found" + self._error_msg = "No releases or tags found on this repository" + if self.verbose: print("No releases or tags found on this repository") + elif self._prefiltered_tag_count == 0 and self._include_branches == True: + self._tag_latest = self._tags[0] + if self.verbose: + branch = self._include_branch_list[0] + print("{} branch found, no releases".format(branch), self._tags[0]) + elif len(self._tags) == 0 and self._prefiltered_tag_count > 0: + self._tag_latest = None + self._error = "No releases available" + self._error_msg = "No versions found within compatible version range" + if self.verbose: print("No versions found within compatible version range") + else: + if self._include_branches == False: + self._tag_latest = self._tags[0] + if self.verbose: print("Most recent tag found:", self._tags[0]) + else: + # don't return branch if in list + n = len(self._include_branch_list) + self._tag_latest = self._tags[n] # guarenteed at least len()=n+1 + if self.verbose: print("Most recent tag found:", self._tags[n]) + + # all API calls to base url + def get_api_raw(self, url): + request = urllib.request.Request(self._api_url + url) + try: + result = urllib.request.urlopen(request) + except urllib.error.HTTPError as e: + self._error = "HTTP error" + self._error_msg = str(e.code) + self._update_ready = None + except urllib.error.URLError as e: + self._error = "URL error, check internet connection" + self._error_msg = str(e.reason) + self._update_ready = None + return None + else: + result_string = result.read() + result.close() + return result_string.decode() + # if we didn't get here, return or raise something else + + # result of all api calls, decoded into json format + def get_api(self, url): + # return the json version + get = None + get = self.get_api_raw(url) # this can fail by self-created error raising + if get != None: + return json.JSONDecoder().decode(get) + else: + return None + + # create a working directory and download the new files + def stage_repository(self, url): + + # first make/clear the staging folder + # ensure the folder is always "clean" + local = os.path.join(self._updater_path, "update_staging") + error = None + + if self._verbose: print("Preparing staging folder for download:\n", local) + if os.path.isdir(local) == True: + try: + shutil.rmtree(local) + os.makedirs(local) + except: + error = "failed to remove existing staging directory" + else: + try: + os.makedirs(local) + except: + error = "failed to make staging directory" + + if error != None: + if self._verbose: print("Error: Aborting update, " + error) + raise ValueError("Aborting update, " + error) + + if self._backup_current == True: + self.create_backup() + if self._verbose: print("Now retrieving the new source zip") + + self._source_zip = os.path.join(local, "source.zip") + + if self._verbose: print("Starting download update zip") + try: + urllib.request.urlretrieve(url, self._source_zip) + except Exception as e: + self._error = "Error retreiving download, bad link?" + self._error_msg = "Error: {}".format(e) + if self._verbose: + print("Error retreiving download, bad link?") + print("Error: {}".format(e)) + return + if self._verbose: print("Successfully downloaded update zip") + + def create_backup(self): + if self._verbose: print("Backing up current addon folder") + local = os.path.join(self._updater_path, "backup") + tempdest = os.path.join(self._addon_root, + os.pardir, + self._addon + "_updater_backup_temp") + + if os.path.isdir(local) == True: + shutil.rmtree(local) + if self._verbose: print("Backup destination path: ", local) + + # make the copy + shutil.copytree(self._addon_root, tempdest) + shutil.move(tempdest, local) + + # save the date for future ref + now = datetime.now() + self._json["backup_date"] = "{m}-{d}-{yr}".format( + m=now.strftime("%B"), d=now.day, yr=now.year) + self.save_updater_json() + + def restore_backup(self): + if self._verbose: print("Restoring backup") + + if self._verbose: print("Backing up current addon folder") + backuploc = os.path.join(self._updater_path, "backup") + tempdest = os.path.join(self._addon_root, + os.pardir, + self._addon + "_updater_backup_temp") + tempdest = os.path.abspath(tempdest) + + # make the copy + shutil.move(backuploc, tempdest) + shutil.rmtree(self._addon_root) + os.rename(tempdest, self._addon_root) + + self._json["backup_date"] = "" + self._json["just_restored"] = True + self._json["just_updated"] = True + self.save_updater_json() + + self.reload_addon() + + def upack_staged_zip(self): + + if os.path.isfile(self._source_zip) == False: + if self._verbose: print("Error, update zip not found") + return -1 + + # clear the existing source folder in case previous files remain + try: + shutil.rmtree(os.path.join(self._updater_path, "source")) + os.makedirs(os.path.join(self._updater_path, "source")) + if self._verbose: print("Source folder cleared and recreated") + except: + pass + + if self.verbose: print("Begin extracting source") + if zipfile.is_zipfile(self._source_zip): + with zipfile.ZipFile(self._source_zip) as zf: + # extractall is no longer a security hazard + zf.extractall(os.path.join(self._updater_path, "source")) + else: + if self._verbose: + print("Not a zip file, future add support for just .py files") + raise ValueError("Resulting file is not a zip") + if self.verbose: print("Extracted source") + + # either directly in root of zip, or one folder level deep + unpath = os.path.join(self._updater_path, "source") + if os.path.isfile(os.path.join(unpath, "__init__.py")) == False: + dirlist = os.listdir(unpath) + if len(dirlist) > 0: + unpath = os.path.join(unpath, dirlist[0]) + + if os.path.isfile(os.path.join(unpath, "__init__.py")) == False: + if self._verbose: + print("not a valid addon found") + print("Paths:") + print(dirlist) + + raise ValueError("__init__ file not found in new source") + + # now commence merging in the two locations: + origpath = os.path.dirname(__file__) # verify, is __file__ always valid? + + self.deepMergeDirectory(origpath, unpath) + + # now save the json state + # Change to True, to trigger the handler on other side + # if allowing reloading within same blender instance + self._json["just_updated"] = True + self.save_updater_json() + self.reload_addon() + self._update_ready = False + + # merge folder 'merger' into folder 'base' without deleting existing + def deepMergeDirectory(self, base, merger): + if not os.path.exists(base): + if self._verbose: print("Base path does not exist") + return -1 + elif not os.path.exists(merger): + if self._verbose: print("Merger path does not exist") + return -1 + + # this should have better error handling + # and also avoid the addon dir + # Could also do error handling outside this function + for path, dirs, files in os.walk(merger): + relPath = os.path.relpath(path, merger) + destPath = os.path.join(base, relPath) + if not os.path.exists(destPath): + os.makedirs(destPath) + for file in files: + destFile = os.path.join(destPath, file) + if os.path.isfile(destFile): + os.remove(destFile) + srcFile = os.path.join(path, file) + os.rename(srcFile, destFile) + + def reload_addon(self): + # if post_update false, skip this function + # else, unload/reload addon & trigger popup + if self._auto_reload_post_update == False: + print("Restart blender to reload addon and complete update") + return + + if self._verbose: print("Reloading addon...") + addon_utils.modules(refresh=True) + bpy.utils.refresh_script_paths() + + # not allowed in restricted context, such as register module + # toggle to refresh + bpy.ops.wm.addon_disable(module=self._addon_package) + bpy.ops.wm.addon_refresh() + bpy.ops.wm.addon_enable(module=self._addon_package) + + # ------------------------------------------------------------------------- + # Other non-api functions and setups + # ------------------------------------------------------------------------- + + def clear_state(self): + self._update_ready = None + self._update_link = None + self._update_version = None + self._source_zip = None + self._error = None + self._error_msg = None + + def version_tuple_from_text(self, text): + + if text == None: return () + + # should go through string and remove all non-integers, + # and for any given break split into a different section + + segments = [] + tmp = '' + for l in str(text): + if l.isdigit() == False: + if len(tmp) > 0: + segments.append(int(tmp)) + tmp = '' + else: + tmp += l + if len(tmp) > 0: + segments.append(int(tmp)) + + if len(segments) == 0: + if self._verbose: print("No version strings found text: ", text) + if self._include_branches == False: + return () + else: + return (text) + return tuple(segments) + + # called for running check in a background thread + def check_for_update_async(self, callback=None): + + if self._json != None and "update_ready" in self._json: + if self._json["update_ready"] == True: + self._update_ready = True + self._update_link = self._json["version_text"]["link"] + self._update_version = str(self._json["version_text"]["version"]) + # cached update + callback(True) + return + + # do the check + if self._check_interval_enable == False: + return + elif self._async_checking == True: + if self._verbose: print("Skipping async check, already started") + return # already running the bg thread + elif self._update_ready == None: + self.start_async_check_update(False, callback) + + def check_for_update_now(self, callback=None): + + self._error = None + self._error_msg = None + + if self._verbose: + print("Check update pressed, first getting current status") + if self._async_checking == True: + if self._verbose: print("Skipping async check, already started") + return # already running the bg thread + elif self._update_ready == None: + self.start_async_check_update(True, callback) + else: + self._update_ready = None + self.start_async_check_update(True, callback) + + # this function is not async, will always return in sequential fashion + # but should have a parent which calls it in another thread + def check_for_update(self, now=False): + if self._verbose: print("Checking for update function") + + # clear the errors if any + self._error = None + self._error_msg = None + + # avoid running again in, just return past result if found + # but if force now check, then still do it + if self._update_ready != None and now == False: + return (self._update_ready, self._update_version, self._update_link) + + if self._current_version == None: + raise ValueError("current_version not yet defined") + if self._repo == None: + raise ValueError("repo not yet defined") + if self._user == None: + raise ValueError("username not yet defined") + + self.set_updater_json() # self._json + + if now == False and self.past_interval_timestamp() == False: + if self.verbose: + print("Aborting check for updated, check interval not reached") + return (False, None, None) + + # check if using tags or releases + # note that if called the first time, this will pull tags from online + if self._fake_install == True: + if self._verbose: + print("fake_install = True, setting fake version as ready") + self._update_ready = True + self._update_version = "(999,999,999)" + self._update_link = "http://127.0.0.1" + + return (self._update_ready, self._update_version, self._update_link) + + # primary internet call + self.get_tags() # sets self._tags and self._tag_latest + + self._json["last_check"] = str(datetime.now()) + self.save_updater_json() + + # can be () or ('master') in addition to branchs, and version tag + new_version = self.version_tuple_from_text(self.tag_latest) + + if len(self._tags) == 0: + self._update_ready = False + self._update_version = None + self._update_link = None + return (False, None, None) + elif self._include_branches == False: + link = self._tags[0]["zipball_url"] # potentially other sources + else: + n = len(self._include_branch_list) + if len(self._tags) == n: + # effectively means no tags found on repo + # so provide the first one as default + link = self._tags[0]["zipball_url"] # potentially other sources + else: + link = self._tags[n]["zipball_url"] # potentially other sources + + if new_version == (): + self._update_ready = False + self._update_version = None + self._update_link = None + return (False, None, None) + elif str(new_version).lower() in self._include_branch_list: + # handle situation where master/whichever branch is included + # however, this code effectively is not triggered now + # as new_version will only be tag names, not branch names + if self._include_branch_autocheck == False: + # don't offer update as ready, + # but set the link for the default + # branch for installing + self._update_ready = True + self._update_version = new_version + self._update_link = link + self.save_updater_json() + return (True, new_version, link) + else: + raise ValueError("include_branch_autocheck: NOT YET DEVELOPED") + # bypass releases and look at timestamp of last update + # from a branch compared to now, see if commit values + # match or not. + + else: + # situation where branches not included + + if new_version > self._current_version: + self._update_ready = True + self._update_version = new_version + self._update_link = link + self.save_updater_json() + return (True, new_version, link) + + # elif new_version != self._current_version: + # self._update_ready = False + # self._update_version = new_version + # self._update_link = link + # self.save_updater_json() + # return (True, new_version, link) + + # if no update, set ready to False from None + self._update_ready = False + self._update_version = None + self._update_link = None + return (False, None, None) + + def set_tag(self, name): + tg = None + for tag in self._tags: + if name == tag["name"]: + tg = tag + break + if tg == None: + raise ValueError("Version tag not found: " + revert_tag) + new_version = self.version_tuple_from_text(self.tag_latest) + self._update_version = new_version + self._update_link = tg["zipball_url"] + + def run_update(self, force=False, revert_tag=None, clean=False, callback=None): + # revert_tag: could e.g. get from drop down list + # different versions of the addon to revert back to + # clean: not used, but in future could use to totally refresh addon + self._json["update_ready"] = False + self._json["ignore"] = False # clear ignore flag + self._json["version_text"] = {} + + if revert_tag != None: + self.set_tag(revert_tag) + self._update_ready = True + + # clear the errors if any + self._error = None + self._error_msg = None + + if self.verbose: print("Running update") + + if self._fake_install == True: + # change to True, to trigger the reload/"update installed" handler + if self._verbose: + print("fake_install=True") + print("Just reloading and running any handler triggers") + self._json["just_updated"] = True + self.save_updater_json() + if self._backup_current == True: + self.create_backup() + self.reload_addon() + self._update_ready = False + + elif force == False: + if self._update_ready != True: + if self.verbose: print("Update stopped, new version not ready") + return 1 # stopped + elif self._update_link == None: + # this shouldn't happen if update is ready + if self.verbose: print("Update stopped, update link unavailable") + return 1 # stopped + + if self.verbose and revert_tag == None: + print("Staging update") + elif self.verbose: + print("Staging install") + self.stage_repository(self._update_link) + self.upack_staged_zip() + + else: + if self._update_link == None: + return # stopped, no link - run check update first or set tag + if self.verbose: print("Forcing update") + # first do a check + if self._update_link == None: + if self.verbose: print("Update stopped, could not get link") + return + self.stage_repository(self._update_link) + self.upack_staged_zip() + # would need to compare against other versions held in tags + + # run the front-end's callback if provided + if callback != None: callback() + + # return something meaningful, 0 means it worked + return 0 + + def past_interval_timestamp(self): + if self._check_interval_enable == False: + return True # ie this exact feature is disabled + + if "last_check" not in self._json or self._json["last_check"] == "": + return True + else: + now = datetime.now() + last_check = datetime.strptime(self._json["last_check"], + "%Y-%m-%d %H:%M:%S.%f") + next_check = last_check + offset = timedelta( + days=self._check_interval_days + 30 * self._check_interval_months, + hours=self._check_interval_hours, + minutes=self._check_interval_minutes + ) + + delta = (now - offset) - last_check + if delta.total_seconds() > 0: + if self._verbose: + print("{} Updater: Time to check for updates!".format(self._addon)) + return True + else: + if self._verbose: + print("{} Updater: Determined it's not yet time to check for updates".format(self._addon)) + return False + + def set_updater_json(self): + if self._updater_path == None: + raise ValueError("updater_path is not defined") + elif os.path.isdir(self._updater_path) == False: + os.makedirs(self._updater_path) + + jpath = os.path.join(self._updater_path, "updater_status.json") + if os.path.isfile(jpath): + with open(jpath) as data_file: + self._json = json.load(data_file) + if self._verbose: print("{} Updater: Read in json settings from file".format(self._addon)) + else: + # set data structure + self._json = { + "last_check": "", + "backup_date": "", + "update_ready": False, + "ignore": False, + "just_restored": False, + "just_updated": False, + "version_text": {} + } + self.save_updater_json() + + def save_updater_json(self): + + # first save the state + if self._update_ready == True: + if type(self._update_version) == type((0, 0, 0)): + self._json["update_ready"] = True + self._json["version_text"]["link"] = self._update_link + self._json["version_text"]["version"] = self._update_version + else: + self._json["update_ready"] = False + self._json["version_text"] = {} + else: + self._json["update_ready"] = False + self._json["version_text"] = {} + + jpath = os.path.join(self._updater_path, "updater_status.json") + outf = open(jpath, 'w') + data_out = json.dumps(self._json, indent=4) + outf.write(data_out) + outf.close() + if self._verbose: + print(self._addon + ": Wrote out updater json settings to file, with the contents:") + print(self._json) + + def json_reset_postupdate(self): + self._json["just_updated"] = False + self._json["update_ready"] = False + self._json["version_text"] = {} + self.save_updater_json() + + def json_reset_restore(self): + self._json["just_restored"] = False + self._json["update_ready"] = False + self._json["version_text"] = {} + self.save_updater_json() + self._update_ready = None # reset so you could check update again + + def ignore_update(self): + self._json["ignore"] = True + self.save_updater_json() + + # ------------------------------------------------------------------------- + # ASYNC stuff + # ------------------------------------------------------------------------- + + def start_async_check_update(self, now=False, callback=None): + if self._async_checking == True: + return + if self._verbose: print("{} updater: Starting background checking thread".format(self._addon)) + check_thread = threading.Thread(target=self.async_check_update, + args=(now, callback,)) + check_thread.daemon = True + self._check_thread = check_thread + check_thread.start() + + return True + + def async_check_update(self, now, callback=None): + self._async_checking = True + if self._verbose: print("{} BG thread: Checking for update now in background".format(self._addon)) + # time.sleep(3) # to test background, in case internet too fast to tell + # try: + self.check_for_update(now=now) + # except Exception as exception: + # print("Checking for update error:") + # print(exception) + # self._update_ready = False + # self._update_version = None + # self._update_link = None + # self._error = "Error occurred" + # self._error_msg = "Encountered an error while checking for updates" + + if self._verbose: + print("{} BG thread: Finished checking for update, doing callback".format(self._addon)) + if callback != None: callback(self._update_ready) + self._async_checking = False + self._check_thread = None + + def stop_async_check_update(self): + if self._check_thread != None: + try: + if self._verbose: print("Thread will end in normal course.") + # however, "There is no direct kill method on a thread object." + # better to let it run its course + # self._check_thread.stop() + except: + pass + self._async_checking = False + self._error = None + self._error_msg = None + + +# ----------------------------------------------------------------------------- +# The module-shared class instance, +# should be what's imported to other files +# ----------------------------------------------------------------------------- + +Updater = Singleton_updater() +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +""" +See documentation for usage +https://github.com/CGCookie/blender-addon-updater """ import urllib.request @@ -29,10 +1150,10 @@ import json import zipfile import shutil -import asyncio # for async processing +import asyncio # for async processing import threading import time -from datetime import datetime,timedelta +from datetime import datetime, timedelta # blender imports, used in limited cases import bpy @@ -42,7 +1163,7 @@ # Define error messages/notices & hard coded globals # ----------------------------------------------------------------------------- -DEFAULT_API_URL = "https://api.github.com" # plausibly could be some other system +DEFAULT_API_URL = "https://api.github.com" # plausibly could be some other system DEFAULT_TIMEOUT = 10 DEFAULT_PER_PAGE = 30 @@ -53,10 +1174,10 @@ class Singleton_updater(object): """ - This is the singleton class to reference a copy from, + This is the singleton class to reference a copy from, it is the shared module level class """ - + def __init__(self): """ #UPDATE @@ -76,13 +1197,15 @@ def __init__(self): self._tag_latest = None self._tag_names = [] self._latest_release = None - self._include_master = False + self._include_branches = False + self._include_branch_list = ['master'] + self._include_branch_autocheck = False self._manual_only = False self._version_min_update = None self._version_max_update = None # by default, backup current addon if new is being loaded - self._backup_current = True + self._backup_current = True # by default, enable/disable the addon.. but less safe. self._auto_reload_post_update = False @@ -96,7 +1219,7 @@ def __init__(self): # runtime variables, initial conditions self._verbose = False self._fake_install = False - self._async_checking = False # only true when async daemon started + self._async_checking = False # only true when async daemon started self._update_ready = None self._update_link = None self._update_version = None @@ -106,15 +1229,17 @@ def __init__(self): # get from module data self._addon = __package__.lower() - self._addon_package = __package__ # must not change + self._addon_package = __package__ # must not change self._updater_path = os.path.join(os.path.dirname(__file__), - self._addon+"_updater") + self._addon + "_updater") self._addon_root = os.path.dirname(__file__) self._json = {} self._error = None self._error_msg = None self._prefiltered_tag_count = 0 + # to verify a valid import, in place of placeholder import + self.invalidupdater = False # ------------------------------------------------------------------------- # Getters and setters @@ -123,6 +1248,7 @@ def __init__(self): @property def addon(self): return self._addon + @addon.setter def addon(self, value): self._addon = str(value) @@ -130,28 +1256,59 @@ def addon(self, value): @property def verbose(self): return self._verbose + @verbose.setter def verbose(self, value): try: self._verbose = bool(value) if self._verbose == True: - print(self._addon+" updater verbose is enabled") + print(self._addon + " updater verbose is enabled") except: raise ValueError("Verbose must be a boolean value") @property - def include_master(self): - return self._include_master - @include_master.setter - def include_master(self, value): + def include_branches(self): + return self._include_branches + + @include_branches.setter + def include_branches(self, value): + try: + self._include_branches = bool(value) + except: + raise ValueError("include_branches must be a boolean value") + + @property + def include_branch_list(self): + return self._include_branch_list + + @include_branch_list.setter + def include_branch_list(self, value): + try: + if value == None: + self._include_branch_list = ['master'] + elif type(value) != type(['master']): + raise ValueError("include_branch_list should be a list of valid branches") + else: + self._include_branch_list = value + except: + raise ValueError("include_branch_list should be a list of valid branches") + + # not currently used + @property + def include_branch_autocheck(self): + return self._include_branch_autocheck + + @include_branch_autocheck.setter + def include_branch_autocheck(self, value): try: - self._include_master = bool(value) + self._include_branch_autocheck = bool(value) except: - raise ValueError("include_master must be a boolean value") + raise ValueError("include_branch_autocheck must be a boolean value") @property def manual_only(self): return self._manual_only + @manual_only.setter def manual_only(self, value): try: @@ -162,6 +1319,7 @@ def manual_only(self, value): @property def auto_reload_post_update(self): return self._auto_reload_post_update + @auto_reload_post_update.setter def auto_reload_post_update(self, value): try: @@ -171,16 +1329,18 @@ def auto_reload_post_update(self, value): @property def fake_install(self): - return self._verbose + return self._fake_install + @fake_install.setter def fake_install(self, value): if type(value) != type(False): - raise ValueError("Verbose must be a boolean value") + raise ValueError("fake_install must be a boolean value") self._fake_install = bool(value) - + @property def user(self): return self._user + @user.setter def user(self, value): try: @@ -197,6 +1357,7 @@ def json(self): @property def repo(self): return self._repo + @repo.setter def repo(self, value): try: @@ -207,6 +1368,7 @@ def repo(self, value): @property def website(self): return self._website + @website.setter def website(self, value): if self.check_is_url(value) == False: @@ -220,6 +1382,7 @@ def async_checking(self): @property def api_url(self): return self._api_url + @api_url.setter def api_url(self, value): if self.check_is_url(value) == False: @@ -229,21 +1392,20 @@ def api_url(self, value): @property def stage_path(self): return self._updater_path + @stage_path.setter def stage_path(self, value): if value == None: - if self._verbose:print("Aborting assigning stage_path, it's null") + if self._verbose: print("Aborting assigning stage_path, it's null") return elif value != None and not os.path.exists(value): try: os.makedirs(value) except: - if self._verbose:print("Error trying to staging path") + if self._verbose: print("Error trying to staging path") return - # definitely check for errors here, user issues self._updater_path = value - @property def tags(self): if self._tags == []: @@ -283,17 +1445,17 @@ def update_link(self): return self._update_link @current_version.setter - def current_version(self,tuple_values): + def current_version(self, tuple_values): if type(tuple_values) is not tuple: - raise ValueError(\ - "Not a tuple! current_version must be a tuple of integers") + raise ValueError( \ + "Not a tuple! current_version must be a tuple of integers") for i in tuple_values: if type(i) is not int: - raise ValueError(\ - "Not an integer! current_version must be a tuple of integers") + raise ValueError( \ + "Not an integer! current_version must be a tuple of integers") self._current_version = tuple_values - def set_check_interval(self,enable=False,months=0,days=14,hours=0,minutes=0): + def set_check_interval(self, enable=False, months=0, days=14, hours=0, minutes=0): # enabled = False, default initially will not check against frequency # if enabled, default is then 2 weeks @@ -308,11 +1470,11 @@ def set_check_interval(self,enable=False,months=0,days=14,hours=0,minutes=0): if type(minutes) is not int: raise ValueError("Minutes must be an integer value") - if enable==False: + if enable == False: self._check_interval_enable = False else: self._check_interval_enable = True - + self._check_interval_months = months self._check_interval_days = days self._check_interval_hours = hours @@ -337,40 +1499,39 @@ def error_msg(self): @property def version_min_update(self): return self._version_min_update + @version_min_update.setter def version_min_update(self, value): if value == None: self._version_min_update = None return - if type(value) != type((1,2,3)): + if type(value) != type((1, 2, 3)): raise ValueError("Version minimum must be a tuple") else: # potentially check entries are integers self._version_min_update = value - @property def version_max_update(self): return self._version_max_update + @version_max_update.setter def version_max_update(self, value): if value == None: self._version_max_update = None return - if type(value) != type((1,2,3)): + if type(value) != type((1, 2, 3)): raise ValueError("Version maximum must be a tuple") else: # potentially check entries are integers self._version_max_update = value - - # ------------------------------------------------------------------------- # Parameter validation related functions # ------------------------------------------------------------------------- - def check_is_url(self,url): + def check_is_url(self, url): if not ("http://" in url or "https://" in url): return False if "." not in url: @@ -391,21 +1552,19 @@ def __repr__(self): def __str__(self): return "Updater, with user: {a}, repository: {b}, url: {c}".format( - a=self._user, - b=self._repo, c=self.form_repo_url()) - + a=self._user, + b=self._repo, c=self.form_repo_url()) # ------------------------------------------------------------------------- # API-related functions # ------------------------------------------------------------------------- def form_repo_url(self): - return self._api_url+"/repos/"+self.user+"/"+self.repo - + return self._api_url + "/repos/" + self.user + "/" + self.repo def get_tags(self): - request = "/repos/"+self.user+"/"+self.repo+"/tags" - if self.verbose:print("Getting tags from server") + request = "/repos/" + self.user + "/" + self.repo + "/tags" + if self.verbose: print("Getting tags from server") # get all tags, internet call all_tags = self.get_api(request) @@ -413,42 +1572,53 @@ def get_tags(self): # pre-process to skip tags if self.skip_tag != None: - self._tags = [tg for tg in all_tags if self.skip_tag(tg)==False] + self._tags = [tg for tg in all_tags if self.skip_tag(tg) == False] else: self._tags = all_tags - # get master too, if needed, and place in front but not actively - if self._include_master == True: - request = self._api_url +"/repos/" \ - +self.user+"/"+self.repo+"/zipball/master" - master = { - "name":"Master", - "zipball_url":request - } - self._tags = [master] + self._tags # append to front + # get additional branches too, if needed, and place in front + # does NO checking here whether branch is valid + if self._include_branches == True: + temp_branches = self._include_branch_list.copy() + temp_branches.reverse() + for branch in temp_branches: + request = self._api_url + "/repos/" \ + + self.user + "/" + self.repo + "/zipball/" + branch + include = { + "name": branch.title(), + "zipball_url": request + } + self._tags = [include] + self._tags # append to front if self._tags == None: - # some error occured + # some error occurred self._tag_latest = None self._tags = [] return - elif self._prefiltered_tag_count == 0 and self._include_master == False: + elif self._prefiltered_tag_count == 0 and self._include_branches == False: self._tag_latest = None self._error = "No releases found" self._error_msg = "No releases or tags found on this repository" - if self.verbose:print("No releases or tags found on this repository") - elif self._prefiltered_tag_count == 0 and self._include_master == True: + if self.verbose: print("No releases or tags found on this repository") + elif self._prefiltered_tag_count == 0 and self._include_branches == True: self._tag_latest = self._tags[0] - if self.verbose:print("Only master branch found:",self._tags[0]) + if self.verbose: + branch = self._include_branch_list[0] + print("{} branch found, no releases".format(branch), self._tags[0]) elif len(self._tags) == 0 and self._prefiltered_tag_count > 0: self._tag_latest = None self._error = "No releases available" self._error_msg = "No versions found within compatible version range" - if self.verbose:print("No versions found within compatible version range") + if self.verbose: print("No versions found within compatible version range") else: - self._tag_latest = self._tags[0] - if self.verbose:print("Most recent tag found:",self._tags[0]) - + if self._include_branches == False: + self._tag_latest = self._tags[0] + if self.verbose: print("Most recent tag found:", self._tags[0]) + else: + # don't return branch if in list + n = len(self._include_branch_list) + self._tag_latest = self._tags[n] # guarenteed at least len()=n+1 + if self.verbose: print("Most recent tag found:", self._tags[n]) # all API calls to base url def get_api_raw(self, url): @@ -457,46 +1627,41 @@ def get_api_raw(self, url): result = urllib.request.urlopen(request) except urllib.error.HTTPError as e: self._error = "HTTP error" - if str(e.code) == '404': - self._error_msg = "404 - repository not found, verify register settings" - else: - self._error_msg = "Response: "+str(e.code) - self._update_ready = None + self._error_msg = str(e.code) + self._update_ready = None except urllib.error.URLError as e: self._error = "URL error, check internet connection" self._error_msg = str(e.reason) - self._update_ready = None + self._update_ready = None return None else: result_string = result.read() result.close() return result_string.decode() # if we didn't get here, return or raise something else - - + # result of all api calls, decoded into json format def get_api(self, url): # return the json version get = None - get = self.get_api_raw(url) # this can fail by self-created error raising + get = self.get_api_raw(url) # this can fail by self-created error raising if get != None: - return json.JSONDecoder().decode( get ) + return json.JSONDecoder().decode(get) else: return None - # create a working directory and download the new files def stage_repository(self, url): # first make/clear the staging folder # ensure the folder is always "clean" - local = os.path.join(self._updater_path,"update_staging") + local = os.path.join(self._updater_path, "update_staging") error = None - if self._verbose:print("Preparing staging folder for download:\n",local) + if self._verbose: print("Preparing staging folder for download:\n", local) if os.path.isdir(local) == True: try: - shutil.rmtree(local) + shutil.rmtree(local) os.makedirs(local) except: error = "failed to remove existing staging directory" @@ -505,111 +1670,117 @@ def stage_repository(self, url): os.makedirs(local) except: error = "failed to make staging directory" - + if error != None: - if self._verbose: print("Error: Aborting update, "+error) - raise ValueError("Aborting update, "+error) + if self._verbose: print("Error: Aborting update, " + error) + raise ValueError("Aborting update, " + error) - if self._backup_current==True: + if self._backup_current == True: self.create_backup() - if self._verbose:print("Now retreiving the new source zip") + if self._verbose: print("Now retrieving the new source zip") + + self._source_zip = os.path.join(local, "source.zip") - self._source_zip = os.path.join(local,"source.zip") - - if self._verbose:print("Starting download update zip") - urllib.request.urlretrieve(url, self._source_zip) - if self._verbose:print("Successfully downloaded update zip") + if self._verbose: print("Starting download update zip") + try: + urllib.request.urlretrieve(url, self._source_zip) + except Exception as e: + self._error = "Error retreiving download, bad link?" + self._error_msg = "Error: {}".format(e) + if self._verbose: + print("Error retreiving download, bad link?") + print("Error: {}".format(e)) + return + if self._verbose: print("Successfully downloaded update zip") def create_backup(self): - if self._verbose:print("Backing up current addon folder") - local = os.path.join(self._updater_path,"backup") + if self._verbose: print("Backing up current addon folder") + local = os.path.join(self._updater_path, "backup") tempdest = os.path.join(self._addon_root, - os.pardir, - self._addon+"_updater_backup_temp") + os.pardir, + self._addon + "_updater_backup_temp") if os.path.isdir(local) == True: shutil.rmtree(local) - if self._verbose:print("Backup destination path: ",local) + if self._verbose: print("Backup destination path: ", local) # make the copy - shutil.copytree(self._addon_root,tempdest) - shutil.move(tempdest,local) + shutil.copytree(self._addon_root, tempdest) + shutil.move(tempdest, local) # save the date for future ref now = datetime.now() self._json["backup_date"] = "{m}-{d}-{yr}".format( - m=now.strftime("%B"),d=now.day,yr=now.year) + m=now.strftime("%B"), d=now.day, yr=now.year) self.save_updater_json() def restore_backup(self): - if self._verbose:print("Restoring backup") + if self._verbose: print("Restoring backup") - if self._verbose:print("Backing up current addon folder") - backuploc = os.path.join(self._updater_path,"backup") + if self._verbose: print("Backing up current addon folder") + backuploc = os.path.join(self._updater_path, "backup") tempdest = os.path.join(self._addon_root, - os.pardir, - self._addon+"_updater_backup_temp") + os.pardir, + self._addon + "_updater_backup_temp") tempdest = os.path.abspath(tempdest) # make the copy - shutil.move(backuploc,tempdest) + shutil.move(backuploc, tempdest) shutil.rmtree(self._addon_root) - os.rename(tempdest,self._addon_root) + os.rename(tempdest, self._addon_root) self._json["backup_date"] = "" self._json["just_restored"] = True self._json["just_updated"] = True - self.save_updater_json() + self.save_updater_json() self.reload_addon() def upack_staged_zip(self): if os.path.isfile(self._source_zip) == False: - if self._verbose:print("Error, update zip not found") + if self._verbose: print("Error, update zip not found") return -1 # clear the existing source folder in case previous files remain try: - shutil.rmtree( os.path.join(self._updater_path,"source") ) - os.makedirs( os.path.join(self._updater_path,"source") ) - if self._verbose:print("Source folder cleared and recreated") + shutil.rmtree(os.path.join(self._updater_path, "source")) + os.makedirs(os.path.join(self._updater_path, "source")) + if self._verbose: print("Source folder cleared and recreated") except: pass - - if self.verbose:print("Begin extracting source") + if self.verbose: print("Begin extracting source") if zipfile.is_zipfile(self._source_zip): with zipfile.ZipFile(self._source_zip) as zf: # extractall is no longer a security hazard - zf.extractall(os.path.join(self._updater_path,"source")) + zf.extractall(os.path.join(self._updater_path, "source")) else: if self._verbose: print("Not a zip file, future add support for just .py files") raise ValueError("Resulting file is not a zip") - if self.verbose:print("Extracted source") + if self.verbose: print("Extracted source") # either directly in root of zip, or one folder level deep - unpath = os.path.join(self._updater_path,"source") - if os.path.isfile(os.path.join(unpath,"__init__.py")) == False: + unpath = os.path.join(self._updater_path, "source") + if os.path.isfile(os.path.join(unpath, "__init__.py")) == False: dirlist = os.listdir(unpath) - if len(dirlist)>0: - unpath = os.path.join(unpath,dirlist[0]) + if len(dirlist) > 0: + unpath = os.path.join(unpath, dirlist[0]) - if os.path.isfile(os.path.join(unpath,"__init__.py")) == False: - if self._verbose:print("not a valid addon found") - if self._verbose:print("Paths:") - if self._verbose:print(dirlist) - self._error = "Install addon update manually" - self._error_msg = "Valid addon zip not found" + if os.path.isfile(os.path.join(unpath, "__init__.py")) == False: + if self._verbose: + print("not a valid addon found") + print("Paths:") + print(dirlist) raise ValueError("__init__ file not found in new source") # now commence merging in the two locations: - origpath = os.path.dirname(__file__) # verify, is __file__ always valid? + origpath = os.path.dirname(__file__) # verify, is __file__ always valid? + + self.deepMergeDirectory(origpath, unpath) - self.deepMergeDirectory(origpath,unpath) - # now save the json state # Change to True, to trigger the handler on other side # if allowing reloading within same blender instance @@ -618,14 +1789,13 @@ def upack_staged_zip(self): self.reload_addon() self._update_ready = False - # merge folder 'merger' into folder 'base' without deleting existing - def deepMergeDirectory(self,base,merger): + def deepMergeDirectory(self, base, merger): if not os.path.exists(base): - if self._verbose:print("Base path does not exist") + if self._verbose: print("Base path does not exist") return -1 elif not os.path.exists(merger): - if self._verbose:print("Merger path does not exist") + if self._verbose: print("Merger path does not exist") return -1 # this should have better error handling @@ -642,7 +1812,6 @@ def deepMergeDirectory(self,base,merger): os.remove(destFile) srcFile = os.path.join(path, file) os.rename(srcFile, destFile) - def reload_addon(self): # if post_update false, skip this function @@ -651,8 +1820,7 @@ def reload_addon(self): print("Restart blender to reload addon and complete update") return - - if self._verbose:print("Reloading addon...") + if self._verbose: print("Reloading addon...") addon_utils.modules(refresh=True) bpy.utils.refresh_script_paths() @@ -662,7 +1830,6 @@ def reload_addon(self): bpy.ops.wm.addon_refresh() bpy.ops.wm.addon_enable(module=self._addon_package) - # ------------------------------------------------------------------------- # Other non-api functions and setups # ------------------------------------------------------------------------- @@ -675,31 +1842,31 @@ def clear_state(self): self._error = None self._error_msg = None - def version_tuple_from_text(self,text): + def version_tuple_from_text(self, text): if text == None: return () - # should go through string and remove all non-integers, + # should go through string and remove all non-integers, # and for any given break split into a different section segments = [] tmp = '' for l in str(text): - if l.isdigit()==False: - if len(tmp)>0: + if l.isdigit() == False: + if len(tmp) > 0: segments.append(int(tmp)) tmp = '' else: - tmp+=l - if len(tmp)>0: + tmp += l + if len(tmp) > 0: segments.append(int(tmp)) - if len(segments)==0: - if self._verbose:print("No version strings found text: ",text) - if self._include_master == False: + if len(segments) == 0: + if self._verbose: print("No version strings found text: ", text) + if self._include_branches == False: return () else: - return ('master') + return (text) return tuple(segments) # called for running check in a background thread @@ -718,8 +1885,8 @@ def check_for_update_async(self, callback=None): if self._check_interval_enable == False: return elif self._async_checking == True: - if self._verbose:print("Skipping async check, already started") - return # already running the bg thread + if self._verbose: print("Skipping async check, already started") + return # already running the bg thread elif self._update_ready == None: self.start_async_check_update(False, callback) @@ -731,19 +1898,18 @@ def check_for_update_now(self, callback=None): if self._verbose: print("Check update pressed, first getting current status") if self._async_checking == True: - if self._verbose:print("Skipping async check, already started") - return # already running the bg thread + if self._verbose: print("Skipping async check, already started") + return # already running the bg thread elif self._update_ready == None: self.start_async_check_update(True, callback) else: self._update_ready = None self.start_async_check_update(True, callback) - # this function is not async, will always return in sequential fashion # but should have a parent which calls it in another thread def check_for_update(self, now=False): - if self._verbose:print("Checking for update function") + if self._verbose: print("Checking for update function") # clear the errors if any self._error = None @@ -752,7 +1918,7 @@ def check_for_update(self, now=False): # avoid running again in, just return past result if found # but if force now check, then still do it if self._update_ready != None and now == False: - return (self._update_ready,self._update_version,self._update_link) + return (self._update_ready, self._update_version, self._update_link) if self._current_version == None: raise ValueError("current_version not yet defined") @@ -761,13 +1927,13 @@ def check_for_update(self, now=False): if self._user == None: raise ValueError("username not yet defined") - self.set_updater_json() # self._json + self.set_updater_json() # self._json - if now == False and self.past_interval_timestamp()==False: + if now == False and self.past_interval_timestamp() == False: if self.verbose: print("Aborting check for updated, check interval not reached") return (False, None, None) - + # check if using tags or releases # note that if called the first time, this will pull tags from online if self._fake_install == True: @@ -776,59 +1942,68 @@ def check_for_update(self, now=False): self._update_ready = True self._update_version = "(999,999,999)" self._update_link = "http://127.0.0.1" - + return (self._update_ready, self._update_version, self._update_link) - - # primaryb internet call - self.get_tags() # sets self._tags and self._tag_latest + + # primary internet call + self.get_tags() # sets self._tags and self._tag_latest self._json["last_check"] = str(datetime.now()) self.save_updater_json() - - # if (len(self._tags) == 0 and self._include_master == False) or\ - # (len(self._tags) < 2 and self._include_master == True): - # if self._verbose:print("No tag found on this repository") - # self._update_ready = False - # self._error = "No online versions found" - # if self._include_master == True: - # self._error_msg = "Try installing master from Reinstall" - # else: - # self._error_msg = "No repository tags found for version comparison" - # return (False, None, None) - - # can be () or ('master') in addition to version tag + # can be () or ('master') in addition to branchs, and version tag new_version = self.version_tuple_from_text(self.tag_latest) - if len(self._tags)==0: + if len(self._tags) == 0: self._update_ready = False self._update_version = None self._update_link = None return (False, None, None) - elif self._include_master == False: - link = self._tags[0]["zipball_url"] # potentially other sources - elif self._include_master == True and len(self._tags)>1: - link = self._tags[1]["zipball_url"] # potentially other sources + elif self._include_branches == False: + link = self._tags[0]["zipball_url"] # potentially other sources else: - link = self._tags[0]["zipball_url"] # potentially other sources - + n = len(self._include_branch_list) + if len(self._tags) == n: + # effectively means no tags found on repo + # so provide the first one as default + link = self._tags[0]["zipball_url"] # potentially other sources + else: + link = self._tags[n]["zipball_url"] # potentially other sources + if new_version == (): self._update_ready = False self._update_version = None self._update_link = None return (False, None, None) - elif str(new_version).lower() == "master": - self._update_ready = True - self._update_version = new_version - self._update_link = link - self.save_updater_json() - return (True, new_version, link) - elif new_version > self._current_version: - self._update_ready = True - self._update_version = new_version - self._update_link = link - self.save_updater_json() - return (True, new_version, link) + elif str(new_version).lower() in self._include_branch_list: + # handle situation where master/whichever branch is included + # however, this code effectively is not triggered now + # as new_version will only be tag names, not branch names + if self._include_branch_autocheck == False: + # don't offer update as ready, + # but set the link for the default + # branch for installing + self._update_ready = True + self._update_version = new_version + self._update_link = link + self.save_updater_json() + return (True, new_version, link) + else: + raise ValueError("include_branch_autocheck: NOT YET DEVELOPED") + # bypass releases and look at timestamp of last update + # from a branch compared to now, see if commit values + # match or not. + + else: + # situation where branches not included + + if new_version > self._current_version: + self._update_ready = True + self._update_version = new_version + self._update_link = link + self.save_updater_json() + return (True, new_version, link) + # elif new_version != self._current_version: # self._update_ready = False # self._update_version = new_version @@ -842,25 +2017,24 @@ def check_for_update(self, now=False): self._update_link = None return (False, None, None) - def set_tag(self,name): + def set_tag(self, name): tg = None for tag in self._tags: if name == tag["name"]: tg = tag break if tg == None: - raise ValueError("Version tag not found: "+revert_tag) + raise ValueError("Version tag not found: " + revert_tag) new_version = self.version_tuple_from_text(self.tag_latest) self._update_version = new_version self._update_link = tg["zipball_url"] - - def run_update(self,force=False,revert_tag=None,clean=False,callback=None): + def run_update(self, force=False, revert_tag=None, clean=False, callback=None): # revert_tag: could e.g. get from drop down list # different versions of the addon to revert back to # clean: not used, but in future could use to totally refresh addon self._json["update_ready"] = False - self._json["ignore"] = False # clear ignore flag + self._json["ignore"] = False # clear ignore flag self._json["version_text"] = {} if revert_tag != None: @@ -871,13 +2045,13 @@ def run_update(self,force=False,revert_tag=None,clean=False,callback=None): self._error = None self._error_msg = None - if self.verbose:print("Running update") + if self.verbose: print("Running update") if self._fake_install == True: # change to True, to trigger the reload/"update installed" handler if self._verbose: print("fake_install=True") - print("Just reloading and running any trigger") + print("Just reloading and running any handler triggers") self._json["just_updated"] = True self.save_updater_json() if self._backup_current == True: @@ -885,16 +2059,16 @@ def run_update(self,force=False,revert_tag=None,clean=False,callback=None): self.reload_addon() self._update_ready = False - elif force==False: + elif force == False: if self._update_ready != True: - if self.verbose:print("Update stopped, new version not ready") - return 1 # stopped + if self.verbose: print("Update stopped, new version not ready") + return 1 # stopped elif self._update_link == None: # this shouldn't happen if update is ready - if self.verbose:print("Update stopped, update link unavailable") - return 1 # stopped + if self.verbose: print("Update stopped, update link unavailable") + return 1 # stopped - if self.verbose and revert_tag==None: + if self.verbose and revert_tag == None: print("Staging update") elif self.verbose: print("Staging install") @@ -903,94 +2077,95 @@ def run_update(self,force=False,revert_tag=None,clean=False,callback=None): else: if self._update_link == None: - return # stopped, no link - run check update first or set tag - if self.verbose:print("Forcing update") + return # stopped, no link - run check update first or set tag + if self.verbose: print("Forcing update") # first do a check if self._update_link == None: - if self.verbose:print("Update stopped, could not get link") + if self.verbose: print("Update stopped, could not get link") return self.stage_repository(self._update_link) self.upack_staged_zip() - # would need to compare against other versions held in tags + # would need to compare against other versions held in tags # run the front-end's callback if provided - if callback != None:callback() + if callback != None: callback() # return something meaningful, 0 means it worked return 0 - def past_interval_timestamp(self): if self._check_interval_enable == False: - return True # ie this exact feature is disabled - + return True # ie this exact feature is disabled + if "last_check" not in self._json or self._json["last_check"] == "": return True else: now = datetime.now() last_check = datetime.strptime(self._json["last_check"], - "%Y-%m-%d %H:%M:%S.%f") + "%Y-%m-%d %H:%M:%S.%f") next_check = last_check offset = timedelta( - days=self._check_interval_days + 30*self._check_interval_months, + days=self._check_interval_days + 30 * self._check_interval_months, hours=self._check_interval_hours, minutes=self._check_interval_minutes - ) + ) delta = (now - offset) - last_check if delta.total_seconds() > 0: if self._verbose: - print("Determined it's time to check for updates") + print("{} Updater: Time to check for updates!".format(self._addon)) return True else: if self._verbose: - print("Determined it's not yet time to check for updates") + print("{} Updater: Determined it's not yet time to check for updates".format(self._addon)) return False - def set_updater_json(self): if self._updater_path == None: raise ValueError("updater_path is not defined") elif os.path.isdir(self._updater_path) == False: os.makedirs(self._updater_path) - jpath = os.path.join(self._updater_path,"updater_status.json") + jpath = os.path.join(self._updater_path, "updater_status.json") if os.path.isfile(jpath): with open(jpath) as data_file: self._json = json.load(data_file) - if self._verbose:print("Read in json settings from file") + if self._verbose: print("{} Updater: Read in json settings from file".format(self._addon)) else: # set data structure self._json = { - "last_check":"", - "backup_date":"", - "update_ready":False, - "ignore":False, - "just_restored":False, - "just_updated":False, - "version_text":{} + "last_check": "", + "backup_date": "", + "update_ready": False, + "ignore": False, + "just_restored": False, + "just_updated": False, + "version_text": {} } self.save_updater_json() - def save_updater_json(self): # first save the state if self._update_ready == True: - self._json["update_ready"] = True - self._json["version_text"]["link"]=self._update_link - self._json["version_text"]["version"]=self._update_version + if type(self._update_version) == type((0, 0, 0)): + self._json["update_ready"] = True + self._json["version_text"]["link"] = self._update_link + self._json["version_text"]["version"] = self._update_version + else: + self._json["update_ready"] = False + self._json["version_text"] = {} else: self._json["update_ready"] = False self._json["version_text"] = {} - jpath = os.path.join(self._updater_path,"updater_status.json") - outf = open(jpath,'w') - data_out = json.dumps(self._json,indent=4) + jpath = os.path.join(self._updater_path, "updater_status.json") + outf = open(jpath, 'w') + data_out = json.dumps(self._json, indent=4) outf.write(data_out) outf.close() if self._verbose: - print("Wrote out json settings to file, with the contents:") + print(self._addon + ": Wrote out updater json settings to file, with the contents:") print(self._json) def json_reset_postupdate(self): @@ -998,12 +2173,13 @@ def json_reset_postupdate(self): self._json["update_ready"] = False self._json["version_text"] = {} self.save_updater_json() + def json_reset_restore(self): self._json["just_restored"] = False self._json["update_ready"] = False self._json["version_text"] = {} self.save_updater_json() - self._update_ready = None # reset so you could check update again + self._update_ready = None # reset so you could check update again def ignore_update(self): self._json["ignore"] = True @@ -1013,21 +2189,21 @@ def ignore_update(self): # ASYNC stuff # ------------------------------------------------------------------------- - def start_async_check_update(self, now=False,callback=None): + def start_async_check_update(self, now=False, callback=None): if self._async_checking == True: return - if self._verbose: print("Starting background checking thread") + if self._verbose: print("{} updater: Starting background checking thread".format(self._addon)) check_thread = threading.Thread(target=self.async_check_update, - args=(now,callback,)) + args=(now, callback,)) check_thread.daemon = True self._check_thread = check_thread check_thread.start() - + return True def async_check_update(self, now, callback=None): self._async_checking = True - if self._verbose:print("BG: Checking for update now in background") + if self._verbose: print("{} BG thread: Checking for update now in background".format(self._addon)) # time.sleep(3) # to test background, in case internet too fast to tell # try: self.check_for_update(now=now) @@ -1041,19 +2217,18 @@ def async_check_update(self, now, callback=None): # self._error_msg = "Encountered an error while checking for updates" if self._verbose: - print("BG: Finished checking for update, doing callback") - if callback != None:callback(self._update_ready) + print("{} BG thread: Finished checking for update, doing callback".format(self._addon)) + if callback != None: callback(self._update_ready) self._async_checking = False self._check_thread = None - def stop_async_check_update(self): if self._check_thread != None: try: - if self._verbose:print("Thread will end in normal course.") - # however, "There is no direct kill method on a thread object." - # better to let it run its course - #self._check_thread.stop() + if self._verbose: print("Thread will end in normal course.") + # however, "There is no direct kill method on a thread object." + # better to let it run its course + # self._check_thread.stop() except: pass self._async_checking = False @@ -1061,12 +2236,9 @@ def stop_async_check_update(self): self._error_msg = None - - # ----------------------------------------------------------------------------- # The module-shared class instance, # should be what's imported to other files # ----------------------------------------------------------------------------- Updater = Singleton_updater() - diff --git a/addon_updater_ops.py b/addon_updater_ops.py index a458a31..05e6b24 100755 --- a/addon_updater_ops.py +++ b/addon_updater_ops.py @@ -17,12 +17,33 @@ # ##### END GPL LICENSE BLOCK ##### import bpy -from .addon_updater import Updater as updater from bpy.app.handlers import persistent import os +# updater import, import safely +try: + from .addon_updater import Updater as updater +except Exception as e: + print("ERROR INITIALIZING UPDATER") + print(str(e)) + + + class Singleton_updater_none(object): + def __init__(self): + self.addon = None + self.verbose = False + self.invalidupdater = True # used to distinguish bad install + self.error = None + self.error_msg = None + self.async_checking = None + + + updater = Singleton_updater_none() + updater.error = "Error initializing updater module" + updater.error_msg = str(e) + # Must declare this before classes are loaded -# otherwise the bl_idnames will not match and have errors. +# otherwise the bl_idname's will not match and have errors. # Must be all lowercase and no spaces updater.addon = "ultimaze_pro" @@ -44,46 +65,57 @@ def invoke(self, context, event): def draw(self, context): layout = self.layout + if updater.invalidupdater == True: + layout.label("Updater module error") + return if updater.update_ready == True: - layout.label("Update ready! Press OK to install v" \ - + str(updater.update_version)) - layout.label("or click outside window to defer") + col = layout.column() + col.scale_y = 0.7 + col.label("Update ready! Press OK to install " \ + + str(updater.update_version)) + col.label("or click outside window to defer") # could offer to remove popups here, but window will not redraw # so may be confusing to the user/look like a bug # row = layout.row() # row.label("Prevent future popups:") # row.operator(addon_updater_ignore.bl_idname,text="Ignore update") elif updater.update_ready == False: - layout.label("No updates available") - layout.label("Press okay to dismiss dialog") + col = layout.column() + col.scale_y = 0.7 + col.label("No updates available") + col.label("Press okay to dismiss dialog") # add option to force install else: # case: updater.update_ready = None # we have not yet checked for the update layout.label("Check for update now?") - # potentially in future, could have UI for 'check to select old version' - # to revert back to. + # potentially in future, could have UI for 'check to select old version' + # to revert back to. def execute(self, context): - if updater.update_ready == True: + # in case of error importing updater + if updater.invalidupdater == True: + return {'CANCELLED'} + + if updater.manual_only == True: + row.operator("wm.url_open", text="Open website").url = \ + updater.website + elif updater.update_ready == True: res = updater.run_update(force=False, callback=post_update_callback) # should return 0, if not something happened if updater.verbose: if res == 0: print("Updater returned successful") else: - print("Updater returned " + str(res) + ", error occured") - + print("Updater returned " + str(res) + ", error occurred") elif updater.update_ready == None: (update_ready, version, link) = updater.check_for_update(now=True) # re-launch this dialog atr = addon_updater_install_popup.bl_idname.split(".") getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') - # bpy.ops.retopoflow.updater_install_popup('INVOKE_DEFAULT') - else: if updater.verbose: print("Doing nothing, not ready for update") return {'FINISHED'} @@ -97,10 +129,15 @@ class addon_updater_check_now(bpy.types.Operator): x=updater.addon) def execute(self, context): + + # in case of error importing updater + if updater.invalidupdater == True: + return {'CANCELLED'} + if updater.async_checking == True and updater.error == None: # Check already happened # Used here to just avoid constant applying settings below - # Ignoring if erro, to prevent being stuck on the error screen + # Ignoring if error, to prevent being stuck on the error screen return {'CANCELLED'} return @@ -124,11 +161,18 @@ def execute(self, context): class addon_updater_update_now(bpy.types.Operator): bl_label = "Update " + updater.addon + " addon now" bl_idname = updater.addon + ".updater_update_now" - bl_description = "Update to the latest verison of the {x} addon".format( + bl_description = "Update to the latest version of the {x} addon".format( x=updater.addon) def execute(self, context): + # in case of error importing updater + if updater.invalidupdater == True: + return {'CANCELLED'} + + if updater.manual_only == True: + row.operator("wm.url_open", text="Open website").url = \ + updater.website if updater.update_ready == True: # if it fails, offer to open the website instead try: @@ -141,7 +185,7 @@ def execute(self, context): if res == 0: print("Updater returned successful") else: - print("Updater returned " + str(res) + ", error occured") + print("Updater returned " + str(res) + ", error occurred") except: atr = addon_updater_install_manually.bl_idname.split(".") getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') @@ -166,10 +210,14 @@ class addon_updater_update_target(bpy.types.Operator): x=updater.addon) def target_version(self, context): + # in case of error importing updater + if updater.invalidupdater == True: + ret = [] + ret = [] i = 0 for tag in updater.tags: - ret.append((tag, tag, "Select to install version " + tag)) + ret.append((tag, tag, "Select to install " + tag)) i += 1 return ret @@ -181,6 +229,7 @@ def target_version(self, context): @classmethod def poll(cls, context): + if updater.invalidupdater == True: return False return updater.update_ready != None and len(updater.tags) > 0 def invoke(self, context, event): @@ -188,6 +237,9 @@ def invoke(self, context, event): def draw(self, context): layout = self.layout + if updater.invalidupdater == True: + layout.label("Updater error") + return split = layout.split(percentage=0.66) subcol = split.column() subcol.label("Select install version") @@ -196,6 +248,10 @@ def draw(self, context): def execute(self, context): + # in case of error importing updater + if updater.invalidupdater == True: + return {'CANCELLED'} + res = updater.run_update( force=False, revert_tag=self.target, @@ -207,10 +263,11 @@ def execute(self, context): print("Updater returned successful") else: print("Updater returned " + str(res) + ", error occurred") + return {'CANCELLED'} # try: - # updater.run_update(force=False,revert_tag=self.target) + # updater.run_update(force=False,revert_tag=self.target) # except: - # self.report({'ERROR'}, "Problem installing target version") + # self.report({'ERROR'}, "Problem installing target version") return {'FINISHED'} @@ -226,13 +283,20 @@ def invoke(self, context, event): def draw(self, context): layout = self.layout - # use a "failed flag"? it show this label if the case failed. + + if updater.invalidupdater == True: + layout.label("Updater error") + return + + # use a "failed flag"? it shows this label if the case failed. if False: layout.label("There was an issue trying to auto-install") else: - layout.label("Install the addon manually") - layout.label("Press the download button below and install") - layout.label("the zip file like a normal addon.") + col = layout.column() + col.scale_y = 0.7 + col.label("Install the addon manually") + col.label("Press the download button below and install") + col.label("the zip file like a normal addon.") # if check hasn't happened, ie accidentally called this menu # allow to check here @@ -243,18 +307,15 @@ def draw(self, context): row.operator("wm.url_open", text="Direct download").url = \ updater.update_link else: - row.operator("wm.url_open", text="(failed to retreive)") + row.operator("wm.url_open", text="(failed to retrieve)") row.enabled = False if updater.website != None: row = layout.row() - row.label("Grab update from account") - row.operator("wm.url_open", text="Open website").url = \ updater.website else: row = layout.row() - row.label("See source website to download the update") def execute(self, context): @@ -274,27 +335,40 @@ def invoke(self, context, event): def draw(self, context): layout = self.layout + + if updater.invalidupdater == True: + layout.label("Updater error") + return + # use a "failed flag"? it show this label if the case failed. saved = updater.json if updater.auto_reload_post_update == False: # tell user to restart blender if "just_restored" in saved and saved["just_restored"] == True: - layout.label("Addon restored") - layout.label("Restart blender to reload.") + col = layout.column() + col.scale_y = 0.7 + col.label("Addon restored") + col.label("Restart blender to reload.") updater.json_reset_restore() else: - layout.label("Addon successfully installed") - layout.label("Restart blender to reload.") + col = layout.column() + col.scale_y = 0.7 + col.label("Addon successfully installed") + col.label("Restart blender to reload.") else: # reload addon, but still recommend they restart blender if "just_restored" in saved and saved["just_restored"] == True: - layout.label("Addon restored") - layout.label("Consider restarting blender to fully reload.") + col = layout.column() + col.scale_y = 0.7 + col.label("Addon restored") + col.label("Consider restarting blender to fully reload.") updater.json_reset_restore() else: - layout.label("Addon successfully installed.") - layout.label("Consider restarting blender to fully reload.") + col = layout.column() + col.scale_y = 0.7 + col.label("Addon successfully installed.") + col.label("Consider restarting blender to fully reload.") def execut(self, context): return {'FINISHED'} @@ -314,6 +388,9 @@ def poll(cls, context): return False def execute(self, context): + # in case of error importing updater + if updater.invalidupdater == True: + return {'CANCELLED'} updater.restore_backup() return {'FINISHED'} @@ -326,12 +403,17 @@ class addon_updater_ignore(bpy.types.Operator): @classmethod def poll(cls, context): - if updater.update_ready == True: + if updater.invalidupdater == True: + return False + elif updater.update_ready == True: return True else: return False def execute(self, context): + # in case of error importing updater + if updater.invalidupdater == True: + return {'CANCELLED'} updater.ignore_update() self.report({"INFO"}, "Open addon preferences for updater options") return {'FINISHED'} @@ -345,12 +427,15 @@ class addon_updater_end_background(bpy.types.Operator): # @classmethod # def poll(cls, context): - # if updater.async_checking == True: - # return True - # else: - # return False + # if updater.async_checking == True: + # return True + # else: + # return False def execute(self, context): + # in case of error importing updater + if updater.invalidupdater == True: + return {'CANCELLED'} updater.stop_async_check_update() return {'FINISHED'} @@ -364,7 +449,7 @@ def execute(self, context): ran_autocheck_install_popup = False ran_update_sucess_popup = False -# global var for preventing successive calls +# global var for preventing successive calls ran_background_check = False @@ -372,6 +457,11 @@ def execute(self, context): def updater_run_success_popup_handler(scene): global ran_update_sucess_popup ran_update_sucess_popup = True + + # in case of error importing updater + if updater.invalidupdater == True: + return + try: bpy.app.handlers.scene_update_post.remove( updater_run_success_popup_handler) @@ -386,6 +476,11 @@ def updater_run_success_popup_handler(scene): def updater_run_install_popup_handler(scene): global ran_autocheck_install_popup ran_autocheck_install_popup = True + + # in case of error importing updater + if updater.invalidupdater == True: + return + try: bpy.app.handlers.scene_update_post.remove( updater_run_install_popup_handler) @@ -394,6 +489,22 @@ def updater_run_install_popup_handler(scene): if "ignore" in updater.json and updater.json["ignore"] == True: return # don't do popup if ignore pressed + elif type(updater.update_version) != type((0, 0, 0)): + # likely was from master or another branch, shouldn't trigger popup + updater.json_reset_restore() + return + elif "version_text" in updater.json and "version" in updater.json["version_text"]: + version = updater.json["version_text"]["version"] + ver_tuple = updater.version_tuple_from_text(version) + + if ver_tuple < updater.current_version: + # user probably manually installed to get the up to date addon + # in here. Clear out the update flag using this function + if updater.verbose: + print("{} updater: appears user updated, clearing flag".format( \ + updater.addon)) + updater.json_reset_restore() + return atr = addon_updater_install_popup.bl_idname.split(".") getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') @@ -402,6 +513,10 @@ def updater_run_install_popup_handler(scene): def background_update_callback(update_ready): global ran_autocheck_install_popup + # in case of error importing updater + if updater.invalidupdater == True: + return + if update_ready != True: return @@ -418,9 +533,13 @@ def background_update_callback(update_ready): # Only makes sense to use this if "auto_reload_post_update" == False, # ie don't auto-restart the addon def post_update_callback(): + # in case of error importing updater + if updater.invalidupdater == True: + return + # this is the same code as in conditional at the end of the register function # ie if "auto_reload_post_update" == True, comment out this code - if updater.verbose: print("Running post update callback") + if updater.verbose: print("{} updater: Running post update callback".format(updater.addon)) # bpy.app.handlers.scene_update_post.append(updater_run_success_popup_handler) atr = addon_updater_updated_successful.bl_idname.split(".") @@ -432,6 +551,10 @@ def post_update_callback(): # function for asynchronous background check, which *could* be called on register def check_for_update_background(context): + # in case of error importing updater + if updater.invalidupdater == True: + return + global ran_background_check if ran_background_check == True: # Global var ensures check only happens once @@ -453,13 +576,19 @@ def check_for_update_background(context): # input is an optional callback function # this function should take a bool input, if true: update ready # if false, no update ready - if updater.verbose: print("Running background check for update") + if updater.verbose: + print("{} updater: Running background check for update".format( \ + updater.addon)) updater.check_for_update_async(background_update_callback) ran_background_check = True # can be placed in front of other operators to launch when pressed def check_for_update_nonthreaded(self, context): + # in case of error importing updater + if updater.invalidupdater == True: + return + # only check if it's ready, ie after the time interval specified # should be the async wrapper call here @@ -483,6 +612,10 @@ def check_for_update_nonthreaded(self, context): # for use in register only, to show popup after re-enabling the addon # must be enabled by developer def showReloadPopup(): + # in case of error importing updater + if updater.invalidupdater == True: + return + saved_state = updater.json global ran_update_sucess_popup @@ -505,19 +638,25 @@ def showReloadPopup(): # ----------------------------------------------------------------------------- -# Example includable UI integrations +# Example UI integrations # ----------------------------------------------------------------------------- # UI to place e.g. at the end of a UI panel where to notify update available def update_notice_box_ui(self, context): + # in case of error importing updater + if updater.invalidupdater == True: + return + saved_state = updater.json if updater.auto_reload_post_update == False: if "just_updated" in saved_state and saved_state["just_updated"] == True: layout = self.layout box = layout.box() - box.label("Restart blender", icon="ERROR") - box.label("to complete update") + col = box.column() + col.scale_y = 0.7 + col.label("Restart blender", icon="ERROR") + col.label("to complete update") return # if user pressed ignore, don't draw the box @@ -530,13 +669,17 @@ def update_notice_box_ui(self, context): layout = self.layout box = layout.box() col = box.column(align=True) - col.label("Update ready!", icon="ERROR") - col.operator("wm.url_open", text="Open website").url = updater.website - # col.operator("wm.url_open",text="Direct download").url=updater.update_link - col.operator(addon_updater_install_manually.bl_idname, "Install manually") + if updater.manual_only == False: + col.label("Update ready!", icon="ERROR") + col.operator("wm.url_open", text="Open website").url = updater.website + # col.operator("wm.url_open",text="Direct download").url=updater.update_link + col.operator(addon_updater_install_manually.bl_idname, "Install manually") col.operator(addon_updater_update_now.bl_idname, "Update now", icon="LOOP_FORWARDS") + else: + col.operator("wm.url_open", text="Get update", icon="ERROR").url = \ + updater.website col.operator(addon_updater_ignore.bl_idname, icon="X") @@ -544,11 +687,17 @@ def update_notice_box_ui(self, context): # place inside UI draw using: addon_updater_ops.updaterSettingsUI(self, context) # or by: addon_updater_ops.updaterSettingsUI(context) def update_settings_ui(self, context): - settings = context.user_preferences.addons[__package__].preferences - layout = self.layout box = layout.box() + # in case of error importing updater + if updater.invalidupdater == True: + box.label("Error initializing updater code:") + box.label(updater.error_msg) + return + + settings = context.user_preferences.addons[__package__].preferences + # auto-update settings box.label("Updater Settings") row = box.row() @@ -611,6 +760,21 @@ def update_settings_ui(self, context): split.operator(addon_updater_end_background.bl_idname, text="", icon="X") + elif updater.include_branches == True and \ + len(updater.tags) == len(updater.include_branch_list) and \ + updater.manual_only == False: + # no releases found, but still show the appropriate branch + subcol = col.row(align=True) + subcol.scale_y = 1 + split = subcol.split(align=True) + split.scale_y = 2 + split.operator(addon_updater_update_now.bl_idname, + "Update directly to " + str(updater.include_branch_list[0])) + split = subcol.split(align=True) + split.scale_y = 2 + split.operator(addon_updater_check_now.bl_idname, + text="", icon="FILE_REFRESH") + elif updater.update_ready == True and updater.manual_only == False: subcol = col.row(align=True) subcol.scale_y = 1 @@ -642,12 +806,14 @@ def update_settings_ui(self, context): if updater.manual_only == False: col = row.column(align=True) - if updater.include_master == True: + # col.operator(addon_updater_update_target.bl_idname, + if updater.include_branches == True and len(updater.include_branch_list) > 0: + branch = updater.include_branch_list[0] col.operator(addon_updater_update_target.bl_idname, - "Install master / old verison") + "Install latest {} / old version".format(branch)) else: col.operator(addon_updater_update_target.bl_idname, - "Reinstall / install old verison") + "Reinstall / install old version") lastdate = "none found" backuppath = os.path.join(updater.stage_path, "backup") if "backup_date" in updater.json and os.path.isdir(backuppath): @@ -655,10 +821,11 @@ def update_settings_ui(self, context): lastdate = "Date not found" else: lastdate = updater.json["backup_date"] - backuptext = "Restore addon backup ({x})".format(x=lastdate) + backuptext = "Restore addon backup ({})".format(lastdate) col.operator(addon_updater_restore_backup.bl_idname, backuptext) row = box.row() + row.scale_y = 0.7 lastcheck = updater.json["last_check"] if updater.error != None and updater.error_msg != None: row.label(updater.error_msg) @@ -672,25 +839,30 @@ def update_settings_ui(self, context): # a global function for tag skipping -# a way to filter which tags are displayed, +# a way to filter which tags are displayed, # e.g. to limit downgrading too far # input is a tag text, e.g. "v1.2.3" -# output is True for skipping this tag number, +# output is True for skipping this tag number, # False if the tag is allowed (default for all) def skip_tag_function(tag): + # in case of error importing updater + if updater.invalidupdater == True: + return False + # ---- write any custom code here, return true to disallow version ---- # # # # Filter out e.g. if 'beta' is in name of release # if 'beta' in tag.lower(): - # return True + # return True # ---- write any custom code above, return true to disallow version --- # - if tag["name"].lower() == 'master' and updater.include_master == True: - return False + if updater.include_branches == True: + for branch in updater.include_branch_list: + if tag["name"].lower() == branch: return False # function converting string to tuple, ignoring e.g. leading 'v' tupled = updater.version_tuple_from_text(tag["name"]) - if type(tupled) != type((1, 2, 3)): return True # master + if type(tupled) != type((1, 2, 3)): return True # select the min tag version - change tuple accordingly if updater.version_min_update != None: @@ -730,44 +902,60 @@ def register(bl_info): # used to check/compare versions updater.current_version = bl_info["version"] - # to hard-set udpate frequency, use this here - however, this demo + # to hard-set update frequency, use this here - however, this demo # has this set via UI properties. Optional # updater.set_check_interval( # enable=False,months=0,days=0,hours=0,minutes=2) - # optional, consider turning off for production or allow as an option + # Optional, consider turning off for production or allow as an option # This will print out additional debugging info to the console - updater.verbose = False # make False for production default + updater.verbose = True # make False for production default - # optional, customize where the addon updater processing subfolder is, - # needs to be within the same folder as the addon itself + # Optional, customize where the addon updater processing subfolder is, + # essentially a staging folder used by the updater on its own + # Needs to be within the same folder as the addon itself # updater.updater_path = # set path of updater folder, by default: # /addons/{__package__}/{__package__}_updater # auto create a backup of the addon when installing other versions updater.backup_current = True # True by default - # allow 'master' as an option to update to, skipping any releases. - # releases are still accessible from re-install menu - # updater.include_master = True - - # only allow manual install, thus prompting the user to open - # the webpage to download but not auto-installing. Useful if - # only wanting to get notification of updates - # updater.manual_only = True - - # used for development only, "pretend" to install an update to test + # Allow branches like 'master' as an option to update to, regardless + # of release or version. + # Default behavior: releases will still be used for auto check (popup), + # but the user has the option from user preferences to directly + # update to the master branch or any other branches specified using + # the "install {branch}/older version" operator. + updater.include_branches = True + + # if using "include_branches", + # updater.include_branch_list defaults to ['master'] branch if set to none + # example targeting another multiple branches allowed to pull from + # updater.include_branch_list = ['master', 'dev'] # example with two branches + updater.include_branch_list = None # is the equvalent to setting ['master'] + + # Only allow manual install, thus prompting the user to open + # the addon's webpage to download, specifically: updater.website + # Useful if only wanting to get notification of updates but not + # directly install. + updater.manual_only = False + + # Used for development only, "pretend" to install an update to test # reloading conditions updater.fake_install = False # Set to true to test callback/reloading # Override with a custom function on what tags - # to skip showing for udpater; see code for function above. + # to skip showing for updater; see code for function above. # Set the min and max versions allowed to install. # Optional, default None - updater.version_min_update = (0, 0, 0) # min install (>=) will install this and higher + # min install (>=) will install this and higher + updater.version_min_update = (0, 0, 0) # updater.version_min_update = None # if not wanting to define a min - updater.version_max_update = (9, 9, 9) # max install (<) will install strictly anything lower - # updater.version_max_update = None # if not wanting to define a max + + # max install (<) will install strictly anything lower + # updater.version_max_update = (9,9,9) + updater.version_max_update = None # if not wanting to define a max + updater.skip_tag = skip_tag_function # min and max used in this function # The register line items for all operators/panels From 77c8f5d55b4ea3bc9ebc623bc6341f36e3d2d6be Mon Sep 17 00:00:00 2001 From: Jake Dube Date: Sat, 8 Jul 2017 22:19:33 -0400 Subject: [PATCH 3/3] Update version number --- __init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index 6320dd2..5756193 100644 --- a/__init__.py +++ b/__init__.py @@ -18,7 +18,7 @@ # ##### END GPL LICENSE BLOCK ##### """ -===== MAZE GENERATOR [PRO] V.2.2 ===== +===== MAZE GENERATOR [PRO] V.2.3.3 ===== This __init__ module handles some UI and also registers all classes and properties. """ @@ -51,7 +51,7 @@ bl_info = { "name": "UltiMaze [PRO]", "author": "Jake Dube", - "version": (2, 3, 2), + "version": (2, 3, 3), "blender": (2, 76, 0), "location": "3D View > Tools > Maze Gen", "description": "Generates 3-dimensional mazes.",