nab-python 0.0.4__tar.gz → 0.0.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nab_python-0.0.4 → nab_python-0.0.5}/PKG-INFO +3 -3
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/canary.py +3 -1
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios.py +6 -1
- {nab_python-0.0.4 → nab_python-0.0.5}/pyproject.toml +3 -3
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_lockfile/pylock.py +66 -1
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_lockfile/requirements.py +13 -5
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_provider/extras.py +33 -24
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_provider/listing.py +19 -12
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_provider/metadata_resolver.py +5 -17
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_provider/priority.py +3 -1
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vcs_admission.py +7 -6
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/download.py +4 -2
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/fetch.py +33 -6
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/lockfile.py +2 -1
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/metadata.py +24 -2
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/provider.py +11 -12
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/resolve.py +2 -4
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/universal/matrix.py +11 -4
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/universal/reresolve.py +11 -1
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/universal/validate.py +49 -11
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/universal/wheel_selection.py +22 -2
- nab_python-0.0.5/tests/property_python/test_build_policy_never.py +151 -0
- nab_python-0.0.5/tests/property_python/test_download_hashes.py +123 -0
- nab_python-0.0.5/tests/property_python/test_fetch_coordinator.py +262 -0
- nab_python-0.0.5/tests/property_python/test_vcs_admission.py +193 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_cached_client.py +8 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_config.py +14 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_download.py +30 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_fetch.py +106 -2
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_local_index.py +24 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_lockfile.py +124 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_metadata.py +22 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_provider.py +260 -39
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_resolve.py +9 -22
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_resolver_packaging.py +18 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_vcs_admission.py +17 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/universal/test_matrix.py +10 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/universal/test_reresolve.py +51 -3
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/universal/test_validate.py +160 -11
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/universal/test_wheel_selection.py +24 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/.gitignore +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/LICENSE +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/README.md +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/README.md +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/_profile_runner.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/cache/.gitignore +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/canary_results/.gitignore +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/compare.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/results/.gitignore +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/ai-stack-lowest-direct.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/ai-stack-lowest.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/ai-stack.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/airflow-lowest-direct.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/airflow-lowest.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/airflow.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/big-packages-lowest-direct.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/big-packages-lowest.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/big-packages.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/cross-tracker-lowest-direct.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/cross-tracker-lowest.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/cross-tracker.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/ecosystem-lowest-direct.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/ecosystem-lowest.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/ecosystem.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/forums-lowest-direct.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/forums-lowest.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/forums.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/pdm-lowest-direct.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/pdm-lowest.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/pdm.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/pip-lowest-direct.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/pip-lowest.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/pip.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/poetry-lowest-direct.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/poetry-lowest.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/poetry.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/pytorch-lowest-direct.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/pytorch-lowest.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/pytorch.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/quick-lowest-direct.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/quick-lowest.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/quick.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/rip-lowest-direct.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/rip-lowest.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/rip.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/universal.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/unsupported-lowest-direct.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/unsupported-lowest.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/unsupported.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/uv-lowest-direct.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/uv-lowest.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/scenarios/uv.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/strategy_sweep.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/strategy_sweep_results/summary.json +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/strategy_sweep_summary.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/universal_scenarios.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/benchmarks/universal_summary.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/__init__.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_build/__init__.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_build/env.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_build/errors.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_build/runner.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_conflict_kind.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_lockfile/__init__.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_lockfile/builder.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_lockfile/disjointness.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_packaging_provider.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_provider/__init__.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_provider/build_remote.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_provider/lookahead.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_provider/sources.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_testing/__init__.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_testing/coordinator_fake.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_toml.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/__init__.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/LICENSE +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/LICENSE.APACHE +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/LICENSE.BSD +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/PROVENANCE.md +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/__init__.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/_elffile.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/_manylinux.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/_musllinux.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/_parser.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/_range_utils.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/_structures.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/_tokenizer.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/_version_utils.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/dependency_groups.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/direct_url.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/errors.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/licenses/__init__.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/licenses/_spdx.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/markers.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/metadata.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/py.typed +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/pylock.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/ranges.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/requirements.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/specifiers.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/tags.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/utils.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/_vendor/packaging/version.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/build_backend.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/config.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/py.typed +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/requirements_file.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/universal/__init__.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/universal/provider.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/universal/resolve.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/src/nab_python/workspace.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/__init__.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/property_python/__init__.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/property_python/strategies.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/property_python/test_extras_pep685.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/property_python/test_lockfile_pep751.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/property_python/test_marker_overlay.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/property_python/test_multi_index_pep503.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/property_python/test_resolver_pep440.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/ruff.toml +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_async_transports.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_build_backend.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_build_runner.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_cache.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_multi_index.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_requirements_file.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_simple_client_filenames.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_simple_client_hashes.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_vcs.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/test_workspace.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/universal/__init__.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/universal/property_universal/__init__.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/universal/property_universal/strategies.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/universal/property_universal/test_alignment.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/universal/property_universal/test_matrix.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/universal/property_universal/test_provider_pep425.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/universal/property_universal/test_validate.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/universal/test_resolve.py +0 -0
- {nab_python-0.0.4 → nab_python-0.0.5}/tests/universal/test_universal_provider.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nab-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: Index-backed provider, lockfile emitter, and downloader for nab
|
|
5
5
|
Project-URL: Homepage, https://github.com/notatallshaw/nab
|
|
6
6
|
Project-URL: Documentation, https://nab.readthedocs.io/
|
|
@@ -20,8 +20,8 @@ Classifier: Typing :: Typed
|
|
|
20
20
|
Requires-Python: >=3.10
|
|
21
21
|
Requires-Dist: build>=1.2
|
|
22
22
|
Requires-Dist: installer>=0.7
|
|
23
|
-
Requires-Dist: nab-index==0.0.
|
|
24
|
-
Requires-Dist: nab-resolver==0.0.
|
|
23
|
+
Requires-Dist: nab-index==0.0.5
|
|
24
|
+
Requires-Dist: nab-resolver==0.0.5
|
|
25
25
|
Requires-Dist: pyproject-hooks>=1.2
|
|
26
26
|
Requires-Dist: tomli-w>=1.2
|
|
27
27
|
Requires-Dist: tomli>=2.0
|
|
@@ -368,8 +368,10 @@ def median_run(scenario: dict, runs: int) -> tuple[list[dict], dict]:
|
|
|
368
368
|
else None
|
|
369
369
|
)
|
|
370
370
|
uploaded_prior_to = parse_datetime(datetime_str) if datetime_str else None
|
|
371
|
+
# See scenarios.py: trust pre-2.2 sdist PKG-INFO deps by default so the
|
|
372
|
+
# benchmark measures search, not strict PEP 643 sdist rejection.
|
|
371
373
|
trust_unverified_sdist_deps = bool(
|
|
372
|
-
scenario.get("trust_unverified_sdist_deps",
|
|
374
|
+
scenario.get("trust_unverified_sdist_deps", True)
|
|
373
375
|
)
|
|
374
376
|
|
|
375
377
|
runs_data: list[dict] = [
|
|
@@ -543,8 +543,13 @@ def process_scenario(
|
|
|
543
543
|
f" got {resolution_raw!r}"
|
|
544
544
|
)
|
|
545
545
|
raise ValueError(msg) from exc
|
|
546
|
+
# Trust a pre-2.2 sdist's PKG-INFO deps by default. The benchmark measures
|
|
547
|
+
# resolver search, and under BuildPolicy.NEVER the strict PEP 643 product
|
|
548
|
+
# default rejects every sdist-only pre-2.2 pin (UnsupportedSdistError),
|
|
549
|
+
# dropping in-window versions uv resolves by building. A scenario sets this
|
|
550
|
+
# false to exercise strict behavior.
|
|
546
551
|
trust_unverified_sdist_deps: bool = scenario.get(
|
|
547
|
-
"trust_unverified_sdist_deps",
|
|
552
|
+
"trust_unverified_sdist_deps", True
|
|
548
553
|
)
|
|
549
554
|
optional_dependencies: dict[str, list[str]] = scenario.get(
|
|
550
555
|
"optional_dependencies", {}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nab-python"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.5"
|
|
4
4
|
description = "Index-backed provider, lockfile emitter, and downloader for nab"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -19,8 +19,8 @@ classifiers = [
|
|
|
19
19
|
"Typing :: Typed",
|
|
20
20
|
]
|
|
21
21
|
dependencies = [
|
|
22
|
-
"nab-resolver==0.0.
|
|
23
|
-
"nab-index==0.0.
|
|
22
|
+
"nab-resolver==0.0.5",
|
|
23
|
+
"nab-index==0.0.5",
|
|
24
24
|
"tomli>=2.0",
|
|
25
25
|
"tomli_w>=1.2",
|
|
26
26
|
"build>=1.2",
|
|
@@ -45,11 +45,26 @@ if TYPE_CHECKING:
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
__all__ = [
|
|
48
|
+
"DivergentBaseDependencyError",
|
|
48
49
|
"build_pylock",
|
|
49
50
|
"write_lock",
|
|
50
51
|
]
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
class DivergentBaseDependencyError(ValueError):
|
|
55
|
+
"""An environment's conflict forks disagree on a base dependency's pin.
|
|
56
|
+
|
|
57
|
+
A base dependency present in every fork of an environment drops its
|
|
58
|
+
membership clause so it installs even when no conflicting member is
|
|
59
|
+
selected, which requires the forks to agree on one (version, source).
|
|
60
|
+
When they diverge, every candidate entry keeps a membership clause,
|
|
61
|
+
so nothing would fire in the no-member install context and the
|
|
62
|
+
dependency would silently not install. Surface the divergence with
|
|
63
|
+
the offending package and per-fork pins so the producer can
|
|
64
|
+
reconcile the forks rather than commit an incomplete lock.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
|
|
53
68
|
def write_lock(
|
|
54
69
|
lock_input: LockInput,
|
|
55
70
|
*,
|
|
@@ -317,7 +332,9 @@ def _build_per_tuple_packages(lock_input: LockInput, lock_dir: Path) -> list[Pac
|
|
|
317
332
|
not by the base is absent from that set, so it keeps the
|
|
318
333
|
membership clause and does not install when no member is selected.
|
|
319
334
|
See :class:`LockInput.env_base_names` for the missing-signature
|
|
320
|
-
contract.
|
|
335
|
+
contract. Forks of one environment that disagree on a base
|
|
336
|
+
dependency's pin raise :class:`DivergentBaseDependencyError`
|
|
337
|
+
instead of emitting a lock whose no-member context misses it.
|
|
321
338
|
"""
|
|
322
339
|
out: list[Package] = []
|
|
323
340
|
by_name = _group_by_name(lock_input.per_tuple_pins)
|
|
@@ -330,6 +347,14 @@ def _build_per_tuple_packages(lock_input: LockInput, lock_dir: Path) -> list[Pac
|
|
|
330
347
|
)
|
|
331
348
|
for canonical_name, per_tuple in by_name.items():
|
|
332
349
|
groups = _group_pins_by_pin(per_tuple)
|
|
350
|
+
_check_base_fork_agreement(
|
|
351
|
+
canonical_name,
|
|
352
|
+
per_tuple,
|
|
353
|
+
groups,
|
|
354
|
+
env_signatures,
|
|
355
|
+
env_fork_counts,
|
|
356
|
+
lock_input.env_base_names,
|
|
357
|
+
)
|
|
333
358
|
for pins, tuple_labels in groups:
|
|
334
359
|
marker = _build_marker(
|
|
335
360
|
canonical_name,
|
|
@@ -452,6 +477,46 @@ def _merge_pins_in_group(pins: list[PinShape]) -> PinShape:
|
|
|
452
477
|
)
|
|
453
478
|
|
|
454
479
|
|
|
480
|
+
def _check_base_fork_agreement(
|
|
481
|
+
name: str,
|
|
482
|
+
per_tuple: Mapping[str, PinShape],
|
|
483
|
+
groups: list[tuple[list[PinShape], list[str]]],
|
|
484
|
+
env_signatures: Mapping[str, tuple[tuple[str, str], ...]],
|
|
485
|
+
env_fork_counts: Mapping[tuple[tuple[str, str], ...], int],
|
|
486
|
+
env_base_names: Mapping[tuple[tuple[str, str], ...], frozenset[str]],
|
|
487
|
+
) -> None:
|
|
488
|
+
"""Reject a base dep whose forks within one env pin it differently.
|
|
489
|
+
|
|
490
|
+
The env-only collapse in :func:`_build_marker` needs a single
|
|
491
|
+
(version, source) group spanning every fork of the environment.
|
|
492
|
+
Divergent pins split the forks across groups, so every entry would
|
|
493
|
+
keep its membership clause and none would fire when no member is
|
|
494
|
+
selected: the base dependency would silently not install.
|
|
495
|
+
"""
|
|
496
|
+
by_env: defaultdict[tuple[tuple[str, str], ...], list[str]] = defaultdict(list)
|
|
497
|
+
for label in sorted(per_tuple.keys() & env_signatures.keys()):
|
|
498
|
+
by_env[env_signatures[label]].append(label)
|
|
499
|
+
for signature, labels in by_env.items():
|
|
500
|
+
if len(labels) < env_fork_counts[signature]:
|
|
501
|
+
continue
|
|
502
|
+
if name not in env_base_names.get(signature, frozenset()):
|
|
503
|
+
continue
|
|
504
|
+
in_env = set(labels)
|
|
505
|
+
widest = max(
|
|
506
|
+
sum(1 for label in group_labels if label in in_env)
|
|
507
|
+
for _, group_labels in groups
|
|
508
|
+
)
|
|
509
|
+
if widest >= env_fork_counts[signature]:
|
|
510
|
+
continue
|
|
511
|
+
forks = ", ".join(f"{label} -> {per_tuple[label].version}" for label in labels)
|
|
512
|
+
msg = (
|
|
513
|
+
f"{name}: the conflict forks of one environment pin this base"
|
|
514
|
+
f" dependency differently ({forks}); no lockfile entry would"
|
|
515
|
+
" install it when no conflicting member is selected"
|
|
516
|
+
)
|
|
517
|
+
raise DivergentBaseDependencyError(msg)
|
|
518
|
+
|
|
519
|
+
|
|
455
520
|
def _build_marker(
|
|
456
521
|
name: str,
|
|
457
522
|
tuple_labels: Sequence[str],
|
|
@@ -33,8 +33,10 @@ def write_requirements_with_hashes(
|
|
|
33
33
|
Each line is ``name==version`` followed by one ``--hash=sha256:...``
|
|
34
34
|
per recorded artefact, in the format pip's hash-checking mode
|
|
35
35
|
accepts. Local and VCS pins are emitted as ``name @ <url>`` lines
|
|
36
|
-
without hashes (pip does not hash-check those forms)
|
|
37
|
-
|
|
36
|
+
without hashes (pip does not hash-check those forms); an editable
|
|
37
|
+
local pin renders as ``-e <url>`` and a ``subdirectory`` as a
|
|
38
|
+
``#subdirectory=`` fragment. Returns the text and, when
|
|
39
|
+
``output_path`` is provided, atomically writes it.
|
|
38
40
|
"""
|
|
39
41
|
return _render_requirements(lock_input, with_hashes=True, output_path=output_path)
|
|
40
42
|
|
|
@@ -45,8 +47,8 @@ def write_requirements_without_hashes(
|
|
|
45
47
|
"""Render ``lock_input`` as a plain ``name==version`` list.
|
|
46
48
|
|
|
47
49
|
Same shape as :func:`write_requirements_with_hashes` but without
|
|
48
|
-
the ``--hash=sha256:...`` lines. Local and VCS pins
|
|
49
|
-
|
|
50
|
+
the ``--hash=sha256:...`` lines. Local and VCS pins render the
|
|
51
|
+
same in both variants. Returns the text and, when ``output_path``
|
|
50
52
|
is provided, atomically writes it.
|
|
51
53
|
"""
|
|
52
54
|
return _render_requirements(lock_input, with_hashes=False, output_path=output_path)
|
|
@@ -97,7 +99,13 @@ def _render_pins(pins: Mapping[str, PinShape], *, with_hashes: bool) -> list[str
|
|
|
97
99
|
if isinstance(pin, IndexPin):
|
|
98
100
|
lines.extend(_render_index_pin(pin, with_hashes=with_hashes))
|
|
99
101
|
elif isinstance(pin, LocalPin):
|
|
100
|
-
|
|
102
|
+
url = Path(pin.path).resolve().as_uri()
|
|
103
|
+
if pin.subdirectory is not None:
|
|
104
|
+
url += f"#subdirectory={pin.subdirectory}"
|
|
105
|
+
if pin.editable:
|
|
106
|
+
lines.append(f"-e {url}")
|
|
107
|
+
else:
|
|
108
|
+
lines.append(f"{pin.name} @ {url}")
|
|
101
109
|
elif isinstance(pin, VcsPin):
|
|
102
110
|
lines.append(f"{pin.name} @ {pin.repo_url}")
|
|
103
111
|
else: # pragma: no cover - exhaustive
|
|
@@ -76,17 +76,23 @@ def _pick_in_mode(
|
|
|
76
76
|
) -> Version | None:
|
|
77
77
|
"""Pick a candidate honoring ``ExtrasMode``.
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Missing-metadata cases (no PEP 658, no sdist)
|
|
86
|
-
|
|
79
|
+
Fetches base metadata so an extraction failure (unparseable
|
|
80
|
+
PKG-INFO, or an sdist build the policy disallows) becomes a
|
|
81
|
+
candidate skip instead of a fatal error during the later
|
|
82
|
+
dependency fetch. BACKTRACK mode additionally checks
|
|
83
|
+
``Provides-Extra`` for transitive extras.
|
|
84
|
+
|
|
85
|
+
Missing-metadata cases (no PEP 658, no sdist) skip transitive
|
|
86
|
+
extras but fall through for user-requested ones; mock test
|
|
87
|
+
coordinators rely on this.
|
|
87
88
|
"""
|
|
88
89
|
# Late import: ``pypi`` imports this module at module load.
|
|
89
|
-
from ..provider import
|
|
90
|
+
from ..provider import (
|
|
91
|
+
ExtrasMode,
|
|
92
|
+
MetadataError,
|
|
93
|
+
UnsupportedSdistError,
|
|
94
|
+
_normalize_extra,
|
|
95
|
+
)
|
|
90
96
|
|
|
91
97
|
_, _, normalized = provider.split_and_normalize(base)
|
|
92
98
|
is_user = (normalized, extra) in provider.root_extras
|
|
@@ -94,17 +100,15 @@ def _pick_in_mode(
|
|
|
94
100
|
for version in candidates:
|
|
95
101
|
if provider.has_invalid_metadata(normalized, version):
|
|
96
102
|
continue
|
|
97
|
-
if is_user:
|
|
98
|
-
return version
|
|
99
|
-
# Fetch base metadata so an unparseable PKG-INFO is caught
|
|
100
|
-
# before the extras proxy decides this version. Any
|
|
101
|
-
# MetadataError (parse failure or no metadata source) is a
|
|
102
|
-
# candidate skip.
|
|
103
103
|
try:
|
|
104
104
|
provider.get_dependencies(base, version)
|
|
105
|
-
except
|
|
105
|
+
except UnsupportedSdistError:
|
|
106
106
|
continue
|
|
107
|
-
|
|
107
|
+
except MetadataError:
|
|
108
|
+
if not is_user or provider.has_invalid_metadata(normalized, version):
|
|
109
|
+
continue
|
|
110
|
+
return version
|
|
111
|
+
if is_user or not backtrack:
|
|
108
112
|
return version
|
|
109
113
|
metadata = provider.metadata_cache.get((normalized, version))
|
|
110
114
|
provided = (
|
|
@@ -188,7 +192,11 @@ def get_extra_dependencies(
|
|
|
188
192
|
return handle_missing_extra(provider, normalized, extra, version, cache_key)
|
|
189
193
|
|
|
190
194
|
deps = dict(extra_map[extra])
|
|
191
|
-
|
|
195
|
+
# Pin the base, intersected with any bound the extra itself
|
|
196
|
+
# places on it (``foo>=2; extra == "bar"``).
|
|
197
|
+
deps[normalized] = deps.get(
|
|
198
|
+
normalized, VersionRange.full()
|
|
199
|
+
) & VersionRange.singleton(version)
|
|
192
200
|
|
|
193
201
|
provider.deps_cache[cache_key] = deps
|
|
194
202
|
provider.prefetch_new_deps(deps)
|
|
@@ -223,9 +231,10 @@ def handle_missing_extra(
|
|
|
223
231
|
version,
|
|
224
232
|
extra,
|
|
225
233
|
)
|
|
226
|
-
#
|
|
227
|
-
#
|
|
228
|
-
#
|
|
229
|
-
#
|
|
230
|
-
|
|
231
|
-
|
|
234
|
+
# The extra contributes no deps at this version, but the proxy
|
|
235
|
+
# must still pin its base: without the pin the proxy and the base
|
|
236
|
+
# can settle on different versions, and if the base's version does
|
|
237
|
+
# provide the extra its dependencies are silently dropped.
|
|
238
|
+
deps = {normalized: VersionRange.singleton(version)}
|
|
239
|
+
provider.deps_cache[cache_key] = deps
|
|
240
|
+
return deps
|
|
@@ -271,10 +271,10 @@ def excluded_by_python(provider: Provider, dist: DistFile) -> bool:
|
|
|
271
271
|
try:
|
|
272
272
|
spec = SpecifierSet(requires_python)
|
|
273
273
|
cached = Version(provider.python_version) not in spec
|
|
274
|
-
except
|
|
275
|
-
# Malformed Requires-Python on the dist
|
|
276
|
-
#
|
|
277
|
-
#
|
|
274
|
+
except InvalidSpecifier:
|
|
275
|
+
# Malformed Requires-Python on the dist: treat as
|
|
276
|
+
# not-excluded, let downstream logic decide. Our own
|
|
277
|
+
# python_version is validated at Provider construction.
|
|
278
278
|
cached = False
|
|
279
279
|
provider.requires_python_cache[requires_python] = cached
|
|
280
280
|
if cached:
|
|
@@ -425,14 +425,21 @@ def await_metadata_batch(
|
|
|
425
425
|
event.wait()
|
|
426
426
|
text = provider.coordinator.index.get_metadata(package, ver_str)
|
|
427
427
|
if text is None:
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
428
|
+
# No PEP 658 text arrived: leave the version un-cached so
|
|
429
|
+
# look-ahead's get_dependencies runs the sdist fallback (or
|
|
430
|
+
# refuses it) rather than pinning it as dependency-free.
|
|
431
|
+
continue
|
|
432
|
+
if provider.coordinator.index.metadata_from_sdist(package, ver_str):
|
|
433
|
+
# The shared slot holds sdist PKG-INFO from an earlier
|
|
434
|
+
# fallback; caching it here would skip the PEP 643 gate
|
|
435
|
+
# that get_dependencies applies on the from_sdist path.
|
|
436
|
+
continue
|
|
437
|
+
try:
|
|
438
|
+
provider.parse_and_cache_metadata(cache_key, text)
|
|
439
|
+
except (ValueError, InvalidVersion, InvalidSpecifier):
|
|
440
|
+
# Malformed metadata: same reason, refuse via get_dependencies
|
|
441
|
+
# (_invalid_metadata) instead of caching empty deps.
|
|
442
|
+
continue
|
|
436
443
|
|
|
437
444
|
|
|
438
445
|
def prefetch_new_deps(provider: Provider, deps: Mapping[str, VersionRange]) -> None:
|
|
@@ -48,11 +48,11 @@ def resolve_metadata(
|
|
|
48
48
|
|
|
49
49
|
text = provider.coordinator.index.get_metadata(normalized, ver_str)
|
|
50
50
|
if text is not None:
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
from_sdist =
|
|
51
|
+
# Wheel METADATA and sdist PKG-INFO share the slot; the index
|
|
52
|
+
# records which kind the last write was. Inferring from the
|
|
53
|
+
# listing instead would mislabel the text whenever this
|
|
54
|
+
# provider's view differs from the one that stored it.
|
|
55
|
+
from_sdist = provider.coordinator.index.metadata_from_sdist(normalized, ver_str)
|
|
56
56
|
return (text, from_sdist)
|
|
57
57
|
|
|
58
58
|
dist = pick_dist_for_metadata(versions, version)
|
|
@@ -85,18 +85,6 @@ def resolve_metadata(
|
|
|
85
85
|
raise MetadataError(msg)
|
|
86
86
|
|
|
87
87
|
|
|
88
|
-
def has_wheel_metadata_at(
|
|
89
|
-
versions: Sequence[tuple[Version, DistFile]], version: Version
|
|
90
|
-
) -> bool:
|
|
91
|
-
"""Report whether the listing has a wheel with PEP 658 metadata."""
|
|
92
|
-
for v, d in versions:
|
|
93
|
-
if v != version:
|
|
94
|
-
continue
|
|
95
|
-
if isinstance(d, WheelFile) and d.metadata_url is not None:
|
|
96
|
-
return True
|
|
97
|
-
return False
|
|
98
|
-
|
|
99
|
-
|
|
100
88
|
def pick_dist_for_metadata(
|
|
101
89
|
versions: Sequence[tuple[Version, DistFile]], version: Version
|
|
102
90
|
) -> DistFile | None:
|
|
@@ -93,7 +93,9 @@ def compute_matching(
|
|
|
93
93
|
elif has_local_source:
|
|
94
94
|
matching = 1
|
|
95
95
|
else:
|
|
96
|
-
|
|
96
|
+
# Not cached, so the next call re-checks the index and the
|
|
97
|
+
# listing-arrival side effect above can still fire.
|
|
98
|
+
return _NO_LISTING_PRIOR
|
|
97
99
|
|
|
98
100
|
if per_pkg is None:
|
|
99
101
|
per_pkg = provider.matching_cache[normalized] = {}
|
|
@@ -103,15 +103,16 @@ def split_vcs_scheme(url: str) -> tuple[str | None, str]:
|
|
|
103
103
|
def has_full_commit_sha(url: str) -> bool:
|
|
104
104
|
"""Return True if the URL pins to a 40-char hex commit hash.
|
|
105
105
|
|
|
106
|
-
Looks for ``@<sha>``
|
|
107
|
-
``#`` fragment.
|
|
108
|
-
|
|
106
|
+
Looks for ``@<sha>`` in the path component (after the authority);
|
|
107
|
+
ignores any ``#`` fragment. A ``user@host`` in the authority is
|
|
108
|
+
left alone, matching the ref parsing in :mod:`nab_index.vcs`.
|
|
109
109
|
"""
|
|
110
110
|
fragmentless = url.split("#", 1)[0]
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
after_scheme = fragmentless.split("://", 1)[-1]
|
|
112
|
+
path = after_scheme.partition("/")[2]
|
|
113
|
+
if "@" not in path:
|
|
113
114
|
return False
|
|
114
|
-
ref =
|
|
115
|
+
ref = path.rsplit("@", 1)[1]
|
|
115
116
|
return bool(FULL_GIT_SHA_RE.match(ref))
|
|
116
117
|
|
|
117
118
|
|
|
@@ -104,6 +104,8 @@ def _entries_for_pin(canonical: str, pin: PinShape) -> Iterable[DownloadEntry]:
|
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
def _iter_index_pin(canonical: str, pin: IndexPin) -> Iterable[DownloadEntry]:
|
|
107
|
+
# Recorded digests are lowercased to match hashlib.hexdigest() output:
|
|
108
|
+
# index-fed flows already lowercase, but a caller-built LockInput may not.
|
|
107
109
|
if pin.sdist is not None:
|
|
108
110
|
algo, digest = pin.sdist.primary_digest
|
|
109
111
|
yield DownloadEntry(
|
|
@@ -112,7 +114,7 @@ def _iter_index_pin(canonical: str, pin: IndexPin) -> Iterable[DownloadEntry]:
|
|
|
112
114
|
filename=pin.sdist.filename,
|
|
113
115
|
url=pin.sdist.url,
|
|
114
116
|
hash_algo=algo,
|
|
115
|
-
digest=digest,
|
|
117
|
+
digest=digest.lower(),
|
|
116
118
|
)
|
|
117
119
|
for wheel in pin.wheels:
|
|
118
120
|
algo, digest = wheel.primary_digest
|
|
@@ -122,7 +124,7 @@ def _iter_index_pin(canonical: str, pin: IndexPin) -> Iterable[DownloadEntry]:
|
|
|
122
124
|
filename=wheel.filename,
|
|
123
125
|
url=wheel.url,
|
|
124
126
|
hash_algo=algo,
|
|
125
|
-
digest=digest,
|
|
127
|
+
digest=digest.lower(),
|
|
126
128
|
)
|
|
127
129
|
|
|
128
130
|
|
|
@@ -141,6 +141,10 @@ class InMemoryIndex:
|
|
|
141
141
|
self._listing_errors: dict[str, BaseException] = {}
|
|
142
142
|
self._listing_indexes: dict[str, str] = {}
|
|
143
143
|
self._metadata: dict[tuple[str, str], str | None] = {}
|
|
144
|
+
# Keys whose ``_metadata`` slot was last written from an sdist
|
|
145
|
+
# PKG-INFO rather than a wheel METADATA; readers need the
|
|
146
|
+
# origin because only sdist deps go through the PEP 643 gate.
|
|
147
|
+
self._metadata_from_sdist: set[tuple[str, str]] = set()
|
|
144
148
|
self._sdist_pyproject: dict[tuple[str, str], str | None] = {}
|
|
145
149
|
self._sdist_archives: dict[tuple[str, str], bytes | None] = {}
|
|
146
150
|
self._pending: dict[str, _Pending] = {}
|
|
@@ -227,6 +231,7 @@ class InMemoryIndex:
|
|
|
227
231
|
key = f"metadata:{package}:{version}"
|
|
228
232
|
with self._lock:
|
|
229
233
|
self._metadata[(package, version)] = data
|
|
234
|
+
self._metadata_from_sdist.discard((package, version))
|
|
230
235
|
pending = self._pending.get(key)
|
|
231
236
|
if pending is not None:
|
|
232
237
|
pending.result = data
|
|
@@ -241,15 +246,27 @@ class InMemoryIndex:
|
|
|
241
246
|
because PKG-INFO is core-metadata-equivalent. The pending
|
|
242
247
|
keys differ so a sdist request can run in parallel with (or
|
|
243
248
|
after) a failed wheel metadata request.
|
|
249
|
+
:meth:`metadata_from_sdist` reports which kind the slot holds.
|
|
244
250
|
"""
|
|
245
251
|
key = f"sdist:{package}:{version}"
|
|
246
252
|
with self._lock:
|
|
247
253
|
self._metadata[(package, version)] = data
|
|
254
|
+
self._metadata_from_sdist.add((package, version))
|
|
248
255
|
pending = self._pending.get(key)
|
|
249
256
|
if pending is not None:
|
|
250
257
|
pending.result = data
|
|
251
258
|
pending.event.set()
|
|
252
259
|
|
|
260
|
+
def metadata_from_sdist(self, package: str, version: str) -> bool:
|
|
261
|
+
"""Return ``True`` when the metadata slot was last written from an sdist.
|
|
262
|
+
|
|
263
|
+
The slot itself cannot distinguish wheel METADATA from sdist
|
|
264
|
+
PKG-INFO; readers that apply the :pep:`643` dynamic-deps gate
|
|
265
|
+
only to sdist values ask here for the current text's origin.
|
|
266
|
+
"""
|
|
267
|
+
with self._lock:
|
|
268
|
+
return (package, version) in self._metadata_from_sdist
|
|
269
|
+
|
|
253
270
|
def store_sdist_pyproject(self, package: str, version: str, data: str) -> None:
|
|
254
271
|
"""Store sdist-derived pyproject.toml text for static-metadata fallback.
|
|
255
272
|
|
|
@@ -470,6 +487,11 @@ class FetchCoordinator:
|
|
|
470
487
|
self._thread.join(timeout=_COORDINATOR_JOIN_TIMEOUT_SECONDS)
|
|
471
488
|
self._thread = None
|
|
472
489
|
self._started = False
|
|
490
|
+
# Drop the dead loop so a later start() waits for the fresh one
|
|
491
|
+
# instead of submitting to a closed loop.
|
|
492
|
+
self._loop = None
|
|
493
|
+
self._async_q = None
|
|
494
|
+
self._queue_ready.clear()
|
|
473
495
|
|
|
474
496
|
def _submit(self, item: _QueueItem) -> None:
|
|
475
497
|
"""Schedule ``item`` on the fetcher loop's queue from any thread."""
|
|
@@ -711,7 +733,8 @@ class FetchCoordinator:
|
|
|
711
733
|
|
|
712
734
|
client = self._build_client()
|
|
713
735
|
try:
|
|
714
|
-
|
|
736
|
+
stopping = False
|
|
737
|
+
while not stopping:
|
|
715
738
|
item = await queue.get()
|
|
716
739
|
if item is None:
|
|
717
740
|
break
|
|
@@ -725,10 +748,11 @@ class FetchCoordinator:
|
|
|
725
748
|
except asyncio.QueueEmpty:
|
|
726
749
|
break
|
|
727
750
|
if extra is None:
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
751
|
+
# Fall through to the gather below instead of
|
|
752
|
+
# cancelling: a cancelled _handle never records a
|
|
753
|
+
# result, leaving its waiter's event unset forever.
|
|
754
|
+
stopping = True
|
|
755
|
+
break
|
|
732
756
|
self._dispatch(extra, client, sem, tasks)
|
|
733
757
|
|
|
734
758
|
if tasks:
|
|
@@ -856,9 +880,12 @@ class FetchCoordinator:
|
|
|
856
880
|
pkg_info, pyproject = await client.get_sdist_files(
|
|
857
881
|
req.package, req.version, req.url
|
|
858
882
|
)
|
|
859
|
-
|
|
883
|
+
# Store pyproject.toml first: store_sdist_metadata fires the
|
|
884
|
+
# pending event, and a released waiter reads the pyproject slot
|
|
885
|
+
# with no further synchronisation.
|
|
860
886
|
if pyproject is not None:
|
|
861
887
|
self.index.store_sdist_pyproject(req.package, req.version, pyproject)
|
|
888
|
+
self.index.store_sdist_metadata(req.package, req.version, pkg_info)
|
|
862
889
|
|
|
863
890
|
async def _fetch_sdist_archive(
|
|
864
891
|
self,
|
|
@@ -24,7 +24,7 @@ from ._lockfile.builder import (
|
|
|
24
24
|
read_lockfile_packages,
|
|
25
25
|
)
|
|
26
26
|
from ._lockfile.disjointness import DisjointnessError
|
|
27
|
-
from ._lockfile.pylock import build_pylock, write_lock
|
|
27
|
+
from ._lockfile.pylock import DivergentBaseDependencyError, build_pylock, write_lock
|
|
28
28
|
from ._lockfile.requirements import (
|
|
29
29
|
write_requirements_with_hashes,
|
|
30
30
|
write_requirements_without_hashes,
|
|
@@ -44,6 +44,7 @@ __all__ = [
|
|
|
44
44
|
"ACCEPTED_HASH_ALGORITHMS",
|
|
45
45
|
"LOCK_VERSION",
|
|
46
46
|
"DisjointnessError",
|
|
47
|
+
"DivergentBaseDependencyError",
|
|
47
48
|
"IndexPin",
|
|
48
49
|
"LocalPin",
|
|
49
50
|
"LockInput",
|
|
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
import email.parser
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
14
|
from functools import lru_cache
|
|
15
|
-
from typing import Any
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
16
|
|
|
17
17
|
import tomli
|
|
18
18
|
|
|
@@ -20,6 +20,9 @@ from ._vendor.packaging.requirements import Requirement
|
|
|
20
20
|
from ._vendor.packaging.specifiers import SpecifierSet
|
|
21
21
|
from ._vendor.packaging.version import Version
|
|
22
22
|
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from ._vendor.packaging.markers import Marker
|
|
25
|
+
|
|
23
26
|
__all__ = [
|
|
24
27
|
"DEPENDENCY_FIELDS",
|
|
25
28
|
"WheelMetadata",
|
|
@@ -88,6 +91,22 @@ def metadata_deps_are_static(metadata: WheelMetadata) -> bool:
|
|
|
88
91
|
return not (DEPENDENCY_FIELDS & metadata.dynamic)
|
|
89
92
|
|
|
90
93
|
|
|
94
|
+
@lru_cache(maxsize=8192)
|
|
95
|
+
def _intern_marker(marker: Marker) -> Marker:
|
|
96
|
+
"""Return a shared :class:`Marker` for an equal marker expression.
|
|
97
|
+
|
|
98
|
+
``Marker`` hashes and compares by its text, so a single marker like
|
|
99
|
+
``extra == "test"`` recurs across hundreds of distinct dep strings
|
|
100
|
+
(``pytest; extra == "test"``, ``coverage; extra == "test"``, ...),
|
|
101
|
+
each parsing to its own object. The provider caches marker
|
|
102
|
+
evaluation by ``id(marker)``, so sharing one object per distinct
|
|
103
|
+
expression lets that cache hit across every candidate instead of
|
|
104
|
+
re-evaluating the same expression per dep. Markers are read-only,
|
|
105
|
+
so sharing is safe.
|
|
106
|
+
"""
|
|
107
|
+
return marker
|
|
108
|
+
|
|
109
|
+
|
|
91
110
|
@lru_cache(maxsize=16384)
|
|
92
111
|
def _parse_requirement_cached(req_str: str) -> Requirement:
|
|
93
112
|
"""Cache ``Requirement(req_str)`` parsing across wheel metadata.
|
|
@@ -97,7 +116,10 @@ def _parse_requirement_cached(req_str: str) -> Requirement:
|
|
|
97
116
|
only read operations (specifier, marker, extras, name) so sharing
|
|
98
117
|
parsed objects is safe.
|
|
99
118
|
"""
|
|
100
|
-
|
|
119
|
+
req = Requirement(req_str)
|
|
120
|
+
if req.marker is not None:
|
|
121
|
+
req.marker = _intern_marker(req.marker)
|
|
122
|
+
return req
|
|
101
123
|
|
|
102
124
|
|
|
103
125
|
@lru_cache(maxsize=65536)
|
|
@@ -7,7 +7,6 @@ types. Uses a thread pool with a shared HTTP session to overlap I/O.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
import contextlib
|
|
11
10
|
import enum
|
|
12
11
|
import logging
|
|
13
12
|
import re
|
|
@@ -98,16 +97,17 @@ _PYTHON_FULL_VERSION_PARTS = 3
|
|
|
98
97
|
def python_axis_environment(python_version: str) -> dict[str, str]:
|
|
99
98
|
"""Map an explicit Python version to its PEP 508 marker keys.
|
|
100
99
|
|
|
101
|
-
``
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
``python_version`` is padded to two components and
|
|
101
|
+
``python_full_version`` to three so patch-precision markers evaluate
|
|
102
|
+
the same here as in the universal matrix. Raises ``InvalidVersion``
|
|
103
|
+
if the input is not a version.
|
|
104
104
|
"""
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
)
|
|
105
|
+
try:
|
|
106
|
+
release = Version(python_version).release
|
|
107
|
+
except InvalidVersion:
|
|
108
|
+
msg = f"python_version {python_version!r} is not a valid version"
|
|
109
|
+
raise InvalidVersion(msg) from None
|
|
110
|
+
minor = ".".join(str(part) for part in (*release, 0)[:_PYTHON_VERSION_PARTS])
|
|
111
111
|
full = (
|
|
112
112
|
python_version
|
|
113
113
|
if len(release) >= _PYTHON_FULL_VERSION_PARTS
|
|
@@ -494,8 +494,7 @@ class Provider:
|
|
|
494
494
|
}
|
|
495
495
|
self.environment: dict[str, str] = env_init
|
|
496
496
|
if python_version is not None:
|
|
497
|
-
|
|
498
|
-
self.environment.update(python_axis_environment(python_version))
|
|
497
|
+
self.environment.update(python_axis_environment(python_version))
|
|
499
498
|
if marker_environment:
|
|
500
499
|
for key, value in marker_environment.items():
|
|
501
500
|
self.environment[key] = value
|