22
33Creates a single shared venv, installs astropy first then each package
44from packages.yaml in order, recording per-package install outcome
5- (installed / skipped / install-fail / no-spec). Then runs
5+ (installed / skipped / install-fail / no-spec). Each install is
6+ constrained so it can never downgrade a package already in the venv;
7+ a package needing an older version is reported as skipped. Then runs
68`pytest --pyargs <module>` for each successfully-installed package.
79Writes results/<variant>__<python>.json with the full venv freeze and
810per-package data.
@@ -223,6 +225,29 @@ def _freeze(python):
223225 return out
224226
225227
228+ def _write_no_downgrade_constraints (python , path ):
229+ """Pin every installed package to '>=' its current version.
230+
231+ Passed as a uv `--constraint` file to later installs so a new package
232+ can pull its deps forward but never downgrade what the shared venv
233+ already has; a package that genuinely needs an older version then
234+ shows up as a resolver conflict (skipped) instead of silently
235+ poisoning the venv for everything tested afterwards.
236+ """
237+ frozen = _freeze (python )
238+ lines = []
239+ for name , ver in sorted (frozen .items ()):
240+ try :
241+ # Drop any PEP 440 local-version segment (e.g. the '+g1a2b3c4'
242+ # on astropy/pyerfa nightly wheels): uv rejects a '>=' specifier
243+ # with a local segment and would fail to parse the whole file.
244+ public = Version (ver ).public
245+ except InvalidVersion :
246+ continue
247+ lines .append (f"{ name } >={ public } " )
248+ Path (path ).write_text ("\n " .join (lines ) + ("\n " if lines else "" ))
249+
250+
226251def _load_packages (path ):
227252 raw = yaml .safe_load (Path (path ).read_text ()) or {}
228253 return list (raw .get ("packages" , []))
@@ -313,6 +338,7 @@ def run_variant(variant, python_version, packages, repo_root, results_dir, timeo
313338 return result , out_path
314339 python = os .path .join (venv , "bin" , "python" )
315340 result ["python_version" ] = _venv_python_version (python )
341+ constraints_path = os .path .join (tmpdir , "no-downgrade-constraints.txt" )
316342
317343 common = ["uv" , "pip" , "install" , "--python" , python , "-q" ]
318344 for url in astropy ["extra_index_urls" ]:
@@ -339,6 +365,7 @@ def run_variant(variant, python_version, packages, repo_root, results_dir, timeo
339365 result ["astropy" ]["version" ] = (
340366 _pkg_version (python , "astropy" ) or result ["astropy" ]["version" ]
341367 )
368+ _write_no_downgrade_constraints (python , constraints_path )
342369
343370 installed_pkgs = []
344371 for pkg , install_spec , target_version in pkg_specs :
@@ -364,13 +391,18 @@ def run_variant(variant, python_version, packages, repo_root, results_dir, timeo
364391 continue
365392
366393 print (f"\n Installing { pkg ['pypi_name' ]} ..." )
367- install_cmd = common + [install_spec ] + (pkg .get ("extra_deps" ) or [])
394+ install_cmd = (
395+ common
396+ + ["--constraint" , constraints_path , install_spec ]
397+ + (pkg .get ("extra_deps" ) or [])
398+ )
368399 rc , err = _run_install (install_cmd , timeouts ["install" ])
369400 if rc == 0 :
370401 entry ["install_status" ] = status .INSTALLED
371402 entry ["resolved_version" ] = _pkg_version (python , pkg ["pypi_name" ])
372403 print (f" installed at { entry ['resolved_version' ]} " )
373404 installed_pkgs .append ((pkg , entry ))
405+ _write_no_downgrade_constraints (python , constraints_path )
374406 else :
375407 entry ["install_error" ] = err
376408 if _resolver_conflict (err ):
@@ -467,12 +499,20 @@ def run(args):
467499 # instead of buffering until the script exits.
468500 sys .stdout .reconfigure (line_buffering = True )
469501
470- packages = _load_packages (args .config )
502+ all_packages = _load_packages (args .config )
503+ packages = all_packages
471504 if args .tiers :
472505 wanted_tiers = {t .strip () for t in args .tiers .split ("," ) if t .strip ()}
473506 packages = [p for p in packages if p .get ("tier" , "coordinated" ) in wanted_tiers ]
474507 if args .packages :
475508 wanted = {n .strip () for n in args .packages .split ("," ) if n .strip ()}
509+ known = {p ["pypi_name" ] for p in all_packages }
510+ unknown = wanted - known
511+ if unknown :
512+ sys .exit (
513+ f"Unknown package name(s): { ', ' .join (sorted (unknown ))} . "
514+ f"Valid names are the pypi_name entries in { args .config } ."
515+ )
476516 packages = [p for p in packages if p ["pypi_name" ] in wanted ]
477517 packages = _install_order (packages )
478518
0 commit comments