Skip to content

Commit 1640731

Browse files
committed
Make pkg_resources more forgiving of non-compliant versions (#3839)
2 parents 330035d + 94b583b commit 1640731

File tree

2 files changed

+64
-1
lines changed

2 files changed

+64
-1
lines changed

changelog.d/3839.misc.rst

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed ``pkg_resources`` errors caused when parsing metadata of packages that
2+
are already installed but do not conform with PEP 440.

pkg_resources/__init__.py

+62-1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@
121121
warnings.warn("pkg_resources is deprecated as an API", DeprecationWarning)
122122

123123

124+
_PEP440_FALLBACK = re.compile(r"^v?(?P<safe>(?:[0-9]+!)?[0-9]+(?:\.[0-9]+)*)", re.I)
125+
126+
124127
class PEP440Warning(RuntimeWarning):
125128
"""
126129
Used when there is an issue with a version or specifier not complying with
@@ -1396,6 +1399,38 @@ def safe_version(version):
13961399
return re.sub('[^A-Za-z0-9.]+', '-', version)
13971400

13981401

1402+
def _forgiving_version(version):
1403+
"""Fallback when ``safe_version`` is not safe enough
1404+
>>> parse_version(_forgiving_version('0.23ubuntu1'))
1405+
<Version('0.23.dev0+sanitized.ubuntu1')>
1406+
>>> parse_version(_forgiving_version('0.23-'))
1407+
<Version('0.23.dev0+sanitized')>
1408+
>>> parse_version(_forgiving_version('0.-_'))
1409+
<Version('0.dev0+sanitized')>
1410+
>>> parse_version(_forgiving_version('42.+?1'))
1411+
<Version('42.dev0+sanitized.1')>
1412+
>>> parse_version(_forgiving_version('hello world'))
1413+
<Version('0.dev0+sanitized.hello.world')>
1414+
"""
1415+
version = version.replace(' ', '.')
1416+
match = _PEP440_FALLBACK.search(version)
1417+
if match:
1418+
safe = match["safe"]
1419+
rest = version[len(safe):]
1420+
else:
1421+
safe = "0"
1422+
rest = version
1423+
local = f"sanitized.{_safe_segment(rest)}".strip(".")
1424+
return f"{safe}.dev0+{local}"
1425+
1426+
1427+
def _safe_segment(segment):
1428+
"""Convert an arbitrary string into a safe segment"""
1429+
segment = re.sub('[^A-Za-z0-9.]+', '-', segment)
1430+
segment = re.sub('-[^A-Za-z0-9]+', '-', segment)
1431+
return re.sub(r'\.[^A-Za-z0-9]+', '.', segment).strip(".-")
1432+
1433+
13991434
def safe_extra(extra):
14001435
"""Convert an arbitrary string to a standard 'extra' name
14011436
@@ -2642,7 +2677,7 @@ def _reload_version(self):
26422677
@property
26432678
def hashcmp(self):
26442679
return (
2645-
self.parsed_version,
2680+
self._forgiving_parsed_version,
26462681
self.precedence,
26472682
self.key,
26482683
self.location,
@@ -2700,6 +2735,32 @@ def parsed_version(self):
27002735

27012736
return self._parsed_version
27022737

2738+
@property
2739+
def _forgiving_parsed_version(self):
2740+
try:
2741+
return self.parsed_version
2742+
except packaging.version.InvalidVersion as ex:
2743+
self._parsed_version = parse_version(_forgiving_version(self.version))
2744+
2745+
notes = "\n".join(getattr(ex, "__notes__", [])) # PEP 678
2746+
msg = f"""!!\n\n
2747+
*************************************************************************
2748+
{str(ex)}\n{notes}
2749+
2750+
This is a long overdue deprecation.
2751+
For the time being, `pkg_resources` will use `{self._parsed_version}`
2752+
as a replacement to avoid breaking existing environments,
2753+
but no future compatibility is guaranteed.
2754+
2755+
If you maintain package {self.project_name} you should implement
2756+
the relevant changes to adequate the project to PEP 440 immediately.
2757+
*************************************************************************
2758+
\n\n!!
2759+
"""
2760+
warnings.warn(msg, DeprecationWarning)
2761+
2762+
return self._parsed_version
2763+
27032764
@property
27042765
def version(self):
27052766
try:

0 commit comments

Comments
 (0)