Skip to content

Commit 80efd1c

Browse files
committed
Make pkg_resources more forgiving of non-compliant versions
1 parent 9022a21 commit 80efd1c

File tree

1 file changed

+62
-1
lines changed

1 file changed

+62
-1
lines changed

pkg_resources/__init__.py

+62-1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@
112112
_namespace_packages = None
113113

114114

115+
_PEP440_FALLBACK = re.compile(r"^v?(?P<safe>(?:[0-9]+!)?[0-9]+(?:\.[0-9]+)*)", re.I)
116+
117+
115118
class PEP440Warning(RuntimeWarning):
116119
"""
117120
Used when there is an issue with a version or specifier not complying with
@@ -1389,6 +1392,38 @@ def safe_version(version):
13891392
return re.sub('[^A-Za-z0-9.]+', '-', version)
13901393

13911394

1395+
def _forgiving_version(version):
1396+
"""Fallback when ``safe_version`` is not safe enough
1397+
>>> parse_version(_forgiving_version('0.23ubuntu1'))
1398+
<Version('0.23.dev0+sanitized.ubuntu1')>
1399+
>>> parse_version(_forgiving_version('0.23-'))
1400+
<Version('0.23.dev0+sanitized')>
1401+
>>> parse_version(_forgiving_version('0.-_'))
1402+
<Version('0.dev0+sanitized')>
1403+
>>> parse_version(_forgiving_version('42.+?1'))
1404+
<Version('42.dev0+sanitized.1')>
1405+
>>> parse_version(_forgiving_version('hello world'))
1406+
<Version('0.dev0+sanitized.hello.world')>
1407+
"""
1408+
version = version.replace(' ', '.')
1409+
match = _PEP440_FALLBACK.search(version)
1410+
if match:
1411+
safe = match["safe"]
1412+
rest = version[len(safe):]
1413+
else:
1414+
safe = "0"
1415+
rest = version
1416+
local = f"sanitized.{_safe_segment(rest)}".strip(".")
1417+
return f"{safe}.dev0+{local}"
1418+
1419+
1420+
def _safe_segment(segment):
1421+
"""Convert an arbitrary string into a safe segment"""
1422+
segment = re.sub('[^A-Za-z0-9.]+', '-', segment)
1423+
segment = re.sub('-[^A-Za-z0-9]+', '-', segment)
1424+
return re.sub(r'\.[^A-Za-z0-9]+', '.', segment).strip(".-")
1425+
1426+
13921427
def safe_extra(extra):
13931428
"""Convert an arbitrary string to a standard 'extra' name
13941429
@@ -2637,7 +2672,7 @@ def _reload_version(self):
26372672
@property
26382673
def hashcmp(self):
26392674
return (
2640-
self.parsed_version,
2675+
self._forgiving_parsed_version,
26412676
self.precedence,
26422677
self.key,
26432678
self.location,
@@ -2695,6 +2730,32 @@ def parsed_version(self):
26952730

26962731
return self._parsed_version
26972732

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

0 commit comments

Comments
 (0)