diff --git a/src/Mod/AddonManager/addonmanager_git.py b/src/Mod/AddonManager/addonmanager_git.py index 61bbb09dbf..fc79bf926b 100644 --- a/src/Mod/AddonManager/addonmanager_git.py +++ b/src/Mod/AddonManager/addonmanager_git.py @@ -24,7 +24,6 @@ # pylint: disable=too-few-public-methods -import multiprocessing import os import platform import shutil @@ -306,4 +305,3 @@ class GitManager: ) return proc.stdout.decode() - diff --git a/src/Mod/AddonManager/addonmanager_utilities.py b/src/Mod/AddonManager/addonmanager_utilities.py index bbe0911a2b..f02c37f09e 100644 --- a/src/Mod/AddonManager/addonmanager_utilities.py +++ b/src/Mod/AddonManager/addonmanager_utilities.py @@ -21,12 +21,14 @@ # * * # *************************************************************************** +""" Utilities to work across different platforms, providers and python versions """ + import os import platform import shutil import re import ctypes -from typing import Union, Optional, Any +from typing import Optional, Any from urllib.parse import urlparse @@ -46,7 +48,8 @@ translate = FreeCAD.Qt.translate def symlink(source, link_name): - """Creates a symlink of a file, if possible. Note that it fails on most modern Windows installations""" + """Creates a symlink of a file, if possible. Note that it fails on most modern Windows + installations""" if os.path.exists(link_name) or os.path.lexists(link_name): pass @@ -55,14 +58,15 @@ def symlink(source, link_name): if callable(os_symlink): os_symlink(source, link_name) else: - # NOTE: This does not work on most normal Windows 10 and later installations, unless developer - # mode is turned on. Make sure to catch any exception thrown and have a fallback plan. + # NOTE: This does not work on most normal Windows 10 and later installations, unless + # developer mode is turned on. Make sure to catch any exception thrown and have a + # fallback plan. csl = ctypes.windll.kernel32.CreateSymbolicLinkW csl.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32) csl.restype = ctypes.c_ubyte flags = 1 if os.path.isdir(source) else 0 # set the SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE flag - # (see https://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10/#joC5tFKhdXs2gGml.97) + # (see https://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10) flags += 2 if csl(link_name, source, flags) == 0: raise ctypes.WinError() @@ -78,9 +82,7 @@ def update_macro_details(old_macro, new_macro): if old_macro.on_git and new_macro.on_git: FreeCAD.Console.PrintLog( - 'The macro "{}" is present twice in github, please report'.format( - old_macro.name - ) + f'The macro "{old_macro.name}" is present twice in github, please report' ) # We don't report macros present twice on the wiki because a link to a # macro is considered as a macro. For example, 'Perpendicular To Wire' @@ -91,16 +93,14 @@ def update_macro_details(old_macro, new_macro): setattr(old_macro, attr, getattr(new_macro, attr)) -def remove_directory_if_empty(dir): - """Remove the directory if it is empty +def remove_directory_if_empty(dir_to_remove): + """Remove the directory if it is empty, with one exception: the directory returned by + FreeCAD.getUserMacroDir(True) will not be removed even if it is empty.""" - Directory FreeCAD.getUserMacroDir(True) will not be removed even if empty. - """ - - if dir == FreeCAD.getUserMacroDir(True): + if dir_to_remove == FreeCAD.getUserMacroDir(True): return - if not os.listdir(dir): - os.rmdir(dir) + if not os.listdir(dir_to_remove): + os.rmdir(dir_to_remove) def restart_freecad(): @@ -119,30 +119,27 @@ def get_zip_url(repo): parsed_url = urlparse(repo.url) if parsed_url.netloc == "github.com": return f"{repo.url}/archive/{repo.branch}.zip" - elif parsed_url.netloc in ["gitlab.com", "framagit.org", "salsa.debian.org"]: - return f"{repo.url}/-/archive/{repo.branch}/{repo.name}-{repo.branch}.zip" - else: - FreeCAD.Console.PrintLog( - "Debug: addonmanager_utilities.get_zip_url: Unknown git host fetching zip URL:", - parsed_url.netloc, - "\n", - ) + if parsed_url.netloc in ["gitlab.com", "framagit.org", "salsa.debian.org"]: return f"{repo.url}/-/archive/{repo.branch}/{repo.name}-{repo.branch}.zip" + FreeCAD.Console.PrintLog( + "Debug: addonmanager_utilities.get_zip_url: Unknown git host fetching zip URL:", + parsed_url.netloc, + "\n", + ) + return f"{repo.url}/-/archive/{repo.branch}/{repo.name}-{repo.branch}.zip" def recognized_git_location(repo) -> bool: - """Returns whether this repo is based at a known git repo location: works with github, gitlab, framagit, and salsa.debian.org""" + """Returns whether this repo is based at a known git repo location: works with github, gitlab, + framagit, and salsa.debian.org""" parsed_url = urlparse(repo.url) - if parsed_url.netloc in [ + return parsed_url.netloc in [ "github.com", "gitlab.com", "framagit.org", "salsa.debian.org", - ]: - return True - else: - return False + ] def construct_git_url(repo, filename): @@ -151,16 +148,15 @@ def construct_git_url(repo, filename): parsed_url = urlparse(repo.url) if parsed_url.netloc == "github.com": return f"{repo.url}/raw/{repo.branch}/{filename}" - elif parsed_url.netloc in ["gitlab.com", "framagit.org", "salsa.debian.org"]: - return f"{repo.url}/-/raw/{repo.branch}/{filename}" - else: - FreeCAD.Console.PrintLog( - "Debug: addonmanager_utilities.construct_git_url: Unknown git host:" - + parsed_url.netloc - + f" for file {filename}\n" - ) - # Assume it's some kind of local GitLab instance... + if parsed_url.netloc in ["gitlab.com", "framagit.org", "salsa.debian.org"]: return f"{repo.url}/-/raw/{repo.branch}/{filename}" + FreeCAD.Console.PrintLog( + "Debug: addonmanager_utilities.construct_git_url: Unknown git host:" + + parsed_url.netloc + + f" for file {filename}\n" + ) + # Assume it's some kind of local GitLab instance... + return f"{repo.url}/-/raw/{repo.branch}/{filename}" def get_readme_url(repo): @@ -182,15 +178,14 @@ def get_desc_regex(repo): parsedUrl = urlparse(repo.url) if parsedUrl.netloc == "github.com": return r'' - else: - FreeCAD.Console.PrintLog( - "Debug: addonmanager_utilities.get_desc_regex: Unknown git host:", - repo.url, - "\n", - ) + if parsedUrl.netloc in ["gitlab.com", "salsa.debian.org", "framagit.org"]: return r'' + FreeCAD.Console.PrintLog( + "Debug: addonmanager_utilities.get_desc_regex: Unknown git host:", + repo.url, + "\n", + ) + return r'' def get_readme_html_url(repo): @@ -199,62 +194,12 @@ def get_readme_html_url(repo): parsedUrl = urlparse(repo.url) if parsedUrl.netloc == "github.com": return f"{repo.url}/blob/{repo.branch}/README.md" - elif parsedUrl.netloc in ["gitlab.com", "salsa.debian.org", "framagit.org"]: + if parsedUrl.netloc in ["gitlab.com", "salsa.debian.org", "framagit.org"]: return f"{repo.url}/-/blob/{repo.branch}/README.md" - else: - FreeCAD.Console.PrintLog( - "Unrecognized git repo location '' -- guessing it is a GitLab instance..." - ) - return f"{repo.url}/-/blob/{repo.branch}/README.md" - - -def repair_git_repo(repo_url: str, clone_dir: str) -> None: - # Repair addon installed with raw download by adding the .git - # directory to it - - try: - import git - - # If GitPython is not installed, but the user has a directory named "git" in their Python path, they - # may have the import succeed, but it will not be a real GitPython installation - have_git = hasattr(git, "Repo") - if not have_git: - return - except ImportError: - return - - try: - bare_repo = git.Repo.clone_from( - repo_url, clone_dir + os.sep + ".git", bare=True - ) - with bare_repo.config_writer() as cw: - cw.set("core", "bare", False) - except AttributeError: - FreeCAD.Console.PrintLog( - translate( - "AddonsInstaller", - "Outdated GitPython detected, consider upgrading with pip.", - ) - + "\n" - ) - cw = bare_repo.config_writer() - cw.set("core", "bare", False) - del cw - except Exception as e: - FreeCAD.Console.PrintWarning( - translate("AddonsInstaller", "Failed to repair missing .git directory") - + "\n" - ) - FreeCAD.Console.PrintWarning( - translate("AddonsInstaller", "Repository URL") + f": {repo_url}\n" - ) - FreeCAD.Console.PrintWarning( - translate("AddonsInstaller", "Clone directory") + f": {clone_dir}\n" - ) - FreeCAD.Console.PrintWarning(e) - return - repo = git.Repo(clone_dir) - repo.head.reset("--hard") + FreeCAD.Console.PrintLog( + "Unrecognized git repo location '' -- guessing it is a GitLab instance..." + ) + return f"{repo.url}/-/blob/{repo.branch}/README.md" def is_darkmode() -> bool: @@ -294,11 +239,11 @@ def get_assigned_string_literal(line: str) -> Optional[str]: def get_macro_version_from_file(filename: str) -> str: - """Get the version of the macro from a local macro file. Supports strings, ints, and floats, as - well as a reference to __date__""" + """Get the version of the macro from a local macro file. Supports strings, ints, and floats, + as well as a reference to __date__""" date = "" - with open(filename, "r", errors="ignore") as f: + with open(filename, "r", errors="ignore", encoding="utf-8") as f: line_counter = 0 max_lines_to_scan = 200 while line_counter < max_lines_to_scan: @@ -310,9 +255,20 @@ def get_macro_version_from_file(filename: str) -> str: match = get_assigned_string_literal(line) if match: return match - elif "__date__" in line.lower(): - # Don't do any real syntax checking, just assume the line is something like __version__ = __date__ - return date + if "__date__" in line.lower(): + # Don't do any real syntax checking, just assume the line is something + # like __version__ = __date__ + if date: + return date + # pylint: disable=line-too-long,consider-using-f-string + FreeCAD.Console.PrintWarning( + translate( + "AddonsInstaller", + "Macro {} specified '__version__ = __date__' prior to setting a value for __date__".format( + filename + ), + ) + ) elif line.lower().startswith("__date__"): match = get_assigned_string_literal(line) if match: @@ -321,8 +277,10 @@ def get_macro_version_from_file(filename: str) -> str: def update_macro_installation_details(repo) -> None: + """Determine if a given macro is installed, either in its plain name, + or prefixed with "Macro_" """ if repo is None or not hasattr(repo, "macro") or repo.macro is None: - FreeCAD.Console.PrintLog(f"Requested macro details for non-macro object\n") + FreeCAD.Console.PrintLog("Requested macro details for non-macro object\n") return test_file_one = os.path.join(FreeCAD.getUserMacroDir(True), repo.macro.filename) test_file_two = os.path.join( @@ -338,8 +296,10 @@ 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 +# Borrowed from Stack Overflow: +# https://stackoverflow.com/questions/736043/checking-if-a-string-can-be-converted-to-float def is_float(element: Any) -> bool: + """Determine whether a given item can be converted to a floating-point number""" try: float(element) return True @@ -351,12 +311,12 @@ def is_float(element: Any) -> bool: def get_python_exe() -> str: - # Find Python. In preference order - # A) The value of the PythonExecutableForPip user preference - # B) The executable located in the same bin directory as FreeCAD and called "python3" - # C) The executable located in the same bin directory as FreeCAD and called "python" - # D) The result of an shutil search for your system's "python3" executable - # E) The result of an shutil search for your system's "python" executable + """Find Python. In preference order + A) The value of the PythonExecutableForPip user preference + B) The executable located in the same bin directory as FreeCAD and called "python3" + C) The executable located in the same bin directory as FreeCAD and called "python" + D) The result of an shutil search for your system's "python3" executable + E) The result of an shutil search for your system's "python" executable""" prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") python_exe = prefs.GetString("PythonExecutableForPip", "Not set") if not python_exe or python_exe == "Not set" or not os.path.exists(python_exe): @@ -384,6 +344,7 @@ def get_python_exe() -> str: def get_cache_file_name(file: str) -> str: + """Get the full path to a cache file with a given name.""" cache_path = FreeCAD.getUserCachePath() am_path = os.path.join(cache_path, "AddonManager") os.makedirs(am_path, exist_ok=True)