Addon Manager: pylint cleanup of devmode

This commit is contained in:
Chris Hennes
2023-02-21 10:39:57 -06:00
committed by Chris Hennes
parent efdbccd0b2
commit 0a8037a27d
11 changed files with 103 additions and 98 deletions

View File

@@ -25,16 +25,16 @@
import os
import subprocess
from time import sleep
from typing import List
import FreeCAD
import addonmanager_freecad_interface as fci
from PySide import QtCore
import addonmanager_utilities as utils
from addonmanager_installer import AddonInstaller, MacroInstaller
from Addon import Addon
translate = FreeCAD.Qt.translate
translate = fci.translate
class DependencyInstaller(QtCore.QObject):
@@ -48,7 +48,7 @@ class DependencyInstaller(QtCore.QObject):
def __init__(
self,
addons: List[object],
addons: List[Addon],
python_requires: List[str],
python_optional: List[str],
location: os.PathLike = None,
@@ -56,7 +56,8 @@ class DependencyInstaller(QtCore.QObject):
"""Install the various types of dependencies that might be specified. If an optional
dependency fails this is non-fatal, but other failures are considered fatal. If location
is specified it overrides the FreeCAD user base directory setting: this is used mostly
for testing purposes and shouldn't be set by normal code in most circumstances."""
for testing purposes and shouldn't be set by normal code in most circumstances.
"""
super().__init__()
self.addons = addons
self.python_requires = python_requires
@@ -95,18 +96,19 @@ class DependencyInstaller(QtCore.QObject):
return False
try:
proc = self._run_pip(["--version"])
FreeCAD.Console.PrintMessage(proc.stdout + "\n")
fci.Console.PrintMessage(proc.stdout + "\n")
except subprocess.CalledProcessError:
self.no_pip.emit(f"{python_exe} -m pip --version")
return False
return True
def _install_required(self, vendor_path: os.PathLike) -> bool:
"""Install the required Python package dependencies. If any fail a failure signal is
emitted and the function exits without proceeding with any additional installs."""
def _install_required(self, vendor_path: str) -> bool:
"""Install the required Python package dependencies. If any fail a failure
signal is emitted and the function exits without proceeding with any additional
installations."""
for pymod in self.python_requires:
if QtCore.QThread.currentThread().isInterruptionRequested():
return
return False
try:
proc = self._run_pip(
[
@@ -117,9 +119,9 @@ class DependencyInstaller(QtCore.QObject):
pymod,
]
)
FreeCAD.Console.PrintMessage(proc.stdout + "\n")
fci.Console.PrintMessage(proc.stdout + "\n")
except subprocess.CalledProcessError as e:
FreeCAD.Console.PrintError(str(e) + "\n")
fci.Console.PrintError(str(e) + "\n")
self.failure.emit(
translate(
"AddonsInstaller",
@@ -127,9 +129,10 @@ class DependencyInstaller(QtCore.QObject):
).format(pymod),
str(e),
)
return
return False
return True
def _install_optional(self, vendor_path: os.PathLike):
def _install_optional(self, vendor_path: str):
"""Install the optional Python package dependencies. If any fail a message is printed to
the console, but installation of the others continues."""
for pymod in self.python_optional:
@@ -145,9 +148,9 @@ class DependencyInstaller(QtCore.QObject):
pymod,
]
)
FreeCAD.Console.PrintMessage(proc.stdout + "\n")
fci.Console.PrintMessage(proc.stdout + "\n")
except subprocess.CalledProcessError as e:
FreeCAD.Console.PrintError(
fci.Console.PrintError(
translate(
"AddonsInstaller", "Installation of optional package failed"
)
@@ -162,7 +165,8 @@ class DependencyInstaller(QtCore.QObject):
final_args.extend(args)
return self._subprocess_wrapper(final_args)
def _subprocess_wrapper(self, args) -> object:
@staticmethod
def _subprocess_wrapper(args) -> subprocess.CompletedProcess:
"""Wrap subprocess call so test code can mock it."""
return utils.run_interruptable_subprocess(args)
@@ -177,7 +181,7 @@ class DependencyInstaller(QtCore.QObject):
for addon in self.addons:
if QtCore.QThread.currentThread().isInterruptionRequested():
return
FreeCAD.Console.PrintMessage(
fci.Console.PrintMessage(
translate(
"AddonsInstaller", "Installing required dependency {}"
).format(addon.name)

View File

@@ -110,7 +110,6 @@ 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 = {
@@ -153,7 +152,7 @@ class DeveloperMode:
QIcon.fromTheme("remove", QIcon(":/icons/list-remove.svg"))
)
def show(self, parent=None, path=None):
def show(self, parent=None, path: str = None):
"""Show the main dev mode dialog"""
if parent:
self.dialog.setParent(parent)
@@ -178,7 +177,7 @@ class DeveloperMode:
self.metadata.Date = str(now)
self.metadata.write(os.path.join(self.current_mod, "package.xml"))
def _populate_dialog(self, path_to_repo):
def _populate_dialog(self, path_to_repo: str):
"""Populate this dialog using the best available parsing of the contents of the repo at
path_to_repo. This is a multi-layered process that starts with any existing package.xml
file or other known metadata files, and proceeds through examining the contents of the
@@ -330,7 +329,7 @@ class DeveloperMode:
predictor = Predictor()
self.metadata = predictor.predict_metadata(self.current_mod)
def _scan_for_git_info(self, path):
def _scan_for_git_info(self, path: str):
"""Look for branch availability"""
self.git_interface = AddonGitInterface(path)
if self.git_interface.git_exists:
@@ -383,7 +382,7 @@ class DeveloperMode:
# Finally, populate the combo boxes, etc.
self._populate_combo()
# Disable all of the "Remove" buttons until something is selected
# Disable all the "Remove" buttons until something is selected
self.dialog.removeContentItemToolButton.setDisabled(True)
def _sync_metadata_to_ui(self):
@@ -445,7 +444,7 @@ class DeveloperMode:
else:
self.metadata.PythonMin = "0.0.0" # Code for "unset"
# Content, people, and licenses should already be sync'ed
# Content, people, and licenses should already be synchronized
###############################################################################################
# DIALOG SLOTS
@@ -516,7 +515,7 @@ class DeveloperMode:
if entry and entry not in recent_mod_paths and os.path.exists(entry):
recent_mod_paths.append(entry)
# Remove the whole thing so we can recreate it from scratch
# Remove the whole thing, so we can recreate it from scratch
self.pref.RemGroup("recentModsList")
if recent_mod_paths:
@@ -604,11 +603,10 @@ class DeveloperMode:
import vermin
required_minor_version = 0
for dirpath, _, filenames in os.walk(self.current_mod):
for dir_path, _, filenames in os.walk(self.current_mod):
for filename in filenames:
if filename.endswith(".py"):
with open(os.path.join(dirpath, filename), encoding="utf-8") as f:
with open(os.path.join(dir_path, filename), encoding="utf-8") as f:
contents = f.read()
version_strings = vermin.version_strings(
vermin.detect(contents)
@@ -649,7 +647,7 @@ class DeveloperMode:
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?",
"Auto-detecting the required version of Python for this Addon requires Vermin (https://pypi.org/project/vermin/). OK to install?",
),
QMessageBox.Yes | QMessageBox.Cancel,
)
@@ -682,7 +680,7 @@ class DeveloperMode:
)
FreeCAD.Console.PrintMessage(proc.stdout.decode())
if proc.returncode != 0:
response = QMessageBox.critical(
QMessageBox.critical(
self.dialog,
translate("AddonsInstaller", "Installation failed"),
translate(
@@ -697,7 +695,7 @@ class DeveloperMode:
# pylint: disable=import-outside-toplevel
import vermin
except ImportError:
response = QMessageBox.critical(
QMessageBox.critical(
self.dialog,
translate("AddonsInstaller", "Installation failed"),
translate(

View File

@@ -58,7 +58,7 @@ translate = FreeCAD.Qt.translate
class AddContent:
"""A dialog for adding a single content item to the package metadata."""
def __init__(self, path_to_addon: os.PathLike, toplevel_metadata: FreeCAD.Metadata):
def __init__(self, path_to_addon: str, toplevel_metadata: FreeCAD.Metadata):
"""path_to_addon is the full path to the toplevel directory of this Addon, and
toplevel_metadata is to overall package.xml Metadata object for this Addon. This
information is used to assist the use in filling out the dialog by providing
@@ -147,7 +147,7 @@ class AddContent:
result = self.dialog.exec()
if result == QDialog.Accepted:
return self._generate_metadata()
return None, None
return None
def _populate_dialog(self, metadata: FreeCAD.Metadata) -> None:
"""Fill in the dialog with the details from the passed metadata object"""
@@ -231,7 +231,7 @@ class AddContent:
# Early return if this is the only addon
if self.dialog.singletonCheckBox.isChecked():
return (current_data, self.metadata)
return current_data, self.metadata
# Otherwise, process the rest of the metadata (display name is already done)
self.metadata.Description = (
@@ -254,13 +254,14 @@ class AddContent:
licenses = []
for row in range(self.dialog.licensesTableWidget.rowCount()):
new_license = {}
new_license["name"] = self.dialog.licensesTableWidget.item(row, 0).text
new_license["file"] = self.dialog.licensesTableWidget.item(row, 1).text()
new_license = {
"name": self.dialog.licensesTableWidget.item(row, 0).text,
"file": self.dialog.licensesTableWidget.item(row, 1).text(),
}
licenses.append(new_license)
self.metadata.License = licenses
return (self.dialog.addonKindComboBox.currentData(), self.metadata)
return self.dialog.addonKindComboBox.currentData(), self.metadata
###############################################################################################
# DIALOG SLOTS
@@ -381,7 +382,8 @@ class EditTags:
def exec(self):
"""Execute the dialog, returning a list of tags (which may be empty, but still represents
the expected list of tags to be set, e.g. the user may have removed them all)."""
the expected list of tags to be set, e.g. the user may have removed them all).
"""
result = self.dialog.exec()
if result == QDialog.Accepted:
new_tags: List[str] = self.dialog.lineEdit.text().split(",")
@@ -541,7 +543,8 @@ class EditDependency:
self, dep_type="", dep_name="", dep_optional=False
) -> Tuple[str, str, bool]:
"""Execute the dialog, returning a tuple of the type of dependency (workbench, addon, or
python), the name of the dependency, and a boolean indicating whether this is optional."""
python), the name of the dependency, and a boolean indicating whether this is optional.
"""
# If we are editing an existing row, set up the dialog:
if dep_type and dep_name:
@@ -564,8 +567,8 @@ class EditDependency:
dep_name = self.dialog.dependencyComboBox.currentData()
if dep_name == "other":
dep_name = self.dialog.lineEdit.text()
return (dep_type, dep_name, dep_optional)
return ("", "", False)
return dep_type, dep_name, dep_optional
return "", "", False
def _populate_internal_workbenches(self):
"""Add all known internal FreeCAD Workbenches to the list"""

View File

@@ -43,6 +43,8 @@ try:
RegexWrapper = QRegularExpression
RegexValidatorWrapper = QRegularExpressionValidator
except ImportError:
QRegularExpressionValidator = None
QRegularExpression = None
from PySide.QtGui import (
QRegExpValidator,
)
@@ -150,7 +152,7 @@ class LicenseSelector:
new_short_code = self.dialog.otherLineEdit.text()
self.pref.SetString("devModeLastSelectedLicense", new_short_code)
return new_short_code, new_license_path
return None, None
return None
def set_license(self, short_code):
"""Set the currently-selected license."""

View File

@@ -72,9 +72,9 @@ class LicensesTable:
"""Use the passed metadata object to populate the maintainers and authors"""
self.widget.tableWidget.setRowCount(0)
row = 0
for l in self.metadata.License:
shortcode = l["name"]
path = l["file"]
for lic in self.metadata.License:
shortcode = lic["name"]
path = lic["file"]
self._add_row(row, shortcode, path)
row += 1

View File

@@ -65,7 +65,7 @@ class MetadataValidators:
return
# The package.xml standard has some required elements that the basic XML reader is not
# actually checking for. In developer mode, actually make sure that all of the rules are
# actually checking for. In developer mode, actually make sure that all the rules are
# being followed for each element.
errors = []
@@ -141,7 +141,8 @@ class MetadataValidators:
errors.extend(self.validate_urls(urls))
return errors
def validate_urls(self, urls) -> List[str]:
@staticmethod
def validate_urls(urls) -> List[str]:
"""Check the URLs provided by the addon"""
errors = []
if len(urls) == 0:
@@ -183,14 +184,16 @@ class MetadataValidators:
)
return errors
def validate_workbench_metadata(self, workbench) -> List[str]:
@staticmethod
def validate_workbench_metadata(workbench) -> List[str]:
"""Validate the required element(s) for a workbench"""
errors = []
if not workbench.Classname or len(workbench.Classname) == 0:
errors.append("No <classname> specified for workbench")
return errors
def validate_preference_pack_metadata(self, pack) -> List[str]:
@staticmethod
def validate_preference_pack_metadata(pack) -> List[str]:
"""Validate the required element(s) for a preference pack"""
errors = []
if not pack.Name or len(pack.Name) == 0:

View File

@@ -61,7 +61,7 @@ class PersonEditor:
self.dialog.nameLineEdit.text(),
self.dialog.emailLineEdit.text(),
)
return (None, None, None)
return "", "", ""
def setup(
self, person_type: str = "maintainer", name: str = "", email: str = ""

View File

@@ -63,7 +63,7 @@ class Predictor:
if not self.git_manager:
raise Exception("Cannot use Developer Mode without git installed")
def predict_metadata(self, path: os.PathLike) -> FreeCAD.Metadata:
def predict_metadata(self, path: str) -> FreeCAD.Metadata:
"""Create a predicted Metadata object based on the contents of the passed-in directory"""
if not os.path.isdir(path):
return None
@@ -85,7 +85,7 @@ class Predictor:
committers = self.git_manager.get_last_committers(self.path)
# This is a dictionary keyed to the author's name (which can be many different
# This is a dictionary keyed to the author's name (which can be many
# things, depending on the author) containing two fields, "email" and "count". It
# is common for there to be multiple entries representing the same human being,
# so a passing attempt is made to reconcile:

View File

@@ -40,6 +40,8 @@ try:
RegexWrapper = QRegularExpression
RegexValidatorWrapper = QRegularExpressionValidator
except ImportError:
QRegularExpressionValidator = None
QRegularExpression = None
from PySide.QtGui import (
QRegExpValidator,
)
@@ -163,4 +165,4 @@ class VersionValidator(QValidator):
return semver_result
if calver_result[0] == QValidator.Intermediate:
return calver_result
return (QValidator.Invalid, value, position)
return QValidator.Invalid, value, position

View File

@@ -29,34 +29,15 @@ import os
import platform
import shutil
import subprocess
from typing import List
from typing import List, Optional
import time
import FreeCAD
from PySide import QtCore # Needed to detect thread interruption
import addonmanager_utilities as utils
translate = FreeCAD.Qt.translate
def initialize_git() -> object:
"""If git is enabled, locate the git executable if necessary and return a new
GitManager object. The executable location is saved in user preferences for reuse,
and git can be disabled by setting the disableGit parameter in the Addons
preference group. Returns None if for any of those reasons we aren't using git."""
git_manager = None
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
disable_git = pref.GetBool("disableGit", False)
if not disable_git:
try:
git_manager = GitManager()
except NoGitFound:
pass
return git_manager
class NoGitFound(RuntimeError):
"""Could not locate the git executable on this system."""
@@ -219,7 +200,7 @@ class GitManager:
original_cwd = os.getcwd()
# Make sure we are not currently in that directory, otherwise on Windows the rename
# Make sure we are not currently in that directory, otherwise on Windows the "rename"
# will fail. To guarantee we aren't in it, change to it, then shift up one.
os.chdir(local_path)
os.chdir("..")
@@ -284,7 +265,7 @@ class GitManager:
return branches
def get_last_committers(self, local_path, n=10):
"""Examine the last n entries of the commit history, and return a list of all of the
"""Examine the last n entries of the commit history, and return a list of all the
committers, their email addresses, and how many commits each one is responsible for."""
old_dir = os.getcwd()
os.chdir(local_path)
@@ -313,7 +294,7 @@ class GitManager:
return result_dict
def get_last_authors(self, local_path, n=10):
"""Examine the last n entries of the commit history, and return a list of all of the
"""Examine the last n entries of the commit history, and return a list of all the
authors, their email addresses, and how many commits each one is responsible for."""
old_dir = os.getcwd()
os.chdir(local_path)
@@ -338,7 +319,7 @@ class GitManager:
# Find git. In preference order
# A) The value of the GitExecutable user preference
# B) The executable located in the same bin directory as FreeCAD and called "git"
# C) The result of an shutil search for your system's "git" executable
# C) The result of a shutil search for your system's "git" executable
prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
git_exe = prefs.GetString("GitExecutable", "Not set")
if not git_exe or git_exe == "Not set" or not os.path.exists(git_exe):
@@ -371,3 +352,20 @@ class GitManager:
)
return proc.stdout
def initialize_git() -> Optional[GitManager]:
"""If git is enabled, locate the git executable if necessary and return a new
GitManager object. The executable location is saved in user preferences for reuse,
and git can be disabled by setting the disableGit parameter in the Addons
preference group. Returns None if for any of those reasons we aren't using git."""
git_manager = None
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
disable_git = pref.GetBool("disableGit", False)
if not disable_git:
try:
git_manager = GitManager()
except NoGitFound:
pass
return git_manager

View File

@@ -47,17 +47,13 @@ if FreeCAD.GuiUp:
# Python urllib.request (if requests is not available).
import NetworkManager # Requires an event loop, so is only available with the GUI
else:
has_requests = False
try:
import requests
has_requests = True
except ImportError:
has_requests = False
requests = None
import urllib.request
import ssl
# @package AddonManager_utilities
# \ingroup ADDONMANAGER
# \brief Utilities to work across different platforms, providers and python versions
@@ -99,7 +95,7 @@ def symlink(source, link_name):
def rmdir(path: os.PathLike) -> bool:
try:
shutil.rmtree(path, onerror=remove_readonly)
except Exception:
except (WindowsError, PermissionError, OSError):
return False
return True
@@ -214,10 +210,10 @@ 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(repo.url)
if parsedUrl.netloc == "github.com":
parsed_url = urlparse(repo.url)
if parsed_url.netloc == "github.com":
return r'<meta property="og:description" content="(.*?)"'
if parsedUrl.netloc in ["gitlab.com", "salsa.debian.org", "framagit.org"]:
if parsed_url.netloc in ["gitlab.com", "salsa.debian.org", "framagit.org"]:
return r'<meta.*?content="(.*?)".*?og:description.*?>'
FreeCAD.Console.PrintLog(
"Debug: addonmanager_utilities.get_desc_regex: Unknown git host:",
@@ -230,10 +226,10 @@ def get_desc_regex(repo):
def get_readme_html_url(repo):
"""Returns the location of a html file containing readme"""
parsedUrl = urlparse(repo.url)
if parsedUrl.netloc == "github.com":
parsed_url = urlparse(repo.url)
if parsed_url.netloc == "github.com":
return f"{repo.url}/blob/{repo.branch}/README.md"
if parsedUrl.netloc in ["gitlab.com", "salsa.debian.org", "framagit.org"]:
if parsed_url.netloc in ["gitlab.com", "salsa.debian.org", "framagit.org"]:
return f"{repo.url}/-/blob/{repo.branch}/README.md"
FreeCAD.Console.PrintLog(
"Unrecognized git repo location '' -- guessing it is a GitLab instance..."
@@ -358,8 +354,8 @@ def get_python_exe() -> str:
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")
fc_dir = FreeCAD.getHomePath()
if not python_exe or python_exe == "Not set" or not os.path.exists(python_exe):
fc_dir = FreeCAD.getHomePath()
python_exe = os.path.join(fc_dir, "bin", "python3")
if "Windows" in platform.system():
python_exe += ".exe"
@@ -409,7 +405,7 @@ def blocking_get(url: str, method=None) -> str:
if FreeCAD.GuiUp and method is None or method == "networkmanager":
NetworkManager.InitializeNetworkManager()
p = NetworkManager.AM_NETWORK_MANAGER.blocking_get(url)
elif has_requests and method is None or method == "requests":
elif requests and method is None or method == "requests":
response = requests.get(url)
if response.status_code == 200:
p = response.text
@@ -420,18 +416,18 @@ def blocking_get(url: str, method=None) -> str:
return p
def run_interruptable_subprocess(args) -> object:
def run_interruptable_subprocess(args) -> subprocess.CompletedProcess:
"""Wrap subprocess call so it can be interrupted gracefully."""
creationflags = 0
creation_flags = 0
if hasattr(subprocess, "CREATE_NO_WINDOW"):
# Added in Python 3.7 -- only used on Windows
creationflags = subprocess.CREATE_NO_WINDOW
creation_flags = subprocess.CREATE_NO_WINDOW
try:
p = subprocess.Popen(
args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
creationflags=creationflags,
creationflags=creation_flags,
text=True,
encoding="utf-8",
)
@@ -447,13 +443,12 @@ def run_interruptable_subprocess(args) -> object:
except subprocess.TimeoutExpired:
if QtCore.QThread.currentThread().isInterruptionRequested():
p.kill()
stdout, stderr = p.communicate()
return_code = -1
raise ProcessInterrupted()
if return_code is None or return_code != 0:
raise subprocess.CalledProcessError(return_code, args, stdout, stderr)
return subprocess.CompletedProcess(args, return_code, stdout, stderr)
def get_main_am_window():
windows = QtWidgets.QApplication.topLevelWidgets()
for widget in windows: