1515import sys as _sys
1616import tempfile as _tempfile
1717import xml .dom .minidom as _xml_minidom
18- import xml .etree .ElementTree as _xml_ET
18+ import xml .etree .ElementTree as _ET # noqa: N814, ICN001
1919from pathlib import Path as _Path
2020from typing import TYPE_CHECKING as _TYPE_CHECKING
2121
8686"""
8787
8888
89- def run (
89+ def run ( # noqa: PLR0913
9090 packages : Sequence [str | dict ],
91+ * ,
9192 python_version : str | None = None ,
9293 build_platform : PlatformName | None = None ,
9394 target_platform : PlatformName | None = None ,
@@ -111,11 +112,19 @@ def run(
111112 indent_xml : int | None = 4 ,
112113 indent_yaml : int | None = 2 ,
113114 filepath : str | _Path = ".github/.repodynamics/metadata.json" ,
114- ):
115+ ) -> tuple [ dict [ SourceName , list [ dict ]], dict [ SourceName , str ], dict [ SourceName , str ]] :
115116 """Generate and/or install package dependencies based on the given configurations."""
116- dependencies , files = get_dependencies (
117+ filepath = _Path (filepath ).resolve ()
118+ if not filepath .is_file ():
119+ error_msg = f"Metadata file not found at '{ filepath } '"
120+ raise FileNotFoundError (error_msg )
121+ try :
122+ data = _json .loads (filepath .read_text ())
123+ except _json .JSONDecodeError as e :
124+ error_msg = f"Failed to load dependencies from '{ filepath } '"
125+ raise ValueError (error_msg ) from e
126+ dependencies , files = DependencyInstaller (data ).run (
117127 packages = packages ,
118- filepath = filepath ,
119128 python_version = python_version ,
120129 build_platform = build_platform ,
121130 target_platform = target_platform ,
@@ -148,44 +157,6 @@ def run(
148157 return dependencies , files , paths
149158
150159
151- def get_dependencies (
152- packages : Sequence [str | dict ],
153- python_version : str | None = None ,
154- build_platform : PlatformName | None = None ,
155- target_platform : PlatformName | None = None ,
156- sources : Sequence [SourceName ] | None = None ,
157- exclude_sources : Sequence [SourceName ] | None = None ,
158- exclude_installed : bool = True ,
159- pip_in_conda : bool = True ,
160- conda_env_name : str | None = None ,
161- indent_json : int | None = 4 ,
162- indent_xml : int | None = 4 ,
163- indent_yaml : int | None = 2 ,
164- filepath : str | _Path = ".github/.repodynamics/metadata.json" ,
165- ) -> tuple [dict [SourceName , list [dict ]], dict [SourceName , str ]]:
166- filepath = _Path (filepath ).resolve ()
167- if not filepath .is_file ():
168- raise FileNotFoundError (f"Metadata file not found at '{ filepath } '" )
169- try :
170- data = _json .loads (filepath .read_text ())
171- except _json .JSONDecodeError as e :
172- raise ValueError (f"Failed to load dependencies from '{ filepath } '" ) from e
173- return DependencyInstaller (data ).run (
174- packages = packages ,
175- python_version = python_version ,
176- build_platform = build_platform ,
177- target_platform = target_platform ,
178- sources = sources ,
179- exclude_sources = exclude_sources ,
180- exclude_installed = exclude_installed ,
181- pip_in_conda = pip_in_conda ,
182- conda_env_name = conda_env_name ,
183- indent_json = indent_json ,
184- indent_xml = indent_xml ,
185- indent_yaml = indent_yaml ,
186- )
187-
188-
189160def install_files (
190161 files : dict [SourceName , str ],
191162 cmd_bash : Sequence [str ] = ("bash" , "{filepath}" ),
@@ -214,29 +185,30 @@ def install_files(
214185 inputs = locals ()
215186
216187 def _install (content : str , cmd : Sequence [str ]):
217- file = _tempfile .NamedTemporaryFile (mode = "w" , delete = False )
218- file .write (content )
219- filepath = file .name
220- cmd_filled = [cmd_part .format (filepath = filepath ) for cmd_part in cmd ]
221- try :
222- _subprocess .run (cmd_filled , check = True )
223- finally :
224- _Path (filepath ).unlink ()
188+ with _tempfile .NamedTemporaryFile (mode = "w" , delete = False ) as file :
189+ file .write (content )
190+ filepath = file .name
191+ cmd_filled = [cmd_part .format (filepath = filepath ) for cmd_part in cmd ]
192+ try :
193+ _subprocess .run (cmd_filled , check = True ) # noqa: S603
194+ finally :
195+ _Path (filepath ).unlink ()
225196 return
226197
227198 for source in ("bash" , "pwsh" , "apt" , "brew" , "choco" , "winget" , "conda" , "pip" ):
228199 if source not in files :
229200 continue
230201 if source == "apt" :
231- _subprocess .run (list (cmd_apt ) + files [source ].splitlines (), check = True )
202+ _subprocess .run (list (cmd_apt ) + files [source ].splitlines (), check = True ) # noqa: S603
232203 else :
233204 _install (files [source ], inputs [f"cmd_{ source } " ])
234205 return
235206
236207
237- def write_files (
208+ def write_files ( # noqa: PLR0913
238209 files : dict [SourceName , str ],
239210 output_dir : str | _Path ,
211+ * ,
240212 overwrite : bool = False ,
241213 filename_conda : str = "environment.yml" ,
242214 filename_pip : str = "requirements.txt" ,
@@ -256,7 +228,8 @@ def _write_file(filename: str, dep_content: str):
256228 if not filepath .exists () or overwrite :
257229 filepath .write_text (dep_content )
258230 else :
259- raise FileExistsError (f"File already exists: '{ filepath } '" )
231+ error_msg = f"File already exists: '{ filepath } '"
232+ raise FileExistsError (error_msg )
260233 return
261234
262235 inputs = locals ()
@@ -274,9 +247,10 @@ def __init__(self, package_data: dict):
274247 self ._data = package_data
275248 return
276249
277- def run (
250+ def run ( # noqa: PLR0913
278251 self ,
279252 packages : Sequence [str | dict ],
253+ * ,
280254 build_platform : PlatformName | None = None ,
281255 target_platform : PlatformName | None = None ,
282256 python_version : str | None = None ,
@@ -376,14 +350,16 @@ def _resolve_python_version(packages: list[dict], python_version: str | None = N
376350 python_version = "." .join (_sys .version_info [:2 ])
377351 if python_version not in ("latest" , "earliest" ):
378352 if python_version not in common_python_versions :
379- raise ValueError (f"Python version '{ python_version } ' is not supported." )
353+ error_msg = f"Python version '{ python_version } ' is not supported."
354+ raise ValueError (error_msg )
380355 return python_version
381356 python_versions = sorted (common_python_versions , key = lambda x : tuple (map (int , x .split ("." ))))
382357 return python_versions [- 1 ] if python_version == "latest" else python_versions [0 ]
383358
384359
385- def _resolve_dependencies (
360+ def _resolve_dependencies ( # noqa: PLR0912
386361 pkg : dict ,
362+ * ,
387363 python_version : str ,
388364 extras : Sequence [str ] | Literal ["all" ] | None = "all" ,
389365 build_platform : PlatformName | None = None ,
@@ -425,10 +401,6 @@ def _resolve_dependencies(
425401 Whether to exclude dependencies that are already installed.
426402 This is determined by running the validator script
427403 provided in the dependency data.
428-
429- Returns
430- -------
431-
432404 """
433405 if not build_platform :
434406 build_platform = _get_native_platform ()
@@ -463,8 +435,10 @@ def _resolve_dependencies(
463435 if selector and not _evaluate_selector (selector , selector_vars ):
464436 continue
465437 if exclude_installed and dependency .get ("validator" ):
466- validator_result = _subprocess .run (
467- ["python" , "-c" , dependency ["validator" ]], capture_output = True , check = False
438+ validator_result = _subprocess .run ( # noqa: S603
439+ ["python" , "-c" , dependency ["validator" ]], # noqa: S607
440+ capture_output = True ,
441+ check = False ,
468442 )
469443 if validator_result .returncode == 0 :
470444 continue
@@ -473,10 +447,11 @@ def _resolve_dependencies(
473447 out .setdefault (source , []).append (dependency )
474448 break
475449 else :
476- raise ValueError (
450+ error_msg = (
477451 f"Dependency '{ dependency ['name' ]} ' not installable from any source. "
478452 f"Available sources are: { list (dependency ['install' ].keys ())} "
479453 )
454+ raise ValueError (error_msg )
480455 return out
481456
482457
@@ -501,14 +476,15 @@ def _collect_dependencies(
501476 if extras != "all" :
502477 for extra in extras :
503478 if extra not in optional_group_keys :
504- raise ValueError (f"Invalid optional dependency group: { extra } " )
505- for group_key , group in data .get ("optional" , {}).items ():
479+ error_msg = f"Invalid optional dependency group: { extra } "
480+ raise ValueError (error_msg )
481+ for group in data .get ("optional" , {}).values ():
506482 if extras == "all" or group ["name" ] in extras :
507483 deps .extend (list (group ["package" ].values ()))
508484 return _copy .deepcopy (deps )
509485
510486
511- def _resolve_variants (
487+ def _resolve_variants ( # noqa: PLR0912, C901
512488 pkg : dict , pyver : str , input_variants : dict [str , str | int | bool ] | None = None
513489) -> dict :
514490 """Get a full set of variant values based on input variants and project variant data."""
@@ -519,9 +495,11 @@ def _resolve_variants(
519495 # Validate input variants
520496 for variant_key , variant_value in input_variants .items ():
521497 if variant_key not in pkg_vars :
522- raise ValueError (f"Invalid variant key '{ variant_key } '" )
498+ error_msg = f"Invalid variant key '{ variant_key } '"
499+ raise ValueError (error_msg )
523500 if variant_value not in pkg_vars [variant_key ]:
524- raise ValueError (f"Invalid variant value '{ variant_value } ' for key '{ variant_key } '" )
501+ error_msg = f"Invalid variant value '{ variant_value } ' for key '{ variant_key } '"
502+ raise ValueError (error_msg )
525503 for zip_keys in pkg_zip_keys :
526504 input_keys = []
527505 input_indices = []
@@ -530,9 +508,8 @@ def _resolve_variants(
530508 input_keys .append (zip_key )
531509 input_indices .append (pkg_vars [zip_key ].index (input_variants [zip_key ]))
532510 if len (input_indices ) > 1 and len (set (input_indices )) != 1 :
533- raise ValueError (
534- f"Variant keys '{ input_keys } ' must be zipped, but values correspond to indices { input_indices } "
535- )
511+ error_msg = f"Variant keys '{ input_keys } ' must be zipped, but values correspond to indices { input_indices } "
512+ raise ValueError (error_msg )
536513 output = {}
537514 # Set the variant values
538515 for pkg_var_key , pkg_var_items in pkg_vars .items ():
@@ -580,7 +557,7 @@ def _evaluate_selector(selector: str, selector_vars: dict[str, str | int | bool]
580557 result
581558 Result of the selector evaluation.
582559 """
583- return eval (selector , selector_vars )
560+ return eval (selector , selector_vars ) # noqa: S307
584561
585562
586563def _get_native_platform () -> PlatformName :
@@ -620,7 +597,8 @@ def _get_native_platform() -> PlatformName:
620597 machine = _platform .machine ()
621598 platform = _platform_map .get (_sys .platform )
622599 if not platform :
623- raise RuntimeError (f"Unknown current platform: { _sys .platform } " )
600+ error_msg = f"Unknown current platform: { _sys .platform } "
601+ raise RuntimeError (error_msg )
624602 if machine in non_x86_machines :
625603 return f"{ platform } -{ machine } "
626604 if platform == "zos" :
@@ -635,9 +613,7 @@ def _create_env_file_conda(
635613 env_name : str | None = None ,
636614 indent : int | None = 2 ,
637615) -> str :
638- """Create a Conda
639- [environment.yml](https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-file-manually)
640- file.
616+ """Create a Conda [environment.yml](https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-file-manually) file.
641617
642618 Parameters
643619 ----------
@@ -660,18 +636,15 @@ def _create_env_file_conda(
660636 if env_name :
661637 lines .append (f"name: { env_name } " )
662638 lines .append ("dependencies:" )
663- for match_spec in sorted (match_specs ):
664- lines .append (f"{ ' ' * indent } - { match_spec } " )
639+ lines .extend ([f"{ ' ' * indent } - { match_spec } " for match_spec in sorted (match_specs )])
665640 if pip_packages :
666641 lines .append (f"{ ' ' * indent } - pip:" )
667- for pkg in pip_packages :
668- lines .append (f"{ ' ' * (indent * 2 )} - { pkg ['spec' ]} " )
642+ lines .extend ([f"{ ' ' * (indent * 2 )} - { pkg ['spec' ]} " for pkg in pip_packages ])
669643 return f"{ '\n ' .join (lines )} \n "
670644
671645
672646def _create_env_file_pip (packages : list [dict ]) -> str :
673- """Create a pip
674- [requirements.txt](https://pip.pypa.io/en/stable/user_guide/#requirements-files) file.
647+ """Create a pip [requirements.txt](https://pip.pypa.io/en/stable/user_guide/#requirements-files) file.
675648
676649 Parameters
677650 ----------
@@ -713,16 +686,16 @@ def _create_env_file_brew(packages: list[dict]) -> str:
713686 if "tap" in pkg :
714687 out .setdefault ("tap" , []).append (pkg ["tap" ])
715688 out .setdefault (pkg ["type" ], []).append (pkg ["spec" ])
716- sections = []
717- for section in ("tap" , "brew" , "cask" , "mas" , "whalebrew" , "vscode" ):
718- if section in out :
719- sections .append ("\n " .join ([f"{ section } : { spec } " for spec in sorted (out [section ])]))
689+ sections = [
690+ "\n " .join ([f"{ section } : { spec } " for spec in sorted (out [section ])])
691+ for section in ("tap" , "brew" , "cask" , "mas" , "whalebrew" , "vscode" )
692+ if section in out
693+ ]
720694 return f"{ '\n \n ' .join (sections )} \n "
721695
722696
723697def _create_env_file_choco (packages : list [dict ], indent : int | None = 4 ) -> str :
724- """Create a Chocolatey
725- [packages.config](https://docs.chocolatey.org/en-us/choco/commands/install/#packagesconfig) file.
698+ """Create a Chocolatey [packages.config](https://docs.chocolatey.org/en-us/choco/commands/install/#packagesconfig) file.
726699
727700 Parameters
728701 ----------
@@ -734,15 +707,15 @@ def _create_env_file_choco(packages: list[dict], indent: int | None = 4) -> str:
734707 Number of spaces to use for indentation.
735708 If `None`, a compact format is used with no indentation or newlines.
736709 """
737- root = _xml_ET .Element ("packages" )
710+ root = _ET .Element ("packages" )
738711 for pkg in packages :
739- package_element = _xml_ET .SubElement (root , "package" )
712+ package_element = _ET .SubElement (root , "package" )
740713 for key , value in pkg .items ():
741714 if value is not None and value not in ("homepage" ,):
742715 package_element .set (_snake_case_to_camel_case (key ), str (value ))
743- xml_str = _xml_ET .tostring (root , encoding = "utf-8" )
716+ xml_str = _ET .tostring (root , encoding = "utf-8" )
744717 # Format the XML string to add indentation
745- parsed_xml = _xml_minidom .parseString (xml_str )
718+ parsed_xml = _xml_minidom .parseString (xml_str ) # noqa: S318
746719 if indent is None :
747720 # Produce a single-line XML with no newlines or indentation
748721 formatted_xml = parsed_xml .toxml (encoding = "utf-8" )
@@ -753,9 +726,7 @@ def _create_env_file_choco(packages: list[dict], indent: int | None = 4) -> str:
753726
754727
755728def _create_env_file_winget (packages : list [dict ], indent : int | None = 4 ) -> str :
756- """Create a winget
757- [packages.json](https://github.com/microsoft/winget-cli/blob/master/schemas/JSON/packages/packages.schema.2.0.json)
758- file.
729+ """Create a winget [packages.json](https://github.com/microsoft/winget-cli/blob/master/schemas/JSON/packages/packages.schema.2.0.json) file.
759730
760731 Parameters
761732 ----------
@@ -791,12 +762,7 @@ def _snake_case_to_camel_case(string: str) -> str:
791762 return "" .join ([components [0 ]] + [x .title () for x in components [1 :]])
792763
793764
794- def _parse_args ():
795- def parse_extras (value ):
796- if value .lower () == "all" :
797- return "all"
798- return value .split ("," ) if "," in value else [value ]
799-
765+ def _parse_args () -> _argparse .Namespace :
800766 parser = _argparse .ArgumentParser (description = "Install package and/or test-suite dependencies." )
801767 parser .add_argument ("--filepath" , type = str , default = ".github/.repodynamics/metadata.json" )
802768 parser .add_argument (
0 commit comments