diff --git a/src/Mod/AddonManager/addonmanager_devmode.py b/src/Mod/AddonManager/addonmanager_devmode.py index b728e770e7..2a1357a73c 100644 --- a/src/Mod/AddonManager/addonmanager_devmode.py +++ b/src/Mod/AddonManager/addonmanager_devmode.py @@ -24,6 +24,7 @@ import os import datetime +import subprocess import FreeCAD import FreeCADGui @@ -33,6 +34,7 @@ from PySide2.QtWidgets import ( QListWidgetItem, QDialog, QSizePolicy, + QMessageBox, ) from PySide2.QtGui import ( QIcon, @@ -46,6 +48,7 @@ from addonmanager_devmode_validators import NameValidator, VersionValidator from addonmanager_devmode_predictor import Predictor from addonmanager_devmode_people_table import PeopleTable from addonmanager_devmode_licenses_table import LicensesTable +import addonmanager_utilities as utils translate = FreeCAD.Qt.translate @@ -66,7 +69,9 @@ class AddonGitInterface: try: AddonGitInterface.git_manager = GitManager() except NoGitFound: - FreeCAD.Console.PrintLog("No git found, Addon Manager Developer Mode disabled.") + FreeCAD.Console.PrintLog( + "No git found, Addon Manager Developer Mode disabled." + ) return self.path = path @@ -96,14 +101,15 @@ class AddonGitInterface: return AddonGitInterface.git_manager.get_last_authors(self.path, 10) return [] -#pylint: disable=too-many-instance-attributes + +# pylint: disable=too-many-instance-attributes + class DeveloperMode: """The main Developer Mode dialog, for editing package.xml metadata graphically.""" def __init__(self): - # In the UI we want to show a translated string for the person type, but the underlying # string must be the one expected by the metadata parser, in English self.person_type_translation = { @@ -137,6 +143,7 @@ class DeveloperMode: self.dialog.displayNameLineEdit.setValidator(NameValidator()) self.dialog.versionLineEdit.setValidator(VersionValidator()) + self.dialog.minPythonLineEdit.setValidator(VersionValidator()) self.dialog.addContentItemToolButton.setIcon( QIcon.fromTheme("add", QIcon(":/icons/list-add.svg")) @@ -210,6 +217,7 @@ class DeveloperMode: self.dialog.displayNameLineEdit.setText(self.metadata.Name) self.dialog.descriptionTextEdit.setPlainText(self.metadata.Description) self.dialog.versionLineEdit.setText(self.metadata.Version) + self.dialog.minPythonLineEdit.setText(self.metadata.PythonMin) self._populate_urls_from_metadata(self.metadata) self._populate_contents_from_metadata(self.metadata) @@ -342,6 +350,7 @@ class DeveloperMode: self.dialog.readmeURLLineEdit.clear() self.dialog.documentationURLLineEdit.clear() self.dialog.discussionURLLineEdit.clear() + self.dialog.minPythonLineEdit.clear() self.dialog.iconDisplayLabel.setPixmap(QPixmap()) self.dialog.iconPathLineEdit.clear() @@ -354,6 +363,9 @@ class DeveloperMode: self.dialog.pathToAddonComboBox.editTextChanged.connect( self._addon_combo_text_changed ) + self.dialog.detectMinPythonButton.clicked.connect( + self._detect_min_python_clicked + ) self.dialog.iconBrowseButton.clicked.connect(self._browse_for_icon_clicked) self.dialog.addContentItemToolButton.clicked.connect(self._add_content_clicked) @@ -427,6 +439,11 @@ class DeveloperMode: ) self.metadata.Urls = urls + if self.dialog.minPythonLineEdit.text(): + self.metadata.PythonMin = self.dialog.minPythonLineEdit.text() + else: + self.metadata.PythonMin = "0.0.0" # Code for "unset" + # Content, people, and licenses should already be sync'ed ############################################################################################### @@ -565,6 +582,135 @@ class DeveloperMode: version_string = f"{year}.{month:>02}.{day:>02}" self.dialog.versionLineEdit.setText(version_string) + def _detect_min_python_clicked(self): + if not self._ensure_vermin_loaded(): + FreeCAD.Console.PrintWarning( + translate( + "AddonsInstaller", + "No Vermin, cancelling operation.\n", + "'Vermin' is a Python package - do not translate", + ) + ) + return + FreeCAD.Console.PrintMessage( + translate( + "AddonsInstaller", "Scanning Addon for Python version compatibility" + ) + + "...\n" + ) + #pylint: disable=import-outside-toplevel + import vermin + + required_minor_version = 0 + for dirpath, _, filenames in os.walk(self.current_mod): + for filename in filenames: + if filename.endswith(".py"): + + with open( + os.path.join(dirpath, filename), "r", encoding="utf-8" + ) as f: + contents = f.read() + version_strings = vermin.version_strings( + vermin.detect(contents) + ) + version = version_strings.split(",") + if len(version) >= 2: + # Only care about Py3, and only if there is a dot in the version: + if "." in version[1]: + py3 = version[1].split(".") + major = int(py3[0].strip()) + minor = int(py3[1].strip()) + if major == 3: + FreeCAD.Console.PrintLog( + f"Detected Python 3.{minor} required by {filename}\n" + ) + required_minor_version = max( + required_minor_version, minor + ) + self.dialog.minPythonLineEdit.setText(f"3.{required_minor_version}") + QMessageBox.information( + self.dialog, + translate("AddonsInstaller", "Minimum Python Version Detected"), + translate( + "AddonsInstaller", + "Vermin auto-detected a required version of Python 3.{}", + ).format(required_minor_version), + QMessageBox.Ok, + ) + + def _ensure_vermin_loaded(self) -> bool: + try: + #pylint: disable=import-outside-toplevel,unused-import + import vermin + except ImportError: + #pylint: disable=line-too-long + response = QMessageBox.question( + self.dialog, + translate("AddonsInstaller", "Install Vermin?"), + translate( + "AddonsInstaller", + "Autodetecting the required version of Python for this Addon requires Vermin (https://pypi.org/project/vermin/). OK to install?", + ), + QMessageBox.Yes | QMessageBox.Cancel, + ) + if response == QMessageBox.Cancel: + return False + FreeCAD.Console.PrintMessage( + translate("AddonsInstaller", "Attempting to install Vermin from PyPi") + + "...\n" + ) + python_exe = utils.get_python_exe() + vendor_path = os.path.join( + FreeCAD.getUserAppDataDir(), "AdditionalPythonPackages" + ) + if not os.path.exists(vendor_path): + os.makedirs(vendor_path) + + proc = subprocess.run( + [ + python_exe, + "-m", + "pip", + "install", + "--disable-pip-version-check", + "--target", + vendor_path, + "vermin", + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + ) + FreeCAD.Console.PrintMessage(proc.stdout.decode()) + if proc.returncode != 0: + response = QMessageBox.critical( + self.dialog, + translate("AddonsInstaller", "Installation failed"), + translate( + "AddonsInstaller", + "Failed to install Vermin -- check Report View for details.", + "'Vermin' is the name of a Python package, do not translate", + ), + QMessageBox.Cancel, + ) + return False + try: + #pylint: disable=import-outside-toplevel + import vermin + except ImportError: + response = QMessageBox.critical( + self.dialog, + translate("AddonsInstaller", "Installation failed"), + translate( + "AddonsInstaller", + "Failed to import vermin after installation -- cannot scan Addon.", + "'vermin' is the name of a Python package, do not translate", + ), + QMessageBox.Cancel, + ) + return False + return True + def _browse_for_icon_clicked(self): """Callback: when the "Browse..." button for the icon field is clicked""" new_icon_path, _ = QFileDialog.getOpenFileName( diff --git a/src/Mod/AddonManager/addonmanager_utilities.py b/src/Mod/AddonManager/addonmanager_utilities.py index f02c37f09e..733a91d2eb 100644 --- a/src/Mod/AddonManager/addonmanager_utilities.py +++ b/src/Mod/AddonManager/addonmanager_utilities.py @@ -339,6 +339,7 @@ def get_python_exe() -> str: if not python_exe or not os.path.exists(python_exe): return "" + python_exe = python_exe.replace("/",os.path.sep) prefs.SetString("PythonExecutableForPip", python_exe) return python_exe diff --git a/src/Mod/AddonManager/developer_mode.ui b/src/Mod/AddonManager/developer_mode.ui index f6a03e667f..5e270dfca9 100644 --- a/src/Mod/AddonManager/developer_mode.ui +++ b/src/Mod/AddonManager/developer_mode.ui @@ -54,95 +54,6 @@ Metadata - - - - Icon - - - - - - - README URL - - - - - - - Displayed in the Addon Manager's list of Addons. Should not include the word "FreeCAD", and must be a valid directory name on all support operating systems. - - - - - - - Explanation of what this Addon provides. Displayed in the Addon Manager. It is not necessary for this to state that this is a FreeCAD Addon. - - - true - - - TIP: Since this is displayed within FreeCAD, in the Addon Manager, it is not necessary to take up space saying things like "This is a FreeCAD Addon..." -- just say what it does. - - - - - - - (Recommended) - - - - - - - (Optional) - - - - - - - (Optional) - - - - - - - - - - Displayed in the Addon Manager's list of Addons. Should not include the word "FreeCAD", and must be a valid directory name on all support operating systems. - - - Addon Name - - - - - - - Version - - - - - - - Documentation URL - - - - - - - Repository URL - - - @@ -160,13 +71,6 @@ - - - - Discussion URL - - - @@ -180,17 +84,17 @@ - - - - (Optional) + + + + Discussion URL - - + + - Website URL + Icon @@ -219,7 +123,65 @@ - + + + + + + + (Optional) + + + + + + + Displayed in the Addon Manager's list of Addons. Should not include the word "FreeCAD", and must be a valid directory name on all support operating systems. + + + + + + + (Optional) + + + + + + + README URL + + + + + + + Explanation of what this Addon provides. Displayed in the Addon Manager. It is not necessary for this to state that this is a FreeCAD Addon. + + + true + + + TIP: Since this is displayed within FreeCAD, in the Addon Manager, it is not necessary to take up space saying things like "This is a FreeCAD Addon..." -- just say what it does. + + + + + + + Repository URL + + + + + + + (Optional) + + + + @@ -236,13 +198,76 @@ - - + + + + Website URL + + + + + + + Documentation URL + + + + + (Optional) + + + + Displayed in the Addon Manager's list of Addons. Should not include the word "FreeCAD", and must be a valid directory name on all support operating systems. + + + Addon Name + + + + + + + Version + + + + + + + (Recommended) + + + + + + + Minimum Python + + + + + + + + + (Optional, only 3.x version supported) + + + + + + + Detect... + + + + +