Skip to content

Commit a5db072

Browse files
committed
Test fixes, getting dependencies for pinned requirements in advance.
Signed-off-by: Thomas Neidhart <thomas.neidhart@gmail.com>
1 parent 0d4dff6 commit a5db072

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
@@ -230,7 +231,7 @@ def resolve_dependencies(
230231
if not direct_dependencies:
231232
return Resolution(
232233
packages=[],
233-
resolution={},
234+
resolution=[],
234235
files=files,
235236
)
236237

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

303304
if verbose:
304-
printer(f"retrieve data from pypi:")
305+
printer(f"retrieve package data from pypi:")
305306

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

@@ -391,6 +392,8 @@ def get_resolved_dependencies(
391392
ignore_errors=ignore_errors,
392393
)
393394

395+
# gather version data for all requirements concurrently in advance.
396+
394397
async def gather_version_data():
395398
async def get_version_data(name: str):
396399
versions = await provider.fill_versions_for_package(name)
@@ -407,6 +410,28 @@ async def get_version_data(name: str):
407410

408411
asyncio.run(gather_version_data())
409412

413+
# gather dependencies for all pinned requirements concurrently in advance.
414+
415+
async def gather_dependencies():
416+
async def get_dependencies(requirement: Requirement):
417+
purl = PackageURL(type="pypi", name=requirement.name)
418+
resolved_purl = get_resolved_purl(purl=purl, specifiers=requirement.specifier)
419+
420+
if resolved_purl:
421+
purl = resolved_purl.purl
422+
candidate = Candidate(requirement.name, purl.version, requirement.extras)
423+
await provider.fill_requirements_for_package(purl, candidate)
424+
425+
if verbose:
426+
printer(f" retrieved dependencies for requirement '{str(purl)}'")
427+
428+
if verbose:
429+
printer(f"dependencies:")
430+
431+
return await asyncio.gather(*[get_dependencies(requirement) for requirement in requirements])
432+
433+
asyncio.run(gather_dependencies())
434+
410435
resolver = Resolver(
411436
provider=provider,
412437
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
@@ -1600,7 +1601,7 @@ async def fetch_links(
16001601
name using the `index_url` of this repository.
16011602
"""
16021603
package_url = f"{self.index_url}/{normalized_name}"
1603-
text = await CACHE.get(
1604+
text, _ = await CACHE.get(
16041605
path_or_url=package_url,
16051606
credentials=self.credentials,
16061607
as_text=True,
@@ -1678,7 +1679,7 @@ async def get(
16781679
force=False,
16791680
verbose=False,
16801681
echo_func=None,
1681-
):
1682+
) -> Tuple[Union[str, bytes], str]:
16821683
"""
16831684
Return the content fetched from a ``path_or_url`` through the cache.
16841685
Raise an Exception on errors. Treats the content as text if as_text is
@@ -1699,13 +1700,13 @@ async def get(
16991700
echo_func=echo_func,
17001701
)
17011702
wmode = "w" if as_text else "wb"
1702-
with open(cached, wmode) as fo:
1703-
fo.write(content)
1704-
return content
1703+
async with aiofiles.open(cached, mode=wmode) as fo:
1704+
await fo.write(content)
1705+
return content, cached
17051706
else:
17061707
if TRACE_DEEP:
17071708
print(f" FILE CACHE HIT: {path_or_url}")
1708-
return get_local_file_content(path=cached, as_text=as_text)
1709+
return await get_local_file_content(path=cached, as_text=as_text), cached
17091710

17101711

17111712
CACHE = Cache()
@@ -1737,13 +1738,13 @@ async def get_file_content(
17371738
elif path_or_url.startswith("file://") or (
17381739
path_or_url.startswith("/") and os.path.exists(path_or_url)
17391740
):
1740-
return get_local_file_content(path=path_or_url, as_text=as_text)
1741+
return await get_local_file_content(path=path_or_url, as_text=as_text)
17411742

17421743
else:
17431744
raise Exception(f"Unsupported URL scheme: {path_or_url}")
17441745

17451746

1746-
def get_local_file_content(path, as_text=True):
1747+
async def get_local_file_content(path: str, as_text=True) -> str:
17471748
"""
17481749
Return the content at `url` as text. Return the content as bytes is
17491750
`as_text` is False.
@@ -1752,8 +1753,8 @@ def get_local_file_content(path, as_text=True):
17521753
path = path[7:]
17531754

17541755
mode = "r" if as_text else "rb"
1755-
with open(path, mode) as fo:
1756-
return fo.read()
1756+
async with aiofiles.open(path, mode=mode) as fo:
1757+
return await fo.read()
17571758

17581759

17591760
class RemoteNotFetchedException(Exception):
@@ -1835,7 +1836,7 @@ async def fetch_and_save(
18351836
errors. Treats the content as text if as_text is True otherwise as treat as
18361837
binary.
18371838
"""
1838-
content = await CACHE.get(
1839+
content, path = await CACHE.get(
18391840
path_or_url=path_or_url,
18401841
credentials=credentials,
18411842
as_text=as_text,
@@ -1844,7 +1845,8 @@ async def fetch_and_save(
18441845
)
18451846

18461847
output = os.path.join(dest_dir, filename)
1847-
wmode = "w" if as_text else "wb"
1848-
with open(output, wmode) as fo:
1849-
fo.write(content)
1848+
if os.path.exists(output):
1849+
os.remove(output)
1850+
1851+
os.symlink(os.path.abspath(path), output)
18501852
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)