Skip to content

Commit 648b17b

Browse files
committed
Preserve download integrity with lockfile
Introduce cross-process and asyncio lockfile with "fasterner" to avoid damaging wheels downloads with partila overwrites, reusing code from ScanCode Toolkit used for the license cache index Reference: #215 Signed-off-by: Philippe Ombredanne <[email protected]>
1 parent 9c56374 commit 648b17b

File tree

5 files changed

+54
-10
lines changed

5 files changed

+54
-10
lines changed

requirements-dev.txt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ cryptography==37.0.2
77
dataclasses==0.8
88
docutils==0.18.1
99
et-xmlfile==1.1.0
10-
execnet==1.9.0
10+
execnet==2.1.1
1111
importlib-resources==5.4.0
1212
iniconfig==1.1.1
1313
isort==5.10.1
@@ -21,14 +21,15 @@ openpyxl==3.0.10
2121
pathspec==0.9.0
2222
pkginfo==1.8.3
2323
platformdirs==2.4.0
24-
pluggy==1.0.0
24+
pluggy==1.6.0
2525
py==1.11.0
2626
pycodestyle==2.8.0
2727
pycparser==2.21
2828
pygments==2.12.0
29-
pytest==7.0.1
30-
pytest-forked==1.4.0
31-
pytest-xdist==2.5.0
29+
pytest==8.4.0
30+
pytest-xdist==3.7.0
31+
pytest-forked==1.6.0
32+
pytest-rerunfailures==15.1
3233
readme-renderer==34.0
3334
requests-toolbelt==0.9.1
3435
rfc3986==1.5.0
@@ -39,4 +40,4 @@ tqdm==4.64.0
3940
twine==3.8.0
4041
typed-ast==1.5.4
4142
webencodings==0.5.1
42-
pytest-asyncio==0.21.1
43+
pytest-asyncio==1.0.0

requirements.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ click==8.1.3
66
colorama==0.4.5
77
commoncode==30.2.0
88
dparse2==0.7.0
9+
fasteners==0.17.3
910
idna==3.3
1011
importlib-metadata==4.12.0
1112
intbitset==3.1.0
@@ -26,5 +27,5 @@ text-unidecode==1.3
2627
toml==0.10.2
2728
urllib3==1.26.11
2829
zipp==3.8.1
29-
aiohttp==3.11.14
30-
aiofiles==23.2.1
30+
aiohttp==3.12.7
31+
aiofiles==24.1.0

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ install_requires =
5959
colorama >= 0.3.9
6060
commoncode >= 30.0.0
6161
dparse2 >= 0.7.0
62+
fasteners >= 0.17.3
6263
importlib_metadata >= 4.12.0
6364
packageurl_python >= 0.9.0
6465
pkginfo2 >= 30.0.0

src/python_inspector/lockfile.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# ScanCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/nexB/scancode-toolkit for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
from contextlib import contextmanager
11+
12+
import fasteners
13+
14+
"""
15+
An interprocess lockfile with a timeout.
16+
"""
17+
18+
19+
class LockTimeout(Exception):
20+
pass
21+
22+
23+
class FileLock(fasteners.InterProcessLock):
24+
@contextmanager
25+
def locked(self, timeout):
26+
acquired = self.acquire(timeout=timeout)
27+
if not acquired:
28+
raise LockTimeout(timeout)
29+
try:
30+
yield
31+
finally:
32+
self.release()

src/python_inspector/utils_pypi.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from packvers import version as packaging_version
4242
from packvers.specifiers import SpecifierSet
4343

44+
from python_inspector import lockfile
4445
from python_inspector import pyinspector_settings as settings
4546
from python_inspector import utils_pip_compatibility_tags
4647

@@ -1650,6 +1651,8 @@ def resolve_relative_url(package_url, url):
16501651
#
16511652
################################################################################
16521653

1654+
PYINSP_CACHE_LOCK_TIMEOUT = 120 # in seconds
1655+
16531656

16541657
@attr.attributes
16551658
class Cache:
@@ -1681,6 +1684,7 @@ async def get(
16811684
True otherwise as treat as binary. `path_or_url` can be a path or a URL
16821685
to a file.
16831686
"""
1687+
# the cache key is a hash of the normalized path
16841688
cache_key = self.sha256_hash(quote_plus(path_or_url.strip("/")))
16851689
cached = os.path.join(self.directory, cache_key)
16861690

@@ -1695,8 +1699,13 @@ async def get(
16951699
echo_func=echo_func,
16961700
)
16971701
wmode = "w" if as_text else "wb"
1698-
async with aiofiles.open(cached, mode=wmode) as fo:
1699-
await fo.write(content)
1702+
1703+
# acquire lock and wait until timeout to get a lock or die
1704+
lock_file = os.path.join(self.directory, f"{cache_key}.lockfile")
1705+
1706+
with lockfile.FileLock(lock_file).locked(timeout=PYINSP_CACHE_LOCK_TIMEOUT):
1707+
async with aiofiles.open(cached, mode=wmode) as fo:
1708+
await fo.write(content)
17001709
return content, cached
17011710
else:
17021711
if TRACE_DEEP:

0 commit comments

Comments
 (0)