nab-python 0.0.6__tar.gz → 0.0.7__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.
Files changed (200) hide show
  1. {nab_python-0.0.6 → nab_python-0.0.7}/PKG-INFO +3 -3
  2. {nab_python-0.0.6 → nab_python-0.0.7}/pyproject.toml +3 -3
  3. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_build/runner.py +14 -10
  4. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_lockfile/builder.py +62 -4
  5. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_lockfile/pylock.py +29 -6
  6. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_lockfile/requirements.py +16 -5
  7. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_provider/extras.py +34 -20
  8. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_provider/listing.py +16 -4
  9. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_provider/metadata_resolver.py +46 -35
  10. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_provider/priority.py +4 -2
  11. nab_python-0.0.7/src/nab_python/_provider/sources.py +396 -0
  12. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vcs_admission.py +38 -1
  13. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/PROVENANCE.md +2 -2
  14. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/markers.py +3 -5
  15. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/build_backend.py +47 -27
  16. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/config.py +92 -5
  17. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/config_sources.py +34 -2
  18. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/download.py +48 -9
  19. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/lockfile.py +32 -1
  20. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/metadata.py +14 -2
  21. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/provider.py +104 -41
  22. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/requirements_file.py +272 -20
  23. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/resolve.py +24 -12
  24. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/universal/provider.py +13 -7
  25. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/universal/reresolve.py +13 -0
  26. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/universal/resolve.py +26 -5
  27. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/universal/validate.py +19 -8
  28. nab_python-0.0.7/tests/property_python/test_cached_client_equiv.py +320 -0
  29. nab_python-0.0.7/tests/property_python/test_conflict_fork_markers.py +249 -0
  30. nab_python-0.0.7/tests/property_python/test_disjointness_differential.py +293 -0
  31. nab_python-0.0.7/tests/property_python/test_filename_differential.py +183 -0
  32. nab_python-0.0.7/tests/property_python/test_multi_index_contract.py +180 -0
  33. nab_python-0.0.7/tests/property_python/test_sdist_archive_safety.py +210 -0
  34. nab_python-0.0.7/tests/property_python/test_universal_faithfulness.py +259 -0
  35. {nab_python-0.0.6 → nab_python-0.0.7}/tests/property_python/test_vcs_admission.py +24 -2
  36. nab_python-0.0.7/tests/test_archive.py +442 -0
  37. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_async_transports.py +63 -0
  38. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_build_backend.py +62 -44
  39. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_build_runner.py +4 -6
  40. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_cache.py +30 -5
  41. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_cached_client.py +95 -10
  42. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_config.py +133 -2
  43. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_config_sources.py +9 -0
  44. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_local_index.py +145 -0
  45. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_lockfile.py +214 -0
  46. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_marker_evaluation.py +38 -0
  47. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_metadata.py +24 -0
  48. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_provider.py +254 -61
  49. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_requirements_file.py +395 -0
  50. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_resolve.py +107 -5
  51. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_resolver_packaging.py +86 -67
  52. nab_python-0.0.7/tests/test_simple_client_malformed.py +92 -0
  53. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_vcs_admission.py +121 -3
  54. nab_python-0.0.7/tests/universal/property_universal/test_matrix_labels.py +158 -0
  55. nab_python-0.0.7/tests/universal/property_universal/test_reresolve_fixed_point.py +168 -0
  56. nab_python-0.0.7/tests/universal/property_universal/test_validate_mutations.py +184 -0
  57. nab_python-0.0.7/tests/universal/property_universal/test_wheel_selection_differential.py +316 -0
  58. {nab_python-0.0.6 → nab_python-0.0.7}/tests/universal/test_resolve.py +84 -0
  59. {nab_python-0.0.6 → nab_python-0.0.7}/tests/universal/test_universal_provider.py +71 -0
  60. {nab_python-0.0.6 → nab_python-0.0.7}/tests/universal/test_validate.py +128 -15
  61. nab_python-0.0.6/src/nab_python/_provider/sources.py +0 -223
  62. {nab_python-0.0.6 → nab_python-0.0.7}/.gitignore +0 -0
  63. {nab_python-0.0.6 → nab_python-0.0.7}/LICENSE +0 -0
  64. {nab_python-0.0.6 → nab_python-0.0.7}/README.md +0 -0
  65. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/README.md +0 -0
  66. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/_profile_runner.py +0 -0
  67. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/cache/.gitignore +0 -0
  68. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/canary.py +0 -0
  69. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/canary_results/.gitignore +0 -0
  70. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/compare.py +0 -0
  71. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/results/.gitignore +0 -0
  72. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/ai-stack-lowest-direct.toml +0 -0
  73. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/ai-stack-lowest.toml +0 -0
  74. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/ai-stack.toml +0 -0
  75. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/airflow-lowest-direct.toml +0 -0
  76. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/airflow-lowest.toml +0 -0
  77. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/airflow.toml +0 -0
  78. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/big-packages-lowest-direct.toml +0 -0
  79. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/big-packages-lowest.toml +0 -0
  80. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/big-packages.toml +0 -0
  81. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/cross-tracker-lowest-direct.toml +0 -0
  82. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/cross-tracker-lowest.toml +0 -0
  83. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/cross-tracker.toml +0 -0
  84. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/ecosystem-lowest-direct.toml +0 -0
  85. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/ecosystem-lowest.toml +0 -0
  86. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/ecosystem.toml +0 -0
  87. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/forums-lowest-direct.toml +0 -0
  88. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/forums-lowest.toml +0 -0
  89. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/forums.toml +0 -0
  90. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/pdm-lowest-direct.toml +0 -0
  91. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/pdm-lowest.toml +0 -0
  92. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/pdm.toml +0 -0
  93. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/pip-lowest-direct.toml +0 -0
  94. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/pip-lowest.toml +0 -0
  95. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/pip.toml +0 -0
  96. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/poetry-lowest-direct.toml +0 -0
  97. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/poetry-lowest.toml +0 -0
  98. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/poetry.toml +0 -0
  99. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/pytorch-lowest-direct.toml +0 -0
  100. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/pytorch-lowest.toml +0 -0
  101. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/pytorch.toml +0 -0
  102. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/quick-lowest-direct.toml +0 -0
  103. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/quick-lowest.toml +0 -0
  104. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/quick.toml +0 -0
  105. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/rip-lowest-direct.toml +0 -0
  106. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/rip-lowest.toml +0 -0
  107. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/rip.toml +0 -0
  108. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/universal.toml +0 -0
  109. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/unsupported-lowest-direct.toml +0 -0
  110. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/unsupported-lowest.toml +0 -0
  111. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/unsupported.toml +0 -0
  112. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/uv-lowest-direct.toml +0 -0
  113. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/uv-lowest.toml +0 -0
  114. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios/uv.toml +0 -0
  115. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/scenarios.py +0 -0
  116. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/strategy_sweep.py +0 -0
  117. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/strategy_sweep_results/summary.json +0 -0
  118. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/strategy_sweep_summary.py +0 -0
  119. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/universal_scenarios.py +0 -0
  120. {nab_python-0.0.6 → nab_python-0.0.7}/benchmarks/universal_summary.py +0 -0
  121. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/__init__.py +0 -0
  122. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_build/__init__.py +0 -0
  123. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_build/env.py +0 -0
  124. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_build/errors.py +0 -0
  125. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_conflict_kind.py +0 -0
  126. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_lockfile/__init__.py +0 -0
  127. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_lockfile/disjointness.py +0 -0
  128. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_packaging_provider.py +0 -0
  129. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_provider/__init__.py +0 -0
  130. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_provider/build_remote.py +0 -0
  131. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_provider/lookahead.py +0 -0
  132. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_testing/__init__.py +0 -0
  133. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_testing/coordinator_fake.py +0 -0
  134. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_testing/overrides.py +0 -0
  135. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_toml.py +0 -0
  136. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/__init__.py +0 -0
  137. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/LICENSE +0 -0
  138. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/LICENSE.APACHE +0 -0
  139. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/LICENSE.BSD +0 -0
  140. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/__init__.py +0 -0
  141. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/_elffile.py +0 -0
  142. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/_manylinux.py +0 -0
  143. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/_musllinux.py +0 -0
  144. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/_parser.py +0 -0
  145. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/_ranges.py +0 -0
  146. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/_structures.py +0 -0
  147. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/_tokenizer.py +0 -0
  148. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/dependency_groups.py +0 -0
  149. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/direct_url.py +0 -0
  150. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/errors.py +0 -0
  151. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/licenses/__init__.py +0 -0
  152. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/licenses/_spdx.py +0 -0
  153. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/metadata.py +0 -0
  154. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/py.typed +0 -0
  155. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/pylock.py +0 -0
  156. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/ranges.py +0 -0
  157. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/requirements.py +0 -0
  158. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/specifiers.py +0 -0
  159. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/tags.py +0 -0
  160. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/utils.py +0 -0
  161. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/_vendor/packaging/version.py +0 -0
  162. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/fetch.py +0 -0
  163. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/py.typed +0 -0
  164. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/universal/__init__.py +0 -0
  165. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/universal/matrix.py +0 -0
  166. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/universal/wheel_selection.py +0 -0
  167. {nab_python-0.0.6 → nab_python-0.0.7}/src/nab_python/workspace.py +0 -0
  168. {nab_python-0.0.6 → nab_python-0.0.7}/tests/__init__.py +0 -0
  169. {nab_python-0.0.6 → nab_python-0.0.7}/tests/property_python/__init__.py +0 -0
  170. {nab_python-0.0.6 → nab_python-0.0.7}/tests/property_python/strategies.py +0 -0
  171. {nab_python-0.0.6 → nab_python-0.0.7}/tests/property_python/test_build_policy_never.py +0 -0
  172. {nab_python-0.0.6 → nab_python-0.0.7}/tests/property_python/test_download_hashes.py +0 -0
  173. {nab_python-0.0.6 → nab_python-0.0.7}/tests/property_python/test_extras_pep685.py +0 -0
  174. {nab_python-0.0.6 → nab_python-0.0.7}/tests/property_python/test_fetch_coordinator.py +0 -0
  175. {nab_python-0.0.6 → nab_python-0.0.7}/tests/property_python/test_lockfile_pep751.py +0 -0
  176. {nab_python-0.0.6 → nab_python-0.0.7}/tests/property_python/test_marker_overlay.py +0 -0
  177. {nab_python-0.0.6 → nab_python-0.0.7}/tests/property_python/test_multi_index_pep503.py +0 -0
  178. {nab_python-0.0.6 → nab_python-0.0.7}/tests/property_python/test_override_overlap.py +0 -0
  179. {nab_python-0.0.6 → nab_python-0.0.7}/tests/property_python/test_prerelease_admission.py +0 -0
  180. {nab_python-0.0.6 → nab_python-0.0.7}/tests/property_python/test_resolver_pep440.py +0 -0
  181. {nab_python-0.0.6 → nab_python-0.0.7}/tests/ruff.toml +0 -0
  182. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_benchmark_parse_requirements.py +0 -0
  183. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_download.py +0 -0
  184. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_fetch.py +0 -0
  185. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_multi_index.py +0 -0
  186. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_simple_client_filenames.py +0 -0
  187. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_simple_client_hashes.py +0 -0
  188. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_simple_client_upload_time.py +0 -0
  189. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_vcs.py +0 -0
  190. {nab_python-0.0.6 → nab_python-0.0.7}/tests/test_workspace.py +0 -0
  191. {nab_python-0.0.6 → nab_python-0.0.7}/tests/universal/__init__.py +0 -0
  192. {nab_python-0.0.6 → nab_python-0.0.7}/tests/universal/property_universal/__init__.py +0 -0
  193. {nab_python-0.0.6 → nab_python-0.0.7}/tests/universal/property_universal/strategies.py +0 -0
  194. {nab_python-0.0.6 → nab_python-0.0.7}/tests/universal/property_universal/test_alignment.py +0 -0
  195. {nab_python-0.0.6 → nab_python-0.0.7}/tests/universal/property_universal/test_matrix.py +0 -0
  196. {nab_python-0.0.6 → nab_python-0.0.7}/tests/universal/property_universal/test_provider_pep425.py +0 -0
  197. {nab_python-0.0.6 → nab_python-0.0.7}/tests/universal/property_universal/test_validate.py +0 -0
  198. {nab_python-0.0.6 → nab_python-0.0.7}/tests/universal/test_matrix.py +0 -0
  199. {nab_python-0.0.6 → nab_python-0.0.7}/tests/universal/test_reresolve.py +0 -0
  200. {nab_python-0.0.6 → nab_python-0.0.7}/tests/universal/test_wheel_selection.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nab-python
3
- Version: 0.0.6
3
+ Version: 0.0.7
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.6
24
- Requires-Dist: nab-resolver==0.0.6
23
+ Requires-Dist: nab-index==0.0.7
24
+ Requires-Dist: nab-resolver==0.0.7
25
25
  Requires-Dist: pyproject-hooks>=1.2
26
26
  Requires-Dist: tomli-w>=1.2
27
27
  Requires-Dist: tomli>=2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nab-python"
3
- version = "0.0.6"
3
+ version = "0.0.7"
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.6",
23
- "nab-index==0.0.6",
22
+ "nab-resolver==0.0.7",
23
+ "nab-index==0.0.7",
24
24
  "tomli>=2.0",
25
25
  "tomli_w>=1.2",
26
26
  "build>=1.2",
@@ -33,12 +33,12 @@ import build
33
33
  import pyproject_hooks
34
34
  import tomli
35
35
 
36
- from .._vendor.packaging.requirements import Requirement
36
+ from nab_resolver.resolver import ResolutionError
37
+
38
+ from .._vendor.packaging.requirements import InvalidRequirement, Requirement
37
39
  from .._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
38
40
  from .._vendor.packaging.utils import canonicalize_name
39
41
  from .._vendor.packaging.version import InvalidVersion, Version
40
- from nab_resolver.resolver import ResolutionError
41
-
42
42
  from ..metadata import WheelMetadata
43
43
  from .env import BuildEnvError, NabBuildEnv
44
44
  from .errors import BuildBackendError
@@ -238,15 +238,19 @@ def _parse_metadata(metadata_path: Path) -> WheelMetadata:
238
238
  SpecifierSet(requires_python_raw) if requires_python_raw else None
239
239
  )
240
240
  except InvalidSpecifier as exc:
241
- msg = f"backend METADATA has invalid Requires-Python {requires_python_raw!r}: {exc}"
241
+ msg = (
242
+ f"backend METADATA has invalid Requires-Python "
243
+ f"{requires_python_raw!r}: {exc}"
244
+ )
242
245
  raise BuildBackendError(msg) from exc
243
246
 
244
- requires_dist: list[Requirement] = []
245
- for raw in msg_obj.get_all("Requires-Dist") or ():
246
- try:
247
- requires_dist.append(Requirement(raw))
248
- except (ValueError, TypeError): # noqa: PERF203
249
- logger.warning("skipping unparseable Requires-Dist: %s", raw)
247
+ try:
248
+ requires_dist: list[Requirement] = [
249
+ Requirement(raw) for raw in msg_obj.get_all("Requires-Dist") or ()
250
+ ]
251
+ except InvalidRequirement as exc:
252
+ msg = f"backend METADATA has an invalid Requires-Dist: {exc}"
253
+ raise BuildBackendError(msg) from exc
250
254
 
251
255
  provides_extra: list[str] = sorted(
252
256
  {
@@ -21,6 +21,7 @@ from nab_index.client import SdistFile, WheelFile
21
21
 
22
22
  from .._toml import tool_nab_section
23
23
  from .._vendor.packaging.pylock import Pylock, PylockValidationError
24
+ from .._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
24
25
  from .._vendor.packaging.utils import canonicalize_name
25
26
 
26
27
  if TYPE_CHECKING:
@@ -30,6 +31,7 @@ if TYPE_CHECKING:
30
31
 
31
32
  from .._vendor.packaging.version import Version
32
33
  from ..lockfile import (
34
+ ArchivePin,
33
35
  IndexPin,
34
36
  LockInput,
35
37
  PinShape,
@@ -37,7 +39,7 @@ if TYPE_CHECKING:
37
39
  VcsPin,
38
40
  WheelArtifact,
39
41
  )
40
- from ..provider import DistPolicy, LocalSource, VcsSource
42
+ from ..provider import ArchiveSource, DistPolicy, LocalSource, VcsSource
41
43
 
42
44
 
43
45
  __all__ = [
@@ -95,6 +97,10 @@ class LockInputProvider(Protocol):
95
97
  """Return the configured VcsSource for ``canonical_name`` or None."""
96
98
  ...
97
99
 
100
+ def archive_source_for(self, canonical_name: str, /) -> ArchiveSource | None:
101
+ """Return the configured ArchiveSource for ``canonical_name`` or None."""
102
+ ...
103
+
98
104
  def vcs_pin_for(self, canonical_name: str, /) -> str | None:
99
105
  """Return the resolved 40-char SHA captured during materialisation."""
100
106
  ...
@@ -287,6 +293,12 @@ def build_lock_input_from_provider( # noqa: PLR0913 - each flag maps to a disti
287
293
  resolved_sha=provider.vcs_pin_for(canonical),
288
294
  )
289
295
  continue
296
+ archive_source = provider.archive_source_for(canonical)
297
+ if archive_source is not None:
298
+ lock_pins[canonical] = _archive_pin_from_source(
299
+ canonical, version, archive_source
300
+ )
301
+ continue
290
302
  lock_pins[canonical] = _index_pin_from_listing(
291
303
  provider, canonical, version, indexes
292
304
  )
@@ -339,6 +351,9 @@ def _forward_dependency_graph(
339
351
  for dep in extra_map.get(extra, {})
340
352
  )
341
353
  dep_names &= pinned
354
+ # An umbrella extra (pkg[all] pulling pkg[graphviz]) can name its own
355
+ # package; drop it so pkg is never an edge to itself.
356
+ dep_names.discard(canonical)
342
357
  if dep_names:
343
358
  graph[canonical] = tuple(sorted(dep_names))
344
359
  return graph
@@ -519,11 +534,27 @@ def require_artifact_hashes(lock_input: LockInput) -> None:
519
534
 
520
535
 
521
536
  def _common_requires_python(files: Iterable[WheelFile | SdistFile]) -> str | None:
522
- """Return a single Requires-Python value if all files agree, else ``None``."""
537
+ """Return the package-level Requires-Python value, or ``None``.
538
+
539
+ An artefact with no Requires-Python is unconstrained, so a single
540
+ such artefact leaves the whole package unconstrained. Otherwise the
541
+ value survives only when every artefact agrees.
542
+
543
+ A malformed value counts as unconstrained too, matching
544
+ ``excluded_by_python``, which admits a dist whose Requires-Python is
545
+ an unparseable specifier on any Python. So the lock writer always
546
+ parses a valid specifier or ``None``, and the pin is never
547
+ over-constrained by an artefact whose floor nab could not read.
548
+ """
523
549
  seen: set[str] = set()
524
550
  for f in files:
525
- if f.requires_python is not None:
526
- seen.add(f.requires_python)
551
+ if f.requires_python is None:
552
+ return None
553
+ try:
554
+ SpecifierSet(f.requires_python)
555
+ except InvalidSpecifier:
556
+ return None
557
+ seen.add(f.requires_python)
527
558
  if len(seen) == 1:
528
559
  return next(iter(seen))
529
560
  return None
@@ -593,3 +624,30 @@ def _vcs_pin_from_source(
593
624
  requested_revision=requested_revision,
594
625
  vcs_type=parsed.scheme,
595
626
  )
627
+
628
+
629
+ def _archive_pin_from_source(
630
+ canonical: str,
631
+ version: Version,
632
+ source: ArchiveSource,
633
+ ) -> ArchivePin:
634
+ """Build an :class:`ArchivePin` from an :class:`ArchiveSource`.
635
+
636
+ The URL, hashes, and subdirectory come from the source declaration,
637
+ which config parse validated (a hash is required), so the pin records
638
+ the exact archive the resolve used. The URL is stripped of any
639
+ credential userinfo, like every other pin, so a committed lockfile
640
+ never carries a token.
641
+ """
642
+ from nab_index.archive import ArchiveRequest
643
+
644
+ from ..lockfile import ArchivePin
645
+
646
+ request = ArchiveRequest.parse(source.url)
647
+ return ArchivePin(
648
+ name=canonical,
649
+ version=str(version),
650
+ url=_strip_userinfo(request.url),
651
+ hashes=request.hashes,
652
+ subdirectory=request.subdirectory or None,
653
+ )
@@ -21,6 +21,7 @@ import tomli_w
21
21
  from .._vendor.packaging.markers import Marker
22
22
  from .._vendor.packaging.pylock import (
23
23
  Package,
24
+ PackageArchive,
24
25
  PackageDirectory,
25
26
  PackageSdist,
26
27
  PackageVcs,
@@ -214,7 +215,7 @@ def _pin_to_package(
214
215
  lock_dir: Path,
215
216
  dependencies: list[dict[str, str]] | None = None,
216
217
  ) -> Package:
217
- from ..lockfile import IndexPin, LocalPin, VcsPin
218
+ from ..lockfile import ArchivePin, IndexPin, LocalPin, VcsPin
218
219
 
219
220
  if isinstance(pin, IndexPin):
220
221
  return Package(
@@ -267,6 +268,20 @@ def _pin_to_package(
267
268
  requested_revision=pin.requested_revision,
268
269
  ),
269
270
  )
271
+ if isinstance(pin, ArchivePin):
272
+ # An archive is content-pinned by its hash, so the version is
273
+ # stable and recorded (unlike the directory/VCS cases above).
274
+ return Package(
275
+ name=canonicalize_name(pin.name),
276
+ version=Version(pin.version),
277
+ marker=marker,
278
+ dependencies=dependencies,
279
+ archive=PackageArchive(
280
+ url=pin.url,
281
+ hashes=dict(pin.hashes),
282
+ subdirectory=pin.subdirectory,
283
+ ),
284
+ )
270
285
  msg = f"unknown pin shape: {pin!r}"
271
286
  raise TypeError(msg)
272
287
 
@@ -423,7 +438,7 @@ def _group_pins_by_pin(
423
438
 
424
439
  def _pin_discriminator(pin: PinShape) -> tuple:
425
440
  """Return a hashable key that identifies the source + version of ``pin``."""
426
- from ..lockfile import IndexPin, LocalPin, VcsPin
441
+ from ..lockfile import ArchivePin, IndexPin, LocalPin, VcsPin
427
442
 
428
443
  if isinstance(pin, IndexPin):
429
444
  return ("index", pin.version, pin.index)
@@ -447,6 +462,8 @@ def _pin_discriminator(pin: PinShape) -> tuple:
447
462
  pin.repo_url,
448
463
  pin.subdirectory or "",
449
464
  )
465
+ if isinstance(pin, ArchivePin):
466
+ return ("archive", pin.version, pin.url, pin.hashes, pin.subdirectory or "")
450
467
  msg = f"unknown pin shape: {pin!r}"
451
468
  raise TypeError(msg)
452
469
 
@@ -456,8 +473,9 @@ def _merge_pins_in_group(pins: list[PinShape]) -> PinShape:
456
473
 
457
474
  For :class:`IndexPin`, accumulates every distinct wheel filename
458
475
  across the contributing tuples and keeps the first non-``None``
459
- sdist. ``requires_python`` survives only when every tuple agreed,
460
- matching :func:`_common_requires_python`'s rule.
476
+ sdist. ``requires_python`` survives only when every tuple carried
477
+ the same value and none was unconstrained, matching
478
+ :func:`_common_requires_python`'s rule.
461
479
  Non-IndexPin shapes are already fully discriminated, so the first
462
480
  pin is returned unchanged.
463
481
  """
@@ -469,16 +487,21 @@ def _merge_pins_in_group(pins: list[PinShape]) -> PinShape:
469
487
  seen_wheels: dict[str, WheelArtifact] = {}
470
488
  sdist = head.sdist
471
489
  requires_python_set: set[str] = set()
490
+ any_unconstrained = False
472
491
  for pin in pins:
473
492
  assert isinstance(pin, IndexPin)
474
493
  for wheel in pin.wheels:
475
494
  seen_wheels.setdefault(wheel.filename, wheel)
476
495
  if sdist is None and pin.sdist is not None:
477
496
  sdist = pin.sdist
478
- if pin.requires_python is not None:
497
+ if pin.requires_python is None:
498
+ any_unconstrained = True
499
+ else:
479
500
  requires_python_set.add(pin.requires_python)
480
501
  requires_python = (
481
- next(iter(requires_python_set)) if len(requires_python_set) == 1 else None
502
+ next(iter(requires_python_set))
503
+ if not any_unconstrained and len(requires_python_set) == 1
504
+ else None
482
505
  )
483
506
  return IndexPin(
484
507
  name=head.name,
@@ -38,8 +38,11 @@ def write_requirements_with_hashes(
38
38
  accepts. Local and VCS pins are emitted as ``name @ <url>`` lines
39
39
  without hashes (pip does not hash-check those forms); an editable
40
40
  local pin renders as ``-e <url>`` and a ``subdirectory`` as a
41
- ``#subdirectory=`` fragment. Returns the text and, when
42
- ``output_path`` is provided, atomically writes it.
41
+ ``#subdirectory=`` fragment. An archive pin is a third form, ``name @
42
+ <url>#sha256=...``, carrying its hash in the fragment;
43
+ :func:`require_artifact_hashes` skips it because that hash is guaranteed
44
+ at config parse. Returns the text and, when ``output_path`` is provided,
45
+ atomically writes it.
43
46
  """
44
47
  require_artifact_hashes(lock_input)
45
48
  return _render_requirements(lock_input, with_hashes=True, output_path=output_path)
@@ -51,8 +54,8 @@ def write_requirements_without_hashes(
51
54
  """Render ``lock_input`` as a plain ``name==version`` list.
52
55
 
53
56
  Same shape as :func:`write_requirements_with_hashes` but without
54
- the ``--hash=sha256:...`` lines. Local and VCS pins render the
55
- same in both variants. Returns the text and, when ``output_path``
57
+ the ``--hash=sha256:...`` lines. Local, VCS, and archive pins render
58
+ the same in both variants. Returns the text and, when ``output_path``
56
59
  is provided, atomically writes it.
57
60
  """
58
61
  return _render_requirements(lock_input, with_hashes=False, output_path=output_path)
@@ -94,7 +97,7 @@ def _render_per_tuple_requirements(lock_input: LockInput, *, with_hashes: bool)
94
97
 
95
98
  def _render_pins(pins: Mapping[str, PinShape], *, with_hashes: bool) -> list[str]:
96
99
  """Render a flat ``{name: pin}`` mapping in alphabetical order."""
97
- from ..lockfile import IndexPin, LocalPin, VcsPin
100
+ from ..lockfile import ArchivePin, IndexPin, LocalPin, VcsPin
98
101
 
99
102
  lines: list[str] = []
100
103
  for canonical in sorted(pins):
@@ -112,6 +115,14 @@ def _render_pins(pins: Mapping[str, PinShape], *, with_hashes: bool) -> list[str
112
115
  lines.append(f"{pin.name} @ {url}")
113
116
  elif isinstance(pin, VcsPin):
114
117
  lines.append(f"{pin.name} @ {pin.repo_url}")
118
+ elif isinstance(pin, ArchivePin):
119
+ # The hash is the archive's identity, so carry it (and any
120
+ # subdirectory) in the fragment for a reproducible, hash-checkable
121
+ # install line, mirroring how VcsPin pins its commit in the URL.
122
+ fragment = "&".join(f"{algo}={digest}" for algo, digest in pin.hashes)
123
+ if pin.subdirectory is not None:
124
+ fragment += f"&subdirectory={quote(pin.subdirectory, safe='/')}"
125
+ lines.append(f"{pin.name} @ {pin.url}#{fragment}")
115
126
  else: # pragma: no cover - exhaustive
116
127
  msg = f"unknown pin shape: {pin!r}"
117
128
  raise TypeError(msg)
@@ -77,23 +77,15 @@ def _pick_in_mode(
77
77
  ) -> Version | None:
78
78
  """Pick a candidate honoring ``ExtrasMode``.
79
79
 
80
- Fetches base metadata so an extraction failure (unparseable
81
- PKG-INFO, or an sdist build the policy disallows) becomes a
82
- candidate skip instead of a fatal error during the later
83
- dependency fetch. BACKTRACK mode additionally checks
84
- ``Provides-Extra`` for transitive extras.
85
-
86
- Missing-metadata cases (no PEP 658, no sdist) skip transitive
87
- extras but fall through for user-requested ones; mock test
88
- coordinators rely on this.
80
+ Fetches base metadata so an extraction failure (unparseable PKG-INFO,
81
+ a disallowed sdist build, or no metadata source at all) skips the
82
+ candidate rather than raising later, when the proxy refetches the base
83
+ to expand the extra. This applies to user-requested extras too, since
84
+ the proxy always needs the base metadata. BACKTRACK mode additionally
85
+ checks ``Provides-Extra`` for transitive extras.
89
86
  """
90
87
  # Late import: ``pypi`` imports this module at module load.
91
- from ..provider import (
92
- ExtrasMode,
93
- MetadataError,
94
- UnsupportedSdistError,
95
- _normalize_extra,
96
- )
88
+ from ..provider import ExtrasMode, MetadataError, _normalize_extra
97
89
 
98
90
  _, _, normalized = provider.split_and_normalize(base)
99
91
  is_user = (normalized, extra) in provider.root_extras
@@ -103,12 +95,8 @@ def _pick_in_mode(
103
95
  continue
104
96
  try:
105
97
  provider.get_dependencies(base, version)
106
- except UnsupportedSdistError:
107
- continue
108
98
  except MetadataError:
109
- if not is_user or provider.has_invalid_metadata(normalized, version):
110
- continue
111
- return version
99
+ continue
112
100
  if is_user or not backtrack:
113
101
  return version
114
102
  metadata = provider.metadata_cache.get((normalized, version))
@@ -122,6 +110,32 @@ def _pick_in_mode(
122
110
  return None
123
111
 
124
112
 
113
+ def version_provides_extra(
114
+ provider: Provider,
115
+ base: str,
116
+ extra: str,
117
+ version: Version,
118
+ ) -> bool:
119
+ """Whether ``version`` of ``base`` declares ``extra`` and yields metadata here.
120
+
121
+ Honoring a cross-tuple preference for a ``base[extra]`` proxy is only
122
+ safe when the preferred version both provides the extra and has
123
+ extractable metadata in this tuple.
124
+ """
125
+ # Late import: provider imports this module at module load.
126
+ from ..provider import MetadataError, _normalize_extra
127
+
128
+ _, _, normalized = provider.split_and_normalize(base)
129
+ try:
130
+ provider.get_dependencies(base, version)
131
+ except MetadataError:
132
+ return False
133
+
134
+ metadata = provider.metadata_cache[(normalized, version)]
135
+ provided = {_normalize_extra(e) for e in metadata.provides_extra}
136
+ return extra in provided
137
+
138
+
125
139
  def _record_base_range_blocks(
126
140
  provider: Provider,
127
141
  proxy_pkg: str,
@@ -50,6 +50,14 @@ def fetch_versions(provider: Provider, package: str) -> list[tuple[Version, Dist
50
50
  provider.versions_cache[normalized] = result
51
51
  return result
52
52
 
53
+ archive = provider.archive_sources.get(normalized)
54
+ if archive is not None:
55
+ # The download-and-verify guards are gated everywhere; only the
56
+ # post-extraction success tail needs the tar data filter (see sources.py).
57
+ result = provider.materialize_archive_source(normalized, archive)
58
+ provider.versions_cache[normalized] = result # pragma: no cover
59
+ return result # pragma: no cover
60
+
53
61
  files = provider.coordinator.index.get_listing(normalized)
54
62
  if files is None:
55
63
  event = provider.coordinator.request_listing(normalized)
@@ -572,13 +580,17 @@ def prefetch_new_deps(provider: Provider, deps: Mapping[str, VersionRange]) -> N
572
580
  candidate. This deepens the prefetch cascade so metadata is
573
581
  ready before the resolver asks for it.
574
582
 
575
- Local and VCS sources are skipped; they have no PyPI listing
576
- and the materialise path in ``fetch_versions`` will surface
577
- them when the resolver asks.
583
+ Local, VCS, and archive sources are skipped; they have no PyPI
584
+ listing and the materialise path in ``fetch_versions`` will
585
+ surface them when the resolver asks.
578
586
  """
579
587
  for dep in deps:
580
588
  _, _, normalized = provider.split_and_normalize(dep)
581
- if normalized in provider.local_sources or normalized in provider.vcs_sources:
589
+ if (
590
+ normalized in provider.local_sources
591
+ or normalized in provider.vcs_sources
592
+ or normalized in provider.archive_sources
593
+ ):
582
594
  continue
583
595
  if normalized not in provider.versions_cache:
584
596
  # Listing not cached: request it. When it arrives,
@@ -11,11 +11,11 @@ from __future__ import annotations
11
11
  from typing import TYPE_CHECKING
12
12
 
13
13
  from nab_index.client import SdistFile, WheelFile
14
+ from nab_index.local_index import read_wheel_metadata
14
15
 
15
16
  from .._conflict_kind import EMPTY_MEMBERSHIP_SETS
16
17
  from .._vcs_admission import admit_vcs_url
17
18
  from .._vendor.packaging.ranges import VersionRange
18
- from .._vendor.packaging.requirements import InvalidRequirement, Requirement
19
19
  from .._vendor.packaging.specifiers import SpecifierSet
20
20
  from .._vendor.packaging.utils import canonicalize_name
21
21
  from ..metadata import (
@@ -24,11 +24,18 @@ from ..metadata import (
24
24
  metadata_deps_are_static,
25
25
  parse_metadata,
26
26
  )
27
+ from ..requirements_file import (
28
+ InvalidProjectRequirementError,
29
+ _parse_project_requirement,
30
+ _parse_requirements,
31
+ _require_string_list,
32
+ )
27
33
 
28
34
  if TYPE_CHECKING:
29
35
  from collections.abc import Sequence
30
36
 
31
37
  from .._vendor.packaging.markers import Marker
38
+ from .._vendor.packaging.requirements import Requirement
32
39
  from .._vendor.packaging.version import Version
33
40
  from ..provider import DistFile, Provider
34
41
 
@@ -47,7 +54,7 @@ def resolve_metadata(
47
54
  only sdist values are subject to the :pep:`643` Dynamic
48
55
  guarantees and may need a ``pyproject.toml`` fallback.
49
56
  """
50
- # Late import: ``pypi`` imports this module at module load.
57
+ # Late import: ``provider`` imports this module at module load.
51
58
  from ..provider import MetadataError
52
59
 
53
60
  _, _, normalized = provider.split_and_normalize(package)
@@ -78,6 +85,8 @@ def resolve_metadata(
78
85
  if integrity_error is not None:
79
86
  raise integrity_error
80
87
  metadata_text = provider.coordinator.index.get_metadata(normalized, ver_str)
88
+ elif isinstance(dist, WheelFile) and dist.local_path is not None:
89
+ metadata_text = read_wheel_metadata(dist.local_path)
81
90
  else:
82
91
  metadata_text = None
83
92
 
@@ -210,8 +219,15 @@ def augment_from_pyproject(
210
219
  """Replace dynamic deps with statically-declared pyproject deps.
211
220
 
212
221
  Returns the augmented metadata, or ``None`` if pyproject.toml
213
- is missing, malformed, or itself marks deps dynamic via
222
+ is missing, unparseable, or itself marks deps dynamic via
214
223
  ``[project].dynamic``.
224
+
225
+ Raises :class:`InvalidProjectRequirementError` when ``dependencies``
226
+ or ``optional-dependencies`` is present but structurally wrong (not
227
+ an array of strings / not a table), rather than silently dropping the
228
+ declared dependencies. ``get_dependencies`` catches it and rejects the
229
+ candidate version. A well-typed entry that is not valid PEP 508 is
230
+ dropped with a warning.
215
231
  """
216
232
  # Late import keeps the resolver-time path off ``WheelMetadata``
217
233
  # construction unless the dynamic-deps pyproject fallback fires.
@@ -223,15 +239,16 @@ def augment_from_pyproject(
223
239
  if project is None:
224
240
  return None
225
241
 
226
- deps_field = project.get("dependencies")
227
- if deps_field is not None and not isinstance(deps_field, list):
228
- return None
229
- optional = project.get("optional-dependencies")
230
- if optional is not None and not isinstance(optional, dict):
231
- return None
242
+ deps = _require_string_list(
243
+ project.get("dependencies", []), "[project].dependencies"
244
+ )
245
+ optional = project.get("optional-dependencies", {})
246
+ if not isinstance(optional, dict):
247
+ msg = "[project].optional-dependencies must be a table"
248
+ raise InvalidProjectRequirementError(msg)
232
249
 
233
- requires_dist = list(parse_pyproject_deps(deps_field or []))
234
- provides_extra = extend_with_extras(requires_dist, optional or {})
250
+ requires_dist = list(parse_pyproject_deps(deps))
251
+ provides_extra = extend_with_extras(requires_dist, optional)
235
252
 
236
253
  provider.stats.sdist_pyproject_fallbacks += 1
237
254
  return _WheelMetadata(
@@ -246,38 +263,32 @@ def augment_from_pyproject(
246
263
 
247
264
 
248
265
  def extend_with_extras(requires_dist: list[Requirement], optional: dict) -> list[str]:
249
- """Append extras-gated requirements and return Provides-Extra names."""
250
- # Late import: ``pypi`` imports this module at module load.
251
- from ..provider import _add_extra_marker
266
+ """Append extras-gated requirements and return Provides-Extra names.
252
267
 
268
+ A per-extra value that is not an array of strings, or a per-extra entry
269
+ that is not valid PEP 508, raises :class:`InvalidProjectRequirementError`,
270
+ so the version is rejected rather than resolved with the entry dropped.
271
+ """
253
272
  provides_extra: list[str] = []
254
273
  for extra_name, extra_deps in optional.items():
255
- if not isinstance(extra_deps, list):
256
- continue
274
+ source = f"[project].optional-dependencies extra {extra_name!r}"
257
275
  provides_extra.append(extra_name)
258
- for dep_str in extra_deps:
259
- if not isinstance(dep_str, str):
260
- continue
261
- try:
262
- requires_dist.append(
263
- Requirement(_add_extra_marker(dep_str, extra_name))
264
- )
265
- except InvalidRequirement:
266
- continue
276
+ requires_dist.extend(
277
+ _parse_project_requirement(dep_str, source, extra=extra_name)
278
+ for dep_str in _require_string_list(extra_deps, source)
279
+ )
267
280
  return provides_extra
268
281
 
269
282
 
270
283
  def parse_pyproject_deps(deps: list) -> list[Requirement]:
271
- """Parse a ``project.dependencies`` list, dropping malformed entries."""
272
- out: list[Requirement] = []
273
- for dep_str in deps:
274
- if not isinstance(dep_str, str):
275
- continue
276
- try:
277
- out.append(Requirement(dep_str))
278
- except InvalidRequirement:
279
- continue
280
- return out
284
+ """Parse a ``project.dependencies`` list, raising on a malformed entry.
285
+
286
+ Entries are already validated as strings by :func:`_require_string_list`;
287
+ a string that is not valid PEP 508 raises
288
+ :class:`InvalidProjectRequirementError`, so the whole version is rejected
289
+ rather than resolved with the dependency dropped.
290
+ """
291
+ return _parse_requirements(deps, "[project].dependencies")
281
292
 
282
293
 
283
294
  def find_sdist(
@@ -74,10 +74,12 @@ def compute_matching(
74
74
  if cached is not None:
75
75
  return cached
76
76
 
77
- # Local/VCS sources short-circuit the listing path; their synthetic
77
+ # Local/VCS/archive sources short-circuit the listing path; their synthetic
78
78
  # listing is materialised lazily by fetch_versions.
79
79
  has_local_source = (
80
- normalized in provider.local_sources or normalized in provider.vcs_sources
80
+ normalized in provider.local_sources
81
+ or normalized in provider.vcs_sources
82
+ or normalized in provider.archive_sources
81
83
  )
82
84
  if normalized not in provider.versions_cache and not has_local_source:
83
85
  files = provider.coordinator.index.get_listing(normalized)