From 781f0bd6262830b764c291001a5176d21ce65690 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Fri, 18 Feb 2022 17:10:52 -0600 Subject: [PATCH] Addon Manager: Improve macro metadata extraction --- src/Mod/AddonManager/addonmanager_macro.py | 126 ++++++++++++------ .../AddonManager/addonmanager_utilities.py | 11 +- src/Mod/AddonManager/package_details.py | 5 +- src/Mod/AddonManager/package_list.py | 32 ++--- 4 files changed, 113 insertions(+), 61 deletions(-) diff --git a/src/Mod/AddonManager/addonmanager_macro.py b/src/Mod/AddonManager/addonmanager_macro.py index 0d7861bc87..97f036925c 100644 --- a/src/Mod/AddonManager/addonmanager_macro.py +++ b/src/Mod/AddonManager/addonmanager_macro.py @@ -27,6 +27,8 @@ import io import codecs import shutil import time +from urllib.parse import urlparse +import tempfile from typing import Dict, Tuple, List, Union import FreeCAD @@ -34,7 +36,7 @@ import NetworkManager translate = FreeCAD.Qt.translate -from addonmanager_utilities import remove_directory_if_empty +from addonmanager_utilities import remove_directory_if_empty, is_float try: from HTMLParser import HTMLParser @@ -65,6 +67,7 @@ class Macro(object): self.date = "" self.src_filename = "" self.author = "" + self.icon = "" self.other_files = [] self.parsed = False @@ -112,52 +115,81 @@ class Macro(object): # __Files__ # __Author__ # __Date__ - max_lines_to_search = 50 + max_lines_to_search = 200 line_counter = 0 - number_of_fields = 5 ic = re.IGNORECASE # Shorten the line for Black - re_comment = re.compile(r"^__Comment__\s*=\s*(['\"])(.*)\1", flags=ic) - re_url = re.compile(r"^__Web__\s*=\s*(['\"])(.*)\1", flags=ic) - re_version = re.compile(r"^__Version__\s*=\s*(['\"])(.*)\1", flags=ic) - re_files = re.compile(r"^__Files__\s*=\s*(['\"])(.*)\1", flags=ic) - re_author = re.compile(r"^__Author__\s*=\s*(['\"])(.*)\1", flags=ic) - re_date = re.compile(r"^__Date__\s*=\s*(['\"])(.*)\1", flags=ic) + string_search_mapping = { + "__comment__": "comment", + "__web__": "url", + "__version__": "version", + "__files__": "other_files", + "__author__": "author", + "__date__": "date", + "__icon__": "icon", + } + + string_search_regex = re.compile(r"\s*(['\"])(.*)\1") f = io.StringIO(code) while f and line_counter < max_lines_to_search: line = f.readline() line_counter += 1 - if not line.startswith( - "__" - ): # Speed things up a bit... this comparison is very cheap - continue - match = re.match(re_comment, line) - if match: - self.comment = match.group(2) - self.comment = re.sub("<.*?>", "", self.comment) # Strip any HTML tags - number_of_fields -= 1 - match = re.match(re_author, line) - if match: - self.author = match.group(2) - number_of_fields -= 1 - match = re.match(re_url, line) - if match: - self.url = match.group(2) - number_of_fields -= 1 - match = re.match(re_version, line) - if match: - self.version = match.group(2) - number_of_fields -= 1 - match = re.match(re_date, line) - if match: - self.date = match.group(2) - number_of_fields -= 1 - match = re.match(re_files, line) - if match: - self.other_files = [of.strip() for of in match.group(2).split(",")] - number_of_fields -= 1 - if number_of_fields <= 0: - break + # if not line.startswith("__"): + # # Speed things up a bit... this comparison is very cheap + # continue + + lowercase_line = line.lower() + for key, value in string_search_mapping.items(): + if lowercase_line.startswith(key): + _, _, after_equals = line.partition("=") + match = re.match(string_search_regex, after_equals) + if match: + if type(self.__dict__[value]) == str: + self.__dict__[value] = match.group(2) + elif type(self.__dict__[value]) == list: + self.__dict__[value] = [ + of.strip() for of in match.group(2).split(",") + ] + string_search_mapping.pop(key) + break + else: + # Macro authors are supposed to be providing strings here, but in some + # cases they are not doing so. If this is the "__version__" tag, try + # to apply some special handling to accepts numbers, and "__date__" + if key == "__version__": + if "__date__" in after_equals.lower(): + FreeCAD.Console.PrintMessage( + translate( + "AddonsInstaller", + "In macro {}, string literal not found for {} element. Guessing at intent and using string from date element.", + ).format(self.name, key) + + "\n" + ) + self.version = self.date + break + elif is_float(after_equals): + FreeCAD.Console.PrintMessage( + translate( + "AddonsInstaller", + "In macro {}, string literal not found for {} element. Guessing at intent and using string representation of contents.", + ).format(self.name, key) + + "\n" + ) + self.version = str(after_equals).strip() + break + FreeCAD.Console.PrintError( + translate( + "AddonsInstaller", + "Syntax error while reading {} from macro {}", + ).format(key, self.name) + + "\n" + ) + FreeCAD.Console.PrintError(line + "\n") + continue + + # Do some cleanup of the values: + if self.comment: + self.comment = re.sub("<.*?>", "", self.comment) # Strip any HTML tags # Truncate long comments to speed up searches, and clean up display if len(self.comment) > 512: @@ -251,9 +283,7 @@ class Macro(object): def install(self, macro_dir: str) -> Tuple[bool, List[str]]: """Install a macro and all its related files - Returns True if the macro was installed correctly. - Parameters ---------- - macro_dir: the directory to install into @@ -283,8 +313,16 @@ class Macro(object): os.makedirs(dst_dir) except OSError: return False, [f"Failed to create {dst_dir}"] - src_file = os.path.join(base_dir, other_file) - dst_file = os.path.join(macro_dir, other_file) + src_file = os.path.normpath(os.path.join(base_dir, other_file)) + dst_file = os.path.normpath(os.path.join(macro_dir, other_file)) + if not os.path.isfile(src_file): + warnings.append( + translate( + "AddonsInstaller", + "Could not locate macro-specified file {} (should have been at {})", + ).format(other_file, src_file) + ) + continue try: shutil.copy(src_file, dst_file) except IOError: diff --git a/src/Mod/AddonManager/addonmanager_utilities.py b/src/Mod/AddonManager/addonmanager_utilities.py index 3d65e336d4..28e01c5bc5 100644 --- a/src/Mod/AddonManager/addonmanager_utilities.py +++ b/src/Mod/AddonManager/addonmanager_utilities.py @@ -24,7 +24,7 @@ import os import re import ctypes -from typing import Union, Optional +from typing import Union, Optional, Any import urllib from urllib.request import Request @@ -340,4 +340,13 @@ def update_macro_installation_details(repo) -> None: return +# Borrowed from Stack Overflow: https://stackoverflow.com/questions/736043/checking-if-a-string-can-be-converted-to-float-in-python +def is_float(element: Any) -> bool: + try: + float(element) + return True + except ValueError: + return False + + # @} diff --git a/src/Mod/AddonManager/package_details.py b/src/Mod/AddonManager/package_details.py index 0f9a9cc34c..ed8f6c8e3e 100644 --- a/src/Mod/AddonManager/package_details.py +++ b/src/Mod/AddonManager/package_details.py @@ -21,6 +21,7 @@ # * USA * # * * # *************************************************************************** +from posixpath import normpath from PySide2.QtCore import * from PySide2.QtGui import * from PySide2.QtWidgets import * @@ -291,7 +292,9 @@ class PackageDetails(QWidget): basedir = FreeCAD.getUserAppDataDir() moddir = os.path.join(basedir, "Mod", repo.name) installationLocationString = ( - translate("AddonsInstaller", "Installation location") + ": " + moddir + translate("AddonsInstaller", "Installation location") + + ": " + + os.path.normpath(moddir) ) self.ui.labelInstallationLocation.setText(installationLocationString) diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py index ff7ae5bf9c..61d2134f69 100644 --- a/src/Mod/AddonManager/package_list.py +++ b/src/Mod/AddonManager/package_list.py @@ -371,22 +371,24 @@ class PackageListItemDelegate(QStyledItemDelegate): ) elif repo.macro and repo.macro.parsed: self.widget.ui.labelDescription.setText(repo.macro.comment) - self.widget.ui.labelVersion.setText(repo.macro.version) + version_string = "" + if repo.macro.version: + version_string = repo.macro.version + " " + if repo.macro.on_wiki: + version_string += "(wiki)" + elif repo.macro.on_git: + version_string += "(git)" + else: + version_string += "(unknown source)" if repo.macro.date: - if repo.macro.version: - new_label = ( - "v" - + repo.macro.version - + ", " - + translate("AddonsInstaller", "updated") - + " " - + repo.macro.date - ) - else: - new_label = ( - translate("AddonsInstaller", "Updated") + " " + repo.macro.date - ) - self.widget.ui.labelVersion.setText(new_label) + version_string = ( + version_string + + ", " + + translate("AddonsInstaller", "updated") + + " " + + repo.macro.date + ) + self.widget.ui.labelVersion.setText("" + version_string + "") if self.displayStyle == ListDisplayStyle.EXPANDED: if repo.macro.author: caption = translate("AddonsInstaller", "Author")