[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:
committed by
Yorik van Havre
parent
e4f01ed99a
commit
a09be6f0ee
@@ -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("&","&")
|
||||
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>')
|
||||
|
||||
Reference in New Issue
Block a user