diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index be38767ba5..fd1b8f94a1 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -1,28 +1,28 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -#*************************************************************************** -#* * -#* Copyright (c) 2015 Yorik van Havre * -#* Copyright (c) 2021 Chris Hennes * -#* * -#* This program is free software; you can redistribute it and/or modify * -#* it under the terms of the GNU Lesser General Public License (LGPL) * -#* as published by the Free Software Foundation; either version 2 of * -#* the License, or (at your option) any later version. * -#* for detail see the LICENCE text file. * -#* * -#* This program is distributed in the hope that it will be useful, * -#* but WITHOUT ANY WARRANTY; without even the implied warranty of * -#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -#* GNU Library General Public License for more details. * -#* * -#* You should have received a copy of the GNU Library General Public * -#* License along with this program; if not, write to the Free Software * -#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -#* USA * -#* * -#*************************************************************************** +# *************************************************************************** +# * * +# * Copyright (c) 2015 Yorik van Havre * +# * Copyright (c) 2021 Chris Hennes * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** import os import shutil @@ -70,44 +70,68 @@ installed. def QT_TRANSLATE_NOOP(ctx, txt): return txt + class CommandAddonManager: """The main Addon Manager class and FreeCAD command""" - workers = ["update_worker", "check_worker", "show_worker", - "showmacro_worker", "macro_worker", "install_worker", - "update_metadata_cache_worker", "update_all_worker"] + workers = [ + "update_worker", + "check_worker", + "show_worker", + "showmacro_worker", + "macro_worker", + "install_worker", + "update_metadata_cache_worker", + "update_all_worker", + ] lock = threading.Lock() restart_required = False def __init__(self): - FreeCADGui.addPreferencePage(os.path.join(os.path.dirname(__file__), - "AddonManagerOptions.ui"),"Addon Manager") + FreeCADGui.addPreferencePage( + os.path.join(os.path.dirname(__file__), "AddonManagerOptions.ui"), + "Addon Manager", + ) - def GetResources(self) -> Dict[str,str]: - return {"Pixmap": "AddonManager", - "MenuText": QT_TRANSLATE_NOOP("Std_AddonMgr", "&Addon manager"), - "ToolTip": QT_TRANSLATE_NOOP("Std_AddonMgr", "Manage external workbenches, macros, and preference packs"), - "Group": "Tools"} + def GetResources(self) -> Dict[str, str]: + return { + "Pixmap": "AddonManager", + "MenuText": QT_TRANSLATE_NOOP("Std_AddonMgr", "&Addon manager"), + "ToolTip": QT_TRANSLATE_NOOP( + "Std_AddonMgr", + "Manage external workbenches, macros, and preference packs", + ), + "Group": "Tools", + } def Activated(self) -> None: # display first use dialog if needed - readWarningParameter = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") + readWarningParameter = FreeCAD.ParamGet( + "User parameter:BaseApp/Preferences/Addons" + ) readWarning = readWarningParameter.GetBool("readWarning", False) - newReadWarningParameter = FreeCAD.ParamGet("User parameter:Plugins/addonsRepository") + newReadWarningParameter = FreeCAD.ParamGet( + "User parameter:Plugins/addonsRepository" + ) readWarning |= newReadWarningParameter.GetBool("readWarning", False) if not readWarning: - if (QtWidgets.QMessageBox.warning(None, - "FreeCAD", - translate("AddonsInstaller", - "The addons that can be installed here are not " - "officially part of FreeCAD, and are not reviewed " - "by the FreeCAD team. Make sure you know what you " - "are installing!"), - QtWidgets.QMessageBox.Cancel | - QtWidgets.QMessageBox.Ok) != - QtWidgets.QMessageBox.StandardButton.Cancel): + if ( + QtWidgets.QMessageBox.warning( + None, + "FreeCAD", + translate( + "AddonsInstaller", + "The addons that can be installed here are not " + "officially part of FreeCAD, and are not reviewed " + "by the FreeCAD team. Make sure you know what you " + "are installing!", + ), + QtWidgets.QMessageBox.Cancel | QtWidgets.QMessageBox.Ok, + ) + != QtWidgets.QMessageBox.StandardButton.Cancel + ): readWarningParameter.SetBool("readWarning", True) readWarning = True @@ -118,8 +142,9 @@ class CommandAddonManager: """Shows the Addon Manager UI""" # create the dialog - self.dialog = FreeCADGui.PySideUic.loadUi(os.path.join(os.path.dirname(__file__), - "AddonManager.ui")) + self.dialog = FreeCADGui.PySideUic.loadUi( + os.path.join(os.path.dirname(__file__), "AddonManager.ui") + ) # cleanup the leftovers from previous runs self.macro_repo_dir = FreeCAD.getUserMacroDir() @@ -151,7 +176,7 @@ class CommandAddonManager: days_between_updates = pref.GetInt("DaysBetweenUpdates", days_between_updates) last_cache_update_string = pref.GetString("LastCacheUpdate", "never") cache_path = FreeCAD.getUserCachePath() - am_path = os.path.join(cache_path,"AddonManager") + am_path = os.path.join(cache_path, "AddonManager") if last_cache_update_string == "never": self.update_cache = True elif days_between_updates > 0: @@ -169,7 +194,9 @@ class CommandAddonManager: self.item_model = PackageListItemModel() self.packageList.setModel(self.item_model) self.dialog.contentPlaceholder.hide() - self.dialog.layout().replaceWidget(self.dialog.contentPlaceholder, self.packageList) + self.dialog.layout().replaceWidget( + self.dialog.contentPlaceholder, self.packageList + ) self.packageList.show() # Package details start out hidden @@ -181,8 +208,14 @@ class CommandAddonManager: # set nice icons to everything, by theme with fallback to FreeCAD icons self.dialog.setWindowIcon(QtGui.QIcon(":/icons/AddonManager.svg")) self.dialog.buttonUpdateAll.setIcon(QtGui.QIcon(":/icons/button_valid.svg")) - self.dialog.buttonClose.setIcon(QtGui.QIcon.fromTheme("close", QtGui.QIcon(":/icons/process-stop.svg"))) - self.dialog.buttonPauseUpdate.setIcon(QtGui.QIcon.fromTheme("pause", QtGui.QIcon(":/icons/media-playback-stop.svg"))) + self.dialog.buttonClose.setIcon( + QtGui.QIcon.fromTheme("close", QtGui.QIcon(":/icons/process-stop.svg")) + ) + self.dialog.buttonPauseUpdate.setIcon( + QtGui.QIcon.fromTheme( + "pause", QtGui.QIcon(":/icons/media-playback-stop.svg") + ) + ) # enable/disable stuff self.dialog.buttonUpdateAll.setEnabled(False) @@ -206,10 +239,14 @@ class CommandAddonManager: # center the dialog over the FreeCAD window mw = FreeCADGui.getMainWindow() - self.dialog.move(mw.frameGeometry().topLeft() + mw.rect().center() - self.dialog.rect().center()) + self.dialog.move( + mw.frameGeometry().topLeft() + + mw.rect().center() + - self.dialog.rect().center() + ) # set info for the progress bar: - self.dialog.progressBar.setMaximum (100) + self.dialog.progressBar.setMaximum(100) # begin populating the table in a set of sub-threads self.startup() @@ -221,7 +258,7 @@ class CommandAddonManager: self.dialog.exec_() def cleanup_workers(self, wait=False) -> None: - """ Ensure that no workers are running by explicitly asking them to stop and waiting for them until they do """ + """Ensure that no workers are running by explicitly asking them to stop and waiting for them until they do""" for worker in self.workers: if hasattr(self, worker): thread = getattr(self, worker) @@ -283,26 +320,31 @@ class CommandAddonManager: m = QtWidgets.QMessageBox() m.setWindowTitle(translate("AddonsInstaller", "Addon manager")) m.setWindowIcon(QtGui.QIcon(":/icons/AddonManager.svg")) - m.setText(translate("AddonsInstaller", - "You must restart FreeCAD for changes to take " - "effect.")) + m.setText( + translate( + "AddonsInstaller", + "You must restart FreeCAD for changes to take " "effect.", + ) + ) m.setIcon(m.Warning) m.setStandardButtons(m.Ok | m.Cancel) m.setDefaultButton(m.Cancel) okBtn = m.button(QtWidgets.QMessageBox.StandardButton.Ok) cancelBtn = m.button(QtWidgets.QMessageBox.StandardButton.Cancel) - okBtn.setText(translate("AddonsInstaller","Restart now")) - cancelBtn.setText(translate("AddonsInstaller","Restart later")) + okBtn.setText(translate("AddonsInstaller", "Restart now")) + cancelBtn.setText(translate("AddonsInstaller", "Restart later")) ret = m.exec_() if ret == m.Ok: # restart FreeCAD after a delay to give time to this dialog to close QtCore.QTimer.singleShot(1000, utils.restart_freecad) else: - FreeCAD.Console.PrintWarning("Could not terminate sub-threads in Addon Manager.\n") + FreeCAD.Console.PrintWarning( + "Could not terminate sub-threads in Addon Manager.\n" + ) self.cleanup_workers() def startup(self) -> None: - """ Downloads the available packages listings and populates the table + """Downloads the available packages listings and populates the table This proceeds in four stages: first, the main GitHub repository is queried for a list of possible addons. Each addon is specified as a git submodule with name and branch information. The actual specific @@ -324,22 +366,24 @@ class CommandAddonManager: """ - # Each function in this list is expected to launch a thread and connect its completion signal + # Each function in this list is expected to launch a thread and connect its completion signal # to self.do_next_startup_phase, or to shortcut to calling self.do_next_startup_phase if it # is not launching a worker - self.startup_sequence = [self.populate_packages_table, - self.activate_table_widgets, - self.populate_macros, - self.update_metadata_cache, - self.check_updates] + self.startup_sequence = [ + self.populate_packages_table, + self.activate_table_widgets, + self.populate_macros, + self.update_metadata_cache, + self.check_updates, + ] self.current_progress_region = 0 self.number_of_progress_regions = len(self.startup_sequence) self.do_next_startup_phase() def do_next_startup_phase(self) -> None: - """ Pop the top item in self.startup_sequence off the list and run it """ + """Pop the top item in self.startup_sequence off the list and run it""" - if (len(self.startup_sequence) > 0): + if len(self.startup_sequence) > 0: phase_runner = self.startup_sequence.pop(0) self.current_progress_region += 1 phase_runner() @@ -349,11 +393,11 @@ class CommandAddonManager: pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") pref.SetString("LastCacheUpdate", date.today().isoformat()) - def get_cache_file_name(self, file:str) -> str: + def get_cache_file_name(self, file: str) -> str: cache_path = FreeCAD.getUserCachePath() - am_path = os.path.join(cache_path,"AddonManager") - os.makedirs(am_path,exist_ok=True) - return os.path.join(am_path,file) + am_path = os.path.join(cache_path, "AddonManager") + os.makedirs(am_path, exist_ok=True) + return os.path.join(am_path, file) def populate_packages_table(self) -> None: self.item_model.clear() @@ -362,7 +406,7 @@ class CommandAddonManager: use_cache = not self.update_cache if use_cache: if os.path.isfile(self.get_cache_file_name("package_cache.json")): - with open(self.get_cache_file_name("package_cache.json"),"r") as f: + with open(self.get_cache_file_name("package_cache.json"), "r") as f: data = f.read() try: from_json = json.loads(data) @@ -374,28 +418,34 @@ class CommandAddonManager: use_cache = False if not use_cache: - self.update_cache = True # Make sure to trigger the other cache updates, if the json file was missing + self.update_cache = True # Make sure to trigger the other cache updates, if the json file was missing self.update_worker = UpdateWorker() self.update_worker.status_message.connect(self.show_information) self.update_worker.addon_repo.connect(self.add_addon_repo) - self.update_progress_bar(10,100) - self.update_worker.done.connect(self.do_next_startup_phase) # Link to step 2 + self.update_progress_bar(10, 100) + self.update_worker.done.connect( + self.do_next_startup_phase + ) # Link to step 2 self.update_worker.start() else: - self.update_worker = LoadPackagesFromCacheWorker(self.get_cache_file_name("package_cache.json")) + self.update_worker = LoadPackagesFromCacheWorker( + self.get_cache_file_name("package_cache.json") + ) self.update_worker.addon_repo.connect(self.add_addon_repo) - self.update_progress_bar(10,100) - self.update_worker.done.connect(self.do_next_startup_phase) # Link to step 2 + self.update_progress_bar(10, 100) + self.update_worker.done.connect( + self.do_next_startup_phase + ) # Link to step 2 self.update_worker.start() - def cache_package(self, repo:AddonManagerRepo): + def cache_package(self, repo: AddonManagerRepo): if not hasattr(self, "package_cache"): self.package_cache = [] self.package_cache.append(repo.to_cache()) def write_package_cache(self): package_cache_path = self.get_cache_file_name("package_cache.json") - with open(package_cache_path,"w") as f: + with open(package_cache_path, "w") as f: f.write(json.dumps(self.package_cache)) self.package_cache = [] @@ -406,38 +456,52 @@ class CommandAddonManager: def populate_macros(self) -> None: self.current_progress_region += 1 - if self.update_cache or not os.path.isfile(self.get_cache_file_name("macro_cache.json")): + if self.update_cache or not os.path.isfile( + self.get_cache_file_name("macro_cache.json") + ): self.macro_worker = FillMacroListWorker(self.get_cache_file_name("Macros")) self.macro_worker.status_message_signal.connect(self.show_information) self.macro_worker.progress_made.connect(self.update_progress_bar) self.macro_worker.add_macro_signal.connect(self.add_addon_repo) - self.macro_worker.done.connect(self.do_next_startup_phase) # Link to step 3 + self.macro_worker.done.connect(self.do_next_startup_phase) # Link to step 3 self.macro_worker.start() else: - self.macro_worker = LoadMacrosFromCacheWorker(self.get_cache_file_name("macro_cache.json")) + self.macro_worker = LoadMacrosFromCacheWorker( + self.get_cache_file_name("macro_cache.json") + ) self.macro_worker.add_macro_signal.connect(self.add_addon_repo) - self.macro_worker.done.connect(self.do_next_startup_phase) # Link to step 3 + self.macro_worker.done.connect(self.do_next_startup_phase) # Link to step 3 self.macro_worker.start() - def cache_macro(self, macro:AddonManagerRepo): + def cache_macro(self, macro: AddonManagerRepo): if not hasattr(self, "macro_cache"): self.macro_cache = [] self.macro_cache.append(macro.macro.to_cache()) def write_macro_cache(self): macro_cache_path = self.get_cache_file_name("macro_cache.json") - with open(macro_cache_path,"w") as f: + with open(macro_cache_path, "w") as f: f.write(json.dumps(self.macro_cache)) self.macro_cache = [] - + def update_metadata_cache(self) -> None: self.current_progress_region += 1 if self.update_cache: - self.update_metadata_cache_worker = UpdateMetadataCacheWorker(self.item_model.repos) - self.update_metadata_cache_worker.status_message.connect(self.show_information) - self.update_metadata_cache_worker.done.connect(self.do_next_startup_phase) # Link to step 4 - self.update_metadata_cache_worker.progress_made.connect(self.update_progress_bar) - self.update_metadata_cache_worker.package_updated.connect(self.on_package_updated) + self.update_metadata_cache_worker = UpdateMetadataCacheWorker( + self.item_model.repos + ) + self.update_metadata_cache_worker.status_message.connect( + self.show_information + ) + self.update_metadata_cache_worker.done.connect( + self.do_next_startup_phase + ) # Link to step 4 + self.update_metadata_cache_worker.progress_made.connect( + self.update_progress_bar + ) + self.update_metadata_cache_worker.package_updated.connect( + self.on_package_updated + ) self.update_metadata_cache_worker.start() else: self.do_next_startup_phase() @@ -446,18 +510,19 @@ class CommandAddonManager: self.update_cache = True self.startup() - def on_package_updated(self, repo:AddonManagerRepo) -> None: + def on_package_updated(self, repo: AddonManagerRepo) -> None: """Called when the named package has either new metadata or a new icon (or both)""" - + with self.lock: - cache_path = os.path.join(FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata", repo.name) + cache_path = os.path.join( + FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata", repo.name + ) repo.icon = self.get_icon(repo, update=True) self.item_model.reload_item(repo) - def check_updates(self) -> None: "checks every installed addon for available updates" - + self.current_progress_region += 1 pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") autocheck = pref.GetBool("AutoCheck", False) @@ -471,7 +536,9 @@ class CommandAddonManager: if not thread.isFinished(): self.do_next_startup_phase() return - self.dialog.buttonUpdateAll.setText(translate("AddonsInstaller", "Checking for updates...")) + self.dialog.buttonUpdateAll.setText( + translate("AddonsInstaller", "Checking for updates...") + ) self.check_worker = CheckWorkbenchesForUpdatesWorker(self.item_model.repos) self.check_worker.done.connect(self.do_next_startup_phase) self.check_worker.progress_made.connect(self.update_progress_bar) @@ -479,36 +546,44 @@ class CommandAddonManager: self.check_worker.start() self.enable_updates(len(self.packages_with_updates)) - def status_updated(self, repo:AddonManagerRepo) -> None: + def status_updated(self, repo: AddonManagerRepo) -> None: self.item_model.reload_item(repo) if repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: self.packages_with_updates.append(repo) self.enable_updates(len(self.packages_with_updates)) - def enable_updates(self, number_of_updates:int) -> None: + def enable_updates(self, number_of_updates: int) -> None: """enables the update button""" if number_of_updates: - self.dialog.buttonUpdateAll.setText(translate("AddonsInstaller", "Apply") + - " " + str(number_of_updates) + " " + - translate("AddonsInstaller", "update(s)")) + self.dialog.buttonUpdateAll.setText( + translate("AddonsInstaller", "Apply") + + " " + + str(number_of_updates) + + " " + + translate("AddonsInstaller", "update(s)") + ) self.dialog.buttonUpdateAll.setEnabled(True) else: - self.dialog.buttonUpdateAll.setText(translate("AddonsInstaller", "No updates available")) + self.dialog.buttonUpdateAll.setText( + translate("AddonsInstaller", "No updates available") + ) self.dialog.buttonUpdateAll.setEnabled(False) - def add_addon_repo(self, addon_repo:AddonManagerRepo) -> None: + def add_addon_repo(self, addon_repo: AddonManagerRepo) -> None: """adds a workbench to the list""" - - if addon_repo.icon is None or addon_repo.icon.isNull(): + + if addon_repo.icon is None or addon_repo.icon.isNull(): addon_repo.icon = self.get_icon(addon_repo) for repo in self.item_model.repos: if repo.name == addon_repo.name: - FreeCAD.Console.PrintLog(f"Possible duplicate addon: ignoring second addition of {addon_repo.name}\n") + FreeCAD.Console.PrintLog( + f"Possible duplicate addon: ignoring second addition of {addon_repo.name}\n" + ) return self.item_model.append_item(addon_repo) - def get_icon(self, repo:AddonManagerRepo, update:bool=False) -> QtGui.QIcon: + def get_icon(self, repo: AddonManagerRepo, update: bool = False) -> QtGui.QIcon: """returns an icon for a repo""" if not update and repo.icon and not repo.icon.isNull() and repo.icon.isValid(): @@ -516,8 +591,8 @@ class CommandAddonManager: path = ":/icons/" + repo.name.replace(" ", "_") if repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH: - path += "_workbench_icon.svg" - default_icon = QtGui.QIcon(":/icons/document-package.svg") + path += "_workbench_icon.svg" + default_icon = QtGui.QIcon(":/icons/document-package.svg") elif repo.repo_type == AddonManagerRepo.RepoType.MACRO: path += "_macro_icon.svg" default_icon = QtGui.QIcon(":/icons/document-python.svg") @@ -542,19 +617,19 @@ class CommandAddonManager: return addonicon - def table_row_activated(self, selected_repo:AddonManagerRepo) -> None: + def table_row_activated(self, selected_repo: AddonManagerRepo) -> None: """a row was activated, show the relevant data""" self.packageList.hide() self.packageDetails.show() self.packageDetails.show_repo(selected_repo) - def show_information(self, message:str) -> None: + def show_information(self, message: str) -> None: """shows generic text in the information pane (which might be collapsed)""" self.dialog.labelStatusInfo.setText(message) - def show_workbench(self, repo:AddonManagerRepo) -> None: + def show_workbench(self, repo: AddonManagerRepo) -> None: self.packageList.hide() self.packageDetails.show() self.packageDetails.show_repo(repo) @@ -563,12 +638,12 @@ class CommandAddonManager: self.packageDetails.hide() self.packageList.show() - def append_to_repos_list(self, repo:AddonManagerRepo) -> None: + def append_to_repos_list(self, repo: AddonManagerRepo) -> None: """this function allows threads to update the main list of workbenches""" self.item_model.append_item(repo) - def install(self, repo:AddonManagerRepo) -> None: + def install(self, repo: AddonManagerRepo) -> None: """installs or updates a workbench, macro, or package""" if hasattr(self, "install_worker") and self.install_worker: @@ -578,7 +653,10 @@ class CommandAddonManager: if not repo: return - if repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH or repo.repo_type == AddonManagerRepo.RepoType.PACKAGE: + if ( + repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH + or repo.repo_type == AddonManagerRepo.RepoType.PACKAGE + ): self.show_progress_widgets() self.install_worker = InstallWorkbenchWorker(repo) self.install_worker.status_message.connect(self.show_information) @@ -607,22 +685,26 @@ class CommandAddonManager: failed = True if not failed: - message = translate("AddonsInstaller", - "Macro successfully installed. The macro is " - "now available from the Macros dialog.") - self.on_package_installed (repo, message) + message = translate( + "AddonsInstaller", + "Macro successfully installed. The macro is " + "now available from the Macros dialog.", + ) + self.on_package_installed(repo, message) else: - message = translate("AddonsInstaller", "Installation of macro failed") + ":" + message = ( + translate("AddonsInstaller", "Installation of macro failed") + ":" + ) for error in errors: message += "\n * " message += error - self.on_installation_failed (repo, message) + self.on_installation_failed(repo, message) - def update(self, repo:AddonManagerRepo) -> None: + def update(self, repo: AddonManagerRepo) -> None: self.install(repo) def update_all(self) -> None: - """ Asynchronously apply all available updates: individual failures are noted, but do not stop other updates """ + """Asynchronously apply all available updates: individual failures are noted, but do not stop other updates""" if hasattr(self, "update_all_worker") and self.update_all_worker: if self.update_all_worker.isRunning(): @@ -630,31 +712,50 @@ class CommandAddonManager: self.subupdates_succeeded = [] self.subupdates_failed = [] - + self.show_progress_widgets() self.current_progress_region = 1 self.number_of_progress_regions = 1 self.update_all_worker = UpdateAllWorker(self.packages_with_updates) self.update_all_worker.progress_made.connect(self.update_progress_bar) self.update_all_worker.status_message.connect(self.show_information) - self.update_all_worker.success.connect(lambda repo : self.subupdates_succeeded.append(repo)) - self.update_all_worker.failure.connect(lambda repo : self.subupdates_failed.append(repo)) + self.update_all_worker.success.connect( + lambda repo: self.subupdates_succeeded.append(repo) + ) + self.update_all_worker.failure.connect( + lambda repo: self.subupdates_failed.append(repo) + ) self.update_all_worker.done.connect(self.on_update_all_completed) self.update_all_worker.start() def on_update_all_completed(self) -> None: self.hide_progress_widgets() if not self.subupdates_failed: - message = translate ("AddonsInstaller", "All packages were successfully updated. Packages:") + "\n" - message += ''.join([repo.name + "\n" for repo in self.subupdates_succeeded]) + message = ( + translate( + "AddonsInstaller", + "All packages were successfully updated. Packages:", + ) + + "\n" + ) + message += "".join([repo.name + "\n" for repo in self.subupdates_succeeded]) elif not self.subupdates_succeeded: - message = translate ("AddonsInstaller", "All packages updates failed. Packages:") + "\n" - message += ''.join([repo.name + "\n" for repo in self.subupdates_failed]) + message = ( + translate("AddonsInstaller", "All packages updates failed. Packages:") + + "\n" + ) + message += "".join([repo.name + "\n" for repo in self.subupdates_failed]) else: - message = translate ("AddonsInstaller", "Some packages updates failed. Successful packages:") + "\n" - message += ''.join([repo.name + "\n" for repo in self.subupdates_succeeded]) - message += translate ("AddonsInstaller", "Failed packages:") + "\n" - message += ''.join([repo.name + "\n" for repo in self.subupdates_failed]) + message = ( + translate( + "AddonsInstaller", + "Some packages updates failed. Successful packages:", + ) + + "\n" + ) + message += "".join([repo.name + "\n" for repo in self.subupdates_succeeded]) + message += translate("AddonsInstaller", "Failed packages:") + "\n" + message += "".join([repo.name + "\n" for repo in self.subupdates_failed]) for installed_repo in self.subupdates_succeeded: self.restart_required = True @@ -665,13 +766,15 @@ class CommandAddonManager: self.packages_with_updates.remove(installed_repo) break self.enable_updates(len(self.packages_with_updates)) - QtWidgets.QMessageBox.information(None, - translate("AddonsInstaller", "Update report"), - message, - QtWidgets.QMessageBox.Close) + QtWidgets.QMessageBox.information( + None, + translate("AddonsInstaller", "Update report"), + message, + QtWidgets.QMessageBox.Close, + ) def hide_progress_widgets(self) -> None: - """ hides the progress bar and related widgets""" + """hides the progress bar and related widgets""" self.dialog.labelStatusInfo.hide() self.dialog.progressBar.hide() @@ -689,12 +792,14 @@ class CommandAddonManager: self.dialog.buttonShowDetails.setArrowType(QtCore.Qt.RightArrow) self.dialog.labelUpdateInProgress.show() - def update_progress_bar(self, current_value:int, max_value:int) -> None: - """ Update the progress bar, showing it if it's hidden """ + def update_progress_bar(self, current_value: int, max_value: int) -> None: + """Update the progress bar, showing it if it's hidden""" self.show_progress_widgets() region_size = 100 / self.number_of_progress_regions - value = (self.current_progress_region-1)*region_size + (current_value / max_value / self.number_of_progress_regions)*region_size + value = (self.current_progress_region - 1) * region_size + ( + current_value / max_value / self.number_of_progress_regions + ) * region_size self.dialog.progressBar.setValue(value) def toggle_details(self) -> None: @@ -705,29 +810,33 @@ class CommandAddonManager: self.dialog.labelStatusInfo.hide() self.dialog.buttonShowDetails.setArrowType(QtCore.Qt.RightArrow) - def stop_update(self)-> None: + def stop_update(self) -> None: self.cleanup_workers() self.hide_progress_widgets() - def on_package_installed(self, repo:AddonManagerRepo, message:str) -> None: + def on_package_installed(self, repo: AddonManagerRepo, message: str) -> None: self.hide_progress_widgets() - QtWidgets.QMessageBox.information(None, - translate("AddonsInstaller", "Installation succeeded"), - message, - QtWidgets.QMessageBox.Close) + QtWidgets.QMessageBox.information( + None, + translate("AddonsInstaller", "Installation succeeded"), + message, + QtWidgets.QMessageBox.Close, + ) repo.update_status = AddonManagerRepo.UpdateStatus.PENDING_RESTART self.item_model.reload_item(repo) self.packageDetails.show_repo(repo) self.restart_required = True - def on_installation_failed(self, _:AddonManagerRepo, message:str) -> None: + def on_installation_failed(self, _: AddonManagerRepo, message: str) -> None: self.hide_progress_widgets() - QtWidgets.QMessageBox.warning(None, - translate("AddonsInstaller", "Installation failed"), - message, - QtWidgets.QMessageBox.Close) + QtWidgets.QMessageBox.warning( + None, + translate("AddonsInstaller", "Installation failed"), + message, + QtWidgets.QMessageBox.Close, + ) - def executemacro(self, repo:AddonManagerRepo) -> None: + def executemacro(self, repo: AddonManagerRepo) -> None: """executes a selected macro""" macro = repo.macro @@ -735,7 +844,7 @@ class CommandAddonManager: return if macro.is_installed(): - macro_path = os.path.join(self.macro_repo_dir,macro.filename) + macro_path = os.path.join(self.macro_repo_dir, macro.filename) FreeCADGui.open(str(macro_path)) self.dialog.hide() FreeCADGui.SendMsgToActiveView("Run") @@ -743,14 +852,17 @@ class CommandAddonManager: with tempfile.TemporaryDirectory() as dir: temp_install_succeeded = macro.install(dir) if not temp_install_succeeded: - message = translate("AddonsInstaller", "Execution of macro failed. See console for failure details.") - self.on_installation_failed (repo, message) + message = translate( + "AddonsInstaller", + "Execution of macro failed. See console for failure details.", + ) + self.on_installation_failed(repo, message) return else: - macro_path = os.path.join(dir,macro.filename) + macro_path = os.path.join(dir, macro.filename) FreeCADGui.open(str(macro_path)) self.dialog.hide() - FreeCADGui.SendMsgToActiveView("Run") + FreeCADGui.SendMsgToActiveView("Run") def remove_readonly(self, func, path, _) -> None: """Remove a read-only file.""" @@ -758,29 +870,45 @@ class CommandAddonManager: os.chmod(path, stat.S_IWRITE) func(path) - def remove(self, repo:AddonManagerRepo) -> None: + def remove(self, repo: AddonManagerRepo) -> None: """uninstalls a macro or workbench""" - if repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH or \ - repo.repo_type == AddonManagerRepo.RepoType.PACKAGE: + if ( + repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH + or repo.repo_type == AddonManagerRepo.RepoType.PACKAGE + ): basedir = FreeCAD.getUserAppDataDir() moddir = basedir + os.sep + "Mod" clonedir = moddir + os.sep + repo.name if os.path.exists(clonedir): shutil.rmtree(clonedir, onerror=self.remove_readonly) - self.item_model.update_item_status(repo.name, AddonManagerRepo.UpdateStatus.NOT_INSTALLED) - self.addon_removed = True # A value to trigger the restart message on dialog close + self.item_model.update_item_status( + repo.name, AddonManagerRepo.UpdateStatus.NOT_INSTALLED + ) + self.addon_removed = ( + True # A value to trigger the restart message on dialog close + ) self.packageDetails.show_repo(repo) self.restart_required = True else: - self.dialog.textBrowserReadMe.setText(translate("AddonsInstaller", "Unable to remove this addon with the Addon Manager.")) + self.dialog.textBrowserReadMe.setText( + translate( + "AddonsInstaller", + "Unable to remove this addon with the Addon Manager.", + ) + ) elif repo.repo_type == AddonManagerRepo.RepoType.MACRO: macro = repo.macro if macro.remove(): - self.item_model.update_item_status(repo.name, AddonManagerRepo.UpdateStatus.NOT_INSTALLED) + self.item_model.update_item_status( + repo.name, AddonManagerRepo.UpdateStatus.NOT_INSTALLED + ) self.packageDetails.show_repo(repo) else: - self.dialog.textBrowserReadMe.setText(translate("AddonsInstaller", "Macro could not be removed.")) + self.dialog.textBrowserReadMe.setText( + translate("AddonsInstaller", "Macro could not be removed.") + ) + # @} diff --git a/src/Mod/AddonManager/AddonManagerRepo.py b/src/Mod/AddonManager/AddonManagerRepo.py index ca34db03a9..f931555319 100644 --- a/src/Mod/AddonManager/AddonManagerRepo.py +++ b/src/Mod/AddonManager/AddonManagerRepo.py @@ -1,24 +1,24 @@ -#*************************************************************************** -#* * -#* Copyright (c) 2021 Chris Hennes * -#* * -#* This program is free software; you can redistribute it and/or modify * -#* it under the terms of the GNU Lesser General Public License (LGPL) * -#* as published by the Free Software Foundation; either version 2 of * -#* the License, or (at your option) any later version. * -#* for detail see the LICENCE text file. * -#* * -#* This program is distributed in the hope that it will be useful, * -#* but WITHOUT ANY WARRANTY; without even the implied warranty of * -#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -#* GNU Library General Public License for more details. * -#* * -#* You should have received a copy of the GNU Library General Public * -#* License along with this program; if not, write to the Free Software * -#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -#* USA * -#* * -#*************************************************************************** +# *************************************************************************** +# * * +# * Copyright (c) 2021 Chris Hennes * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** import FreeCAD @@ -27,6 +27,7 @@ from typing import Dict from addonmanager_macro import Macro + class AddonManagerRepo: "Encapsulate information about a FreeCAD addon" @@ -37,7 +38,7 @@ class AddonManagerRepo: MACRO = 2 PACKAGE = 3 - def __str__(self) ->str : + def __str__(self) -> str: if self.value == 1: return "Workbench" elif self.value == 2: @@ -54,10 +55,10 @@ class AddonManagerRepo: def __lt__(self, other): if self.__class__ is other.__class__: - return self.value < other.value + return self.value < other.value return NotImplemented - def __str__(self) ->str : + def __str__(self) -> str: if self.value == 0: return "Not installed" elif self.value == 1: @@ -69,7 +70,7 @@ class AddonManagerRepo: elif self.value == 4: return "Restart required" - def __init__ (self, name:str, url:str, status:UpdateStatus, branch:str): + def __init__(self, name: str, url: str, status: UpdateStatus, branch: str): self.name = name.strip() self.url = url.strip() self.branch = branch.strip() @@ -77,28 +78,33 @@ class AddonManagerRepo: self.repo_type = AddonManagerRepo.RepoType.WORKBENCH self.description = None from addonmanager_utilities import construct_git_url - self.metadata_url = "" if not self.url else construct_git_url(self, "package.xml") + + self.metadata_url = ( + "" if not self.url else construct_git_url(self, "package.xml") + ) self.metadata = None self.icon = None self.cached_icon_filename = "" - self.macro = None # Bridge to Gaël Écorchard's macro management class + self.macro = None # Bridge to Gaël Écorchard's macro management class self.updated_timestamp = None self.installed_version = None - def __str__ (self) -> str: - result = f"FreeCAD {self.repo_type}\n" + def __str__(self) -> str: + result = f"FreeCAD {self.repo_type}\n" result += f"Name: {self.name}\n" result += f"URL: {self.url}\n" - result += "Has metadata\n" if self.metadata is not None else "No metadata found\n" + result += ( + "Has metadata\n" if self.metadata is not None else "No metadata found\n" + ) if self.macro is not None: result += "Has linked Macro object\n" return result @classmethod - def from_macro (self, macro:Macro): + def from_macro(self, macro: Macro): if macro.is_installed(): status = AddonManagerRepo.UpdateStatus.UNCHECKED - else: + else: status = AddonManagerRepo.UpdateStatus.NOT_INSTALLED instance = AddonManagerRepo(macro.name, macro.url, status, "master") instance.macro = macro @@ -107,10 +113,10 @@ class AddonManagerRepo: return instance @classmethod - def from_cache (self, data:Dict): - """ Load basic data from cached dict data. Does not include Macro or Metadata information, which must be populated separately. """ + def from_cache(self, data: Dict): + """Load basic data from cached dict data. Does not include Macro or Metadata information, which must be populated separately.""" - mod_dir = os.path.join(FreeCAD.getUserAppDataDir(),"Mod",data["name"]) + mod_dir = os.path.join(FreeCAD.getUserAppDataDir(), "Mod", data["name"]) if os.path.isdir(mod_dir): status = AddonManagerRepo.UpdateStatus.UNCHECKED else: @@ -121,18 +127,20 @@ class AddonManagerRepo: instance.cached_icon_filename = data["cached_icon_filename"] return instance - def to_cache (self) -> Dict: - """ Returns a dictionary with cache information that can be used later with from_cache to recreate this object. """ + def to_cache(self) -> Dict: + """Returns a dictionary with cache information that can be used later with from_cache to recreate this object.""" + + return { + "name": self.name, + "url": self.url, + "branch": self.branch, + "repo_type": int(self.repo_type), + "description": self.description, + "cached_icon_filename": self.get_cached_icon_filename(), + } - return {"name":self.name, - "url":self.url, - "branch":self.branch, - "repo_type":int(self.repo_type), - "description":self.description, - "cached_icon_filename":self.get_cached_icon_filename()} - def contains_workbench(self) -> bool: - """ Determine if this package contains (or is) a workbench """ + """Determine if this package contains (or is) a workbench""" if self.repo_type == AddonManagerRepo.RepoType.WORKBENCH: return True @@ -143,7 +151,7 @@ class AddonManagerRepo: return False def contains_macro(self) -> bool: - """ Determine if this package contains (or is) a macro """ + """Determine if this package contains (or is) a macro""" if self.repo_type == AddonManagerRepo.RepoType.MACRO: return True @@ -154,7 +162,7 @@ class AddonManagerRepo: return False def contains_preference_pack(self) -> bool: - """ Determine if this package contains a preference pack """ + """Determine if this package contains a preference pack""" if self.repo_type == AddonManagerRepo.RepoType.PACKAGE: content = self.metadata.Content @@ -162,8 +170,8 @@ class AddonManagerRepo: else: return False - def get_cached_icon_filename(self) ->str: - """ Get the filename for the locally-cached copy of the icon """ + def get_cached_icon_filename(self) -> str: + """Get the filename for the locally-cached copy of the icon""" if self.cached_icon_filename: return self.cached_icon_filename @@ -185,10 +193,16 @@ class AddonManagerRepo: subdir = wb.Name real_icon = subdir + wb.Icon - real_icon = real_icon.replace("/", os.path.sep) # Required path separator in the metadata.xml file to local separator + real_icon = real_icon.replace( + "/", os.path.sep + ) # Required path separator in the metadata.xml file to local separator _, file_extension = os.path.splitext(real_icon) - store = os.path.join(FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata") - self.cached_icon_filename = os.path.join(store, self.name, "cached_icon"+file_extension) + store = os.path.join( + FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata" + ) + self.cached_icon_filename = os.path.join( + store, self.name, "cached_icon" + file_extension + ) return self.cached_icon_filename diff --git a/src/Mod/AddonManager/InitGui.py b/src/Mod/AddonManager/InitGui.py index 993cf5ae1e..57650d3bb3 100644 --- a/src/Mod/AddonManager/InitGui.py +++ b/src/Mod/AddonManager/InitGui.py @@ -6,4 +6,4 @@ import AddonManager FreeCADGui.addLanguagePath(":/translations") -FreeCADGui.addCommand('Std_AddonMgr', AddonManager.CommandAddonManager()) +FreeCADGui.addCommand("Std_AddonMgr", AddonManager.CommandAddonManager()) diff --git a/src/Mod/AddonManager/addonmanager_macro.py b/src/Mod/AddonManager/addonmanager_macro.py index 6743a8d9d8..4b4273226c 100644 --- a/src/Mod/AddonManager/addonmanager_macro.py +++ b/src/Mod/AddonManager/addonmanager_macro.py @@ -1,25 +1,25 @@ # -*- coding: utf-8 -*- -#*************************************************************************** -#* * -#* Copyright (c) 2018 Gaël Écorchard * -#* * -#* This program is free software; you can redistribute it and/or modify * -#* it under the terms of the GNU Lesser General Public License (LGPL) * -#* as published by the Free Software Foundation; either version 2 of * -#* the License, or (at your option) any later version. * -#* for detail see the LICENCE text file. * -#* * -#* This program is distributed in the hope that it will be useful, * -#* but WITHOUT ANY WARRANTY; without even the implied warranty of * -#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -#* GNU Library General Public License for more details. * -#* * -#* You should have received a copy of the GNU Library General Public * -#* License along with this program; if not, write to the Free Software * -#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -#* USA * -#* * -#*************************************************************************** +# *************************************************************************** +# * * +# * Copyright (c) 2018 Gaël Écorchard * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** import os import re @@ -36,6 +36,7 @@ from addonmanager_utilities import remove_directory_if_empty try: from HTMLParser import HTMLParser + unescape = HTMLParser().unescape except ImportError: from html import unescape @@ -46,6 +47,7 @@ except ImportError: # different sources # @{ + class Macro(object): """This class provides a unified way to handle macros coming from different sources""" @@ -65,14 +67,14 @@ class Macro(object): return self.filename == other.filename @classmethod - def from_cache (self, cache_dict:Dict): + def from_cache(self, cache_dict: Dict): instance = Macro(cache_dict["name"]) - for key,value in cache_dict.items(): + for key, value in cache_dict.items(): instance.__dict__[key] = value return instance - def to_cache (self) -> Dict: - """ For cache purposes this entire class is dumped directly """ + def to_cache(self) -> Dict: + """For cache purposes this entire class is dumped directly""" return self.__dict__ @@ -85,7 +87,11 @@ class Macro(object): def is_installed(self): if self.on_git and not self.src_filename: return False - return (os.path.exists(os.path.join(FreeCAD.getUserMacroDir(True), self.filename)) or os.path.exists(os.path.join(FreeCAD.getUserMacroDir(True), "Macro_" + self.filename))) + return os.path.exists( + os.path.join(FreeCAD.getUserMacroDir(True), self.filename) + ) or os.path.exists( + os.path.join(FreeCAD.getUserMacroDir(True), "Macro_" + self.filename) + ) def fill_details_from_file(self, filename): with open(filename) as f: @@ -123,7 +129,11 @@ class Macro(object): code = "" u = urlopen(url) if u is None: - FreeCAD.Console.PrintWarning("AddonManager: Debug: connection is lost (proxy setting changed?)", url, "\n") + FreeCAD.Console.PrintWarning( + "AddonManager: Debug: connection is lost (proxy setting changed?)", + url, + "\n", + ) return p = u.read() if isinstance(p, bytes): @@ -132,12 +142,14 @@ class Macro(object): # check if the macro page has its code hosted elsewhere, download if # needed if "rawcodeurl" in p: - rawcodeurl = re.findall("rawcodeurl.*?href=\"(http.*?)\">", p) + rawcodeurl = re.findall('rawcodeurl.*?href="(http.*?)">', p) if rawcodeurl: rawcodeurl = rawcodeurl[0] u2 = urlopen(rawcodeurl) if u2 is None: - FreeCAD.Console.PrintWarning("AddonManager: Debug: unable to open URL", rawcodeurl, "\n") + FreeCAD.Console.PrintWarning( + "AddonManager: Debug: unable to open URL", rawcodeurl, "\n" + ) return # code = u2.read() # github is slow to respond... We need to use this trick below @@ -165,14 +177,27 @@ class Macro(object): code = unescape(code) code = code.replace(b"\xc2\xa0".decode("utf-8"), " ") else: - FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "Unable to fetch the code of this macro.") + "\n") + FreeCAD.Console.PrintWarning( + translate( + "AddonsInstaller", "Unable to fetch the code of this macro." + ) + + "\n" + ) - desc = re.findall(r"(.*?)", p.replace("\n", " ")) + desc = re.findall( + r"(.*?)", + p.replace("\n", " "), + ) if desc: desc = desc[0] else: - FreeCAD.Console.PrintWarning(translate("AddonsInstaller", - "Unable to retrieve a description for this macro.") + "\n") + FreeCAD.Console.PrintWarning( + translate( + "AddonsInstaller", + "Unable to retrieve a description for this macro.", + ) + + "\n" + ) desc = "No description available" self.desc = desc self.url = url @@ -184,7 +209,7 @@ class Macro(object): self.code = code self.parsed = True - def install(self, macro_dir:str) -> (bool, List[str]): + def install(self, macro_dir: str) -> (bool, List[str]): """Install a macro and all its related files Returns True if the macro was installed correctly. @@ -195,7 +220,7 @@ class Macro(object): """ if not self.code: - return False,["No code"] + return False, ["No code"] if not os.path.isdir(macro_dir): try: os.makedirs(macro_dir) @@ -203,7 +228,7 @@ class Macro(object): return False, [f"Failed to create {macro_dir}"] macro_path = os.path.join(macro_dir, self.filename) try: - with codecs.open(macro_path, 'w', 'utf-8') as macrofile: + with codecs.open(macro_path, "w", "utf-8") as macrofile: macrofile.write(self.code) except IOError: return False, [f"Failed to write {macro_path}"] @@ -226,11 +251,10 @@ class Macro(object): warnings.append(f"Failed to copy {src_file} to {dst_file}") if len(warnings) > 0: return False, warnings - + FreeCAD.Console.PrintMessage(f"Macro {self.name} was installed successfully.\n") return True, [] - def remove(self) -> bool: """Remove a macro and all its related files @@ -242,7 +266,7 @@ class Macro(object): return True macro_dir = FreeCAD.getUserMacroDir(True) macro_path = os.path.join(macro_dir, self.filename) - macro_path_with_macro_prefix = os.path.join(macro_dir, 'Macro_' + self.filename) + macro_path_with_macro_prefix = os.path.join(macro_dir, "Macro_" + self.filename) if os.path.exists(macro_path): os.remove(macro_path) elif os.path.exists(macro_path_with_macro_prefix): @@ -255,7 +279,10 @@ class Macro(object): os.remove(dst_file) remove_directory_if_empty(os.path.dirname(dst_file)) except Exception: - FreeCAD.Console.PrintWarning(f"Failed to remove macro file '{dst_file}': it might not exist, or its permissions changed\n") + FreeCAD.Console.PrintWarning( + f"Failed to remove macro file '{dst_file}': it might not exist, or its permissions changed\n" + ) return True + # @} diff --git a/src/Mod/AddonManager/addonmanager_metadata.py b/src/Mod/AddonManager/addonmanager_metadata.py index c6f40a8194..d1b1ec4c66 100644 --- a/src/Mod/AddonManager/addonmanager_metadata.py +++ b/src/Mod/AddonManager/addonmanager_metadata.py @@ -1,24 +1,24 @@ -#*************************************************************************** -#* * -#* Copyright (c) 2021 Chris Hennes * -#* * -#* This program is free software; you can redistribute it and/or modify * -#* it under the terms of the GNU Lesser General Public License (LGPL) * -#* as published by the Free Software Foundation; either version 2 of * -#* the License, or (at your option) any later version. * -#* for detail see the LICENCE text file. * -#* * -#* This program is distributed in the hope that it will be useful, * -#* but WITHOUT ANY WARRANTY; without even the implied warranty of * -#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -#* GNU Library General Public License for more details. * -#* * -#* You should have received a copy of the GNU Library General Public * -#* License along with this program; if not, write to the Free Software * -#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -#* USA * -#* * -#*************************************************************************** +# *************************************************************************** +# * * +# * Copyright (c) 2021 Chris Hennes * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** import FreeCAD @@ -32,19 +32,20 @@ from PySide2.QtCore import QObject import addonmanager_utilities as utils from AddonManagerRepo import AddonManagerRepo + class MetadataDownloadWorker(QObject): """A worker for downloading package.xml and associated icon(s) - + To use, instantiate an object of this class and call the start_fetch() function with a QNetworkAccessManager. It is expected that many of these objects will all be created and associated with the same QNAM, which will then handle the actual asynchronous downloads in some Qt-defined number of threads. To monitor progress you should connect to the QNAM's "finished" signal, and ensure it is called the number of times you expect based on how many workers you have enqueued. - + """ - updated = QtCore.Signal(AddonManagerRepo) + updated = QtCore.Signal(AddonManagerRepo) def __init__(self, parent, repo, index): "repo is an AddonManagerRepo object, and index is a dictionary of SHA1 hashes of the package.xml files in the cache" @@ -52,15 +53,19 @@ class MetadataDownloadWorker(QObject): super().__init__(parent) self.repo = repo self.index = index - self.store = os.path.join(FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata") + self.store = os.path.join( + FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata" + ) self.last_sha1 = "" self.url = self.repo.metadata_url def start_fetch(self, network_manager): - "Asynchronously begin the network access. Intended as a set-and-forget black box for downloading metadata." + "Asynchronously begin the network access. Intended as a set-and-forget black box for downloading metadata." self.request = QtNetwork.QNetworkRequest(QtCore.QUrl(self.url)) - self.request.setAttribute(QtNetwork.QNetworkRequest.RedirectPolicyAttribute, - QtNetwork.QNetworkRequest.UserVerifiedRedirectPolicy) + self.request.setAttribute( + QtNetwork.QNetworkRequest.RedirectPolicyAttribute, + QtNetwork.QNetworkRequest.UserVerifiedRedirectPolicy, + ) self.fetch_task = network_manager.get(self.request) self.fetch_task.finished.connect(self.resolve_fetch) @@ -84,7 +89,9 @@ class MetadataDownloadWorker(QObject): def resolve_fetch(self): "Called when the data fetch completed, either with an error, or if it found the metadata file" if self.fetch_task.error() == QtNetwork.QNetworkReply.NetworkError.NoError: - FreeCAD.Console.PrintMessage(f"Found a metadata file for {self.repo.name}\n") + FreeCAD.Console.PrintMessage( + f"Found a metadata file for {self.repo.name}\n" + ) self.repo.repo_type = AddonManagerRepo.RepoType.PACKAGE new_xml = self.fetch_task.readAll() hasher = hashlib.sha1() @@ -110,17 +117,25 @@ class MetadataDownloadWorker(QObject): # There is no local copy yet, so we definitely have to update # the cache self.update_local_copy(new_xml) - elif self.fetch_task.error() == QtNetwork.QNetworkReply.NetworkError.ContentNotFoundError: + elif ( + self.fetch_task.error() + == QtNetwork.QNetworkReply.NetworkError.ContentNotFoundError + ): pass - elif self.fetch_task.error() == QtNetwork.QNetworkReply.NetworkError.OperationCanceledError: + elif ( + self.fetch_task.error() + == QtNetwork.QNetworkReply.NetworkError.OperationCanceledError + ): pass else: - FreeCAD.Console.PrintWarning(f"Failed to connect to {self.url}:\n {self.fetch_task.error()}\n") + FreeCAD.Console.PrintWarning( + f"Failed to connect to {self.url}:\n {self.fetch_task.error()}\n" + ) def update_local_copy(self, new_xml): # We have to update the local copy of the metadata file and re-download # the icon file - + name = self.repo.name repo_url = self.repo.url package_cache_directory = os.path.join(self.store, name) diff --git a/src/Mod/AddonManager/addonmanager_utilities.py b/src/Mod/AddonManager/addonmanager_utilities.py index 0ac073e449..d5085c81dd 100644 --- a/src/Mod/AddonManager/addonmanager_utilities.py +++ b/src/Mod/AddonManager/addonmanager_utilities.py @@ -1,25 +1,25 @@ # -*- coding: utf-8 -*- -#*************************************************************************** -#* * -#* Copyright (c) 2018 Gaël Écorchard * -#* * -#* This program is free software; you can redistribute it and/or modify * -#* it under the terms of the GNU Lesser General Public License (LGPL) * -#* as published by the Free Software Foundation; either version 2 of * -#* the License, or (at your option) any later version. * -#* for detail see the LICENCE text file. * -#* * -#* This program is distributed in the hope that it will be useful, * -#* but WITHOUT ANY WARRANTY; without even the implied warranty of * -#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -#* GNU Library General Public License for more details. * -#* * -#* You should have received a copy of the GNU Library General Public * -#* License along with this program; if not, write to the Free Software * -#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -#* USA * -#* * -#*************************************************************************** +# *************************************************************************** +# * * +# * Copyright (c) 2018 Gaël Écorchard * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** import codecs import os @@ -49,9 +49,9 @@ except ImportError: pass else: try: - #ssl_ctx = ssl.create_default_context(cafile=certifi.where()) + # ssl_ctx = ssl.create_default_context(cafile=certifi.where()) ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) - #ssl_ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + # ssl_ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) except AttributeError: pass @@ -94,7 +94,7 @@ def symlink(source, link_name): raise ctypes.WinError() -def urlopen(url:str): +def urlopen(url: str): """Opens an url with urllib and streams it to a temp file""" timeout = 5 @@ -106,7 +106,7 @@ def urlopen(url:str): else: if pref.GetBool("SystemProxyCheck", False): proxy = urllib.request.getproxies() - proxies = {"http": proxy.get('http'), "https": proxy.get('http')} + proxies = {"http": proxy.get("http"), "https": proxy.get("http")} elif pref.GetBool("UserProxyCheck", False): proxy = pref.GetString("ProxyUrl", "") proxies = {"http": proxy, "https": proxy} @@ -120,8 +120,7 @@ def urlopen(url:str): urllib.request.install_opener(opener) # Url opening - req = urllib.request.Request(url, - headers={'User-Agent': "Magic Browser"}) + req = urllib.request.Request(url, headers={"User-Agent": "Magic Browser"}) try: u = urllib.request.urlopen(req, timeout=timeout) @@ -137,7 +136,7 @@ def urlopen(url:str): def getserver(url): """returns the server part of an url""" - return '{uri.scheme}://{uri.netloc}/'.format(uri=urlparse(url)) + return "{uri.scheme}://{uri.netloc}/".format(uri=urlparse(url)) def update_macro_details(old_macro, new_macro): @@ -149,19 +148,20 @@ def update_macro_details(old_macro, new_macro): """ if old_macro.on_git and new_macro.on_git: - FreeCAD.Console.PrintWarning('The macro "{}" is present twice in github, please report'.format(old_macro.name)) + FreeCAD.Console.PrintWarning( + 'The macro "{}" is present twice in github, please report'.format( + old_macro.name + ) + ) # We don't report macros present twice on the wiki because a link to a # macro is considered as a macro. For example, 'Perpendicular To Wire' # appears twice, as of 2018-05-05). old_macro.on_wiki = new_macro.on_wiki - for attr in ['desc', 'url', 'code']: + for attr in ["desc", "url", "code"]: if not hasattr(old_macro, attr): setattr(old_macro, attr, getattr(new_macro, attr)) - - - def remove_directory_if_empty(dir): """Remove the directory if it is empty @@ -179,7 +179,9 @@ def restart_freecad(): args = QtWidgets.QApplication.arguments()[1:] if FreeCADGui.getMainWindow().close(): - QtCore.QProcess.startDetached(QtWidgets.QApplication.applicationFilePath(), args) + QtCore.QProcess.startDetached( + QtWidgets.QApplication.applicationFilePath(), args + ) def get_zip_url(repo): @@ -188,15 +190,23 @@ def get_zip_url(repo): parsedUrl = urlparse(repo.url) if parsedUrl.netloc == "github.com": return f"{repo.url}/archive/{repo.branch}.zip" - elif parsedUrl.netloc == "framagit.org" or parsedUrl.netloc == "gitlab.com" or parsedUrl.netloc == "salsa.debian.org": + elif ( + parsedUrl.netloc == "framagit.org" + or parsedUrl.netloc == "gitlab.com" + or parsedUrl.netloc == "salsa.debian.org" + ): # https://framagit.org/freecad-france/mooc-workbench/-/archive/master/mooc-workbench-master.zip # https://salsa.debian.org/mess42/pyrate/-/archive/master/pyrate-master.zip reponame = baseurl.strip("/").split("/")[-1] return f"{repo.url}/-/archive/{repo.branch}/{repo.name}-{repo.branch}.zip" else: - FreeCAD.Console.PrintWarning("Debug: addonmanager_utilities.get_zip_url: Unknown git host:", parsedUrl.netloc) + FreeCAD.Console.PrintWarning( + "Debug: addonmanager_utilities.get_zip_url: Unknown git host:", + parsedUrl.netloc, + ) return None + def construct_git_url(repo, filename): "Returns a direct download link to a file in an online Git repo: works with github, gitlab, and framagit" @@ -209,14 +219,19 @@ def construct_git_url(repo, filename): # e.g. https://salsa.debian.org/joha2/pyrate/-/raw/master/package.xml return f"{repo.url}/-/raw/{repo.branch}/{filename}" else: - FreeCAD.Console.PrintLog("Debug: addonmanager_utilities.construct_git_url: Unknown git host:" + parsed_url.netloc) + FreeCAD.Console.PrintLog( + "Debug: addonmanager_utilities.construct_git_url: Unknown git host:" + + parsed_url.netloc + ) return None + def get_readme_url(repo): "Returns the location of a readme file" return construct_git_url(repo, "README.md") + def get_metadata_url(url): "Returns the location of a package.xml metadata file" @@ -230,9 +245,15 @@ def get_desc_regex(repo): parsedUrl = urlparse(repo.url) if parsedUrl.netloc == "github.com": return r'' - FreeCAD.Console.PrintWarning("Debug: addonmanager_utilities.get_desc_regex: Unknown git host:", repo.url) + FreeCAD.Console.PrintWarning( + "Debug: addonmanager_utilities.get_desc_regex: Unknown git host:", repo.url + ) return None @@ -263,14 +284,16 @@ def fix_relative_links(text, base_url): new_text = "" for line in text.splitlines(): - for link in (re.findall(r"!\[.*?\]\((.*?)\)", line) + - re.findall(r"src\s*=\s*[\"'](.+?)[\"']", line)): - parts = link.split('/') + for link in re.findall(r"!\[.*?\]\((.*?)\)", line) + re.findall( + r"src\s*=\s*[\"'](.+?)[\"']", line + ): + parts = link.split("/") if len(parts) < 2 or not re.match(r"^http|^www|^.+\.|^/", parts[0]): - newlink = os.path.join(base_url, link.lstrip('./')) + newlink = os.path.join(base_url, link.lstrip("./")) line = line.replace(link, newlink) FreeCAD.Console.PrintLog("Debug: replaced " + link + " with " + newlink) - new_text = new_text + '\n' + line + new_text = new_text + "\n" + line return new_text + # @} diff --git a/src/Mod/AddonManager/addonmanager_workers.py b/src/Mod/AddonManager/addonmanager_workers.py index 986d076818..2d03fcef7a 100644 --- a/src/Mod/AddonManager/addonmanager_workers.py +++ b/src/Mod/AddonManager/addonmanager_workers.py @@ -1,26 +1,26 @@ # -*- coding: utf-8 -*- -#*************************************************************************** -#* * -#* Copyright (c) 2019 Yorik van Havre * -#* Copyright (c) 2021 Chris Hennes * -#* * -#* This program is free software; you can redistribute it and/or modify * -#* it under the terms of the GNU Lesser General Public License (LGPL) * -#* as published by the Free Software Foundation; either version 2 of * -#* the License, or (at your option) any later version. * -#* for detail see the LICENCE text file. * -#* * -#* This program is distributed in the hope that it will be useful, * -#* but WITHOUT ANY WARRANTY; without even the implied warranty of * -#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -#* GNU Library General Public License for more details. * -#* * -#* You should have received a copy of the GNU Library General Public * -#* License along with this program; if not, write to the Free Software * -#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -#* USA * -#* * -#*************************************************************************** +# *************************************************************************** +# * * +# * Copyright (c) 2019 Yorik van Havre * +# * Copyright (c) 2021 Chris Hennes * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** import os import re @@ -36,6 +36,7 @@ from typing import Union, List from PySide2 import QtCore, QtGui, QtNetwork import FreeCAD + if FreeCAD.GuiUp: import FreeCADGui @@ -49,15 +50,17 @@ translate = FreeCAD.Qt.translate have_git = False try: import git + # some versions of git module have no "Repo" class?? Bug #4072 module # 'git' has no attribute 'Repo' - have_git = hasattr(git,"Repo") + have_git = hasattr(git, "Repo") except ImportError: pass have_zip = False try: import zipfile + have_zip = True except ImportError: pass @@ -65,6 +68,7 @@ except ImportError: have_markdown = False try: import markdown + have_markdown = True except ImportError: pass @@ -74,8 +78,8 @@ except ImportError: # \brief Multithread workers for the addon manager # @{ -# Blacklisted addons -macros_blacklist = [] +# reject_listed addons +macros_reject_list = [] # These addons will print an additional message informing the user obsolete = [] @@ -105,8 +109,10 @@ class UpdateWorker(QtCore.QThread): self.current_thread = QtCore.QThread.currentThread() # update info lists - global obsolete, macros_blacklist, py2only - u = utils.urlopen("https://raw.githubusercontent.com/FreeCAD/FreeCAD-addons/master/addonflags.json") + global obsolete, macros_reject_list, py2only + u = utils.urlopen( + "https://raw.githubusercontent.com/FreeCAD/FreeCAD-addons/master/addonflags.json" + ) if u: p = u.read() u.close() @@ -114,8 +120,8 @@ class UpdateWorker(QtCore.QThread): if "obsolete" in j and "Mod" in j["obsolete"]: obsolete = j["obsolete"]["Mod"] - if "blacklisted" in j and "Macro" in j["blacklisted"]: - macros_blacklist = j["blacklisted"]["Macro"] + if "reject_listed" in j and "Macro" in j["reject_listed"]: + macros_reject_list = j["reject_listed"]["Macro"] if "py2only" in j and "Mod" in j["py2only"]: py2only = j["py2only"]["Mod"] @@ -127,21 +133,26 @@ class UpdateWorker(QtCore.QThread): package_names = [] # querying custom addons first - addon_list = (FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") - .GetString("CustomRepositories", "").split("\n")) + addon_list = ( + FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") + .GetString("CustomRepositories", "") + .split("\n") + ) custom_addons = [] for addon in addon_list: if " " in addon: addon_and_branch = addon.split(" ") - custom_addons.append({"url":addon_and_branch[0], "branch":addon_and_branch[1]}) + custom_addons.append( + {"url": addon_and_branch[0], "branch": addon_and_branch[1]} + ) else: - custom_addons.append({"url":addon, "branch":"master"}) + custom_addons.append({"url": addon, "branch": "master"}) for addon in custom_addons: if self.current_thread.isInterruptionRequested(): return if addon and addon["url"]: if addon["url"][-1] == "/": - addon["url"] = addon["url"][0:-1] # Strip trailing slash + addon["url"] = addon["url"][0:-1] # Strip trailing slash name = addon["url"].split("/")[-1] if name.lower().endswith(".git"): name = name[:-4] @@ -153,10 +164,14 @@ class UpdateWorker(QtCore.QThread): state = AddonManagerRepo.UpdateStatus.UNCHECKED else: state = AddonManagerRepo.UpdateStatus.NOT_INSTALLED - self.addon_repo.emit(AddonManagerRepo(name, addon["url"], state, addon["branch"])) + self.addon_repo.emit( + AddonManagerRepo(name, addon["url"], state, addon["branch"]) + ) # querying official addons - u = utils.urlopen("https://raw.githubusercontent.com/FreeCAD/FreeCAD-addons/master/.gitmodules") + u = utils.urlopen( + "https://raw.githubusercontent.com/FreeCAD/FreeCAD-addons/master/.gitmodules" + ) if not u: self.done.emit() self.stop = True @@ -165,10 +180,15 @@ class UpdateWorker(QtCore.QThread): if isinstance(p, bytes): p = p.decode("utf-8") u.close() - p = re.findall((r'(?m)\[submodule\s*"(?P.*)"\]\s*' - r"path\s*=\s*(?P.+)\s*" - r"url\s*=\s*(?Phttps?://.*)\s*" - r"(branch\s*=\s*(?P[^\s]*)\s*)?"), p) + p = re.findall( + ( + r'(?m)\[submodule\s*"(?P.*)"\]\s*' + r"path\s*=\s*(?P.+)\s*" + r"url\s*=\s*(?Phttps?://.*)\s*" + r"(branch\s*=\s*(?P[^\s]*)\s*)?" + ), + p, + ) for name, path, url, _, branch in p: if self.current_thread.isInterruptionRequested(): return @@ -185,24 +205,29 @@ class UpdateWorker(QtCore.QThread): else: state = AddonManagerRepo.UpdateStatus.NOT_INSTALLED self.addon_repo.emit(AddonManagerRepo(name, url, state, branch)) - - self.status_message.emit(translate("AddonsInstaller", "Workbenches list was updated.")) - + + self.status_message.emit( + translate("AddonsInstaller", "Workbenches list was updated.") + ) + if not self.current_thread.isInterruptionRequested(): self.done.emit() self.stop = True + class LoadPackagesFromCacheWorker(QtCore.QThread): addon_repo = QtCore.Signal(object) done = QtCore.Signal() - def __init__(self, cache_file:str): + def __init__(self, cache_file: str): QtCore.QThread.__init__(self) self.cache_file = cache_file def run(self): - metadata_cache_path = os.path.join(FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata") - with open(self.cache_file,"r") as f: + metadata_cache_path = os.path.join( + FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata" + ) + with open(self.cache_file, "r") as f: data = f.read() if data: dict_data = json.loads(data) @@ -210,7 +235,9 @@ class LoadPackagesFromCacheWorker(QtCore.QThread): if QtCore.QThread.currentThread().isInterruptionRequested(): return repo = AddonManagerRepo.from_cache(item) - repo_metadata_cache_path = os.path.join(metadata_cache_path, repo.name, "package.xml") + repo_metadata_cache_path = os.path.join( + metadata_cache_path, repo.name, "package.xml" + ) if os.path.isfile(repo_metadata_cache_path): try: repo.metadata = FreeCAD.Metadata(repo_metadata_cache_path) @@ -219,16 +246,17 @@ class LoadPackagesFromCacheWorker(QtCore.QThread): self.addon_repo.emit(repo) self.done.emit() + class LoadMacrosFromCacheWorker(QtCore.QThread): add_macro_signal = QtCore.Signal(object) done = QtCore.Signal() - def __init__(self, cache_file:str): + def __init__(self, cache_file: str): QtCore.QThread.__init__(self) self.cache_file = cache_file def run(self): - with open(self.cache_file,"r") as f: + with open(self.cache_file, "r") as f: data = f.read() dict_data = json.loads(data) for item in dict_data: @@ -238,6 +266,7 @@ class LoadMacrosFromCacheWorker(QtCore.QThread): self.add_macro_signal.emit(AddonManagerRepo.from_macro(new_macro)) self.done.emit() + class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): """This worker checks for available updates for all workbenches""" @@ -245,7 +274,7 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): progress_made = QtCore.Signal(int, int) done = QtCore.Signal() - def __init__(self, repos:List[AddonManagerRepo]): + def __init__(self, repos: List[AddonManagerRepo]): QtCore.QThread.__init__(self) self.repos = repos @@ -275,7 +304,6 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): elif repo.repo_type == AddonManagerRepo.RepoType.PACKAGE: self.check_package(repo) - self.stop = True self.done.emit() @@ -287,15 +315,22 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): # mark as already installed AND already checked for updates if not os.path.exists(clonedir + os.sep + ".git"): # Repair addon installed with raw download - bare_repo = git.Repo.clone_from(wb.url, clonedir + os.sep + ".git", bare=True) + bare_repo = git.Repo.clone_from( + wb.url, clonedir + os.sep + ".git", bare=True + ) try: with bare_repo.config_writer() as cw: cw.set("core", "bare", False) except AttributeError: if not gitpython_warning: - FreeCAD.Console.PrintWarning(translate("AddonsInstaller", - "Outdated GitPython detected, " - "consider upgrading with pip.") + "\n") + FreeCAD.Console.PrintWarning( + translate( + "AddonsInstaller", + "Outdated GitPython detected, " + "consider upgrading with pip.", + ) + + "\n" + ) gitpython_warning = True cw = bare_repo.config_writer() cw.set("core", "bare", False) @@ -306,19 +341,32 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): try: gitrepo.fetch() except Exception: - FreeCAD.Console.PrintWarning("AddonManager: " + translate("AddonsInstaller","Unable to fetch git updates for workbench") + " " + wb.name) + FreeCAD.Console.PrintWarning( + "AddonManager: " + + translate( + "AddonsInstaller", "Unable to fetch git updates for workbench" + ) + + " " + + wb.name + ) else: try: if "git pull" in gitrepo.status(): - wb.update_status = AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE + wb.update_status = ( + AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE + ) else: - wb.update_status = AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE + wb.update_status = ( + AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE + ) self.update_status.emit(wb) except Exception: - FreeCAD.Console.PrintWarning("AddonManager - " + wb.name + " git status" - " fatal: this operation must be run in a work tree \n") + FreeCAD.Console.PrintWarning( + "AddonManager - " + wb.name + " git status" + " fatal: this operation must be run in a work tree \n" + ) - def check_package(self, package:AddonManagerRepo) -> None: + def check_package(self, package: AddonManagerRepo) -> None: clonedir = self.moddir + os.sep + package.name if os.path.exists(clonedir): installed_metadata_file = os.path.join(clonedir, "package.xml") @@ -331,26 +379,34 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): self.update_status.emit(package) return else: - package.updated_timestamp = os.path.getmtime(installed_metadata_file) + package.updated_timestamp = os.path.getmtime(installed_metadata_file) try: installed_metadata = FreeCAD.Metadata(installed_metadata_file) package.installed_version = installed_metadata.Version # Packages are considered up-to-date if the metadata version matches. Authors should update # their version string when they want the addon manager to alert users of a new version. if package.metadata.Version != installed_metadata.Version: - package.update_status = AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE + package.update_status = ( + AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE + ) else: - package.update_status = AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE + package.update_status = ( + AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE + ) self.update_status.emit(package) except Exception as e: - FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "Failed to read metadata from") + f" {installed_metadata_file}") + FreeCAD.Console.PrintWarning( + translate("AddonsInstaller", "Failed to read metadata from") + + f" {installed_metadata_file}" + ) - - def check_macro(self, macro_wrapper:AddonManagerRepo) -> None: + def check_macro(self, macro_wrapper: AddonManagerRepo) -> None: # Make sure this macro has its code downloaded: try: if not macro_wrapper.macro.parsed and macro_wrapper.macro.on_git: - macro_wrapper.macro.fill_details_from_file(macro_wrapper.macro.src_filename) + macro_wrapper.macro.fill_details_from_file( + macro_wrapper.macro.src_filename + ) if not macro_wrapper.macro.parsed and macro_wrapper.macro.on_wiki: mac = macro_wrapper.macro.name.replace(" ", "_") mac = mac.replace("&", "%26") @@ -358,15 +414,21 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): url = "https://wiki.freecad.org/Macro_" + mac macro_wrapper.macro.fill_details_from_wiki(url) except Exception: - FreeCAD.Console.PrintWarning(f"Failed to fetch code for macro '{macro_wrapper.macro.name}'\n") + FreeCAD.Console.PrintWarning( + f"Failed to fetch code for macro '{macro_wrapper.macro.name}'\n" + ) return hasher1 = hashlib.sha1() hasher2 = hashlib.sha1() hasher1.update(macro_wrapper.macro.code.encode("utf-8")) new_sha1 = hasher1.hexdigest() - test_file_one = os.path.join(FreeCAD.getUserMacroDir(True), macro_wrapper.macro.filename) - test_file_two = os.path.join(FreeCAD.getUserMacroDir(True), "Macro_" + macro_wrapper.macro.filename) + test_file_one = os.path.join( + FreeCAD.getUserMacroDir(True), macro_wrapper.macro.filename + ) + test_file_two = os.path.join( + FreeCAD.getUserMacroDir(True), "Macro_" + macro_wrapper.macro.filename + ) if os.path.exists(test_file_one): with open(test_file_one, "rb") as f: contents = f.read() @@ -380,11 +442,13 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): else: return if new_sha1 == old_sha1: - macro_wrapper.update_status = AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE + macro_wrapper.update_status = ( + AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE + ) else: macro_wrapper.update_status = AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE self.update_status.emit(macro_wrapper) - + class FillMacroListWorker(QtCore.QThread): """This worker populates the list of macros""" @@ -402,21 +466,33 @@ class FillMacroListWorker(QtCore.QThread): def run(self): """Populates the list of macros""" - + self.current_thread = QtCore.QThread.currentThread() if not self.current_thread.isInterruptionRequested(): - self.status_message_signal.emit(translate("AddonInstaller","Retrieving macros from FreeCAD/FreeCAD-Macros Git repository")) + self.status_message_signal.emit( + translate( + "AddonInstaller", + "Retrieving macros from FreeCAD/FreeCAD-Macros Git repository", + ) + ) self.retrieve_macros_from_git() if not self.current_thread.isInterruptionRequested(): - self.status_message_signal.emit(translate("AddonInstaller","Retrieving macros from FreeCAD/FreeCAD-Macros Git repository")) + self.status_message_signal.emit( + translate( + "AddonInstaller", + "Retrieving macros from FreeCAD/FreeCAD-Macros Git repository", + ) + ) self.retrieve_macros_from_wiki() if self.current_thread.isInterruptionRequested(): return - self.status_message_signal.emit(translate("AddonsInstaller", "Done locating macros.")) + self.status_message_signal.emit( + translate("AddonsInstaller", "Done locating macros.") + ) self.stop = True self.done.emit() @@ -428,19 +504,33 @@ class FillMacroListWorker(QtCore.QThread): """ if not have_git or NOGIT: - self.status_message_signal.emit("GitPython not installed! Cannot retrieve macros from Git") - FreeCAD.Console.PrintWarning(translate("AddonsInstaller", - "GitPython not installed! Cannot retrieve macros from git") + "\n") + self.status_message_signal.emit( + "GitPython not installed! Cannot retrieve macros from Git" + ) + FreeCAD.Console.PrintWarning( + translate( + "AddonsInstaller", + "GitPython not installed! Cannot retrieve macros from git", + ) + + "\n" + ) return try: # TODO: someday see if the directory exists, and do a pull instead # of a clone - git.Repo.clone_from("https://github.com/FreeCAD/FreeCAD-macros.git", self.repo_dir) + git.Repo.clone_from( + "https://github.com/FreeCAD/FreeCAD-macros.git", self.repo_dir + ) except Exception: - FreeCAD.Console.PrintWarning(translate("AddonsInstaller", - "Something went wrong with the Git Macro Retrieval, " - "possibly the Git executable is not in the path") + "\n") + FreeCAD.Console.PrintWarning( + translate( + "AddonsInstaller", + "Something went wrong with the Git Macro Retrieval, " + "possibly the Git executable is not in the path", + ) + + "\n" + ) n_files = 0 for _, _, filenames in os.walk(self.repo_dir): n_files += len(filenames) @@ -472,9 +562,14 @@ class FillMacroListWorker(QtCore.QThread): u = utils.urlopen("https://wiki.freecad.org/Macros_recipes") if not u: - FreeCAD.Console.PrintWarning(translate("AddonsInstaller", - "There appears to be an issue connecting to the Wiki, " - "therefore FreeCAD cannot retrieve the Wiki macro list at this time") + "\n") + FreeCAD.Console.PrintWarning( + translate( + "AddonsInstaller", + "There appears to be an issue connecting to the Wiki, " + "therefore FreeCAD cannot retrieve the Wiki macro list at this time", + ) + + "\n" + ) return p = u.read() u.close() @@ -491,7 +586,11 @@ class FillMacroListWorker(QtCore.QThread): macname = macname.replace("&", "&") if not macname: continue - if (macname not in macros_blacklist) and ("recipes" not in macname.lower()) and (macname not in macro_names): + if ( + (macname not in macros_reject_list) + and ("recipes" not in macname.lower()) + and (macname not in macro_names) + ): macro_names.append(macname) macro = Macro(macname) macro.on_wiki = True @@ -515,10 +614,14 @@ class ShowWorker(QtCore.QThread): self.cache_path = cache_path def run(self): - self.status_message.emit(translate("AddonsInstaller", "Retrieving description...")) + self.status_message.emit( + translate("AddonsInstaller", "Retrieving description...") + ) u = None url = self.repo.url - self.status_message.emit(translate("AddonsInstaller", "Retrieving info from") + " " + str(url)) + self.status_message.emit( + translate("AddonsInstaller", "Retrieving info from") + " " + str(url) + ) desc = "" regex = utils.get_readme_regex(self.repo) if regex: @@ -528,7 +631,9 @@ class ShowWorker(QtCore.QThread): FreeCAD.Console.PrintWarning(f"Debug: README not found for {url}\n") u = utils.urlopen(readmeurl) if not u: - FreeCAD.Console.PrintWarning(f"Debug: README not found at {readmeurl}\n") + FreeCAD.Console.PrintWarning( + f"Debug: README not found at {readmeurl}\n" + ) u = utils.urlopen(readmeurl) if u: p = u.read() @@ -539,7 +644,9 @@ class ShowWorker(QtCore.QThread): if readme: desc = readme[0] else: - FreeCAD.Console.PrintWarning(f"Debug: README not found at {readmeurl}\n") + FreeCAD.Console.PrintWarning( + f"Debug: README not found at {readmeurl}\n" + ) else: # convert raw markdown using lib readmeurl = utils.get_readme_url(self.repo) @@ -561,7 +668,9 @@ class ShowWorker(QtCore.QThread): """ message += translate("AddonsInstaller", "Raw markdown displayed") message += "

" - message += translate("AddonsInstaller", "Python Markdown library is missing.") + message += translate( + "AddonsInstaller", "Python Markdown library is missing." + ) message += "

" + desc + "
" desc = message else: @@ -593,18 +702,27 @@ class ShowWorker(QtCore.QThread): # checking for updates if not NOGIT and have_git: repo = self.repo - clonedir = FreeCAD.getUserAppDataDir() + os.sep + "Mod" + os.sep + repo.name + clonedir = ( + FreeCAD.getUserAppDataDir() + os.sep + "Mod" + os.sep + repo.name + ) if os.path.exists(clonedir): if not os.path.exists(clonedir + os.sep + ".git"): # Repair addon installed with raw download - bare_repo = git.Repo.clone_from(repo.url, clonedir + os.sep + ".git", bare=True) + bare_repo = git.Repo.clone_from( + repo.url, clonedir + os.sep + ".git", bare=True + ) try: with bare_repo.config_writer() as cw: cw.set("core", "bare", False) except AttributeError: - FreeCAD.Console.PrintWarning(translate("AddonsInstaller", - "Outdated GitPython detected, " - "consider upgrading with pip.") + "\n") + FreeCAD.Console.PrintWarning( + translate( + "AddonsInstaller", + "Outdated GitPython detected, " + "consider upgrading with pip.", + ) + + "\n" + ) cw = bare_repo.config_writer() cw.set("core", "bare", False) del cw @@ -617,9 +735,11 @@ class ShowWorker(QtCore.QThread): if upd: self.repo.update_status = AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE else: - self.repo.update_status = AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE + self.repo.update_status = ( + AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE + ) self.update_status.emit(self.repo) - + if QtCore.QThread.currentThread().isInterruptionRequested(): return @@ -629,11 +749,20 @@ class ShowWorker(QtCore.QThread):
""" - message += translate("AddonsInstaller", "This addon is marked as obsolete") + "

" - message += translate("AddonsInstaller", - "This usually means it is no longer maintained, " - "and some more advanced addon in this list " - "provides the same functionality.") + "

" + desc + message += ( + translate("AddonsInstaller", "This addon is marked as obsolete") + + "

" + ) + message += ( + translate( + "AddonsInstaller", + "This usually means it is no longer maintained, " + "and some more advanced addon in this list " + "provides the same functionality.", + ) + + "

" + + desc + ) # If the Addon is Python 2 only, let the user know through the Addon UI if self.repo.name in py2only: @@ -641,11 +770,16 @@ class ShowWorker(QtCore.QThread):
""" - message += translate("AddonsInstaller", "This addon is marked as Python 2 Only") + "

" - message += translate("AddonsInstaller", - "This workbench may no longer be maintained and " - "installing it on a Python 3 system will more than " - "likely result in errors at startup or while in use.") + message += ( + translate("AddonsInstaller", "This addon is marked as Python 2 Only") + + "

" + ) + message += translate( + "AddonsInstaller", + "This workbench may no longer be maintained and " + "installing it on a Python 3 system will more than " + "likely result in errors at startup or while in use.", + ) message += "

" + desc if QtCore.QThread.currentThread().isInterruptionRequested(): @@ -672,14 +806,16 @@ class ShowWorker(QtCore.QThread): # here, and replace the image link in the html code with the # downloaded version - imagepaths = re.findall(" 300) or (img.height() > 300): pix = QtGui.QPixmap() - pix = pix.fromImage(img.scaled(300, 300, - QtCore.Qt.KeepAspectRatio, - QtCore.Qt.FastTransformation)) + pix = pix.fromImage( + img.scaled( + 300, + 300, + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.FastTransformation, + ) + ) pix.save(storename, "jpeg", 100) - message = message.replace("src=\"" + origpath, "src=\"file:///" + storename.replace("\\", "/")) - os.remove(os.path.join(store,"download_in_progress")) + message = message.replace( + 'src="' + origpath, + 'src="file:///' + storename.replace("\\", "/"), + ) + os.remove(os.path.join(store, "download_in_progress")) return message return None @@ -742,22 +889,43 @@ class GetMacroDetailsWorker(QtCore.QThread): def run(self): - self.status_message.emit(translate("AddonsInstaller", "Retrieving macro description...")) + self.status_message.emit( + translate("AddonsInstaller", "Retrieving macro description...") + ) if not self.macro.parsed and self.macro.on_git: - self.status_message.emit(translate("AddonsInstaller", "Retrieving info from git")) + self.status_message.emit( + translate("AddonsInstaller", "Retrieving info from git") + ) self.macro.fill_details_from_file(self.macro.src_filename) if not self.macro.parsed and self.macro.on_wiki: - self.status_message.emit(translate("AddonsInstaller", "Retrieving info from wiki")) + self.status_message.emit( + translate("AddonsInstaller", "Retrieving info from wiki") + ) mac = self.macro.name.replace(" ", "_") mac = mac.replace("&", "%26") mac = mac.replace("+", "%2B") url = "https://wiki.freecad.org/Macro_" + mac self.macro.fill_details_from_wiki(url) if self.macro.is_installed(): - already_installed_msg = ('' + translate("AddonsInstaller", "This macro is already installed.") + '
') + already_installed_msg = ( + '' + + translate("AddonsInstaller", "This macro is already installed.") + + "
" + ) else: already_installed_msg = "" - message = (already_installed_msg + "

" + self.macro.name + "

" + self.macro.desc + "

Macro location: " + self.macro.url + "") + message = ( + already_installed_msg + + "

" + + self.macro.name + + "

" + + self.macro.desc + + '

Macro location: ' + + self.macro.url + + "" + ) if QtCore.QThread.currentThread().isInterruptionRequested(): return self.readme_updated.emit(message) @@ -773,29 +941,39 @@ class InstallWorkbenchWorker(QtCore.QThread): success = QtCore.Signal(AddonManagerRepo, str) failure = QtCore.Signal(AddonManagerRepo, str) - def __init__(self, repo:AddonManagerRepo): + def __init__(self, repo: AddonManagerRepo): QtCore.QThread.__init__(self) self.repo = repo if have_git and not NOGIT: self.git_progress = GitProgressMonitor() # TODO: What is wrong with these? - #self.git_progress.progress_made.connect(self.progress_made.emit) - #self.git_progress.info_message.connect(self.status_message.emit) + # self.git_progress.progress_made.connect(self.progress_made.emit) + # self.git_progress.info_message.connect(self.status_message.emit) def run(self): "installs or updates the selected addon" - + if not self.repo: return if not have_git or NOGIT: - FreeCAD.Console.PrintWarning(translate("AddonsInstaller", - "GitPython not found. Using ZIP file download instead.") + "\n") + FreeCAD.Console.PrintWarning( + translate( + "AddonsInstaller", + "GitPython not found. Using ZIP file download instead.", + ) + + "\n" + ) if not have_zip: - FreeCAD.Console.PrintError(translate("AddonsInstaller", - "Your version of python doesn't appear to support ZIP " - "files. Unable to proceed.") + "\n") + FreeCAD.Console.PrintError( + translate( + "AddonsInstaller", + "Your version of python doesn't appear to support ZIP " + "files. Unable to proceed.", + ) + + "\n" + ) return basedir = FreeCAD.getUserAppDataDir() @@ -811,36 +989,52 @@ class InstallWorkbenchWorker(QtCore.QThread): self.stop = True - def run_git(self, clonedir:str) -> None: + def run_git(self, clonedir: str) -> None: if NOGIT or not have_git: - FreeCAD.Console.PrintWarning(translate("AddonsInstaller", - "No Git Python installed, skipping git operations") + "\n") + FreeCAD.Console.PrintWarning( + translate( + "AddonsInstaller", + "No Git Python installed, skipping git operations", + ) + + "\n" + ) return - if os.path.exists(clonedir): self.run_git_update(clonedir) else: self.run_git_clone(clonedir) - def run_git_update(self, clonedir:str) -> None: + def run_git_update(self, clonedir: str) -> None: self.status_message.emit("Updating module...") if str(self.repo.name) in py2only: - FreeCAD.Console.PrintWarning(translate("AddonsInstaller", - "User requested updating a Python 2 workbench on " - "a system running Python 3 - ") + str(self.repo.name) + "\n") + FreeCAD.Console.PrintWarning( + translate( + "AddonsInstaller", + "User requested updating a Python 2 workbench on " + "a system running Python 3 - ", + ) + + str(self.repo.name) + + "\n" + ) if not os.path.exists(clonedir + os.sep + ".git"): # Repair addon installed with raw download by adding the .git # directory to it - bare_repo = git.Repo.clone_from(self.repo.url, clonedir + os.sep + ".git", bare=True) + bare_repo = git.Repo.clone_from( + self.repo.url, clonedir + os.sep + ".git", bare=True + ) try: with bare_repo.config_writer() as cw: cw.set("core", "bare", False) except AttributeError: - FreeCAD.Console.PrintWarning(translate("AddonsInstaller", - "Outdated GitPython detected, consider " - "upgrading with pip.") + "\n") + FreeCAD.Console.PrintWarning( + translate( + "AddonsInstaller", + "Outdated GitPython detected, consider " "upgrading with pip.", + ) + + "\n" + ) cw = bare_repo.config_writer() cw.set("core", "bare", False) del cw @@ -849,12 +1043,18 @@ class InstallWorkbenchWorker(QtCore.QThread): repo = git.Git(clonedir) try: repo.pull() - answer = translate("AddonsInstaller","Workbench successfully updated. " - "Please restart FreeCAD to apply the changes.") + answer = translate( + "AddonsInstaller", + "Workbench successfully updated. " + "Please restart FreeCAD to apply the changes.", + ) except Exception: - answer = translate("AddonsInstaller", "Error updating module ") + \ - self.repo.name + " - " + \ - translate("AddonsInstaller", "Please fix manually") + answer = ( + translate("AddonsInstaller", "Error updating module ") + + self.repo.name + + " - " + + translate("AddonsInstaller", "Please fix manually") + ) answer += repo.status() self.failure.emit(self.repo, answer) else: @@ -865,15 +1065,21 @@ class InstallWorkbenchWorker(QtCore.QThread): submodule.update(init=True, recursive=True) self.update_metadata() self.success.emit(self.repo, answer) - - def run_git_clone(self, clonedir:str) -> None: + + def run_git_clone(self, clonedir: str) -> None: self.status_message.emit("Checking module dependencies...") depsok, answer = self.check_python_dependencies(self.repo.url) if depsok: if str(self.repo.name) in py2only: - FreeCAD.Console.PrintWarning(translate("AddonsInstaller", - "User requested installing a Python 2 " - "workbench on a system running Python 3 - ") + str(self.repo.name) + "\n") + FreeCAD.Console.PrintWarning( + translate( + "AddonsInstaller", + "User requested installing a Python 2 " + "workbench on a system running Python 3 - ", + ) + + str(self.repo.name) + + "\n" + ) self.status_message.emit("Cloning module...") repo = git.Repo.clone_from(self.repo.url, clonedir) @@ -883,14 +1089,16 @@ class InstallWorkbenchWorker(QtCore.QThread): if self.repo.branch in repo.heads: repo.heads[self.repo.branch].checkout() - - answer = translate("AddonsInstaller", - "Workbench successfully installed. Please restart " - "FreeCAD to apply the changes.") + + answer = translate( + "AddonsInstaller", + "Workbench successfully installed. Please restart " + "FreeCAD to apply the changes.", + ) else: self.emit.failure(self.repo, answer) return - + if self.repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH: # symlink any macro contained in the module to the macros folder macro_dir = FreeCAD.getUserMacroDir(True) @@ -899,16 +1107,22 @@ class InstallWorkbenchWorker(QtCore.QThread): if os.path.exists(clonedir): for f in os.listdir(clonedir): if f.lower().endswith(".fcmacro"): - utils.symlink(os.path.join(clonedir, f), os.path.join(macro_dir, f)) - FreeCAD.ParamGet('User parameter:Plugins/' + self.repo.name).SetString("destination", clonedir) - answer += "\n\n" + translate("AddonsInstaller", - "A macro has been installed and is available " - "under Macro -> Macros menu") + utils.symlink( + os.path.join(clonedir, f), os.path.join(macro_dir, f) + ) + FreeCAD.ParamGet( + "User parameter:Plugins/" + self.repo.name + ).SetString("destination", clonedir) + answer += "\n\n" + translate( + "AddonsInstaller", + "A macro has been installed and is available " + "under Macro -> Macros menu", + ) answer += ":\n" + f + "" self.update_metadata() self.success.emit(self.repo, answer) - def check_python_dependencies(self, baseurl:str) -> Union[bool,str]: + def check_python_dependencies(self, baseurl: str) -> Union[bool, str]: """checks if the repo contains a metadata.txt and check its contents""" ok = True @@ -920,7 +1134,10 @@ class InstallWorkbenchWorker(QtCore.QThread): try: mu = utils.urlopen(depsurl) except Exception: - return True, translate("AddonsInstaller", "No metadata.txt found, cannot evaluate Python dependencies") + return True, translate( + "AddonsInstaller", + "No metadata.txt found, cannot evaluate Python dependencies", + ) if mu: # metadata.txt found depsfile = mu.read() @@ -940,9 +1157,19 @@ class InstallWorkbenchWorker(QtCore.QThread): for wb in depswb: if wb.strip(): if not wb.strip() in FreeCADGui.listWorkbenches().keys(): - if not wb.strip() + "Workbench" in FreeCADGui.listWorkbenches().keys(): + if ( + not wb.strip() + "Workbench" + in FreeCADGui.listWorkbenches().keys() + ): ok = False - message += translate("AddonsInstaller", "Missing workbench") + ": " + wb + ", " + message += ( + translate( + "AddonsInstaller", "Missing workbench" + ) + + ": " + + wb + + ", " + ) elif line.startswith("pylibs="): depspy = line.split("=")[1].split(",") for pl in depspy: @@ -951,7 +1178,14 @@ class InstallWorkbenchWorker(QtCore.QThread): __import__(pl.strip()) except ImportError: ok = False - message += translate("AddonsInstaller", "Missing python module") + ": " + pl + ", " + message += ( + translate( + "AddonsInstaller", "Missing python module" + ) + + ": " + + pl + + ", " + ) elif line.startswith("optionalpylibs="): opspy = line.split("=")[1].split(",") for pl in opspy: @@ -959,20 +1193,27 @@ class InstallWorkbenchWorker(QtCore.QThread): try: __import__(pl.strip()) except ImportError: - message += translate("AddonsInstaller", - "Missing optional python module (doesn't prevent installing)") + message += translate( + "AddonsInstaller", + "Missing optional python module (doesn't prevent installing)", + ) message += ": " + pl + ", " if message and (not ok): - message = translate("AddonsInstaller", "Some errors were found that prevent installation of this workbench") + message = translate( + "AddonsInstaller", + "Some errors were found that prevent installation of this workbench", + ) message += ": " + message + ". " - message += translate("AddonsInstaller", "Please install the missing components first.") + message += translate( + "AddonsInstaller", "Please install the missing components first." + ) return ok, message def check_package_dependencies(self): # TODO: Use the dependencies set in the package.xml metadata pass - def run_zip(self, zipdir:str) -> None: + def run_zip(self, zipdir: str) -> None: "downloads and unzip a zip version from a git repo" bakdir = None @@ -984,18 +1225,33 @@ class InstallWorkbenchWorker(QtCore.QThread): os.makedirs(zipdir) zipurl = utils.get_zip_url(self.repo) if not zipurl: - self.failure.emit(self.repo, translate("AddonsInstaller", "Error: Unable to locate zip from") + " " + self.repo.name) + self.failure.emit( + self.repo, + translate("AddonsInstaller", "Error: Unable to locate zip from") + + " " + + self.repo.name, + ) return try: u = utils.urlopen(zipurl) except Exception: - self.failure.emit(self.repo, translate("AddonsInstaller", "Error: Unable to download") + " " + zipurl) + self.failure.emit( + self.repo, + translate("AddonsInstaller", "Error: Unable to download") + + " " + + zipurl, + ) return if not u: - self.failure.emit(self.repo, translate("AddonsInstaller", "Error: Unable to download") + " " + zipurl) + self.failure.emit( + self.repo, + translate("AddonsInstaller", "Error: Unable to download") + + " " + + zipurl, + ) return - - data_size = u.headers['content-length'] + + data_size = u.headers["content-length"] current_thread = QtCore.QThread.currentThread() if data_size and data_size > 5 * 1024 * 1024: @@ -1023,21 +1279,27 @@ class InstallWorkbenchWorker(QtCore.QThread): u.close() zfile.close() for filename in os.listdir(zipdir + os.sep + master): - shutil.move(zipdir + os.sep + master + os.sep + filename, zipdir + os.sep + filename) + shutil.move( + zipdir + os.sep + master + os.sep + filename, zipdir + os.sep + filename + ) os.rmdir(zipdir + os.sep + master) if bakdir: shutil.rmtree(bakdir) self.update_metadata() - self.success.emit(self.repo, translate("AddonsInstaller", "Successfully installed") + " " + zipurl) + self.success.emit( + self.repo, + translate("AddonsInstaller", "Successfully installed") + " " + zipurl, + ) def update_metadata(self): basedir = FreeCAD.getUserAppDataDir() - package_xml = os.path.join(basedir,"Mod",self.repo.name,"package.xml") + package_xml = os.path.join(basedir, "Mod", self.repo.name, "package.xml") if os.path.isfile(package_xml): self.repo.metadata = FreeCAD.Metadata(package_xml) self.repo.installed_version = self.repo.metadata.Version self.repo.updated_timestamp = datetime.now().timestamp() + class CheckSingleWorker(QtCore.QThread): """Worker to check for updates for a single addon""" @@ -1052,7 +1314,9 @@ class CheckSingleWorker(QtCore.QThread): if not have_git or NOGIT: return - FreeCAD.Console.PrintLog("Checking for available updates of the " + self.name + " addon\n") + FreeCAD.Console.PrintLog( + "Checking for available updates of the " + self.name + " addon\n" + ) addondir = os.path.join(FreeCAD.getUserAppDataDir(), "Mod", self.name) if os.path.exists(addondir): if os.path.exists(addondir + os.sep + ".git"): @@ -1067,6 +1331,7 @@ class CheckSingleWorker(QtCore.QThread): pass self.updateAvailable.emit(False) + class UpdateMetadataCacheWorker(QtCore.QThread): "Scan through all available packages and see if our local copy of package.xml needs to be updated" @@ -1076,22 +1341,26 @@ class UpdateMetadataCacheWorker(QtCore.QThread): package_updated = QtCore.Signal(AddonManagerRepo) class AtomicCounter(object): - def __init__(self,start=0): + def __init__(self, start=0): self.lock = threading.Lock() self.count = start - def set(self,new_value): + + def set(self, new_value): with self.lock: self.count = new_value + def get(self): with self.lock: return self.count + def increment(self): with self.lock: self.count += 1 + def decrement(self): with self.lock: self.count -= 1 - + def __init__(self, repos): QtCore.QThread.__init__(self) @@ -1102,16 +1371,22 @@ class UpdateMetadataCacheWorker(QtCore.QThread): current_thread = QtCore.QThread.currentThread() self.num_downloads_required = len(self.repos) self.progress_made.emit(0, self.num_downloads_required) - self.status_message.emit(translate("AddonsInstaller", "Retrieving package metadata...")) - store = os.path.join(FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata") - index_file = os.path.join(store,"index.json") + self.status_message.emit( + translate("AddonsInstaller", "Retrieving package metadata...") + ) + store = os.path.join( + FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata" + ) + index_file = os.path.join(store, "index.json") self.index = {} if os.path.isfile(index_file): with open(index_file, "r") as f: index_string = f.read() self.index = json.loads(index_string) - download_queue = QtNetwork.QNetworkAccessManager() # Must be created on this thread + download_queue = ( + QtNetwork.QNetworkAccessManager() + ) # Must be created on this thread download_queue.finished.connect(self.on_finished) self.downloaders = [] @@ -1126,7 +1401,7 @@ class UpdateMetadataCacheWorker(QtCore.QThread): # this is local to this thread, and does not affect the main event loop ui_updater = QtCore.QTimer() ui_updater.timeout.connect(self.send_ui_update) - ui_updater.start(100) # Send an update back to the main thread every 100ms + ui_updater.start(100) # Send an update back to the main thread every 100ms self.num_downloads_required = len(self.downloaders) self.num_downloads_completed = UpdateMetadataCacheWorker.AtomicCounter() aborted = False @@ -1142,7 +1417,7 @@ class UpdateMetadataCacheWorker(QtCore.QThread): if aborted: FreeCAD.Console.PrintMessage("Metadata update cancelled\n") return - + # Update and serialize the updated index, overwriting whatever was # there before for downloader in self.downloaders: @@ -1151,7 +1426,7 @@ class UpdateMetadataCacheWorker(QtCore.QThread): os.makedirs(store) with open(index_file, "w") as f: json.dump(self.index, f, indent=" ") - + # Signal completion to our parent thread self.done.emit() self.stop = True @@ -1167,12 +1442,15 @@ class UpdateMetadataCacheWorker(QtCore.QThread): self.package_updated.emit(repo) def send_ui_update(self): - self.progress_made.emit(self.num_downloads_completed.get(), self.num_downloads_required) + self.progress_made.emit( + self.num_downloads_completed.get(), self.num_downloads_required + ) if have_git and not NOGIT: + class GitProgressMonitor(git.RemoteProgress): - """ An object that receives git progress updates and transforms them into Qt signals """ + """An object that receives git progress updates and transforms them into Qt signals""" progress_made = QtCore.Signal(int, int) info_message = QtCore.Signal(str) @@ -1180,7 +1458,13 @@ if have_git and not NOGIT: def __init__(self): super().__init__() - def update(self, op_code: int, cur_count: Union[str, float], max_count: Union[str, float, None]=None, message: str='') -> None: + def update( + self, + op_code: int, + cur_count: Union[str, float], + max_count: Union[str, float, None] = None, + message: str = "", + ) -> None: if max_count: self.progress_made.emit(int(cur_count), int(max_count)) if message: @@ -1188,8 +1472,8 @@ if have_git and not NOGIT: class UpdateAllWorker(QtCore.QThread): - """ Update all listed packages, of any kind """ - + """Update all listed packages, of any kind""" + progress_made = QtCore.Signal(int, int) status_message = QtCore.Signal(str) success = QtCore.Signal(AddonManagerRepo) @@ -1200,8 +1484,8 @@ class UpdateAllWorker(QtCore.QThread): super().__init__() self.repos = repos - def run (self): - self.progress_made.emit(0,len(self.repos)) + def run(self): + self.progress_made.emit(0, len(self.repos)) self.repo_queue = queue.Queue() current_thread = QtCore.QThread.currentThread() for repo in self.repos: @@ -1215,7 +1499,7 @@ class UpdateAllWorker(QtCore.QThread): worker.failure.connect(self.on_failure) worker.start() workers.append(worker) - + while not self.repo_queue.empty(): if current_thread.isInterruptionRequested(): for worker in workers: @@ -1233,19 +1517,20 @@ class UpdateAllWorker(QtCore.QThread): self.done.emit() - def on_success(self, repo:AddonManagerRepo) -> None: + def on_success(self, repo: AddonManagerRepo) -> None: self.progress_made.emit(self.repo_queue.qsize(), len(self.repos)) self.success.emit(repo) - def on_failure(self, repo:AddonManagerRepo) -> None: + def on_failure(self, repo: AddonManagerRepo) -> None: self.progress_made.emit(self.repo_queue.qsize(), len(self.repos)) self.failure.emit(repo) + class UpdateSingleWorker(QtCore.QThread): success = QtCore.Signal(AddonManagerRepo) failure = QtCore.Signal(AddonManagerRepo) - def __init__(self,repo_queue:queue.Queue): + def __init__(self, repo_queue: queue.Queue): super().__init__() self.repo_queue = repo_queue @@ -1263,12 +1548,13 @@ class UpdateSingleWorker(QtCore.QThread): else: self.update_package(repo) self.repo_queue.task_done() - - def update_macro(self, repo:AddonManagerRepo): - """ Updating a macro happens in this function, in the current thread """ - - cache_path = os.path.join(FreeCAD.getUserCachePath(), "AddonManager", "MacroCache") + def update_macro(self, repo: AddonManagerRepo): + """Updating a macro happens in this function, in the current thread""" + + cache_path = os.path.join( + FreeCAD.getUserCachePath(), "AddonManager", "MacroCache" + ) os.makedirs(cache_path, exist_ok=True) temp_install_succeeded = macro.install(cache_path) if not temp_install_succeeded: @@ -1282,12 +1568,12 @@ class UpdateSingleWorker(QtCore.QThread): else: self.failure.emit(repo) - def update_package(self, repo:AddonManagerRepo): - """ Updating a package re-uses the package installation worker, so actually spawns another thread that we block on """ - + def update_package(self, repo: AddonManagerRepo): + """Updating a package re-uses the package installation worker, so actually spawns another thread that we block on""" + worker = InstallWorkbenchWorker(repo) - worker.success.connect(lambda repo,_:self.success.emit(repo)) - worker.failure.connect(lambda repo,_:self.failure.emit(repo)) + worker.success.connect(lambda repo, _: self.success.emit(repo)) + worker.failure.connect(lambda repo, _: self.failure.emit(repo)) worker.start() while True: # Ensure our signals propagate out by running an internal thread-local event loop @@ -1295,4 +1581,5 @@ class UpdateSingleWorker(QtCore.QThread): if not worker.isRunning(): break + # @} diff --git a/src/Mod/AddonManager/compact_view.py b/src/Mod/AddonManager/compact_view.py index f5b6bc3962..a550f14358 100644 --- a/src/Mod/AddonManager/compact_view.py +++ b/src/Mod/AddonManager/compact_view.py @@ -54,7 +54,9 @@ class Ui_CompactView(object): sizePolicy2 = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred) sizePolicy2.setHorizontalStretch(0) sizePolicy2.setVerticalStretch(0) - sizePolicy2.setHeightForWidth(self.labelDescription.sizePolicy().hasHeightForWidth()) + sizePolicy2.setHeightForWidth( + self.labelDescription.sizePolicy().hasHeightForWidth() + ) self.labelDescription.setSizePolicy(sizePolicy2) self.labelDescription.setTextFormat(Qt.PlainText) self.labelDescription.setWordWrap(False) @@ -67,18 +69,28 @@ class Ui_CompactView(object): self.horizontalLayout_2.addWidget(self.labelStatus) - self.retranslateUi(CompactView) QMetaObject.connectSlotsByName(CompactView) + # setupUi def retranslateUi(self, CompactView): - CompactView.setWindowTitle(QCoreApplication.translate("CompactView", u"Form", None)) + CompactView.setWindowTitle( + QCoreApplication.translate("CompactView", u"Form", None) + ) self.labelIcon.setText(QCoreApplication.translate("CompactView", u"Icon", None)) - self.labelPackageName.setText(QCoreApplication.translate("CompactView", u"Package Name", None)) - self.labelVersion.setText(QCoreApplication.translate("CompactView", u"Version", None)) - self.labelDescription.setText(QCoreApplication.translate("CompactView", u"Description", None)) - self.labelStatus.setText(QCoreApplication.translate("CompactView", u"UpdateAvailable", None)) - # retranslateUi + self.labelPackageName.setText( + QCoreApplication.translate("CompactView", u"Package Name", None) + ) + self.labelVersion.setText( + QCoreApplication.translate("CompactView", u"Version", None) + ) + self.labelDescription.setText( + QCoreApplication.translate("CompactView", u"Description", None) + ) + self.labelStatus.setText( + QCoreApplication.translate("CompactView", u"UpdateAvailable", None) + ) + # retranslateUi diff --git a/src/Mod/AddonManager/expanded_view.py b/src/Mod/AddonManager/expanded_view.py index 1dfd7767b8..5c8a7c2797 100644 --- a/src/Mod/AddonManager/expanded_view.py +++ b/src/Mod/AddonManager/expanded_view.py @@ -39,7 +39,9 @@ class Ui_ExpandedView(object): self.horizontalLayout_2.addWidget(self.labelIcon) - self.horizontalSpacer = QSpacerItem(8, 20, QSizePolicy.Fixed, QSizePolicy.Minimum) + self.horizontalSpacer = QSpacerItem( + 8, 20, QSizePolicy.Fixed, QSizePolicy.Minimum + ) self.horizontalLayout_2.addItem(self.horizontalSpacer) @@ -59,60 +61,81 @@ class Ui_ExpandedView(object): sizePolicy2 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) sizePolicy2.setHorizontalStretch(0) sizePolicy2.setVerticalStretch(0) - sizePolicy2.setHeightForWidth(self.labelVersion.sizePolicy().hasHeightForWidth()) + sizePolicy2.setHeightForWidth( + self.labelVersion.sizePolicy().hasHeightForWidth() + ) self.labelVersion.setSizePolicy(sizePolicy2) self.horizontalLayout.addWidget(self.labelVersion) - self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + self.horizontalSpacer_2 = QSpacerItem( + 40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum + ) self.horizontalLayout.addItem(self.horizontalSpacer_2) - self.verticalLayout.addLayout(self.horizontalLayout) self.labelDescription = QLabel(ExpandedView) self.labelDescription.setObjectName(u"labelDescription") - sizePolicy.setHeightForWidth(self.labelDescription.sizePolicy().hasHeightForWidth()) + sizePolicy.setHeightForWidth( + self.labelDescription.sizePolicy().hasHeightForWidth() + ) self.labelDescription.setSizePolicy(sizePolicy) self.labelDescription.setTextFormat(Qt.PlainText) - self.labelDescription.setAlignment(Qt.AlignLeading|Qt.AlignLeft|Qt.AlignTop) + self.labelDescription.setAlignment(Qt.AlignLeading | Qt.AlignLeft | Qt.AlignTop) self.labelDescription.setWordWrap(True) self.verticalLayout.addWidget(self.labelDescription) self.labelMaintainer = QLabel(ExpandedView) self.labelMaintainer.setObjectName(u"labelMaintainer") - sizePolicy2.setHeightForWidth(self.labelMaintainer.sizePolicy().hasHeightForWidth()) + sizePolicy2.setHeightForWidth( + self.labelMaintainer.sizePolicy().hasHeightForWidth() + ) self.labelMaintainer.setSizePolicy(sizePolicy2) - self.labelMaintainer.setAlignment(Qt.AlignLeading|Qt.AlignLeft|Qt.AlignTop) + self.labelMaintainer.setAlignment(Qt.AlignLeading | Qt.AlignLeft | Qt.AlignTop) self.labelMaintainer.setWordWrap(False) self.verticalLayout.addWidget(self.labelMaintainer) - self.horizontalLayout_2.addLayout(self.verticalLayout) - self.labelStatus = QLabel(ExpandedView) self.labelStatus.setObjectName(u"labelStatus") - self.labelStatus.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter) + self.labelStatus.setAlignment( + Qt.AlignRight | Qt.AlignTrailing | Qt.AlignVCenter + ) self.horizontalLayout_2.addWidget(self.labelStatus) - self.retranslateUi(ExpandedView) QMetaObject.connectSlotsByName(ExpandedView) + # setupUi def retranslateUi(self, ExpandedView): - ExpandedView.setWindowTitle(QCoreApplication.translate("ExpandedView", u"Form", None)) - self.labelIcon.setText(QCoreApplication.translate("ExpandedView", u"Icon", None)) - self.labelPackageName.setText(QCoreApplication.translate("ExpandedView", u"

Package Name

", None)) - self.labelVersion.setText(QCoreApplication.translate("ExpandedView", u"Version", None)) - self.labelDescription.setText(QCoreApplication.translate("ExpandedView", u"Description", None)) - self.labelMaintainer.setText(QCoreApplication.translate("ExpandedView", u"Maintainer", None)) - self.labelStatus.setText(QCoreApplication.translate("ExpandedView", u"UpdateAvailable", None)) - # retranslateUi + ExpandedView.setWindowTitle( + QCoreApplication.translate("ExpandedView", u"Form", None) + ) + self.labelIcon.setText( + QCoreApplication.translate("ExpandedView", u"Icon", None) + ) + self.labelPackageName.setText( + QCoreApplication.translate("ExpandedView", u"

Package Name

", None) + ) + self.labelVersion.setText( + QCoreApplication.translate("ExpandedView", u"Version", None) + ) + self.labelDescription.setText( + QCoreApplication.translate("ExpandedView", u"Description", None) + ) + self.labelMaintainer.setText( + QCoreApplication.translate("ExpandedView", u"Maintainer", None) + ) + self.labelStatus.setText( + QCoreApplication.translate("ExpandedView", u"UpdateAvailable", None) + ) + # retranslateUi diff --git a/src/Mod/AddonManager/package_details.py b/src/Mod/AddonManager/package_details.py index d5ee5d2bc4..a62ed16ef7 100644 --- a/src/Mod/AddonManager/package_details.py +++ b/src/Mod/AddonManager/package_details.py @@ -1,26 +1,26 @@ # -*- coding: utf-8 -*- -#*************************************************************************** -#* * -#* Copyright (c) 2021 Chris Hennes * -#* * -#* This program is free software; you can redistribute it and/or modify * -#* it under the terms of the GNU Lesser General Public License (LGPL) * -#* as published by the Free Software Foundation; either version 2 of * -#* the License, or (at your option) any later version. * -#* for detail see the LICENCE text file. * -#* * -#* This program is distributed in the hope that it will be useful, * -#* but WITHOUT ANY WARRANTY; without even the implied warranty of * -#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -#* GNU Library General Public License for more details. * -#* * -#* You should have received a copy of the GNU Library General Public * -#* License along with this program; if not, write to the Free Software * -#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -#* USA * -#* * -#*************************************************************************** +# *************************************************************************** +# * * +# * Copyright (c) 2021 Chris Hennes * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** from PySide2.QtCore import * from PySide2.QtGui import * from PySide2.QtWidgets import * @@ -37,6 +37,7 @@ from AddonManagerRepo import AddonManagerRepo import inspect + class PackageDetails(QWidget): back = Signal() @@ -61,7 +62,7 @@ class PackageDetails(QWidget): self.ui.buttonUninstall.clicked.connect(lambda: self.uninstall.emit(self.repo)) self.ui.buttonUpdate.clicked.connect(lambda: self.update.emit(self.repo)) - def show_repo(self, repo:AddonManagerRepo, reload:bool = False) -> None: + def show_repo(self, repo: AddonManagerRepo, reload: bool = False) -> None: self.repo = repo @@ -90,36 +91,76 @@ class PackageDetails(QWidget): if repo.update_status != AddonManagerRepo.UpdateStatus.NOT_INSTALLED: installed_version_string = "" if repo.installed_version: - installed_version_string = translate("AddonsInstaller", "Version") + " " + installed_version_string = translate("AddonsInstaller", "Version") + " " installed_version_string += repo.installed_version else: - installed_version_string = translate("AddonsInstaller", "Unknown version (no package.xml file found)") + " " + installed_version_string = ( + translate( + "AddonsInstaller", "Unknown version (no package.xml file found)" + ) + + " " + ) - if repo.updated_timestamp: - installed_version_string += " " + translate("AddonsInstaller", "installed on") + " " - installed_version_string += QDateTime.fromTime_t(repo.updated_timestamp).date().toString(Qt.SystemLocaleShortDate) + installed_version_string += ( + " " + translate("AddonsInstaller", "installed on") + " " + ) + installed_version_string += ( + QDateTime.fromTime_t(repo.updated_timestamp) + .date() + .toString(Qt.SystemLocaleShortDate) + ) installed_version_string += ". " else: - installed_version_string += translate("AddonsInstaller", "installed") + ". " + installed_version_string += ( + translate("AddonsInstaller", "installed") + ". " + ) if repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: if repo.metadata: - installed_version_string += "" + translate("AddonsInstaller", "Update available to version") + " " + installed_version_string += ( + "" + + translate("AddonsInstaller", "Update available to version") + + " " + ) installed_version_string += repo.metadata.Version installed_version_string += "." else: - installed_version_string += "" + translate("AddonsInstaller", "Update available to unknown version (no package.xml file found)") + "." - elif repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE: - installed_version_string += translate("AddonsInstaller", "This is the latest version available") + "." + installed_version_string += ( + "" + + translate( + "AddonsInstaller", + "Update available to unknown version (no package.xml file found)", + ) + + "." + ) + elif ( + repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE + ): + installed_version_string += ( + translate("AddonsInstaller", "This is the latest version available") + + "." + ) elif repo.update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART: - installed_version_string += translate("AddonsInstaller", "Updated, please restart FreeCAD to use") + "." + installed_version_string += ( + translate( + "AddonsInstaller", "Updated, please restart FreeCAD to use" + ) + + "." + ) elif repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED: - installed_version_string += translate("AddonsInstaller", "Update check in progress") + "." + installed_version_string += ( + translate("AddonsInstaller", "Update check in progress") + "." + ) basedir = FreeCAD.getUserAppDataDir() - moddir = os.path.join(basedir , "Mod", repo.name) - installed_version_string += "
" + translate("AddonsInstaller", "Installation location") + ": " + moddir + moddir = os.path.join(basedir, "Mod", repo.name) + installed_version_string += ( + "
" + + translate("AddonsInstaller", "Installation location") + + ": " + + moddir + ) self.ui.labelPackageDetails.setText(installed_version_string) self.ui.labelPackageDetails.show() @@ -148,30 +189,42 @@ class PackageDetails(QWidget): self.ui.buttonUpdate.hide() @classmethod - def cache_path(self, repo:AddonManagerRepo) -> str: + def cache_path(self, repo: AddonManagerRepo) -> str: cache_path = FreeCAD.getUserCachePath() - full_path = os.path.join(cache_path,"AddonManager",repo.name) + full_path = os.path.join(cache_path, "AddonManager", repo.name) return full_path - def check_and_clean_cache(self, force:bool = False) -> None: + def check_and_clean_cache(self, force: bool = False) -> None: cache_path = PackageDetails.cache_path(self.repo) - readme_cache_file = os.path.join(cache_path,"README.html") - readme_images_path = os.path.join(cache_path,"Images") - download_interrupted_sentinel = os.path.join(readme_images_path,"download_in_progress") + readme_cache_file = os.path.join(cache_path, "README.html") + readme_images_path = os.path.join(cache_path, "Images") + download_interrupted_sentinel = os.path.join( + readme_images_path, "download_in_progress" + ) download_interrupted = os.path.isfile(download_interrupted_sentinel) if os.path.isfile(readme_cache_file): pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") - days_between_updates = pref.GetInt("DaysBetweenUpdates", 2^32) + days_between_updates = pref.GetInt("DaysBetweenUpdates", 2 ^ 32) timestamp = os.path.getmtime(readme_cache_file) last_cache_update = date.fromtimestamp(timestamp) delta_update = timedelta(days=days_between_updates) - if date.today() >= last_cache_update + delta_update or download_interrupted or force: + if ( + date.today() >= last_cache_update + delta_update + or download_interrupted + or force + ): if force: - FreeCAD.Console.PrintMessage(f"Forced README cache update for {self.repo.name}\n") + FreeCAD.Console.PrintMessage( + f"Forced README cache update for {self.repo.name}\n" + ) elif download_interrupted: - FreeCAD.Console.PrintMessage(f"Restarting interrupted README download for {self.repo.name}\n") + FreeCAD.Console.PrintMessage( + f"Restarting interrupted README download for {self.repo.name}\n" + ) else: - FreeCAD.Console.PrintMessage(f"Cache expired, downloading README for {self.repo.name} again\n") + FreeCAD.Console.PrintMessage( + f"Cache expired, downloading README for {self.repo.name} again\n" + ) os.remove(readme_cache_file) if os.path.isdir(readme_images_path): shutil.rmtree(readme_images_path) @@ -180,99 +233,129 @@ class PackageDetails(QWidget): self.check_and_clean_cache(force=True) self.show_repo(self.repo) - def show_cached_readme(self, repo:AddonManagerRepo) -> bool: - """ Attempts to show a cached readme, returns true if there was a cache, or false if not """ + def show_cached_readme(self, repo: AddonManagerRepo) -> bool: + """Attempts to show a cached readme, returns true if there was a cache, or false if not""" cache_path = PackageDetails.cache_path(repo) - readme_cache_file = os.path.join(cache_path,"README.html") + readme_cache_file = os.path.join(cache_path, "README.html") if os.path.isfile(readme_cache_file): - with open(readme_cache_file,"rb") as f: + with open(readme_cache_file, "rb") as f: data = f.read() self.ui.textBrowserReadMe.setText(data.decode()) return True return False - def show_workbench(self, repo:AddonManagerRepo) -> None: + def show_workbench(self, repo: AddonManagerRepo) -> None: """loads information of a given workbench""" - + if not self.show_cached_readme(repo): - self.ui.textBrowserReadMe.setText(translate("AddonsInstaller","Fetching README.md from package repository")) + self.ui.textBrowserReadMe.setText( + translate( + "AddonsInstaller", "Fetching README.md from package repository" + ) + ) self.worker = ShowWorker(repo, PackageDetails.cache_path(repo)) - self.worker.readme_updated.connect(lambda desc: self.cache_readme(repo, desc)) - self.worker.readme_updated.connect(lambda desc: self.ui.textBrowserReadMe.setText(desc)) + self.worker.readme_updated.connect( + lambda desc: self.cache_readme(repo, desc) + ) + self.worker.readme_updated.connect( + lambda desc: self.ui.textBrowserReadMe.setText(desc) + ) self.worker.update_status.connect(self.update_status.emit) self.worker.update_status.connect(self.show) self.worker.start() - def show_package(self, repo:AddonManagerRepo) -> None: - """ Show the details for a package (a repo with a package.xml metadata file) """ - + def show_package(self, repo: AddonManagerRepo) -> None: + """Show the details for a package (a repo with a package.xml metadata file)""" + if not self.show_cached_readme(repo): - self.ui.textBrowserReadMe.setText(translate("AddonsInstaller","Fetching README.md from package repository")) - self.worker = ShowWorker(repo,PackageDetails.cache_path(repo)) - self.worker.readme_updated.connect(lambda desc: self.cache_readme(repo, desc)) - self.worker.readme_updated.connect(lambda desc: self.ui.textBrowserReadMe.setText(desc)) + self.ui.textBrowserReadMe.setText( + translate( + "AddonsInstaller", "Fetching README.md from package repository" + ) + ) + self.worker = ShowWorker(repo, PackageDetails.cache_path(repo)) + self.worker.readme_updated.connect( + lambda desc: self.cache_readme(repo, desc) + ) + self.worker.readme_updated.connect( + lambda desc: self.ui.textBrowserReadMe.setText(desc) + ) self.worker.update_status.connect(self.update_status.emit) self.worker.update_status.connect(self.show) self.worker.start() - def show_macro(self, repo:AddonManagerRepo) -> None: + def show_macro(self, repo: AddonManagerRepo) -> None: """loads information of a given macro""" - + if not self.show_cached_readme(repo): - self.ui.textBrowserReadMe.setText(translate("AddonsInstaller","Fetching README.md from package repository")) + self.ui.textBrowserReadMe.setText( + translate( + "AddonsInstaller", "Fetching README.md from package repository" + ) + ) self.worker = GetMacroDetailsWorker(repo) - self.worker.readme_updated.connect(lambda desc: self.cache_readme(repo, desc)) - self.worker.readme_updated.connect(lambda desc: self.ui.textBrowserReadMe.setText(desc)) + self.worker.readme_updated.connect( + lambda desc: self.cache_readme(repo, desc) + ) + self.worker.readme_updated.connect( + lambda desc: self.ui.textBrowserReadMe.setText(desc) + ) self.worker.start() - def cache_readme(self, repo:AddonManagerRepo, readme:str) -> None: + def cache_readme(self, repo: AddonManagerRepo, readme: str) -> None: cache_path = PackageDetails.cache_path(repo) - readme_cache_file = os.path.join(cache_path,"README.html") - os.makedirs(cache_path,exist_ok=True) - with open(readme_cache_file,"wb") as f: + readme_cache_file = os.path.join(cache_path, "README.html") + os.makedirs(cache_path, exist_ok=True) + with open(readme_cache_file, "wb") as f: f.write(readme.encode()) + class Ui_PackageDetails(object): def setupUi(self, PackageDetails): if not PackageDetails.objectName(): - PackageDetails.setObjectName(u"PackageDetails") + PackageDetails.setObjectName("PackageDetails") self.verticalLayout_2 = QVBoxLayout(PackageDetails) - self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.verticalLayout_2.setObjectName("verticalLayout_2") self.layoutDetailsBackButton = QHBoxLayout() - self.layoutDetailsBackButton.setObjectName(u"layoutDetailsBackButton") + self.layoutDetailsBackButton.setObjectName("layoutDetailsBackButton") self.buttonBack = QToolButton(PackageDetails) - self.buttonBack.setObjectName(u"buttonBack") - self.buttonBack.setIcon(QIcon.fromTheme("back", QIcon(":/icons/button_left.svg"))) + self.buttonBack.setObjectName("buttonBack") + self.buttonBack.setIcon( + QIcon.fromTheme("back", QIcon(":/icons/button_left.svg")) + ) self.buttonRefresh = QToolButton(PackageDetails) - self.buttonRefresh.setObjectName(u"buttonRefresh") - self.buttonRefresh.setIcon(QIcon.fromTheme("refresh", QIcon(":/icons/view-refresh.svg"))) + self.buttonRefresh.setObjectName("buttonRefresh") + self.buttonRefresh.setIcon( + QIcon.fromTheme("refresh", QIcon(":/icons/view-refresh.svg")) + ) self.layoutDetailsBackButton.addWidget(self.buttonBack) self.layoutDetailsBackButton.addWidget(self.buttonRefresh) - self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + self.horizontalSpacer = QSpacerItem( + 40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum + ) self.layoutDetailsBackButton.addItem(self.horizontalSpacer) - self.buttonInstall = QPushButton(PackageDetails) - self.buttonInstall.setObjectName(u"buttonInstall") + self.buttonInstall.setObjectName("buttonInstall") self.layoutDetailsBackButton.addWidget(self.buttonInstall) self.buttonUninstall = QPushButton(PackageDetails) - self.buttonUninstall.setObjectName(u"buttonUninstall") + self.buttonUninstall.setObjectName("buttonUninstall") self.layoutDetailsBackButton.addWidget(self.buttonUninstall) self.buttonUpdate = QPushButton(PackageDetails) - self.buttonUpdate.setObjectName(u"buttonUpdate") + self.buttonUpdate.setObjectName("buttonUpdate") self.layoutDetailsBackButton.addWidget(self.buttonUpdate) self.buttonExecute = QPushButton(PackageDetails) - self.buttonExecute.setObjectName(u"buttonExecute") + self.buttonExecute.setObjectName("buttonExecute") self.layoutDetailsBackButton.addWidget(self.buttonExecute) @@ -284,25 +367,43 @@ class Ui_PackageDetails(object): self.verticalLayout_2.addWidget(self.labelPackageDetails) self.textBrowserReadMe = QTextBrowser(PackageDetails) - self.textBrowserReadMe.setObjectName(u"textBrowserReadMe") + self.textBrowserReadMe.setObjectName("textBrowserReadMe") self.textBrowserReadMe.setOpenExternalLinks(True) self.textBrowserReadMe.setOpenLinks(True) self.verticalLayout_2.addWidget(self.textBrowserReadMe) - self.retranslateUi(PackageDetails) QMetaObject.connectSlotsByName(PackageDetails) + # setupUi def retranslateUi(self, PackageDetails): self.buttonBack.setText("") - self.buttonInstall.setText(QCoreApplication.translate("AddonsInstaller", u"Install", None)) - self.buttonUninstall.setText(QCoreApplication.translate("AddonsInstaller", u"Uninstall", None)) - self.buttonUpdate.setText(QCoreApplication.translate("AddonsInstaller", u"Update", None)) - self.buttonExecute.setText(QCoreApplication.translate("AddonsInstaller", u"Run Macro", None)) - self.buttonBack.setToolTip(QCoreApplication.translate("AddonsInstaller", u"Return to package list", None)) - self.buttonRefresh.setToolTip(QCoreApplication.translate("AddonsInstaller", u"Delete cached version of this README and re-download", None)) - # retranslateUi + self.buttonInstall.setText( + QCoreApplication.translate("AddonsInstaller", "Install", None) + ) + self.buttonUninstall.setText( + QCoreApplication.translate("AddonsInstaller", "Uninstall", None) + ) + self.buttonUpdate.setText( + QCoreApplication.translate("AddonsInstaller", "Update", None) + ) + self.buttonExecute.setText( + QCoreApplication.translate("AddonsInstaller", "Run Macro", None) + ) + self.buttonBack.setToolTip( + QCoreApplication.translate( + "AddonsInstaller", "Return to package list", None + ) + ) + self.buttonRefresh.setToolTip( + QCoreApplication.translate( + "AddonsInstaller", + "Delete cached version of this README and re-download", + None, + ) + ) + # retranslateUi diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py index 4d335be22e..14f7258c3c 100644 --- a/src/Mod/AddonManager/package_list.py +++ b/src/Mod/AddonManager/package_list.py @@ -1,26 +1,26 @@ # -*- coding: utf-8 -*- -#*************************************************************************** -#* * -#* Copyright (c) 2021 Chris Hennes * -#* * -#* This program is free software; you can redistribute it and/or modify * -#* it under the terms of the GNU Lesser General Public License (LGPL) * -#* as published by the Free Software Foundation; either version 2 of * -#* the License, or (at your option) any later version. * -#* for detail see the LICENCE text file. * -#* * -#* This program is distributed in the hope that it will be useful, * -#* but WITHOUT ANY WARRANTY; without even the implied warranty of * -#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -#* GNU Library General Public License for more details. * -#* * -#* You should have received a copy of the GNU Library General Public * -#* License along with this program; if not, write to the Free Software * -#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -#* USA * -#* * -#*************************************************************************** +# *************************************************************************** +# * * +# * Copyright (c) 2021 Chris Hennes * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** import FreeCAD @@ -39,31 +39,37 @@ from AddonManagerRepo import AddonManagerRepo from compact_view import Ui_CompactView from expanded_view import Ui_ExpandedView + class ListDisplayStyle(IntEnum): COMPACT = 0 EXPANDED = 1 + class PackageList(QWidget): - """ A widget that shows a list of packages and various widgets to control the display of the list """ + """A widget that shows a list of packages and various widgets to control the display of the list""" itemSelected = Signal(AddonManagerRepo) - - def __init__(self,parent=None): + + def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_PackageList() self.ui.setupUi(self) - + self.item_filter = PackageListFilter() - self.ui.listPackages.setModel (self.item_filter) + self.ui.listPackages.setModel(self.item_filter) self.item_delegate = PackageListItemDelegate(self.ui.listPackages) self.ui.listPackages.setItemDelegate(self.item_delegate) self.ui.listPackages.clicked.connect(self.on_listPackages_clicked) self.ui.comboPackageType.currentIndexChanged.connect(self.update_type_filter) self.ui.lineEditFilter.textChanged.connect(self.update_text_filter) - self.ui.buttonCompactLayout.clicked.connect(lambda: self.set_view_style(ListDisplayStyle.COMPACT)) - self.ui.buttonExpandedLayout.clicked.connect(lambda: self.set_view_style(ListDisplayStyle.EXPANDED)) - + self.ui.buttonCompactLayout.clicked.connect( + lambda: self.set_view_style(ListDisplayStyle.COMPACT) + ) + self.ui.buttonExpandedLayout.clicked.connect( + lambda: self.set_view_style(ListDisplayStyle.EXPANDED) + ) + # Only shows when the user types in a filter self.ui.labelFilterValidity.hide() @@ -85,51 +91,56 @@ class PackageList(QWidget): else: self.ui.buttonCompactLayout.setChecked(True) - def on_listPackages_clicked(self, index:QModelIndex): - source_selection = self.item_filter.mapToSource (index) + def on_listPackages_clicked(self, index: QModelIndex): + source_selection = self.item_filter.mapToSource(index) selected_repo = self.item_model.repos[source_selection.row()] self.itemSelected.emit(selected_repo) - def update_type_filter(self, type_filter:int) -> None: + def update_type_filter(self, type_filter: int) -> None: """hide/show rows corresponding to the type filter - + type_filter is an integer: 0 for all, 1 for workbenches, 2 for macros, and 3 for preference packs - + """ self.item_filter.setPackageFilter(type_filter) pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") pref.SetInt("PackageTypeSelection", type_filter) - def update_text_filter(self, text_filter:str) -> None: + def update_text_filter(self, text_filter: str) -> None: """filter name and description by the regex specified by text_filter""" if text_filter: test_regex = QRegularExpression(text_filter) if test_regex.isValid(): - self.ui.labelFilterValidity.setToolTip(translate("AddonsInstaller","Filter is valid")) + self.ui.labelFilterValidity.setToolTip( + translate("AddonsInstaller", "Filter is valid") + ) icon = QIcon.fromTheme("ok", QIcon(":/icons/edit_OK.svg")) - self.ui.labelFilterValidity.setPixmap(icon.pixmap(16,16)) + self.ui.labelFilterValidity.setPixmap(icon.pixmap(16, 16)) else: - self.ui.labelFilterValidity.setToolTip(translate("AddonsInstaller","Filter regular expression is invalid")) + self.ui.labelFilterValidity.setToolTip( + translate("AddonsInstaller", "Filter regular expression is invalid") + ) icon = QIcon.fromTheme("cancel", QIcon(":/icons/edit_Cancel.svg")) - self.ui.labelFilterValidity.setPixmap(icon.pixmap(16,16)) + self.ui.labelFilterValidity.setPixmap(icon.pixmap(16, 16)) self.ui.labelFilterValidity.show() else: self.ui.labelFilterValidity.hide() self.item_filter.setFilterRegularExpression(text_filter) - def set_view_style(self, style:ListDisplayStyle) -> None: + def set_view_style(self, style: ListDisplayStyle) -> None: self.item_delegate.set_view(style) if style == ListDisplayStyle.COMPACT: self.ui.listPackages.setSpacing(2) else: self.ui.listPackages.setSpacing(5) self.item_model.layoutChanged.emit() - + pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") pref.SetInt("ViewStyle", style) + class PackageListItemModel(QAbstractListModel): repos = [] @@ -142,28 +153,37 @@ class PackageListItemModel(QAbstractListModel): def __init__(self, parent=None) -> None: super().__init__(parent) - def rowCount(self, parent:QModelIndex=QModelIndex()) -> int: + def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: if parent.isValid(): return 0 return len(self.repos) - def columnCount(self, parent:QModelIndex=QModelIndex()) -> int: + def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: if parent.isValid(): return 0 return 1 - def data(self, index:QModelIndex, role:int=Qt.DisplayRole): + def data(self, index: QModelIndex, role: int = Qt.DisplayRole): if not index.isValid(): return None row = index.row() if role == Qt.ToolTipRole: tooltip = "" if self.repos[row].repo_type == AddonManagerRepo.RepoType.PACKAGE: - tooltip = translate("AddonsInstaller","Click for details about package") + f" '{self.repos[row].name}'" + tooltip = ( + translate("AddonsInstaller", "Click for details about package") + + f" '{self.repos[row].name}'" + ) elif self.repos[row].repo_type == AddonManagerRepo.RepoType.WORKBENCH: - tooltip = translate("AddonsInstaller","Click for details about workbench") + f" '{self.repos[row].name}'" + tooltip = ( + translate("AddonsInstaller", "Click for details about workbench") + + f" '{self.repos[row].name}'" + ) elif self.repos[row].repo_type == AddonManagerRepo.RepoType.MACRO: - tooltip = translate("AddonsInstaller","Click for details about macro") + f" '{self.repos[row].name}'" + tooltip = ( + translate("AddonsInstaller", "Click for details about macro") + + f" '{self.repos[row].name}'" + ) return tooltip elif role == PackageListItemModel.DataAccessRole: return self.repos[row] @@ -171,20 +191,28 @@ class PackageListItemModel(QAbstractListModel): def headerData(self, section, orientation, role=Qt.DisplayRole): return None - def setData(self, index:QModelIndex, value, role=Qt.EditRole) -> None: - """ Set the data for this row. The column of the index is ignored. """ + def setData(self, index: QModelIndex, value, role=Qt.EditRole) -> None: + """Set the data for this row. The column of the index is ignored.""" row = index.row() self.write_lock.acquire() if role == PackageListItemModel.StatusUpdateRole: self.repos[row].update_status = value - self.dataChanged.emit(self.index(row,2), self.index(row,2), [PackageListItemModel.StatusUpdateRole]) + self.dataChanged.emit( + self.index(row, 2), + self.index(row, 2), + [PackageListItemModel.StatusUpdateRole], + ) elif role == PackageListItemModel.IconUpdateRole: self.repos[row].icon = value - self.dataChanged.emit(self.index(row,0), self.index(row,0), [PackageListItemModel.IconUpdateRole]) + self.dataChanged.emit( + self.index(row, 0), + self.index(row, 0), + [PackageListItemModel.IconUpdateRole], + ) self.write_lock.release() - def append_item(self, repo:AddonManagerRepo) -> None: + def append_item(self, repo: AddonManagerRepo) -> None: if repo in self.repos: # Cowardly refuse to insert the same repo a second time return @@ -197,53 +225,62 @@ class PackageListItemModel(QAbstractListModel): def clear(self) -> None: if self.rowCount() > 0: self.write_lock.acquire() - self.beginRemoveRows(QModelIndex(), 0, self.rowCount()-1) + self.beginRemoveRows(QModelIndex(), 0, self.rowCount() - 1) self.repos = [] self.endRemoveRows() self.write_lock.release() - def update_item_status(self, name:str, status:AddonManagerRepo.UpdateStatus) -> None: - for row,item in enumerate(self.repos): + def update_item_status( + self, name: str, status: AddonManagerRepo.UpdateStatus + ) -> None: + for row, item in enumerate(self.repos): if item.name == name: - self.setData(self.index(row,0), status, PackageListItemModel.StatusUpdateRole) + self.setData( + self.index(row, 0), status, PackageListItemModel.StatusUpdateRole + ) return - def update_item_icon(self, name:str, icon:QIcon) -> None: - for row,item in enumerate(self.repos): + def update_item_icon(self, name: str, icon: QIcon) -> None: + for row, item in enumerate(self.repos): if item.name == name: - self.setData(self.index(row,0), icon, PackageListItemModel.IconUpdateRole) + self.setData( + self.index(row, 0), icon, PackageListItemModel.IconUpdateRole + ) return - def reload_item(self,repo:AddonManagerRepo) -> None: - for index,item in enumerate(self.repos): + def reload_item(self, repo: AddonManagerRepo) -> None: + for index, item in enumerate(self.repos): if item.name == repo.name: self.write_lock.acquire() self.repos[index] = repo self.write_lock.release() return + class CompactView(QWidget): - """ A single-line view of the package information """ + """A single-line view of the package information""" from compact_view import Ui_CompactView - def __init__(self,parent=None): + def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_CompactView() self.ui.setupUi(self) + class ExpandedView(QWidget): - """ A multi-line view of the package information """ + """A multi-line view of the package information""" from expanded_view import Ui_ExpandedView - def __init__(self,parent=None): + def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_ExpandedView() self.ui.setupUi(self) + class PackageListItemDelegate(QStyledItemDelegate): - """ Render the repo data as a formatted region """ + """Render the repo data as a formatted region""" def __init__(self, parent=None): super().__init__(parent) @@ -251,8 +288,8 @@ class PackageListItemDelegate(QStyledItemDelegate): self.expanded = ExpandedView() self.compact = CompactView() self.widget = self.expanded - - def set_view (self, style:ListDisplayStyle) -> None: + + def set_view(self, style: ListDisplayStyle) -> None: if not self.displayStyle == style: self.displayStyle = style @@ -265,11 +302,11 @@ class PackageListItemDelegate(QStyledItemDelegate): if self.displayStyle == ListDisplayStyle.EXPANDED: self.widget = self.expanded self.widget.ui.labelPackageName.setText(f"

{repo.name}

") - self.widget.ui.labelIcon.setPixmap(repo.icon.pixmap(QSize(48,48))) + self.widget.ui.labelIcon.setPixmap(repo.icon.pixmap(QSize(48, 48))) else: self.widget = self.compact self.widget.ui.labelPackageName.setText(f"{repo.name}") - self.widget.ui.labelIcon.setPixmap(repo.icon.pixmap(QSize(16,16))) + self.widget.ui.labelIcon.setPixmap(repo.icon.pixmap(QSize(16, 16))) self.widget.ui.labelIcon.setText("") if repo.metadata: @@ -279,11 +316,16 @@ class PackageListItemDelegate(QStyledItemDelegate): maintainers = repo.metadata.Maintainer maintainers_string = "" if len(maintainers) == 1: - maintainers_string = translate("AddonsInstaller","Maintainer") + f": {maintainers[0]['name']} <{maintainers[0]['email']}>" + maintainers_string = ( + translate("AddonsInstaller", "Maintainer") + + f": {maintainers[0]['name']} <{maintainers[0]['email']}>" + ) elif len(maintainers) > 1: - maintainers_string = translate("AddonsInstaller","Maintainers:") + maintainers_string = translate("AddonsInstaller", "Maintainers:") for maintainer in maintainers: - maintainers_string += f"\n{maintainer['name']} <{maintainer['email']}>" + maintainers_string += ( + f"\n{maintainer['name']} <{maintainer['email']}>" + ) self.widget.ui.labelMaintainer.setText(maintainers_string) else: self.widget.ui.labelDescription.setText("") @@ -296,86 +338,103 @@ class PackageListItemDelegate(QStyledItemDelegate): self.widget.ui.labelStatus.setText(self.get_expanded_update_string(repo)) else: self.widget.ui.labelStatus.setText(self.get_compact_update_string(repo)) - + self.widget.adjustSize() - def get_compact_update_string(self, repo:AddonManagerRepo) -> str: - """ Get a single-line string listing details about the installed version and date """ + def get_compact_update_string(self, repo: AddonManagerRepo) -> str: + """Get a single-line string listing details about the installed version and date""" result = "" if repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED: - result = translate("AddonsInstaller","Installed") + result = translate("AddonsInstaller", "Installed") elif repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE: - result = translate("AddonsInstaller","Up-to-date") + result = translate("AddonsInstaller", "Up-to-date") elif repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: - result = translate("AddonsInstaller","Update available") + result = translate("AddonsInstaller", "Update available") elif repo.update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART: - result = translate("AddonsInstaller","Pending restart") + result = translate("AddonsInstaller", "Pending restart") return result - def get_expanded_update_string(self, repo:AddonManagerRepo) -> str: - """ Get a multi-line string listing details about the installed version and date """ + def get_expanded_update_string(self, repo: AddonManagerRepo) -> str: + """Get a multi-line string listing details about the installed version and date""" result = "" - + installed_version_string = "" if repo.update_status != AddonManagerRepo.UpdateStatus.NOT_INSTALLED: if repo.installed_version: - installed_version_string = "\n" + translate("AddonsInstaller", "Installed version") + ": " + installed_version_string = ( + "\n" + translate("AddonsInstaller", "Installed version") + ": " + ) installed_version_string += repo.installed_version else: - installed_version_string = "\n" + translate("AddonsInstaller", "Unknown version") - + installed_version_string = "\n" + translate( + "AddonsInstaller", "Unknown version" + ) + installed_date_string = "" if repo.updated_timestamp: - installed_date_string = "\n" + translate("AddonsInstaller", "Installed on") + ": " - installed_date_string += QDateTime.fromTime_t(repo.updated_timestamp).date().toString(Qt.SystemLocaleShortDate) + installed_date_string = ( + "\n" + translate("AddonsInstaller", "Installed on") + ": " + ) + installed_date_string += ( + QDateTime.fromTime_t(repo.updated_timestamp) + .date() + .toString(Qt.SystemLocaleShortDate) + ) available_version_string = "" if repo.metadata: - available_version_string = "\n" + translate("AddonsInstaller", "Available version") + ": " + available_version_string = ( + "\n" + translate("AddonsInstaller", "Available version") + ": " + ) available_version_string += repo.metadata.Version if repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED: - result = translate("AddonsInstaller","Installed") + result = translate("AddonsInstaller", "Installed") result += installed_version_string result += installed_date_string elif repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE: - result = translate("AddonsInstaller","Up-to-date") + result = translate("AddonsInstaller", "Up-to-date") result += installed_version_string result += installed_date_string elif repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: - result = translate("AddonsInstaller","Update available") + result = translate("AddonsInstaller", "Update available") result += installed_version_string result += installed_date_string result += available_version_string elif repo.update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART: - result = translate("AddonsInstaller","Pending restart") + result = translate("AddonsInstaller", "Pending restart") return result - def paint(self, painter:QPainter, option:QStyleOptionViewItem, index:QModelIndex): + def paint( + self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex + ): painter.save() self.widget.resize(option.rect.size()) painter.translate(option.rect.topLeft()) self.widget.render(painter, QPoint(), QRegion(), QWidget.DrawChildren) painter.restore() + class PackageListFilter(QSortFilterProxyModel): - """ Handle filtering the item list on various criteria """ + """Handle filtering the item list on various criteria""" def __init__(self): super().__init__() - self.package_type = 0 # Default to showing everything + self.package_type = 0 # Default to showing everything self.setSortCaseSensitivity(Qt.CaseInsensitive) - def setPackageFilter(self, type:int) -> None: # 0=All, 1=Workbenches, 2=Macros, 3=Preference Packs + def setPackageFilter( + self, type: int + ) -> None: # 0=All, 1=Workbenches, 2=Macros, 3=Preference Packs self.package_type = type self.invalidateFilter() - + def lessThan(self, left, right) -> bool: - l = self.sourceModel().data(left,PackageListItemModel.DataAccessRole) - r = self.sourceModel().data(right,PackageListItemModel.DataAccessRole) + l = self.sourceModel().data(left, PackageListItemModel.DataAccessRole) + r = self.sourceModel().data(right, PackageListItemModel.DataAccessRole) lname = l.name if l.metadata is None else l.metadata.Name rname = r.name if r.metadata is None else r.metadata.Name @@ -383,17 +442,17 @@ class PackageListFilter(QSortFilterProxyModel): def filterAcceptsRow(self, row, parent=QModelIndex()): index = self.sourceModel().createIndex(row, 0) - data = self.sourceModel().data(index,PackageListItemModel.DataAccessRole) + data = self.sourceModel().data(index, PackageListItemModel.DataAccessRole) if self.package_type == 1: if not data.contains_workbench(): return False elif self.package_type == 2: - if not data.contains_macro(): - return False + if not data.contains_macro(): + return False elif self.package_type == 3: - if not data.contains_preference_pack(): - return False - + if not data.contains_preference_pack(): + return False + name = data.name if data.metadata is None else data.metadata.Name desc = data.description if not data.metadata else data.metadata.Description re = self.filterRegularExpression() @@ -407,35 +466,40 @@ class PackageListFilter(QSortFilterProxyModel): else: return False + class Ui_PackageList(object): - """ The contents of the PackageList widget """ + """The contents of the PackageList widget""" def setupUi(self, Form): if not Form.objectName(): - Form.setObjectName(u"PackageList") + Form.setObjectName("PackageList") self.verticalLayout = QVBoxLayout(Form) - self.verticalLayout.setObjectName(u"verticalLayout") + self.verticalLayout.setObjectName("verticalLayout") self.horizontalLayout_6 = QHBoxLayout() - self.horizontalLayout_6.setObjectName(u"horizontalLayout_6") + self.horizontalLayout_6.setObjectName("horizontalLayout_6") self.buttonCompactLayout = QToolButton(Form) - self.buttonCompactLayout.setObjectName(u"buttonCompactLayout") + self.buttonCompactLayout.setObjectName("buttonCompactLayout") self.buttonCompactLayout.setCheckable(True) self.buttonCompactLayout.setAutoExclusive(True) - self.buttonCompactLayout.setIcon(QIcon.fromTheme("expanded_view", QIcon(":/icons/compact_view.svg"))) + self.buttonCompactLayout.setIcon( + QIcon.fromTheme("expanded_view", QIcon(":/icons/compact_view.svg")) + ) self.horizontalLayout_6.addWidget(self.buttonCompactLayout) self.buttonExpandedLayout = QToolButton(Form) - self.buttonExpandedLayout.setObjectName(u"buttonExpandedLayout") + self.buttonExpandedLayout.setObjectName("buttonExpandedLayout") self.buttonExpandedLayout.setCheckable(True) self.buttonExpandedLayout.setChecked(True) self.buttonExpandedLayout.setAutoExclusive(True) - self.buttonExpandedLayout.setIcon(QIcon.fromTheme("expanded_view", QIcon(":/icons/expanded_view.svg"))) + self.buttonExpandedLayout.setIcon( + QIcon.fromTheme("expanded_view", QIcon(":/icons/expanded_view.svg")) + ) self.horizontalLayout_6.addWidget(self.buttonExpandedLayout) self.labelPackagesContaining = QLabel(Form) - self.labelPackagesContaining.setObjectName(u"labelPackagesContaining") + self.labelPackagesContaining.setObjectName("labelPackagesContaining") self.horizontalLayout_6.addWidget(self.labelPackagesContaining) @@ -444,25 +508,25 @@ class Ui_PackageList(object): self.comboPackageType.addItem("") self.comboPackageType.addItem("") self.comboPackageType.addItem("") - self.comboPackageType.setObjectName(u"comboPackageType") + self.comboPackageType.setObjectName("comboPackageType") self.horizontalLayout_6.addWidget(self.comboPackageType) self.lineEditFilter = QLineEdit(Form) - self.lineEditFilter.setObjectName(u"lineEditFilter") + self.lineEditFilter.setObjectName("lineEditFilter") self.lineEditFilter.setClearButtonEnabled(True) self.horizontalLayout_6.addWidget(self.lineEditFilter) self.labelFilterValidity = QLabel(Form) - self.labelFilterValidity.setObjectName(u"labelFilterValidity") + self.labelFilterValidity.setObjectName("labelFilterValidity") self.horizontalLayout_6.addWidget(self.labelFilterValidity) self.verticalLayout.addLayout(self.horizontalLayout_6) self.listPackages = QListView(Form) - self.listPackages.setObjectName(u"listPackages") + self.listPackages.setObjectName("listPackages") self.listPackages.setEditTriggers(QAbstractItemView.NoEditTriggers) self.listPackages.setProperty("showDropIndicator", False) self.listPackages.setSelectionMode(QAbstractItemView.NoSelection) @@ -480,12 +544,27 @@ class Ui_PackageList(object): QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - self.labelPackagesContaining.setText(QCoreApplication.translate("AddonsInstaller", u"Show packages containing:", None)) - self.comboPackageType.setItemText(0, QCoreApplication.translate("AddonsInstaller", u"All", None)) - self.comboPackageType.setItemText(1, QCoreApplication.translate("AddonsInstaller", u"Workbenches", None)) - self.comboPackageType.setItemText(2, QCoreApplication.translate("AddonsInstaller", u"Macros", None)) - self.comboPackageType.setItemText(3, QCoreApplication.translate("AddonsInstaller", u"Preference Packs", None)) - - self.lineEditFilter.setPlaceholderText(QCoreApplication.translate("AddonsInstaller", u"Filter", None)) - self.labelFilterValidity.setText(QCoreApplication.translate("AddonsInstaller", u"OK", None)) + self.labelPackagesContaining.setText( + QCoreApplication.translate( + "AddonsInstaller", "Show packages containing:", None + ) + ) + self.comboPackageType.setItemText( + 0, QCoreApplication.translate("AddonsInstaller", "All", None) + ) + self.comboPackageType.setItemText( + 1, QCoreApplication.translate("AddonsInstaller", "Workbenches", None) + ) + self.comboPackageType.setItemText( + 2, QCoreApplication.translate("AddonsInstaller", "Macros", None) + ) + self.comboPackageType.setItemText( + 3, QCoreApplication.translate("AddonsInstaller", "Preference Packs", None) + ) + self.lineEditFilter.setPlaceholderText( + QCoreApplication.translate("AddonsInstaller", "Filter", None) + ) + self.labelFilterValidity.setText( + QCoreApplication.translate("AddonsInstaller", "OK", None) + )