AddonManager: update python source formatting

Update formatting in compliance with pep8 with the following exceptions:

 * truncate to 120 characters in line

 * prefer double quotes `"` to single quotes `'` in strings
This commit is contained in:
Matsievskiy S.V
2020-09-21 21:45:57 +03:00
committed by Yorik van Havre
parent 07db27d0dd
commit ba69518c36
4 changed files with 606 additions and 528 deletions

View File

@@ -25,8 +25,23 @@
from __future__ import print_function
__title__="FreeCAD Addon Manager Module"
__author__ = "Yorik van Havre","Jonathan Wiedemann","Kurt Kremitzki"
import os
import re
import shutil
import stat
import sys
import tempfile
from PySide import QtGui, QtCore
import AddonManager_rc
import FreeCADGui
from addonmanager_utilities import translate # this needs to be as is for pylupdate
from addonmanager_workers import *
import addonmanager_utilities as utils
__title__ = "FreeCAD Addon Manager Module"
__author__ = "Yorik van Havre", "Jonathan Wiedemann", "Kurt Kremitzki"
__url__ = "http://www.freecadweb.org"
"""
@@ -37,58 +52,54 @@ You need a working internet connection, and optionally the GitPython package
installed.
"""
import os
import re
import shutil
import stat
import sys
import tempfile
import addonmanager_utilities as utils
from addonmanager_utilities import translate # this needs to be as is for pylupdate
from addonmanager_workers import *
def QT_TRANSLATE_NOOP(ctx,txt):
return txt
## \defgroup ADDONMANAGER AddonManager
# \defgroup ADDONMANAGER AddonManager
# \ingroup ADDONMANAGER
# \brief The Addon Manager allows to install workbenches and macros made by users
# @{
def QT_TRANSLATE_NOOP(ctx, txt):
return txt
class CommandAddonManager:
"""The main Addon Manager class and FreeCAD command"""
def GetResources(self):
return {'Pixmap': 'AddonManager',
'MenuText': QT_TRANSLATE_NOOP("Std_AddonMgr", '&Addon manager'),
'ToolTip': QT_TRANSLATE_NOOP("Std_AddonMgr", 'Manage external workbenches and macros'),
'Group': 'Tools'}
return {"Pixmap": "AddonManager",
"MenuText": QT_TRANSLATE_NOOP("Std_AddonMgr", "&Addon manager"),
"ToolTip": QT_TRANSLATE_NOOP("Std_AddonMgr", "Manage external workbenches and macros"),
"Group": "Tools"}
def Activated(self):
# display first use dialog if needed
from PySide import QtGui
readWarning = FreeCAD.ParamGet('User parameter:Plugins/addonsRepository').GetBool('readWarning',False)
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)
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:
self.launch()
def launch(self):
"""Shows the Addon Manager UI"""
import FreeCADGui
from PySide import QtGui
# 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.repos = []
@@ -96,12 +107,13 @@ class CommandAddonManager:
self.macro_repo_dir = tempfile.mkdtemp()
self.doUpdate = []
self.addon_removed = False
for worker in ["update_worker","check_worker","show_worker","showmacro_worker","macro_worker","install_worker"]:
if hasattr(self,worker):
thread = getattr(self,worker)
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 thread.isFinished():
setattr(self,worker,None)
setattr(self, worker, None)
self.dialog.tabWidget.setCurrentIndex(0)
# these 2 settings to prevent loading an addon description on start (let the user click something first)
self.firsttime = True
@@ -109,24 +121,23 @@ class CommandAddonManager:
# restore window geometry and splitter state from stored state
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
w = pref.GetInt("WindowWidth",600)
h = pref.GetInt("WindowHeight",480)
self.dialog.resize(w,h)
sl = pref.GetInt("SplitterLeft",298)
sr = pref.GetInt("SplitterRight",274)
self.dialog.splitter.setSizes([sl,sr])
w = pref.GetInt("WindowWidth", 600)
h = pref.GetInt("WindowHeight", 480)
self.dialog.resize(w, h)
sl = pref.GetInt("SplitterLeft", 298)
sr = pref.GetInt("SplitterRight", 274)
self.dialog.splitter.setSizes([sl, sr])
# set nice icons to everything, by theme with fallback to FreeCAD icons
self.dialog.setWindowIcon(QtGui.QIcon(":/icons/AddonManager.svg"))
self.dialog.buttonExecute.setIcon(QtGui.QIcon.fromTheme("execute",QtGui.QIcon(":/icons/button_valid.svg")))
self.dialog.buttonUninstall.setIcon(QtGui.QIcon.fromTheme("cancel",QtGui.QIcon(":/icons/edit_Cancel.svg")))
self.dialog.buttonInstall.setIcon(QtGui.QIcon.fromTheme("download",QtGui.QIcon(":/icons/edit_OK.svg")))
self.dialog.buttonExecute.setIcon(QtGui.QIcon.fromTheme("execute", QtGui.QIcon(":/icons/button_valid.svg")))
self.dialog.buttonUninstall.setIcon(QtGui.QIcon.fromTheme("cancel", QtGui.QIcon(":/icons/edit_Cancel.svg")))
self.dialog.buttonInstall.setIcon(QtGui.QIcon.fromTheme("download", QtGui.QIcon(":/icons/edit_OK.svg")))
self.dialog.buttonUpdateAll.setIcon(QtGui.QIcon(":/icons/button_valid.svg"))
self.dialog.buttonConfigure.setIcon(QtGui.QIcon(":/icons/preferences-system.svg"))
self.dialog.buttonClose.setIcon(QtGui.QIcon.fromTheme("close",QtGui.QIcon(":/icons/process-stop.svg")))
self.dialog.tabWidget.setTabIcon(0,QtGui.QIcon.fromTheme("folder",QtGui.QIcon(":/icons/folder.svg")))
self.dialog.tabWidget.setTabIcon(1,QtGui.QIcon(":/icons/applications-python.svg"))
self.dialog.buttonClose.setIcon(QtGui.QIcon.fromTheme("close", QtGui.QIcon(":/icons/process-stop.svg")))
self.dialog.tabWidget.setTabIcon(0, QtGui.QIcon.fromTheme("folder", QtGui.QIcon(":/icons/folder.svg")))
self.dialog.tabWidget.setTabIcon(1, QtGui.QIcon(":/icons/applications-python.svg"))
# enable/disable stuff
self.dialog.buttonExecute.setEnabled(False)
@@ -161,52 +172,53 @@ class CommandAddonManager:
self.dialog.exec_()
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])
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)
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):
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.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.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)
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,utils.restartFreeCAD)
QtCore.QTimer.singleShot(1000, utils.restart_freecad)
try:
shutil.rmtree(self.macro_repo_dir,onerror=self.remove_readonly)
except:
shutil.rmtree(self.macro_repo_dir, onerror=self.remove_readonly)
except Exception:
pass
return True
def update(self):
"""updates the list of workbenches"""
self.dialog.listWorkbenches.clear()
@@ -220,17 +232,16 @@ class CommandAddonManager:
self.update_worker.start()
def check_updates(self):
"checks every installed addon for available updates"
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
if pref.GetBool("AutoCheck",False) and not self.doUpdate:
if hasattr(self,"check_worker"):
if pref.GetBool("AutoCheck", False) and not self.doUpdate:
if hasattr(self, "check_worker"):
thread = self.check_worker
if thread:
if not thread.isFinished():
return
self.dialog.buttonUpdateAll.setText(translate("AddonsInstaller","Checking for updates..."))
self.dialog.buttonUpdateAll.setText(translate("AddonsInstaller", "Checking for updates..."))
self.check_worker = CheckWBWorker(self.repos)
self.check_worker.mark.connect(self.mark)
self.check_worker.enable.connect(self.enable_updates)
@@ -238,44 +249,43 @@ class CommandAddonManager:
self.check_worker.start()
def apply_updates(self):
"""apply all available updates"""
if self.doUpdate:
self.install(self.doUpdate)
self.dialog.buttonUpdateAll.setEnabled(False)
def enable_updates(self,num):
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.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.setText(translate("AddonsInstaller", "No update available"))
self.dialog.buttonUpdateAll.setEnabled(False)
def add_addon_repo(self, addon_repo):
"""adds a workbench to the list"""
from PySide import QtGui
self.repos.append(addon_repo)
addonicon = self.get_icon(addon_repo[0])
if addon_repo[2] > 0:
item = QtGui.QListWidgetItem(addonicon,str(addon_repo[0]) + str(" ("+translate("AddonsInstaller","Installed")+")"))
item.setForeground(QtGui.QBrush(QtGui.QColor(0,182,41)))
item = QtGui.QListWidgetItem(addonicon,
str(addon_repo[0]) +
str(" (" +
translate("AddonsInstaller", "Installed") +
")"))
item.setForeground(QtGui.QBrush(QtGui.QColor(0, 182, 41)))
self.dialog.listWorkbenches.addItem(item)
else:
self.dialog.listWorkbenches.addItem(QtGui.QListWidgetItem(addonicon,str(addon_repo[0])))
def get_icon(self,repo):
self.dialog.listWorkbenches.addItem(QtGui.QListWidgetItem(addonicon, str(addon_repo[0])))
def get_icon(self, repo):
"""returns an icon for a repo"""
import AddonManager_rc
from PySide import QtGui
path = ":/icons/" + repo + "_workbench_icon.svg"
if QtCore.QFile.exists(path):
addonicon = QtGui.QIcon(path)
@@ -286,7 +296,6 @@ class CommandAddonManager:
return addonicon
def show_information(self, label):
"""shows text in the information pane"""
self.dialog.description.setText(label)
@@ -295,8 +304,7 @@ class CommandAddonManager:
else:
self.dialog.listMacros.setFocus()
def show(self,idx):
def show(self, idx):
"""loads information of a given workbench"""
# this function is triggered also when the list is populated, prevent that here
@@ -306,7 +314,7 @@ class CommandAddonManager:
return
if self.repos and idx >= 0:
if hasattr(self,"show_worker"):
if hasattr(self, "show_worker"):
# kill existing show worker (might still be busy loading images...)
if self.show_worker:
self.show_worker.exit()
@@ -323,8 +331,7 @@ class CommandAddonManager:
self.dialog.buttonInstall.setEnabled(True)
self.dialog.buttonUninstall.setEnabled(True)
def show_macro(self,idx):
def show_macro(self, idx):
"""loads information of a given macro"""
# this function is triggered when the list is populated, prevent that here
@@ -334,7 +341,7 @@ class CommandAddonManager:
return
if self.macros and idx >= 0:
if hasattr(self,"showmacro_worker"):
if hasattr(self, "showmacro_worker"):
if self.showmacro_worker:
if not self.showmacro_worker.isFinished():
self.showmacro_worker.exit()
@@ -351,8 +358,7 @@ class CommandAddonManager:
else:
self.dialog.buttonExecute.setEnabled(False)
def switchtab(self,idx):
def switchtab(self, idx):
"""does what needs to be done when switching tabs"""
if idx == 1:
@@ -367,13 +373,11 @@ class CommandAddonManager:
self.dialog.listMacros.setCurrentRow(0)
def update_repos(self, repos):
"""this function allows threads to update the main list of workbenches"""
self.repos = repos
def add_macro(self, macro):
"""adds a macro to the list"""
if macro.name:
@@ -382,10 +386,8 @@ class CommandAddonManager:
old_macro = self.macros[self.macros.index(macro)]
utils.update_macro_details(old_macro, macro)
else:
from PySide import QtGui
self.macros.append(macro)
import AddonManager_rc
path = ":/icons/" + macro.name.replace(" ","_") + "_macro_icon.svg"
path = ":/icons/" + macro.name.replace(" ", "_") + "_macro_icon.svg"
if QtCore.QFile.exists(path):
addonicon = QtGui.QIcon(path)
else:
@@ -393,14 +395,13 @@ class CommandAddonManager:
if addonicon.isNull():
addonicon = QtGui.QIcon(":/icons/document-python.svg")
if macro.is_installed():
item = QtGui.QListWidgetItem(addonicon, macro.name + str(' (Installed)'))
item.setForeground(QtGui.QBrush(QtGui.QColor(0,182,41)))
item = QtGui.QListWidgetItem(addonicon, macro.name + str(" (Installed)"))
item.setForeground(QtGui.QBrush(QtGui.QColor(0, 182, 41)))
self.dialog.listMacros.addItem(item)
else:
self.dialog.listMacros.addItem(QtGui.QListWidgetItem(addonicon,macro.name))
def install(self,repos=None):
self.dialog.listMacros.addItem(QtGui.QListWidgetItem(addonicon, macro.name))
def install(self, repos=None):
"""installs a workbench or macro"""
if self.dialog.tabWidget.currentIndex() == 0:
@@ -409,13 +410,13 @@ class CommandAddonManager:
if repos:
idx = []
for repo in repos:
for i,r in enumerate(self.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 idx is not None:
if hasattr(self, "install_worker") and self.install_worker:
if self.install_worker.isRunning():
return
self.install_worker = InstallWorker(self.repos, idx)
@@ -428,15 +429,16 @@ class CommandAddonManager:
# 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."))
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 show_progress_bar(self, state):
"""shows or hides the progress bar"""
if state == True:
if state:
self.dialog.tabWidget.setEnabled(False)
self.dialog.buttonInstall.setEnabled(False)
self.dialog.buttonUninstall.setEnabled(False)
@@ -453,10 +455,8 @@ class CommandAddonManager:
self.dialog.listMacros.setFocus()
def executemacro(self):
"""executes a selected macro"""
import FreeCADGui
if self.dialog.tabWidget.currentIndex() == 1:
# Tab "Macros".
macro = self.macros[self.dialog.listMacros.currentRow()]
@@ -465,7 +465,7 @@ class CommandAddonManager:
return
macro_path = os.path.join(FreeCAD.getUserMacroDir(True), macro.filename)
if os.path.exists(macro_path):
macro_path = macro_path.replace("\\","/")
macro_path = macro_path.replace("\\", "/")
FreeCADGui.open(str(macro_path))
self.dialog.hide()
@@ -481,7 +481,6 @@ class CommandAddonManager:
func(path)
def remove(self):
"""uninstalls a macro or workbench"""
if self.dialog.tabWidget.currentIndex() == 0:
@@ -492,7 +491,8 @@ class CommandAddonManager:
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"))
self.dialog.description.setText(translate("AddonsInstaller",
"Addon successfully removed. Please restart FreeCAD"))
else:
self.dialog.description.setText(translate("AddonsInstaller", "Unable to remove this addon"))
@@ -500,60 +500,59 @@ class CommandAddonManager:
# Tab "Macros".
macro = self.macros[self.dialog.listMacros.currentRow()]
if utils.remove_macro(macro):
self.dialog.description.setText(translate('AddonsInstaller', 'Macro successfully removed.'))
self.dialog.description.setText(translate("AddonsInstaller", "Macro successfully removed."))
else:
self.dialog.description.setText(translate('AddonsInstaller', 'Macro could not be removed.'))
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 mark_recompute(self,addon):
self.addon_removed = True # A value to trigger the restart message on dialog close
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")+")"):
if txt.endswith(" ("+translate("AddonsInstaller", "Installed")+")"):
txt = txt[:-12]
elif txt.endswith(" ("+translate("AddonsInstaller","Update available")+")"):
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).setText(txt + " (" +
translate("AddonsInstaller",
"Restart required") +
")")
self.dialog.listWorkbenches.item(i).setIcon(QtGui.QIcon(":/icons/edit-undo.svg"))
def update_status(self,soft=False):
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"
from PySide import QtGui
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")+")"):
if txt.endswith(" ("+translate("AddonsInstaller", "Installed")+")"):
txt = txt[:-12]
ext = " ("+translate("AddonsInstaller","Installed")+")"
elif txt.endswith(" ("+translate("AddonsInstaller","Update available")+")"):
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")+")"):
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)):
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")+")"):
if txt.endswith(" ("+translate("AddonsInstaller", "Installed")+")"):
txt = txt[:-12]
elif txt.endswith(" ("+translate("AddonsInstaller","Update available")+")"):
elif txt.endswith(" ("+translate("AddonsInstaller", "Update available")+")"):
txt = txt[:-19]
if os.path.exists(os.path.join(moddir,txt)):
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)
@@ -562,69 +561,71 @@ class CommandAddonManager:
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")+")"))
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])))
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 mark(self,repo):
self.dialog.listMacros.addItem(
QtGui.QListWidgetItem(QtGui.QIcon(":/icons/document-python.svg"), macro.name))
def mark(self, repo):
"""mark a workbench as updatable"""
from PySide import QtGui
for i in range(self.dialog.listWorkbenches.count()):
w = self.dialog.listWorkbenches.item(i)
if (w.text() == str(repo)) or w.text().startswith(str(repo)+" "):
w.setText(str(repo) + str(" ("+translate("AddonsInstaller","Update available")+")"))
w.setForeground(QtGui.QBrush(QtGui.QColor(182,90,0)))
if not repo in self.doUpdate:
w.setText(str(repo) + str(" ("+translate("AddonsInstaller", "Update available")+")"))
w.setForeground(QtGui.QBrush(QtGui.QColor(182, 90, 0)))
if repo not in self.doUpdate:
self.doUpdate.append(repo)
def show_config(self):
"""shows the configuration dialog"""
import FreeCADGui
from PySide import QtGui
self.config = FreeCADGui.PySideUic.loadUi(os.path.join(os.path.dirname(__file__),"AddonManagerOptions.ui"))
self.config = FreeCADGui.PySideUic.loadUi(os.path.join(os.path.dirname(__file__), "AddonManagerOptions.ui"))
# restore stored values
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
self.config.checkUpdates.setChecked(pref.GetBool("AutoCheck",False))
self.config.customRepositories.setPlainText(pref.GetString("CustomRepositories",""))
self.config.radioButtonNoProxy.setChecked(pref.GetBool("NoProxyCheck",True))
self.config.radioButtonSystemProxy.setChecked(pref.GetBool("SystemProxyCheck",False))
self.config.radioButtonUserProxy.setChecked(pref.GetBool("UserProxyCheck",False))
self.config.userProxy.setPlainText(pref.GetString("ProxyUrl",""))
self.config.checkUpdates.setChecked(pref.GetBool("AutoCheck", False))
self.config.customRepositories.setPlainText(pref.GetString("CustomRepositories", ""))
self.config.radioButtonNoProxy.setChecked(pref.GetBool("NoProxyCheck", True))
self.config.radioButtonSystemProxy.setChecked(pref.GetBool("SystemProxyCheck", False))
self.config.radioButtonUserProxy.setChecked(pref.GetBool("UserProxyCheck", False))
self.config.userProxy.setPlainText(pref.GetString("ProxyUrl", ""))
# center the dialog over the Addon Manager
self.config.move(self.dialog.frameGeometry().topLeft() + self.dialog.rect().center() - self.config.rect().center())
self.config.move(self.dialog.frameGeometry().topLeft() +
self.dialog.rect().center() -
self.config.rect().center())
ret = self.config.exec_()
if ret:
# OK button has been pressed
pref.SetBool("AutoCheck",self.config.checkUpdates.isChecked())
pref.SetString("CustomRepositories",self.config.customRepositories.toPlainText())
pref.SetBool("NoProxyCheck",self.config.radioButtonNoProxy.isChecked())
pref.SetBool("SystemProxyCheck",self.config.radioButtonSystemProxy.isChecked())
pref.SetBool("UserProxyCheck",self.config.radioButtonUserProxy.isChecked())
pref.SetString("ProxyUrl",self.config.userProxy.toPlainText())
pref.SetBool("AutoCheck", self.config.checkUpdates.isChecked())
pref.SetString("CustomRepositories", self.config.customRepositories.toPlainText())
pref.SetBool("NoProxyCheck", self.config.radioButtonNoProxy.isChecked())
pref.SetBool("SystemProxyCheck", self.config.radioButtonSystemProxy.isChecked())
pref.SetBool("UserProxyCheck", self.config.radioButtonUserProxy.isChecked())
pref.SetString("ProxyUrl", self.config.userProxy.toPlainText())
def check_updates(addon_name,callback):
def check_updates(addon_name, callback):
"""Checks for updates for a given addon"""
oname = "update_checker_"+addon_name
setattr(FreeCAD,oname,CheckSingleWorker(addon_name))
getattr(FreeCAD,oname).updateAvailable.connect(callback)
getattr(FreeCAD,oname).start()
setattr(FreeCAD, oname, CheckSingleWorker(addon_name))
getattr(FreeCAD, oname).updateAvailable.connect(callback)
getattr(FreeCAD, oname).start()
## @}
# @}

View File

@@ -21,18 +21,50 @@
#* *
#***************************************************************************
## @package AddonManager_macro
# \ingroup ADDONMANAGER
# \brief Unified handler for FreeCAD macros that can be obtained from different sources
import os
import re
import sys
from PySide import QtCore, QtGui
import FreeCAD
import FreeCADGui
from addonmanager_utilities import translate
from addonmanager_utilities import urlopen
try:
from HTMLParser import HTMLParser
unescape = HTMLParser().unescape
except ImportError:
from html import unescape
try:
import StringIO as io
_stringio = io.StringIO
except ImportError: # StringIO is not available with python3
import io
_stringio = io.BytesIO
have_git = False
try:
import git
have_git = True
except ImportError:
pass
have_zip = False
try:
import zipfile
have_zip = True
except ImportError:
pass
# @package AddonManager_macro
# \ingroup ADDONMANAGER
# \brief Unified handler for FreeCAD macros that can be obtained from different sources
# @{
class Macro(object):
"""This class provides a unified way to handle macros coming from different sources"""
@@ -41,11 +73,11 @@ class Macro(object):
self.name = name
self.on_wiki = False
self.on_git = False
self.desc = ''
self.code = ''
self.url = ''
self.version = ''
self.src_filename = ''
self.desc = ""
self.code = ""
self.url = ""
self.version = ""
self.src_filename = ""
self.other_files = []
self.parsed = False
@@ -56,13 +88,13 @@ class Macro(object):
def filename(self):
if self.on_git:
return os.path.basename(self.src_filename)
return (self.name + '.FCMacro').replace(' ', '_')
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(FreeCAD.getUserMacroDir(True), self.filename))
or os.path.exists(os.path.join(FreeCAD.getUserMacroDir(True), 'Macro_' + 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:
@@ -73,22 +105,22 @@ class Macro(object):
re_url = re.compile(r"^__Web__\s*=\s*(['\"])(.*)\1")
re_version = re.compile(r"^__Version__\s*=\s*(['\"])(.*)\1")
re_files = re.compile(r"^__Files__\s*=\s*(['\"])(.*)\1")
for l in f.readlines():
match = re.match(re_desc, l)
for line in f.readlines():
match = re.match(re_desc, line)
if match:
self.desc = match.group(2)
number_of_required_fields -= 1
match = re.match(re_url, l)
match = re.match(re_url, line)
if match:
self.url = match.group(2)
number_of_required_fields -= 1
match = re.match(re_version, l)
match = re.match(re_version, line)
if match:
self.version = match.group(2)
number_of_required_fields -= 1
match = re.match(re_files, l)
match = re.match(re_files, line)
if match:
self.other_files = [of.strip() for of in match.group(2).split(',')]
self.other_files = [of.strip() for of in match.group(2).split(",")]
number_of_required_fields -= 1
if number_of_required_fields <= 0:
break
@@ -98,75 +130,65 @@ class Macro(object):
def fill_details_from_wiki(self, url):
code = ""
try:
u = urlopen(url)
except:
print("AddonManager: Debug: unable to open URL",url)
return
if u is None :
print("AddonManager: Debug: connection is lost (proxy setting changed?)",url)
u = urlopen(url)
if u is None:
print("AddonManager: Debug: connection is lost (proxy setting changed?)", url)
return
p = u.read()
if sys.version_info.major >= 3 and isinstance(p, bytes):
p = p.decode('utf-8')
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)
rawcodeurl = re.findall("rawcodeurl.*?href=\"(http.*?)\">", p)
if rawcodeurl:
rawcodeurl = rawcodeurl[0]
try:
u2 = urlopen(rawcodeurl)
except:
print("AddonManager: Debug: unable to open URL",rawcodeurl)
u2 = urlopen(rawcodeurl)
if u2 is None:
print("AddonManager: Debug: unable to open URL", rawcodeurl)
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))
# expected = int(u2.headers["content-length"])
while True:
# 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')
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--'))
code = re.findall(r"<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')
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')
code = code.decode("utf8")
code = unescape(code)
code = code.replace(b"\xc2\xa0".decode("utf-8"), " ")
if sys.version_info.major < 3:
code = code.encode('utf8')
desc = re.findall("<td class=\"ctEven left macro-description\">(.*?)<\/td>", p.replace('\n', ' '))
code = code.encode("utf8")
desc = re.findall(r"<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."))
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
# @}

View File

@@ -21,16 +21,26 @@
#* *
#***************************************************************************
import os
import sys
import codecs
import FreeCAD
import shutil
import os
import re
import shutil
import sys
import ctypes
## @package AddonManager_utilities
# \ingroup ADDONMANAGER
# \brief Utilities to work across different platforms, providers and python versions
if sys.version_info.major < 3:
import urllib2
from urllib2 import URLError
from urlparse import urlparse
else:
import urllib.request as urllib2
from urllib.error import URLError
from urllib.parse import urlparse
from PySide import QtGui, QtCore
import FreeCAD
import FreeCADGui
# check for SSL support
@@ -44,14 +54,17 @@ else:
ssl_ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
except AttributeError:
pass
# @package AddonManager_utilities
# \ingroup ADDONMANAGER
# \brief Utilities to work across different platforms, providers and python versions
# @{
def translate(context, text, disambig=None):
"Main translation function"
from PySide import QtGui
try:
_encoding = QtGui.QApplication.UnicodeUTF8
except AttributeError:
@@ -61,18 +74,16 @@ def translate(context, text, disambig=None):
def symlink(source, link_name):
"creates a symlink of a file, if possible"
if os.path.exists(link_name) or os.path.lexists(link_name):
#print("macro already exists")
# print("macro already exists")
pass
else:
os_symlink = getattr(os, "symlink", None)
if callable(os_symlink):
os_symlink(source, link_name)
else:
import ctypes
csl = ctypes.windll.kernel32.CreateSymbolicLinkW
csl.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32)
csl.restype = ctypes.c_ubyte
@@ -85,59 +96,48 @@ def symlink(source, link_name):
def urlopen(url):
"""Opens an url with urllib2"""
timeout = 5
if sys.version_info.major < 3:
import urllib2
else:
import urllib.request as urllib2
# Proxy an ssl configuration
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
if pref.GetBool("NoProxyCheck",True):
proxies = {}
if pref.GetBool("NoProxyCheck", True):
proxies = {}
else:
if pref.GetBool("SystemProxyCheck",False):
proxy = urllib2.getproxies()
proxies = {"http": proxy.get('http'),"https": proxy.get('http')}
elif pref.GetBool("UserProxyCheck",False):
proxy = pref.GetString("ProxyUrl","")
proxies = {"http": proxy, "https": proxy}
if pref.GetBool("SystemProxyCheck", False):
proxy = urllib2.getproxies()
proxies = {"http": proxy.get('http'), "https": proxy.get('http')}
elif pref.GetBool("UserProxyCheck", False):
proxy = pref.GetString("ProxyUrl", "")
proxies = {"http": proxy, "https": proxy}
if ssl_ctx:
handler = urllib2.HTTPSHandler(context=ssl_ctx)
else:
handler = {}
proxy_support = urllib2.ProxyHandler(proxies)
proxy_support = urllib2.ProxyHandler(proxies)
opener = urllib2.build_opener(proxy_support, handler)
urllib2.install_opener(opener)
urllib2.install_opener(opener)
# Url opening
req = urllib2.Request(url,
headers={'User-Agent' : "Magic Browser"})
headers={'User-Agent': "Magic Browser"})
try:
u = urllib2.urlopen(req, timeout=timeout)
except:
except Exception:
return None
else:
return u
def getserver(url):
def getserver(url):
"""returns the server part of an url"""
if sys.version_info.major < 3:
from urlparse import urlparse
else:
from urllib.parse import urlparse
return '{uri.scheme}://{uri.netloc}/'.format(uri=urlparse(url))
def update_macro_details(old_macro, new_macro):
"""Update a macro with information from another one
Update a macro with information from another one, supposedly the same but
@@ -157,7 +157,6 @@ def update_macro_details(old_macro, new_macro):
def install_macro(macro, macro_repo_dir):
"""Install a macro and all its related files
Returns True if the macro was installed correctly.
@@ -201,7 +200,6 @@ def install_macro(macro, macro_repo_dir):
def remove_macro(macro):
"""Remove a macro and all its related files
Returns True if the macro was removed correctly.
@@ -231,7 +229,6 @@ def remove_macro(macro):
def remove_directory_if_empty(dir):
"""Remove the directory if it is empty
Directory FreeCAD.getUserMacroDir(True) will not be removed even if empty.
@@ -243,21 +240,17 @@ def remove_directory_if_empty(dir):
os.rmdir(dir)
def restartFreeCAD():
def restart_freecad():
"Shuts down and restarts FreeCAD"
import FreeCADGui
from PySide import QtGui,QtCore
args = QtGui.QApplication.arguments()[1:]
if FreeCADGui.getMainWindow().close():
QtCore.QProcess.startDetached(QtGui.QApplication.applicationFilePath(),args)
QtCore.QProcess.startDetached(QtGui.QApplication.applicationFilePath(), args)
def getZipUrl(baseurl):
def get_zip_url(baseurl):
"Returns the location of a zip file from a repo, if available"
url = getserver(baseurl).strip("/")
if url.endswith("github.com"):
return baseurl+"/archive/master.zip"
@@ -266,59 +259,54 @@ def getZipUrl(baseurl):
reponame = baseurl.strip("/").split("/")[-1]
return baseurl+"/-/archive/master/"+reponame+"-master.zip"
else:
print("Debug: addonmanager_utilities.getZipUrl: Unknown git host:",url)
print("Debug: addonmanager_utilities.get_zip_url: Unknown git host:", url)
return None
def getReadmeUrl(url):
def get_readme_url(url):
"Returns the location of a readme file"
if "github" in url or "framagit" in url or "gitlab" in url:
return url+"/raw/master/README.md"
else:
print("Debug: addonmanager_utilities.getReadmeUrl: Unknown git host:",url)
print("Debug: addonmanager_utilities.get_readme_url: Unknown git host:", url)
return None
def getDescRegex(url):
def get_desc_regex(url):
"""Returns a regex string that extracts a WB description to be displayed in the description
panel of the Addon manager, if the README could not be found"""
if "github" in url:
return "<meta property=\"og:description\" content=\"(.*?)\""
return r'<meta property="og:description" content="(.*?)"'
elif "framagit" in url or "gitlab" in url:
return "<meta.*?content=\"(.*?)\".*?og\:description.*?>"
print("Debug: addonmanager_utilities.getDescRegex: Unknown git host:",url)
return r'<meta.*?content="(.*?)".*?og:description.*?>'
print("Debug: addonmanager_utilities.get_desc_regex: Unknown git host:", url)
return None
def getReadmeHTMLUrl(url):
"Returns the location of a html file containing readme"
def get_readme_html_url(url):
"""Returns the location of a html file containing readme"""
if ("github" in url):
return url+"/blob/master/README.md"
if "github" in url:
return url + "/blob/master/README.md"
else:
print("Debug: addonmanager_utilities.getReadmeUrl: Unknown git host:",url)
print("Debug: addonmanager_utilities.get_readme_html_url: Unknown git host:", url)
return None
def getReadmeRegex(url):
def get_readme_regex(url):
"""Return a regex string that extracts the contents to be displayed in the description
panel of the Addon manager, from raw HTML data (the readme's html rendering usually)"""
if ("github" in url):
return "<article.*?>(.*?)</article>"
else:
print("Debug: addonmanager_utilities.getReadmeRegex: Unknown git host:",url)
print("Debug: addonmanager_utilities.get_readme_regex: Unknown git host:", url)
return None
def fixRelativeLinks(text, base_url):
def fix_relative_links(text, base_url):
"""Replace markdown image relative links with
absolute ones using the base URL"""
@@ -333,3 +321,5 @@ def fixRelativeLinks(text, base_url):
print("Debug: replaced " + link + " with " + newlink)
new_text = new_text + '\n' + line
return new_text
# @}

View File

@@ -27,17 +27,40 @@ import shutil
import stat
import sys
import tempfile
import FreeCAD
from PySide import QtCore
import FreeCAD
import addonmanager_utilities as utils
from addonmanager_utilities import translate # this needs to be as is for pylupdate
from addonmanager_utilities import translate # this needs to be as is for pylupdate
from addonmanager_macro import Macro
## @package AddonManager_workers
have_git = False
try:
import git
have_git = True
except ImportError:
pass
have_zip = False
try:
import zipfile
have_zip = True
except ImportError:
pass
have_markdown = False
try:
import markdown
have_markdown = True
except ImportError:
pass
# @package AddonManager_workers
# \ingroup ADDONMANAGER
# \brief Multithread workers for the addon manager
# @{
# Blacklisted addons
macros_blacklist = []
@@ -48,9 +71,9 @@ obsolete = []
# These addons will print an additional message informing the user Python2 only
py2only = []
NOGIT = False # for debugging purposes, set this to True to always use http downloads
NOGIT = False # for debugging purposes, set this to True to always use http downloads
NOMARKDOWN = False # for debugging purposes, set this to True to disable Markdown lib
NOMARKDOWN = False # for debugging purposes, set this to True to disable Markdown lib
"""Multithread workers for the Addon Manager"""
@@ -68,7 +91,6 @@ class UpdateWorker(QtCore.QThread):
QtCore.QThread.__init__(self)
def run(self):
"populates the list of addons"
self.progressbar_show.emit(True)
@@ -82,17 +104,17 @@ class UpdateWorker(QtCore.QThread):
p = p.decode("utf-8")
u.close()
hit = re.findall(r'"obsolete"[^\{]*?{[^\{]*?"Mod":\[(?P<obsolete>[^\[\]]+?)\]}',
p.replace('\n', '').replace(' ', ''))
p.replace("\n", "").replace(" ", ""))
if hit:
obsolete = hit[0].replace('"', '').split(',')
obsolete = hit[0].replace('"', "").split(",")
hit = re.findall(r'"blacklisted"[^\{]*?{[^\{]*?"Macro":\[(?P<blacklisted>[^\[\]]+?)\]}',
p.replace('\n', '').replace(' ', ''))
p.replace("\n", "").replace(" ", ""))
if hit:
macros_blacklist = hit[0].replace('"', '').split(',')
macros_blacklist = hit[0].replace('"', "").split(",")
hit = re.findall(r'"py2only"[^\{]*?{[^\{]*?"Mod":\[(?P<py2only>[^\[\]]+?)\]}',
p.replace('\n', '').replace(' ', ''))
p.replace("\n", "").replace(" ", ""))
if hit:
py2only = hit[0].replace('"', '').split(',')
py2only = hit[0].replace('"', "").split(",")
else:
print("Debug: addon_flags.json not found")
@@ -106,7 +128,7 @@ class UpdateWorker(QtCore.QThread):
if sys.version_info.major >= 3 and isinstance(p, bytes):
p = p.decode("utf-8")
u.close()
p = re.findall((r"(?m)\[submodule\s*\"(?P<name>.*)\"\]\s*"
p = re.findall((r'(?m)\[submodule\s*"(?P<name>.*)"\]\s*'
r"path\s*=\s*(?P<path>.+)\s*"
r"url\s*=\s*(?P<url>https?://.*)"), p)
basedir = FreeCAD.getUserAppDataDir()
@@ -124,7 +146,8 @@ class UpdateWorker(QtCore.QThread):
state = 0
repos.append([name, url, state])
# querying custom addons
customaddons = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons").GetString("CustomRepositories","").split("\n")
customaddons = (FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
.GetString("CustomRepositories", "").split("\n"))
for url in customaddons:
if url:
name = url.split("/")[-1]
@@ -135,7 +158,7 @@ class UpdateWorker(QtCore.QThread):
state = 0
else:
state = 1
repos.append([name,url,state])
repos.append([name, url, state])
if not repos:
self.info_label.emit(translate("AddonsInstaller", "Unable to download addon list."))
else:
@@ -170,7 +193,7 @@ class InfoWorker(QtCore.QThread):
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)
desc = re.findall('<meta property="og:description" content="(.*?)"', p)
if desc:
desc = desc[0]
else:
@@ -188,19 +211,14 @@ class CheckWBWorker(QtCore.QThread):
mark = QtCore.Signal(str)
addon_repos = QtCore.Signal(object)
def __init__(self,repos):
def __init__(self, repos):
QtCore.QThread.__init__(self)
self.repos = repos
def run(self):
if NOGIT:
self.stop = True
return
try:
import git
except:
if NOGIT or not have_git:
self.stop = True
return
basedir = FreeCAD.getUserAppDataDir()
@@ -208,36 +226,40 @@ class CheckWBWorker(QtCore.QThread):
upds = []
gitpython_warning = False
for repo in self.repos:
if repo[2] == 1: #installed
#print("Checking for updates for",repo[0])
if repo[2] == 1: # installed
# print("Checking for updates for", repo[0])
clonedir = moddir + os.sep + repo[0]
if os.path.exists(clonedir):
self.repos[self.repos.index(repo)][2] = 2 # mark as already installed AND already checked for updates
if not os.path.exists(clonedir + os.sep + '.git'):
# mark as already installed AND already checked for updates
self.repos[self.repos.index(repo)][2] = 2
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)
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)
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)
cw.set("core", "bare", False)
del cw
repo = git.Repo(clonedir)
repo.head.reset('--hard')
repo.head.reset("--hard")
gitrepo = git.Git(clonedir)
try:
gitrepo.fetch()
except:
print("AddonManager: Unable to fetch git updates for repo",repo[0])
except Exception:
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] = 3 # mark as already installed AND already checked for updates AND update available
# mark as already installed AND already checked for updates AND update available
self.repos[self.repos.index(repo)][2] = 3
self.addon_repos.emit(self.repos)
self.enable.emit(len(upds))
self.stop = True
@@ -257,49 +279,47 @@ class FillMacroListWorker(QtCore.QThread):
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())]
if self.macros:
self.info_label_signal.emit(translate('AddonsInstaller', 'List of macros successfully retrieved.'))
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
Emits a signal for each macro in
https://github.com/FreeCAD/FreeCAD-macros.git
"""
try:
import git
except ImportError:
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")
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...')
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")
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'):
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
Read the wiki and emit a signal for each found macro.
@@ -310,18 +330,20 @@ class FillMacroListWorker(QtCore.QThread):
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")
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)]
macros = [mac for mac in macros if ("translated" not in mac)]
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()):
macname = macname.replace("&amp;", "&")
if (macname not in macros_blacklist) and ("recipes" not in macname.lower()):
macro = Macro(macname)
macro.on_wiki = True
self.macros.append(macro)
@@ -336,7 +358,7 @@ class ShowWorker(QtCore.QThread):
def __init__(self, repos, idx):
# repos is a list of [name,url,installbit,descr]
# repos is a list of [name, url, installbit, descr]
# name : Addon name
# url : Addon repository location
# installbit: 0 = Addon is not installed
@@ -358,51 +380,54 @@ class ShowWorker(QtCore.QThread):
else:
u = None
url = self.repos[self.idx][1]
self.info_label.emit(translate("AddonsInstaller", "Retrieving info from") + ' ' + str(url))
self.info_label.emit(translate("AddonsInstaller", "Retrieving info from") + " " + str(url))
desc = ""
regex = utils.getReadmeRegex(url)
regex = utils.get_readme_regex(url)
if regex:
# extract readme from html via regex
readmeurl = utils.getReadmeHTMLUrl(url)
readmeurl = utils.get_readme_html_url(url)
if not readmeurl:
print("Debug: README not found for",url)
print("Debug: README not found for", url)
u = utils.urlopen(readmeurl)
if not u:
print("Debug: README not found at",readmeurl)
print("Debug: README not found at", readmeurl)
u = utils.urlopen(readmeurl)
if u:
p = u.read()
if sys.version_info.major >= 3 and isinstance(p, bytes):
p = p.decode("utf-8")
u.close()
readme = re.findall(regex,p,flags=re.MULTILINE|re.DOTALL)
readme = re.findall(regex, p, flags=re.MULTILINE | re.DOTALL)
if readme:
desc = readme[0]
else:
print("Debug: README not found at",readmeurl)
print("Debug: README not found at", readmeurl)
else:
# convert raw markdown using lib
readmeurl = utils.getReadmeUrl(url)
readmeurl = utils.get_readme_url(url)
if not readmeurl:
print("Debug: README not found for",url)
print("Debug: README not found for", url)
u = utils.urlopen(readmeurl)
if u:
p = u.read()
if sys.version_info.major >= 3 and isinstance(p, bytes):
p = p.decode("utf-8")
u.close()
desc = utils.fixRelativeLinks(p,readmeurl.rsplit("/README.md")[0])
try:
if NOMARKDOWN:
raise ImportError
import markdown # try to use system Markdown lib
desc = markdown.markdown(desc,extensions=['md_in_html'])
except ImportError:
message = " <div style=\"width: 100%; text-align:center; background: #91bbe0;\"><strong style=\"color: #FFFFFF;\">"+translate("AddonsInstaller","Raw markdown displayed")+"</strong><br/><br/>"
message += translate("AddonsInstaller","Python Markdown library is missing.")+"<br/></div><hr/><pre>" + desc + "</pre>"
desc = utils.fix_relative_links(p, readmeurl.rsplit("/README.md")[0])
if NOMARKDOWN or not have_markdown:
desc = markdown.markdown(desc, extensions=["md_in_html"])
else:
message = """
<div style="width: 100%; text-align:center;background: #91bbe0;">
<strong style="color: #FFFFFF;">
"""
message += translate("AddonsInstaller", "Raw markdown displayed")
message += "</strong><br/><br/>"
message += translate("AddonsInstaller", "Python Markdown library is missing.")
message += "<br/></div><hr/><pre>" + desc + "</pre>"
desc = message
else:
print("Debug: README not found at",readmeurl)
print("Debug: README not found at", readmeurl)
if desc == "":
# fall back to the description text
u = utils.urlopen(url)
@@ -414,9 +439,9 @@ class ShowWorker(QtCore.QThread):
if sys.version_info.major >= 3 and isinstance(p, bytes):
p = p.decode("utf-8")
u.close()
descregex = utils.getDescRegex(url)
descregex = utils.get_desc_regex(url)
if descregex:
desc = re.findall(descregex,p)
desc = re.findall(descregex, p)
if desc:
desc = desc[0]
if not desc:
@@ -427,92 +452,133 @@ class ShowWorker(QtCore.QThread):
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 not NOGIT and have_git:
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 there is an update pending, lets user know via the UI
if upd:
message = "<div style=\"width: 100%;text-align: center;background: #75AFFD;\"><br/><strong style=\"background: #397FF7;color: #FFFFFF;\">" + translate("AddonsInstaller", "An update is available for this addon.")
message += "</strong><br/></div><hr/>" + desc + '<br/><br/>Addon repository: <a href="' + self.repos[self.idx][1] + '">' + self.repos[self.idx][1] + '</a>'
self.repos[self.idx][2] = 3 # mark as already installed AND already checked for updates AND update is available
message = """
<div style="width: 100%;text-align: center;background: #75AFFD;">
<br/>
<strong style="background: #397FF7;color: #FFFFFF;">
"""
message += translate("AddonsInstaller", "An update is available for this addon.")
message += "</strong><br/></div><hr/>" + desc + '<br/><br/>Addon repository: <a href="'
message += self.repos[self.idx][1] + '">' + self.repos[self.idx][1] + "</a>"
# mark as already installed AND already checked for updates AND update is available
self.repos[self.idx][2] = 3
# If there isn't, indicate that this addon is already installed
else:
message = "<div style=\"width: 100%;text-align: center;background: #C1FEB2;\"><br/><strong style=\"background: #00B629;color: #FFFFFF;\">" + translate("AddonsInstaller", "This addon is already installed.") + "</strong><br/></div><hr/>"
message += desc + '<br/><br/>Addon repository: <a href="' + self.repos[self.idx][1] + '">' + self.repos[self.idx][1] + '</a>'
self.repos[self.idx][2] = 2 # mark as already installed AND already checked for updates
message = """
<div style="width: 100%;text-align: center;background: #C1FEB2;">
<br/>
<strong style="background: #00B629;color: #FFFFFF;">
"""
message += translate("AddonsInstaller", "This addon is already installed.")
message += "</strong><br/></div><hr/>" + desc
message += '<br/><br/>Addon repository: <a href="'
message += self.repos[self.idx][1] + '">' + self.repos[self.idx][1] + "</a>"
self.repos[self.idx][2] = 2 # mark as already installed AND already checked for updates
# Let the user know the install path for this addon
message += '<br/>' + translate("AddonInstaller","Installed location")+": "+ FreeCAD.getUserAppDataDir() + os.sep + "Mod" + os.sep + self.repos[self.idx][0]
message += "<br/>" + translate("AddonInstaller", "Installed location") + ": "
message += FreeCAD.getUserAppDataDir() + os.sep + "Mod" + os.sep + self.repos[self.idx][0]
self.addon_repos.emit(self.repos)
elif self.repos[self.idx][2] == 2:
message = "<div style=\"width: 100%;text-align: center;background: #C1FEB2;\"><br/><strong style=\"background: #00B629;color: #FFFFFF;\">" + translate("AddonsInstaller", "This addon is already installed.") + "</strong><br></div><hr/>"
message += desc + '<br/><br/>Addon repository: <a href="' + self.repos[self.idx][1] + '">' + self.repos[self.idx][1] + '</a>'
message += '<br/>' + translate("AddonInstaller","Installed location")+": "+ FreeCAD.getUserAppDataDir() + os.sep + "Mod" + os.sep + self.repos[self.idx][0]
message = """
<div style="width: 100%;text-align: center;background: #C1FEB2;">
<br/>
<strong style="background: #00B629;color: #FFFFFF;">
"""
message += translate("AddonsInstaller", "This addon is already installed.")
message += "</strong><br></div><hr/>" + desc
message += '<br/><br/>Addon repository: <a href="'
message += self.repos[self.idx][1] + '">' + self.repos[self.idx][1] + "</a>"
message += "<br/>" + translate("AddonInstaller", "Installed location") + ": "
message += FreeCAD.getUserAppDataDir() + os.sep + "Mod" + os.sep + self.repos[self.idx][0]
elif self.repos[self.idx][2] == 3:
message = "<div style=\"width: 100%;text-align: center;background: #75AFFD;\"><br/><strong style=\"background: #397FF7;color: #FFFFFF;\">" + translate("AddonsInstaller", "An update is available for this addon.")
message += "</strong><br/></div><hr/>" + desc + '<br/><br/>Addon repository: <a href="' + self.repos[self.idx][1] + '">' + self.repos[self.idx][1] + '</a>'
message += '<br/>' + translate("AddonInstaller","Installed location")+": "+ FreeCAD.getUserAppDataDir() + os.sep + "Mod" + os.sep + self.repos[self.idx][0]
message = """
<div style="width: 100%;text-align: center;background: #75AFFD;">
<br/>
<strong style="background: #397FF7;color: #FFFFFF;">
"""
message += translate("AddonsInstaller", "An update is available for this addon.")
message += "</strong><br/></div><hr/>" + desc + '<br/><br/>Addon repository: <a href="'
message += self.repos[self.idx][1] + '">' + self.repos[self.idx][1] + "</a>"
message += "<br/>" + translate("AddonInstaller", "Installed location") + ": "
message += FreeCAD.getUserAppDataDir() + os.sep + "Mod" + os.sep + self.repos[self.idx][0]
else:
message = desc + '<br/><br/>Addon repository: <a href="' + self.repos[self.idx][1] + '">' + self.repos[self.idx][1] + '</a>'
message = desc + '<br/><br/>Addon repository: <a href="'
message += self.repos[self.idx][1] + '">' + self.repos[self.idx][1] + '</a>'
# If the Addon is obsolete, let the user know through the Addon UI
if self.repos[self.idx][0] in obsolete:
message = " <div style=\"width: 100%; text-align:center; background: #FFB3B3;\"><strong style=\"color: #FFFFFF; background: #FF0000;\">"+translate("AddonsInstaller","This addon is marked as obsolete")+"</strong><br/><br/>"
message += translate("AddonsInstaller","This usually means it is no longer maintained, and some more advanced addon in this list provides the same functionality.")+"<br/></div><hr/>" + desc
message = """
<div style="width: 100%; text-align:center; background: #FFB3B3;">
<strong style="color: #FFFFFF; background: #FF0000;">
"""
message += translate("AddonsInstaller", "This addon is marked as obsolete") + "</strong><br/><br/>"
message += translate("AddonsInstaller",
"This usually means it is no longer maintained, "
"and some more advanced addon in this list "
"provides the same functionality.") + "<br/></div><hr/>" + desc
# If the Addon is Python 2 only, let the user know through the Addon UI
if self.repos[self.idx][0] in py2only:
message = " <div style=\"width: 100%; text-align:center; background: #ffe9b3;\"><strong style=\"color: #FFFFFF; background: #ff8000;\">"+translate("AddonsInstaller","This addon is marked as Python 2 Only")+"</strong><br/><br/>"
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.")+"<br/></div><hr/>" + desc
message = """
<div style="width: 100%; text-align:center; background: #ffe9b3;">
<strong style="color: #FFFFFF; background: #ff8000;">
"""
message += translate("AddonsInstaller", "This addon is marked as Python 2 Only") + "</strong><br/><br/>"
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 += "<br/></div><hr/>" + desc
self.info_label.emit( message )
self.info_label.emit(message)
self.progressbar_show.emit(False)
self.mustLoadImages = True
l = self.loadImages( message, self.repos[self.idx][1], self.repos[self.idx][0])
if l:
self.info_label.emit( l )
label = self.loadImages(message, self.repos[self.idx][1], self.repos[self.idx][0])
if label:
self.info_label.emit(label)
self.stop = True
def stopImageLoading(self):
"this stops the image loading process and allow the thread to terminate earlier"
self.mustLoadImages = False
def loadImages(self,message,url,wbName):
def loadImages(self, message, url, wbName):
"checks if the given page contains images and downloads them"
# QTextBrowser cannot display online images. So we download them
# here, and replace the image link in the html code with the
# downloaded version
imagepaths = re.findall("<img.*?src=\"(.*?)\"",message)
imagepaths = re.findall("<img.*?src=\"(.*?)\"", message)
if imagepaths:
storedimages = []
store = os.path.join(FreeCAD.getUserAppDataDir(),"AddonManager","Images")
store = os.path.join(FreeCAD.getUserAppDataDir(), "AddonManager", "Images")
if not os.path.exists(store):
os.makedirs(store)
for path in imagepaths:
@@ -526,38 +592,37 @@ class ShowWorker(QtCore.QThread):
path = utils.getserver(url) + path
name = path.split("/")[-1]
if name and path.startswith("http"):
storename = os.path.join(store,name)
storename = os.path.join(store, name)
if len(storename) >= 260:
remainChars = 259 - (len(store) + len(wbName) + 1)
storename = os.path.join(store,wbName+name[-remainChars:])
storename = os.path.join(store, wbName+name[-remainChars:])
if not os.path.exists(storename):
try:
u = utils.urlopen(path)
imagedata = u.read()
u.close()
except:
print("AddonManager: Debug: Error retrieving image from",path)
except Exception:
print("AddonManager: Debug: Error retrieving image from", path)
else:
f = open(storename,"wb")
f = open(storename, "wb")
f.write(imagedata)
f.close()
# resize the image to 300x300px if needed
from PySide import QtCore,QtGui
img = QtGui.QImage(storename)
if (img.width() > 300) or (img.height() > 300):
pix = QtGui.QPixmap()
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("\\","/"))
#print(message)
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("\\", "/"))
# print(message)
return message
return None
class GetMacroDetailsWorker(QtCore.QThread):
"""Retrieve the macro details for a macro"""
info_label = QtCore.Signal(str)
@@ -573,36 +638,35 @@ class GetMacroDetailsWorker(QtCore.QThread):
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.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(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>')
+ translate("AddonsInstaller", "This macro is already installed.")
+ '</strong><br>')
else:
already_installed_msg = ''
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>')
+ "<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
class InstallWorker(QtCore.QThread):
"This worker installs a workbench"
info_label = QtCore.Signal(str)
@@ -616,35 +680,26 @@ class InstallWorker(QtCore.QThread):
self.repos = repos
def run(self):
"installs or updates the selected addon"
git = None
try:
import git
except Exception as e:
if not have_git:
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:
FreeCAD.Console.PrintWarning(translate("AddonsInstaller",
"GitPython not found. Using standard download instead.") + "\n")
if not have_zip:
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")
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):
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):
@@ -654,26 +709,33 @@ class InstallWorker(QtCore.QThread):
if os.path.exists(clonedir):
self.info_label.emit("Updating module...")
if sys.version_info.major > 2 and str(self.repos[idx][0]) in py2only:
FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "User requested updating a Python 2 workbench on a system running Python 3 - ")+str(self.repos[idx][0])+"\n")
if git:
if not os.path.exists(clonedir + os.sep + '.git'):
FreeCAD.Console.PrintWarning(translate("AddonsInstaller",
"User requested updating a Python 2 workbench on "
"a system running Python 3 - ") +
str(self.repos[idx][0])+"\n")
if have_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)
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)
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)
cw.set("core", "bare", False)
del cw
repo = git.Repo(clonedir)
repo.head.reset('--hard')
repo.head.reset("--hard")
repo = git.Git(clonedir)
try:
answer = repo.pull() + "\n\n" + translate("AddonsInstaller", "Workbench successfully updated. Please restart FreeCAD to apply the changes.")
except:
print("Error updating module",self.repos[idx][1]," - Please fix manually")
answer = repo.pull() + "\n\n" + translate("AddonsInstaller",
"Workbench successfully updated. "
"Please restart FreeCAD to apply the changes.")
except Exception:
print("Error updating module", self.repos[idx][1], " - Please fix manually")
answer = repo.status()
print(answer)
else:
@@ -682,24 +744,32 @@ class InstallWorker(QtCore.QThread):
for submodule in repo_sms.submodules:
submodule.update(init=True, recursive=True)
else:
answer = self.download(self.repos[idx][1],clonedir) + "\n\n" + translate("AddonsInstaller", "Workbench successfully updated. Please restart FreeCAD to apply the changes.")
answer = self.download(self.repos[idx][1], clonedir) + "\n\n"
answer += translate("AddonsInstaller",
"Workbench successfully updated. Please "
"restart FreeCAD to apply the changes.")
else:
self.info_label.emit("Checking module dependencies...")
depsok,answer = self.checkDependencies(self.repos[idx][1])
depsok, answer = self.check_dependencies(self.repos[idx][1])
if depsok:
if sys.version_info.major > 2 and str(self.repos[idx][0]) in py2only:
FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "User requested installing a Python 2 workbench on a system running Python 3 - ")+str(self.repos[idx][0])+"\n")
if git:
FreeCAD.Console.PrintWarning(translate("AddonsInstaller",
"User requested installing a Python 2 "
"workbench on a system running Python 3 - ") +
str(self.repos[idx][0]) + "\n")
if have_git:
self.info_label.emit("Cloning module...")
repo = git.Repo.clone_from(self.repos[idx][1], clonedir, branch='master')
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.")
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):
@@ -707,24 +777,25 @@ class InstallWorker(QtCore.QThread):
if os.path.exists(clonedir):
for f in os.listdir(clonedir):
if f.lower().endswith(".fcmacro"):
print("copying macro:",f)
print("copying macro:", f)
utils.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 += "\n\n"+translate("AddonsInstaller", "A macro has been installed and is available under Macro -> Macros menu")+":"
answer += "\n<b>" + f + "</b>"
FreeCAD.ParamGet('User parameter:Plugins/' +
self.repos[idx][0]).SetString("destination", clonedir)
answer += "\n\n" + translate("AddonsInstaller",
"A macro has been installed and is available "
"under Macro -> Macros menu")
answer += ":\n<b>" + f + "</b>"
self.progressbar_show.emit(False)
self.info_label.emit(answer)
self.mark_recompute.emit(self.repos[idx][0])
self.stop = True
def checkDependencies(self,baseurl):
def check_dependencies(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")
depsurl = baseurl.replace("github.com", "raw.githubusercontent.com")
if not depsurl.endswith("/"):
depsurl += "/"
depsurl += "master/metadata.txt"
@@ -736,74 +807,69 @@ class InstallWorker(QtCore.QThread):
# urllib2 gives us a bytelike object instead of a string. Have to consider that
try:
depsfile = depsfile.decode('utf-8')
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 line in deps:
if line.startswith("workbenches="):
depswb = line.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(",")
message += translate("AddonsInstaller", "Missing workbench") + ": " + wb + ", "
elif line.startswith("pylibs="):
depspy = line.split("=")[1].split(",")
for pl in depspy:
if pl.strip():
try:
__import__(pl.strip())
except:
except ImportError:
ok = False
message += translate("AddonsInstaller","Missing python module") +": " + pl + ", "
elif l.startswith("optionalpylibs="):
opspy = l.split("=")[1].split(",")
message += translate("AddonsInstaller", "Missing python module") + ": " + pl + ", "
elif line.startswith("optionalpylibs="):
opspy = line.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 + ", "
except ImportError:
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 to install this workbench") + ": <b>" + message + "</b>. "
message += translate("AddonsInstaller","Please install the missing components first.")
message = translate("AddonsInstaller", "Some errors were found that prevent to install this workbench")
message += ": <b>" + message + "</b>. "
message += translate("AddonsInstaller", "Please install the missing components first.")
return ok, message
def download(self,baseurl,clonedir):
def download(self, baseurl, clonedir):
"downloads and unzip a zip version from a git repo"
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.rename(clonedir, bakdir)
os.makedirs(clonedir)
zipurl = utils.getZipUrl(baseurl)
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:
except Exception:
return translate("AddonsInstaller", "Error: Unable to download") + " " + zipurl
if not u:
return translate("AddonsInstaller", "Error: Unable to download") + " " + zipurl
if sys.version_info.major < 3:
import StringIO as io
_stringio = io.StringIO
else:
import io
_stringio = io.BytesIO
zfile = _stringio()
zfile.write(u.read())
zfile = zipfile.ZipFile(zfile)
master = zfile.namelist()[0] # github will put everything in a subfolder
master = zfile.namelist()[0] # github will put everything in a subfolder
zfile.extractall(clonedir)
u.close()
zfile.close()
@@ -816,7 +882,6 @@ class InstallWorker(QtCore.QThread):
class CheckSingleWorker(QtCore.QThread):
"""Worker to check for updates for a single addon"""
updateAvailable = QtCore.Signal(bool)
@@ -828,21 +893,21 @@ class CheckSingleWorker(QtCore.QThread):
def run(self):
try:
import git
except:
if not have_git:
return
FreeCAD.Console.PrintLog("Checking for available updates of the "+self.name+" addon\n")
addondir = os.path.join(FreeCAD.getUserAppDataDir(),"Mod",self.name)
addondir = os.path.join(FreeCAD.getUserAppDataDir(), "Mod", self.name)
if os.path.exists(addondir):
if os.path.exists(addondir + os.sep + '.git'):
if os.path.exists(addondir + os.sep + ".git"):
gitrepo = git.Git(addondir)
try:
gitrepo.fetch()
if "git pull" in gitrepo.status():
self.updateAvailable.emit(True)
return
except:
except Exception:
# can fail for any number of reasons, ex. not being online
pass
self.updateAvailable.emit(False)
# @}