123 lines
4.5 KiB
Python
123 lines
4.5 KiB
Python
"""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 ""
|
|
version_conditional = (
|
|
"?"
|
|
# Windows Python executables are almost always unversioned
|
|
if windows
|
|
# Spec is an empty string
|
|
or self.major is None
|
|
else ""
|
|
)
|
|
# Try matching `direct` first, so the `direct` group is filled when possible.
|
|
return re.compile(
|
|
rf"(?P<impl>{impl})(?P<v>{version}){version_conditional}{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",
|
|
]
|