2424
2525""" Utilities to work across different platforms, providers and python versions """
2626
27+ # pylint: disable=deprecated-module, ungrouped-imports
28+
2729from datetime import datetime
2830from typing import Optional , Any , List
2931import os
5254except ImportError :
5355
5456 def get_python_exe ():
57+ """Use shutil.which to find python executable"""
5558 return shutil .which ("python" )
5659
5760
@@ -61,9 +64,17 @@ def get_python_exe():
6164 # loop running this is not possible, so fall back to requests (if available), or the native
6265 # Python urllib.request (if requests is not available).
6366 import NetworkManager # Requires an event loop, so is only available with the GUI
67+
68+ requests = None
69+ ssl = None
70+ urllib = None
6471else :
72+ NetworkManager = None
6573 try :
6674 import requests
75+
76+ ssl = None
77+ urllib = None
6778 except ImportError :
6879 requests = None
6980 import urllib .request
@@ -85,11 +96,14 @@ def get_python_exe():
8596 except ImportError :
8697
8798 def loadUi (ui_file : str ):
99+ """If there are no available versions of QtUiTools, then raise an error if this
100+ method is used."""
88101 raise RuntimeError ("Cannot use QUiLoader without PySide or FreeCAD" )
89102
90103 if has_loader :
91104
92105 def loadUi (ui_file : str ) -> QtWidgets .QWidget :
106+ """Load a Qt UI from an on-disk file."""
93107 q_ui_file = QtCore .QFile (ui_file )
94108 q_ui_file .open (QtCore .QFile .OpenModeFlag .ReadOnly )
95109 loader = QUiLoader ()
@@ -135,10 +149,13 @@ def symlink(source, link_name):
135149
136150
137151def rmdir (path : str ) -> bool :
152+ """Remove a directory or symlink, even if it is read-only."""
138153 try :
139154 if os .path .islink (path ):
140155 os .unlink (path ) # Remove symlink
141156 else :
157+ # NOTE: the onerror argument was deprecated in Python 3.12, replaced by onexc -- replace
158+ # when earlier versions are no longer supported.
142159 shutil .rmtree (path , onerror = remove_readonly )
143160 except (WindowsError , PermissionError , OSError ):
144161 return False
@@ -213,7 +230,7 @@ def get_zip_url(repo):
213230
214231
215232def recognized_git_location (repo ) -> bool :
216- """Returns whether this repo is based at a known git repo location: works with github , gitlab,
233+ """Returns whether this repo is based at a known git repo location: works with GitHub , gitlab,
217234 framagit, and salsa.debian.org"""
218235
219236 parsed_url = urlparse (repo .url )
@@ -395,7 +412,7 @@ def is_float(element: Any) -> bool:
395412
396413
397414def get_pip_target_directory ():
398- # Get the default location to install new pip packages
415+ """ Get the default location to install new pip packages"""
399416 major , minor , _ = platform .python_version_tuple ()
400417 vendor_path = os .path .join (
401418 fci .DataPaths ().mod_dir , ".." , "AdditionalPythonPackages" , f"py{ major } { minor } "
@@ -417,7 +434,12 @@ def blocking_get(url: str, method=None) -> bytes:
417434 succeeded, or an empty string if it failed, or returned no data. The method argument is
418435 provided mainly for testing purposes."""
419436 p = b""
420- if fci .FreeCADGui and method is None or method == "networkmanager" :
437+ if (
438+ fci .FreeCADGui
439+ and method is None
440+ or method == "networkmanager"
441+ and NetworkManager is not None
442+ ):
421443 NetworkManager .InitializeNetworkManager ()
422444 p = NetworkManager .AM_NETWORK_MANAGER .blocking_get (url , 10000 ) # 10 second timeout
423445 if p :
@@ -462,13 +484,13 @@ def run_interruptable_subprocess(args, timeout_secs: int = 10) -> subprocess.Com
462484 # one second timeout allows interrupting the run once per second
463485 stdout , stderr = p .communicate (timeout = 1 )
464486 return_code = p .returncode
465- except subprocess .TimeoutExpired :
487+ except subprocess .TimeoutExpired as timeout_exception :
466488 if (
467489 hasattr (QtCore , "QThread" )
468490 and QtCore .QThread .currentThread ().isInterruptionRequested ()
469491 ):
470492 p .kill ()
471- raise ProcessInterrupted ()
493+ raise ProcessInterrupted () from timeout_exception
472494 if time .time () - start_time >= timeout_secs : # The real timeout
473495 p .kill ()
474496 stdout , stderr = p .communicate ()
@@ -481,9 +503,10 @@ def run_interruptable_subprocess(args, timeout_secs: int = 10) -> subprocess.Com
481503
482504
483505def process_date_string_to_python_datetime (date_string : str ) -> datetime :
484- """For modern macros the expected date format is ISO 8601, YYYY-MM-DD. For older macros this standard was not always
485- used, and various orderings and separators were used. This function tries to match the majority of those older
486- macros. Commonly-used separators are periods, slashes, and dashes."""
506+ """For modern macros the expected date format is ISO 8601, YYYY-MM-DD. For older macros this
507+ standard was not always used, and various orderings and separators were used. This function
508+ tries to match the majority of those older macros. Commonly-used separators are periods,
509+ slashes, and dashes."""
487510
488511 def raise_error (bad_string : str , root_cause : Exception = None ):
489512 raise ValueError (
@@ -499,19 +522,19 @@ def raise_error(bad_string: str, root_cause: Exception = None):
499522 # The earliest possible year an addon can be created or edited is 2001:
500523 if split_result [0 ] > 2000 :
501524 return datetime (split_result [0 ], split_result [1 ], split_result [2 ])
502- elif split_result [2 ] > 2000 :
503- # Generally speaking it's not possible to distinguish between DD-MM and MM-DD, so try the first, and
504- # only if that fails try the second
525+ if split_result [2 ] > 2000 :
526+ # Generally speaking it's not possible to distinguish between DD-MM and MM-DD, so try
527+ # the first, and only if that fails try the second
505528 if split_result [1 ] <= 12 :
506529 return datetime (split_result [2 ], split_result [1 ], split_result [0 ])
507530 return datetime (split_result [2 ], split_result [0 ], split_result [1 ])
508- else :
509- raise ValueError (f"Invalid year in date string '{ date_string } '" )
531+ raise ValueError (f"Invalid year in date string '{ date_string } '" )
510532 except ValueError as exception :
511533 raise_error (date_string , exception )
512534
513535
514536def get_main_am_window ():
537+ """Find the Addon Manager's main window in the Qt widget hierarchy."""
515538 windows = QtWidgets .QApplication .topLevelWidgets ()
516539 for widget in windows :
517540 if widget .objectName () == "AddonManager_Main_Window" :
@@ -540,7 +563,7 @@ def create_pip_call(args: List[str]) -> List[str]:
540563 else :
541564 python_exe = get_python_exe ()
542565 if not python_exe :
543- raise ( RuntimeError ("Could not locate Python executable on this system" ) )
566+ raise RuntimeError ("Could not locate Python executable on this system" )
544567 call_args = [python_exe , "-m" , "pip" , "--disable-pip-version-check" ]
545568 call_args .extend (args )
546569 return call_args
0 commit comments