Skip to content

Commit 97797b1

Browse files
committed
Test fixes, getting dependencies for pinned requirements in advance.
Signed-off-by: Thomas Neidhart <[email protected]>
1 parent 47feb17 commit 97797b1

File tree

7 files changed

+63
-28
lines changed

7 files changed

+63
-28
lines changed

requirements-dev.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ tomli==1.2.3
3838
tqdm==4.64.0
3939
twine==3.8.0
4040
typed-ast==1.5.4
41-
webencodings==0.5.1
41+
webencodings==0.5.1
42+
pytest-asyncio==0.21.1

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ toml==0.10.2
2525
urllib3==2.1.0
2626
zipp==3.17.0
2727
aiohttp==3.9.1
28+
aiofiles==23.2.1

setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ install_requires =
7070
mock >= 3.0.5
7171
packvers >= 21.5
7272
aiohttp >= 3.9
73+
aiofiles >= 23.1
7374

7475
[options.packages.find]
7576
where = src
@@ -88,6 +89,7 @@ testing =
8889
black
8990
isort
9091
pytest-rerunfailures
92+
pytest-asyncio >= 0.21
9193

9294
docs =
9395
Sphinx>=5.0.2

src/python_inspector/api.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
from _packagedcode.models import DependentPackage
2525
from _packagedcode.models import PackageData
26-
from _packagedcode.pypi import PipRequirementsFileHandler
26+
from _packagedcode.pypi import PipRequirementsFileHandler, get_resolved_purl
2727
from _packagedcode.pypi import PythonSetupPyHandler
2828
from _packagedcode.pypi import can_process_dependent_package
2929
from python_inspector import dependencies
@@ -38,6 +38,7 @@
3838
from python_inspector.resolution import get_python_version_from_env_tag
3939
from python_inspector.resolution import get_reqs_insecurely
4040
from python_inspector.resolution import get_requirements_from_python_manifest
41+
from python_inspector.utils import Candidate
4142
from python_inspector.utils_pypi import PLATFORMS_BY_OS
4243
from python_inspector.utils_pypi import PYPI_SIMPLE_URL
4344
from python_inspector.utils_pypi import Environment
@@ -229,7 +230,7 @@ def resolve_dependencies(
229230
if not direct_dependencies:
230231
return Resolution(
231232
packages=[],
232-
resolution={},
233+
resolution=[],
233234
files=files,
234235
)
235236

@@ -300,7 +301,7 @@ async def get_pypi_data(package):
300301
return data
301302

302303
if verbose:
303-
printer(f"retrieve data from pypi:")
304+
printer(f"retrieve package data from pypi:")
304305

305306
return await asyncio.gather(*[get_pypi_data(package) for package in purls])
306307

@@ -390,6 +391,8 @@ def get_resolved_dependencies(
390391
ignore_errors=ignore_errors,
391392
)
392393

394+
# gather version data for all requirements concurrently in advance.
395+
393396
async def gather_version_data():
394397
async def get_version_data(name: str):
395398
versions = await provider.fill_versions_for_package(name)
@@ -406,6 +409,28 @@ async def get_version_data(name: str):
406409

407410
asyncio.run(gather_version_data())
408411

412+
# gather dependencies for all pinned requirements concurrently in advance.
413+
414+
async def gather_dependencies():
415+
async def get_dependencies(requirement: Requirement):
416+
purl = PackageURL(type="pypi", name=requirement.name)
417+
resolved_purl = get_resolved_purl(purl=purl, specifiers=requirement.specifier)
418+
419+
if resolved_purl:
420+
purl = resolved_purl.purl
421+
candidate = Candidate(requirement.name, purl.version, requirement.extras)
422+
await provider.fill_requirements_for_package(purl, candidate)
423+
424+
if verbose:
425+
printer(f" retrieved dependencies for requirement '{str(purl)}'")
426+
427+
if verbose:
428+
printer(f"dependencies:")
429+
430+
return await asyncio.gather(*[get_dependencies(requirement) for requirement in requirements])
431+
432+
asyncio.run(gather_dependencies())
433+
409434
resolver = Resolver(
410435
provider=provider,
411436
reporter=BaseReporter(),

src/python_inspector/resolution.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ async def _get_versions_for_package_from_repo(
438438
)
439439
if valid_wheel_present or pypi_valid_python_version:
440440
versions.append(version)
441+
441442
return versions
442443

443444
async def _get_versions_for_package_from_pypi_json_api(self, name: str) -> List[Version]:
@@ -556,7 +557,7 @@ async def _get_requirements_for_package_from_pypi_json_api(
556557
return []
557558
info = resp.get("info") or {}
558559
requires_dist = info.get("requires_dist") or []
559-
return requires_dist
560+
return list(map(lambda r: Requirement(r), requires_dist))
560561

561562
def get_candidates(
562563
self,

src/python_inspector/utils_pypi.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@
1818
import tempfile
1919
import time
2020
from collections import defaultdict
21-
from typing import List, Dict
21+
from typing import List, Dict, Union, Tuple
2222
from typing import NamedTuple
2323
from urllib.parse import quote_plus
2424
from urllib.parse import unquote
2525
from urllib.parse import urlparse
2626
from urllib.parse import urlunparse
2727

28+
import aiofiles
2829
import aiohttp
2930
import attr
3031
import packageurl
@@ -1593,7 +1594,7 @@ async def fetch_links(
15931594
name using the `index_url` of this repository.
15941595
"""
15951596
package_url = f"{self.index_url}/{normalized_name}"
1596-
text = await CACHE.get(
1597+
text, _ = await CACHE.get(
15971598
path_or_url=package_url,
15981599
credentials=self.credentials,
15991600
as_text=True,
@@ -1671,7 +1672,7 @@ async def get(
16711672
force=False,
16721673
verbose=False,
16731674
echo_func=None,
1674-
):
1675+
) -> Tuple[Union[str, bytes], str]:
16751676
"""
16761677
Return the content fetched from a ``path_or_url`` through the cache.
16771678
Raise an Exception on errors. Treats the content as text if as_text is
@@ -1692,13 +1693,13 @@ async def get(
16921693
echo_func=echo_func,
16931694
)
16941695
wmode = "w" if as_text else "wb"
1695-
with open(cached, wmode) as fo:
1696-
fo.write(content)
1697-
return content
1696+
async with aiofiles.open(cached, mode=wmode) as fo:
1697+
await fo.write(content)
1698+
return content, cached
16981699
else:
16991700
if TRACE_DEEP:
17001701
print(f" FILE CACHE HIT: {path_or_url}")
1701-
return get_local_file_content(path=cached, as_text=as_text)
1702+
return await get_local_file_content(path=cached, as_text=as_text), cached
17021703

17031704

17041705
CACHE = Cache()
@@ -1730,13 +1731,13 @@ async def get_file_content(
17301731
elif path_or_url.startswith("file://") or (
17311732
path_or_url.startswith("/") and os.path.exists(path_or_url)
17321733
):
1733-
return get_local_file_content(path=path_or_url, as_text=as_text)
1734+
return await get_local_file_content(path=path_or_url, as_text=as_text)
17341735

17351736
else:
17361737
raise Exception(f"Unsupported URL scheme: {path_or_url}")
17371738

17381739

1739-
def get_local_file_content(path, as_text=True):
1740+
async def get_local_file_content(path: str, as_text=True) -> str:
17401741
"""
17411742
Return the content at `url` as text. Return the content as bytes is
17421743
`as_text` is False.
@@ -1745,8 +1746,8 @@ def get_local_file_content(path, as_text=True):
17451746
path = path[7:]
17461747

17471748
mode = "r" if as_text else "rb"
1748-
with open(path, mode) as fo:
1749-
return fo.read()
1749+
async with aiofiles.open(path, mode=mode) as fo:
1750+
return await fo.read()
17501751

17511752

17521753
class RemoteNotFetchedException(Exception):
@@ -1828,7 +1829,7 @@ async def fetch_and_save(
18281829
errors. Treats the content as text if as_text is True otherwise as treat as
18291830
binary.
18301831
"""
1831-
content = await CACHE.get(
1832+
content, path = await CACHE.get(
18321833
path_or_url=path_or_url,
18331834
credentials=credentials,
18341835
as_text=as_text,
@@ -1837,7 +1838,8 @@ async def fetch_and_save(
18371838
)
18381839

18391840
output = os.path.join(dest_dir, filename)
1840-
wmode = "w" if as_text else "wb"
1841-
with open(output, wmode) as fo:
1842-
fo.write(content)
1841+
if os.path.exists(output):
1842+
os.remove(output)
1843+
1844+
os.symlink(os.path.abspath(path), output)
18431845
return content

tests/test_utils.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from netrc import netrc
1515
from unittest import mock
1616

17+
import pytest
1718
from commoncode.testcase import FileDrivenTesting
1819
from test_cli import check_json_file_results
1920

@@ -47,22 +48,23 @@ def test_get_netrc_auth_with_no_matching_url():
4748
assert get_netrc_auth(url="https://pypi2.org/simple", netrc=parsed_netrc) == (None, None)
4849

4950

51+
@pytest.mark.asyncio
5052
@mock.patch("python_inspector.utils_pypi.CACHE.get")
51-
def test_fetch_links(mock_get):
53+
async def test_fetch_links(mock_get):
5254
file_name = test_env.get_test_loc("psycopg2.html")
5355
with open(file_name) as file:
5456
mock_get.return_value = file.read()
55-
links = PypiSimpleRepository().fetch_links(normalized_name="psycopg2")
57+
links = await PypiSimpleRepository().fetch_links(normalized_name="psycopg2")
5658
result_file = test_env.get_temp_file("json")
5759
expected_file = test_env.get_test_loc("psycopg2-links-expected.json", must_exist=False)
5860
with open(result_file, "w") as file:
5961
json.dump(links, file, indent=4)
6062
check_json_file_results(result_file, expected_file)
6163
# Testing relative links
62-
realtive_links_file = test_env.get_test_loc("fetch_links_test.html")
63-
with open(realtive_links_file) as realtive_file:
64-
mock_get.return_value = realtive_file.read()
65-
relative_links = PypiSimpleRepository().fetch_links(normalized_name="sources.whl")
64+
relative_links_file = test_env.get_test_loc("fetch_links_test.html")
65+
with open(relative_links_file) as relative_file:
66+
mock_get.return_value = relative_file.read()
67+
relative_links = await PypiSimpleRepository().fetch_links(normalized_name="sources.whl")
6668
relative_links_result_file = test_env.get_temp_file("json")
6769
relative_links_expected_file = test_env.get_test_loc(
6870
"relative-links-expected.json", must_exist=False
@@ -83,8 +85,9 @@ def test_parse_reqs():
8385
check_json_file_results(result_file, expected_file)
8486

8587

86-
def test_get_sdist_file():
87-
sdist_file = fetch_and_extract_sdist(
88+
@pytest.mark.asyncio
89+
async def test_get_sdist_file():
90+
sdist_file = await fetch_and_extract_sdist(
8891
repos=tuple([PypiSimpleRepository()]),
8992
candidate=Candidate(name="psycopg2", version="2.7.5", extras=None),
9093
python_version="3.8",

0 commit comments

Comments
 (0)