Skip to content

Commit a04a634

Browse files
committed
Add tests and address review comments
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent a6cb7d4 commit a04a634

10 files changed

Lines changed: 3034 additions & 134 deletions

File tree

src/_packagedcode/pypi.py

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,9 @@ def parse(cls, location):
741741

742742
@classmethod
743743
def parse_reqs(cls, reqs, scope):
744+
"""
745+
Parse a list of requirements and return a list of dependencies
746+
"""
744747
dependent_packages = []
745748
for req in reqs:
746749
is_resolved = False
@@ -1303,10 +1306,10 @@ def parse_with_dparse2(location, file_name=None):
13031306
dependent_packages = []
13041307

13051308
for dependency in dep_file.dependencies:
1306-
name = dependency.name
1309+
requirement = dependency.name
13071310
is_resolved = False
13081311
purl = PackageURL(type='pypi', name=dependency.name)
1309-
extracted_requirement = name
1312+
13101313
# note: dparse2.dependencies.Dependency.specs comes from
13111314
# packaging.requirements.Requirement.specifier
13121315
# which in turn is a packaging.specifiers.SpecifierSet objects
@@ -1329,8 +1332,6 @@ def parse_with_dparse2(location, file_name=None):
13291332
is_resolved = True
13301333
purl = purl._replace(version=specifier.version)
13311334

1332-
extracted_requirement = f"{name}{requirement}"
1333-
13341335
dependent_packages.append(
13351336
models.DependentPackage(
13361337
purl=purl.to_string(),
@@ -1339,7 +1340,7 @@ def parse_with_dparse2(location, file_name=None):
13391340
is_runtime=True,
13401341
is_optional=False,
13411342
is_resolved=is_resolved,
1342-
extracted_requirement=extracted_requirement
1343+
extracted_requirement=requirement
13431344
)
13441345
)
13451346

@@ -1936,22 +1937,22 @@ def get_requirement_from_section(section, sub_section):
19361937
"""
19371938
Generate requirements from the `sub_section`
19381939
"""
1939-
content = section.get(sub_section)
1940-
if content:
1941-
for req in content.splitlines():
1942-
if req:
1943-
#pytest-mypy >= 0.9.1; \
1944-
req = req.replace("; \\", "")
1945-
# pip>=19.1 # For proper file:// URLs support.
1946-
if "#" in req:
1947-
req , _ = req.rsplit("#")
1948-
#pure-eval; black; tox;
1949-
req_split_by_semi_colon = req.split(";")
1950-
req_split_by_semi_colon = [req.strip() for req in req_split_by_semi_colon]
1951-
if len(req_split_by_semi_colon) >= 2 and not(req_split_by_semi_colon[1].startswith("python_version") # pip>=19.1 ;python_version > 3.7
1952-
or req_split_by_semi_colon[1].startswith("sys_platform") # pip>=19.1 ;sys_platform = "Windows"
1953-
or req_split_by_semi_colon[1].startswith("platform_system")): # pip>=19.1 ;platform_system = "Windows"
1954-
for temp_req in req_split_by_semi_colon:
1955-
yield temp_req
1956-
else:
1957-
yield req
1940+
content = section.get(sub_section) or ""
1941+
for req in content.splitlines():
1942+
if req:
1943+
#pytest-mypy >= 0.9.1; \
1944+
req = req.replace("; \\", "")
1945+
# pip>=19.1 # For proper file:// URLs support.
1946+
if "#" in req:
1947+
req , _ = req.rsplit("#")
1948+
#pure-eval; black; tox
1949+
req_split_by_semi_colon = req.split(";")
1950+
req_split_by_semi_colon = [req.strip() for req in req_split_by_semi_colon if req]
1951+
if len(req_split_by_semi_colon) >= 2 and not(req_split_by_semi_colon[1].startswith("python_version") # pip>=19.1 ;python_version > 3.7
1952+
or req_split_by_semi_colon[1].startswith("sys_platform") # pip>=19.1 ;sys_platform = "Windows"
1953+
or req_split_by_semi_colon[1].startswith("platform_system") # pip>=19.1 ;platform_system = "Windows"
1954+
or req_split_by_semi_colon[1].startswith("platform_python_implementation")):#pytest-black>=0.3.7; platform_python_implementation != "PyPy"
1955+
for temp_req in req_split_by_semi_colon:
1956+
yield temp_req
1957+
else:
1958+
yield req

src/python_inspector/resolution.py

Lines changed: 72 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,35 @@ def get_python_version_from_env_tag(python_version: str):
6969
return python_version
7070

7171

72+
def get_sdist_file(repos, candidate):
73+
"""
74+
Return the sdist file for a candidate.
75+
"""
76+
sdist = utils_pypi.download_sdist(
77+
name=candidate.name,
78+
version=str(candidate.version),
79+
repos=repos,
80+
)
81+
sdist_file = None
82+
83+
if sdist.endswith(".tar.gz"):
84+
sdist_file = sdist.rstrip(".tar.gz")
85+
with tarfile.open(os.path.join(utils_pypi.CACHE_THIRDPARTY_DIR, sdist)) as file:
86+
file.extractall(
87+
os.path.join(utils_pypi.CACHE_THIRDPARTY_DIR, "extracted_sdists", sdist_file)
88+
)
89+
if sdist.endswith(".zip"):
90+
sdist_file = sdist.rstrip(".zip")
91+
with ZipFile(os.path.join(utils_pypi.CACHE_THIRDPARTY_DIR, sdist)) as zip:
92+
zip.extractall(
93+
os.path.join(utils_pypi.CACHE_THIRDPARTY_DIR, "extracted_sdists", sdist_file)
94+
)
95+
96+
if not sdist_file:
97+
raise Exception(f"Unable to extract sdist {sdist}")
98+
return sdist_file
99+
100+
72101
class PythonInputProvider(AbstractProvider):
73102
def __init__(self, environment=None, repos=tuple(), resolved_requirements=[]):
74103
self.environment = environment
@@ -170,7 +199,9 @@ def get_requirements_for_package(self, purl, candidate):
170199
return self.get_requirements_for_package_from_pypi_json_api(purl)
171200

172201
def get_requirements_for_package_from_pypi_simple(self, candidate):
173-
202+
"""
203+
Return requirements for a package from the simple repositories.
204+
"""
174205
purl = PackageURL(type="pypi", name=candidate.name, version=str(candidate.version))
175206

176207
formats = self.wheel_or_sdist_by_package[str(purl)]
@@ -194,28 +225,7 @@ def get_requirements_for_package_from_pypi_simple(self, candidate):
194225
yield packaging.requirements.Requirement(str(dep.extracted_requirement))
195226

196227
if format == "Sdist":
197-
sdist = utils_pypi.download_sdist(
198-
name=candidate.name,
199-
version=str(candidate.version),
200-
repos=self.repos,
201-
)
202-
if sdist.endswith(".tar.gz"):
203-
sdist_file = sdist.rstrip(".tar.gz")
204-
file = tarfile.open(os.path.join(utils_pypi.CACHE_THIRDPARTY_DIR, sdist))
205-
file.extractall(
206-
os.path.join(
207-
utils_pypi.CACHE_THIRDPARTY_DIR, "extracted_sdists", sdist_file
208-
)
209-
)
210-
file.close()
211-
if sdist.endswith(".zip"):
212-
sdist_file = sdist.rstrip(".zip")
213-
with ZipFile(os.path.join(utils_pypi.CACHE_THIRDPARTY_DIR, sdist), "r") as zip:
214-
zip.extractall(
215-
os.path.join(
216-
utils_pypi.CACHE_THIRDPARTY_DIR, "extracted_sdists", sdist_file
217-
)
218-
)
228+
sdist_file = get_sdist_file(repos=self.repos, candidate=candidate)
219229
setup_py_path = os.path.join(
220230
utils_pypi.CACHE_THIRDPARTY_DIR,
221231
"extracted_sdists",
@@ -241,48 +251,47 @@ def get_requirements_for_package_from_pypi_simple(self, candidate):
241251
"requirements.txt",
242252
)
243253

244-
path_by_format = {
245-
"pkginfo": pkg_info_path,
246-
"setup-py": setup_py_path,
247-
"setup-cfg": setup_cfg_path,
248-
"requirement": requirement_path,
254+
path_by_sdist_parser = {
255+
PythonSdistPkgInfoFile: pkg_info_path,
256+
PythonSetupPyHandler: setup_py_path,
257+
SetupCfgHandler: setup_cfg_path,
258+
PipRequirementsFileHandler: requirement_path,
249259
}
250260

251-
handler_by_format = {
252-
"pkginfo": PythonSdistPkgInfoFile,
253-
"setup-py": PythonSetupPyHandler,
254-
"setup-cfg": SetupCfgHandler,
255-
"requirement": PipRequirementsFileHandler,
256-
}
257-
for format in ["pkginfo", "setup-py", "setup-cfg", "requirement"]:
258-
path = path_by_format[format]
259-
if os.path.exists(path):
260-
handler = handler_by_format[format]
261-
deps = list(handler.parse(path))
262-
assert len(deps) == 1
263-
dependencies = deps[0].dependencies
264-
for dep in dependencies:
265-
# skip if no purl can be extracted for dependency
266-
if dep.purl:
267-
dep_purl = PackageURL.from_string(dep.purl)
268-
if dep.scope == "install" and (
269-
not (dep.is_resolved)
270-
or (
271-
dep.is_resolved
272-
and dep_purl.name not in self.resolved_requirements
273-
)
274-
):
275-
if dep.is_resolved:
276-
self.resolved_requirements.append(dep_purl)
277-
# skip the requirement starting with -- like
278-
# --editable, --requirement
279-
if not dep.extracted_requirement.startswith("--"):
280-
yield packaging.requirements.Requirement(
281-
str(dep.extracted_requirement)
282-
)
261+
for handler, path in path_by_sdist_parser.items():
262+
if not os.path.exists(path):
263+
continue
283264

284-
def get_requirements_for_package_from_pypi_json_api(self, purl):
265+
deps = list(handler.parse(path))
266+
assert len(deps) == 1
267+
dependencies = deps[0].dependencies
268+
for dep in dependencies:
269+
if not dep.purl:
270+
continue
271+
272+
dep_purl = PackageURL.from_string(dep.purl)
273+
if not (
274+
dep.scope == "install"
275+
and (
276+
not (dep.is_resolved)
277+
or (
278+
dep.is_resolved
279+
and dep_purl.name not in self.resolved_requirements
280+
)
281+
)
282+
):
283+
continue
284+
if dep.is_resolved:
285+
self.resolved_requirements.append(dep_purl)
286+
# skip the requirement starting with -- like
287+
# --editable, --requirement
288+
if not dep.extracted_requirement.startswith("--"):
289+
yield packaging.requirements.Requirement(str(dep.extracted_requirement))
285290

291+
def get_requirements_for_package_from_pypi_json_api(self, purl):
292+
"""
293+
Return requirements for a package from the PyPI.org JSON API
294+
"""
286295
# if no repos are provided use the incorrect but fast JSON API
287296
if str(purl) not in self.dependencies_by_purl:
288297
api_url = f"https://pypi.org/pypi/{purl.name}/{purl.version}/json"
@@ -455,11 +464,10 @@ def get_resolved_dependencies(
455464
If empty, use instead the PyPI.org JSON API exclusively instead
456465
"""
457466
resolved_requirements = [
458-
packaging.utils.canonicalize_name(r["requirement"].name)
467+
packaging.utils.canonicalize_name(r.name)
459468
for r in requirements
460-
if r["is_requirement_resolved"]
469+
if getattr(r, "is_requirement_resolved", False)
461470
]
462-
requirements = [r["requirement"] for r in requirements]
463471
resolver = Resolver(
464472
provider=PythonInputProvider(
465473
environment=environment, repos=repos, resolved_requirements=resolved_requirements

src/python_inspector/resolve_cli.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,9 @@ def resolve_dependencies(
167167
# TODO: deduplicate me
168168
direct_dependencies = []
169169

170+
if PYPI_SIMPLE_URL not in index_urls:
171+
index_urls = (*index_urls, PYPI_SIMPLE_URL)
172+
170173
for req_file in requirement_files:
171174
deps = dependencies.get_dependencies_from_requirements(requirements_file=req_file)
172175
for extra_data in dependencies.get_extra_data_from_requirements(requirements_file=req_file):
@@ -273,13 +276,14 @@ def resolve(direct_dependencies, environment, repos=tuple(), as_tree=False, max_
273276
Used the provided ``repos`` list of PypiSimpleRepository.
274277
If empty, use instead the PyPI.org JSON API exclusively.
275278
"""
276-
requirements = [
277-
dict(
278-
requirement=Requirement(requirement_string=d.extracted_requirement),
279-
is_requirement_resolved=d.is_resolved,
280-
)
281-
for d in direct_dependencies
282-
]
279+
280+
requirements = []
281+
282+
for dependency in direct_dependencies:
283+
requirement = Requirement(requirement_string=dependency.extracted_requirement)
284+
requirement.is_requirement_resolved = dependency.is_resolved
285+
requirements.append(requirement)
286+
283287
resolved_dependencies = get_resolved_dependencies(
284288
requirements=requirements,
285289
environment=environment,

0 commit comments

Comments
 (0)