-
-
Notifications
You must be signed in to change notification settings - Fork 1k
/
py_spec.py
114 lines (96 loc) · 4.27 KB
/
py_spec.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
"""A Python specification is an abstract requirement definition of an interpreter."""
from __future__ import annotations
import os
import re
PATTERN = re.compile(r"^(?P<impl>[a-zA-Z]+)?(?P<version>[0-9.]+)?(?:-(?P<arch>32|64))?$")
class PythonSpec:
"""Contains specification about a Python Interpreter."""
def __init__( # noqa: PLR0913
self,
str_spec: str,
implementation: str | None,
major: int | None,
minor: int | None,
micro: int | None,
architecture: int | None,
path: str | None,
) -> None:
self.str_spec = str_spec
self.implementation = implementation
self.major = major
self.minor = minor
self.micro = micro
self.architecture = architecture
self.path = path
@classmethod
def from_string_spec(cls, string_spec: str): # noqa: C901, PLR0912
impl, major, minor, micro, arch, path = None, None, None, None, None, None
if os.path.isabs(string_spec): # noqa: PLR1702
path = string_spec
else:
ok = False
match = re.match(PATTERN, string_spec)
if match:
def _int_or_none(val):
return None if val is None else int(val)
try:
groups = match.groupdict()
version = groups["version"]
if version is not None:
versions = tuple(int(i) for i in version.split(".") if i)
if len(versions) > 3: # noqa: PLR2004
raise ValueError # noqa: TRY301
if len(versions) == 3: # noqa: PLR2004
major, minor, micro = versions
elif len(versions) == 2: # noqa: PLR2004
major, minor = versions
elif len(versions) == 1:
version_data = versions[0]
major = int(str(version_data)[0]) # first digit major
if version_data > 9: # noqa: PLR2004
minor = int(str(version_data)[1:])
ok = True
except ValueError:
pass
else:
impl = groups["impl"]
if impl in {"py", "python"}:
impl = None
arch = _int_or_none(groups["arch"])
if not ok:
path = string_spec
return cls(string_spec, impl, major, minor, micro, arch, path)
def generate_re(self, *, windows: bool) -> re.Pattern:
"""Generate a regular expression for matching against a filename."""
version = r"{}(\.{}(\.{})?)?".format(
*(r"\d+" if v is None else v for v in (self.major, self.minor, self.micro))
)
impl = "python" if self.implementation is None else f"python|{re.escape(self.implementation)}"
suffix = r"\.exe" if windows else ""
# Try matching `direct` first, so the `direct` group is filled when possible.
return re.compile(
rf"(?P<impl>{impl})(?P<v>{version}){suffix}$",
flags=re.IGNORECASE,
)
@property
def is_abs(self):
return self.path is not None and os.path.isabs(self.path)
def satisfies(self, spec):
"""Called when there's a candidate metadata spec to see if compatible - e.g. PEP-514 on Windows."""
if spec.is_abs and self.is_abs and self.path != spec.path:
return False
if spec.implementation is not None and spec.implementation.lower() != self.implementation.lower():
return False
if spec.architecture is not None and spec.architecture != self.architecture:
return False
for our, req in zip((self.major, self.minor, self.micro), (spec.major, spec.minor, spec.micro)):
if req is not None and our is not None and our != req:
return False
return True
def __repr__(self) -> str:
name = type(self).__name__
params = "implementation", "major", "minor", "micro", "architecture", "path"
return f"{name}({', '.join(f'{k}={getattr(self, k)}' for k in params if getattr(self, k) is not None)})"
__all__ = [
"PythonSpec",
]