def download(self, baseurl, clonedir): "downloads and unzip a zip version from a git repo" bakdir = None if os.path.exists(clonedir): bakdir = clonedir+".bak" if os.path.exists(bakdir): shutil.rmtree(bakdir) os.rename(clonedir, bakdir) os.makedirs(clonedir) zipurl = utils.get_zip_url(baseurl) if not zipurl: return translate("AddonsInstaller", "Error: Unable to locate zip from") + " " + baseurl try: print("Downloading "+zipurl) u = utils.urlopen(zipurl) except Exception: return translate("AddonsInstaller", "Error: Unable to download") + " " + zipurl if not u: return translate("AddonsInstaller", "Error: Unable to download") + " " + zipurl zfile = _stringio() zfile.write(u.read()) zfile = zipfile.ZipFile(zfile) master = zfile.namelist()[0] # github will put everything in a subfolder zfile.extractall(clonedir) u.close() zfile.close() for filename in os.listdir(clonedir+os.sep+master): shutil.move(clonedir+os.sep+master+os.sep+filename, clonedir+os.sep+filename) os.rmdir(clonedir+os.sep+master) if bakdir: shutil.rmtree(bakdir) return translate("AddonsInstaller", "Successfully installed") + " " + zipurl
def run(self): "populates the list of addons" self.progressbar_show.emit(True) u = urlopen("https://github.com/FreeCAD/FreeCAD-addons") p = u.read() if sys.version_info.major >= 3 and isinstance(p, bytes): p = p.decode("utf-8") u.close() p = p.replace("\n"," ") p = re.findall("octicon-file-submodule(.*?)message",p) basedir = FreeCAD.getUserAppDataDir() moddir = basedir + os.sep + "Mod" repos = [] for l in p: #name = re.findall("data-skip-pjax=\"true\">(.*?)<",l)[0] name = re.findall("title=\"(.*?) @",l)[0] self.info_label.emit(name) #url = re.findall("title=\"(.*?) @",l)[0] url = "https://github.com/" + re.findall("href=\"\/(.*?)\/tree",l)[0] addondir = moddir + os.sep + name #print ("found:",name," at ",url) if not os.path.exists(addondir): state = 0 else: state = 1 repos.append([name,url,state]) if not repos: self.info_label.emit(translate("AddonsInstaller", "Unable to download addon list.")) else: repos = sorted(repos, key=lambda s: s[0].lower()) for repo in repos: self.addon_repo.emit(repo) self.info_label.emit(translate("AddonsInstaller", "Workbenches list was updated.")) self.progressbar_show.emit(False) self.stop = True
def download(self,giturl,clonedir): "downloads and unzip from github" import zipfile bakdir = None if os.path.exists(clonedir): bakdir = clonedir+".bak" if os.path.exists(bakdir): shutil.rmtree(bakdir) os.rename(clonedir,bakdir) os.makedirs(clonedir) zipurl = giturl+"/archive/master.zip" try: print("Downloading "+zipurl) u = urlopen(zipurl) except: return translate("AddonsInstaller", "Error: Unable to download") + " " + zipurl zfile = _stringio() zfile.write(u.read()) zfile = zipfile.ZipFile(zfile) master = zfile.namelist()[0] # github will put everything in a subfolder zfile.extractall(clonedir) u.close() zfile.close() for filename in os.listdir(clonedir+os.sep+master): shutil.move(clonedir+os.sep+master+os.sep+filename, clonedir+os.sep+filename) os.rmdir(clonedir+os.sep+master) if bakdir: shutil.rmtree(bakdir) return translate("AddonsInstaller", "Successfully installed") + " " + zipurl
def install(self,repos=None): if self.tabWidget.currentIndex() == 0: # Tab "Workbenches". idx = None if repos: idx = [] for repo in repos: for i,r in enumerate(self.repos): if r[0] == repo: idx.append(i) else: idx = self.listWorkbenches.currentRow() if idx != None: if hasattr(self,"install_worker"): if self.install_worker.isRunning(): return self.install_worker = InstallWorker(self.repos, idx) self.install_worker.info_label.connect(self.set_information_label) self.install_worker.progressbar_show.connect(self.show_progress_bar) self.install_worker.start() elif self.tabWidget.currentIndex() == 1: # Tab "Macros". macro = self.macros[self.listMacros.currentRow()] if install_macro(macro, self.macro_repo_dir): self.labelDescription.setText(translate("AddonsInstaller", "Macro successfully installed. The macro is now available from the Macros dialog.")) else: self.labelDescription.setText(translate("AddonsInstaller", "Unable to install")) self.update_status()
def remove(self): "uninstalls a macro or workbench" if self.dialog.tabWidget.currentIndex() == 0: # Tab "Workbenches". idx = self.dialog.listWorkbenches.currentRow() basedir = FreeCAD.getUserAppDataDir() moddir = basedir + os.sep + "Mod" clonedir = moddir + os.sep + self.repos[idx][0] if os.path.exists(clonedir): shutil.rmtree(clonedir, onerror=self.remove_readonly) self.dialog.description.setText( translate( "AddonsInstaller", "Addon successfully removed. Please restart FreeCAD")) else: self.dialog.description.setText( translate("AddonsInstaller", "Unable to remove this addon")) elif self.dialog.tabWidget.currentIndex() == 1: # Tab "Macros". macro = self.macros[self.dialog.listMacros.currentRow()] if remove_macro(macro): self.dialog.description.setText( translate('AddonsInstaller', 'Macro successfully removed.')) else: self.dialog.description.setText( translate('AddonsInstaller', 'Macro could not be removed.')) self.update_status(soft=True) self.addon_removed = True # A value to trigger the restart message on dialog close
def run(self): self.progressbar_show.emit(True) self.info_label.emit(translate("AddonsInstaller", "Retrieving description...")) if not self.macro.parsed and self.macro.on_git: self.info_label.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.info_label.emit(translate('AddonsInstaller', 'Retrieving info from wiki')) mac = self.macro.name.replace(' ', '_') mac = mac.replace('&', '%26') mac = mac.replace('+', '%2B') url = 'https://www.freecadweb.org/wiki/Macro_' + mac self.macro.fill_details_from_wiki(url) if self.macro.is_installed(): already_installed_msg = ('<strong>' + translate("AddonsInstaller", "This addon is already installed.") + '</strong><br>') else: already_installed_msg = '' message = (already_installed_msg + self.macro.desc + ' - <a href="' + self.macro.url + '"><span style="word-wrap: break-word;width:15em;text-decoration: underline; color:#0000ff;">' + self.macro.url + '</span></a>') self.info_label.emit(message) self.progressbar_show.emit(False) self.stop = True
def install(self,repos=None): """installs a workbench or macro""" if self.dialog.tabWidget.currentIndex() == 0: # Tab "Workbenches". idx = None if repos: idx = [] for repo in repos: for i,r in enumerate(self.repos): if r[0] == repo: idx.append(i) else: idx = self.dialog.listWorkbenches.currentRow() if idx != None: if hasattr(self,"install_worker") and self.install_worker: if self.install_worker.isRunning(): return self.install_worker = InstallWorker(self.repos, idx) self.install_worker.info_label.connect(self.show_information) self.install_worker.progressbar_show.connect(self.show_progress_bar) self.install_worker.mark_recompute.connect(self.mark_recompute) self.install_worker.start() elif self.dialog.tabWidget.currentIndex() == 1: # Tab "Macros". macro = self.macros[self.dialog.listMacros.currentRow()] if utils.install_macro(macro, self.macro_repo_dir): self.dialog.description.setText(translate("AddonsInstaller", "Macro successfully installed. The macro is now available from the Macros dialog.")) else: self.dialog.description.setText(translate("AddonsInstaller", "Unable to install"))
def run(self): self.progressbar_show.emit(True) self.info_label.emit(translate("AddonsInstaller", "Retrieving description...")) if len(self.repos[self.idx]) == 4: desc = self.repos[self.idx][3] else: url = self.repos[self.idx][1] self.info_label.emit(translate("AddonsInstaller", "Retrieving info from") + ' ' + str(url)) u = urlopen(url) p = u.read() if sys.version_info.major >= 3 and isinstance(p, bytes): p = p.decode("utf-8") u.close() desc = re.findall("<meta property=\"og:description\" content=\"(.*?)\"",p) if desc: desc = desc[0] if self.repos[self.idx][0] in OBSOLETE: desc += " <b>This add-on is marked as obsolete</b> - This usually means it is no longer maintained, and some more advanced add-on in this list provides the same functionality." else: desc = "Unable to retrieve addon description" self.repos[self.idx].append(desc) self.addon_repos.emit(self.repos) if self.repos[self.idx][2] == 1: upd = False # checking for updates if not NOGIT: try: import git except: pass else: repo = self.repos[self.idx] clonedir = FreeCAD.getUserAppDataDir() + os.sep + "Mod" + os.sep + repo[0] 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[1], 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") cw = bare_repo.config_writer() cw.set('core', 'bare', False) del cw repo = git.Repo(clonedir) repo.head.reset('--hard') gitrepo = git.Git(clonedir) gitrepo.fetch() if "git pull" in gitrepo.status(): upd = True if upd: message = "<strong>" + translate("AddonsInstaller", "An update is available for this addon.") + "</strong><br>" + desc + ' - <a href="' + self.repos[self.idx][1] + '"><span style="word-wrap: break-word;width:15em;text-decoration: underline; color:#0000ff;">' + self.repos[self.idx][1] + '</span></a>' else: message = "<strong>" + translate("AddonsInstaller", "This addon is already installed.") + "</strong><br>" + desc + ' - <a href="' + self.repos[self.idx][1] + '"><span style="word-wrap: break-word;width:15em;text-decoration: underline; color:#0000ff;">' + self.repos[self.idx][1] + '</span></a>' else: message = desc + ' - <a href="' + self.repos[self.idx][1] + '"><span style="word-wrap: break-word;width:15em;text-decoration: underline; color:#0000ff;">' + self.repos[self.idx][1] + '</span></a>' self.info_label.emit( message ) self.progressbar_show.emit(False) self.stop = True
def run(self): self.progressbar_show.emit(True) self.info_label.emit(translate("AddonsInstaller", "Retrieving description...")) if not self.macro.parsed and self.macro.on_git: self.info_label.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.info_label.emit(translate("AddonsInstaller", "Retrieving info from wiki")) mac = self.macro.name.replace(" ", "_") mac = mac.replace("&", "%26") mac = mac.replace("+", "%2B") url = "https://wiki.freecadweb.org/Macro_" + mac self.macro.fill_details_from_wiki(url) if self.macro.is_installed(): already_installed_msg = ('<strong style=\"background: #00B629;\">' + translate("AddonsInstaller", "This macro is already installed.") + '</strong><br>') else: already_installed_msg = "" message = (already_installed_msg + "<h1>"+self.macro.name+"</h1>" + self.macro.desc + "<br/><br/>Macro location: <a href=\"" + self.macro.url + "\">" + self.macro.url + "</a>") self.info_label.emit(message) self.progressbar_show.emit(False) self.stop = True
def reject(self): # ensure all threads are finished before closing oktoclose = True for worker in ["update_worker","check_worker","show_worker","showmacro_worker", "macro_worker","install_worker"]: if hasattr(self,worker): thread = getattr(self,worker) if thread: if not thread.isFinished(): oktoclose = False if oktoclose: if hasattr(self,"install_worker") or hasattr(self,"addon_removed"): m = QtGui.QMessageBox() m.setWindowTitle(translate("AddonsInstaller","Addon manager")) m.setText(translate("AddonsInstaller","You must restart FreeCAD for changes to take effect. Press Ok to restart FreeCAD now, or Cancel to restart later.")) m.setIcon(m.Warning) m.setStandardButtons(m.Ok | m.Cancel) m.setDefaultButton(m.Cancel) ret = m.exec_() if ret == m.Ok: shutil.rmtree(self.macro_repo_dir,onerror=self.remove_readonly) # restart FreeCAD after a delay to give time to close this dialog QtCore.QTimer.singleShot(1000,restartFreeCAD) try: shutil.rmtree(self.macro_repo_dir,onerror=self.remove_readonly) except: pass QtGui.QDialog.reject(self)
def update_text_filter(self, text_filter: str) -> None: """filter name and description by the regex specified by text_filter""" if text_filter: if hasattr(self.item_filter, "setFilterRegularExpression"): # Added in Qt 5.12 test_regex = QRegularExpression(text_filter) else: test_regex = QRegExp(text_filter) if test_regex.isValid(): 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)) else: 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.show() else: self.ui.labelFilterValidity.hide() if hasattr(self.item_filter, "setFilterRegularExpression"): # Added in Qt 5.12 self.item_filter.setFilterRegularExpression(text_filter) else: self.item_filter.setFilterRegExp(text_filter)
def retrieve_macros_from_git(self): """Retrieve macros from FreeCAD-macros.git Emits a signal for each macro in https://github.com/FreeCAD/FreeCAD-macros.git """ try: import git except ImportError: self.info_label_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 self.info_label_signal.emit('Downloading list of macros from git...') try: git.Repo.clone_from('https://github.com/FreeCAD/FreeCAD-macros.git', self.repo_dir) except: FreeCAD.Console.PrintWarning(translate('AddonsInstaller', 'Something went wrong with the Git Macro Retrieval, possibly the Git executable is not in the path')+"\n") for dirpath, _, filenames in os.walk(self.repo_dir): if '.git' in dirpath: continue for filename in filenames: if filename.lower().endswith('.fcmacro'): macro = Macro(filename[:-8]) # Remove ".FCMacro". macro.on_git = True macro.src_filename = os.path.join(dirpath, filename) self.macros.append(macro)
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): 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.packageDetails.show_repo(repo) self.restart_required = True else: 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.packageDetails.show_repo(repo) else: self.dialog.textBrowserReadMe.setText( translate("AddonsInstaller", "Macro could not be removed."))
def retrieve_macros_from_git(self): """Retrieve macros from FreeCAD-macros.git Emits a signal for each macro in https://github.com/FreeCAD/FreeCAD-macros.git """ if not have_git: self.info_label_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 self.info_label_signal.emit("Downloading list of macros from git...") try: 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") for dirpath, _, filenames in os.walk(self.repo_dir): if ".git" in dirpath: continue for filename in filenames: if filename.lower().endswith(".fcmacro"): macro = Macro(filename[:-8]) # Remove ".FCMacro". macro.on_git = True macro.src_filename = os.path.join(dirpath, filename) self.macros.append(macro)
def run(self): self.progressbar_show.emit(True) self.info_label.emit( translate("AddonsInstaller", "Retrieving description...")) if not self.macro.parsed and self.macro.on_git: self.info_label.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.info_label.emit( translate('AddonsInstaller', 'Retrieving info from wiki')) mac = self.macro.name.replace(' ', '_') mac = mac.replace('&', '%26') mac = mac.replace('+', '%2B') url = 'https://www.freecadweb.org/wiki/Macro_' + mac self.macro.fill_details_from_wiki(url) if self.macro.is_installed(): already_installed_msg = ( '<strong style=\"background: #00B629;\">' + translate( "AddonsInstaller", "This macro is already installed.") + '</strong><br>') else: already_installed_msg = '' message = (already_installed_msg + "<h1>" + self.macro.name + "</h1>" + self.macro.desc + '<br/><br/>Macro location: <a href="' + self.macro.url + '">' + self.macro.url + '</a>') self.info_label.emit(message) self.progressbar_show.emit(False) self.stop = True
def run(self): "populates the list of addons" self.progressbar_show.emit(True) u = utils.urlopen("https://github.com/FreeCAD/FreeCAD-addons") if not u: self.progressbar_show.emit(False) self.done.emit() self.stop = True return p = u.read() if sys.version_info.major >= 3 and isinstance(p, bytes): p = p.decode("utf-8") u.close() p = p.replace("\n"," ") p = re.findall("octicon-file-submodule(.*?)message",p) basedir = FreeCAD.getUserAppDataDir() moddir = basedir + os.sep + "Mod" repos = [] # querying official addons for l in p: #name = re.findall("data-skip-pjax=\"true\">(.*?)<",l)[0] name = re.findall("title=\"(.*?) @",l)[0] self.info_label.emit(name) #url = re.findall("title=\"(.*?) @",l)[0] url = utils.getRepoUrl(l) if url: addondir = moddir + os.sep + name #print ("found:",name," at ",url) if os.path.exists(addondir) and os.listdir(addondir): # make sure the folder exists and it contains files! state = 1 else: state = 0 repos.append([name,url,state]) # querying custom addons customaddons = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons").GetString("CustomRepositories","").split("\n") for url in customaddons: if url: name = url.split("/")[-1] if name.lower().endswith(".git"): name = name[:-4] addondir = moddir + os.sep + name if not os.path.exists(addondir): state = 0 else: state = 1 repos.append([name,url,state]) if not repos: self.info_label.emit(translate("AddonsInstaller", "Unable to download addon list.")) else: repos = sorted(repos, key=lambda s: s[0].lower()) for repo in repos: self.addon_repo.emit(repo) self.info_label.emit(translate("AddonsInstaller", "Workbenches list was updated.")) self.progressbar_show.emit(False) self.done.emit() self.stop = True
def reject(self) -> None: """called when the window has been closed""" # save window geometry for next use pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") pref.SetInt("WindowWidth", self.dialog.width()) pref.SetInt("WindowHeight", self.dialog.height()) # ensure all threads are finished before closing oktoclose = True self.startup_sequence = [] for worker in self.workers: if hasattr(self, worker): thread = getattr(self, worker) if thread: if not thread.isFinished(): thread.requestInterruption() oktoclose = False while not oktoclose: oktoclose = True for worker in self.workers: if hasattr(self, worker): thread = getattr(self, worker) if thread: thread.wait(25) if not thread.isFinished(): oktoclose = False QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents) # Write the cache data for repo in self.item_model.repos: if repo.repo_type == AddonManagerRepo.RepoType.MACRO: self.cache_macro(repo) else: self.cache_package(repo) self.write_package_cache() self.write_macro_cache() if self.restart_required: # display restart dialog 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.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")) 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)
def checkDependencies(self,baseurl): "checks if the repo contains a metadata.txt and check its contents" import FreeCADGui ok = True message = "" depsurl = baseurl.replace("github.com","raw.githubusercontent.com") if not depsurl.endswith("/"): depsurl += "/" depsurl += "master/metadata.txt" try: mu = urlopen(depsurl) except: # no metadata.txt, we just continue without deps checking pass else: # metadata.txt found depsfile = mu.read() mu.close() # urllib2 gives us a bytelike object instead of a string. Have to consider that try: depsfile = depsfile.decode('utf-8') except AttributeError: pass deps = depsfile.split("\n") for l in deps: if l.startswith("workbenches="): depswb = l.split("=")[1].split(",") for wb in depswb: if wb.strip(): if not wb.strip() in FreeCADGui.listWorkbenches().keys(): if not wb.strip()+"Workbench" in FreeCADGui.listWorkbenches().keys(): ok = False message += translate("AddonsInstaller","Missing workbench") + ": " + wb + ", " elif l.startswith("pylibs="): depspy = l.split("=")[1].split(",") for pl in depspy: if pl.strip(): try: __import__(pl.strip()) except: ok = False message += translate("AddonsInstaller","Missing python module") +": " + pl + ", " elif l.startswith("optionalpylibs="): opspy = l.split("=")[1].split(",") for pl in opspy: if pl.strip(): try: __import__(pl.strip()) except: message += translate("AddonsInstaller","Missing optional python module (doesn't prevent installing)") +": " + pl + ", " if message and (not ok): message = translate("AddonsInstaller", "Some errors were found that prevent to install this workbench") + ": <b>" + message + "</b>. " message += translate("AddonsInstaller","Please install the missing components first.") return ok, message
def install(self, repo: AddonManagerRepo) -> None: """installs or updates a workbench, macro, or package""" if hasattr(self, "install_worker") and self.install_worker: if self.install_worker.isRunning(): return if not repo: return 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) self.current_progress_region = 1 self.number_of_progress_regions = 1 self.install_worker.progress_made.connect(self.update_progress_bar) self.install_worker.success.connect(self.on_package_installed) self.install_worker.failure.connect(self.on_installation_failed) self.install_worker.start() elif repo.repo_type == AddonManagerRepo.RepoType.MACRO: macro = repo.macro # To try to ensure atomicity, test the installation into a temp directory first, # and assume if that worked we have good odds of the real installation working failed = False errors = [] with tempfile.TemporaryDirectory() as dir: temp_install_succeeded, error_list = macro.install(dir) if not temp_install_succeeded: failed = True errors = error_list if not failed: real_install_succeeded, errors = macro.install( self.macro_repo_dir) if not real_install_succeeded: failed = True else: utils.update_macro_installation_details(repo) if not failed: 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") + ":") for error in errors: message += "\n * " message += error self.on_installation_failed(repo, message)
def checkDependencies(self,baseurl): "checks if the repo contains a metadata.txt and check its contents" import FreeCADGui ok = True message = "" depsurl = baseurl.replace("github.com","raw.githubusercontent.com") if not depsurl.endswith("/"): depsurl += "/" depsurl += "master/metadata.txt" try: mu = urlopen(depsurl) except urllib2.HTTPError: # no metadata.txt, we just continue without deps checking pass else: # metadata.txt found depsfile = mu.read() mu.close() # urllib2 gives us a bytelike object instead of a string. Have to consider that try: depsfile = depsfile.decode('utf-8') except AttributeError: pass deps = depsfile.split("\n") for l in deps: if l.startswith("workbenches="): depswb = l.split("=")[1].split(",") for wb in depswb: if wb.strip(): if not wb.strip() in FreeCADGui.listWorkbenches().keys(): if not wb.strip()+"Workbench" in FreeCADGui.listWorkbenches().keys(): ok = False message += translate("AddonsInstaller","Missing workbench") + ": " + wb + ", " elif l.startswith("pylibs="): depspy = l.split("=")[1].split(",") for pl in depspy: if pl.strip(): try: __import__(pl.strip()) except: ok = False message += translate("AddonsInstaller","Missing python module") +": " + pl + ", " elif l.startswith("optionalpylibs="): opspy = l.split("=")[1].split(",") for pl in opspy: if pl.strip(): try: __import__(pl.strip()) except: message += translate("AddonsInstaller","Missing optional python module (doesn't prevent installing)") +": " + pl + ", " if message and (not ok): message = translate("AddonsInstaller", "Some errors were found that prevent to install this workbench") + ": <b>" + message + "</b>. " message += translate("AddonsInstaller","Please install the missing components first.") return ok, message
def enable_updates(self,num): """enables the update button""" if num: self.dialog.buttonUpdateAll.setText(translate("AddonsInstaller","Apply")+" "+str(num)+" "+translate("AddonsInstaller","update(s)")) self.dialog.buttonUpdateAll.setEnabled(True) else: self.dialog.buttonUpdateAll.setText(translate("AddonsInstaller","No update available")) self.dialog.buttonUpdateAll.setEnabled(False)
def retranslateUi(self): self.setWindowTitle(translate("AddonsInstaller","Addon manager")) self.labelDescription.setText(translate("AddonsInstaller", "Downloading addon list...")) self.buttonExecute.setText(translate("AddonsInstaller", "Execute")) self.buttonExecute.setToolTip(translate("AddonsInstaller", "This button runs the selected macro (which must be installed first)")) self.buttonCheck.setToolTip(translate("AddonsInstaller", "Check for available updates")) self.buttonCancel.setText(translate("AddonsInstaller", "Close")) self.buttonInstall.setText(translate("AddonsInstaller", "Install / update")) self.buttonRemove.setText(translate("AddonsInstaller", "Remove")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.listWorkbenches), translate("AddonsInstaller", "Workbenches")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.listMacros), translate("AddonsInstaller", "Macros"))
def reject(self): "called when the window has been closed" # save window geometry and splitter state for next use pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") pref.SetInt("WindowWidth", self.dialog.width()) pref.SetInt("WindowHeight", self.dialog.height()) pref.SetInt("SplitterLeft", self.dialog.splitter.sizes()[0]) pref.SetInt("SplitterRight", self.dialog.splitter.sizes()[1]) # ensure all threads are finished before closing oktoclose = True for worker in [ "update_worker", "check_worker", "show_worker", "showmacro_worker", "macro_worker", "install_worker" ]: if hasattr(self, worker): thread = getattr(self, worker) if thread: if not thread.isFinished(): oktoclose = False # all threads have finished if oktoclose: if (hasattr(self, "install_worker") and self.install_worker) or ( hasattr(self, "addon_removed") and self.addon_removed): # display restart dialog from PySide import QtGui, QtCore m = QtGui.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. Press Ok to restart FreeCAD now, or Cancel to restart later." )) m.setIcon(m.Warning) m.setStandardButtons(m.Ok | m.Cancel) m.setDefaultButton(m.Cancel) ret = m.exec_() if ret == m.Ok: shutil.rmtree(self.macro_repo_dir, onerror=self.remove_readonly) # restart FreeCAD after a delay to give time to this dialog to close QtCore.QTimer.singleShot(1000, restartFreeCAD) try: shutil.rmtree(self.macro_repo_dir, onerror=self.remove_readonly) except: pass return True
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") elif repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE: result = translate("AddonsInstaller", "Up-to-date") elif repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: result = translate("AddonsInstaller", "Update available") elif repo.update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART: result = translate("AddonsInstaller", "Pending restart") return result
def mark_recompute(self,addon): """marks an addon in the list as installed but needs recompute""" for i in range(self.dialog.listWorkbenches.count()): txt = self.dialog.listWorkbenches.item(i).text().strip() if txt.endswith(" ("+translate("AddonsInstaller","Installed")+")"): txt = txt[:-12] elif txt.endswith(" ("+translate("AddonsInstaller","Update available")+")"): txt = txt[:-19] if txt == addon: from PySide import QtGui self.dialog.listWorkbenches.item(i).setText(txt+" ("+translate("AddonsInstaller","Restart required")+")") self.dialog.listWorkbenches.item(i).setIcon(QtGui.QIcon(":/icons/edit-undo.svg"))
def update_status(self, soft=False): """Updates the list of workbenches/macros. If soft is true, items are not recreated (and therefore display text isn't triggered)" """ moddir = FreeCAD.getUserAppDataDir() + os.sep + "Mod" if soft: for i in range(self.dialog.listWorkbenches.count()): txt = self.dialog.listWorkbenches.item(i).text().strip() ext = "" if txt.endswith(" ("+translate("AddonsInstaller", "Installed")+")"): txt = txt[:-12] ext = " ("+translate("AddonsInstaller", "Installed")+")" elif txt.endswith(" ("+translate("AddonsInstaller", "Update available")+")"): txt = txt[:-19] ext = " ("+translate("AddonsInstaller", "Update available")+")" elif txt.endswith(" ("+translate("AddonsInstaller", "Restart required")+")"): txt = txt[:-19] ext = " ("+translate("AddonsInstaller", "Restart required")+")" if os.path.exists(os.path.join(moddir, txt)): self.dialog.listWorkbenches.item(i).setText(txt+ext) else: self.dialog.listWorkbenches.item(i).setText(txt) self.dialog.listWorkbenches.item(i).setIcon(self.get_icon(txt)) for i in range(self.dialog.listMacros.count()): txt = self.dialog.listMacros.item(i).text().strip() if txt.endswith(" ("+translate("AddonsInstaller", "Installed")+")"): txt = txt[:-12] elif txt.endswith(" ("+translate("AddonsInstaller", "Update available")+")"): txt = txt[:-19] if os.path.exists(os.path.join(moddir, txt)): self.dialog.listMacros.item(i).setText(txt+ext) else: self.dialog.listMacros.item(i).setText(txt) self.dialog.listMacros.item(i).setIcon(QtGui.QIcon(":/icons/document-python.svg")) else: self.dialog.listWorkbenches.clear() self.dialog.listMacros.clear() for wb in self.repos: if os.path.exists(os.path.join(moddir, wb[0])): self.dialog.listWorkbenches.addItem( QtGui.QListWidgetItem(QtGui.QIcon(":/icons/button_valid.svg"), str(wb[0]) + " (" + translate("AddonsInstaller", "Installed") + ")")) wb[2] = 1 else: self.dialog.listWorkbenches.addItem( QtGui.QListWidgetItem(QtGui.QIcon(":/icons/document-python.svg"), str(wb[0]))) wb[2] = 0 for macro in self.macros: if macro.is_installed(): self.dialog.listMacros.addItem(item) else: self.dialog.listMacros.addItem( QtGui.QListWidgetItem(QtGui.QIcon(":/icons/document-python.svg"), macro.name))
def remove(self) -> bool: """Remove a macro and all its related files Returns True if the macro was removed correctly. """ if not self.is_installed(): # Macro not installed, nothing to do. 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) if os.path.exists(macro_path): os.remove(macro_path) elif os.path.exists(macro_path_with_macro_prefix): os.remove(macro_path_with_macro_prefix) # Remove related files, which are supposed to be given relative to # self.src_filename. for other_file in self.other_files: dst_file = os.path.join(macro_dir, other_file) try: os.remove(dst_file) remove_directory_if_empty(os.path.dirname(dst_file)) except Exception: FreeCAD.Console.PrintWarning( translate( "AddonsInstaller", f"Failed to remove macro file '{dst_file}': it might not exist, or its permissions changed", ) + "\n") return True
def retrieve_macros_from_wiki(self): """Retrieve macros from the wiki Read the wiki and emit a signal for each found macro. Reads only the page https://www.freecadweb.org/wiki/Macros_recipes """ self.info_label_signal.emit( "Downloading list of macros from the FreeCAD wiki...") self.progressbar_show.emit(True) u = utils.urlopen("https://www.freecadweb.org/wiki/Macros_recipes") if not u: FreeCAD.Console.PrintWarning( translate( "AddonsInstaller", "Appears to be an issue connecting to the Wiki, " "therefore cannot retrieve Wiki macro list at this time") + "\n") return p = u.read() u.close() if sys.version_info.major >= 3 and isinstance(p, bytes): p = p.decode("utf-8") macros = re.findall('title="(Macro.*?)"', p) macros = [mac for mac in macros if ("translated" not in mac)] for mac in macros: macname = mac[6:] # Remove "Macro ". macname = macname.replace("&", "&") if (macname not in macros_blacklist) and ("recipes" not in macname.lower()): macro = Macro(macname) macro.on_wiki = True self.macros.append(macro)
def check_updates(self) -> None: "checks every installed addon for available updates" pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") autocheck = pref.GetBool("AutoCheck", False) if not autocheck: FreeCAD.Console.PrintLog( "Addon Manager: Skipping update check because AutoCheck user preference is False\n" ) self.do_next_startup_phase() return if not self.packages_with_updates: if hasattr(self, "check_worker"): thread = self.check_worker if thread: if not thread.isFinished(): self.do_next_startup_phase() return self.dialog.buttonUpdateAll.setText( translate("AddonsInstaller", "Checking for updates...")) self.check_worker = CheckWorkbenchesForUpdatesWorker( self.item_model.repos) self.check_worker.finished.connect(self.do_next_startup_phase) self.check_worker.progress_made.connect(self.update_progress_bar) self.check_worker.update_status.connect(self.status_updated) self.check_worker.start() self.enable_updates(len(self.packages_with_updates)) else: self.do_next_startup_phase()
def executemacro(self, repo: AddonManagerRepo) -> None: """executes a selected macro""" macro = repo.macro if not macro or not macro.code: return if macro.is_installed(): macro_path = os.path.join(self.macro_repo_dir, macro.filename) FreeCADGui.open(str(macro_path)) self.dialog.hide() FreeCADGui.SendMsgToActiveView("Run") else: 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) return else: macro_path = os.path.join(dir, macro.filename) FreeCADGui.open(str(macro_path)) self.dialog.hide() FreeCADGui.SendMsgToActiveView("Run")
def run(self): if NOGIT: self.stop = True return try: import git except: self.stop = True return self.progressbar_show.emit(True) basedir = FreeCAD.getUserAppDataDir() moddir = basedir + os.sep + "Mod" self.info_label.emit(translate("AddonsInstaller", "Checking for new versions...")) upds = [] gitpython_warning = False for repo in self.repos: if repo[2] == 1: #installed self.info_label.emit(translate("AddonsInstaller","Checking repo")+" "+repo[0]+"...") clonedir = moddir + os.sep + repo[0] 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[1], 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") gitpython_warning = True cw = bare_repo.config_writer() cw.set('core', 'bare', False) del cw repo = git.Repo(clonedir) repo.head.reset('--hard') gitrepo = git.Git(clonedir) gitrepo.fetch() if "git pull" in gitrepo.status(): self.mark.emit(repo[0]) upds.append(repo[0]) self.progressbar_show.emit(False) if upds: self.info_label.emit(str(len(upds))+" "+translate("AddonsInstaller", "update(s) available")+": "+",".join(upds)+". "+translate("AddonsInstaller","Press the update button again to update them all at once.")) self.change_button.emit() else: self.info_label.emit(translate("AddonsInstaller","Everything is up to date")) self.stop = True
def run(self): if NOGIT: self.stop = True return try: import git except: self.stop = True return basedir = FreeCAD.getUserAppDataDir() moddir = basedir + os.sep + "Mod" upds = [] gitpython_warning = False for repo in self.repos: if repo[2] == 1: #installed #print("Checking for updates for",repo[0]) clonedir = moddir + os.sep + repo[0] 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[1], 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") gitpython_warning = True cw = bare_repo.config_writer() cw.set('core', 'bare', False) del cw repo = git.Repo(clonedir) repo.head.reset('--hard') gitrepo = git.Git(clonedir) try: gitrepo.fetch() except: print( "AddonManager: Unable to fetch git updates for repo", repo[0]) else: if "git pull" in gitrepo.status(): self.mark.emit(repo[0]) upds.append(repo[0]) self.repos[self.repos.index( repo )][2] = 2 # mark as already installed AND already checked for updates self.addon_repos.emit(self.repos) self.enable.emit(len(upds)) self.stop = True
def run(self): """Populates the list of macros""" self.retrieve_macros_from_git() self.retrieve_macros_from_wiki() [self.add_macro_signal.emit(m) for m in sorted(self.macros, key=lambda m: m.name.lower())] if self.macros: self.info_label_signal.emit(translate('AddonsInstaller', 'List of macros successfully retrieved.')) self.progressbar_show.emit(False) self.stop = True
def remove(self): if self.tabWidget.currentIndex() == 0: # Tab "Workbenches". idx = self.listWorkbenches.currentRow() basedir = FreeCAD.getUserAppDataDir() moddir = basedir + os.sep + "Mod" clonedir = moddir + os.sep + self.repos[idx][0] if os.path.exists(clonedir): shutil.rmtree(clonedir, onerror=self.remove_readonly) self.labelDescription.setText(translate("AddonsInstaller", "Addon successfully removed. Please restart FreeCAD")) else: self.labelDescription.setText(translate("AddonsInstaller", "Unable to remove this addon")) elif self.tabWidget.currentIndex() == 1: # Tab "Macros". macro = self.macros[self.listMacros.currentRow()] if remove_macro(macro): self.labelDescription.setText(translate('AddonsInstaller', 'Macro successfully removed.')) else: self.labelDescription.setText(translate('AddonsInstaller', 'Macro could not be removed.')) self.update_status()
def launchAddonMgr(): # first use dialog readWarning = FreeCAD.ParamGet('User parameter:Plugins/addonsRepository').GetBool('readWarning',False) if not readWarning: if QtGui.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!"), QtGui.QMessageBox.Cancel | QtGui.QMessageBox.Ok) != QtGui.QMessageBox.StandardButton.Cancel: FreeCAD.ParamGet('User parameter:Plugins/addonsRepository').SetBool('readWarning',True) readWarning = True if readWarning: dialog = AddonsInstaller() dialog.exec_()
def reject(self): # ensure all threads are finished before closing oktoclose = True for worker in ["update_worker","check_worker","show_worker","showmacro_worker", "macro_worker","install_worker"]: if hasattr(self,worker): thread = getattr(self,worker) if thread: if not thread.isFinished(): oktoclose = False if oktoclose: if hasattr(self,"install_worker"): QtGui.QMessageBox.information(self, translate("AddonsInstaller","Addon manager"), translate("AddonsInstaller","Please restart FreeCAD for changes to take effect.")) shutil.rmtree(self.macro_repo_dir,onerror=self.remove_readonly) QtGui.QDialog.reject(self)
def retrieve_macros_from_git(self): """Retrieve macros from FreeCAD-macros.git Emits a signal for each macro in https://github.com/FreeCAD/FreeCAD-macros.git """ try: import git except ImportError: self.info_label_signal.emit("GitPython not installed! Cannot retrieve macros from git") FreeCAD.Console.PrintWarning(translate('AddonInstaller', 'GitPython not installed! Cannot retrieve macros from git')+"\n") return self.info_label_signal.emit('Downloading list of macros for git...') git.Repo.clone_from('https://github.com/FreeCAD/FreeCAD-macros.git', self.repo_dir) for dirpath, _, filenames in os.walk(self.repo_dir): if '.git' in dirpath: continue for filename in filenames: if filename.lower().endswith('.fcmacro'): macro = Macro(filename[:-8]) # Remove ".FCMacro". macro.on_git = True macro.src_filename = os.path.join(dirpath, filename) self.macros.append(macro)
def run(self): "installs or updates the selected addon" git = None try: import git except Exception as e: self.info_label.emit("GitPython not found.") print(e) FreeCAD.Console.PrintWarning(translate("AddonsInstaller","GitPython not found. Using standard download instead.")+"\n") try: import zipfile except: self.info_label.emit("no zip support.") FreeCAD.Console.PrintError(translate("AddonsInstaller","Your version of python doesn't appear to support ZIP files. Unable to proceed.")+"\n") return try: import StringIO as io except ImportError: # StringIO is not available with python3 import io if not isinstance(self.idx,list): self.idx = [self.idx] for idx in self.idx: if idx < 0: return if not self.repos: return if NOGIT: git = None basedir = FreeCAD.getUserAppDataDir() moddir = basedir + os.sep + "Mod" if not os.path.exists(moddir): os.makedirs(moddir) clonedir = moddir + os.sep + self.repos[idx][0] self.progressbar_show.emit(True) if os.path.exists(clonedir): self.info_label.emit("Updating module...") if git: if not os.path.exists(clonedir + os.sep + '.git'): # Repair addon installed with raw download bare_repo = git.Repo.clone_from(self.repos[idx][1], 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") cw = bare_repo.config_writer() cw.set('core', 'bare', False) del cw repo = git.Repo(clonedir) repo.head.reset('--hard') repo = git.Git(clonedir) answer = repo.pull() # Update the submodules for this repository repo_sms = git.Repo(clonedir) for submodule in repo_sms.submodules: submodule.update(init=True, recursive=True) else: answer = self.download(self.repos[idx][1],clonedir) else: self.info_label.emit("Checking module dependencies...") depsok,answer = self.checkDependencies(self.repos[idx][1]) if depsok: if git: self.info_label.emit("Cloning module...") repo = git.Repo.clone_from(self.repos[idx][1], clonedir, branch='master') # Make sure to clone all the submodules as well if repo.submodules: repo.submodule_update(recursive=True) else: self.info_label.emit("Downloading module...") self.download(self.repos[idx][1],clonedir) answer = translate("AddonsInstaller", "Workbench successfully installed. Please restart FreeCAD to apply the changes.") # symlink any macro contained in the module to the macros folder macro_dir = FreeCAD.getUserMacroDir(True) if not os.path.exists(macro_dir): os.makedirs(macro_dir) for f in os.listdir(clonedir): if f.lower().endswith(".fcmacro"): print("copying macro:",f) symlink(os.path.join(clonedir, f), os.path.join(macro_dir, f)) FreeCAD.ParamGet('User parameter:Plugins/'+self.repos[idx][0]).SetString("destination",clonedir) answer += translate("AddonsInstaller", "A macro has been installed and is available the Macros menu") + ": <b>" answer += f + "</b>" self.progressbar_show.emit(False) self.info_label.emit(answer) self.stop = True
def change_update_button(self): self.buttonCheck.setText(translate("AddonsInstaller", "Update all")) self.buttonCheck.setToolTip(translate("AddonsInstaller", "Apply all available updates"))
def fill_details_from_wiki(self, url): code = "" try: u = urlopen(url) except urllib2.HTTPError: return p = u.read() if sys.version_info.major >= 3 and isinstance(p, bytes): p = p.decode('utf-8') u.close() # check if the macro page has its code hosted elsewhere, download if needed if "rawcodeurl" in p: rawcodeurl = re.findall("rawcodeurl.*?href=\"(http.*?)\">",p) if rawcodeurl: rawcodeurl = rawcodeurl[0] try: u2 = urlopen(rawcodeurl) except urllib2.HTTPError: return # code = u2.read() # github is slow to respond... We need to use this trick below response = "" block = 8192 #expected = int(u2.headers['content-length']) while 1: #print("expected:",expected,"got:",len(response)) data = u2.read(block) if not data: break if sys.version_info.major >= 3 and isinstance(data, bytes): data = data.decode('utf-8') response += data if response: code = response u2.close() if not code: code = re.findall('<pre>(.*?)<\/pre>', p.replace('\n', '--endl--')) if code: # code = code[0] # take the biggest code block code = sorted(code, key=len)[-1] code = code.replace('--endl--', '\n') else: FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "Unable to fetch the code of this macro.")) # Clean HTML escape codes. try: from HTMLParser import HTMLParser except ImportError: from html.parser import HTMLParser if sys.version_info.major < 3: code = code.decode('utf8') try: code = HTMLParser().unescape(code) code = code.replace(b'\xc2\xa0'.decode("utf-8"), ' ') except: FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "Unable to clean macro code: ") + code + '\n') if sys.version_info.major < 3: code = code.encode('utf8') desc = re.findall("<td class=\"ctEven left macro-description\">(.*?)<\/td>", p.replace('\n', ' ')) if desc: desc = desc[0] else: FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "Unable to retrieve a description for this macro.")) desc = "No description available" self.desc = desc self.url = url self.code = code self.parsed = True