diff --git a/MCprep_addon/addon_updater.py b/MCprep_addon/addon_updater.py index d6935293..378fbc22 100755 --- a/MCprep_addon/addon_updater.py +++ b/MCprep_addon/addon_updater.py @@ -20,12 +20,12 @@ """ See documentation for usage https://github.com/CGCookie/blender-addon-updater - """ -__version__ = "1.0.8" +__version__ = "1.1.0" import errno +import traceback import platform import ssl import urllib.request @@ -38,27 +38,21 @@ import fnmatch from datetime import datetime, timedelta -# blender imports, used in limited cases +# Blender imports, used in limited cases. import bpy import addon_utils # ----------------------------------------------------------------------------- -# Define error messages/notices & hard coded globals +# The main class # ----------------------------------------------------------------------------- -# currently not used -DEFAULT_TIMEOUT = 10 -DEFAULT_PER_PAGE = 30 +class SingletonUpdater: + """Addon updater service class. -# ----------------------------------------------------------------------------- -# The main class -# ----------------------------------------------------------------------------- - -class Singleton_updater(object): - """ - This is the singleton class to reference a copy from, - it is the shared module level class + This is the singleton class to instance once and then reference where + needed throughout the addon. It implements all the interfaces for running + updates. """ def __init__(self): @@ -68,32 +62,32 @@ def __init__(self): self._website = None self._current_version = None self._subfolder_path = None - self._tags = [] + self._tags = list() self._tag_latest = None - self._tag_names = [] + self._tag_names = list() self._latest_release = None self._use_releases = False self._include_branches = False self._include_branch_list = ['master'] - self._include_branch_autocheck = False + self._include_branch_auto_check = 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 + # By default, backup current addon on update/target install. self._backup_current = True self._backup_ignore_patterns = None - # set patterns for what files to overwrite on update - self._overwrite_patterns = ["*.py","*.pyc"] - self._remove_pre_update_patterns = [] + # Set patterns the files to overwrite during an update. + self._overwrite_patterns = ["*.py", "*.pyc"] + self._remove_pre_update_patterns = list() - # by default, don't auto enable/disable the addon on update - # as it is slightly less stable/won't always fully reload module + # By default, don't auto disable+re-enable the addon after an update, + # as this is less stable/often won't fully reload all modules anyways. self._auto_reload_post_update = False - # settings relating to frequency and whether to enable auto background check - self._check_interval_enable = False + # Settings for the frequency of automated background checks. + self._check_interval_enabled = False self._check_interval_months = 0 self._check_interval_days = 7 self._check_interval_hours = 0 @@ -101,6 +95,7 @@ def __init__(self): # runtime variables, initial conditions self._verbose = False + self._use_print_traces = True self._fake_install = False self._async_checking = False # only true when async daemon started self._update_ready = None @@ -111,23 +106,22 @@ def __init__(self): self._select_link = None self.skip_tag = None - # get from module data + # Get data from the running blender module (addon). 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_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._json = dict() self._error = None self._error_msg = None self._prefiltered_tag_count = 0 - # UI code only, ie not used within this module but still useful - # properties to have + # UI properties, not used within this module but still useful to have. # to verify a valid import, in place of placeholder import - self.showpopups = True # used in UI to show or not show update popups - self.invalidupdater = False + self.show_popups = True # UI uses to show popups or not. + self.invalid_updater = False # pre-assign basic select-link function def select_link_function(self, tag): @@ -135,15 +129,24 @@ def select_link_function(self, tag): self._select_link = select_link_function + def print_trace(self): + """Print handled exception details when use_print_traces is set""" + if self._use_print_traces: + traceback.print_exc() + + def print_verbose(self, msg): + """Print out a verbose logging message if verbose is true.""" + if not self._verbose: + return + print("{} addon: ".format(self.addon) + msg) # ------------------------------------------------------------------------- # Getters and setters # ------------------------------------------------------------------------- - - @property def addon(self): return self._addon + @addon.setter def addon(self, value): self._addon = str(value) @@ -151,9 +154,10 @@ def addon(self, value): @property def api_url(self): return self._engine.api_url + @api_url.setter def api_url(self, value): - if self.check_is_url(value) == False: + if not self.check_is_url(value): raise ValueError("Not a valid URL: " + value) self._engine.api_url = value @@ -164,40 +168,41 @@ def async_checking(self): @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") + raise ValueError("auto_reload_post_update must be a boolean value") @property def backup_current(self): return self._backup_current + @backup_current.setter def backup_current(self, value): - if value == None: + if value is None: self._backup_current = False - return else: self._backup_current = value @property def backup_ignore_patterns(self): return self._backup_ignore_patterns + @backup_ignore_patterns.setter def backup_ignore_patterns(self, value): - if value == None: + if value is None: self._backup_ignore_patterns = None - return - elif type(value) != type(['list']): + elif not isinstance(value, list): raise ValueError("Backup pattern must be in list format") else: self._backup_ignore_patterns = value @property def check_interval(self): - return (self._check_interval_enable, + return (self._check_interval_enabled, self._check_interval_months, self._check_interval_days, self._check_interval_hours, @@ -206,9 +211,10 @@ def check_interval(self): @property def current_version(self): return self._current_version + @current_version.setter def current_version(self, tuple_values): - if tuple_values==None: + if tuple_values is None: self._current_version = None return elif type(tuple_values) is not tuple: @@ -216,23 +222,25 @@ def current_version(self, tuple_values): tuple(tuple_values) except: raise ValueError( - "Not a tuple! current_version must be a tuple of integers") + "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") + "current_version must be a tuple of integers") self._current_version = tuple(tuple_values) @property def engine(self): return self._engine.name + @engine.setter def engine(self, value): - if value.lower()=="github": + engine = value.lower() + if engine == "github": self._engine = GithubEngine() - elif value.lower()=="gitlab": + elif engine == "gitlab": self._engine = GitlabEngine() - elif value.lower()=="bitbucket": + elif engine == "bitbucket": self._engine = BitbucketEngine() else: raise ValueError("Invalid engine selection") @@ -248,41 +256,47 @@ def error_msg(self): @property def fake_install(self): return self._fake_install + @fake_install.setter def fake_install(self, value): - if type(value) != type(False): + if not isinstance(value, bool): raise ValueError("fake_install must be a boolean value") self._fake_install = bool(value) # not currently used @property - def include_branch_autocheck(self): - return self._include_branch_autocheck - @include_branch_autocheck.setter - def include_branch_autocheck(self, value): + def include_branch_auto_check(self): + return self._include_branch_auto_check + + @include_branch_auto_check.setter + def include_branch_auto_check(self, value): try: - self._include_branch_autocheck = bool(value) + self._include_branch_auto_check = bool(value) except: - raise ValueError("include_branch_autocheck must be a boolean value") + raise ValueError("include_branch_autocheck must be a boolean") @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: + if value is None: self._include_branch_list = ['master'] - elif type(value) != type(['master']) or value==[]: - raise ValueError("include_branch_list should be a list of valid branches") + elif not isinstance(value, list) or len(value) == 0: + 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") + raise ValueError( + "include_branch_list should be a list of valid branches") @property def include_branches(self): return self._include_branches + @include_branches.setter def include_branches(self, value): try: @@ -292,19 +306,20 @@ def include_branches(self, value): @property def json(self): - if self._json == {}: + if len(self._json) == 0: self.set_updater_json() return self._json @property def latest_release(self): - if self._latest_release == None: + if self._latest_release is None: return None return self._latest_release @property def manual_only(self): return self._manual_only + @manual_only.setter def manual_only(self, value): try: @@ -315,11 +330,12 @@ def manual_only(self, value): @property def overwrite_patterns(self): return self._overwrite_patterns + @overwrite_patterns.setter def overwrite_patterns(self, value): - if value == None: - self._overwrite_patterns = ["*.py","*.pyc"] - elif type(value) != type(['']): + if value is None: + self._overwrite_patterns = ["*.py", "*.pyc"] + elif not isinstance(value, list): raise ValueError("overwrite_patterns needs to be in a list format") else: self._overwrite_patterns = value @@ -327,9 +343,10 @@ def overwrite_patterns(self, value): @property def private_token(self): return self._engine.token + @private_token.setter def private_token(self, value): - if value==None: + if value is None: self._engine.token = None else: self._engine.token = str(value) @@ -337,28 +354,32 @@ def private_token(self, value): @property def remove_pre_update_patterns(self): return self._remove_pre_update_patterns + @remove_pre_update_patterns.setter def remove_pre_update_patterns(self, value): - if value == None: - self._remove_pre_update_patterns = [] - elif type(value) != type(['']): - raise ValueError("remove_pre_update_patterns needs to be in a list format") + if value is None: + self._remove_pre_update_patterns = list() + elif not isinstance(value, list): + raise ValueError( + "remove_pre_update_patterns needs to be in a list format") else: self._remove_pre_update_patterns = value @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") + raise ValueError("repo must be a string value") @property def select_link(self): return self._select_link + @select_link.setter def select_link(self, value): # ensure it is a function assignment, with signature: @@ -370,38 +391,41 @@ def select_link(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 value is None: + self.print_verbose("Aborting assigning stage_path, it's null") return - elif value != None and not os.path.exists(value): + elif value is not None and not os.path.exists(value): try: os.makedirs(value) except: - if self._verbose: print("Error trying to staging path") + self.print_verbose("Error trying to staging path") + self.print_trace() return self._updater_path = value @property def subfolder_path(self): return self._subfolder_path + @subfolder_path.setter def subfolder_path(self, value): self._subfolder_path = value @property def tags(self): - if self._tags == []: - return [] - tag_names = [] + if len(self._tags) == 0: + return list() + tag_names = list() for tag in self._tags: tag_names.append(tag["name"]) return tag_names @property def tag_latest(self): - if self._tag_latest == None: + if self._tag_latest is None: return None return self._tag_latest["name"] @@ -420,6 +444,7 @@ def update_version(self): @property def use_releases(self): return self._use_releases + @use_releases.setter def use_releases(self, value): try: @@ -430,6 +455,7 @@ def use_releases(self, value): @property def user(self): return self._user + @user.setter def user(self, value): try: @@ -440,39 +466,52 @@ def user(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") + self.print_verbose("Verbose is enabled") except: raise ValueError("Verbose must be a boolean value") + @property + def use_print_traces(self): + return self._use_print_traces + + @use_print_traces.setter + def use_print_traces(self, value): + try: + self._use_print_traces = bool(value) + except: + raise ValueError("use_print_traces must be a boolean 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: + if value is None: self._version_max_update = None return - if type(value) != type((1,2,3)): + if not isinstance(value, tuple): raise ValueError("Version maximum must be a tuple") for subvalue in value: - if type(subvalue) != int: + if type(subvalue) is not int: raise ValueError("Version elements must be integers") self._version_max_update = value @property def version_min_update(self): return self._version_min_update + @version_min_update.setter def version_min_update(self, value): - if value == None: + if value is None: self._version_min_update = None return - if type(value) != type((1,2,3)): + if not isinstance(value, tuple): raise ValueError("Version minimum must be a tuple") for subvalue in value: if type(subvalue) != int: @@ -482,37 +521,40 @@ def version_min_update(self, value): @property def website(self): return self._website + @website.setter def website(self, value): - if self.check_is_url(value) == False: + if not self.check_is_url(value): raise ValueError("Not a valid URL: " + value) self._website = value - # ------------------------------------------------------------------------- # Parameter validation related functions # ------------------------------------------------------------------------- - - - def check_is_url(self, url): + @staticmethod + def check_is_url(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 = [] + def _get_tag_names(self): + tag_names = list() self.get_tags() for tag in self._tags: tag_names.append(tag["name"]) return tag_names - 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 + def set_check_interval(self, enabled=False, + months=0, days=14, hours=0, minutes=0): + """Set the time interval between automated checks, and if enabled. - if type(enable) is not bool: + Has enabled = False as default to not check against frequency, + if enabled, default is 2 weeks. + """ + + if type(enabled) 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") @@ -523,31 +565,26 @@ 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: - self._check_interval_enable = False + if not enabled: + self._check_interval_enabled = False else: - self._check_interval_enable = True + self._check_interval_enabled = True self._check_interval_months = months self._check_interval_days = days self._check_interval_hours = hours self._check_interval_minutes = minutes - # 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()) - + a=self._user, b=self._repo, c=self.form_repo_url()) # ------------------------------------------------------------------------- # API-related functions # ------------------------------------------------------------------------- - def form_repo_url(self): return self._engine.form_repo_url(self) @@ -559,7 +596,7 @@ def form_branch_url(self, branch): def get_tags(self): request = self.form_tags_url() - if self._verbose: print("Getting tags from server") + self.print_verbose("Getting tags from server") # get all tags, internet call all_tags = self._engine.parse_tags(self.get_api(request), self) @@ -567,83 +604,89 @@ def get_tags(self): self._prefiltered_tag_count = len(all_tags) else: self._prefiltered_tag_count = 0 - all_tags = [] + all_tags = list() # pre-process to skip tags - if self.skip_tag != None: - self._tags = [tg for tg in all_tags if self.skip_tag(self, tg)==False] + if self.skip_tag is not None: + self._tags = [tg for tg in all_tags if not self.skip_tag(self, tg)] 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: + if self._include_branches: temp_branches = self._include_branch_list.copy() temp_branches.reverse() for branch in temp_branches: request = self.form_branch_url(branch) include = { - "name":branch.title(), - "zipball_url":request + "name": branch.title(), + "zipball_url": request } self._tags = [include] + self._tags # append to front - if self._tags == None: + if self._tags is None: # some error occurred self._tag_latest = None - self._tags = [] - return - elif self._prefiltered_tag_count == 0 and self._include_branches == False: + self._tags = list() + + elif self._prefiltered_tag_count == 0 and not self._include_branches: self._tag_latest = None - if self._error == None: # if not None, could have had no internet + if self._error is None: # if not None, could have had no internet 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: - if not self._error: 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)-len(self._include_branch_list)==0 and self._include_branches==True) \ - or (len(self._tags)==0 and self._include_branches==False) \ - and self._prefiltered_tag_count > 0: + self._error_msg = "No releases or tags found in repository" + self.print_verbose("No releases or tags found in repository") + + elif self._prefiltered_tag_count == 0 and self._include_branches: + if not self._error: + self._tag_latest = self._tags[0] + branch = self._include_branch_list[0] + self.print_verbose("{} branch found, no releases: {}".format( + branch, self._tags[0])) + + elif ((len(self._tags) - len(self._include_branch_list) == 0 + and self._include_branches) + or (len(self._tags) == 0 and not self._include_branches) + 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") + self.print_verbose(self._error_msg) + else: - if self._include_branches == False: + if not self._include_branches: self._tag_latest = self._tags[0] - if self._verbose: print("Most recent tag found:",self._tags[0]['name']) + self.print_verbose( + "Most recent tag found:" + str(self._tags[0]['name'])) else: - # don't return branch if in list + # Don't return branch if in list. n = len(self._include_branch_list) self._tag_latest = self._tags[n] # guaranteed at least len()=n+1 - if self._verbose: print("Most recent tag found:",self._tags[n]['name']) + self.print_verbose( + "Most recent tag found:" + str(self._tags[n]['name'])) - - # all API calls to base url def get_raw(self, url): - # print("Raw request:", url) + """All API calls to base url.""" request = urllib.request.Request(url) try: context = ssl._create_unverified_context() except: - # some blender packaged python versions don't have this, largely - # useful for local network setups otherwise minimal impact + # Some blender packaged python versions don't have this, largely + # useful for local network setups otherwise minimal impact. context = None - # setup private request headers if appropriate - if self._engine.token != None: + # Setup private request headers if appropriate. + if self._engine.token is not None: if self._engine.name == "gitlab": - request.add_header('PRIVATE-TOKEN',self._engine.token) + request.add_header('PRIVATE-TOKEN', self._engine.token) else: - if self._verbose: print("Tokens not setup for engine yet") + self.print_verbose("Tokens not setup for engine yet") - # Always set user agent - request.add_header('User-Agent', "Python/"+str(platform.python_version())) + # Always set user agent. + request.add_header( + 'User-Agent', "Python/" + str(platform.python_version())) - # run the request + # Run the request. try: if context: result = urllib.request.urlopen(request, context=context) @@ -658,6 +701,7 @@ def get_raw(self, url): self._error = "HTTP error" self._error_msg = str(e.code) print(self._error, self._error_msg) + self.print_trace() self._update_ready = None except urllib.error.URLError as e: reason = str(e.reason) @@ -669,6 +713,7 @@ def get_raw(self, url): self._error = "URL error, check internet connection" self._error_msg = reason print(self._error, self._error_msg) + self.print_trace() self._update_ready = None return None else: @@ -676,13 +721,11 @@ def get_raw(self, url): result.close() return result_string.decode() - - # result of all api calls, decoded into json format def get_api(self, url): - # return the json version + """Result of all api calls, decoded into json format.""" get = None get = self.get_raw(url) - if get != None: + if get is not None: try: return json.JSONDecoder().decode(get) except Exception as e: @@ -690,123 +733,139 @@ def get_api(self, url): self._error_msg = str(e.reason) self._update_ready = None print(self._error, self._error_msg) + self.print_trace() return None else: return None - - # create a working directory and download the new files def stage_repository(self, url): + """Create a working directory and download the new files""" - local = os.path.join(self._updater_path,"update_staging") + local = os.path.join(self._updater_path, "update_staging") error = None - # make/clear the staging folder - # ensure the folder is always "clean" - if self._verbose: print("Preparing staging folder for download:\n",local) - if os.path.isdir(local) == True: + # Make/clear the staging folder, to ensure the folder is always clean. + self.print_verbose( + "Preparing staging folder for download:\n" + str(local)) + if os.path.isdir(local): try: shutil.rmtree(local) os.makedirs(local) except: error = "failed to remove existing staging directory" + self.print_trace() else: try: os.makedirs(local) except: error = "failed to create staging directory" + self.print_trace() - if error != None: - if self._verbose: print("Error: Aborting update, "+error) + if error is not None: + self.print_verbose("Error: Aborting update, " + error) self._error = "Update aborted, staging path error" self._error_msg = "Error: {}".format(error) return False - if self._backup_current==True: + if self._backup_current: 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") + self.print_verbose("Now retrieving the new source zip") + self._source_zip = os.path.join(local, "source.zip") + self.print_verbose("Starting download update zip") try: request = urllib.request.Request(url) context = ssl._create_unverified_context() - # setup private token if appropriate - if self._engine.token != None: + # Setup private token if appropriate. + if self._engine.token is not None: if self._engine.name == "gitlab": - request.add_header('PRIVATE-TOKEN',self._engine.token) + request.add_header('PRIVATE-TOKEN', self._engine.token) else: - if self._verbose: print("Tokens not setup for selected engine yet") + self.print_verbose( + "Tokens not setup for selected engine yet") # Always set user agent - request.add_header('User-Agent', "Python/"+str(platform.python_version())) + request.add_header( + 'User-Agent', "Python/" + str(platform.python_version())) - self.urlretrieve(urllib.request.urlopen(request,context=context), self._source_zip) - # add additional checks on file size being non-zero - if self._verbose: print("Successfully downloaded update zip") + self.url_retrieve(urllib.request.urlopen(request, context=context), + self._source_zip) + # Add additional checks on file size being non-zero. + self.print_verbose("Successfully downloaded update zip") return True except Exception as e: self._error = "Error retrieving download, bad link?" self._error_msg = "Error: {}".format(e) - if self._verbose: - print("Error retrieving download, bad link?") - print("Error: {}".format(e)) + print("Error retrieving download, bad link?") + print("Error: {}".format(e)) + self.print_trace() return False - 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") + """Save a backup of the current installed addon prior to an update.""" + self.print_verbose("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 self._verbose: print("Backup destination path: ",local) + self.print_verbose("Backup destination path: " + str(local)) if os.path.isdir(local): try: shutil.rmtree(local) except: - if self._verbose:print("Failed to removed previous backup folder, contininuing") + self.print_verbose( + "Failed to removed previous backup folder, continuing") + self.print_trace() - # remove the temp folder; shouldn't exist but could if previously interrupted + # Remove the temp folder. + # Shouldn't exist but could if previously interrupted. if os.path.isdir(tempdest): try: shutil.rmtree(tempdest) except: - if self._verbose: - print("Failed to remove existing temp folder, contininuing") - # make the full addon copy, which temporarily places outside the addon folder - if self._backup_ignore_patterns != None: - shutil.copytree( - self._addon_root,tempdest, - ignore=shutil.ignore_patterns(*self._backup_ignore_patterns)) + self.print_verbose( + "Failed to remove existing temp folder, continuing") + self.print_trace() + + # Make a full addon copy, temporarily placed outside the addon folder. + if self._backup_ignore_patterns is not None: + try: + shutil.copytree(self._addon_root, tempdest, + ignore=shutil.ignore_patterns( + *self._backup_ignore_patterns)) + except: + print("Failed to create backup, still attempting update.") + self.print_trace() + return else: - shutil.copytree(self._addon_root,tempdest) + try: + shutil.copytree(self._addon_root, tempdest) + except: + print("Failed to create backup, still attempting update.") + self.print_trace() + return shutil.move(tempdest, local) - # save the date for future ref + # Save the date for future reference. 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("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") + """Restore the last backed up addon version, user initiated only""" + self.print_verbose("Restoring backup, 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) + # Move instead contents back in place, instead of copy. + 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 @@ -815,30 +874,30 @@ def restore_backup(self): self.reload_addon() - def unpack_staged_zip(self,clean=False): + def unpack_staged_zip(self, clean=False): """Unzip the downloaded file, and validate contents""" - if os.path.isfile(self._source_zip) == False: - if self._verbose: print("Error, update zip not found") + if not os.path.isfile(self._source_zip): + self.print_verbose("Error, update zip not found") self._error = "Install failed" self._error_msg = "Downloaded zip not found" return -1 - # clear the existing source folder in case previous files remain + # Clear the existing source folder in case previous files remain. outdir = os.path.join(self._updater_path, "source") try: shutil.rmtree(outdir) - if self._verbose: - print("Source folder cleared") + self.print_verbose("Source folder cleared") except: - pass + self.print_trace() # Create parent directories if needed, would not be relevant unless - # installing addon into another location or via an addon manager + # installing addon into another location or via an addon manager. try: os.mkdir(outdir) except Exception as err: print("Error occurred while making extract dir:") print(str(err)) + self.print_trace() self._error = "Install failed" self._error_msg = "Failed to make extract directory" return -1 @@ -849,47 +908,46 @@ def unpack_staged_zip(self,clean=False): self._error_msg = "Failed to create extract directory" return -1 - if self._verbose: - print("Begin extracting source from zip:", self._source_zip) + self.print_verbose( + "Begin extracting source from zip:" + str(self._source_zip)) zfile = zipfile.ZipFile(self._source_zip, "r") if not zfile: - if self._verbose: - print("Resulting file is not a zip, cannot extract") self._error = "Install failed" self._error_msg = "Resulting file is not a zip, cannot extract" + self.print_verbose(self._error_msg) return -1 # Now extract directly from the first subfolder (not root) # this avoids adding the first subfolder to the path length, - # which can be too long if the download has the SHA in the name - zsep = '/' #os.sep # might just always be / even on windows + # which can be too long if the download has the SHA in the name. + zsep = '/' # Not using os.sep, always the / value even on windows. for name in zfile.namelist(): if zsep not in name: continue - top_folder = name[:name.index(zsep)+1] + top_folder = name[:name.index(zsep) + 1] if name == top_folder + zsep: continue # skip top level folder - subpath = name[name.index(zsep)+1:] + sub_path = name[name.index(zsep) + 1:] if name.endswith(zsep): try: - os.mkdir(os.path.join(outdir, subpath)) - if self._verbose: - print("Extract - mkdir: ", os.path.join(outdir, subpath)) + os.mkdir(os.path.join(outdir, sub_path)) + self.print_verbose( + "Extract - mkdir: " + os.path.join(outdir, sub_path)) except OSError as exc: if exc.errno != errno.EEXIST: self._error = "Install failed" self._error_msg = "Could not create folder from zip" + self.print_trace() return -1 else: - with open(os.path.join(outdir, subpath), "wb") as outfile: + with open(os.path.join(outdir, sub_path), "wb") as outfile: data = zfile.read(name) outfile.write(data) - if self._verbose: - print("Extract - create:", os.path.join(outdir, subpath)) + self.print_verbose( + "Extract - create: " + os.path.join(outdir, sub_path)) - if self._verbose: - print("Extracted source") + self.print_verbose("Extracted source") unpath = os.path.join(self._updater_path, "source") if not os.path.isdir(unpath): @@ -902,165 +960,181 @@ def unpack_staged_zip(self,clean=False): self._subfolder_path.replace('/', os.path.sep) self._subfolder_path.replace('\\', os.path.sep) - # either directly in root of zip/one subfolder, or use specified path - if os.path.isfile(os.path.join(unpath,"__init__.py")) == False: + # Either directly in root of zip/one subfolder, or use specified path. + if not os.path.isfile(os.path.join(unpath, "__init__.py")): dirlist = os.listdir(unpath) - if len(dirlist)>0: - if self._subfolder_path == "" or self._subfolder_path == None: + if len(dirlist) > 0: + if self._subfolder_path == "" or self._subfolder_path is None: unpath = os.path.join(unpath, dirlist[0]) else: unpath = os.path.join(unpath, self._subfolder_path) - # smarter check for additional sub folders for a single folder - # containing __init__.py - if os.path.isfile(os.path.join(unpath,"__init__.py")) == False: - if self._verbose: - print("not a valid addon found") - print("Paths:") - print(dirlist) + # Smarter check for additional sub folders for a single folder + # containing the __init__.py file. + if not os.path.isfile(os.path.join(unpath, "__init__.py")): + print("Not a valid addon found") + print("Paths:") + print(dirlist) self._error = "Install failed" self._error_msg = "No __init__ file found in new source" return -1 - # merge code with running addon directory, using blender default behavior - # plus any modifiers indicated by user (e.g. force remove/keep) - self.deepMergeDirectory(self._addon_root, unpath, clean) + # Merge code with the addon directory, using blender default behavior, + # plus any modifiers indicated by user (e.g. force remove/keep). + self.deep_merge_directory(self._addon_root, unpath, clean) - # Now save the json state - # Change to True, to trigger the handler on other side - # if allowing reloading within same blender instance + # Now save the json state. + # Change to True to trigger the handler on other side if allowing + # reloading within same blender session. self._json["just_updated"] = True self.save_updater_json() self.reload_addon() self._update_ready = False return 0 - - def deepMergeDirectory(self,base,merger,clean=False): - """Merge folder 'merger' into folder 'base' without deleting existing""" + def deep_merge_directory(self, base, merger, clean=False): + """Merge folder 'merger' into 'base' without deleting existing""" if not os.path.exists(base): - if self._verbose: - print("Base path does not exist:", base) + self.print_verbose("Base path does not exist:" + str(base)) return -1 elif not os.path.exists(merger): - if self._verbose: - print("Merger path does not exist") + self.print_verbose("Merger path does not exist") return -1 - # paths to be aware of and not overwrite/remove/etc - staging_path = os.path.join(self._updater_path,"update_staging") - backup_path = os.path.join(self._updater_path,"backup") + # Path to be aware of and not overwrite/remove/etc. + staging_path = os.path.join(self._updater_path, "update_staging") # If clean install is enabled, clear existing files ahead of time - # note: will not delete the update.json, update folder, staging, or staging - # but will delete all other folders/files in addon directory + # note: will not delete the update.json, update folder, staging, or + # staging but will delete all other folders/files in addon directory. error = None - if clean==True: + if clean: try: - # implement clearing of all folders/files, except the - # updater folder and updater json + # Implement clearing of all folders/files, except the updater + # folder and updater json. # Careful, this deletes entire subdirectories recursively... - # make sure that base is not a high level shared folder, but - # is dedicated just to the addon itself - if self._verbose: print("clean=True, clearing addon folder to fresh install state") + # Make sure that base is not a high level shared folder, but + # is dedicated just to the addon itself. + self.print_verbose( + "clean=True, clearing addon folder to fresh install state") - # remove root files and folders (except update folder) - files = [f for f in os.listdir(base) if os.path.isfile(os.path.join(base,f))] - folders = [f for f in os.listdir(base) if os.path.isdir(os.path.join(base,f))] + # Remove root files and folders (except update folder). + files = [f for f in os.listdir(base) + if os.path.isfile(os.path.join(base, f))] + folders = [f for f in os.listdir(base) + if os.path.isdir(os.path.join(base, f))] for f in files: - os.remove(os.path.join(base,f)) - print("Clean removing file {}".format(os.path.join(base,f))) + os.remove(os.path.join(base, f)) + self.print_verbose( + "Clean removing file {}".format(os.path.join(base, f))) for f in folders: - if os.path.join(base,f)==self._updater_path: continue - shutil.rmtree(os.path.join(base,f)) - print("Clean removing folder and contents {}".format(os.path.join(base,f))) + if os.path.join(base, f) is self._updater_path: + continue + shutil.rmtree(os.path.join(base, f)) + self.print_verbose( + "Clean removing folder and contents {}".format( + os.path.join(base, f))) except Exception as err: error = "failed to create clean existing addon folder" print(error, str(err)) + self.print_trace() # Walk through the base addon folder for rules on pre-removing - # but avoid removing/altering backup and updater file + # but avoid removing/altering backup and updater file. for path, dirs, files in os.walk(base): - # prune ie skip updater folder - dirs[:] = [d for d in dirs if os.path.join(path,d) not in [self._updater_path]] + # Prune ie skip updater folder. + dirs[:] = [d for d in dirs + if os.path.join(path, d) not in [self._updater_path]] for file in files: - for ptrn in self.remove_pre_update_patterns: - if fnmatch.filter([file],ptrn): + for pattern in self.remove_pre_update_patterns: + if fnmatch.filter([file], pattern): try: - fl = os.path.join(path,file) + fl = os.path.join(path, file) os.remove(fl) - if self._verbose: print("Pre-removed file "+file) + self.print_verbose("Pre-removed file " + file) except OSError: - print("Failed to pre-remove "+file) + print("Failed to pre-remove " + file) + self.print_trace() # Walk through the temp addon sub folder for replacements # this implements the overwrite rules, which apply after # the above pre-removal rules. This also performs the - # actual file copying/replacements + # actual file copying/replacements. for path, dirs, files in os.walk(merger): - # verify this structure works to prune updater sub folder overwriting - dirs[:] = [d for d in dirs if os.path.join(path,d) not in [self._updater_path]] - relPath = os.path.relpath(path, merger) - destPath = os.path.join(base, relPath) - if not os.path.exists(destPath): - os.makedirs(destPath) + # Verify structure works to prune updater sub folder overwriting. + dirs[:] = [d for d in dirs + if os.path.join(path, d) not in [self._updater_path]] + rel_path = os.path.relpath(path, merger) + dest_path = os.path.join(base, rel_path) + if not os.path.exists(dest_path): + os.makedirs(dest_path) for file in files: - # bring in additional logic around copying/replacing - # Blender default: overwrite .py's, don't overwrite the rest - destFile = os.path.join(destPath, file) + # Bring in additional logic around copying/replacing. + # Blender default: overwrite .py's, don't overwrite the rest. + dest_file = os.path.join(dest_path, file) srcFile = os.path.join(path, file) - # decide whether to replace if file already exists, and copy new over - if os.path.isfile(destFile): - # otherwise, check each file to see if matches an overwrite pattern - replaced=False - for ptrn in self._overwrite_patterns: - if fnmatch.filter([destFile],ptrn): - replaced=True + # Decide to replace if file already exists, and copy new over. + if os.path.isfile(dest_file): + # Otherwise, check each file for overwrite pattern match. + replaced = False + for pattern in self._overwrite_patterns: + if fnmatch.filter([file], pattern): + replaced = True break if replaced: - os.remove(destFile) - os.rename(srcFile, destFile) - if self._verbose: print("Overwrote file "+os.path.basename(destFile)) + os.remove(dest_file) + os.rename(srcFile, dest_file) + self.print_verbose( + "Overwrote file " + os.path.basename(dest_file)) else: - if self._verbose: print("Pattern not matched to "+os.path.basename(destFile)+", not overwritten") + self.print_verbose( + "Pattern not matched to {}, not overwritten".format( + os.path.basename(dest_file))) else: - # file did not previously exist, simply move it over - os.rename(srcFile, destFile) - if self._verbose: print("New file "+os.path.basename(destFile)) + # File did not previously exist, simply move it over. + os.rename(srcFile, dest_file) + self.print_verbose( + "New file " + os.path.basename(dest_file)) # now remove the temp staging folder and downloaded zip try: shutil.rmtree(staging_path) except: - error = "Error: Failed to remove existing staging directory, consider manually removing "+staging_path - if self._verbose: print(error) - + error = ("Error: Failed to remove existing staging directory, " + "consider manually removing ") + staging_path + self.print_verbose(error) + self.print_trace() def reload_addon(self): # if post_update false, skip this function # else, unload/reload addon & trigger popup - if self._auto_reload_post_update == False: + if not self._auto_reload_post_update: print("Restart blender to reload addon and complete update") return - if self._verbose: print("Reloading addon...") + self.print_verbose("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) - + if "addon_disable" in dir(bpy.ops.wm): # 2.7 + bpy.ops.wm.addon_disable(module=self._addon_package) + bpy.ops.wm.addon_refresh() + bpy.ops.wm.addon_enable(module=self._addon_package) + print("2.7 reload complete") + else: # 2.8 + bpy.ops.preferences.addon_disable(module=self._addon_package) + bpy.ops.preferences.addon_refresh() + bpy.ops.preferences.addon_enable(module=self._addon_package) + print("2.8 reload complete") # ------------------------------------------------------------------------- # Other non-api functions and setups # ------------------------------------------------------------------------- - def clear_state(self): self._update_ready = None self._update_link = None @@ -1069,88 +1143,96 @@ def clear_state(self): self._error = None self._error_msg = None - # custom urlretrieve implementation - def urlretrieve(self, urlfile, filepath): - chunk = 1024*8 + def url_retrieve(self, url_file, filepath): + """Custom urlretrieve implementation""" + chunk = 1024 * 8 f = open(filepath, "wb") while 1: - data = urlfile.read(chunk) + data = url_file.read(chunk) if not data: - #print("done.") + # print("done.") break f.write(data) - #print("Read %s bytes"%len(data)) + # print("Read %s bytes" % len(data)) f.close() + def version_tuple_from_text(self, text): + """Convert text into a tuple of numbers (int). - 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. + """ + if text is None: + return () - # should go through string and remove all non-integers, - # and for any given break split into a different section - segments = [] + segments = list() tmp = '' - for l in str(text): - if l.isdigit()==False: - if len(tmp)>0: + for char in str(text): + if not char.isdigit(): + if len(tmp) > 0: segments.append(int(tmp)) tmp = '' else: - tmp+=l - if len(tmp)>0: + tmp += char + 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: + if len(segments) == 0: + self.print_verbose("No version strings found text: " + str(text)) + if not self._include_branches: 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 and self._json["version_text"]!={}: - 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 + """Called for running check in a background thread""" + is_ready = ( + self._json is not None + and "update_ready" in self._json + and self._json["version_text"] != dict() + and self._json["update_ready"]) + + if is_ready: + 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: + if not self._check_interval_enabled: 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: + elif self._async_checking: + self.print_verbose("Skipping async check, already started") + # already running the bg thread + elif self._update_ready is None: + print("{} updater: Running background check for update".format( + self.addon)) 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") + self.print_verbose( + "Check update pressed, first getting current status") + if self._async_checking: + self.print_verbose("Skipping async check, already started") return # already running the bg thread - elif self._update_ready == None: + elif self._update_ready is 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") + """Check for update not in a syncrhonous manner. + + This function is not async, will always return in sequential fashion + but should have a parent which calls it in another thread. + """ + self.print_verbose("Checking for update function") # clear the errors if any self._error = None @@ -1158,53 +1240,60 @@ 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) + if self._update_ready is not None and not now: + return (self._update_ready, + self._update_version, + self._update_link) - if self._current_version == None: + if self._current_version is None: raise ValueError("current_version not yet defined") - if self._repo == None: + + if self._repo is None: raise ValueError("repo not yet defined") - if self._user == None: + + if self._user is 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") + if not now and not self.past_interval_timestamp(): + self.print_verbose( + "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") + if self._fake_install: + self.print_verbose( + "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) + return (self._update_ready, + self._update_version, + self._update_link) - # primary internet call - self.get_tags() # sets self._tags and self._tag_latest + # Primary internet call, sets self._tags and self._tag_latest. + self.get_tags() self._json["last_check"] = str(datetime.now()) self.save_updater_json() - # can be () or ('master') in addition to branches, and version tag + # Can be () or ('master') in addition to branches, 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) - if self._include_branches == False: + + if not self._include_branches: link = self.select_link(self, self._tags[0]) else: n = len(self._include_branch_list) - if len(self._tags)==n: + if len(self._tags) == n: # effectively means no tags found on repo # so provide the first one as default link = self.select_link(self, self._tags[0]) @@ -1217,27 +1306,24 @@ def check_for_update(self, now=False): 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 + # 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 + # as new_version will only be tag names, not branch names. + if not self._include_branch_auto_check: + # Don't offer update as ready, but set the link for the + # default branch for installing. self._update_ready = False self._update_version = new_version self._update_link = link self.save_updater_json() return (True, new_version, link) else: + # Bypass releases and look at timestamp of last update from a + # branch compared to now, see if commit values match or not. 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 - + # Situation where branches not included. if new_version > self._current_version: self._update_ready = True @@ -1246,20 +1332,12 @@ def check_for_update(self, now=False): 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 + # If no update, set ready to False from None to show it was checked. self._update_ready = False self._update_version = None self._update_link = None return (False, None, None) - def set_tag(self, name): """Assign the tag name and url to update to""" tg = None @@ -1278,10 +1356,9 @@ def set_tag(self, name): self._update_version = name # this will break things self._update_link = link if not tg: - raise ValueError("Version tag not found: "+name) + raise ValueError("Version tag not found: " + name) - - 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): """Runs an install, update, or reversion of an addon from online source Arguments: @@ -1292,9 +1369,9 @@ def run_update(self,force=False,revert_tag=None,clean=False,callback=None): """ self._json["update_ready"] = False self._json["ignore"] = False # clear ignore flag - self._json["version_text"] = {} + self._json["version_text"] = dict() - if revert_tag != None: + if revert_tag is not None: self.set_tag(revert_tag) self._update_ready = True @@ -1302,73 +1379,68 @@ 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") + self.print_verbose("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") + if self._fake_install: + # Change to True, to trigger the reload/"update installed" handler. + self.print_verbose("fake_install=True") + self.print_verbose( + "Just reloading and running any handler triggers") self._json["just_updated"] = True self.save_updater_json() - if self._backup_current == True: + if self._backup_current is True: self.create_backup() self.reload_addon() self._update_ready = False res = True # fake "success" zip download flag - elif force==False: - if self._update_ready != True: - if self._verbose: - print("Update stopped, new version not ready") + elif not force: + if not self._update_ready: + self.print_verbose("Update stopped, new version not ready") if callback: callback( self._addon_package, "Update stopped, new version not ready") return "Update stopped, new version not ready" - elif self._update_link == None: + elif self._update_link is None: # this shouldn't happen if update is ready - if self._verbose: - print("Update stopped, update link unavailable") + self.print_verbose("Update stopped, update link unavailable") if callback: - callback( - self._addon_package, - "Update stopped, update link unavailable") + callback(self._addon_package, + "Update stopped, update link unavailable") return "Update stopped, update link unavailable" - if self._verbose and revert_tag==None: - print("Staging update") - elif self._verbose: - print("Staging install") + if revert_tag is None: + self.print_verbose("Staging update") + else: + self.print_verbose("Staging install") res = self.stage_repository(self._update_link) - if res !=True: - print("Error in staging repository: "+str(res)) - if callback != None: + if not res: + print("Error in staging repository: " + str(res)) + if callback is not None: callback(self._addon_package, self._error_msg) return self._error_msg res = self.unpack_staged_zip(clean) - if res<0: + if res < 0: if callback: callback(self._addon_package, self._error_msg) return res else: - if self._update_link == None: - if self._verbose: - print("Update stopped, could not get link") + if self._update_link is None: + self.print_verbose("Update stopped, could not get link") return "Update stopped, could not get link" - if self._verbose: - print("Forcing update") + self.print_verbose("Forcing update") res = self.stage_repository(self._update_link) - if res !=True: - print("Error in staging repository: "+str(res)) + if not res: + print("Error in staging repository: " + str(res)) if callback: callback(self._addon_package, self._error_msg) return self._error_msg res = self.unpack_staged_zip(clean) - if res<0: + if res < 0: return res # would need to compare against other versions held in tags @@ -1379,44 +1451,40 @@ def run_update(self,force=False,revert_tag=None,clean=False,callback=None): # return something meaningful, 0 means it worked return 0 - def past_interval_timestamp(self): - if self._check_interval_enable == False: + if not self._check_interval_enabled: return True # ie this exact feature is disabled if "last_check" not in self._json or self._json["last_check"] == "": return True now = datetime.now() - last_check = datetime.strptime(self._json["last_check"], - "%Y-%m-%d %H:%M:%S.%f") - next_check = last_check + last_check = datetime.strptime( + self._json["last_check"], "%Y-%m-%d %H:%M:%S.%f") 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 - ) + 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)) + self.print_verbose("Time to check for updates!") return True - if self._verbose: - print("{} Updater: Determined it's not yet time to check for updates".format(self._addon)) + self.print_verbose("Determined it's not yet time to check for updates") return False def get_json_path(self): """Returns the full path to the JSON state file used by this updater. - Will also rename old file paths to addon-specific path if found + Will also rename old file paths to addon-specific path if found. """ - json_path = os.path.join(self._updater_path, + json_path = os.path.join( + self._updater_path, "{}_updater_status.json".format(self._addon_package)) old_json_path = os.path.join(self._updater_path, "updater_status.json") - # rename old file if it exists + # Rename old file if it exists. try: os.rename(old_json_path, json_path) except FileNotFoundError: @@ -1424,90 +1492,89 @@ def get_json_path(self): except Exception as err: print("Other OS error occurred while trying to rename old JSON") print(err) + self.print_trace() return json_path def set_updater_json(self): """Load or initialize JSON dictionary data for updater state""" - if self._updater_path == None: + if self._updater_path is None: raise ValueError("updater_path is not defined") - elif os.path.isdir(self._updater_path) == False: + elif not os.path.isdir(self._updater_path): os.makedirs(self._updater_path) jpath = self.get_json_path() 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)) + self.print_verbose("Read in JSON settings from file") 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": dict() } 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)): + """Trigger save of current json structure into file within addon""" + if self._update_ready: + if isinstance(self._update_version, tuple): self._json["update_ready"] = True - self._json["version_text"]["link"]=self._update_link - self._json["version_text"]["version"]=self._update_version + 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"] = {} + self._json["version_text"] = dict() else: self._json["update_ready"] = False - self._json["version_text"] = {} + self._json["version_text"] = dict() jpath = self.get_json_path() - 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) + if not os.path.isdir(os.path.dirname(jpath)): + print("State error: Directory does not exist, cannot save json: ", + os.path.basename(jpath)) + return + try: + with open(jpath, 'w') as outf: + data_out = json.dumps(self._json, indent=4) + outf.write(data_out) + except: + print("Failed to open/save data to json: ", jpath) + self.print_trace() + self.print_verbose("Wrote out updater JSON settings with content:") + self.print_verbose(str(self._json)) def json_reset_postupdate(self): self._json["just_updated"] = False self._json["update_ready"] = False - self._json["version_text"] = {} + self._json["version_text"] = dict() self.save_updater_json() def json_reset_restore(self): self._json["just_restored"] = False self._json["update_ready"] = False - self._json["version_text"] = {} + self._json["version_text"] = dict() 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 self.save_updater_json() - # ------------------------------------------------------------------------- - # ASYNC stuff + # ASYNC related methods # ------------------------------------------------------------------------- - def start_async_check_update(self, now=False, callback=None): """Start a background thread which will check for updates""" - if self._async_checking is True: + if self._async_checking: return - if self._verbose: - print("{} updater: Starting background checking thread".format( - self._addon)) + self.print_verbose("Starting background checking thread") 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() @@ -1515,15 +1582,14 @@ def start_async_check_update(self, now=False, callback=None): def async_check_update(self, now, callback=None): """Perform update check, run as target of background thread""" self._async_checking = True - if self._verbose: - print("{} BG thread: Checking for update now in background".format( - self._addon)) + self.print_verbose("Checking for update now in background") try: self.check_for_update(now=now) except Exception as exception: print("Checking for update error:") print(exception) + self.print_trace() if not self._error: self._update_ready = False self._update_version = None @@ -1534,10 +1600,10 @@ def async_check_update(self, now, callback=None): self._async_checking = False self._check_thread = None - if self._verbose: - print("{} BG thread: Finished checking for update, doing callback".format(self._addon)) if callback: + self.print_verbose("Finished check update, doing callback") callback(self._update_ready) + self.print_verbose("BG thread: Finished check update, no callback") def stop_async_check_update(self): """Method to give impression of stopping check for update. @@ -1548,11 +1614,11 @@ def stop_async_check_update(self): does complete with a successful response, this will be still displayed on next UI refresh (ie no update, or update available). """ - if self._check_thread != None: - if self._verbose: print("Thread will end in normal course.") + if self._check_thread is not None: + self.print_verbose("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() + # self._check_thread.stop() self._async_checking = False self._error = None self._error_msg = None @@ -1563,7 +1629,7 @@ def stop_async_check_update(self): # ----------------------------------------------------------------------------- -class BitbucketEngine(object): +class BitbucketEngine: """Integration to Bitbucket API for git-formatted repositories""" def __init__(self): @@ -1572,7 +1638,8 @@ def __init__(self): self.name = "bitbucket" def form_repo_url(self, updater): - return self.api_url+"/2.0/repositories/"+updater.user+"/"+updater.repo + return "{}/2.0/repositories/{}/{}".format( + self.api_url, updater.user, updater.repo) def form_tags_url(self, updater): return self.form_repo_url(updater) + "/refs/tags?sort=-name" @@ -1587,12 +1654,16 @@ def get_zip_url(self, name, updater): name=name) def parse_tags(self, response, updater): - if response == None: - return [] - return [{"name": tag["name"], "zipball_url": self.get_zip_url(tag["name"], updater)} for tag in response["values"]] + if response is None: + return list() + return [ + { + "name": tag["name"], + "zipball_url": self.get_zip_url(tag["name"], updater) + } for tag in response["values"]] -class GithubEngine(object): +class GithubEngine: """Integration to Github API""" def __init__(self): @@ -1601,29 +1672,28 @@ def __init__(self): self.name = "github" def form_repo_url(self, updater): - return "{}{}{}{}{}".format(self.api_url,"/repos/",updater.user, - "/",updater.repo) + return "{}/repos/{}/{}".format( + self.api_url, updater.user, updater.repo) def form_tags_url(self, updater): if updater.use_releases: - return "{}{}".format(self.form_repo_url(updater),"/releases") + return "{}/releases".format(self.form_repo_url(updater)) else: - return "{}{}".format(self.form_repo_url(updater),"/tags") + return "{}/tags".format(self.form_repo_url(updater)) def form_branch_list_url(self, updater): - return "{}{}".format(self.form_repo_url(updater),"/branches") + return "{}/branches".format(self.form_repo_url(updater)) def form_branch_url(self, branch, updater): - return "{}{}{}".format(self.form_repo_url(updater), - "/zipball/",branch) + return "{}/zipball/{}".format(self.form_repo_url(updater), branch) def parse_tags(self, response, updater): - if response == None: - return [] + if response is None: + return list() return response -class GitlabEngine(object): +class GitlabEngine: """Integration to GitLab API""" def __init__(self): @@ -1632,25 +1702,21 @@ def __init__(self): self.name = "gitlab" def form_repo_url(self, updater): - return "{}{}{}".format(self.api_url,"/api/v4/projects/",updater.repo) + return "{}/api/v4/projects/{}".format(self.api_url, updater.repo) def form_tags_url(self, updater): - return "{}{}".format(self.form_repo_url(updater),"/repository/tags") + return "{}/repository/tags".format(self.form_repo_url(updater)) def form_branch_list_url(self, updater): # does not validate branch name. - return "{}{}".format( - self.form_repo_url(updater), - "/repository/branches") + return "{}/repository/branches".format( + self.form_repo_url(updater)) def form_branch_url(self, branch, updater): - # Could clash with tag names and if it does, it will - # download TAG zip instead of branch zip to get - # direct path, would need. - return "{}{}{}".format( - self.form_repo_url(updater), - "/repository/archive.zip?sha=", - branch) + # Could clash with tag names and if it does, it will download TAG zip + # instead of branch zip to get direct path, would need. + return "{}/repository/archive.zip?sha={}".format( + self.form_repo_url(updater), branch) def get_zip_url(self, sha, updater): return "{base}/repository/archive.zip?sha={sha}".format( @@ -1661,9 +1727,13 @@ def get_zip_url(self, sha, updater): # return self.form_repo_url(updater)+"/repository/archive.zip?sha:"+id def parse_tags(self, response, updater): - if response == None: - return [] - return [{"name": tag["name"], "zipball_url": self.get_zip_url(tag["commit"]["id"], updater)} for tag in response] + if response is None: + return list() + return [ + { + "name": tag["name"], + "zipball_url": self.get_zip_url(tag["commit"]["id"], updater) + } for tag in response] # ----------------------------------------------------------------------------- @@ -1671,4 +1741,4 @@ def parse_tags(self, response, updater): # should be what's imported to other files # ----------------------------------------------------------------------------- -Updater = Singleton_updater() +Updater = SingletonUpdater() diff --git a/MCprep_addon/addon_updater_ops.py b/MCprep_addon/addon_updater_ops.py index 4da5a764..ba1c39d8 100644 --- a/MCprep_addon/addon_updater_ops.py +++ b/MCprep_addon/addon_updater_ops.py @@ -16,63 +16,80 @@ # # ##### END GPL LICENSE BLOCK ##### +"""Blender UI integrations for the addon updater. + +Implements draw calls, popups, and operators that use the addon_updater. +""" + import os +import traceback import bpy from bpy.app.handlers import persistent from . import tracking -# updater import, import safely +# Safely import the updater. # Prevents popups for users with invalid python installs e.g. missing libraries +# and will replace with a fake class instead if it fails (so UI draws work). try: from .addon_updater import Updater as updater except Exception as e: print("ERROR INITIALIZING UPDATER") print(str(e)) - class Singleton_updater_none(object): + traceback.print_exc() + + class SingletonUpdaterNone(object): + """Fake, bare minimum fields and functions for the updater object.""" + def __init__(self): + self.invalid_updater = True # Used to distinguish bad install. + self.addon = None self.verbose = False - self.invalidupdater = True # used to distinguish bad install + self.use_print_traces = True self.error = None self.error_msg = None self.async_checking = None + def clear_state(self): self.addon = None self.verbose = False - self.invalidupdater = True + self.invalid_updater = True self.error = None self.error_msg = None self.async_checking = None - def run_update(self): pass - def check_for_update(self): pass - updater = Singleton_updater_none() + + def run_update(self, force, callback, clean): + pass + + def check_for_update(self, now): + pass + + updater = SingletonUpdaterNone() updater.error = "Error initializing updater module" updater.error_msg = str(e) -# Must declare this before classes are loaded -# otherwise the bl_idname's will not match and have errors. -# Must be all lowercase and no spaces +# Must declare this before classes are loaded, otherwise the bl_idname's will +# not match and have errors. Must be all lowercase and no spaces! Should also +# be unique among any other addons that could exist (using this updater code), +# to avoid clashes in operator registration. updater.addon = "mcprep" # ----------------------------------------------------------------------------- # Blender version utils # ----------------------------------------------------------------------------- - - def make_annotations(cls): - """Add annotation attribute to class fields to avoid Blender 2.8 warnings""" + """Add annotation attribute to fields to avoid Blender 2.8+ warnings""" if not hasattr(bpy.app, "version") or bpy.app.version < (2, 80): return cls if bpy.app.version < (2, 93, 0): - bl_props = { - k: v for k, v in cls.__dict__.items() if isinstance(v, tuple)} + bl_props = {k: v for k, v in cls.__dict__.items() + if isinstance(v, tuple)} else: - bl_props = { - k: v for k, v in cls.__dict__.items() - if isinstance(v, bpy.props._PropertyDeferred)} + bl_props = {k: v for k, v in cls.__dict__.items() + if isinstance(v, bpy.props._PropertyDeferred)} if bl_props: if '__annotations__' not in cls.__dict__: setattr(cls, '__annotations__', {}) @@ -111,12 +128,12 @@ def get_user_preferences(context=None): # ----------------------------------------------------------------------------- -# simple popup for prompting checking for update & allow to install if available -class addon_updater_install_popup(bpy.types.Operator): +# Simple popup to prompt use to check for update & offer install if available. +class AddonUpdaterInstallPopup(bpy.types.Operator): """Check and install update if available""" bl_label = "Update {x} addon".format(x=updater.addon) - bl_idname = updater.addon+".updater_install_popup" - bl_description = "Popup menu to check and display current updates available" + bl_idname = updater.addon + ".updater_install_popup" + bl_description = "Popup to check and display current updates available" bl_options = {'REGISTER', 'INTERNAL'} # if true, run clean install - ie remove all files before adding new @@ -124,22 +141,24 @@ class addon_updater_install_popup(bpy.types.Operator): # updater folder/backup folder remains clean_install = bpy.props.BoolProperty( name="Clean install", - description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install", + description=("If enabled, completely clear the addon's folder before " + "installing new update, creating a fresh install"), default=False, options={'HIDDEN'} ) + ignore_enum = bpy.props.EnumProperty( name="Process update", description="Decide to install, ignore, or defer new addon update", items=[ - ("install","Update Now","Install update now"), - ("ignore","Ignore", "Ignore this update to prevent future popups"), - ("defer","Defer","Defer choice till next blender session") + ("install", "Update Now", "Install update now"), + ("ignore", "Ignore", "Ignore this update to prevent future popups"), + ("defer", "Defer", "Defer choice till next blender session") ], options={'HIDDEN'} ) - def check (self, context): + def check(self, context): return True def invoke(self, context, event): @@ -147,134 +166,128 @@ def invoke(self, context, event): def draw(self, context): layout = self.layout - if updater.invalidupdater == True: + if updater.invalid_updater: layout.label(text="Updater module error") return - elif updater.update_ready == True: + elif updater.update_ready: col = layout.column() col.scale_y = 0.7 - col.label(text="Update {} ready!".format(str(updater.update_version)), - icon="LOOP_FORWARDS") - col.label(text="Choose 'Update Now' & press OK to install, ",icon="BLANK1") - col.label(text="or click outside window to defer",icon="BLANK1") + col.label(text="Update {} ready!".format(updater.update_version), + icon="LOOP_FORWARDS") + col.label(text="Choose 'Update Now' & press OK to install, ", + icon="BLANK1") + col.label(text="or click outside window to defer", icon="BLANK1") row = col.row() - row.prop(self,"ignore_enum",expand=True) + row.prop(self, "ignore_enum", expand=True) col.split() - elif updater.update_ready == False: + elif not updater.update_ready: col = layout.column() col.scale_y = 0.7 col.label(text="No updates available") col.label(text="Press okay to dismiss dialog") # add option to force install else: - # case: updater.update_ready = None - # we have not yet checked for the update + # Case: updater.update_ready = None + # we have not yet checked for the update. layout.label(text="Check for update now?") - # potentially in future, could have UI for 'check to select old version' - # to revert back to. + # Potentially in future, UI to 'check to select/revert to old version'. track_function = "install_update_popup" track_param = None @tracking.report_error - def execute(self,context): - - # in case of error importing updater - if updater.invalidupdater == True: + def execute(self, context): + # In case of error importing updater. + if updater.invalid_updater: return {'CANCELLED'} - if updater.manual_only==True: + if updater.manual_only: bpy.ops.wm.url_open(url=updater.website) - elif updater.update_ready == True: + elif updater.update_ready: - self.track_param = str(updater.update_version) - if len(self.track_param)>13: - self.track_param = self.track_param[:13] - - # action based on enum selection - if self.ignore_enum=='defer': + # Action based on enum selection. + if self.ignore_enum == 'defer': return {'FINISHED'} - elif self.ignore_enum=='ignore': + elif self.ignore_enum == 'ignore': updater.ignore_update() return {'FINISHED'} - #else: "install update now!" - res = updater.run_update( - force=False, - callback=post_update_callback, - clean=self.clean_install) - # should return 0, if not something happened + res = updater.run_update(force=False, + callback=post_update_callback, + clean=self.clean_install) + + # Should return 0, if not something happened. if updater.verbose: - if res==0: + if res == 0: print("Updater returned successful") else: print("Updater returned {}, error occurred".format(res)) - elif updater.update_ready == None: + elif updater.update_ready is None: _ = 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') + # Re-launch this dialog. + atr = AddonUpdaterInstallPopup.bl_idname.split(".") + getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') else: - if updater.verbose: - print("Doing nothing, not ready for update") + updater.print_verbose("Doing nothing, not ready for update") return {'FINISHED'} # User preference check-now operator -class addon_updater_check_now(bpy.types.Operator): - bl_label = "Check now for "+updater.addon+" update" - bl_idname = updater.addon+".updater_check_now" - bl_description = "Check now for an update to the {x} addon".format( - x=updater.addon) +class AddonUpdaterCheckNow(bpy.types.Operator): + bl_label = "Check now for " + updater.addon + " update" + bl_idname = updater.addon + ".updater_check_now" + bl_description = "Check now for an update to the {} addon".format( + updater.addon) bl_options = {'REGISTER', 'INTERNAL'} @tracking.report_error - def execute(self,context): - if updater.invalidupdater == True: + def execute(self, context): + if updater.invalid_updater: 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 error, to prevent being stuck on the error screen + if updater.async_checking and updater.error is None: + # Check already happened. + # Used here to just avoid constant applying settings below. + # Ignoring if error, to prevent being stuck on the error screen. return {'CANCELLED'} # apply the UI settings settings = get_user_preferences(context) if not settings: - if updater.verbose: - print("Could not get {} preferences, update check skipped".format( + updater.print_verbose( + "Could not get {} preferences, update check skipped".format( __package__)) return {'CANCELLED'} - updater.set_check_interval(enable=settings.auto_check_update, - months=settings.updater_intrval_months, - days=settings.updater_intrval_days, - hours=settings.updater_intrval_hours, - minutes=settings.updater_intrval_minutes - ) # optional, if auto_check_update - - # input is an optional callback function - # this function should take a bool input, if true: update ready - # if false, no update ready + + updater.set_check_interval( + enabled=settings.auto_check_update, + months=settings.updater_interval_months, + days=settings.updater_interval_days, + hours=settings.updater_interval_hours, + minutes=settings.updater_interval_minutes) + + # Input is an optional callback function. This function should take a + # bool input. If true: update ready, if false: no update ready. updater.check_for_update_now(ui_refresh) return {'FINISHED'} -class addon_updater_update_now(bpy.types.Operator): - bl_label = "Update "+updater.addon+" addon now" - bl_idname = updater.addon+".updater_update_now" +class AddonUpdaterUpdateNow(bpy.types.Operator): + bl_label = "Update " + updater.addon + " addon now" + bl_idname = updater.addon + ".updater_update_now" bl_description = "Update to the latest version of the {x} addon".format( - x=updater.addon) + x=updater.addon) bl_options = {'REGISTER', 'INTERNAL'} - # if true, run clean install - ie remove all files before adding new - # equivalent to deleting the addon and reinstalling, except the - # updater folder/backup folder remains + # If true, run clean install - ie remove all files before adding new + # equivalent to deleting the addon and reinstalling, except the updater + # folder/backup folder remains. clean_install = bpy.props.BoolProperty( name="Clean install", - description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install", + description=("If enabled, completely clear the addon's folder before " + "installing new update, creating a fresh install"), default=False, options={'HIDDEN'} ) @@ -282,143 +295,143 @@ class addon_updater_update_now(bpy.types.Operator): track_function = "install_update" track_param = None @tracking.report_error - def execute(self,context): + def execute(self, context): # in case of error importing updater - if updater.invalidupdater == True: + if updater.invalid_updater: return {'CANCELLED'} - if updater.manual_only == True: + if updater.manual_only: bpy.ops.wm.url_open(url=updater.website) - if updater.update_ready == True: + if updater.update_ready: # if it fails, offer to open the website instead - self.track_param = str(updater.update_version) - if len(self.track_param)>13: - self.track_param = self.track_param[:13] try: - res = updater.run_update( - force=False, - callback=post_update_callback, - clean=self.clean_install) + res = updater.run_update(force=False, + callback=post_update_callback, + clean=self.clean_install) - # should return 0, if not something happened + # Should return 0, if not something happened. if updater.verbose: - if res==0: print("Updater returned successful") - else: print("Updater returned "+str(res)+", error occurred") - except Exception as e: + if res == 0: + print("Updater returned successful") + else: + print("Updater error response: {}".format(res)) + except Exception as expt: updater._error = "Error trying to run update" - updater._error_msg = str(e) - atr = addon_updater_install_manually.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT') - elif updater.update_ready == None: + updater._error_msg = str(expt) + updater.print_trace() + atr = AddonUpdaterInstallManually.bl_idname.split(".") + getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') + elif updater.update_ready is 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') + # Re-launch this dialog. + atr = AddonUpdaterInstallPopup.bl_idname.split(".") + getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') - elif updater.update_ready == False: + elif not updater.update_ready: self.report({'INFO'}, "Nothing to update") return {'CANCELLED'} else: - self.report({'ERROR'}, "Encountered problem while trying to update") + self.report( + {'ERROR'}, "Encountered a problem while trying to update") return {'CANCELLED'} return {'FINISHED'} -class addon_updater_update_target(bpy.types.Operator): - bl_label = updater.addon+" version target" - bl_idname = updater.addon+".updater_update_target" +class AddonUpdaterUpdateTarget(bpy.types.Operator): + bl_label = updater.addon + " version target" + bl_idname = updater.addon + ".updater_update_target" bl_description = "Install a targeted version of the {x} addon".format( - x=updater.addon) + x=updater.addon) bl_options = {'REGISTER', 'INTERNAL'} def target_version(self, context): - # in case of error importing updater - if updater.invalidupdater == True: + # In case of error importing updater. + if updater.invalid_updater: ret = [] ret = [] - i=0 + i = 0 for tag in updater.tags: - ret.append( (tag,tag,"Select to install "+tag) ) - i+=1 + ret.append((tag, tag, "Select to install " + tag)) + i += 1 return ret target = bpy.props.EnumProperty( name="Target version to install", description="Select the version to install", items=target_version - ) + ) - # if true, run clean install - ie remove all files before adding new + # If true, run clean install - ie remove all files before adding new # equivalent to deleting the addon and reinstalling, except the - # updater folder/backup folder remains + # updater folder/backup folder remains. clean_install = bpy.props.BoolProperty( name="Clean install", - description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install", + description=("If enabled, completely clear the addon's folder before " + "installing new update, creating a fresh install"), default=False, options={'HIDDEN'} ) @classmethod def poll(cls, context): - if updater.invalidupdater == True: return False - return updater.update_ready != None and len(updater.tags)>0 + if updater.invalid_updater: + return False + return updater.update_ready is not None and len(updater.tags) > 0 def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) def draw(self, context): layout = self.layout - if updater.invalidupdater == True: + if updater.invalid_updater: layout.label(text="Updater error") return split = layout_split(layout, factor=0.5) - subcol = split.column() - subcol.label(text="Select install version") - subcol = split.column() - subcol.prop(self, "target", text="") + sub_col = split.column() + sub_col.label(text="Select install version") + sub_col = split.column() + sub_col.prop(self, "target", text="") track_function = "install_update_target" track_param = None @tracking.report_error - def execute(self,context): - - # in case of error importing updater - if updater.invalidupdater == True: + def execute(self, context): + # In case of error importing updater. + if updater.invalid_updater: return {'CANCELLED'} self.track_param = str(self.target) - if len(self.track_param)>13: + if len(self.track_param) > 13: self.track_param = self.track_param[:13] res = updater.run_update( - force=False, - revert_tag=self.target, - callback=post_update_callback, - clean=self.clean_install) - - # should return 0, if not something happened - if res==0: - if updater.verbose: - print("Updater returned successful") + force=False, + revert_tag=self.target, + callback=post_update_callback, + clean=self.clean_install) + + # Should return 0, if not something happened. + if res == 0: + updater.print_verbose("Updater returned successful") else: - if updater.verbose: - print("Updater returned "+str(res)+", error occurred") + updater.print_verbose( + "Updater returned {}, , error occurred".format(res)) return {'CANCELLED'} self.track_param = str(self.target) - if len(self.track_param)>13: + if len(self.track_param) > 13: self.track_param = self.track_param[:13] return {'FINISHED'} -class addon_updater_install_manually(bpy.types.Operator): +class AddonUpdaterInstallManually(bpy.types.Operator): """As a fallback, direct the user to download the addon manually""" bl_label = "Install update manually" - bl_idname = updater.addon+".updater_install_manually" + bl_idname = updater.addon + ".updater_install_manually" bl_description = "Proceed to manually install update" bl_options = {'REGISTER', 'INTERNAL'} @@ -426,7 +439,7 @@ class addon_updater_install_manually(bpy.types.Operator): name="Error Occurred", default="", options={'HIDDEN'} - ) + ) def invoke(self, context, event): return context.window_manager.invoke_popup(self) @@ -434,17 +447,19 @@ def invoke(self, context, event): def draw(self, context): layout = self.layout - if updater.invalidupdater == True: + if updater.invalid_updater: layout.label(text="Updater error") return - # use a "failed flag"? it shows this label if the case failed. - if self.error!="": + # Display error if a prior autoamted install failed. + if self.error != "": col = layout.column() col.scale_y = 0.7 - col.label(text="There was an issue trying to auto-install",icon="ERROR") - col.label(text="Press the download button below and install",icon="BLANK1") - col.label(text="the zip file like a normal addon.",icon="BLANK1") + col.label(text="There was an issue trying to auto-install", + icon="ERROR") + col.label(text="Press the download button below and install", + icon="BLANK1") + col.label(text="the zip file like a normal addon.", icon="BLANK1") else: col = layout.column() col.scale_y = 0.7 @@ -452,35 +467,37 @@ def draw(self, context): col.label(text="Press the download button below and install") col.label(text="the zip file like a normal addon.") - # if check hasn't happened, i.e. accidentally called this menu - # allow to check here + # If check hasn't happened, i.e. accidentally called this menu, + # allow to check here. row = layout.row() - if updater.update_link != None: - row.operator("wm.url_open", - text="Direct download").url=updater.update_link + if updater.update_link is not None: + row.operator( + "wm.url_open", + text="Direct download").url = updater.update_link else: - row.operator("wm.url_open", + row.operator( + "wm.url_open", text="(failed to retrieve direct download)") row.enabled = False - if updater.website != None: + if updater.website is not None: row = layout.row() - row.operator("wm.url_open",text="Open website").url=\ - updater.website + ops = row.operator("wm.url_open", text="Open website") + ops.url = updater.website else: row = layout.row() row.label(text="See source website to download the update") - def execute(self,context): + def execute(self, context): return {'FINISHED'} -class addon_updater_updated_successful(bpy.types.Operator): +class AddonUpdaterUpdatedSuccessful(bpy.types.Operator): """Addon in place, popup telling user it completed or what went wrong""" bl_label = "Installation Report" - bl_idname = updater.addon+".updater_update_successful" + bl_idname = updater.addon + ".updater_update_successful" bl_description = "Update installation response" bl_options = {'REGISTER', 'INTERNAL', 'UNDO'} @@ -488,7 +505,7 @@ class addon_updater_updated_successful(bpy.types.Operator): name="Error Occurred", default="", options={'HIDDEN'} - ) + ) def invoke(self, context, event): return context.window_manager.invoke_props_popup(self, event) @@ -496,7 +513,7 @@ def invoke(self, context, event): def draw(self, context): layout = self.layout - if updater.invalidupdater == True: + if updater.invalid_updater: layout.label(text="Updater error") return @@ -512,14 +529,13 @@ def draw(self, context): col.label(text=str(msg), icon="BLANK1") rw = col.row() rw.scale_y = 2 - rw.operator("wm.url_open", + rw.operator( + "wm.url_open", text="Click for manual download.", - icon="BLANK1" - ).url=updater.website - # manual download button here - elif updater.auto_reload_post_update == False: - # tell user to restart blender - if "just_restored" in saved and saved["just_restored"] == True: + icon="BLANK1").url = updater.website + elif not updater.auto_reload_post_update: + # Tell user to restart blender after an update/restore! + if "just_restored" in saved and saved["just_restored"]: col = layout.column() col.label(text="Addon restored", icon="RECOVER_LAST") alert_row = col.row() @@ -531,7 +547,8 @@ def draw(self, context): updater.json_reset_restore() else: col = layout.column() - col.label(text="Addon successfully installed", icon="FILE_TICK") + col.label( + text="Addon successfully installed", icon="FILE_TICK") alert_row = col.row() alert_row.alert = True alert_row.operator( @@ -541,59 +558,62 @@ def draw(self, context): else: # reload addon, but still recommend they restart blender - if "just_restored" in saved and saved["just_restored"] == True: + if "just_restored" in saved and saved["just_restored"]: col = layout.column() col.scale_y = 0.7 col.label(text="Addon restored", icon="RECOVER_LAST") - col.label(text="Consider restarting blender to fully reload.", + col.label( + text="Consider restarting blender to fully reload.", icon="BLANK1") updater.json_reset_restore() else: col = layout.column() col.scale_y = 0.7 - col.label(text="Addon successfully installed", icon="FILE_TICK") - col.label(text="Consider restarting blender to fully reload.", + col.label( + text="Addon successfully installed", icon="FILE_TICK") + col.label( + text="Consider restarting blender to fully reload.", icon="BLANK1") def execute(self, context): return {'FINISHED'} -class addon_updater_restore_backup(bpy.types.Operator): +class AddonUpdaterRestoreBackup(bpy.types.Operator): """Restore addon from backup""" bl_label = "Restore backup" - bl_idname = updater.addon+".updater_restore_backup" + bl_idname = updater.addon + ".updater_restore_backup" bl_description = "Restore addon from backup" bl_options = {'REGISTER', 'INTERNAL'} @classmethod def poll(cls, context): try: - return os.path.isdir(os.path.join(updater.stage_path,"backup")) + return os.path.isdir(os.path.join(updater.stage_path, "backup")) except: return False @tracking.report_error def execute(self, context): # in case of error importing updater - if updater.invalidupdater == True: + if updater.invalid_updater: return {'CANCELLED'} updater.restore_backup() return {'FINISHED'} -class addon_updater_ignore(bpy.types.Operator): - """Prevent future update notice popups""" +class AddonUpdaterIgnore(bpy.types.Operator): + """Ignore update to prevent future popups""" bl_label = "Ignore update" - bl_idname = updater.addon+".updater_ignore" + bl_idname = updater.addon + ".updater_ignore" bl_description = "Ignore update to prevent future popups" bl_options = {'REGISTER', 'INTERNAL'} @classmethod def poll(cls, context): - if updater.invalidupdater == True: + if updater.invalid_updater: return False - elif updater.update_ready == True: + elif updater.update_ready: return True else: return False @@ -601,30 +621,23 @@ def poll(cls, context): @tracking.report_error def execute(self, context): # in case of error importing updater - if updater.invalidupdater == True: + if updater.invalid_updater: return {'CANCELLED'} updater.ignore_update() - self.report({"INFO"},"Open addon preferences for updater options") + self.report({"INFO"}, "Open addon preferences for updater options") return {'FINISHED'} -class addon_updater_end_background(bpy.types.Operator): +class AddonUpdaterEndBackground(bpy.types.Operator): """Stop checking for update in the background""" bl_label = "End background check" - bl_idname = updater.addon+".end_background_check" + bl_idname = updater.addon + ".end_background_check" bl_description = "Stop checking for update in the background" bl_options = {'REGISTER', 'INTERNAL'} - # @classmethod - # def poll(cls, context): - # if updater.async_checking == True: - # return True - # else: - # return False - def execute(self, context): # in case of error importing updater - if updater.invalidupdater == True: + if updater.invalid_updater: return {'CANCELLED'} updater.stop_async_check_update() return {'FINISHED'} @@ -636,144 +649,143 @@ def execute(self, context): # global vars used to prevent duplicate popup handlers -ran_autocheck_install_popup = False -ran_update_sucess_popup = False +ran_auto_check_install_popup = False +ran_update_success_popup = False # global var for preventing successive calls ran_background_check = False + @persistent def updater_run_success_popup_handler(scene): - global ran_update_sucess_popup - ran_update_sucess_popup = True + global ran_update_success_popup + ran_update_success_popup = True # in case of error importing updater - if updater.invalidupdater == True: + if updater.invalid_updater: return try: if "scene_update_post" in dir(bpy.app.handlers): bpy.app.handlers.scene_update_post.remove( - updater_run_success_popup_handler) + updater_run_success_popup_handler) else: bpy.app.handlers.depsgraph_update_post.remove( - updater_run_success_popup_handler) + updater_run_success_popup_handler) except: pass - atr = addon_updater_updated_successful.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT') + atr = AddonUpdaterUpdatedSuccessful.bl_idname.split(".") + getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') @persistent def updater_run_install_popup_handler(scene): - global ran_autocheck_install_popup - ran_autocheck_install_popup = True + global ran_auto_check_install_popup + ran_auto_check_install_popup = True + updater.print_verbose("Running the install popup handler.") # in case of error importing updater - if updater.invalidupdater == True: + if updater.invalid_updater: return try: if "scene_update_post" in dir(bpy.app.handlers): bpy.app.handlers.scene_update_post.remove( - updater_run_install_popup_handler) + updater_run_install_popup_handler) else: bpy.app.handlers.depsgraph_update_post.remove( - updater_run_install_popup_handler) + updater_run_install_popup_handler) except: pass - 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"]: + if "ignore" in updater.json and updater.json["ignore"]: + return # Don't do popup if ignore pressed. + elif "version_text" in updater.json and updater.json["version_text"].get("version"): 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)) + # User probably manually installed to get the up to date addon + # in here. Clear out the update flag using this function. + updater.print_verbose( + "{} 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') + atr = AddonUpdaterInstallPopup.bl_idname.split(".") + getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') def background_update_callback(update_ready): """Passed into the updater, background thread updater""" - global ran_autocheck_install_popup + global ran_auto_check_install_popup + updater.print_verbose("Running background update callback") - # in case of error importing updater - if updater.invalidupdater == True: + # In case of error importing updater. + if updater.invalid_updater: return - if updater.showpopups == False: + if not updater.show_popups: return - if update_ready != True: + if not update_ready: return - # see if we need add to the update handler to trigger the popup + # See if we need add to the update handler to trigger the popup. handlers = [] - if "scene_update_post" in dir(bpy.app.handlers): # 2.7x + if "scene_update_post" in dir(bpy.app.handlers): # 2.7x handlers = bpy.app.handlers.scene_update_post - else: # 2.8x + else: # 2.8+ handlers = bpy.app.handlers.depsgraph_update_post in_handles = updater_run_install_popup_handler in handlers - if in_handles or ran_autocheck_install_popup: + if in_handles or ran_auto_check_install_popup: return - if "scene_update_post" in dir(bpy.app.handlers): # 2.7x + if "scene_update_post" in dir(bpy.app.handlers): # 2.7x bpy.app.handlers.scene_update_post.append( - updater_run_install_popup_handler) - else: # 2.8x + updater_run_install_popup_handler) + else: # 2.8+ bpy.app.handlers.depsgraph_update_post.append( - updater_run_install_popup_handler) - ran_autocheck_install_popup = True + updater_run_install_popup_handler) + ran_auto_check_install_popup = True + updater.print_verbose("Attempted popup prompt") def post_update_callback(module_name, res=None): - """Callback for once the run_update function has completed + """Callback for once the run_update function has completed. Only makes sense to use this if "auto_reload_post_update" == False, - i.e. don't auto-restart the addon + i.e. don't auto-restart the addon. Arguments: - module_name: returns the module name from updater, but unused here - res: If an error occurred, this is the detail string + module_name: returns the module name from updater, but unused here. + res: If an error occurred, this is the detail string. """ - # in case of error importing updater - if updater.invalidupdater == True: + # In case of error importing updater. + if updater.invalid_updater: return - if res==None: - # 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("{} updater: Running post update callback".format(updater.addon)) + if res is None: + # This is the same code as in conditional at the end of the register + # function, ie if "auto_reload_post_update" == True, skip code. + updater.print_verbose( + "{} updater: Running post update callback".format(updater.addon)) - atr = addon_updater_updated_successful.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT') - global ran_update_sucess_popup - ran_update_sucess_popup = True + atr = AddonUpdaterUpdatedSuccessful.bl_idname.split(".") + getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') + global ran_update_success_popup + ran_update_success_popup = True else: - # some kind of error occurred and it was unable to install, - # offer manual download instead - atr = addon_updater_updated_successful.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT',error=res) + # Some kind of error occurred and it was unable to install, offer + # manual download instead. + atr = AddonUpdaterUpdatedSuccessful.bl_idname.split(".") + getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT', error=res) return def ui_refresh(update_status): - # find a way to just re-draw self? - # callback intended for trigger by async thread + """Redraw the ui once an async thread has completed""" for windowManager in bpy.data.window_managers: for window in windowManager.windows: for area in window.screen.areas: @@ -783,131 +795,124 @@ def ui_refresh(update_status): def check_for_update_background(): """Function for asynchronous background check. - *Could* be called on register, but would be bad practice. + *Could* be called on register, but would be bad practice as the bare + minimum code should run at the moment of registration (addon ticked). """ - if updater.invalidupdater == True: + if updater.invalid_updater: return global ran_background_check - if ran_background_check == True: - # Global var ensures check only happens once + if ran_background_check: + # Global var ensures check only happens once. return - elif updater.update_ready != None or updater.async_checking == True: - # Check already happened - # Used here to just avoid constant applying settings below + elif updater.update_ready is not None or updater.async_checking: + # Check already happened. + # Used here to just avoid constant applying settings below. return - # apply the UI settings + # Apply the UI settings. settings = get_user_preferences(bpy.context) if not settings: return - updater.set_check_interval(enable=settings.auto_check_update, - months=settings.updater_intrval_months, - days=settings.updater_intrval_days, - hours=settings.updater_intrval_hours, - minutes=settings.updater_intrval_minutes - ) # optional, if auto_check_update - - # 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("{} updater: Running background check for update".format(\ - updater.addon)) + updater.set_check_interval(enabled=settings.auto_check_update, + months=settings.updater_interval_months, + days=settings.updater_interval_days, + hours=settings.updater_interval_hours, + minutes=settings.updater_interval_minutes) + + # Input is an optional callback function. This function should take a bool + # input, if true: update ready, if false: no update ready. updater.check_for_update_async(background_update_callback) ran_background_check = True def check_for_update_nonthreaded(self, context): """Can be placed in front of other operators to launch when pressed""" - if updater.invalidupdater == True: + if updater.invalid_updater: return - # only check if it's ready, ie after the time interval specified - # should be the async wrapper call here + # Only check if it's ready, ie after the time interval specified should + # be the async wrapper call here. settings = get_user_preferences(bpy.context) if not settings: if updater.verbose: print("Could not get {} preferences, update check skipped".format( __package__)) return - updater.set_check_interval(enable=settings.auto_check_update, - months=settings.updater_intrval_months, - days=settings.updater_intrval_days, - hours=settings.updater_intrval_hours, - minutes=settings.updater_intrval_minutes - ) # optional, if auto_check_update + updater.set_check_interval(enabled=settings.auto_check_update, + months=settings.updater_interval_months, + days=settings.updater_interval_days, + hours=settings.updater_interval_hours, + minutes=settings.updater_interval_minutes) (update_ready, version, link) = updater.check_for_update(now=False) - if update_ready == True: - atr = addon_updater_install_popup.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT') + if update_ready: + atr = AddonUpdaterInstallPopup.bl_idname.split(".") + getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') else: - if updater.verbose: print("No update ready") + updater.print_verbose("No update ready") self.report({'INFO'}, "No update ready") -def showReloadPopup(): - """For use in register only, to show popup after re-enabling the addon +def show_reload_popup(): + """For use in register only, to show popup after re-enabling the addon. - Must be enabled by developer + Must be enabled by developer. """ - if updater.invalidupdater == True: + if updater.invalid_updater: return saved_state = updater.json - global ran_update_sucess_popup + global ran_update_success_popup - has_state = saved_state != None + has_state = saved_state is not None just_updated = "just_updated" in saved_state updated_info = saved_state["just_updated"] if not (has_state and just_updated and updated_info): return - updater.json_reset_postupdate() # so this only runs once + updater.json_reset_postupdate() # So this only runs once. - # no handlers in this case - if updater.auto_reload_post_update == False: + # No handlers in this case. + if not updater.auto_reload_post_update: return - # see if we need add to the update handler to trigger the popup + # See if we need add to the update handler to trigger the popup. handlers = [] - if "scene_update_post" in dir(bpy.app.handlers): # 2.7x + if "scene_update_post" in dir(bpy.app.handlers): # 2.7x handlers = bpy.app.handlers.scene_update_post - else: # 2.8x + else: # 2.8+ handlers = bpy.app.handlers.depsgraph_update_post in_handles = updater_run_success_popup_handler in handlers - if in_handles or ran_update_sucess_popup is True: + if in_handles or ran_update_success_popup: return - if "scene_update_post" in dir(bpy.app.handlers): # 2.7x + if "scene_update_post" in dir(bpy.app.handlers): # 2.7x bpy.app.handlers.scene_update_post.append( - updater_run_success_popup_handler) - else: # 2.8x + updater_run_success_popup_handler) + else: # 2.8+ bpy.app.handlers.depsgraph_update_post.append( - updater_run_success_popup_handler) - ran_update_sucess_popup = True + updater_run_success_popup_handler) + ran_update_success_popup = True # ----------------------------------------------------------------------------- # Example UI integrations # ----------------------------------------------------------------------------- - - def update_notice_box_ui(self, context): - """ Panel - Update Available for placement at end/beginning of panel + """Update notice draw, to add to the end or beginning of a panel. After a check for update has occurred, this function will draw a box saying an update is ready, and give a button for: update now, open website, - or ignore popup. Ideal to be placed at the end / beginning of a panel + or ignore popup. Ideal to be placed at the end / beginning of a panel. """ - if updater.invalidupdater == True: + if updater.invalid_updater: return saved_state = updater.json - if updater.auto_reload_post_update == False: - if "just_updated" in saved_state and saved_state["just_updated"] == True: + if not updater.auto_reload_post_update: + if "just_updated" in saved_state and saved_state["just_updated"]: layout = self.layout box = layout.box() col = box.column() @@ -918,54 +923,59 @@ def update_notice_box_ui(self, context): text="Restart blender", icon="ERROR") col.label(text="to complete update") - return - # if user pressed ignore, don't draw the box - if "ignore" in updater.json and updater.json["ignore"] == True: + # If user pressed ignore, don't draw the box. + if "ignore" in updater.json and updater.json["ignore"]: return - if updater.update_ready != True: + if not updater.update_ready: return layout = self.layout box = layout.box() col = box.column(align=True) - col.label(text="Update ready!",icon="ERROR") + col.alert = True + col.label(text="Update ready!", icon="ERROR") + col.alert = False col.separator() row = col.row(align=True) split = row.split(align=True) colL = split.column(align=True) colL.scale_y = 1.5 - colL.operator(addon_updater_ignore.bl_idname,icon="X",text="Ignore") + colL.operator(AddonUpdaterIgnore.bl_idname, icon="X", text="Ignore") colR = split.column(align=True) colR.scale_y = 1.5 - if updater.manual_only==False: - colR.operator(addon_updater_update_now.bl_idname, - text="Update", icon="LOOP_FORWARDS") + if not updater.manual_only: + colR.operator(AddonUpdaterUpdateNow.bl_idname, + text="Update", icon="LOOP_FORWARDS") 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, - text="Install manually") + # ops = col.operator("wm.url_open",text="Direct download") + # ops.url=updater.update_link + col.operator(AddonUpdaterInstallManually.bl_idname, + text="Install manually") else: - #col.operator("wm.url_open",text="Direct download").url=updater.update_link + # ops = col.operator("wm.url_open", text="Direct download") + # ops.url=updater.update_link col.operator("wm.url_open", text="Get it now").url = updater.website def update_settings_ui(self, context, element=None): """Preferences - for drawing with full width inside user preferences - Create a function that can be run inside user preferences panel for prefs UI - Place inside UI draw using: addon_updater_ops.updaterSettingsUI(self, context) - or by: addon_updater_ops.updaterSettingsUI(context) + A function that can be run inside user preferences panel for prefs UI. + Place inside UI draw using: + addon_updater_ops.update_settings_ui(self, context) + or by: + addon_updater_ops.update_settings_ui(context) """ - # element is a UI element, such as layout, a row, column, or box - if element==None: + # Element is a UI element, such as layout, a row, column, or box. + if element is None: element = self.layout box = element.box() - # in case of error importing updater - if updater.invalidupdater == True: + # In case of error importing updater. + if updater.invalid_updater: box.label(text="Error initializing updater code:") box.label(text=updater.error_msg) return @@ -979,163 +989,161 @@ def update_settings_ui(self, context, element=None): row = box.row() # special case to tell user to restart blender, if set that way - if updater.auto_reload_post_update == False: + if not updater.auto_reload_post_update: saved_state = updater.json - if "just_updated" in saved_state and saved_state["just_updated"] == True: + if "just_updated" in saved_state and saved_state["just_updated"]: row.alert = True - row.operator( - "wm.quit_blender", - text="Restart blender to complete update", - icon="ERROR") + row.operator("wm.quit_blender", + text="Restart blender to complete update", + icon="ERROR") return split = layout_split(row, factor=0.4) - subcol = split.column() - subcol.prop(settings, "auto_check_update") - subcol = split.column() - - if settings.auto_check_update==False: - subcol.enabled = False - subrow = subcol.row() - subrow.label(text="Interval between checks") - subrow = subcol.row(align=True) - checkcol = subrow.column(align=True) - checkcol.prop(settings,"updater_intrval_months") - checkcol = subrow.column(align=True) - checkcol.prop(settings,"updater_intrval_days") - checkcol = subrow.column(align=True) + sub_col = split.column() + sub_col.prop(settings, "auto_check_update") + sub_col = split.column() + + if not settings.auto_check_update: + sub_col.enabled = False + sub_row = sub_col.row() + sub_row.label(text="Interval between checks") + sub_row = sub_col.row(align=True) + check_col = sub_row.column(align=True) + check_col.prop(settings, "updater_interval_months") + check_col = sub_row.column(align=True) + check_col.prop(settings, "updater_interval_days") + check_col = sub_row.column(align=True) # Consider un-commenting for local dev (e.g. to set shorter intervals) - # checkcol.prop(settings,"updater_intrval_hours") - # checkcol = subrow.column(align=True) - # checkcol.prop(settings,"updater_intrval_minutes") + # check_col.prop(settings,"updater_interval_hours") + # check_col = sub_row.column(align=True) + # check_col.prop(settings,"updater_interval_minutes") - # checking / managing updates + # Checking / managing updates. row = box.row() col = row.column() - if updater.error != None: - subcol = col.row(align=True) - subcol.scale_y = 1 - split = subcol.split(align=True) + if updater.error is not None: + sub_col = col.row(align=True) + sub_col.scale_y = 1 + split = sub_col.split(align=True) split.scale_y = 2 if "ssl" in updater.error_msg.lower(): split.enabled = True - split.operator(addon_updater_install_manually.bl_idname, - text=updater.error) + split.operator(AddonUpdaterInstallManually.bl_idname, + text=updater.error) else: split.enabled = False - split.operator(addon_updater_check_now.bl_idname, - text=updater.error) - split = subcol.split(align=True) + split.operator(AddonUpdaterCheckNow.bl_idname, + text=updater.error) + split = sub_col.split(align=True) split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text = "", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, + text="", icon="FILE_REFRESH") - elif updater.update_ready == None and updater.async_checking == False: + elif updater.update_ready is None and not updater.async_checking: col.scale_y = 2 - col.operator(addon_updater_check_now.bl_idname) - elif updater.update_ready == None: # async is running - subcol = col.row(align=True) - subcol.scale_y = 1 - split = subcol.split(align=True) + col.operator(AddonUpdaterCheckNow.bl_idname) + elif updater.update_ready is None: # async is running + sub_col = col.row(align=True) + sub_col.scale_y = 1 + split = sub_col.split(align=True) split.enabled = False split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text="Checking...") - split = subcol.split(align=True) + split.operator(AddonUpdaterCheckNow.bl_idname, text="Checking...") + split = sub_col.split(align=True) split.scale_y = 2 - 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.operator(AddonUpdaterEndBackground.bl_idname, text="", icon="X") + + elif updater.include_branches and \ + len(updater.tags) == len(updater.include_branch_list) and not \ + updater.manual_only: + # No releases found, but still show the appropriate branch. + sub_col = col.row(align=True) + sub_col.scale_y = 1 + split = sub_col.split(align=True) split.scale_y = 2 - split.operator(addon_updater_update_now.bl_idname, - text="Update directly to "+str(updater.include_branch_list[0])) - split = subcol.split(align=True) + update_now_txt = "Update directly to {}".format( + updater.include_branch_list[0]) + split.operator(AddonUpdaterUpdateNow.bl_idname, text=update_now_txt) + split = sub_col.split(align=True) split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text = "", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.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 - split = subcol.split(align=True) + elif updater.update_ready and not updater.manual_only: + sub_col = col.row(align=True) + sub_col.scale_y = 1 + split = sub_col.split(align=True) split.scale_y = 2 - split.operator(addon_updater_update_now.bl_idname, - text="Update now to "+str(updater.update_version)) - split = subcol.split(align=True) + split.operator(AddonUpdaterUpdateNow.bl_idname, + text="Update now to " + str(updater.update_version)) + split = sub_col.split(align=True) split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text = "", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, + text="", icon="FILE_REFRESH") - elif updater.update_ready==True and updater.manual_only==True: + elif updater.update_ready and updater.manual_only: col.scale_y = 2 + dl_now_txt = "Download " + str(updater.update_version) col.operator("wm.url_open", - text="Download "+str(updater.update_version)).url=updater.website - else: # i.e. that updater.update_ready == False - subcol = col.row(align=True) - subcol.scale_y = 1 - split = subcol.split(align=True) + text=dl_now_txt).url = updater.website + else: # i.e. that updater.update_ready == False. + sub_col = col.row(align=True) + sub_col.scale_y = 1 + split = sub_col.split(align=True) split.enabled = False split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text="Addon is up to date") - split = subcol.split(align=True) + split.operator(AddonUpdaterCheckNow.bl_idname, + text="Addon is up to date") + split = sub_col.split(align=True) split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text = "", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, + text="", icon="FILE_REFRESH") - if updater.manual_only == False: + if not updater.manual_only: col = row.column(align=True) - #col.operator(addon_updater_update_target.bl_idname, - if updater.include_branches == True and len(updater.include_branch_list)>0: + if updater.include_branches and len(updater.include_branch_list) > 0: branch = updater.include_branch_list[0] - col.operator(addon_updater_update_target.bl_idname, - text="Install latest {} / old version".format(branch)) + col.operator(AddonUpdaterUpdateTarget.bl_idname, + text="Install {} / old version".format(branch)) else: - col.operator(addon_updater_update_target.bl_idname, - text="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): + col.operator(AddonUpdaterUpdateTarget.bl_idname, + text="(Re)install addon version") + last_date = "none found" + backup_path = os.path.join(updater.stage_path, "backup") + if "backup_date" in updater.json and os.path.isdir(backup_path): if updater.json["backup_date"] == "": - lastdate = "Date not found" + last_date = "Date not found" else: - lastdate = updater.json["backup_date"] - backuptext = "Restore addon backup ({})".format(lastdate) - col.operator(addon_updater_restore_backup.bl_idname, text=backuptext) + last_date = updater.json["backup_date"] + backup_text = "Restore addon backup ({})".format(last_date) + col.operator(AddonUpdaterRestoreBackup.bl_idname, text=backup_text) row = box.row() row.scale_y = 0.7 - lastcheck = updater.json["last_check"] - if updater.error != None and updater.error_msg != None: + last_check = updater.json["last_check"] + if updater.error is not None and updater.error_msg is not None: row.label(text=updater.error_msg) - elif lastcheck != "" and lastcheck != None: - lastcheck = lastcheck[0: lastcheck.index(".") ] - row.label(text="Last update check: " + lastcheck) + elif last_check: + last_check = last_check[0: last_check.index(".")] + row.label(text="Last update check: " + last_check) else: row.label(text="Last update check: Never") def update_settings_ui_condensed(self, context, element=None): - """Preferences - Condensed drawing within preferences + """Preferences - Condensed drawing within preferences. - Alternate draw for user preferences or other places, does not draw a box + Alternate draw for user preferences or other places, does not draw a box. """ - # element is a UI element, such as layout, a row, column, or box - if element==None: + # Element is a UI element, such as layout, a row, column, or box. + if element is None: element = self.layout row = element.row() - # in case of error importing updater - if updater.invalidupdater == True: + # In case of error importing updater. + if updater.invalid_updater: row.label(text="Error initializing updater code:") row.label(text=updater.error_msg) return @@ -1144,11 +1152,11 @@ def update_settings_ui_condensed(self, context, element=None): row.label(text="Error getting updater preferences", icon='ERROR') return - # special case to tell user to restart blender, if set that way - if updater.auto_reload_post_update == False: + # Special case to tell user to restart blender, if set that way. + if not updater.auto_reload_post_update: saved_state = updater.json - if "just_updated" in saved_state and saved_state["just_updated"] == True: - row.alert = True # mark red + if "just_updated" in saved_state and saved_state["just_updated"]: + row.alert = True # mark red row.operator( "wm.quit_blender", text="Restart blender to complete update", @@ -1156,156 +1164,164 @@ def update_settings_ui_condensed(self, context, element=None): return col = row.column() - if updater.error != None: - subcol = col.row(align=True) - subcol.scale_y = 1 - split = subcol.split(align=True) + if updater.error is not None: + sub_col = col.row(align=True) + sub_col.scale_y = 1 + split = sub_col.split(align=True) split.scale_y = 2 if "ssl" in updater.error_msg.lower(): split.enabled = True - split.operator(addon_updater_install_manually.bl_idname, - text=updater.error) + split.operator(AddonUpdaterInstallManually.bl_idname, + text=updater.error) else: split.enabled = False - split.operator(addon_updater_check_now.bl_idname, - text=updater.error) - split = subcol.split(align=True) + split.operator(AddonUpdaterCheckNow.bl_idname, + text=updater.error) + split = sub_col.split(align=True) split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text = "", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, + text="", icon="FILE_REFRESH") - elif updater.update_ready == None and updater.async_checking == False: + elif updater.update_ready is None and not updater.async_checking: col.scale_y = 2 - col.operator(addon_updater_check_now.bl_idname) - elif updater.update_ready == None: # async is running - subcol = col.row(align=True) - subcol.scale_y = 1 - split = subcol.split(align=True) + col.operator(AddonUpdaterCheckNow.bl_idname) + elif updater.update_ready is None: # Async is running. + sub_col = col.row(align=True) + sub_col.scale_y = 1 + split = sub_col.split(align=True) split.enabled = False split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text="Checking...") - split = subcol.split(align=True) + split.operator(AddonUpdaterCheckNow.bl_idname, text="Checking...") + split = sub_col.split(align=True) split.scale_y = 2 - 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.operator(AddonUpdaterEndBackground.bl_idname, text="", icon="X") + + elif updater.include_branches and \ + len(updater.tags) == len(updater.include_branch_list) and not \ + updater.manual_only: + # No releases found, but still show the appropriate branch. + sub_col = col.row(align=True) + sub_col.scale_y = 1 + split = sub_col.split(align=True) split.scale_y = 2 - split.operator(addon_updater_update_now.bl_idname, - text="Update directly to "+str(updater.include_branch_list[0])) - split = subcol.split(align=True) + now_txt = "Update directly to " + str(updater.include_branch_list[0]) + split.operator(AddonUpdaterUpdateNow.bl_idname, text=now_txt) + split = sub_col.split(align=True) split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text = "", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.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 - split = subcol.split(align=True) + elif updater.update_ready and not updater.manual_only: + sub_col = col.row(align=True) + sub_col.scale_y = 1 + split = sub_col.split(align=True) split.scale_y = 2 - split.operator(addon_updater_update_now.bl_idname, - text="Update now to "+str(updater.update_version)) - split = subcol.split(align=True) + split.operator(AddonUpdaterUpdateNow.bl_idname, + text="Update now to " + str(updater.update_version)) + split = sub_col.split(align=True) split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text = "", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, + text="", icon="FILE_REFRESH") - elif updater.update_ready==True and updater.manual_only==True: + elif updater.update_ready and updater.manual_only: col.scale_y = 2 - col.operator("wm.url_open", - text="Download "+str(updater.update_version)).url=updater.website - else: # i.e. that updater.update_ready == False - subcol = col.row(align=True) - subcol.scale_y = 1 - split = subcol.split(align=True) + dl_txt = "Download " + str(updater.update_version) + col.operator("wm.url_open", text=dl_txt).url = updater.website + else: # i.e. that updater.update_ready == False. + sub_col = col.row(align=True) + sub_col.scale_y = 1 + split = sub_col.split(align=True) split.enabled = False split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text="Addon is up to date") - split = subcol.split(align=True) + split.operator(AddonUpdaterCheckNow.bl_idname, + text="Addon is up to date") + split = sub_col.split(align=True) split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text = "", icon="FILE_REFRESH") + split.operator(AddonUpdaterCheckNow.bl_idname, + text="", icon="FILE_REFRESH") row = element.row() row.prop(settings, "auto_check_update") row = element.row() row.scale_y = 0.7 - lastcheck = updater.json["last_check"] - if updater.error != None and updater.error_msg != None: + last_check = updater.json["last_check"] + if updater.error is not None and updater.error_msg is not None: row.label(text=updater.error_msg) - elif lastcheck != "" and lastcheck != None: - lastcheck = lastcheck[0: lastcheck.index(".") ] - row.label(text="Last check: " + lastcheck) + elif last_check != "" and last_check is not None: + last_check = last_check[0: last_check.index(".")] + row.label(text="Last check: " + last_check) else: row.label(text="Last check: Never") def skip_tag_function(self, tag): - """A global function for tag skipping - - 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, - False if the tag is allowed (default for all) - Note: here, "self" is the acting updater shared class instance + """A global function for tag skipping. + + A way to filter which tags are displayed, e.g. to limit downgrading too + long ago. + + Args: + self: The instance of the singleton addon update. + tag: the text content of a tag from the repo, e.g. "v1.2.3". + + Returns: + bool: True to skip this tag name (ie don't allow for downloading this + version), or False if the tag is allowed. """ - # in case of error importing updater - if self.invalidupdater == True: + # In case of error importing updater. + if self.invalid_updater: 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 self.include_branches == True: + if self.include_branches: for branch in self.include_branch_list: - if tag["name"].lower() == branch: return False + if tag["name"].lower() == branch: + return False - # function converting string to tuple, ignoring e.g. leading 'v' + # Function converting string to tuple, ignoring e.g. leading 'v'. + # Be aware that this strips out other text that you might otherwise + # want to be kept and accounted for when checking tags (e.g. v1.1a vs 1.1b) tupled = self.version_tuple_from_text(tag["name"]) - if type(tupled) != type( (1,2,3) ): return True + if not isinstance(tupled, tuple): + return True - # select the min tag version - change tuple accordingly - if self.version_min_update != None: + # Select the min tag version - change tuple accordingly. + if self.version_min_update is not None: if tupled < self.version_min_update: - return True # skip if current version below this + return True # Skip if current version below this. - # select the max tag version - if self.version_max_update != None: + # Select the max tag version. + if self.version_max_update is not None: if tupled >= self.version_max_update: - return True # skip if current version at or above this + return True # Skip if current version at or above this. - # in all other cases, allow showing the tag for updating/reverting + # In all other cases, allow showing the tag for updating/reverting. + # To simply and always show all tags, this return False could be moved + # to the start of the function definition so all tags are allowed. return False def select_link_function(self, tag): - """Only customize if trying to leverage "attachments" in *GitHub* releases + """Only customize if trying to leverage "attachments" in *GitHub* releases. - A way to select from one or multiple attached donwloadable files from the - server, instead of downloading the default release/tag source code + A way to select from one or multiple attached downloadable files from the + server, instead of downloading the default release/tag source code. """ # -- Default, universal case (and is the only option for GitLab/Bitbucket) - #link = tag["zipball_url"] + link = tag["zipball_url"] # -- Example: select the first (or only) asset instead source code -- - if "assets" in tag and "browser_download_url" in tag["assets"][0]: - link = tag["assets"][0]["browser_download_url"] + # if "assets" in tag and "browser_download_url" in tag["assets"][0]: + # link = tag["assets"][0]["browser_download_url"] # -- Example: select asset based on OS, where multiple builds exist -- # # not tested/no error checking, modify to fit your own needs! @@ -1313,11 +1329,11 @@ def select_link_function(self, tag): # # release_windows.zip, release_OSX.zip, release_linux.zip # # This also would logically not be used with "branches" enabled # if platform.system() == "Darwin": # ie OSX - # link = [asset for asset in tag["assets"] if 'OSX' in asset][0] + # link = [asset for asset in tag["assets"] if 'OSX' in asset][0] # elif platform.system() == "Windows": - # link = [asset for asset in tag["assets"] if 'windows' in asset][0] + # link = [asset for asset in tag["assets"] if 'windows' in asset][0] # elif platform.system() == "Linux": - # link = [asset for asset in tag["assets"] if 'linux' in asset][0] + # link = [asset for asset in tag["assets"] if 'linux' in asset][0] return link @@ -1325,18 +1341,16 @@ def select_link_function(self, tag): # ----------------------------------------------------------------------------- # Register, should be run in the register module itself # ----------------------------------------------------------------------------- - - classes = ( - addon_updater_install_popup, - addon_updater_check_now, - addon_updater_update_now, - addon_updater_update_target, - addon_updater_install_manually, - addon_updater_updated_successful, - addon_updater_restore_backup, - addon_updater_ignore, - addon_updater_end_background + AddonUpdaterInstallPopup, + AddonUpdaterCheckNow, + AddonUpdaterUpdateNow, + AddonUpdaterUpdateTarget, + AddonUpdaterInstallManually, + AddonUpdaterUpdatedSuccessful, + AddonUpdaterRestoreBackup, + AddonUpdaterIgnore, + AddonUpdaterEndBackground ) @@ -1349,11 +1363,11 @@ def register(bl_info): if updater.error != None: print("Exiting updater registration, error return") return - if updater.update_ready is None: # force clear out after blender restart + if updater.update_ready is None: # Force clear out after blender restart. saved_state = updater.json if saved_state.get("just_updated") is True: updater.json_reset_postupdate() - updater.clear_state() # clear internal vars, avoids reloading oddities + updater.clear_state() # Clear internal vars, avoids reloading oddities. updater.engine = "Github" updater.user = "theduckcow" @@ -1361,11 +1375,11 @@ def register(bl_info): updater.website = "https://theduckcow.com/dev/blender/mcprep-download/" updater.subfolder_path = "MCprep_addon/" updater.current_version = bl_info["version"] - updater.backup_current = True # True by default + updater.backup_current = True updater.backup_ignore_patterns = ["__pycache__"] - updater.overwrite_patterns = ["*.png","README.md","*.txt","*.blend"] - updater.remove_pre_update_patterns = ["*.py", "*.pyc", - "mcprep_addon_tracker.json", "mcprep_data.json"] + updater.overwrite_patterns = ["*.png", "README.md", "*.txt", "*.blend"] + updater.remove_pre_update_patterns = [ + "*.py", "*.pyc", "mcprep_addon_tracker.json", "mcprep_data.json"] updater.include_branches = False updater.include_branch_list = None # is the equivalent to setting ['master'] updater.manual_only = False # used to be True @@ -1396,17 +1410,17 @@ def register(bl_info): def unregister(): for cls in reversed(classes): - # comment out this line if using bpy.utils.unregister_module(__name__) + # Comment out this line if using bpy.utils.unregister_module(__name__). bpy.utils.unregister_class(cls) - # clear global vars since they may persist if not restarting blender - updater.clear_state() # clear internal vars, avoids reloading oddities + # Clear global vars since they may persist if not restarting blender. + updater.clear_state() # Clear internal vars, avoids reloading oddities. - global ran_autocheck_install_popup - ran_autocheck_install_popup = False + global ran_auto_check_install_popup + ran_auto_check_install_popup = False - global ran_update_sucess_popup - ran_update_sucess_popup = False + global ran_update_success_popup + ran_update_success_popup = False global ran_background_check ran_background_check = False diff --git a/MCprep_addon/mcprep_ui.py b/MCprep_addon/mcprep_ui.py index f4d7c550..8e5ef232 100755 --- a/MCprep_addon/mcprep_ui.py +++ b/MCprep_addon/mcprep_ui.py @@ -261,26 +261,26 @@ def change_verbose(self, context): description = "If enabled, auto-check for updates using an interval", default = True, ) - updater_intrval_months = bpy.props.IntProperty( + updater_interval_months = bpy.props.IntProperty( name='Months', description = "Number of months between checking for updates", default=0, min=0 ) - updater_intrval_days = bpy.props.IntProperty( + updater_interval_days = bpy.props.IntProperty( name='Days', description = "Number of days between checking for updates", default=1, min=0, ) - updater_intrval_hours = bpy.props.IntProperty( + updater_interval_hours = bpy.props.IntProperty( name='Hours', description = "Number of hours between checking for updates", default=0, min=0, max=23 ) - updater_intrval_minutes = bpy.props.IntProperty( + updater_interval_minutes = bpy.props.IntProperty( name='Minutes', description = "Number of minutes between checking for updates", default=0,