Skip to content

Commit 11583a9

Browse files
committed
Do not use PyPI.org JSON API by default
* Add new CLI flag "--use-pypi-json-api" to use exclusively the PyPI fast but incorrect JSON API. * Remove pypi_simple_repo_in_repos() from 'python_inspector.resolution' This is not longer needed * Also remove dangerous globals from function and methods arguments: * PythonInputProvider.__init__() where repos was a mutable list * get_resolved_dependencies() where repos was a mutable list * Refactor methods that dealt with PyPI JSON vs. simple by splitting them in two specialized methods Signed-off-by: Philippe Ombredanne <pombredanne@nexb.com>
1 parent ebb8cc9 commit 11583a9

4 files changed

Lines changed: 100 additions & 86 deletions

File tree

src/python_inspector/resolution.py

Lines changed: 68 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import operator
1212
import os
1313
from typing import List
14+
from typing import Sequence
1415

1516
import packaging.requirements
1617
import packaging.utils
@@ -51,9 +52,9 @@ def is_valid_version(parsed_version, requirements, identifier, bad_versions):
5152

5253

5354
class PythonInputProvider(AbstractProvider):
54-
def __init__(self, environment=None, repos=[]):
55+
def __init__(self, environment=None, repos=tuple()):
5556
self.environment = environment
56-
self.repos = repos
57+
self.repos = repos or []
5758
self.versions_by_package = {}
5859
self.dependencies_by_purl = {}
5960

@@ -82,54 +83,75 @@ def get_versions_for_package(self, name, repo=None):
8283
"""
8384
Return a list of versions for a package.
8485
"""
85-
versions = []
8686
if repo and self.environment:
87-
for version, package in repo._get_package_versions_map(name).items():
88-
wheels = package.get_supported_wheels(environment=self.environment)
89-
if list(wheels):
90-
versions.append(version)
87+
return self.get_versions_for_package_from_repo(name, repo)
9188
else:
92-
if name not in self.versions_by_package:
93-
api_url = f"https://pypi.org/pypi/{name}/json"
94-
resp = get_response(api_url)
95-
if not resp:
96-
self.versions_by_package[name] = []
97-
releases = resp.get("releases") or {}
98-
self.versions_by_package[name] = releases.keys() or []
99-
versions = self.versions_by_package[name]
89+
return self.get_versions_for_package_from_pypi_json_api(name)
90+
91+
def get_versions_for_package_from_repo(self, name, repo):
92+
"""
93+
Return a list of versions for a package name from a repo
94+
"""
95+
versions = []
96+
for version, package in repo._get_package_versions_map(name).items():
97+
wheels = package.get_supported_wheels(environment=self.environment)
98+
if list(wheels):
99+
versions.append(version)
100+
return versions
101+
102+
def get_versions_for_package_from_pypi_json_api(self, name):
103+
"""
104+
Return a list of versions for a package name from the PyPI.org JSON API
105+
"""
106+
if name not in self.versions_by_package:
107+
api_url = f"https://pypi.org/pypi/{name}/json"
108+
resp = get_response(api_url)
109+
if not resp:
110+
self.versions_by_package[name] = []
111+
releases = resp.get("releases") or {}
112+
self.versions_by_package[name] = releases.keys() or []
113+
versions = self.versions_by_package[name]
100114
return versions
101115

102116
def get_requirements_for_package(self, purl, candidate):
103117
"""
104118
Yield requirements for a package.
105119
"""
106120
if self.repos and self.environment:
107-
wheels = utils_pypi.download_wheel(
108-
name=candidate.name,
109-
version=str(candidate.version),
110-
environment=self.environment,
111-
repos=self.repos,
112-
)
113-
for wheel in wheels:
114-
deps = list(
115-
PypiWheelHandler.parse(os.path.join(utils_pypi.CACHE_THIRDPARTY_DIR, wheel))
116-
)
117-
assert len(deps) == 1
118-
deps = deps[0].dependencies
119-
for dep in deps:
120-
if dep.scope == "install":
121-
yield packaging.requirements.Requirement(str(dep.extracted_requirement))
121+
return self.get_requirements_for_package_from_pypi_simple(candidate)
122122
else:
123-
if str(purl) not in self.dependencies_by_purl:
124-
api_url = f"https://pypi.org/pypi/{purl.name}/{purl.version}/json"
125-
resp = get_response(api_url)
126-
if not resp:
127-
self.dependencies_by_purl[str(purl)] = []
128-
info = resp.get("info") or {}
129-
requires_dist = info.get("requires_dist") or []
130-
self.dependencies_by_purl[str(purl)] = requires_dist
131-
for dependency in self.dependencies_by_purl[str(purl)]:
132-
yield packaging.requirements.Requirement(dependency)
123+
return self.get_requirements_for_package_from_pypi_json_api(purl)
124+
125+
def get_requirements_for_package_from_pypi_simple(self, candidate):
126+
wheels = utils_pypi.download_wheel(
127+
name=candidate.name,
128+
version=str(candidate.version),
129+
environment=self.environment,
130+
repos=self.repos,
131+
)
132+
for wheel in wheels:
133+
deps = list(
134+
PypiWheelHandler.parse(os.path.join(utils_pypi.CACHE_THIRDPARTY_DIR, wheel))
135+
)
136+
assert len(deps) == 1
137+
deps = deps[0].dependencies
138+
for dep in deps:
139+
if dep.scope == "install":
140+
yield packaging.requirements.Requirement(str(dep.extracted_requirement))
141+
142+
def get_requirements_for_package_from_pypi_json_api(self, purl):
143+
144+
# if no repos are provided use the incorrect but fast JSON API
145+
if str(purl) not in self.dependencies_by_purl:
146+
api_url = f"https://pypi.org/pypi/{purl.name}/{purl.version}/json"
147+
resp = get_response(api_url)
148+
if not resp:
149+
self.dependencies_by_purl[str(purl)] = []
150+
info = resp.get("info") or {}
151+
requires_dist = info.get("requires_dist") or []
152+
self.dependencies_by_purl[str(purl)] = requires_dist
153+
for dependency in self.dependencies_by_purl[str(purl)]:
154+
yield packaging.requirements.Requirement(dependency)
133155

134156
def get_candidates(self, all_versions, requirements, identifier, bad_versions, name, extras):
135157
"""
@@ -267,29 +289,22 @@ def format_resolution(results, as_tree=False):
267289
return dependencies
268290

269291

270-
def pypi_simple_repo_in_repos(repos: utils_pypi.PypiSimpleRepository):
271-
"""
272-
Return True if simple pypi index_url is present in any of the repos
273-
"""
274-
return any(repo.index_url == utils_pypi.PYPI_SIMPLE_URL for repo in repos)
275-
276-
277292
def get_resolved_dependencies(
278293
requirements: List[Requirement],
279294
environment: utils_pypi.Environment = None,
280-
repos: List[utils_pypi.PypiSimpleRepository] = [],
295+
repos: Sequence[utils_pypi.PypiSimpleRepository] = tuple(),
281296
as_tree: bool = False,
282297
):
283298
"""
284299
Return resolved dependencies of a ``requirements`` list of Requirement for
285300
an ``enviroment`` Environment. The resolved dependencies are formatted as
286-
parent/children or a nested tree if ``as_tree`` is True
287-
"""
288-
if repos and not pypi_simple_repo_in_repos(repos):
289-
repos.append(utils_pypi.PYPI_PUBLIC_REPO)
301+
parent/children or a nested tree if ``as_tree`` is True.
290302
303+
Used the provided ``repos`` list of PypiSimpleRepository.
304+
If empty, use instead the PyPI.org JSON API exclusively instead
305+
"""
291306
resolver = Resolver(
292-
provider=PythonInputProvider(environment, repos),
307+
provider=PythonInputProvider(environment=environment, repos=repos),
293308
reporter=BaseReporter(),
294309
)
295310
results = resolver.resolve(requirements=requirements)

src/python_inspector/resolve_cli.py

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,13 @@
9494
"--use-cached-index",
9595
is_flag=True,
9696
hidden=True,
97-
help="Use cached on-disk PyPI package indexes and do not refetch if present.",
97+
help="Use cached on-disk PyPI simple package indexes and do not refetch if present.",
98+
)
99+
@click.option(
100+
"--use-pypi-json-api",
101+
is_flag=True,
102+
help="Use PyPI JSON API to fetch dependency data. Faster but not always correct. "
103+
"--index-url are ignored when this option is active.",
98104
)
99105
@click.option(
100106
"--debug",
@@ -111,6 +117,7 @@ def resolve_dependencies(
111117
index_urls,
112118
json_output,
113119
use_cached_index=False,
120+
use_pypi_json_api=False,
114121
debug=TRACE,
115122
):
116123
"""
@@ -126,16 +133,10 @@ def resolve_dependencies(
126133
127134
Error and progress are printed to stderr.
128135
129-
1) Without an --index-url is provided, this tool uses the PyPI JSON API.
130-
131136
For example, display the results of resolving the dependencies for flask==2.1.2
132137
on screen::
133138
134139
dad --spec "flask==2.1.2" --json -
135-
136-
2) If an --index-url is provided, it is used to resolve the dependencies::
137-
138-
dad --spec "flask==2.1.2" --index-url https://pypi.org/simple --json -
139140
"""
140141

141142
click.secho(f"Resolving dependencies...")
@@ -158,35 +159,36 @@ def resolve_dependencies(
158159
if debug:
159160
click.secho("direct_dependencies:")
160161
for dep in direct_dependencies:
161-
click.secho(" ", dep)
162+
click.secho(f" {dep}")
162163

163164
# create a resolution environments
164165
environment = utils_pypi.Environment.from_pyver_and_os(
165166
python_version=python_version, operating_system=operating_system
166167
)
167168

168169
if debug:
169-
click.secho("environment:", environment)
170+
click.secho(f"environment: {environment}")
170171

171-
# Collect PyPI repos
172172
repos = []
173-
for index_url in index_urls:
174-
index_url = index_url.strip("/")
175-
existing = utils_pypi.DEFAULT_PYPI_REPOS_BY_URL.get(index_url)
176-
if existing:
177-
existing.use_cached_index = use_cached_index
178-
repos.append(existing)
179-
else:
180-
repo = utils_pypi.PypiSimpleRepository(
181-
index_url=index_url,
182-
use_cached_index=use_cached_index,
183-
)
184-
repos.append(repo)
173+
if not use_pypi_json_api:
174+
# Collect PyPI repos
175+
for index_url in index_urls:
176+
index_url = index_url.strip("/")
177+
existing = utils_pypi.DEFAULT_PYPI_REPOS_BY_URL.get(index_url)
178+
if existing:
179+
existing.use_cached_index = use_cached_index
180+
repos.append(existing)
181+
else:
182+
repo = utils_pypi.PypiSimpleRepository(
183+
index_url=index_url,
184+
use_cached_index=use_cached_index,
185+
)
186+
repos.append(repo)
185187

186188
if debug:
187189
click.secho("repos:")
188190
for repo in repos:
189-
click.secho(" ", repo)
191+
click.secho(f" {repo}")
190192

191193
# resolve dependencies proper
192194
requirements, resolved_dependencies = resolve(
@@ -230,11 +232,13 @@ def resolve_dependencies(
230232
click.secho("done!")
231233

232234

233-
def resolve(direct_dependencies, environment, repos, as_tree=False):
235+
def resolve(direct_dependencies, environment, repos=tuple(), as_tree=False):
234236
"""
235237
Resolve dependencies given a ``direct_dependencies`` list of
236238
DependentPackage and return a tuple of (initial_requirements,
237239
resolved_dependencies).
240+
Used the provided ``repos`` list of PypiSimpleRepository.
241+
If empty, use instead the PyPI.org JSON API exclusively.
238242
"""
239243

240244
requirements = [

tests/test_cli.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,13 @@
2929
def test_cli_with_default_urls():
3030
expected_file = test_env.get_test_loc("default-url-expected.json", must_exist=False)
3131
specifier = "zipp==3.8.0"
32+
extra_options = [
33+
"--use-pypi-json-api",
34+
]
3235
check_specs_resolution(
3336
specifier=specifier,
3437
expected_file=expected_file,
38+
extra_options=extra_options,
3539
regen=REGEN_TEST_FIXTURES,
3640
)
3741

tests/test_resolution.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
from python_inspector.resolution import get_resolved_dependencies
1616
from python_inspector.resolution import is_valid_version
17-
from python_inspector.resolution import pypi_simple_repo_in_repos
1817
from python_inspector.utils_pypi import PYPI_PUBLIC_REPO
1918
from python_inspector.utils_pypi import Environment
2019

@@ -87,14 +86,6 @@ def test_get_resolved_dependencies_with_tilde_requirement_using_json_api():
8786
]
8887

8988

90-
def test_pypi_simple_repo_in_repos():
91-
assert pypi_simple_repo_in_repos(repos=[PYPI_PUBLIC_REPO])
92-
93-
94-
def test_pypi_simple_repo_in_repos_not_present():
95-
assert not pypi_simple_repo_in_repos(repos=[])
96-
97-
9889
def test_is_valid_version():
9990
parsed_version = packaging.version.parse("2.1.2")
10091
requirements = {"flask": [Requirement("flask>2.0.0")]}

0 commit comments

Comments
 (0)