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...
+
+
+
+
+