[AddonManager] Add github support

- Add support for installing macros from https://github.com/FreeCAD/FreeCAD-addons.git.
- Macros which have the same name on git and on the wiki are not
  repeated.
This commit is contained in:
Gaël Écorchard
2018-05-08 00:06:30 +02:00
committed by Yorik van Havre
parent e4f01ed99a
commit a09be6f0ee

View File

@@ -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>(.*?)<\/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 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
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("&amp;","&")
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>(.*?)<\/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 class=\"ctEven left macro-description\">(.*?)<\/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 = ('<strong>'
+ translate("AddonsInstaller", "This addon is already installed.")
+ '</strong><br>')