Addon Manager: Rework backend to use package.xml

This shifts to use the model-view-controller pattern for the list of addons,
and moves to using a full model class rather than an indexed array for the
data storage and management. This enables much more information to be stored
as part of the new AddonManagerRepo data type. It now wraps the Macro class
for macros, supports Preference Packs, and provides access to the Metadata
object.
This commit is contained in:
Chris Hennes
2021-10-10 14:40:02 -05:00
parent 1844a0161e
commit 768a0f086f
9 changed files with 2166 additions and 874 deletions

View File

@@ -27,12 +27,15 @@ import re
import shutil
import sys
import ctypes
import tempfile
import ssl
import urllib.request as urllib2
import urllib
from urllib.request import Request
from urllib.error import URLError
from urllib.parse import urlparse
from PySide import QtGui, QtCore
from PySide2 import QtGui, QtCore, QtWidgets
import FreeCAD
import FreeCADGui
@@ -46,7 +49,9 @@ except ImportError:
pass
else:
try:
ssl_ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
#ssl_ctx = ssl.create_default_context(cafile=certifi.where())
ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
#ssl_ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
except AttributeError:
pass
@@ -61,18 +66,17 @@ def translate(context, text, disambig=None):
"Main translation function"
try:
_encoding = QtGui.QApplication.UnicodeUTF8
_encoding = QtWidgets.QApplication.UnicodeUTF8
except AttributeError:
return QtGui.QApplication.translate(context, text, disambig)
return QtWidgets.QApplication.translate(context, text, disambig)
else:
return QtGui.QApplication.translate(context, text, disambig, _encoding)
return QtWidgets.QApplication.translate(context, text, disambig, _encoding)
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")
pass
else:
os_symlink = getattr(os, "symlink", None)
@@ -90,8 +94,8 @@ def symlink(source, link_name):
raise ctypes.WinError()
def urlopen(url):
"""Opens an url with urllib2"""
def urlopen(url:str):
"""Opens an url with urllib and streams it to a temp file"""
timeout = 5
@@ -101,25 +105,29 @@ def urlopen(url):
proxies = {}
else:
if pref.GetBool("SystemProxyCheck", False):
proxy = urllib2.getproxies()
proxy = urllib.request.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)
handler = urllib.request.HTTPSHandler(context=ssl_ctx)
else:
handler = {}
proxy_support = urllib2.ProxyHandler(proxies)
opener = urllib2.build_opener(proxy_support, handler)
urllib2.install_opener(opener)
proxy_support = urllib.request.ProxyHandler(proxies)
opener = urllib.request.build_opener(proxy_support, handler)
urllib.request.install_opener(opener)
# Url opening
req = urllib2.Request(url,
req = urllib.request.Request(url,
headers={'User-Agent': "Magic Browser"})
try:
u = urllib2.urlopen(req, timeout=timeout)
u = urllib.request.urlopen(req, timeout=timeout)
except URLError as e:
FreeCAD.Console.PrintError(f"Error loading {url}:\n {e.reason}\n")
return None
except Exception:
return None
else:
@@ -151,76 +159,7 @@ def update_macro_details(old_macro, new_macro):
setattr(old_macro, attr, getattr(new_macro, attr))
def install_macro(macro, macro_repo_dir):
"""Install a macro and all its related files
Returns True if the macro was installed correctly.
Parameters
----------
- macro: an addonmanager_macro.Macro instance
"""
if not macro.code:
return False
macro_dir = FreeCAD.getUserMacroDir(True)
if not os.path.isdir(macro_dir):
try:
os.makedirs(macro_dir)
except OSError:
return False
macro_path = os.path.join(macro_dir, macro.filename)
try:
with codecs.open(macro_path, 'w', 'utf-8') as macrofile:
macrofile.write(macro.code)
except IOError:
return False
# Copy related files, which are supposed to be given relative to
# macro.src_filename.
base_dir = os.path.dirname(macro.src_filename)
for other_file in macro.other_files:
dst_dir = os.path.join(macro_dir, os.path.dirname(other_file))
if not os.path.isdir(dst_dir):
try:
os.makedirs(dst_dir)
except OSError:
return False
src_file = os.path.join(base_dir, other_file)
dst_file = os.path.join(macro_dir, other_file)
try:
shutil.copy(src_file, dst_file)
except IOError:
return False
return True
def remove_macro(macro):
"""Remove a macro and all its related files
Returns True if the macro was removed correctly.
Parameters
----------
- macro: an addonmanager_macro.Macro instance
"""
if not macro.is_installed():
# Macro not installed, nothing to do.
return True
macro_dir = FreeCAD.getUserMacroDir(True)
macro_path = os.path.join(macro_dir, macro.filename)
macro_path_with_macro_prefix = os.path.join(macro_dir, 'Macro_' + macro.filename)
if os.path.exists(macro_path):
os.remove(macro_path)
elif os.path.exists(macro_path_with_macro_prefix):
os.remove(macro_path_with_macro_prefix)
# Remove related files, which are supposed to be given relative to
# macro.src_filename.
for other_file in macro.other_files:
dst_file = os.path.join(macro_dir, other_file)
remove_directory_if_empty(os.path.dirname(dst_file))
os.remove(dst_file)
return True
def remove_directory_if_empty(dir):
@@ -238,72 +177,81 @@ def remove_directory_if_empty(dir):
def restart_freecad():
"Shuts down and restarts FreeCAD"
args = QtGui.QApplication.arguments()[1:]
args = QtWidgets.QApplication.arguments()[1:]
if FreeCADGui.getMainWindow().close():
QtCore.QProcess.startDetached(QtGui.QApplication.applicationFilePath(), args)
QtCore.QProcess.startDetached(QtWidgets.QApplication.applicationFilePath(), args)
def get_zip_url(baseurl):
def get_zip_url(repo):
"Returns the location of a zip file from a repo, if available"
parsedUrl = urlparse(baseurl)
parsedUrl = urlparse(repo.url)
if parsedUrl.netloc == "github.com":
return baseurl+"/archive/master.zip"
return f"{repo.url}/archive/{repo.branch}.zip"
elif parsedUrl.netloc == "framagit.org" or parsedUrl.netloc == "gitlab.com":
# https://framagit.org/freecad-france/mooc-workbench/-/archive/master/mooc-workbench-master.zip
reponame = baseurl.strip("/").split("/")[-1]
return baseurl+"/-/archive/master/"+reponame+"-master.zip"
return f"{repo.url}/-/archive/{repo.branch}/{repo.name}-{repo.branch}.zip"
else:
print("Debug: addonmanager_utilities.get_zip_url: Unknown git host:", parsedUrl.netloc)
FreeCAD.Console.PrintWarning("Debug: addonmanager_utilities.get_zip_url: Unknown git host:", parsedUrl.netloc)
return None
def construct_git_url(repo, filename):
"Returns a direct download link to a file in an online Git repo: works with github, gitlab, and framagit"
def get_readme_url(url):
"Returns the location of a readme file"
parsedUrl = urlparse(url)
if parsedUrl.netloc == "github.com" or parsedUrl.netloc == "framagit.com":
return url+"/raw/master/README.md"
elif parsedUrl.netloc == "gitlab.com":
return url+"/-/raw/master/README.md"
parsed_url = urlparse(repo.url)
if parsed_url.netloc == "github.com" or parsed_url.netloc == "framagit.com":
return f"{repo.url}/raw/{repo.branch}/{filename}"
elif parsed_url.netloc == "gitlab.com":
return f"{repo.url}/-/raw/{repo.branch}/{filename}"
else:
print("Debug: addonmanager_utilities.get_readme_url: Unknown git host:", url)
FreeCAD.Console.PrintLog("Debug: addonmanager_utilities.construct_git_url: Unknown git host:", parsed_url.netloc)
return None
def get_readme_url(repo):
"Returns the location of a readme file"
def get_desc_regex(url):
return construct_git_url(repo, "README.md")
def get_metadata_url(url):
"Returns the location of a package.xml metadata file"
return construct_git_url(repo, "package.xml")
def get_desc_regex(repo):
"""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"""
parsedUrl = urlparse(url)
parsedUrl = urlparse(repo.url)
if parsedUrl.netloc == "github.com":
return r'<meta property="og:description" content="(.*?)"'
elif parsedUrl.netloc == "framagit.org" or parsedUrl.netloc == "gitlab.com":
return r'<meta.*?content="(.*?)".*?og:description.*?>'
print("Debug: addonmanager_utilities.get_desc_regex: Unknown git host:", url)
FreeCAD.Console.PrintWarning("Debug: addonmanager_utilities.get_desc_regex: Unknown git host:", repo.url)
return None
def get_readme_html_url(url):
def get_readme_html_url(repo):
"""Returns the location of a html file containing readme"""
parsedUrl = urlparse(url)
parsedUrl = urlparse(repo.url)
if parsedUrl.netloc == "github.com":
return url + "/blob/master/README.md"
return f"{repo.url}/blob/{repo.branch}/README.md"
else:
print("Debug: addonmanager_utilities.get_readme_html_url: Unknown git host:", url)
FreeCAD.Console.PrintWarning("Debug: addonmanager_utilities.get_readme_html_url: Unknown git host:", repo.url)
return None
def get_readme_regex(url):
def get_readme_regex(repo):
"""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)"""
parsedUrl = urlparse(url)
parsedUrl = urlparse(repo.url)
if parsedUrl.netloc == "github.com":
return "<article.*?>(.*?)</article>"
else:
print("Debug: addonmanager_utilities.get_readme_regex: Unknown git host:", url)
FreeCAD.Console.PrintWarning("Debug: addonmanager_utilities.get_readme_regex: Unknown git host:", repo.url)
return None
@@ -319,7 +267,7 @@ def fix_relative_links(text, base_url):
if len(parts) < 2 or not re.match(r"^http|^www|^.+\.|^/", parts[0]):
newlink = os.path.join(base_url, link.lstrip('./'))
line = line.replace(link, newlink)
print("Debug: replaced " + link + " with " + newlink)
FreeCAD.Console.PrintLog("Debug: replaced " + link + " with " + newlink)
new_text = new_text + '\n' + line
return new_text