diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index bb9df1f8f1..8188d92186 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -36,9 +36,15 @@ It will fetch its contents from https://github.com/FreeCAD/FreeCAD-addons You need a working internet connection, and the GitPython package installed. ''' +import os +import re +import re +import shutil +import stat +import sys +import tempfile from PySide import QtCore, QtGui -import sys, os, re, shutil, stat import FreeCAD if sys.version_info.major < 3: import urllib2 @@ -111,6 +117,9 @@ 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)) + # 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']: if not hasattr(old_macro, attr): @@ -123,6 +132,8 @@ class AddonsInstaller(QtGui.QDialog): QtGui.QDialog.__init__(self) self.repos = [] self.macros = [] + self.macro_repo_dir = tempfile.mkdtemp() + self.setObjectName("AddonsInstaller") self.resize(326, 304) self.verticalLayout = QtGui.QVBoxLayout(self) @@ -132,6 +143,7 @@ class AddonsInstaller(QtGui.QDialog): self.listWorkbenches.setIconSize(QtCore.QSize(16,16)) self.tabWidget.addTab(self.listWorkbenches,"") self.listMacros = QtGui.QListWidget() + self.listMacros.setSortingEnabled(False) self.listMacros.setIconSize(QtCore.QSize(16,16)) self.tabWidget.addTab(self.listMacros,"") self.labelDescription = QtGui.QLabel() @@ -204,6 +216,7 @@ class AddonsInstaller(QtGui.QDialog): if not thread.isFinished(): oktoclose = False if oktoclose: + shutil.rmtree(self.macro_repo_dir) QtGui.QDialog.reject(self) def retranslateUi(self): @@ -272,9 +285,9 @@ class AddonsInstaller(QtGui.QDialog): if not self.macros: self.listMacros.clear() self.macros = [] - self.macro_worker = FillMacroListWorker() + self.macro_worker = FillMacroListWorker(self.macro_repo_dir) self.macro_worker.add_macro_signal.connect(self.add_macro) - self.macro_worker.info_label.connect(self.set_information_label) + self.macro_worker.info_label_signal.connect(self.set_information_label) self.macro_worker.progressbar_show.connect(self.show_progress_bar) self.macro_worker.start() self.buttonCheck.setEnabled(False) @@ -291,10 +304,10 @@ class AddonsInstaller(QtGui.QDialog): update_macro_details(old_macro, macro) else: self.macros.append(macro) - if macro.installed: + if macro.is_installed(): self.listMacros.addItem(QtGui.QListWidgetItem(QtGui.QIcon.fromTheme('dialog-ok'), macro.name + str(' (Installed)'))) else: - self.listMacros.addItem(' ' + macro.name) + self.listMacros.addItem(macro.name) def showlink(self,link): """opens a link with the system browser""" @@ -303,6 +316,7 @@ class AddonsInstaller(QtGui.QDialog): def install(self,repos=None): if self.tabWidget.currentIndex() == 0: + # Tab "Workbenches". idx = None if repos: idx = [] @@ -321,6 +335,7 @@ class AddonsInstaller(QtGui.QDialog): self.install_worker.progressbar_show.connect(self.show_progress_bar) self.install_worker.start() elif self.tabWidget.currentIndex() == 1: + # Tab "Macros". macro_dir = get_macro_dir() if not os.path.isdir(macro_dir): os.makedirs(macro_dir) @@ -328,16 +343,14 @@ class AddonsInstaller(QtGui.QDialog): if not macro.code: self.labelDescription.setText(translate("AddonsInstaller", "Unable to install")) return - macroname = macro.filename - macrofilename = os.path.join(macro_dir, macroname) + macro_path = os.path.join(macro_dir, macro.filename) if sys.version_info.major < 3: - # in python2 the code is a bytes object + # In python2 the code is a bytes object. mode = 'wb' else: mode = 'w' - with open(macrofilename, mode) as macrofile: + with open(macro_path, mode) as macrofile: macrofile.write(macro.code) - macro.installed = True self.labelDescription.setText(translate("AddonsInstaller", "Macro successfully installed. The macro is now available from the Macros dialog.")) self.update_status() @@ -382,18 +395,16 @@ class AddonsInstaller(QtGui.QDialog): elif self.tabWidget.currentIndex() == 1: # Tab "Macros". macro = self.macros[self.listMacros.currentRow()] - if not macro.installed: + if not macro.is_installed(): # Macro not installed, nothing to do. return macro_path = os.path.join(get_macro_dir(), macro.filename) macro_path_with_macro_prefix = os.path.join(get_macro_dir(), 'Macro_' + macro.filename) if os.path.exists(macro_path): os.remove(macro_path) - macro.installed = False self.labelDescription.setText(translate("AddonsInstaller", "Macro successfully removed.")) elif os.path.exists(macro_path_with_macro_prefix): os.remove(macro_path_with_macro_prefix) - macro.installed = False self.labelDescription.setText(translate("AddonsInstaller", "Macro successfully removed.")) self.update_status() @@ -409,10 +420,11 @@ class AddonsInstaller(QtGui.QDialog): self.listWorkbenches.addItem(" "+str(wb[0])) wb[2] = 0 for macro in self.macros: - if macro.installed: + if macro.is_installed(): self.listMacros.addItem(QtGui.QListWidgetItem(QtGui.QIcon.fromTheme('dialog-ok'), macro.name + str(' (Installed)'))) else: - self.listMacros.addItem(' ' + macro.name) + self.listMacros.addItem(macro.name) + self.listMacros.sortItems() def mark(self,repo): for i in range(self.listWorkbenches.count()): @@ -559,37 +571,121 @@ class CheckWBWorker(QtCore.QThread): class Macro(object): - def __init__(self, name, installed): + def __init__(self, name): self.name = name - self.installed = installed self.on_wiki = False self.on_git = False self.desc = '' self.code = '' self.url = '' + self.version = '' + self.src_filename = '' + self.parsed = False def __eq__(self, other): return self.filename == other.filename @property def filename(self): + if self.on_git: + return os.path.basename(self.src_filename) return (self.name + '.FCMacro').replace(' ', '_') + def is_installed(self): + if self.on_git and not self.src_filename: + return False + return (os.path.exists(os.path.join(get_macro_dir(), self.filename)) + or os.path.exists(os.path.join(get_macro_dir(), 'Macro_' + self.filename))) + + def fill_details_from_file(self, filename): + with open(filename) as f: + number_of_required_fields = 3 # Fields __Comment__, __Web__, __Version__. + re_desc = re.compile(r"^__Comment__\s*=\s*(['\"])(.*)\1") + re_url = re.compile(r"^__Web__\s*=\s*(['\"])(.*)\1") + re_version = re.compile(r"^__Version__\s*=\s*(['\"])(.*)\1") + for l in f.readlines(): + match = re.match(re_desc, l) + if match: + self.desc = match.group(2) + number_of_required_fields -= 1 + match = re.match(re_url, l) + if match: + self.url = match.group(2) + number_of_required_fields -= 1 + match = re.match(re_version, l) + if match: + self.version = match.group(2) + number_of_required_fields -= 1 + if number_of_required_fields <= 0: + break + f.seek(0) + self.code = f.read() + self.parsed = True + + def fill_details_from_wiki(self, url): + try: + if ctx: + u = urllib2.urlopen(url, context=ctx) + else: + u = urllib2.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() + code = re.findall('
(.*?)<\/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
+ try:
+ code = code.decode('utf8')
+ code = HTMLParser().unescape(code)
+ code = code.encode('utf8')
+ code = code.replace('\xc2\xa0', ' ')
+ except:
+ FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "Unable to clean macro code: ") + mac + '\n')
+ desc = re.findall("(.*?)<\/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
+
class FillMacroListWorker(QtCore.QThread):
"""Populates the list of macros
"""
add_macro_signal = QtCore.Signal(Macro)
- info_label = QtCore.Signal(str)
+ info_label_signal = QtCore.Signal(str)
progressbar_show = QtCore.Signal(bool)
- def __init__(self):
+ def __init__(self, repo_dir):
QtCore.QThread.__init__(self)
+ self.repo_dir = repo_dir
+ self.macros = []
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())]
+ self.info_label_signal.emit(translate("AddonsInstaller", "List of macros successfully retrieved."))
+ self.progressbar_show.emit(False)
+ self.stop = True
def retrieve_macros_from_git(self):
"""Retrieve macros from FreeCAD-macros.git
@@ -597,7 +693,24 @@ class FillMacroListWorker(QtCore.QThread):
Emits a signal for each macro in
https://github.com/FreeCAD/FreeCAD-macros.git.
"""
- pass
+ try:
+ import git
+ except ImportError:
+ self.info_label_signal.emit("GitPython not installed! Cannot retrieve macros from git")
+ 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 retrieve_macros_from_wiki(self):
"""Retrieve macros from the wiki
@@ -605,7 +718,7 @@ class FillMacroListWorker(QtCore.QThread):
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.emit("Downloading list of macros...")
+ self.info_label_signal.emit("Downloading list of macros...")
self.progressbar_show.emit(True)
if ctx:
u = urllib2.urlopen("https://www.freecadweb.org/wiki/Macros_recipes",context=ctx)
@@ -617,20 +730,13 @@ class FillMacroListWorker(QtCore.QThread):
u.close()
macros = re.findall("title=\"(Macro.*?)\"",p)
macros = [mac for mac in macros if ('translated' not in mac)]
- macros.sort(key=str.lower)
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, False)
+ macro = Macro(macname)
macro.on_wiki = True
- if (os.path.exists(os.path.join(get_macro_dir(), macro.filename))
- or os.path.exists(os.path.join(get_macro_dir(), 'Macro_' + macro.filename))):
- macro.installed = True
- self.add_macro_signal.emit(macro)
- self.info_label.emit(translate("AddonsInstaller", "List of macros successfully retrieved."))
- self.progressbar_show.emit(False)
- self.stop = True
+ self.macros.append(macro)
class ShowWorker(QtCore.QThread):
@@ -720,53 +826,17 @@ class GetMacroDetailsWorker(QtCore.QThread):
def run(self):
self.progressbar_show.emit(True)
self.info_label.emit(translate("AddonsInstaller", "Retrieving description..."))
- if not self.macro.desc:
+ 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.info_label.emit("Retrieving info from " + str(url))
- if ctx:
- u = urllib2.urlopen(url,context=ctx)
- else:
- u = urllib2.urlopen(url)
- p = u.read()
- if sys.version_info.major >= 3 and isinstance(p, bytes):
- p = p.decode("utf-8")
- u.close()
- code = re.findall("(.*?)<\/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:
- self.info_label.emit(translate("AddonsInstaller", "Unable to fetch the code of this macro."))
- self.progressbar_show.emit(False)
- self.stop = True
- return
- desc = re.findall("(.*?)<\/td>",p.replace("\n"," "))
- if desc:
- desc = desc[0]
- else:
- self.info_label.emit(translate("AddonsInstaller", "Unable to retrieve a description for this macro."))
- desc = "No description available"
- # clean HTML escape codes
- try:
- from HTMLParser import HTMLParser
- except ImportError:
- from html.parser import HTMLParser
- try:
- code = code.decode("utf8")
- code = HTMLParser().unescape(code)
- code = code.encode("utf8")
- code = code.replace("\xc2\xa0", " ")
- except:
- FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "Unable to clean macro code: ")+mac+"\n")
- self.macro.desc = desc
- self.macro.url = url
- self.macro.code = code
- if self.macro.installed:
+ 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 = (''
+ translate("AddonsInstaller", "This addon is already installed.")
+ '
')