feat: ж/д слой + cant_swim в скоринг + профили вне модели

Определение специфических рекомендаций (матрица профилей §8):

1. Ж/д слой (закрыт мёртвый railway ×2.5 у РАС):
   - /api/v1/water/{case_id} отдаёт railway=rail как LineString
     (без service/industrial/military веток), кэш общий v2;
   - railway_warning «перекрыть/проверить немедленно» по профилям;
   - SearchMap: Polyline слой ж/д (тёмно-красный), счётчики 💧/🚂.

2. cant_swim → профиль не_умеет_плавать (water ×3.0, без изменения
   радиуса, critical_warning «обследовать водоёмы НЕМЕДЛЕННО»):
   - раньше чекбокс влиял только на текст, в скоринге был пробел;
   - derive в analyze._derive_profiles — работает и для closed_cases.

3. unmodeled_profiles: ДЦП/слабое зрение/слух — честная пометка
   «вне поведенческой модели» с пояснением (vector_tasks B12:
   профили без аналога не выдавать за учтённые); блок на фронте
   в карточке здоровья.

Площадь воды: сферический эксцесс, проверен на квадрате 53° (744017 м²
vs 743272 точного). Тесты: 202 passed (новый test_cant_swim_profile).
This commit is contained in:
2026-09-09 13:12:55 +03:00
parent 98e8e58023
commit f20080305d
2069 changed files with 803865 additions and 97 deletions
@@ -0,0 +1,15 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
__title__ = "packaging"
__summary__ = "Core utilities for Python packages"
__uri__ = "https://github.com/pypa/packaging"
__version__ = "26.3"
__author__ = "Donald Stufft and individual contributors"
__email__ = "donald@stufft.io"
__license__ = "BSD-2-Clause or Apache-2.0"
__copyright__ = f"2014 {__author__}"
@@ -0,0 +1,108 @@
"""
ELF file parser.
This provides a class ``ELFFile`` that parses an ELF executable in a similar
interface to ``ZipFile``. Only the read interface is implemented.
ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
"""
from __future__ import annotations
import enum
import os
import struct
from typing import IO
class ELFInvalid(ValueError):
pass
class EIClass(enum.IntEnum):
C32 = 1
C64 = 2
class EIData(enum.IntEnum):
Lsb = 1
Msb = 2
class EMachine(enum.IntEnum):
I386 = 3
S390 = 22
Arm = 40
X8664 = 62
AArch64 = 183
class ELFFile:
"""
Representation of an ELF executable.
"""
def __init__(self, f: IO[bytes]) -> None:
self._f = f
try:
ident = self._read("16B")
except struct.error as e:
raise ELFInvalid("unable to parse identification") from e
magic = bytes(ident[:4])
if magic != b"\x7fELF":
raise ELFInvalid(f"invalid magic: {magic!r}")
self.capacity = ident[4] # Format for program header (bitness).
self.encoding = ident[5] # Data structure encoding (endianness).
try:
# e_fmt: Format for the ELF header.
# p_fmt: Format for a program header.
# p_idx: Indexes to find p_type, p_offset, and p_filesz.
e_fmt, self._p_fmt, self._p_idx = {
(1, 1): ("<HHIIIIIHHH", "<IIIIIIII", (0, 1, 4)), # 32-bit LSB.
(1, 2): (">HHIIIIIHHH", ">IIIIIIII", (0, 1, 4)), # 32-bit MSB.
(2, 1): ("<HHIQQQIHHH", "<IIQQQQQQ", (0, 2, 5)), # 64-bit LSB.
(2, 2): (">HHIQQQIHHH", ">IIQQQQQQ", (0, 2, 5)), # 64-bit MSB.
}[(self.capacity, self.encoding)]
except KeyError as e:
raise ELFInvalid(
f"unrecognized capacity ({self.capacity}) or encoding ({self.encoding})"
) from e
try:
(
_,
self.machine, # Architecture type.
_,
_,
self._e_phoff, # Offset of program header.
_,
self.flags, # Processor-specific flags.
_,
self._e_phentsize, # Size of a program header entry.
self._e_phnum, # Number of program headers.
) = self._read(e_fmt)
except struct.error as e:
raise ELFInvalid("unable to parse machine and section information") from e
def _read(self, fmt: str) -> tuple[int, ...]:
return struct.unpack(fmt, self._f.read(struct.calcsize(fmt)))
@property
def interpreter(self) -> str | None:
"""
The path recorded in the ``PT_INTERP`` section header.
"""
for index in range(self._e_phnum):
self._f.seek(self._e_phoff + self._e_phentsize * index)
try:
data = self._read(self._p_fmt)
except struct.error:
continue
if data[self._p_idx[0]] != 3: # Not PT_INTERP.
continue
self._f.seek(data[self._p_idx[1]])
return os.fsdecode(self._f.read(data[self._p_idx[2]])).strip("\0")
return None
@@ -0,0 +1,278 @@
from __future__ import annotations
import collections
import contextlib
import functools
import os
import re
import sys
import warnings
from typing import TYPE_CHECKING, NamedTuple
from ._elffile import EIClass, EIData, ELFFile, EMachine
if TYPE_CHECKING:
import types
from collections.abc import Generator, Iterator, Sequence
EF_ARM_ABIMASK = 0xFF000000
EF_ARM_ABI_VER5 = 0x05000000
EF_ARM_ABI_FLOAT_HARD = 0x00000400
_ALLOWED_ARCHS = {
"x86_64",
"aarch64",
"ppc64",
"ppc64le",
"s390x",
"loongarch64",
"riscv64",
}
@contextlib.contextmanager
def _parse_elf(path: str) -> Generator[ELFFile | None, None, None]:
try:
with open(path, "rb") as f:
yield ELFFile(f)
except (OSError, TypeError, ValueError):
yield None
def _is_linux_armhf(executable: str) -> bool:
# hard-float ABI can be detected from the ELF header of the running
# process
# https://static.docs.arm.com/ihi0044/g/aaelf32.pdf
with _parse_elf(executable) as f:
return (
f is not None
and f.capacity == EIClass.C32
and f.encoding == EIData.Lsb
and f.machine == EMachine.Arm
and f.flags & EF_ARM_ABIMASK == EF_ARM_ABI_VER5
and f.flags & EF_ARM_ABI_FLOAT_HARD == EF_ARM_ABI_FLOAT_HARD
)
def _is_linux_i686(executable: str) -> bool:
with _parse_elf(executable) as f:
return (
f is not None
and f.capacity == EIClass.C32
and f.encoding == EIData.Lsb
and f.machine == EMachine.I386
)
def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool:
if "armv7l" in archs:
return _is_linux_armhf(executable)
if "i686" in archs:
return _is_linux_i686(executable)
return any(arch in _ALLOWED_ARCHS for arch in archs)
# If glibc ever changes its major version, we need to know what the last
# minor version was, so we can build the complete list of all versions.
# For now, guess what the highest minor version might be, assume it will
# be 50 for testing. Once this actually happens, update the dictionary
# with the actual value.
_LAST_GLIBC_MINOR: dict[int, int] = collections.defaultdict(lambda: 50)
class _GLibCVersion(NamedTuple):
major: int
minor: int
def _glibc_version_string_confstr() -> str | None:
"""
Primary implementation of glibc_version_string using os.confstr.
"""
# os.confstr is quite a bit faster than ctypes.DLL. It's also less likely
# to be broken or missing. This strategy is used in the standard library
# platform module.
# https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183
try:
# Should be a string like "glibc 2.17".
version_string: str | None = os.confstr("CS_GNU_LIBC_VERSION")
assert version_string is not None
_, version = version_string.rsplit()
except (AssertionError, AttributeError, OSError, ValueError):
# os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)...
return None
return version
def _glibc_version_string_ctypes() -> str | None:
"""
Fallback implementation of glibc_version_string using ctypes.
"""
try:
import ctypes # noqa: PLC0415
except ImportError:
return None
# ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen
# manpage says, "If filename is NULL, then the returned handle is for the
# main program". This way we can let the linker do the work to figure out
# which libc our process is actually using.
#
# We must also handle the special case where the executable is not a
# dynamically linked executable. This can occur when using musl libc,
# for example. In this situation, dlopen() will error, leading to an
# OSError. Interestingly, at least in the case of musl, there is no
# errno set on the OSError. The single string argument used to construct
# OSError comes from libc itself and is therefore not portable to
# hard code here. In any case, failure to call dlopen() means we
# can proceed, so we bail on our attempt.
try:
process_namespace = ctypes.CDLL(None)
except OSError:
return None
try:
gnu_get_libc_version = process_namespace.gnu_get_libc_version
except AttributeError:
# Symbol doesn't exist -> therefore, we are not linked to
# glibc.
return None
# Call gnu_get_libc_version, which returns a string like "2.5".
gnu_get_libc_version.restype = ctypes.c_char_p
# A c_char_p restype comes back as bytes, so decode to text.
version_str: str | bytes = gnu_get_libc_version()
if isinstance(version_str, bytes):
version_str = version_str.decode("ascii")
return version_str
def _glibc_version_string() -> str | None:
"""Returns glibc version string, or None if not using glibc."""
return _glibc_version_string_confstr() or _glibc_version_string_ctypes()
def _parse_glibc_version(version_str: str) -> _GLibCVersion:
"""Parse glibc version.
We use a regexp instead of str.split because we want to discard any
random junk that might come after the minor version -- this might happen
in patched/forked versions of glibc (e.g. Linaro's version of glibc
uses version strings like "2.20-2014.11"). See gh-3588.
"""
m = re.match(r"(?P<major>[0-9]+)\.(?P<minor>[0-9]+)", version_str)
if not m:
warnings.warn(
f"Expected glibc version with 2 components major.minor, got: {version_str}",
RuntimeWarning,
stacklevel=2,
)
return _GLibCVersion(-1, -1)
return _GLibCVersion(int(m.group("major")), int(m.group("minor")))
@functools.lru_cache
def _get_glibc_version() -> _GLibCVersion:
version_str = _glibc_version_string()
if version_str is None:
return _GLibCVersion(-1, -1)
return _parse_glibc_version(version_str)
# From PEP 513, PEP 600
@functools.lru_cache(maxsize=1)
def _get_manylinux_module() -> types.ModuleType | None:
"""Return the ``_manylinux`` C extension module, or None if unavailable.
The result is cached for the lifetime of the process, since the presence
of the module does not change while running.
"""
try:
return __import__("_manylinux")
except ImportError:
return None
def _is_compatible(arch: str, version: _GLibCVersion) -> bool:
sys_glibc = _get_glibc_version()
if sys_glibc < version:
return False
# Check for presence of _manylinux module.
manylinux_mod = _get_manylinux_module()
if manylinux_mod is None:
return True
if hasattr(manylinux_mod, "manylinux_compatible"):
result = manylinux_mod.manylinux_compatible(version[0], version[1], arch)
if result is not None:
return bool(result)
return True
if version == _GLibCVersion(2, 5) and hasattr(
manylinux_mod, "manylinux1_compatible"
):
return bool(manylinux_mod.manylinux1_compatible)
if version == _GLibCVersion(2, 12) and hasattr(
manylinux_mod, "manylinux2010_compatible"
):
return bool(manylinux_mod.manylinux2010_compatible)
if version == _GLibCVersion(2, 17) and hasattr(
manylinux_mod, "manylinux2014_compatible"
):
return bool(manylinux_mod.manylinux2014_compatible)
return True
_LEGACY_MANYLINUX_MAP: dict[_GLibCVersion, str] = {
# CentOS 7 w/ glibc 2.17 (PEP 599)
_GLibCVersion(2, 17): "manylinux2014",
# CentOS 6 w/ glibc 2.12 (PEP 571)
_GLibCVersion(2, 12): "manylinux2010",
# CentOS 5 w/ glibc 2.5 (PEP 513)
_GLibCVersion(2, 5): "manylinux1",
}
def platform_tags(archs: Sequence[str]) -> Iterator[str]:
"""Generate manylinux tags compatible to the current platform.
:param archs: Sequence of compatible architectures.
The first one shall be the closest to the actual architecture and be the part of
platform tag after the ``linux_`` prefix, e.g. ``x86_64``.
The ``linux_`` prefix is assumed as a prerequisite for the current platform to
be manylinux-compatible.
:returns: An iterator of compatible manylinux tags.
"""
if not _have_compatible_abi(sys.executable, archs):
return
# Oldest glibc to be supported regardless of architecture is (2, 17).
too_old_glibc2 = _GLibCVersion(2, 16)
if set(archs) & {"x86_64", "i686"}:
# On x86/i686 also oldest glibc to be supported is (2, 5).
too_old_glibc2 = _GLibCVersion(2, 4)
current_glibc = _GLibCVersion(*_get_glibc_version())
glibc_max_list = [current_glibc]
# We can assume compatibility across glibc major versions.
# https://sourceware.org/bugzilla/show_bug.cgi?id=24636
#
# Build a list of maximum glibc versions so that we can
# output the canonical list of all glibc from current_glibc
# down to too_old_glibc2, including all intermediary versions.
for glibc_major in range(current_glibc.major - 1, 1, -1):
glibc_minor = _LAST_GLIBC_MINOR[glibc_major]
glibc_max_list.append(_GLibCVersion(glibc_major, glibc_minor))
for arch in archs:
for glibc_max in glibc_max_list:
if glibc_max.major == too_old_glibc2.major:
min_minor = too_old_glibc2.minor
else:
# For other glibc major versions oldest supported is (x, 0).
min_minor = -1
for glibc_minor in range(glibc_max.minor, min_minor, -1):
glibc_version = _GLibCVersion(glibc_max.major, glibc_minor)
if _is_compatible(arch, glibc_version):
yield "manylinux_{}_{}_{}".format(*glibc_version, arch)
# Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.
if legacy_tag := _LEGACY_MANYLINUX_MAP.get(glibc_version):
yield f"{legacy_tag}_{arch}"
@@ -0,0 +1,88 @@
"""PEP 656 support.
This module implements logic to detect if the currently running Python is
linked against musl, and what musl version is used.
"""
from __future__ import annotations
import functools
import re
import subprocess
import sys
from typing import TYPE_CHECKING, NamedTuple
from ._elffile import ELFFile
if TYPE_CHECKING:
from collections.abc import Iterator, Sequence
class _MuslVersion(NamedTuple):
major: int
minor: int
def _parse_musl_version(output: str) -> _MuslVersion | None:
lines = [n for n in (n.strip() for n in output.splitlines()) if n]
if len(lines) < 2 or lines[0][:4] != "musl":
return None
m = re.match(r"Version (\d+)\.(\d+)", lines[1])
if not m:
return None
return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2)))
@functools.lru_cache
def _get_musl_version(executable: str) -> _MuslVersion | None:
"""Detect currently-running musl runtime version.
This is done by checking the specified executable's dynamic linking
information, and invoking the loader to parse its output for a version
string. If the loader is musl, the output would be something like::
musl libc (x86_64)
Version 1.2.2
Dynamic Program Loader
"""
try:
with open(executable, "rb") as f:
ld = ELFFile(f).interpreter
except (OSError, TypeError, ValueError):
return None
if ld is None or "musl" not in ld:
return None
proc = subprocess.run([ld], check=False, stderr=subprocess.PIPE, text=True)
return _parse_musl_version(proc.stderr)
def platform_tags(archs: Sequence[str]) -> Iterator[str]:
"""Generate musllinux tags compatible to the current platform.
:param archs: Sequence of compatible architectures.
The first one shall be the closest to the actual architecture and be the part of
platform tag after the ``linux_`` prefix, e.g. ``x86_64``.
The ``linux_`` prefix is assumed as a prerequisite for the current platform to
be musllinux-compatible.
:returns: An iterator of compatible musllinux tags.
"""
sys_musl = _get_musl_version(sys.executable)
if sys_musl is None: # Python not dynamically linked against musl.
return
for arch in archs:
for minor in range(sys_musl.minor, -1, -1):
yield f"musllinux_{sys_musl.major}_{minor}_{arch}"
if __name__ == "__main__": # pragma: no cover
import sysconfig
plat = sysconfig.get_platform()
assert plat.startswith("linux-"), "not linux"
print("plat:", plat)
print("musl:", _get_musl_version(sys.executable))
print("tags:", end=" ")
for t in platform_tags([re.sub(r"[.-]", "_", plat.split("-", 1)[-1])]):
print(t, end="\n ")
@@ -0,0 +1,418 @@
"""Handwritten parser of dependency specifiers.
The docstring for each __parse_* function contains EBNF-inspired grammar representing
the implementation.
"""
from __future__ import annotations
import ast
from collections.abc import Sequence
from typing import Literal, NamedTuple, Union
from ._tokenizer import DEFAULT_RULES, ParserSyntaxError, Tokenizer
class Node:
__slots__ = ("value",)
def __init__(self, value: str) -> None:
self.value = value
def __str__(self) -> str:
return self.value
def __repr__(self) -> str:
return f"<{self.__class__.__name__}({self.value!r})>"
def serialize(self) -> str:
raise NotImplementedError
def __getstate__(self) -> str:
# Return just the value string for compactness and stability.
return self.value
def _restore_value(self, value: object) -> None:
if not isinstance(value, str):
raise TypeError(
f"Cannot restore {self.__class__.__name__} value from {value!r}"
)
self.value = value
def __setstate__(self, state: object) -> None:
if isinstance(state, str):
# New format (26.2+): just the value string.
self._restore_value(state)
return
if isinstance(state, tuple) and len(state) == 2:
# Old format (packaging <= 26.0, __slots__): (None, {slot: value}).
_, slot_dict = state
if isinstance(slot_dict, dict) and "value" in slot_dict:
self._restore_value(slot_dict["value"])
return
if isinstance(state, dict) and "value" in state:
# Old format (packaging <= 25.0, no __slots__): plain __dict__.
self._restore_value(state["value"])
return
raise TypeError(f"Cannot restore {self.__class__.__name__} from {state!r}")
class Variable(Node):
__slots__ = ()
def serialize(self) -> str:
return str(self)
class Value(Node):
__slots__ = ()
def serialize(self) -> str:
value = str(self)
if '"' not in value:
return f'"{value}"'
if "'" not in value:
return f"'{value}'"
raise ValueError(
"Cannot serialize marker value containing both quote characters"
)
class Op(Node):
__slots__ = ()
def serialize(self) -> str:
return str(self)
MarkerLogical = Literal["and", "or"]
MarkerVar = Union[Variable, Value]
MarkerItem = tuple[MarkerVar, Op, MarkerVar]
MarkerAtom = Union[MarkerItem, Sequence["MarkerAtom"]]
MarkerList = list[Union["MarkerList", MarkerAtom, MarkerLogical]]
class ParsedRequirement(NamedTuple):
name: str
url: str
extras: list[str]
specifier: str
marker: MarkerList | None
# --------------------------------------------------------------------------------------
# Recursive descent parser for dependency specifier
# --------------------------------------------------------------------------------------
def parse_requirement(source: str) -> ParsedRequirement:
return _parse_requirement(Tokenizer(source, rules=DEFAULT_RULES))
def _parse_requirement(tokenizer: Tokenizer) -> ParsedRequirement:
"""
requirement = WS? IDENTIFIER WS? extras WS? requirement_details
"""
tokenizer.consume("WS")
name_token = tokenizer.expect(
"IDENTIFIER", expected="package name at the start of dependency specifier"
)
name = name_token.text
tokenizer.consume("WS")
extras = _parse_extras(tokenizer)
tokenizer.consume("WS")
url, specifier, marker = _parse_requirement_details(tokenizer)
tokenizer.expect("END", expected="end of dependency specifier")
return ParsedRequirement(name, url, extras, specifier, marker)
def _parse_requirement_details(
tokenizer: Tokenizer,
) -> tuple[str, str, MarkerList | None]:
"""
requirement_details = AT URL (WS requirement_marker?)?
| specifier WS? (requirement_marker)?
"""
specifier = ""
url = ""
marker = None
if tokenizer.check("AT"):
tokenizer.read()
tokenizer.consume("WS")
url_start = tokenizer.position
url = tokenizer.expect("URL", expected="URL after @").text
if tokenizer.check("END", peek=True):
return (url, specifier, marker)
tokenizer.expect("WS", expected="whitespace after URL")
# The input might end after whitespace.
if tokenizer.check("END", peek=True):
return (url, specifier, marker)
marker = _parse_requirement_marker(
tokenizer,
span_start=url_start,
expected="semicolon (after URL and whitespace)",
)
else:
specifier_start = tokenizer.position
specifier = _parse_specifier(tokenizer)
tokenizer.consume("WS")
if tokenizer.check("END", peek=True):
return (url, specifier, marker)
marker = _parse_requirement_marker(
tokenizer,
span_start=specifier_start,
expected=(
"comma (within version specifier), semicolon (after version specifier)"
if specifier
else "semicolon (after name with no version specifier)"
),
)
return (url, specifier, marker)
def _parse_requirement_marker(
tokenizer: Tokenizer, *, span_start: int, expected: str
) -> MarkerList:
"""
requirement_marker = SEMICOLON marker WS?
"""
if not tokenizer.check("SEMICOLON"):
tokenizer.raise_syntax_error(
f"Expected {expected} or end",
span_start=span_start,
span_end=None,
)
tokenizer.read()
marker = _parse_marker(tokenizer)
tokenizer.consume("WS")
return marker
def _parse_extras(tokenizer: Tokenizer) -> list[str]:
"""
extras = (LEFT_BRACKET wsp* extras_list? wsp* RIGHT_BRACKET)?
"""
if not tokenizer.check("LEFT_BRACKET", peek=True):
return []
with tokenizer.enclosing_tokens(
"LEFT_BRACKET",
"RIGHT_BRACKET",
around="extras",
):
tokenizer.consume("WS")
extras = _parse_extras_list(tokenizer)
tokenizer.consume("WS")
return extras
def _parse_extras_list(tokenizer: Tokenizer) -> list[str]:
"""
extras_list = identifier (wsp* ',' wsp* identifier)*
"""
extras: list[str] = []
if not tokenizer.check("IDENTIFIER"):
return extras
extras.append(tokenizer.read().text)
while True:
tokenizer.consume("WS")
if tokenizer.check("IDENTIFIER", peek=True):
tokenizer.raise_syntax_error("Expected comma between extra names")
elif not tokenizer.check("COMMA"):
break
tokenizer.read()
tokenizer.consume("WS")
extra_token = tokenizer.expect("IDENTIFIER", expected="extra name after comma")
extras.append(extra_token.text)
return extras
def _parse_specifier(tokenizer: Tokenizer) -> str:
"""
specifier = LEFT_PARENTHESIS WS? version_many WS? RIGHT_PARENTHESIS
| WS? version_many WS?
"""
with tokenizer.enclosing_tokens(
"LEFT_PARENTHESIS",
"RIGHT_PARENTHESIS",
around="version specifier",
):
tokenizer.consume("WS")
parsed_specifiers = _parse_version_many(tokenizer)
tokenizer.consume("WS")
return parsed_specifiers
def _parse_version_many(tokenizer: Tokenizer) -> str:
"""
version_many = (SPECIFIER (WS? COMMA WS? SPECIFIER)*)?
"""
parsed_specifiers = ""
while tokenizer.check("SPECIFIER"):
span_start = tokenizer.position
specifier = tokenizer.read().text
parsed_specifiers += specifier
if tokenizer.check("VERSION_PREFIX_TRAIL", peek=True):
message = ".* suffix can only be used with `==` or `!=` operators"
if specifier.startswith("!=") or (
specifier.startswith("==") and not specifier.startswith("===")
):
message = (
".* suffix cannot be used with pre-release, post-release, "
"dev or local versions"
)
tokenizer.raise_syntax_error(
message,
span_start=span_start,
span_end=tokenizer.position + 1,
)
if tokenizer.check("VERSION_LOCAL_LABEL_TRAIL", peek=True):
tokenizer.raise_syntax_error(
"Local version label can only be used with `==` or `!=` operators",
span_start=span_start,
span_end=tokenizer.position,
)
tokenizer.consume("WS")
if not tokenizer.check("COMMA"):
break
parsed_specifiers += tokenizer.read().text
tokenizer.consume("WS")
return parsed_specifiers
# --------------------------------------------------------------------------------------
# Recursive descent parser for marker expression
# --------------------------------------------------------------------------------------
def parse_marker(source: str) -> MarkerList:
return _parse_full_marker(Tokenizer(source, rules=DEFAULT_RULES))
def _parse_full_marker(tokenizer: Tokenizer) -> MarkerList:
retval = _parse_marker(tokenizer)
tokenizer.expect("END", expected="end of marker expression")
return retval
def _parse_marker(tokenizer: Tokenizer) -> MarkerList:
"""
marker = marker_atom (BOOLOP marker_atom)+
"""
expression = [_parse_marker_atom(tokenizer)]
while tokenizer.check("BOOLOP"):
token = tokenizer.read()
expr_right = _parse_marker_atom(tokenizer)
expression.extend((token.text, expr_right))
return expression
def _parse_marker_atom(tokenizer: Tokenizer) -> MarkerAtom:
"""
marker_atom = WS? LEFT_PARENTHESIS WS? marker WS? RIGHT_PARENTHESIS WS?
| WS? marker_item WS?
"""
tokenizer.consume("WS")
if tokenizer.check("LEFT_PARENTHESIS", peek=True):
with tokenizer.enclosing_tokens(
"LEFT_PARENTHESIS",
"RIGHT_PARENTHESIS",
around="marker expression",
):
tokenizer.consume("WS")
marker: MarkerAtom = _parse_marker(tokenizer)
tokenizer.consume("WS")
else:
marker = _parse_marker_item(tokenizer)
tokenizer.consume("WS")
return marker
def _parse_marker_item(tokenizer: Tokenizer) -> MarkerItem:
"""
marker_item = WS? marker_var WS? marker_op WS? marker_var WS?
"""
tokenizer.consume("WS")
marker_var_left = _parse_marker_var(tokenizer)
tokenizer.consume("WS")
marker_op = _parse_marker_op(tokenizer)
tokenizer.consume("WS")
marker_var_right = _parse_marker_var(tokenizer)
tokenizer.consume("WS")
return (marker_var_left, marker_op, marker_var_right)
def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar: # noqa: RET503
"""
marker_var = VARIABLE | QUOTED_STRING
"""
if tokenizer.check("VARIABLE"):
return process_env_var(tokenizer.read().text.replace(".", "_"))
elif tokenizer.check("QUOTED_STRING"):
token = tokenizer.read()
try:
return process_python_str(token.text)
except (SyntaxError, ValueError) as exc:
raise ParserSyntaxError(
"Invalid quoted string",
source=tokenizer.source,
span=(token.position, token.position + len(token.text)),
) from exc
else:
tokenizer.raise_syntax_error(
message="Expected a marker variable or quoted string"
)
def process_env_var(env_var: str) -> Variable:
if env_var in ("platform_python_implementation", "python_implementation"):
return Variable("platform_python_implementation")
else:
return Variable(env_var)
def process_python_str(python_str: str) -> Value:
value = ast.literal_eval(python_str)
return Value(str(value))
def _parse_marker_op(tokenizer: Tokenizer) -> Op:
"""
marker_op = IN | NOT IN | OP
"""
if tokenizer.check("IN"):
tokenizer.read()
return Op("in")
elif tokenizer.check("NOT"):
tokenizer.read()
tokenizer.expect("WS", expected="whitespace after 'not'")
tokenizer.expect("IN", expected="'in' after 'not'")
return Op("not in")
elif tokenizer.check("OP"):
return Op(tokenizer.read().text)
else:
return tokenizer.raise_syntax_error(
"Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in"
)
@@ -0,0 +1,836 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
"""Private version-range helpers used by :mod:`packaging.specifiers`."""
from __future__ import annotations
import enum
import functools
from typing import (
TYPE_CHECKING,
Any,
Final,
)
from .version import InvalidVersion, Version
if TYPE_CHECKING:
from collections.abc import Callable, Iterable, Iterator, Sequence
from typing import Union
# Total-order key for comparing two boundaries (boundary-vs-boundary only).
# The post slot may be ``_BOUNDARY_INF`` for an AFTER_POSTS boundary.
_BoundaryOrderSuffix = tuple[int, int, int, Union[int, float], int, int]
_BoundaryOrderKey = tuple[int, tuple[int, ...], _BoundaryOrderSuffix, float]
__all__ = [
"FULL_RANGE",
"bounds_for_spec",
"coerce_version",
"filter_by_ranges",
"intersect_ranges",
"intersect_specifier_bounds",
"least_version_above",
"matches_bounds_only",
"range_is_empty",
"ranges_are_prerelease_only",
"resolve_prereleases",
"standard_ranges",
"wildcard_ranges",
]
#: The smallest possible PEP 440 version. No valid version is less than this.
MIN_VERSION: Final[Version] = Version("0.dev0")
#: The smallest non-pre-release version, i.e. the nearest non-pre-release at or
#: above the ``-inf`` floor.
MIN_RELEASE: Final[Version] = Version("0")
#: Sorts above any real post number and any local label, so a boundary can be
#: ordered above the version family it covers when two boundaries are compared.
_BOUNDARY_INF: Final[float] = float("inf")
class BoundaryKind(enum.Enum):
"""Where a boundary marker sits in the version ordering."""
AFTER_LOCALS = enum.auto() # after V+local, before V.post0
AFTER_POSTS = enum.auto() # after V.postN, before next release
@functools.total_ordering
class BoundaryVersion:
"""A point on the version line between two real PEP 440 versions.
Relative to a base version V::
V < V+local < AFTER_LOCALS(V) < V.post0 < AFTER_POSTS(V)
AFTER_LOCALS is the upper bound of ``<=V``, ``==V``, ``!=V`` (no
local), and the lower bound of the upper-side range of ``!=V``.
AFTER_POSTS is the lower bound of ``>V`` (V final or pre-release),
excluding V's post-releases per PEP 440.
"""
__slots__ = (
"_cached_dev",
"_cached_epoch",
"_cached_post",
"_cached_pre",
"_cached_trimmed_release",
"kind",
"version",
)
def __init__(self, version: Version, kind: BoundaryKind) -> None:
self.version = version
self.kind = kind
self._cached_trimmed_release = trim_release(version.release)
self._cached_epoch = version.epoch
self._cached_pre = version.pre
self._cached_post = version.post
self._cached_dev = version.dev
def _is_family(self, other: Version) -> bool:
"""Is ``other`` a version that this boundary sorts above?"""
if other.epoch != self._cached_epoch:
return False
# Inline release-trim comparison: other.release matches the
# trimmed release iff its leading slice is equal and any extra
# components are zero. Avoids trim_release's tuple allocation.
other_release = other.release
trimmed_release = self._cached_trimmed_release
trimmed_length = len(trimmed_release)
if len(other_release) < trimmed_length:
return False
if other_release[:trimmed_length] != trimmed_release:
return False
for i in range(trimmed_length, len(other_release)):
if other_release[i] != 0:
return False
if other.pre != self._cached_pre:
return False
if self.kind == BoundaryKind.AFTER_LOCALS:
# Local family: same public version, any local label.
return other.post == self._cached_post and other.dev == self._cached_dev
# Post family: V itself + any post-release of V.
return other.dev == self._cached_dev or other.post is not None
def _order_key(self) -> _BoundaryOrderKey:
"""Sort key placing this boundary just above the versions it covers.
It extends ``V``'s comparison key ``(epoch, release, suffix)`` with
a trailing ``_BOUNDARY_INF`` local component, so the key sorts after
``V`` and every ``V+local`` (whose keys carry a real, finite local
segment). ``suffix`` is the 6-int comparison suffix
``(pre_rank, pre_n, post_rank, post_n, dev_rank, dev_n)``.
For an AFTER_POSTS boundary the suffix is replaced with one whose
post number is ``_BOUNDARY_INF``, so the key also sorts after every
``V.postN``. An AFTER_LOCALS boundary uses ``V``'s suffix unchanged.
"""
version_key = self.version._key
suffix: _BoundaryOrderSuffix = version_key[2]
if self.kind == BoundaryKind.AFTER_POSTS:
suffix = (suffix[0], suffix[1], 1, _BOUNDARY_INF, 1, 0)
return version_key[0], version_key[1], suffix, _BOUNDARY_INF
def __eq__(self, other: object) -> bool:
# Key off the order key so equality matches the ``<`` / ``>`` order:
# ``AFTER_POSTS(1.0)`` and ``AFTER_POSTS(1.0.post1)`` are the same point.
if isinstance(other, BoundaryVersion):
return self._order_key() == other._order_key()
return NotImplemented
def __lt__(self, other: BoundaryVersion | Version) -> bool:
if isinstance(other, BoundaryVersion):
return self._order_key() < other._order_key()
# boundary < other_version iff V < other AND other not in family.
# The cheap V >= other path short-circuits before the family check.
if not (self.version < other):
return False
return not self._is_family(other)
def __gt__(self, other: BoundaryVersion | Version) -> bool:
# Defined directly to bypass functools.total_ordering's
# NotImplemented round-trip on reflected ``Version < boundary``.
if isinstance(other, BoundaryVersion):
return self._order_key() > other._order_key()
if self.version >= other:
return True
return self._is_family(other)
def __hash__(self) -> int:
# Keyed to ``__eq__`` (the order key), so equal boundaries hash equal.
return hash(self._order_key())
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.version!r}, {self.kind.name})"
if TYPE_CHECKING:
_VersionOrBoundary = Union[Version, BoundaryVersion, None]
@functools.total_ordering
class LowerBound:
"""Lower bound of a version range.
A version *v* of ``None`` means unbounded below (-inf).
At equal versions, ``[v`` sorts before ``(v`` because an inclusive
bound starts earlier.
"""
__slots__ = ("_above", "inclusive", "version")
def __init__(self, version: _VersionOrBoundary, inclusive: bool) -> None:
self.version = version
self.inclusive = inclusive
# Pre-bind a predicate "is parsed at or above this lower
# bound?" for the hot filter / contains loops. One direct
# call per check, no operator-dispatch chain.
if version is None:
self._above: Callable[[Version], bool] | None = None
elif isinstance(version, BoundaryVersion):
# >V produces an AFTER_POSTS lower bound; the upper-side
# range of !=V produces an AFTER_LOCALS lower bound.
if version.kind == BoundaryKind.AFTER_POSTS:
self._above = _make_above_after_posts(version.version)
else:
self._above = _make_above_after_locals(version.version)
elif inclusive:
self._above = version.__le__
else:
self._above = version.__lt__
def __eq__(self, other: object) -> bool:
if not isinstance(other, LowerBound):
return NotImplemented
return self.version == other.version and self.inclusive == other.inclusive
def __lt__(self, other: LowerBound) -> bool:
if not isinstance(other, LowerBound):
return NotImplemented
# -inf < anything (except -inf itself).
if self.version is None:
return other.version is not None
if other.version is None:
return False
if self.version != other.version:
return self.version < other.version
# [v < (v: inclusive starts earlier.
return self.inclusive and not other.inclusive
def __hash__(self) -> int:
return hash((self.version, self.inclusive))
def __repr__(self) -> str:
bracket = "[" if self.inclusive else "("
return f"<{self.__class__.__name__} {bracket}{self.version!r}>"
@functools.total_ordering
class UpperBound:
"""Upper bound of a version range.
A version *v* of ``None`` means unbounded above (+inf).
At equal versions, ``v)`` sorts before ``v]`` because an exclusive
bound ends earlier.
"""
__slots__ = ("_below", "inclusive", "version")
def __init__(self, version: _VersionOrBoundary, inclusive: bool) -> None:
self.version = version
self.inclusive = inclusive
# Pre-bind a predicate "is parsed at or below this upper
# bound?". See LowerBound for the rationale.
if version is None:
self._below: Callable[[Version], bool] | None = None
elif isinstance(version, BoundaryVersion):
# Standard specifiers only ever produce AFTER_LOCALS upper
# bounds (from <=V / ==V / !=V with no local).
if version.kind == BoundaryKind.AFTER_LOCALS:
self._below = _make_below_after_locals(version.version)
else:
# An AFTER_POSTS upper is not produced by any specifier, but
# range algebra reaches it: complementing ``>V`` flips the
# ``AFTER_POSTS(V)`` lower into this upper bound.
self._below = version.__ge__
elif inclusive:
self._below = version.__ge__
else:
self._below = version.__gt__
def __eq__(self, other: object) -> bool:
if not isinstance(other, UpperBound):
return NotImplemented
return self.version == other.version and self.inclusive == other.inclusive
def __lt__(self, other: UpperBound) -> bool:
if not isinstance(other, UpperBound):
return NotImplemented
# Nothing < +inf (except +inf itself).
if self.version is None:
return False
if other.version is None:
return True
if self.version != other.version:
return self.version < other.version
# v) < v]: exclusive ends earlier.
return not self.inclusive and other.inclusive
def __hash__(self) -> int:
return hash((self.version, self.inclusive))
def __repr__(self) -> str:
bracket = "]" if self.inclusive else ")"
return f"<{self.__class__.__name__} {self.version!r}{bracket}>"
if TYPE_CHECKING:
#: A single contiguous interval as a (lower, upper) bound pair.
Interval = tuple[LowerBound, UpperBound]
NEG_INF: Final[LowerBound] = LowerBound(None, False)
POS_INF: Final[UpperBound] = UpperBound(None, False)
FULL_RANGE: Final[tuple[Interval]] = ((NEG_INF, POS_INF),)
def trim_release(release: tuple[int, ...]) -> tuple[int, ...]:
"""Strip trailing zeros from a release tuple for normalized comparison."""
end = len(release)
while end > 1 and release[end - 1] == 0:
end -= 1
return release if end == len(release) else release[:end]
def _next_prefix_dev0(version: Version) -> Version:
"""Smallest version in the next prefix: 1.2 -> 1.3.dev0."""
release = (*version.release[:-1], version.release[-1] + 1)
return Version.from_parts(epoch=version.epoch, release=release, dev=0)
def _base_dev0(version: Version) -> Version:
"""The .dev0 of a version's base release: 1.2 -> 1.2.dev0."""
return Version.from_parts(epoch=version.epoch, release=version.release, dev=0)
def coerce_version(version: Version | str) -> Version | None:
if not isinstance(version, Version):
try:
version = Version(version)
except InvalidVersion:
return None
return version
def _make_above_after_posts(version: Version) -> Callable[[Version], bool]:
"""Predicate ``parsed > AFTER_POSTS(V)`` for a lower bound.
Per PEP 440, ``>V`` excludes V's post-releases unless V is itself
a post-release. AFTER_POSTS sits above V and every V.postN (with
or without local), and just below the next release.
"""
version_ge = version.__ge__
version_epoch = version.epoch
version_pre = version.pre
version_release_trimmed = trim_release(version.release)
trimmed_length = len(version_release_trimmed)
def above(parsed: Version) -> bool:
if version_ge(parsed):
return False
# parsed > V cmpkey-wise: above the boundary iff NOT in V's
# post family.
if parsed.epoch != version_epoch:
return True
parsed_release = parsed.release
if len(parsed_release) < trimmed_length:
return True
if parsed_release[:trimmed_length] != version_release_trimmed:
return True
for i in range(trimmed_length, len(parsed_release)):
if parsed_release[i] != 0:
return True
if parsed.pre != version_pre:
return True
# Same release and pre as V: parsed is in V's post family (V itself,
# V+local, or V.postN), which the boundary sits above. A V.devN
# (different dev, no post) sorts before V and was already caught by
# ``version_ge`` above, so the answer here is always "not above".
return False
return above
def _make_above_after_locals(version: Version) -> Callable[[Version], bool]:
"""Predicate ``parsed > AFTER_LOCALS(V)`` for a lower bound.
Used by the upper-side range of ``!=V`` (when V has no local
segment). AFTER_LOCALS sits above V and every ``V+local`` but
just below ``V.post0``.
"""
version_ge = version.__ge__
version_epoch = version.epoch
version_pre = version.pre
version_post = version.post
version_dev = version.dev
version_release_trimmed = trim_release(version.release)
trimmed_length = len(version_release_trimmed)
def above(parsed: Version) -> bool:
if version_ge(parsed):
return False
# parsed > V cmpkey-wise: above the boundary iff NOT in V's
# local family (same public version, any local segment).
if parsed.epoch != version_epoch:
return True
parsed_release = parsed.release
if len(parsed_release) < trimmed_length:
return True
if parsed_release[:trimmed_length] != version_release_trimmed:
return True
for i in range(trimmed_length, len(parsed_release)):
if parsed_release[i] != 0:
return True
if parsed.pre != version_pre:
return True
if parsed.post != version_post:
return True
return parsed.dev != version_dev
return above
def _make_below_after_locals(version: Version) -> Callable[[Version], bool]:
"""Predicate ``parsed <= AFTER_LOCALS(V)`` for an upper bound.
Used by ``<=V``, ``==V``, ``!=V`` (no local). ``parsed`` is at or
below the boundary when it is at or below V cmpkey-wise, or when
it is in V's local family.
"""
version_ge = version.__ge__
version_epoch = version.epoch
version_pre = version.pre
version_post = version.post
version_dev = version.dev
version_release_trimmed = trim_release(version.release)
trimmed_length = len(version_release_trimmed)
def below(parsed: Version) -> bool:
if version_ge(parsed):
return True
# parsed > V cmpkey-wise: below the boundary iff in V's local
# family.
if parsed.epoch != version_epoch:
return False
parsed_release = parsed.release
if len(parsed_release) < trimmed_length:
return False
if parsed_release[:trimmed_length] != version_release_trimmed:
return False
for i in range(trimmed_length, len(parsed_release)):
if parsed_release[i] != 0:
return False
if parsed.pre != version_pre:
return False
if parsed.post != version_post:
return False
return parsed.dev == version_dev
return below
def least_version_above(boundary: BoundaryVersion) -> Version | None:
"""Smallest real version strictly above *boundary*, or ``None`` if none exists."""
base = boundary.version
if boundary.kind == BoundaryKind.AFTER_LOCALS:
# AFTER_LOCALS(V) sits just below V.post0, so its least successor is
# V.post0.dev0 (V.dev(N+1) if V has a dev, V.post(N+1).dev0 if a post).
if base.dev is not None:
return base.__replace__(dev=base.dev + 1, local=None)
next_post = (base.post + 1) if base.post is not None else 0
return base.__replace__(post=next_post, dev=0, local=None)
# AFTER_POSTS(V): a pre-release V steps to the next pre-release's .dev0;
# a final-release AFTER_POSTS has no least successor.
if base.pre is not None:
kind, number = base.pre
return base.__replace__(pre=(kind, number + 1), post=None, dev=0, local=None)
return None
def range_is_empty(lower: LowerBound, upper: UpperBound) -> bool:
"""True when the range defined by *lower* and *upper* contains no versions.
A boundary lower sits just below the next real version, so an ordered pair
is still empty when the upper excludes that least successor:
``(AFTER_POSTS(1.0a1), 1.0a2.dev0)`` holds no version.
"""
if upper.version is None:
return False
if lower.version is None:
# Nothing sorts below MIN_VERSION, so an exclusive upper at or below it
# leaves an empty floor interval such as ``(-inf, 0.dev0)``.
return (
not upper.inclusive
and isinstance(upper.version, Version)
and upper.version <= MIN_VERSION
)
if isinstance(lower.version, BoundaryVersion):
successor = least_version_above(lower.version)
if successor is not None:
if upper.version == successor:
return not upper.inclusive
return upper.version < successor
if lower.version == upper.version:
return not (lower.inclusive and upper.inclusive)
return lower.version > upper.version
def intersect_ranges(
left: Sequence[Interval],
right: Sequence[Interval],
) -> list[Interval]:
"""Intersect two sorted, non-overlapping range lists (two-pointer merge)."""
result: list[Interval] = []
left_index = right_index = 0
while left_index < len(left) and right_index < len(right):
left_lower, left_upper = left[left_index]
right_lower, right_upper = right[right_index]
lower = max(left_lower, right_lower)
upper = min(left_upper, right_upper)
if not range_is_empty(lower, upper):
result.append((lower, upper))
# Advance whichever side has the smaller upper bound.
if left_upper < right_upper:
left_index += 1
else:
right_index += 1
return result
def filter_by_ranges(
ranges: Sequence[Interval],
iterable: Iterable[Any],
key: Callable[[Any], Version | str] | None,
prereleases: bool | None,
region: Sequence[Interval] = (),
) -> Iterator[Any]:
"""Filter *iterable* against precomputed version *ranges*.
With ``prereleases=None``, the PEP 440 default applies: pre-releases are
excluded unless no final matches, in which case buffered pre-releases come
out at the end. A pre-release inside the opt-in ``region`` is the exception:
it is force-admitted in place, as ``prereleases=True`` would yield it. A
force-admitted pre-release is not a final, so it never suppresses the buffer.
"""
if prereleases is None:
prerelease_buffer: list[Any] = []
found_final = False
if len(ranges) == 1:
# Hot path: most specifiers and small SpecifierSets reduce to
# a single contiguous range.
lower, upper = ranges[0]
above = lower._above
below = upper._below
for item in iterable:
parsed = coerce_version(item if key is None else key(item))
if parsed is None:
continue
if above is not None and not above(parsed):
continue
if below is not None and not below(parsed):
continue
if not parsed.is_prerelease:
found_final = True
yield item
elif region and matches_bounds_only(region, parsed):
yield item
elif not found_final:
prerelease_buffer.append(item)
if not found_final:
yield from prerelease_buffer
return
for item in iterable:
parsed = coerce_version(item if key is None else key(item))
if parsed is None:
continue
for lower, upper in ranges:
above = lower._above
if above is not None and not above(parsed):
break
below = upper._below
if below is None or below(parsed):
if not parsed.is_prerelease:
found_final = True
yield item
elif region and matches_bounds_only(region, parsed):
yield item
elif not found_final:
prerelease_buffer.append(item)
break
if not found_final:
yield from prerelease_buffer
return
exclude_prereleases = prereleases is False
if len(ranges) == 1:
# Hot path: most specifiers and small SpecifierSets reduce to
# a single contiguous range.
lower, upper = ranges[0]
above = lower._above
below = upper._below
for item in iterable:
parsed = coerce_version(item if key is None else key(item))
if parsed is None:
continue
if exclude_prereleases and parsed.is_prerelease:
continue
if above is not None and not above(parsed):
continue
if below is None or below(parsed):
yield item
return
for item in iterable:
parsed = coerce_version(item if key is None else key(item))
if parsed is None:
continue
if exclude_prereleases and parsed.is_prerelease:
continue
for lower, upper in ranges:
above = lower._above
if above is not None and not above(parsed):
break
below = upper._below
if below is None or below(parsed):
yield item
break
def _nearest_release_above_prerelease(version: Version) -> Version:
"""Smallest non-pre-release at or above a pre-release *version*."""
if version.pre is not None:
# An a/b/rc pre-release drops to its final release, which outranks
# every post-release of that pre-release (1.0a1.post0 -> 1.0).
return version.__replace__(pre=None, post=None, dev=None, local=None)
# A dev-only release keeps its post-release (1.0.post0.dev0 -> 1.0.post0,
# whose final 1.0 sorts below it).
return version.__replace__(dev=None, local=None)
def _lowest_release_at_or_above(value: Version | BoundaryVersion | None) -> Version:
"""Smallest non-pre-release version at or above *value*.
``None`` is the ``-inf`` floor, whose nearest non-pre-release is
:data:`MIN_RELEASE`.
"""
if value is None:
return MIN_RELEASE
if isinstance(value, BoundaryVersion):
inner_version = value.version
if inner_version.is_prerelease:
return _nearest_release_above_prerelease(inner_version)
# AFTER_LOCALS(1.0) -> nearest non-pre is 1.0.post0
# AFTER_LOCALS(1.0.post0) -> nearest non-pre is 1.0.post1
next_post = (inner_version.post + 1) if inner_version.post is not None else 0
return inner_version.__replace__(post=next_post, local=None)
if not value.is_prerelease:
return value
return _nearest_release_above_prerelease(value)
def ranges_are_prerelease_only(ranges: Sequence[Interval]) -> bool:
"""True when every range in *ranges* contains only pre-releases.
Used to detect unsatisfiable specifier sets when ``prereleases=False``:
if every range is pre-release-only, every contained version is excluded.
"""
for lower, upper in ranges:
nearest = _lowest_release_at_or_above(lower.version)
if upper.version is None or nearest < upper.version:
return False
if nearest == upper.version and upper.inclusive:
return False
return True
def wildcard_ranges(op: str, base: Version) -> list[Interval]:
"""Ranges for ==V.* and !=V.*.
==1.2.* -> [1.2.dev0, 1.3.dev0); !=1.2.* -> complement.
"""
lower = _base_dev0(base)
upper = _next_prefix_dev0(base)
if op == "==":
return [(LowerBound(lower, True), UpperBound(upper, False))]
# !=
return [
(NEG_INF, UpperBound(lower, False)),
(LowerBound(upper, True), POS_INF),
]
def standard_ranges(op: str, version: Version, has_local: bool) -> list[Interval]:
"""Ranges for the standard PEP 440 operators (no wildcard, no ===).
*has_local* indicates whether the spec string included a ``+local``
segment; relevant only for ``==`` / ``!=`` to decide whether the
upper bound includes V's local family.
"""
if op == ">=":
return [(LowerBound(version, True), POS_INF)]
if op == "<=":
return [
(
NEG_INF,
UpperBound(BoundaryVersion(version, BoundaryKind.AFTER_LOCALS), True),
)
]
if op == ">":
if version.dev is not None:
# >V.devN: dev versions have no post-releases, so the
# next real version is V.dev(N+1).
lower_bound = version.__replace__(dev=version.dev + 1, local=None)
return [(LowerBound(lower_bound, True), POS_INF)]
if version.post is not None:
# >V.postN: next real version is V.post(N+1).dev0.
lower_bound = version.__replace__(post=version.post + 1, dev=0, local=None)
return [(LowerBound(lower_bound, True), POS_INF)]
# >V (final or pre-release V): exclude V itself, V+local, and
# every V.postN per PEP 440.
return [
(
LowerBound(BoundaryVersion(version, BoundaryKind.AFTER_POSTS), False),
POS_INF,
)
]
if op == "<":
# <V excludes pre-releases of V when V is not a pre-release.
# V.dev0 is the earliest pre-release of V.
bound = (
version if version.is_prerelease else version.__replace__(dev=0, local=None)
)
if bound <= MIN_VERSION:
return []
return [(NEG_INF, UpperBound(bound, False))]
# ==, !=: local versions of V match when the spec has no local segment.
after_locals = BoundaryVersion(version, BoundaryKind.AFTER_LOCALS)
upper = version if has_local else after_locals
if op == "==":
return [(LowerBound(version, True), UpperBound(upper, True))]
if op == "!=":
return [
(NEG_INF, UpperBound(version, False)),
(LowerBound(upper, False), POS_INF),
]
if op == "~=":
prefix = version.__replace__(release=version.release[:-1])
return [
(LowerBound(version, True), UpperBound(_next_prefix_dev0(prefix), False))
]
raise ValueError(f"Unknown operator: {op!r}") # pragma: no cover
def bounds_for_spec(op: str, version_str: str, version: Version) -> list[Interval]:
"""Ranges for one specifier's ``(op, version_str)``.
Dispatches between the wildcard and standard builders. ``version`` is the
parsed ``version_str`` (its base, without the trailing ``.*``, for
wildcards). ``===`` is not handled here; its match is a literal string
compared in :mod:`packaging.specifiers`.
"""
if version_str.endswith(".*"):
return wildcard_ranges(op, version)
return standard_ranges(op, version, "+" in version_str)
def intersect_specifier_bounds(
per_specifier_ranges: Iterable[Sequence[Interval]],
) -> Sequence[Interval]:
"""Intersect each specifier's ranges into a single sequence.
Short-circuits once the running intersection is empty, since no later
specifier can revive it. Callers must pass at least one specifier.
"""
result: Sequence[Interval] | None = None
for sub in per_specifier_ranges:
if result is None:
result = sub
else:
result = intersect_ranges(result, sub)
if not result:
break
if result is None: # pragma: no cover - callers guard non-empty input
raise RuntimeError("intersect_specifier_bounds called with no specifiers")
return result
def matches_bounds_only(ranges: Sequence[Interval], version: Version) -> bool:
"""Whether ``version`` falls within any of ``ranges``.
The pure bounds membership test, for a single already-parsed version with
no pre-release policy applied. ``ranges`` are sorted and non-overlapping,
so a version below one range's lower bound is below every later range too.
"""
for lower, upper in ranges:
above = lower._above
if above is not None and not above(version):
return False
below = upper._below
if below is None or below(version):
return True
return False
def resolve_prereleases(
configured: bool | None, autodetected: bool | None
) -> bool | None:
"""Resolve a specifier's effective default pre-release policy.
An explicit ``configured`` value wins; otherwise an autodetected ``True``
propagates and anything else falls back to the PEP 440 default (``None``).
"""
if configured is not None:
return configured
if autodetected:
return True
return None
@@ -0,0 +1,33 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
"""Backward-compatibility shim for unpickling Version objects serialized before
packaging 26.1.
Old pickles reference ``packaging._structures.InfinityType`` and
``packaging._structures.NegativeInfinityType``. This module provides minimal
stand-in classes so that ``pickle.loads()`` can resolve those references.
The deserialized objects are not used for comparisons — ``Version.__setstate__``
discards the stale ``_key`` cache and recomputes it from the core version fields.
"""
from __future__ import annotations
class InfinityType:
"""Stand-in for the removed ``InfinityType`` used in old comparison keys."""
def __repr__(self) -> str:
return "Infinity"
class NegativeInfinityType:
"""Stand-in for the removed ``NegativeInfinityType`` used in old comparison keys."""
def __repr__(self) -> str:
return "-Infinity"
Infinity = InfinityType()
NegativeInfinity = NegativeInfinityType()
@@ -0,0 +1,200 @@
from __future__ import annotations
import contextlib
import re
from dataclasses import dataclass
from typing import TYPE_CHECKING, NoReturn
from .specifiers import Specifier
if TYPE_CHECKING:
from collections.abc import Generator, Mapping
@dataclass
class Token:
__slots__ = ("name", "position", "text")
name: str
text: str
position: int
class ParserSyntaxError(Exception):
"""The provided source text could not be parsed correctly."""
def __init__(
self,
message: str,
*,
source: str,
span: tuple[int, int],
) -> None:
self.span = span
self.message = message
self.source = source
super().__init__()
def __str__(self) -> str:
marker = " " * self.span[0] + "~" * (self.span[1] - self.span[0]) + "^"
return f"{self.message}\n {self.source}\n {marker}"
DEFAULT_RULES: dict[str, re.Pattern[str]] = {
"LEFT_PARENTHESIS": re.compile(r"\("),
"RIGHT_PARENTHESIS": re.compile(r"\)"),
"LEFT_BRACKET": re.compile(r"\["),
"RIGHT_BRACKET": re.compile(r"\]"),
"SEMICOLON": re.compile(r";"),
"COMMA": re.compile(r","),
"QUOTED_STRING": re.compile(
r"""
(
('[^']*')
|
("[^"]*")
)
""",
re.VERBOSE,
),
"OP": re.compile(r"(===|==|~=|!=|<=|>=|<|>)"),
"BOOLOP": re.compile(r"\b(or|and)\b"),
"IN": re.compile(r"\bin\b"),
"NOT": re.compile(r"\bnot\b"),
"VARIABLE": re.compile(
r"""
\b(
python_version
|python_full_version
|os[._]name
|sys[._]platform
|platform_(release|system)
|platform[._](version|machine|python_implementation)
|python_implementation
|implementation_(name|version)
|extras?
|dependency_groups
)\b
""",
re.VERBOSE,
),
"SPECIFIER": re.compile(
Specifier._specifier_regex_str,
re.VERBOSE | re.IGNORECASE,
),
"AT": re.compile(r"\@"),
"URL": re.compile(r"[^ \t]+"),
"IDENTIFIER": re.compile(r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b"),
"VERSION_PREFIX_TRAIL": re.compile(r"\.\*"),
"VERSION_LOCAL_LABEL_TRAIL": re.compile(r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*"),
"WS": re.compile(r"[ \t]+"),
"END": re.compile(r"\Z"),
}
class Tokenizer:
"""Context-sensitive token parsing.
Provides methods to examine the input stream to check whether the next token
matches.
"""
__slots__ = ("next_token", "position", "rules", "source")
def __init__(
self,
source: str,
*,
rules: Mapping[str, re.Pattern[str]],
) -> None:
self.source = source
self.rules = rules
self.next_token: Token | None = None
self.position = 0
def consume(self, name: str) -> None:
"""Move beyond provided token name, if at current position."""
if self.check(name):
self.read()
def check(self, name: str, *, peek: bool = False) -> bool:
"""Check whether the next token has the provided name.
By default, if the check succeeds, the token *must* be read before
another check. If `peek` is set to `True`, the token is not loaded and
would need to be checked again.
"""
assert self.next_token is None, (
f"Cannot check for {name!r}, already have {self.next_token!r}"
)
assert name in self.rules, f"Unknown token name: {name!r}"
expression = self.rules[name]
match = expression.match(self.source, self.position)
if match is None:
return False
if not peek:
self.next_token = Token(name, match[0], self.position)
return True
def expect(self, name: str, *, expected: str) -> Token:
"""Expect a certain token name next, failing with a syntax error otherwise.
The token is read and returned.
"""
if not self.check(name):
raise self.raise_syntax_error(f"Expected {expected}")
return self.read()
def read(self) -> Token:
"""Consume the next token and return it."""
token = self.next_token
assert token is not None
self.position += len(token.text)
self.next_token = None
return token
def raise_syntax_error(
self,
message: str,
*,
span_start: int | None = None,
span_end: int | None = None,
) -> NoReturn:
"""Raise ParserSyntaxError at the given position."""
span = (
self.position if span_start is None else span_start,
self.position if span_end is None else span_end,
)
raise ParserSyntaxError(
message,
source=self.source,
span=span,
)
@contextlib.contextmanager
def enclosing_tokens(
self, open_token: str, close_token: str, *, around: str
) -> Generator[None, None, None]:
if self.check(open_token):
open_position = self.position
self.read()
else:
open_position = None
yield
if open_position is None:
return
if not self.check(close_token):
self.raise_syntax_error(
f"Expected matching {close_token} for {open_token}, after {around}",
span_start=open_position,
)
self.read()
@@ -0,0 +1,335 @@
from __future__ import annotations
import re
from collections.abc import Mapping, Sequence
from .errors import _ErrorCollector
from .requirements import InvalidRequirement, Requirement
__all__ = [
"CyclicDependencyGroup",
"DependencyGroupInclude",
"DependencyGroupResolver",
"DuplicateGroupNames",
"InvalidDependencyGroupObject",
"resolve_dependency_groups",
]
def __dir__() -> list[str]:
return __all__
# -----------
# Error Types
# -----------
class DuplicateGroupNames(ValueError):
"""
The same dependency groups were defined twice, with different non-normalized names.
.. versionadded:: 26.1
"""
class CyclicDependencyGroup(ValueError):
"""
The dependency group includes form a cycle.
.. versionadded:: 26.1
"""
def __init__(self, requested_group: str, group: str, include_group: str) -> None:
self.requested_group = requested_group
self.group = group
self.include_group = include_group
if include_group == group:
reason = f"{group} includes itself"
else:
reason = f"{include_group} -> {group}, {group} -> {include_group}"
super().__init__(
"Cyclic dependency group include while resolving "
f"{requested_group}: {reason}"
)
# Support pickling; ``args`` does not match ``__init__``'s signature.
def __reduce__(self) -> tuple[type[CyclicDependencyGroup], tuple[str, str, str]]:
return (self.__class__, (self.requested_group, self.group, self.include_group))
# in the PEP 735 spec, the tables in dependency group lists were described as
# "Dependency Object Specifiers", but the only defined type of object was a
# "Dependency Group Include" -- hence the naming of this error as "Object"
class InvalidDependencyGroupObject(ValueError):
"""
A member of a dependency group was identified as a dict, but was not in a valid
format.
.. versionadded:: 26.1
"""
# ------------------------
# Object Model & Interface
# ------------------------
class DependencyGroupInclude:
"""
A reference to another dependency group by name.
.. versionadded:: 26.1
"""
__slots__ = ("include_group",)
def __init__(self, include_group: str) -> None:
"""
Initialize a DependencyGroupInclude.
:param include_group: The name of the group referred to by this include.
"""
self.include_group = include_group
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.include_group!r})"
class DependencyGroupResolver:
"""
A resolver for Dependency Group data.
This class handles caching, name normalization, cycle detection, and other
parsing requirements. There are only two public methods for exploring the data:
``lookup()`` and ``resolve()``.
:param dependency_groups: A mapping, as provided via pyproject
``[dependency-groups]``.
.. versionadded:: 26.1
"""
def __init__(
self,
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]],
) -> None:
errors = _ErrorCollector()
self.dependency_groups = _normalize_group_names(dependency_groups, errors)
# a map of group names to parsed data
self._parsed_groups: dict[
str, tuple[Requirement | DependencyGroupInclude, ...]
] = {}
# a map of group names to their ancestors, used for cycle detection
self._include_graph_ancestors: dict[str, tuple[str, ...]] = {}
# a cache of completed resolutions to Requirement lists
self._resolve_cache: dict[str, tuple[Requirement, ...]] = {}
errors.finalize("[dependency-groups] data was invalid")
def lookup(self, group: str) -> tuple[Requirement | DependencyGroupInclude, ...]:
"""
Lookup a group name, returning the parsed dependency data for that group.
This will not resolve includes.
:param group: the name of the group to lookup
"""
group = _normalize_name(group)
with _ErrorCollector().on_exit(
f"[dependency-groups] data for {group!r} was malformed"
) as errors:
return self._parse_group(group, errors)
def resolve(self, group: str) -> tuple[Requirement, ...]:
"""
Resolve a dependency group to a list of requirements.
:param group: the name of the group to resolve
"""
group = _normalize_name(group)
with _ErrorCollector().on_exit(
f"[dependency-groups] data for {group!r} was malformed"
) as errors:
return self._resolve(group, group, errors)
def _resolve(
self, group: str, requested_group: str, errors: _ErrorCollector
) -> tuple[Requirement, ...]:
"""
This is a helper for cached resolution to strings. It preserves the name of the
group which the user initially requested in order to present a clearer error in
the event that a cycle is detected.
:param group: The normalized name of the group to resolve.
:param requested_group: The group which was used in the original, user-facing
request.
"""
if group in self._resolve_cache:
return self._resolve_cache[group]
parsed = self._parse_group(group, errors)
resolved_group = []
for item in parsed:
if isinstance(item, Requirement):
resolved_group.append(item)
elif isinstance(item, DependencyGroupInclude):
include_group = _normalize_name(item.include_group)
# if a group is cyclic, record the error
# otherwise, follow the include_group reference
#
# this allows us to examine all includes in a group, even in the
# presence of errors
if include_group in self._include_graph_ancestors.get(group, ()):
errors.error(
CyclicDependencyGroup(
requested_group, group, item.include_group
)
)
else:
self._include_graph_ancestors[include_group] = (
*self._include_graph_ancestors.get(group, ()),
group,
)
resolved_group.extend(
self._resolve(include_group, requested_group, errors)
)
else: # pragma: no cover
raise NotImplementedError(
f"Invalid dependency group item after parse: {item}"
)
# in the event that errors were detected, present the group as empty and do not
# cache the result
# this ensures that repeated access to a cyclic group will raise multiple errors
if errors.errors:
return ()
self._resolve_cache[group] = tuple(resolved_group)
return self._resolve_cache[group]
def _parse_group(
self, group: str, errors: _ErrorCollector
) -> tuple[Requirement | DependencyGroupInclude, ...]:
# short circuit -- never do the work twice
if group in self._parsed_groups:
return self._parsed_groups[group]
if group not in self.dependency_groups:
errors.error(LookupError(f"Dependency group '{group}' not found"))
return ()
raw_group = self.dependency_groups[group]
if isinstance(raw_group, str):
errors.error(
TypeError(
f"Dependency group {group!r} contained a string rather than a list."
)
)
return ()
if not isinstance(raw_group, Sequence):
errors.error(
TypeError(f"Dependency group {group!r} is not a sequence type.")
)
return ()
elements: list[Requirement | DependencyGroupInclude] = []
for item in raw_group:
if isinstance(item, str):
# packaging.requirements.Requirement parsing ensures that this is a
# valid PEP 508 Dependency Specifier. Collect InvalidRequirement
# if it throws that.
with errors.collect(InvalidRequirement):
elements.append(Requirement(item))
elif isinstance(item, Mapping):
if tuple(item.keys()) != ("include-group",):
errors.error(
InvalidDependencyGroupObject(
f"Invalid dependency group item: {item!r}"
)
)
else:
include_group = item["include-group"]
if not isinstance(include_group, str):
msg = (
"Dependency group include-group value is not a string: "
f"{item!r}"
)
errors.error(TypeError(msg))
else:
elements.append(
DependencyGroupInclude(include_group=include_group)
)
else:
errors.error(TypeError(f"Invalid dependency group item: {item!r}"))
if errors.errors:
return ()
self._parsed_groups[group] = tuple(elements)
return self._parsed_groups[group]
# --------------------
# Functional Interface
# --------------------
def resolve_dependency_groups(
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]], /, *groups: str
) -> tuple[str, ...]:
"""
Resolve a dependency group to a tuple of requirements, as strings.
:param dependency_groups: the parsed contents of the ``[dependency-groups]`` table
from ``pyproject.toml``
:param groups: the name of the group(s) to resolve
.. versionadded:: 26.1
"""
resolver = DependencyGroupResolver(dependency_groups)
return tuple(str(r) for group in groups for r in resolver.resolve(group))
# ----------------
# internal helpers
# ----------------
_NORMALIZE_PATTERN = re.compile(r"[-_.]+")
def _normalize_name(name: str) -> str:
return _NORMALIZE_PATTERN.sub("-", name).lower()
def _normalize_group_names(
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]],
errors: _ErrorCollector,
) -> dict[str, Sequence[str | Mapping[str, str]]]:
original_names: dict[str, list[str]] = {}
normalized_groups: dict[str, Sequence[str | Mapping[str, str]]] = {}
for group_name, value in dependency_groups.items():
normed_group_name = _normalize_name(group_name)
original_names.setdefault(normed_group_name, []).append(group_name)
normalized_groups[normed_group_name] = value
for normed_name, names in original_names.items():
if len(names) > 1:
errors.error(
DuplicateGroupNames(
"Duplicate dependency group names: "
f"{normed_name} ({', '.join(names)})"
)
)
return normalized_groups
@@ -0,0 +1,349 @@
from __future__ import annotations
import dataclasses
import re
import urllib.parse
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, Protocol, TypeVar
if TYPE_CHECKING: # pragma: no cover
import sys
from collections.abc import Collection
from urllib.parse import SplitResult
if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self
__all__ = [
"ArchiveInfo",
"DirInfo",
"DirectUrl",
"DirectUrlValidationError",
"VcsInfo",
]
def __dir__() -> list[str]:
return __all__
_T = TypeVar("_T")
class _FromMappingProtocol(Protocol): # pragma: no cover
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self: ...
_FromMappingProtocolT = TypeVar("_FromMappingProtocolT", bound=_FromMappingProtocol)
def _json_dict_factory(data: list[tuple[str, Any]]) -> dict[str, Any]:
return {key: value for key, value in data if value is not None}
def _get(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T | None:
"""Get a value from the dictionary and verify it's the expected type."""
if (value := d.get(key)) is None:
return None
if not isinstance(value, expected_type):
raise DirectUrlValidationError(
f"Unexpected type {type(value).__name__} "
f"(expected {expected_type.__name__})",
context=key,
)
return value
def _get_required(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T:
"""Get a required value from the dictionary and verify it's the expected type."""
if (value := _get(d, expected_type, key)) is None:
raise _DirectUrlRequiredKeyError(key)
return value
def _get_object(
d: Mapping[str, Any], target_type: type[_FromMappingProtocolT], key: str
) -> _FromMappingProtocolT | None:
"""Get a dictionary value from the dictionary and convert it to a dataclass."""
if (value := _get(d, Mapping, key)) is None: # type: ignore[type-abstract]
return None
try:
return target_type._from_dict(value)
except Exception as e:
raise DirectUrlValidationError(e, context=key) from e
_PEP610_USER_PASS_ENV_VARS_REGEX = re.compile(
r"^\$\{[A-Za-z0-9-_]+\}(:\$\{[A-Za-z0-9-_]+\})?$"
)
def _strip_auth_from_netloc(netloc: str, safe_user_passwords: Collection[str]) -> str:
if "@" not in netloc:
return netloc
user_pass, netloc_no_user_pass = netloc.rsplit("@", 1)
if user_pass in safe_user_passwords:
return netloc
if _PEP610_USER_PASS_ENV_VARS_REGEX.match(user_pass):
return netloc
return netloc_no_user_pass
def _strip_url(url: str, safe_user_passwords: Collection[str]) -> str:
"""url with user:password part removed unless it is formed with
environment variables as specified in PEP 610, or it is a safe user:password
such as `git`.
"""
parsed_url = urllib.parse.urlsplit(url)
netloc = _strip_auth_from_netloc(parsed_url.netloc, safe_user_passwords)
return urllib.parse.urlunsplit(
(
parsed_url.scheme,
netloc,
parsed_url.path,
parsed_url.query,
parsed_url.fragment,
)
)
def _file_url_has_absolute_path(parsed_url: SplitResult) -> bool:
return parsed_url.path.startswith("/")
class DirectUrlValidationError(Exception):
"""Raised when when input data is not spec-compliant.
.. versionadded:: 26.1
"""
context: str | None = None
message: str
def __init__(
self,
cause: str | Exception,
*,
context: str | None = None,
) -> None:
if isinstance(cause, DirectUrlValidationError):
if cause.context:
self.context = (
f"{context}.{cause.context}" if context else cause.context
)
else:
self.context = context # pragma: no cover
self.message = cause.message
else:
self.context = context
self.message = str(cause)
def __str__(self) -> str:
if self.context:
return f"{self.message} in {self.context!r}"
return self.message
class _DirectUrlRequiredKeyError(DirectUrlValidationError):
def __init__(self, key: str) -> None:
super().__init__("Missing required value", context=key)
@dataclasses.dataclass(frozen=True, init=False)
class VcsInfo:
"""The version control information of a :class:`DirectUrl`."""
vcs: str
commit_id: str
requested_revision: str | None = None
def __init__(
self,
*,
vcs: str,
commit_id: str,
requested_revision: str | None = None,
) -> None:
object.__setattr__(self, "vcs", vcs)
object.__setattr__(self, "commit_id", commit_id)
object.__setattr__(self, "requested_revision", requested_revision)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
# We can't validate vcs value because is not closed.
return cls(
vcs=_get_required(d, str, "vcs"),
requested_revision=_get(d, str, "requested_revision"),
commit_id=_get_required(d, str, "commit_id"),
)
@dataclasses.dataclass(frozen=True, init=False)
class ArchiveInfo:
"""The archive information of a :class:`DirectUrl`."""
hashes: Mapping[str, str] | None = None
def __init__(
self,
*,
hashes: Mapping[str, str] | None = None,
) -> None:
object.__setattr__(self, "hashes", hashes)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
hashes = _get(d, Mapping, "hashes") # type: ignore[type-abstract]
if hashes is not None and not all(isinstance(h, str) for h in hashes.values()):
raise DirectUrlValidationError(
"Hash values must be strings", context="hashes"
)
legacy_hash = _get(d, str, "hash")
if legacy_hash is not None:
if "=" not in legacy_hash:
raise DirectUrlValidationError(
"Invalid hash format (expected '<algorithm>=<hash>')",
context="hash",
)
hash_algorithm, hash_value = legacy_hash.split("=", 1)
if hashes is None:
# if `hashes` are not present, we can derive it from the legacy `hash`
hashes = {hash_algorithm: hash_value}
else:
# if `hashes` are present, the legacy `hash` must match one of them
if hash_algorithm not in hashes:
raise DirectUrlValidationError(
f"Algorithm {hash_algorithm!r} used in hash field "
f"is not present in hashes field",
context="hashes",
)
if hashes[hash_algorithm] != hash_value:
raise DirectUrlValidationError(
f"Algorithm {hash_algorithm!r} used in hash field "
f"has different value in hashes field",
context="hash",
)
return cls(hashes=hashes)
@dataclasses.dataclass(frozen=True, init=False)
class DirInfo:
"""The local directory information of a :class:`DirectUrl`."""
editable: bool | None = None
def __init__(
self,
*,
editable: bool | None = None,
) -> None:
object.__setattr__(self, "editable", editable)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
return cls(
editable=_get(d, bool, "editable"),
)
@dataclasses.dataclass(frozen=True, init=False)
class DirectUrl:
"""A class representing a direct URL.
.. versionadded:: 26.1
"""
url: str
archive_info: ArchiveInfo | None = None
vcs_info: VcsInfo | None = None
dir_info: DirInfo | None = None
subdirectory: str | None = None # XXX Path or str?
def __init__(
self,
*,
url: str,
archive_info: ArchiveInfo | None = None,
vcs_info: VcsInfo | None = None,
dir_info: DirInfo | None = None,
subdirectory: str | None = None,
) -> None:
object.__setattr__(self, "url", url)
object.__setattr__(self, "archive_info", archive_info)
object.__setattr__(self, "vcs_info", vcs_info)
object.__setattr__(self, "dir_info", dir_info)
object.__setattr__(self, "subdirectory", subdirectory)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
direct_url = cls(
url=_get_required(d, str, "url"),
archive_info=_get_object(d, ArchiveInfo, "archive_info"),
vcs_info=_get_object(d, VcsInfo, "vcs_info"),
dir_info=_get_object(d, DirInfo, "dir_info"),
subdirectory=_get(d, str, "subdirectory"),
)
if (
bool(direct_url.vcs_info)
+ bool(direct_url.archive_info)
+ bool(direct_url.dir_info)
) != 1:
raise DirectUrlValidationError(
"Exactly one of vcs_info, archive_info, dir_info must be present"
)
if direct_url.dir_info is not None:
parsed_url = urllib.parse.urlsplit(direct_url.url)
if parsed_url.scheme != "file":
raise DirectUrlValidationError(
"URL scheme must be file:// when dir_info is present",
context="url",
)
if not _file_url_has_absolute_path(parsed_url):
raise DirectUrlValidationError(
"File URL must be absolute when dir_info is present",
context="url",
)
# XXX subdirectory must be relative, can we, should we validate that here?
return direct_url
@classmethod
def from_dict(cls, d: Mapping[str, Any], /) -> Self:
"""Create and validate a DirectUrl instance from a JSON dictionary."""
return cls._from_dict(d)
def to_dict(
self,
*,
generate_legacy_hash: bool = False,
strip_user_password: bool = True,
safe_user_passwords: Collection[str] = ("git",),
) -> Mapping[str, Any]:
"""Convert the DirectUrl instance to a JSON dictionary.
:param generate_legacy_hash: If True, include a legacy `hash` field in
`archive_info` for backward compatibility with tools that don't
support the `hashes` field.
:param strip_user_password: If True, strip user:password from the URL
unless it is formed with environment variables as specified in PEP
610, or it is a safe user:password such as `git`.
:param safe_user_passwords: A collection of user:password strings that
should not be stripped from the URL even if `strip_user_password` is
True.
"""
res = dataclasses.asdict(self, dict_factory=_json_dict_factory)
if generate_legacy_hash and self.archive_info and self.archive_info.hashes:
hash_algorithm, hash_value = next(iter(self.archive_info.hashes.items()))
res["archive_info"]["hash"] = f"{hash_algorithm}={hash_value}"
if strip_user_password:
res["url"] = _strip_url(self.url, safe_user_passwords)
return res
def validate(self) -> None:
"""Validate the DirectUrl instance against the specification.
Raises :class:`DirectUrlValidationError` if invalid.
"""
self.from_dict(self.to_dict())
@@ -0,0 +1,94 @@
from __future__ import annotations
import contextlib
import dataclasses
import sys
import typing
__all__ = ["ExceptionGroup"]
def __dir__() -> list[str]:
return __all__
if sys.version_info >= (3, 11): # pragma: no cover
from builtins import ExceptionGroup
else: # pragma: no cover
class ExceptionGroup(Exception):
"""A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11.
If :external:exc:`ExceptionGroup` is already defined by Python itself,
that version is used instead.
"""
message: str
exceptions: list[Exception]
def __init__(self, message: str, exceptions: list[Exception]) -> None:
self.message = message
self.exceptions = exceptions
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.message!r}, {self.exceptions!r})"
@dataclasses.dataclass
class _ErrorCollector:
"""
Collect errors into ExceptionGroups.
Used like this:
collector = _ErrorCollector()
# Add a single exception
collector.error(ValueError("one"))
# Supports nesting, including combining ExceptionGroups
with collector.collect():
raise ValueError("two")
collector.finalize("Found some errors")
Since making a collector and then calling finalize later is a common pattern,
a convenience method ``on_exit`` is provided.
"""
errors: list[Exception] = dataclasses.field(default_factory=list, init=False)
def finalize(self, msg: str) -> None:
"""Raise a group exception if there are any errors."""
if self.errors:
raise ExceptionGroup(msg, self.errors)
@contextlib.contextmanager
def on_exit(self, msg: str) -> typing.Generator[_ErrorCollector, None, None]:
"""
Calls finalize if no uncollected errors were present.
Uncollected errors are raised normally.
"""
yield self
self.finalize(msg)
@contextlib.contextmanager
def collect(self, *err_cls: type[Exception]) -> typing.Generator[None, None, None]:
"""
Context manager to collect errors into the error list.
Must be inside loops, as only one error can be collected at a time.
"""
error_classes = err_cls or (Exception,)
try:
yield
except ExceptionGroup as error:
self.errors.extend(error.exceptions)
except error_classes as error:
self.errors.append(error)
def error(
self,
error: Exception,
) -> None:
"""Add an error to the list."""
self.errors.append(error)
@@ -0,0 +1,198 @@
#######################################################################################
#
# Adapted from:
# https://github.com/pypa/hatch/blob/5352e44/backend/src/hatchling/licenses/parse.py
#
# MIT License
#
# Copyright (c) 2017-present Ofek Lev <oss@ofek.dev>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this
# software and associated documentation files (the "Software"), to deal in the Software
# without restriction, including without limitation the rights to use, copy, modify,
# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be included in all copies
# or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
# OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
#
# With additional allowance of arbitrary `LicenseRef-` identifiers, not just
# `LicenseRef-Public-Domain` and `LicenseRef-Proprietary`.
#
#######################################################################################
from __future__ import annotations
import re
from typing import NewType, cast
from ._spdx import EXCEPTIONS, LICENSES
__all__ = [
"InvalidLicenseExpression",
"NormalizedLicenseExpression",
"canonicalize_license_expression",
]
# Simple __dir__ implementation since there are no public submodules
def __dir__() -> list[str]:
return __all__
license_ref_allowed = re.compile("^[A-Za-z0-9.-]+$")
NormalizedLicenseExpression = NewType("NormalizedLicenseExpression", str)
"""
A :class:`typing.NewType` of :class:`str`, representing a normalized
License-Expression.
"""
class InvalidLicenseExpression(ValueError):
"""Raised when a license-expression string is invalid
>>> from packaging.licenses import canonicalize_license_expression
>>> canonicalize_license_expression("invalid")
Traceback (most recent call last):
...
packaging.licenses.InvalidLicenseExpression: Unknown license: 'invalid'
"""
def canonicalize_license_expression(
raw_license_expression: str,
) -> NormalizedLicenseExpression:
"""
This function takes a valid License-Expression, and returns the normalized
form of it.
The return type is typed as :class:`NormalizedLicenseExpression`. This
allows type checkers to help require that a string has passed through this
function before use.
:param str raw_license_expression: The License-Expression to canonicalize.
:raises InvalidLicenseExpression: If the License-Expression is invalid due to an
invalid/unknown license identifier or invalid syntax.
.. doctest::
>>> from packaging.licenses import canonicalize_license_expression
>>> canonicalize_license_expression("mit")
'MIT'
>>> canonicalize_license_expression("mit and (apache-2.0 or bsd-2-clause)")
'MIT AND (Apache-2.0 OR BSD-2-Clause)'
>>> canonicalize_license_expression("(mit")
Traceback (most recent call last):
...
InvalidLicenseExpression: Invalid license expression: '(mit'
>>> canonicalize_license_expression("Use-it-after-midnight")
Traceback (most recent call last):
...
InvalidLicenseExpression: Unknown license: 'Use-it-after-midnight'
"""
if not raw_license_expression:
message = f"Invalid license expression: {raw_license_expression!r}"
raise InvalidLicenseExpression(message)
# Pad any parentheses so tokenization can be achieved by merely splitting on
# whitespace.
license_expression = raw_license_expression.replace("(", " ( ").replace(")", " ) ")
licenseref_prefix = "LicenseRef-"
license_refs = {
ref.lower(): "LicenseRef-" + ref[len(licenseref_prefix) :]
for ref in license_expression.split()
if ref.lower().startswith(licenseref_prefix.lower())
}
# Normalize to lower case so we can look up licenses/exceptions
# and so boolean operators are Python-compatible.
license_expression = license_expression.lower()
tokens = license_expression.split()
# Rather than implementing a parenthesis/boolean logic parser, create an
# expression that Python can parse. Everything that is not involved with the
# grammar itself is replaced with the placeholder `False` and the resultant
# expression should become a valid Python expression.
python_tokens = []
for token in tokens:
if token not in {"or", "and", "with", "(", ")"}:
python_tokens.append("False")
elif token == "with":
python_tokens.append("or")
elif (
token == "("
and python_tokens
and python_tokens[-1] not in {"or", "and", "("}
) or (token == ")" and python_tokens and python_tokens[-1] == "("):
message = f"Invalid license expression: {raw_license_expression!r}"
raise InvalidLicenseExpression(message)
else:
python_tokens.append(token)
python_expression = " ".join(python_tokens)
try:
compile(python_expression, "", "eval")
except SyntaxError:
message = f"Invalid license expression: {raw_license_expression!r}"
raise InvalidLicenseExpression(message) from None
# Take a final pass to check for unknown licenses/exceptions.
normalized_tokens = []
last_license_start = False
for index, token in enumerate(tokens):
if token in {"or", "and", "with", "(", ")"}:
if token == "with" and (
not last_license_start
or index + 1 == len(tokens)
or tokens[index + 1] in {"or", "and", "with", "(", ")"}
):
message = f"Invalid license expression: {raw_license_expression!r}"
raise InvalidLicenseExpression(message)
normalized_tokens.append(token.upper())
last_license_start = False
continue
if normalized_tokens and normalized_tokens[-1] == "WITH":
if token not in EXCEPTIONS:
message = f"Unknown license exception: {token!r}"
raise InvalidLicenseExpression(message)
normalized_tokens.append(EXCEPTIONS[token]["id"])
last_license_start = False
else:
if token.endswith("+"):
final_token = token[:-1]
suffix = "+"
else:
final_token = token
suffix = ""
if final_token.startswith("licenseref-"):
license_ref_id = final_token[len("licenseref-") :]
if suffix or not license_ref_allowed.match(license_ref_id):
message = f"Invalid licenseref: {token!r}"
raise InvalidLicenseExpression(message)
normalized_tokens.append(license_refs[final_token])
else:
if final_token not in LICENSES:
message = f"Unknown license: {final_token!r}"
raise InvalidLicenseExpression(message)
normalized_tokens.append(LICENSES[final_token]["id"] + suffix)
last_license_start = True
normalized_expression = " ".join(normalized_tokens)
return cast(
"NormalizedLicenseExpression",
normalized_expression.replace("( ", "(").replace(" )", ")"),
)
@@ -0,0 +1,799 @@
from __future__ import annotations
from typing import TypedDict
class SPDXLicense(TypedDict):
id: str
deprecated: bool
class SPDXException(TypedDict):
id: str
deprecated: bool
VERSION = '3.27.0'
LICENSES: dict[str, SPDXLicense] = {
'0bsd': {'id': '0BSD', 'deprecated': False},
'3d-slicer-1.0': {'id': '3D-Slicer-1.0', 'deprecated': False},
'aal': {'id': 'AAL', 'deprecated': False},
'abstyles': {'id': 'Abstyles', 'deprecated': False},
'adacore-doc': {'id': 'AdaCore-doc', 'deprecated': False},
'adobe-2006': {'id': 'Adobe-2006', 'deprecated': False},
'adobe-display-postscript': {'id': 'Adobe-Display-PostScript', 'deprecated': False},
'adobe-glyph': {'id': 'Adobe-Glyph', 'deprecated': False},
'adobe-utopia': {'id': 'Adobe-Utopia', 'deprecated': False},
'adsl': {'id': 'ADSL', 'deprecated': False},
'afl-1.1': {'id': 'AFL-1.1', 'deprecated': False},
'afl-1.2': {'id': 'AFL-1.2', 'deprecated': False},
'afl-2.0': {'id': 'AFL-2.0', 'deprecated': False},
'afl-2.1': {'id': 'AFL-2.1', 'deprecated': False},
'afl-3.0': {'id': 'AFL-3.0', 'deprecated': False},
'afmparse': {'id': 'Afmparse', 'deprecated': False},
'agpl-1.0': {'id': 'AGPL-1.0', 'deprecated': True},
'agpl-1.0-only': {'id': 'AGPL-1.0-only', 'deprecated': False},
'agpl-1.0-or-later': {'id': 'AGPL-1.0-or-later', 'deprecated': False},
'agpl-3.0': {'id': 'AGPL-3.0', 'deprecated': True},
'agpl-3.0-only': {'id': 'AGPL-3.0-only', 'deprecated': False},
'agpl-3.0-or-later': {'id': 'AGPL-3.0-or-later', 'deprecated': False},
'aladdin': {'id': 'Aladdin', 'deprecated': False},
'amd-newlib': {'id': 'AMD-newlib', 'deprecated': False},
'amdplpa': {'id': 'AMDPLPA', 'deprecated': False},
'aml': {'id': 'AML', 'deprecated': False},
'aml-glslang': {'id': 'AML-glslang', 'deprecated': False},
'ampas': {'id': 'AMPAS', 'deprecated': False},
'antlr-pd': {'id': 'ANTLR-PD', 'deprecated': False},
'antlr-pd-fallback': {'id': 'ANTLR-PD-fallback', 'deprecated': False},
'any-osi': {'id': 'any-OSI', 'deprecated': False},
'any-osi-perl-modules': {'id': 'any-OSI-perl-modules', 'deprecated': False},
'apache-1.0': {'id': 'Apache-1.0', 'deprecated': False},
'apache-1.1': {'id': 'Apache-1.1', 'deprecated': False},
'apache-2.0': {'id': 'Apache-2.0', 'deprecated': False},
'apafml': {'id': 'APAFML', 'deprecated': False},
'apl-1.0': {'id': 'APL-1.0', 'deprecated': False},
'app-s2p': {'id': 'App-s2p', 'deprecated': False},
'apsl-1.0': {'id': 'APSL-1.0', 'deprecated': False},
'apsl-1.1': {'id': 'APSL-1.1', 'deprecated': False},
'apsl-1.2': {'id': 'APSL-1.2', 'deprecated': False},
'apsl-2.0': {'id': 'APSL-2.0', 'deprecated': False},
'arphic-1999': {'id': 'Arphic-1999', 'deprecated': False},
'artistic-1.0': {'id': 'Artistic-1.0', 'deprecated': False},
'artistic-1.0-cl8': {'id': 'Artistic-1.0-cl8', 'deprecated': False},
'artistic-1.0-perl': {'id': 'Artistic-1.0-Perl', 'deprecated': False},
'artistic-2.0': {'id': 'Artistic-2.0', 'deprecated': False},
'artistic-dist': {'id': 'Artistic-dist', 'deprecated': False},
'aspell-ru': {'id': 'Aspell-RU', 'deprecated': False},
'aswf-digital-assets-1.0': {'id': 'ASWF-Digital-Assets-1.0', 'deprecated': False},
'aswf-digital-assets-1.1': {'id': 'ASWF-Digital-Assets-1.1', 'deprecated': False},
'baekmuk': {'id': 'Baekmuk', 'deprecated': False},
'bahyph': {'id': 'Bahyph', 'deprecated': False},
'barr': {'id': 'Barr', 'deprecated': False},
'bcrypt-solar-designer': {'id': 'bcrypt-Solar-Designer', 'deprecated': False},
'beerware': {'id': 'Beerware', 'deprecated': False},
'bitstream-charter': {'id': 'Bitstream-Charter', 'deprecated': False},
'bitstream-vera': {'id': 'Bitstream-Vera', 'deprecated': False},
'bittorrent-1.0': {'id': 'BitTorrent-1.0', 'deprecated': False},
'bittorrent-1.1': {'id': 'BitTorrent-1.1', 'deprecated': False},
'blessing': {'id': 'blessing', 'deprecated': False},
'blueoak-1.0.0': {'id': 'BlueOak-1.0.0', 'deprecated': False},
'boehm-gc': {'id': 'Boehm-GC', 'deprecated': False},
'boehm-gc-without-fee': {'id': 'Boehm-GC-without-fee', 'deprecated': False},
'borceux': {'id': 'Borceux', 'deprecated': False},
'brian-gladman-2-clause': {'id': 'Brian-Gladman-2-Clause', 'deprecated': False},
'brian-gladman-3-clause': {'id': 'Brian-Gladman-3-Clause', 'deprecated': False},
'bsd-1-clause': {'id': 'BSD-1-Clause', 'deprecated': False},
'bsd-2-clause': {'id': 'BSD-2-Clause', 'deprecated': False},
'bsd-2-clause-darwin': {'id': 'BSD-2-Clause-Darwin', 'deprecated': False},
'bsd-2-clause-first-lines': {'id': 'BSD-2-Clause-first-lines', 'deprecated': False},
'bsd-2-clause-freebsd': {'id': 'BSD-2-Clause-FreeBSD', 'deprecated': True},
'bsd-2-clause-netbsd': {'id': 'BSD-2-Clause-NetBSD', 'deprecated': True},
'bsd-2-clause-patent': {'id': 'BSD-2-Clause-Patent', 'deprecated': False},
'bsd-2-clause-pkgconf-disclaimer': {'id': 'BSD-2-Clause-pkgconf-disclaimer', 'deprecated': False},
'bsd-2-clause-views': {'id': 'BSD-2-Clause-Views', 'deprecated': False},
'bsd-3-clause': {'id': 'BSD-3-Clause', 'deprecated': False},
'bsd-3-clause-acpica': {'id': 'BSD-3-Clause-acpica', 'deprecated': False},
'bsd-3-clause-attribution': {'id': 'BSD-3-Clause-Attribution', 'deprecated': False},
'bsd-3-clause-clear': {'id': 'BSD-3-Clause-Clear', 'deprecated': False},
'bsd-3-clause-flex': {'id': 'BSD-3-Clause-flex', 'deprecated': False},
'bsd-3-clause-hp': {'id': 'BSD-3-Clause-HP', 'deprecated': False},
'bsd-3-clause-lbnl': {'id': 'BSD-3-Clause-LBNL', 'deprecated': False},
'bsd-3-clause-modification': {'id': 'BSD-3-Clause-Modification', 'deprecated': False},
'bsd-3-clause-no-military-license': {'id': 'BSD-3-Clause-No-Military-License', 'deprecated': False},
'bsd-3-clause-no-nuclear-license': {'id': 'BSD-3-Clause-No-Nuclear-License', 'deprecated': False},
'bsd-3-clause-no-nuclear-license-2014': {'id': 'BSD-3-Clause-No-Nuclear-License-2014', 'deprecated': False},
'bsd-3-clause-no-nuclear-warranty': {'id': 'BSD-3-Clause-No-Nuclear-Warranty', 'deprecated': False},
'bsd-3-clause-open-mpi': {'id': 'BSD-3-Clause-Open-MPI', 'deprecated': False},
'bsd-3-clause-sun': {'id': 'BSD-3-Clause-Sun', 'deprecated': False},
'bsd-4-clause': {'id': 'BSD-4-Clause', 'deprecated': False},
'bsd-4-clause-shortened': {'id': 'BSD-4-Clause-Shortened', 'deprecated': False},
'bsd-4-clause-uc': {'id': 'BSD-4-Clause-UC', 'deprecated': False},
'bsd-4.3reno': {'id': 'BSD-4.3RENO', 'deprecated': False},
'bsd-4.3tahoe': {'id': 'BSD-4.3TAHOE', 'deprecated': False},
'bsd-advertising-acknowledgement': {'id': 'BSD-Advertising-Acknowledgement', 'deprecated': False},
'bsd-attribution-hpnd-disclaimer': {'id': 'BSD-Attribution-HPND-disclaimer', 'deprecated': False},
'bsd-inferno-nettverk': {'id': 'BSD-Inferno-Nettverk', 'deprecated': False},
'bsd-protection': {'id': 'BSD-Protection', 'deprecated': False},
'bsd-source-beginning-file': {'id': 'BSD-Source-beginning-file', 'deprecated': False},
'bsd-source-code': {'id': 'BSD-Source-Code', 'deprecated': False},
'bsd-systemics': {'id': 'BSD-Systemics', 'deprecated': False},
'bsd-systemics-w3works': {'id': 'BSD-Systemics-W3Works', 'deprecated': False},
'bsl-1.0': {'id': 'BSL-1.0', 'deprecated': False},
'busl-1.1': {'id': 'BUSL-1.1', 'deprecated': False},
'bzip2-1.0.5': {'id': 'bzip2-1.0.5', 'deprecated': True},
'bzip2-1.0.6': {'id': 'bzip2-1.0.6', 'deprecated': False},
'c-uda-1.0': {'id': 'C-UDA-1.0', 'deprecated': False},
'cal-1.0': {'id': 'CAL-1.0', 'deprecated': False},
'cal-1.0-combined-work-exception': {'id': 'CAL-1.0-Combined-Work-Exception', 'deprecated': False},
'caldera': {'id': 'Caldera', 'deprecated': False},
'caldera-no-preamble': {'id': 'Caldera-no-preamble', 'deprecated': False},
'catharon': {'id': 'Catharon', 'deprecated': False},
'catosl-1.1': {'id': 'CATOSL-1.1', 'deprecated': False},
'cc-by-1.0': {'id': 'CC-BY-1.0', 'deprecated': False},
'cc-by-2.0': {'id': 'CC-BY-2.0', 'deprecated': False},
'cc-by-2.5': {'id': 'CC-BY-2.5', 'deprecated': False},
'cc-by-2.5-au': {'id': 'CC-BY-2.5-AU', 'deprecated': False},
'cc-by-3.0': {'id': 'CC-BY-3.0', 'deprecated': False},
'cc-by-3.0-at': {'id': 'CC-BY-3.0-AT', 'deprecated': False},
'cc-by-3.0-au': {'id': 'CC-BY-3.0-AU', 'deprecated': False},
'cc-by-3.0-de': {'id': 'CC-BY-3.0-DE', 'deprecated': False},
'cc-by-3.0-igo': {'id': 'CC-BY-3.0-IGO', 'deprecated': False},
'cc-by-3.0-nl': {'id': 'CC-BY-3.0-NL', 'deprecated': False},
'cc-by-3.0-us': {'id': 'CC-BY-3.0-US', 'deprecated': False},
'cc-by-4.0': {'id': 'CC-BY-4.0', 'deprecated': False},
'cc-by-nc-1.0': {'id': 'CC-BY-NC-1.0', 'deprecated': False},
'cc-by-nc-2.0': {'id': 'CC-BY-NC-2.0', 'deprecated': False},
'cc-by-nc-2.5': {'id': 'CC-BY-NC-2.5', 'deprecated': False},
'cc-by-nc-3.0': {'id': 'CC-BY-NC-3.0', 'deprecated': False},
'cc-by-nc-3.0-de': {'id': 'CC-BY-NC-3.0-DE', 'deprecated': False},
'cc-by-nc-4.0': {'id': 'CC-BY-NC-4.0', 'deprecated': False},
'cc-by-nc-nd-1.0': {'id': 'CC-BY-NC-ND-1.0', 'deprecated': False},
'cc-by-nc-nd-2.0': {'id': 'CC-BY-NC-ND-2.0', 'deprecated': False},
'cc-by-nc-nd-2.5': {'id': 'CC-BY-NC-ND-2.5', 'deprecated': False},
'cc-by-nc-nd-3.0': {'id': 'CC-BY-NC-ND-3.0', 'deprecated': False},
'cc-by-nc-nd-3.0-de': {'id': 'CC-BY-NC-ND-3.0-DE', 'deprecated': False},
'cc-by-nc-nd-3.0-igo': {'id': 'CC-BY-NC-ND-3.0-IGO', 'deprecated': False},
'cc-by-nc-nd-4.0': {'id': 'CC-BY-NC-ND-4.0', 'deprecated': False},
'cc-by-nc-sa-1.0': {'id': 'CC-BY-NC-SA-1.0', 'deprecated': False},
'cc-by-nc-sa-2.0': {'id': 'CC-BY-NC-SA-2.0', 'deprecated': False},
'cc-by-nc-sa-2.0-de': {'id': 'CC-BY-NC-SA-2.0-DE', 'deprecated': False},
'cc-by-nc-sa-2.0-fr': {'id': 'CC-BY-NC-SA-2.0-FR', 'deprecated': False},
'cc-by-nc-sa-2.0-uk': {'id': 'CC-BY-NC-SA-2.0-UK', 'deprecated': False},
'cc-by-nc-sa-2.5': {'id': 'CC-BY-NC-SA-2.5', 'deprecated': False},
'cc-by-nc-sa-3.0': {'id': 'CC-BY-NC-SA-3.0', 'deprecated': False},
'cc-by-nc-sa-3.0-de': {'id': 'CC-BY-NC-SA-3.0-DE', 'deprecated': False},
'cc-by-nc-sa-3.0-igo': {'id': 'CC-BY-NC-SA-3.0-IGO', 'deprecated': False},
'cc-by-nc-sa-4.0': {'id': 'CC-BY-NC-SA-4.0', 'deprecated': False},
'cc-by-nd-1.0': {'id': 'CC-BY-ND-1.0', 'deprecated': False},
'cc-by-nd-2.0': {'id': 'CC-BY-ND-2.0', 'deprecated': False},
'cc-by-nd-2.5': {'id': 'CC-BY-ND-2.5', 'deprecated': False},
'cc-by-nd-3.0': {'id': 'CC-BY-ND-3.0', 'deprecated': False},
'cc-by-nd-3.0-de': {'id': 'CC-BY-ND-3.0-DE', 'deprecated': False},
'cc-by-nd-4.0': {'id': 'CC-BY-ND-4.0', 'deprecated': False},
'cc-by-sa-1.0': {'id': 'CC-BY-SA-1.0', 'deprecated': False},
'cc-by-sa-2.0': {'id': 'CC-BY-SA-2.0', 'deprecated': False},
'cc-by-sa-2.0-uk': {'id': 'CC-BY-SA-2.0-UK', 'deprecated': False},
'cc-by-sa-2.1-jp': {'id': 'CC-BY-SA-2.1-JP', 'deprecated': False},
'cc-by-sa-2.5': {'id': 'CC-BY-SA-2.5', 'deprecated': False},
'cc-by-sa-3.0': {'id': 'CC-BY-SA-3.0', 'deprecated': False},
'cc-by-sa-3.0-at': {'id': 'CC-BY-SA-3.0-AT', 'deprecated': False},
'cc-by-sa-3.0-de': {'id': 'CC-BY-SA-3.0-DE', 'deprecated': False},
'cc-by-sa-3.0-igo': {'id': 'CC-BY-SA-3.0-IGO', 'deprecated': False},
'cc-by-sa-4.0': {'id': 'CC-BY-SA-4.0', 'deprecated': False},
'cc-pddc': {'id': 'CC-PDDC', 'deprecated': False},
'cc-pdm-1.0': {'id': 'CC-PDM-1.0', 'deprecated': False},
'cc-sa-1.0': {'id': 'CC-SA-1.0', 'deprecated': False},
'cc0-1.0': {'id': 'CC0-1.0', 'deprecated': False},
'cddl-1.0': {'id': 'CDDL-1.0', 'deprecated': False},
'cddl-1.1': {'id': 'CDDL-1.1', 'deprecated': False},
'cdl-1.0': {'id': 'CDL-1.0', 'deprecated': False},
'cdla-permissive-1.0': {'id': 'CDLA-Permissive-1.0', 'deprecated': False},
'cdla-permissive-2.0': {'id': 'CDLA-Permissive-2.0', 'deprecated': False},
'cdla-sharing-1.0': {'id': 'CDLA-Sharing-1.0', 'deprecated': False},
'cecill-1.0': {'id': 'CECILL-1.0', 'deprecated': False},
'cecill-1.1': {'id': 'CECILL-1.1', 'deprecated': False},
'cecill-2.0': {'id': 'CECILL-2.0', 'deprecated': False},
'cecill-2.1': {'id': 'CECILL-2.1', 'deprecated': False},
'cecill-b': {'id': 'CECILL-B', 'deprecated': False},
'cecill-c': {'id': 'CECILL-C', 'deprecated': False},
'cern-ohl-1.1': {'id': 'CERN-OHL-1.1', 'deprecated': False},
'cern-ohl-1.2': {'id': 'CERN-OHL-1.2', 'deprecated': False},
'cern-ohl-p-2.0': {'id': 'CERN-OHL-P-2.0', 'deprecated': False},
'cern-ohl-s-2.0': {'id': 'CERN-OHL-S-2.0', 'deprecated': False},
'cern-ohl-w-2.0': {'id': 'CERN-OHL-W-2.0', 'deprecated': False},
'cfitsio': {'id': 'CFITSIO', 'deprecated': False},
'check-cvs': {'id': 'check-cvs', 'deprecated': False},
'checkmk': {'id': 'checkmk', 'deprecated': False},
'clartistic': {'id': 'ClArtistic', 'deprecated': False},
'clips': {'id': 'Clips', 'deprecated': False},
'cmu-mach': {'id': 'CMU-Mach', 'deprecated': False},
'cmu-mach-nodoc': {'id': 'CMU-Mach-nodoc', 'deprecated': False},
'cnri-jython': {'id': 'CNRI-Jython', 'deprecated': False},
'cnri-python': {'id': 'CNRI-Python', 'deprecated': False},
'cnri-python-gpl-compatible': {'id': 'CNRI-Python-GPL-Compatible', 'deprecated': False},
'coil-1.0': {'id': 'COIL-1.0', 'deprecated': False},
'community-spec-1.0': {'id': 'Community-Spec-1.0', 'deprecated': False},
'condor-1.1': {'id': 'Condor-1.1', 'deprecated': False},
'copyleft-next-0.3.0': {'id': 'copyleft-next-0.3.0', 'deprecated': False},
'copyleft-next-0.3.1': {'id': 'copyleft-next-0.3.1', 'deprecated': False},
'cornell-lossless-jpeg': {'id': 'Cornell-Lossless-JPEG', 'deprecated': False},
'cpal-1.0': {'id': 'CPAL-1.0', 'deprecated': False},
'cpl-1.0': {'id': 'CPL-1.0', 'deprecated': False},
'cpol-1.02': {'id': 'CPOL-1.02', 'deprecated': False},
'cronyx': {'id': 'Cronyx', 'deprecated': False},
'crossword': {'id': 'Crossword', 'deprecated': False},
'cryptoswift': {'id': 'CryptoSwift', 'deprecated': False},
'crystalstacker': {'id': 'CrystalStacker', 'deprecated': False},
'cua-opl-1.0': {'id': 'CUA-OPL-1.0', 'deprecated': False},
'cube': {'id': 'Cube', 'deprecated': False},
'curl': {'id': 'curl', 'deprecated': False},
'cve-tou': {'id': 'cve-tou', 'deprecated': False},
'd-fsl-1.0': {'id': 'D-FSL-1.0', 'deprecated': False},
'dec-3-clause': {'id': 'DEC-3-Clause', 'deprecated': False},
'diffmark': {'id': 'diffmark', 'deprecated': False},
'dl-de-by-2.0': {'id': 'DL-DE-BY-2.0', 'deprecated': False},
'dl-de-zero-2.0': {'id': 'DL-DE-ZERO-2.0', 'deprecated': False},
'doc': {'id': 'DOC', 'deprecated': False},
'docbook-dtd': {'id': 'DocBook-DTD', 'deprecated': False},
'docbook-schema': {'id': 'DocBook-Schema', 'deprecated': False},
'docbook-stylesheet': {'id': 'DocBook-Stylesheet', 'deprecated': False},
'docbook-xml': {'id': 'DocBook-XML', 'deprecated': False},
'dotseqn': {'id': 'Dotseqn', 'deprecated': False},
'drl-1.0': {'id': 'DRL-1.0', 'deprecated': False},
'drl-1.1': {'id': 'DRL-1.1', 'deprecated': False},
'dsdp': {'id': 'DSDP', 'deprecated': False},
'dtoa': {'id': 'dtoa', 'deprecated': False},
'dvipdfm': {'id': 'dvipdfm', 'deprecated': False},
'ecl-1.0': {'id': 'ECL-1.0', 'deprecated': False},
'ecl-2.0': {'id': 'ECL-2.0', 'deprecated': False},
'ecos-2.0': {'id': 'eCos-2.0', 'deprecated': True},
'efl-1.0': {'id': 'EFL-1.0', 'deprecated': False},
'efl-2.0': {'id': 'EFL-2.0', 'deprecated': False},
'egenix': {'id': 'eGenix', 'deprecated': False},
'elastic-2.0': {'id': 'Elastic-2.0', 'deprecated': False},
'entessa': {'id': 'Entessa', 'deprecated': False},
'epics': {'id': 'EPICS', 'deprecated': False},
'epl-1.0': {'id': 'EPL-1.0', 'deprecated': False},
'epl-2.0': {'id': 'EPL-2.0', 'deprecated': False},
'erlpl-1.1': {'id': 'ErlPL-1.1', 'deprecated': False},
'etalab-2.0': {'id': 'etalab-2.0', 'deprecated': False},
'eudatagrid': {'id': 'EUDatagrid', 'deprecated': False},
'eupl-1.0': {'id': 'EUPL-1.0', 'deprecated': False},
'eupl-1.1': {'id': 'EUPL-1.1', 'deprecated': False},
'eupl-1.2': {'id': 'EUPL-1.2', 'deprecated': False},
'eurosym': {'id': 'Eurosym', 'deprecated': False},
'fair': {'id': 'Fair', 'deprecated': False},
'fbm': {'id': 'FBM', 'deprecated': False},
'fdk-aac': {'id': 'FDK-AAC', 'deprecated': False},
'ferguson-twofish': {'id': 'Ferguson-Twofish', 'deprecated': False},
'frameworx-1.0': {'id': 'Frameworx-1.0', 'deprecated': False},
'freebsd-doc': {'id': 'FreeBSD-DOC', 'deprecated': False},
'freeimage': {'id': 'FreeImage', 'deprecated': False},
'fsfap': {'id': 'FSFAP', 'deprecated': False},
'fsfap-no-warranty-disclaimer': {'id': 'FSFAP-no-warranty-disclaimer', 'deprecated': False},
'fsful': {'id': 'FSFUL', 'deprecated': False},
'fsfullr': {'id': 'FSFULLR', 'deprecated': False},
'fsfullrsd': {'id': 'FSFULLRSD', 'deprecated': False},
'fsfullrwd': {'id': 'FSFULLRWD', 'deprecated': False},
'fsl-1.1-alv2': {'id': 'FSL-1.1-ALv2', 'deprecated': False},
'fsl-1.1-mit': {'id': 'FSL-1.1-MIT', 'deprecated': False},
'ftl': {'id': 'FTL', 'deprecated': False},
'furuseth': {'id': 'Furuseth', 'deprecated': False},
'fwlw': {'id': 'fwlw', 'deprecated': False},
'game-programming-gems': {'id': 'Game-Programming-Gems', 'deprecated': False},
'gcr-docs': {'id': 'GCR-docs', 'deprecated': False},
'gd': {'id': 'GD', 'deprecated': False},
'generic-xts': {'id': 'generic-xts', 'deprecated': False},
'gfdl-1.1': {'id': 'GFDL-1.1', 'deprecated': True},
'gfdl-1.1-invariants-only': {'id': 'GFDL-1.1-invariants-only', 'deprecated': False},
'gfdl-1.1-invariants-or-later': {'id': 'GFDL-1.1-invariants-or-later', 'deprecated': False},
'gfdl-1.1-no-invariants-only': {'id': 'GFDL-1.1-no-invariants-only', 'deprecated': False},
'gfdl-1.1-no-invariants-or-later': {'id': 'GFDL-1.1-no-invariants-or-later', 'deprecated': False},
'gfdl-1.1-only': {'id': 'GFDL-1.1-only', 'deprecated': False},
'gfdl-1.1-or-later': {'id': 'GFDL-1.1-or-later', 'deprecated': False},
'gfdl-1.2': {'id': 'GFDL-1.2', 'deprecated': True},
'gfdl-1.2-invariants-only': {'id': 'GFDL-1.2-invariants-only', 'deprecated': False},
'gfdl-1.2-invariants-or-later': {'id': 'GFDL-1.2-invariants-or-later', 'deprecated': False},
'gfdl-1.2-no-invariants-only': {'id': 'GFDL-1.2-no-invariants-only', 'deprecated': False},
'gfdl-1.2-no-invariants-or-later': {'id': 'GFDL-1.2-no-invariants-or-later', 'deprecated': False},
'gfdl-1.2-only': {'id': 'GFDL-1.2-only', 'deprecated': False},
'gfdl-1.2-or-later': {'id': 'GFDL-1.2-or-later', 'deprecated': False},
'gfdl-1.3': {'id': 'GFDL-1.3', 'deprecated': True},
'gfdl-1.3-invariants-only': {'id': 'GFDL-1.3-invariants-only', 'deprecated': False},
'gfdl-1.3-invariants-or-later': {'id': 'GFDL-1.3-invariants-or-later', 'deprecated': False},
'gfdl-1.3-no-invariants-only': {'id': 'GFDL-1.3-no-invariants-only', 'deprecated': False},
'gfdl-1.3-no-invariants-or-later': {'id': 'GFDL-1.3-no-invariants-or-later', 'deprecated': False},
'gfdl-1.3-only': {'id': 'GFDL-1.3-only', 'deprecated': False},
'gfdl-1.3-or-later': {'id': 'GFDL-1.3-or-later', 'deprecated': False},
'giftware': {'id': 'Giftware', 'deprecated': False},
'gl2ps': {'id': 'GL2PS', 'deprecated': False},
'glide': {'id': 'Glide', 'deprecated': False},
'glulxe': {'id': 'Glulxe', 'deprecated': False},
'glwtpl': {'id': 'GLWTPL', 'deprecated': False},
'gnuplot': {'id': 'gnuplot', 'deprecated': False},
'gpl-1.0': {'id': 'GPL-1.0', 'deprecated': True},
'gpl-1.0+': {'id': 'GPL-1.0+', 'deprecated': True},
'gpl-1.0-only': {'id': 'GPL-1.0-only', 'deprecated': False},
'gpl-1.0-or-later': {'id': 'GPL-1.0-or-later', 'deprecated': False},
'gpl-2.0': {'id': 'GPL-2.0', 'deprecated': True},
'gpl-2.0+': {'id': 'GPL-2.0+', 'deprecated': True},
'gpl-2.0-only': {'id': 'GPL-2.0-only', 'deprecated': False},
'gpl-2.0-or-later': {'id': 'GPL-2.0-or-later', 'deprecated': False},
'gpl-2.0-with-autoconf-exception': {'id': 'GPL-2.0-with-autoconf-exception', 'deprecated': True},
'gpl-2.0-with-bison-exception': {'id': 'GPL-2.0-with-bison-exception', 'deprecated': True},
'gpl-2.0-with-classpath-exception': {'id': 'GPL-2.0-with-classpath-exception', 'deprecated': True},
'gpl-2.0-with-font-exception': {'id': 'GPL-2.0-with-font-exception', 'deprecated': True},
'gpl-2.0-with-gcc-exception': {'id': 'GPL-2.0-with-GCC-exception', 'deprecated': True},
'gpl-3.0': {'id': 'GPL-3.0', 'deprecated': True},
'gpl-3.0+': {'id': 'GPL-3.0+', 'deprecated': True},
'gpl-3.0-only': {'id': 'GPL-3.0-only', 'deprecated': False},
'gpl-3.0-or-later': {'id': 'GPL-3.0-or-later', 'deprecated': False},
'gpl-3.0-with-autoconf-exception': {'id': 'GPL-3.0-with-autoconf-exception', 'deprecated': True},
'gpl-3.0-with-gcc-exception': {'id': 'GPL-3.0-with-GCC-exception', 'deprecated': True},
'graphics-gems': {'id': 'Graphics-Gems', 'deprecated': False},
'gsoap-1.3b': {'id': 'gSOAP-1.3b', 'deprecated': False},
'gtkbook': {'id': 'gtkbook', 'deprecated': False},
'gutmann': {'id': 'Gutmann', 'deprecated': False},
'haskellreport': {'id': 'HaskellReport', 'deprecated': False},
'hdf5': {'id': 'HDF5', 'deprecated': False},
'hdparm': {'id': 'hdparm', 'deprecated': False},
'hidapi': {'id': 'HIDAPI', 'deprecated': False},
'hippocratic-2.1': {'id': 'Hippocratic-2.1', 'deprecated': False},
'hp-1986': {'id': 'HP-1986', 'deprecated': False},
'hp-1989': {'id': 'HP-1989', 'deprecated': False},
'hpnd': {'id': 'HPND', 'deprecated': False},
'hpnd-dec': {'id': 'HPND-DEC', 'deprecated': False},
'hpnd-doc': {'id': 'HPND-doc', 'deprecated': False},
'hpnd-doc-sell': {'id': 'HPND-doc-sell', 'deprecated': False},
'hpnd-export-us': {'id': 'HPND-export-US', 'deprecated': False},
'hpnd-export-us-acknowledgement': {'id': 'HPND-export-US-acknowledgement', 'deprecated': False},
'hpnd-export-us-modify': {'id': 'HPND-export-US-modify', 'deprecated': False},
'hpnd-export2-us': {'id': 'HPND-export2-US', 'deprecated': False},
'hpnd-fenneberg-livingston': {'id': 'HPND-Fenneberg-Livingston', 'deprecated': False},
'hpnd-inria-imag': {'id': 'HPND-INRIA-IMAG', 'deprecated': False},
'hpnd-intel': {'id': 'HPND-Intel', 'deprecated': False},
'hpnd-kevlin-henney': {'id': 'HPND-Kevlin-Henney', 'deprecated': False},
'hpnd-markus-kuhn': {'id': 'HPND-Markus-Kuhn', 'deprecated': False},
'hpnd-merchantability-variant': {'id': 'HPND-merchantability-variant', 'deprecated': False},
'hpnd-mit-disclaimer': {'id': 'HPND-MIT-disclaimer', 'deprecated': False},
'hpnd-netrek': {'id': 'HPND-Netrek', 'deprecated': False},
'hpnd-pbmplus': {'id': 'HPND-Pbmplus', 'deprecated': False},
'hpnd-sell-mit-disclaimer-xserver': {'id': 'HPND-sell-MIT-disclaimer-xserver', 'deprecated': False},
'hpnd-sell-regexpr': {'id': 'HPND-sell-regexpr', 'deprecated': False},
'hpnd-sell-variant': {'id': 'HPND-sell-variant', 'deprecated': False},
'hpnd-sell-variant-mit-disclaimer': {'id': 'HPND-sell-variant-MIT-disclaimer', 'deprecated': False},
'hpnd-sell-variant-mit-disclaimer-rev': {'id': 'HPND-sell-variant-MIT-disclaimer-rev', 'deprecated': False},
'hpnd-uc': {'id': 'HPND-UC', 'deprecated': False},
'hpnd-uc-export-us': {'id': 'HPND-UC-export-US', 'deprecated': False},
'htmltidy': {'id': 'HTMLTIDY', 'deprecated': False},
'ibm-pibs': {'id': 'IBM-pibs', 'deprecated': False},
'icu': {'id': 'ICU', 'deprecated': False},
'iec-code-components-eula': {'id': 'IEC-Code-Components-EULA', 'deprecated': False},
'ijg': {'id': 'IJG', 'deprecated': False},
'ijg-short': {'id': 'IJG-short', 'deprecated': False},
'imagemagick': {'id': 'ImageMagick', 'deprecated': False},
'imatix': {'id': 'iMatix', 'deprecated': False},
'imlib2': {'id': 'Imlib2', 'deprecated': False},
'info-zip': {'id': 'Info-ZIP', 'deprecated': False},
'inner-net-2.0': {'id': 'Inner-Net-2.0', 'deprecated': False},
'innosetup': {'id': 'InnoSetup', 'deprecated': False},
'intel': {'id': 'Intel', 'deprecated': False},
'intel-acpi': {'id': 'Intel-ACPI', 'deprecated': False},
'interbase-1.0': {'id': 'Interbase-1.0', 'deprecated': False},
'ipa': {'id': 'IPA', 'deprecated': False},
'ipl-1.0': {'id': 'IPL-1.0', 'deprecated': False},
'isc': {'id': 'ISC', 'deprecated': False},
'isc-veillard': {'id': 'ISC-Veillard', 'deprecated': False},
'jam': {'id': 'Jam', 'deprecated': False},
'jasper-2.0': {'id': 'JasPer-2.0', 'deprecated': False},
'jove': {'id': 'jove', 'deprecated': False},
'jpl-image': {'id': 'JPL-image', 'deprecated': False},
'jpnic': {'id': 'JPNIC', 'deprecated': False},
'json': {'id': 'JSON', 'deprecated': False},
'kastrup': {'id': 'Kastrup', 'deprecated': False},
'kazlib': {'id': 'Kazlib', 'deprecated': False},
'knuth-ctan': {'id': 'Knuth-CTAN', 'deprecated': False},
'lal-1.2': {'id': 'LAL-1.2', 'deprecated': False},
'lal-1.3': {'id': 'LAL-1.3', 'deprecated': False},
'latex2e': {'id': 'Latex2e', 'deprecated': False},
'latex2e-translated-notice': {'id': 'Latex2e-translated-notice', 'deprecated': False},
'leptonica': {'id': 'Leptonica', 'deprecated': False},
'lgpl-2.0': {'id': 'LGPL-2.0', 'deprecated': True},
'lgpl-2.0+': {'id': 'LGPL-2.0+', 'deprecated': True},
'lgpl-2.0-only': {'id': 'LGPL-2.0-only', 'deprecated': False},
'lgpl-2.0-or-later': {'id': 'LGPL-2.0-or-later', 'deprecated': False},
'lgpl-2.1': {'id': 'LGPL-2.1', 'deprecated': True},
'lgpl-2.1+': {'id': 'LGPL-2.1+', 'deprecated': True},
'lgpl-2.1-only': {'id': 'LGPL-2.1-only', 'deprecated': False},
'lgpl-2.1-or-later': {'id': 'LGPL-2.1-or-later', 'deprecated': False},
'lgpl-3.0': {'id': 'LGPL-3.0', 'deprecated': True},
'lgpl-3.0+': {'id': 'LGPL-3.0+', 'deprecated': True},
'lgpl-3.0-only': {'id': 'LGPL-3.0-only', 'deprecated': False},
'lgpl-3.0-or-later': {'id': 'LGPL-3.0-or-later', 'deprecated': False},
'lgpllr': {'id': 'LGPLLR', 'deprecated': False},
'libpng': {'id': 'Libpng', 'deprecated': False},
'libpng-1.6.35': {'id': 'libpng-1.6.35', 'deprecated': False},
'libpng-2.0': {'id': 'libpng-2.0', 'deprecated': False},
'libselinux-1.0': {'id': 'libselinux-1.0', 'deprecated': False},
'libtiff': {'id': 'libtiff', 'deprecated': False},
'libutil-david-nugent': {'id': 'libutil-David-Nugent', 'deprecated': False},
'liliq-p-1.1': {'id': 'LiLiQ-P-1.1', 'deprecated': False},
'liliq-r-1.1': {'id': 'LiLiQ-R-1.1', 'deprecated': False},
'liliq-rplus-1.1': {'id': 'LiLiQ-Rplus-1.1', 'deprecated': False},
'linux-man-pages-1-para': {'id': 'Linux-man-pages-1-para', 'deprecated': False},
'linux-man-pages-copyleft': {'id': 'Linux-man-pages-copyleft', 'deprecated': False},
'linux-man-pages-copyleft-2-para': {'id': 'Linux-man-pages-copyleft-2-para', 'deprecated': False},
'linux-man-pages-copyleft-var': {'id': 'Linux-man-pages-copyleft-var', 'deprecated': False},
'linux-openib': {'id': 'Linux-OpenIB', 'deprecated': False},
'loop': {'id': 'LOOP', 'deprecated': False},
'lpd-document': {'id': 'LPD-document', 'deprecated': False},
'lpl-1.0': {'id': 'LPL-1.0', 'deprecated': False},
'lpl-1.02': {'id': 'LPL-1.02', 'deprecated': False},
'lppl-1.0': {'id': 'LPPL-1.0', 'deprecated': False},
'lppl-1.1': {'id': 'LPPL-1.1', 'deprecated': False},
'lppl-1.2': {'id': 'LPPL-1.2', 'deprecated': False},
'lppl-1.3a': {'id': 'LPPL-1.3a', 'deprecated': False},
'lppl-1.3c': {'id': 'LPPL-1.3c', 'deprecated': False},
'lsof': {'id': 'lsof', 'deprecated': False},
'lucida-bitmap-fonts': {'id': 'Lucida-Bitmap-Fonts', 'deprecated': False},
'lzma-sdk-9.11-to-9.20': {'id': 'LZMA-SDK-9.11-to-9.20', 'deprecated': False},
'lzma-sdk-9.22': {'id': 'LZMA-SDK-9.22', 'deprecated': False},
'mackerras-3-clause': {'id': 'Mackerras-3-Clause', 'deprecated': False},
'mackerras-3-clause-acknowledgment': {'id': 'Mackerras-3-Clause-acknowledgment', 'deprecated': False},
'magaz': {'id': 'magaz', 'deprecated': False},
'mailprio': {'id': 'mailprio', 'deprecated': False},
'makeindex': {'id': 'MakeIndex', 'deprecated': False},
'man2html': {'id': 'man2html', 'deprecated': False},
'martin-birgmeier': {'id': 'Martin-Birgmeier', 'deprecated': False},
'mcphee-slideshow': {'id': 'McPhee-slideshow', 'deprecated': False},
'metamail': {'id': 'metamail', 'deprecated': False},
'minpack': {'id': 'Minpack', 'deprecated': False},
'mips': {'id': 'MIPS', 'deprecated': False},
'miros': {'id': 'MirOS', 'deprecated': False},
'mit': {'id': 'MIT', 'deprecated': False},
'mit-0': {'id': 'MIT-0', 'deprecated': False},
'mit-advertising': {'id': 'MIT-advertising', 'deprecated': False},
'mit-click': {'id': 'MIT-Click', 'deprecated': False},
'mit-cmu': {'id': 'MIT-CMU', 'deprecated': False},
'mit-enna': {'id': 'MIT-enna', 'deprecated': False},
'mit-feh': {'id': 'MIT-feh', 'deprecated': False},
'mit-festival': {'id': 'MIT-Festival', 'deprecated': False},
'mit-khronos-old': {'id': 'MIT-Khronos-old', 'deprecated': False},
'mit-modern-variant': {'id': 'MIT-Modern-Variant', 'deprecated': False},
'mit-open-group': {'id': 'MIT-open-group', 'deprecated': False},
'mit-testregex': {'id': 'MIT-testregex', 'deprecated': False},
'mit-wu': {'id': 'MIT-Wu', 'deprecated': False},
'mitnfa': {'id': 'MITNFA', 'deprecated': False},
'mmixware': {'id': 'MMIXware', 'deprecated': False},
'motosoto': {'id': 'Motosoto', 'deprecated': False},
'mpeg-ssg': {'id': 'MPEG-SSG', 'deprecated': False},
'mpi-permissive': {'id': 'mpi-permissive', 'deprecated': False},
'mpich2': {'id': 'mpich2', 'deprecated': False},
'mpl-1.0': {'id': 'MPL-1.0', 'deprecated': False},
'mpl-1.1': {'id': 'MPL-1.1', 'deprecated': False},
'mpl-2.0': {'id': 'MPL-2.0', 'deprecated': False},
'mpl-2.0-no-copyleft-exception': {'id': 'MPL-2.0-no-copyleft-exception', 'deprecated': False},
'mplus': {'id': 'mplus', 'deprecated': False},
'ms-lpl': {'id': 'MS-LPL', 'deprecated': False},
'ms-pl': {'id': 'MS-PL', 'deprecated': False},
'ms-rl': {'id': 'MS-RL', 'deprecated': False},
'mtll': {'id': 'MTLL', 'deprecated': False},
'mulanpsl-1.0': {'id': 'MulanPSL-1.0', 'deprecated': False},
'mulanpsl-2.0': {'id': 'MulanPSL-2.0', 'deprecated': False},
'multics': {'id': 'Multics', 'deprecated': False},
'mup': {'id': 'Mup', 'deprecated': False},
'naist-2003': {'id': 'NAIST-2003', 'deprecated': False},
'nasa-1.3': {'id': 'NASA-1.3', 'deprecated': False},
'naumen': {'id': 'Naumen', 'deprecated': False},
'nbpl-1.0': {'id': 'NBPL-1.0', 'deprecated': False},
'ncbi-pd': {'id': 'NCBI-PD', 'deprecated': False},
'ncgl-uk-2.0': {'id': 'NCGL-UK-2.0', 'deprecated': False},
'ncl': {'id': 'NCL', 'deprecated': False},
'ncsa': {'id': 'NCSA', 'deprecated': False},
'net-snmp': {'id': 'Net-SNMP', 'deprecated': True},
'netcdf': {'id': 'NetCDF', 'deprecated': False},
'newsletr': {'id': 'Newsletr', 'deprecated': False},
'ngpl': {'id': 'NGPL', 'deprecated': False},
'ngrep': {'id': 'ngrep', 'deprecated': False},
'nicta-1.0': {'id': 'NICTA-1.0', 'deprecated': False},
'nist-pd': {'id': 'NIST-PD', 'deprecated': False},
'nist-pd-fallback': {'id': 'NIST-PD-fallback', 'deprecated': False},
'nist-software': {'id': 'NIST-Software', 'deprecated': False},
'nlod-1.0': {'id': 'NLOD-1.0', 'deprecated': False},
'nlod-2.0': {'id': 'NLOD-2.0', 'deprecated': False},
'nlpl': {'id': 'NLPL', 'deprecated': False},
'nokia': {'id': 'Nokia', 'deprecated': False},
'nosl': {'id': 'NOSL', 'deprecated': False},
'noweb': {'id': 'Noweb', 'deprecated': False},
'npl-1.0': {'id': 'NPL-1.0', 'deprecated': False},
'npl-1.1': {'id': 'NPL-1.1', 'deprecated': False},
'nposl-3.0': {'id': 'NPOSL-3.0', 'deprecated': False},
'nrl': {'id': 'NRL', 'deprecated': False},
'ntia-pd': {'id': 'NTIA-PD', 'deprecated': False},
'ntp': {'id': 'NTP', 'deprecated': False},
'ntp-0': {'id': 'NTP-0', 'deprecated': False},
'nunit': {'id': 'Nunit', 'deprecated': True},
'o-uda-1.0': {'id': 'O-UDA-1.0', 'deprecated': False},
'oar': {'id': 'OAR', 'deprecated': False},
'occt-pl': {'id': 'OCCT-PL', 'deprecated': False},
'oclc-2.0': {'id': 'OCLC-2.0', 'deprecated': False},
'odbl-1.0': {'id': 'ODbL-1.0', 'deprecated': False},
'odc-by-1.0': {'id': 'ODC-By-1.0', 'deprecated': False},
'offis': {'id': 'OFFIS', 'deprecated': False},
'ofl-1.0': {'id': 'OFL-1.0', 'deprecated': False},
'ofl-1.0-no-rfn': {'id': 'OFL-1.0-no-RFN', 'deprecated': False},
'ofl-1.0-rfn': {'id': 'OFL-1.0-RFN', 'deprecated': False},
'ofl-1.1': {'id': 'OFL-1.1', 'deprecated': False},
'ofl-1.1-no-rfn': {'id': 'OFL-1.1-no-RFN', 'deprecated': False},
'ofl-1.1-rfn': {'id': 'OFL-1.1-RFN', 'deprecated': False},
'ogc-1.0': {'id': 'OGC-1.0', 'deprecated': False},
'ogdl-taiwan-1.0': {'id': 'OGDL-Taiwan-1.0', 'deprecated': False},
'ogl-canada-2.0': {'id': 'OGL-Canada-2.0', 'deprecated': False},
'ogl-uk-1.0': {'id': 'OGL-UK-1.0', 'deprecated': False},
'ogl-uk-2.0': {'id': 'OGL-UK-2.0', 'deprecated': False},
'ogl-uk-3.0': {'id': 'OGL-UK-3.0', 'deprecated': False},
'ogtsl': {'id': 'OGTSL', 'deprecated': False},
'oldap-1.1': {'id': 'OLDAP-1.1', 'deprecated': False},
'oldap-1.2': {'id': 'OLDAP-1.2', 'deprecated': False},
'oldap-1.3': {'id': 'OLDAP-1.3', 'deprecated': False},
'oldap-1.4': {'id': 'OLDAP-1.4', 'deprecated': False},
'oldap-2.0': {'id': 'OLDAP-2.0', 'deprecated': False},
'oldap-2.0.1': {'id': 'OLDAP-2.0.1', 'deprecated': False},
'oldap-2.1': {'id': 'OLDAP-2.1', 'deprecated': False},
'oldap-2.2': {'id': 'OLDAP-2.2', 'deprecated': False},
'oldap-2.2.1': {'id': 'OLDAP-2.2.1', 'deprecated': False},
'oldap-2.2.2': {'id': 'OLDAP-2.2.2', 'deprecated': False},
'oldap-2.3': {'id': 'OLDAP-2.3', 'deprecated': False},
'oldap-2.4': {'id': 'OLDAP-2.4', 'deprecated': False},
'oldap-2.5': {'id': 'OLDAP-2.5', 'deprecated': False},
'oldap-2.6': {'id': 'OLDAP-2.6', 'deprecated': False},
'oldap-2.7': {'id': 'OLDAP-2.7', 'deprecated': False},
'oldap-2.8': {'id': 'OLDAP-2.8', 'deprecated': False},
'olfl-1.3': {'id': 'OLFL-1.3', 'deprecated': False},
'oml': {'id': 'OML', 'deprecated': False},
'openpbs-2.3': {'id': 'OpenPBS-2.3', 'deprecated': False},
'openssl': {'id': 'OpenSSL', 'deprecated': False},
'openssl-standalone': {'id': 'OpenSSL-standalone', 'deprecated': False},
'openvision': {'id': 'OpenVision', 'deprecated': False},
'opl-1.0': {'id': 'OPL-1.0', 'deprecated': False},
'opl-uk-3.0': {'id': 'OPL-UK-3.0', 'deprecated': False},
'opubl-1.0': {'id': 'OPUBL-1.0', 'deprecated': False},
'oset-pl-2.1': {'id': 'OSET-PL-2.1', 'deprecated': False},
'osl-1.0': {'id': 'OSL-1.0', 'deprecated': False},
'osl-1.1': {'id': 'OSL-1.1', 'deprecated': False},
'osl-2.0': {'id': 'OSL-2.0', 'deprecated': False},
'osl-2.1': {'id': 'OSL-2.1', 'deprecated': False},
'osl-3.0': {'id': 'OSL-3.0', 'deprecated': False},
'padl': {'id': 'PADL', 'deprecated': False},
'parity-6.0.0': {'id': 'Parity-6.0.0', 'deprecated': False},
'parity-7.0.0': {'id': 'Parity-7.0.0', 'deprecated': False},
'pddl-1.0': {'id': 'PDDL-1.0', 'deprecated': False},
'php-3.0': {'id': 'PHP-3.0', 'deprecated': False},
'php-3.01': {'id': 'PHP-3.01', 'deprecated': False},
'pixar': {'id': 'Pixar', 'deprecated': False},
'pkgconf': {'id': 'pkgconf', 'deprecated': False},
'plexus': {'id': 'Plexus', 'deprecated': False},
'pnmstitch': {'id': 'pnmstitch', 'deprecated': False},
'polyform-noncommercial-1.0.0': {'id': 'PolyForm-Noncommercial-1.0.0', 'deprecated': False},
'polyform-small-business-1.0.0': {'id': 'PolyForm-Small-Business-1.0.0', 'deprecated': False},
'postgresql': {'id': 'PostgreSQL', 'deprecated': False},
'ppl': {'id': 'PPL', 'deprecated': False},
'psf-2.0': {'id': 'PSF-2.0', 'deprecated': False},
'psfrag': {'id': 'psfrag', 'deprecated': False},
'psutils': {'id': 'psutils', 'deprecated': False},
'python-2.0': {'id': 'Python-2.0', 'deprecated': False},
'python-2.0.1': {'id': 'Python-2.0.1', 'deprecated': False},
'python-ldap': {'id': 'python-ldap', 'deprecated': False},
'qhull': {'id': 'Qhull', 'deprecated': False},
'qpl-1.0': {'id': 'QPL-1.0', 'deprecated': False},
'qpl-1.0-inria-2004': {'id': 'QPL-1.0-INRIA-2004', 'deprecated': False},
'radvd': {'id': 'radvd', 'deprecated': False},
'rdisc': {'id': 'Rdisc', 'deprecated': False},
'rhecos-1.1': {'id': 'RHeCos-1.1', 'deprecated': False},
'rpl-1.1': {'id': 'RPL-1.1', 'deprecated': False},
'rpl-1.5': {'id': 'RPL-1.5', 'deprecated': False},
'rpsl-1.0': {'id': 'RPSL-1.0', 'deprecated': False},
'rsa-md': {'id': 'RSA-MD', 'deprecated': False},
'rscpl': {'id': 'RSCPL', 'deprecated': False},
'ruby': {'id': 'Ruby', 'deprecated': False},
'ruby-pty': {'id': 'Ruby-pty', 'deprecated': False},
'sax-pd': {'id': 'SAX-PD', 'deprecated': False},
'sax-pd-2.0': {'id': 'SAX-PD-2.0', 'deprecated': False},
'saxpath': {'id': 'Saxpath', 'deprecated': False},
'scea': {'id': 'SCEA', 'deprecated': False},
'schemereport': {'id': 'SchemeReport', 'deprecated': False},
'sendmail': {'id': 'Sendmail', 'deprecated': False},
'sendmail-8.23': {'id': 'Sendmail-8.23', 'deprecated': False},
'sendmail-open-source-1.1': {'id': 'Sendmail-Open-Source-1.1', 'deprecated': False},
'sgi-b-1.0': {'id': 'SGI-B-1.0', 'deprecated': False},
'sgi-b-1.1': {'id': 'SGI-B-1.1', 'deprecated': False},
'sgi-b-2.0': {'id': 'SGI-B-2.0', 'deprecated': False},
'sgi-opengl': {'id': 'SGI-OpenGL', 'deprecated': False},
'sgp4': {'id': 'SGP4', 'deprecated': False},
'shl-0.5': {'id': 'SHL-0.5', 'deprecated': False},
'shl-0.51': {'id': 'SHL-0.51', 'deprecated': False},
'simpl-2.0': {'id': 'SimPL-2.0', 'deprecated': False},
'sissl': {'id': 'SISSL', 'deprecated': False},
'sissl-1.2': {'id': 'SISSL-1.2', 'deprecated': False},
'sl': {'id': 'SL', 'deprecated': False},
'sleepycat': {'id': 'Sleepycat', 'deprecated': False},
'smail-gpl': {'id': 'SMAIL-GPL', 'deprecated': False},
'smlnj': {'id': 'SMLNJ', 'deprecated': False},
'smppl': {'id': 'SMPPL', 'deprecated': False},
'snia': {'id': 'SNIA', 'deprecated': False},
'snprintf': {'id': 'snprintf', 'deprecated': False},
'sofa': {'id': 'SOFA', 'deprecated': False},
'softsurfer': {'id': 'softSurfer', 'deprecated': False},
'soundex': {'id': 'Soundex', 'deprecated': False},
'spencer-86': {'id': 'Spencer-86', 'deprecated': False},
'spencer-94': {'id': 'Spencer-94', 'deprecated': False},
'spencer-99': {'id': 'Spencer-99', 'deprecated': False},
'spl-1.0': {'id': 'SPL-1.0', 'deprecated': False},
'ssh-keyscan': {'id': 'ssh-keyscan', 'deprecated': False},
'ssh-openssh': {'id': 'SSH-OpenSSH', 'deprecated': False},
'ssh-short': {'id': 'SSH-short', 'deprecated': False},
'ssleay-standalone': {'id': 'SSLeay-standalone', 'deprecated': False},
'sspl-1.0': {'id': 'SSPL-1.0', 'deprecated': False},
'standardml-nj': {'id': 'StandardML-NJ', 'deprecated': True},
'sugarcrm-1.1.3': {'id': 'SugarCRM-1.1.3', 'deprecated': False},
'sul-1.0': {'id': 'SUL-1.0', 'deprecated': False},
'sun-ppp': {'id': 'Sun-PPP', 'deprecated': False},
'sun-ppp-2000': {'id': 'Sun-PPP-2000', 'deprecated': False},
'sunpro': {'id': 'SunPro', 'deprecated': False},
'swl': {'id': 'SWL', 'deprecated': False},
'swrule': {'id': 'swrule', 'deprecated': False},
'symlinks': {'id': 'Symlinks', 'deprecated': False},
'tapr-ohl-1.0': {'id': 'TAPR-OHL-1.0', 'deprecated': False},
'tcl': {'id': 'TCL', 'deprecated': False},
'tcp-wrappers': {'id': 'TCP-wrappers', 'deprecated': False},
'termreadkey': {'id': 'TermReadKey', 'deprecated': False},
'tgppl-1.0': {'id': 'TGPPL-1.0', 'deprecated': False},
'thirdeye': {'id': 'ThirdEye', 'deprecated': False},
'threeparttable': {'id': 'threeparttable', 'deprecated': False},
'tmate': {'id': 'TMate', 'deprecated': False},
'torque-1.1': {'id': 'TORQUE-1.1', 'deprecated': False},
'tosl': {'id': 'TOSL', 'deprecated': False},
'tpdl': {'id': 'TPDL', 'deprecated': False},
'tpl-1.0': {'id': 'TPL-1.0', 'deprecated': False},
'trustedqsl': {'id': 'TrustedQSL', 'deprecated': False},
'ttwl': {'id': 'TTWL', 'deprecated': False},
'ttyp0': {'id': 'TTYP0', 'deprecated': False},
'tu-berlin-1.0': {'id': 'TU-Berlin-1.0', 'deprecated': False},
'tu-berlin-2.0': {'id': 'TU-Berlin-2.0', 'deprecated': False},
'ubuntu-font-1.0': {'id': 'Ubuntu-font-1.0', 'deprecated': False},
'ucar': {'id': 'UCAR', 'deprecated': False},
'ucl-1.0': {'id': 'UCL-1.0', 'deprecated': False},
'ulem': {'id': 'ulem', 'deprecated': False},
'umich-merit': {'id': 'UMich-Merit', 'deprecated': False},
'unicode-3.0': {'id': 'Unicode-3.0', 'deprecated': False},
'unicode-dfs-2015': {'id': 'Unicode-DFS-2015', 'deprecated': False},
'unicode-dfs-2016': {'id': 'Unicode-DFS-2016', 'deprecated': False},
'unicode-tou': {'id': 'Unicode-TOU', 'deprecated': False},
'unixcrypt': {'id': 'UnixCrypt', 'deprecated': False},
'unlicense': {'id': 'Unlicense', 'deprecated': False},
'unlicense-libtelnet': {'id': 'Unlicense-libtelnet', 'deprecated': False},
'unlicense-libwhirlpool': {'id': 'Unlicense-libwhirlpool', 'deprecated': False},
'upl-1.0': {'id': 'UPL-1.0', 'deprecated': False},
'urt-rle': {'id': 'URT-RLE', 'deprecated': False},
'vim': {'id': 'Vim', 'deprecated': False},
'vostrom': {'id': 'VOSTROM', 'deprecated': False},
'vsl-1.0': {'id': 'VSL-1.0', 'deprecated': False},
'w3c': {'id': 'W3C', 'deprecated': False},
'w3c-19980720': {'id': 'W3C-19980720', 'deprecated': False},
'w3c-20150513': {'id': 'W3C-20150513', 'deprecated': False},
'w3m': {'id': 'w3m', 'deprecated': False},
'watcom-1.0': {'id': 'Watcom-1.0', 'deprecated': False},
'widget-workshop': {'id': 'Widget-Workshop', 'deprecated': False},
'wsuipa': {'id': 'Wsuipa', 'deprecated': False},
'wtfpl': {'id': 'WTFPL', 'deprecated': False},
'wwl': {'id': 'wwl', 'deprecated': False},
'wxwindows': {'id': 'wxWindows', 'deprecated': True},
'x11': {'id': 'X11', 'deprecated': False},
'x11-distribute-modifications-variant': {'id': 'X11-distribute-modifications-variant', 'deprecated': False},
'x11-swapped': {'id': 'X11-swapped', 'deprecated': False},
'xdebug-1.03': {'id': 'Xdebug-1.03', 'deprecated': False},
'xerox': {'id': 'Xerox', 'deprecated': False},
'xfig': {'id': 'Xfig', 'deprecated': False},
'xfree86-1.1': {'id': 'XFree86-1.1', 'deprecated': False},
'xinetd': {'id': 'xinetd', 'deprecated': False},
'xkeyboard-config-zinoviev': {'id': 'xkeyboard-config-Zinoviev', 'deprecated': False},
'xlock': {'id': 'xlock', 'deprecated': False},
'xnet': {'id': 'Xnet', 'deprecated': False},
'xpp': {'id': 'xpp', 'deprecated': False},
'xskat': {'id': 'XSkat', 'deprecated': False},
'xzoom': {'id': 'xzoom', 'deprecated': False},
'ypl-1.0': {'id': 'YPL-1.0', 'deprecated': False},
'ypl-1.1': {'id': 'YPL-1.1', 'deprecated': False},
'zed': {'id': 'Zed', 'deprecated': False},
'zeeff': {'id': 'Zeeff', 'deprecated': False},
'zend-2.0': {'id': 'Zend-2.0', 'deprecated': False},
'zimbra-1.3': {'id': 'Zimbra-1.3', 'deprecated': False},
'zimbra-1.4': {'id': 'Zimbra-1.4', 'deprecated': False},
'zlib': {'id': 'Zlib', 'deprecated': False},
'zlib-acknowledgement': {'id': 'zlib-acknowledgement', 'deprecated': False},
'zpl-1.1': {'id': 'ZPL-1.1', 'deprecated': False},
'zpl-2.0': {'id': 'ZPL-2.0', 'deprecated': False},
'zpl-2.1': {'id': 'ZPL-2.1', 'deprecated': False},
}
EXCEPTIONS: dict[str, SPDXException] = {
'389-exception': {'id': '389-exception', 'deprecated': False},
'asterisk-exception': {'id': 'Asterisk-exception', 'deprecated': False},
'asterisk-linking-protocols-exception': {'id': 'Asterisk-linking-protocols-exception', 'deprecated': False},
'autoconf-exception-2.0': {'id': 'Autoconf-exception-2.0', 'deprecated': False},
'autoconf-exception-3.0': {'id': 'Autoconf-exception-3.0', 'deprecated': False},
'autoconf-exception-generic': {'id': 'Autoconf-exception-generic', 'deprecated': False},
'autoconf-exception-generic-3.0': {'id': 'Autoconf-exception-generic-3.0', 'deprecated': False},
'autoconf-exception-macro': {'id': 'Autoconf-exception-macro', 'deprecated': False},
'bison-exception-1.24': {'id': 'Bison-exception-1.24', 'deprecated': False},
'bison-exception-2.2': {'id': 'Bison-exception-2.2', 'deprecated': False},
'bootloader-exception': {'id': 'Bootloader-exception', 'deprecated': False},
'cgal-linking-exception': {'id': 'CGAL-linking-exception', 'deprecated': False},
'classpath-exception-2.0': {'id': 'Classpath-exception-2.0', 'deprecated': False},
'clisp-exception-2.0': {'id': 'CLISP-exception-2.0', 'deprecated': False},
'cryptsetup-openssl-exception': {'id': 'cryptsetup-OpenSSL-exception', 'deprecated': False},
'digia-qt-lgpl-exception-1.1': {'id': 'Digia-Qt-LGPL-exception-1.1', 'deprecated': False},
'digirule-foss-exception': {'id': 'DigiRule-FOSS-exception', 'deprecated': False},
'ecos-exception-2.0': {'id': 'eCos-exception-2.0', 'deprecated': False},
'erlang-otp-linking-exception': {'id': 'erlang-otp-linking-exception', 'deprecated': False},
'fawkes-runtime-exception': {'id': 'Fawkes-Runtime-exception', 'deprecated': False},
'fltk-exception': {'id': 'FLTK-exception', 'deprecated': False},
'fmt-exception': {'id': 'fmt-exception', 'deprecated': False},
'font-exception-2.0': {'id': 'Font-exception-2.0', 'deprecated': False},
'freertos-exception-2.0': {'id': 'freertos-exception-2.0', 'deprecated': False},
'gcc-exception-2.0': {'id': 'GCC-exception-2.0', 'deprecated': False},
'gcc-exception-2.0-note': {'id': 'GCC-exception-2.0-note', 'deprecated': False},
'gcc-exception-3.1': {'id': 'GCC-exception-3.1', 'deprecated': False},
'gmsh-exception': {'id': 'Gmsh-exception', 'deprecated': False},
'gnat-exception': {'id': 'GNAT-exception', 'deprecated': False},
'gnome-examples-exception': {'id': 'GNOME-examples-exception', 'deprecated': False},
'gnu-compiler-exception': {'id': 'GNU-compiler-exception', 'deprecated': False},
'gnu-javamail-exception': {'id': 'gnu-javamail-exception', 'deprecated': False},
'gpl-3.0-389-ds-base-exception': {'id': 'GPL-3.0-389-ds-base-exception', 'deprecated': False},
'gpl-3.0-interface-exception': {'id': 'GPL-3.0-interface-exception', 'deprecated': False},
'gpl-3.0-linking-exception': {'id': 'GPL-3.0-linking-exception', 'deprecated': False},
'gpl-3.0-linking-source-exception': {'id': 'GPL-3.0-linking-source-exception', 'deprecated': False},
'gpl-cc-1.0': {'id': 'GPL-CC-1.0', 'deprecated': False},
'gstreamer-exception-2005': {'id': 'GStreamer-exception-2005', 'deprecated': False},
'gstreamer-exception-2008': {'id': 'GStreamer-exception-2008', 'deprecated': False},
'harbour-exception': {'id': 'harbour-exception', 'deprecated': False},
'i2p-gpl-java-exception': {'id': 'i2p-gpl-java-exception', 'deprecated': False},
'independent-modules-exception': {'id': 'Independent-modules-exception', 'deprecated': False},
'kicad-libraries-exception': {'id': 'KiCad-libraries-exception', 'deprecated': False},
'lgpl-3.0-linking-exception': {'id': 'LGPL-3.0-linking-exception', 'deprecated': False},
'libpri-openh323-exception': {'id': 'libpri-OpenH323-exception', 'deprecated': False},
'libtool-exception': {'id': 'Libtool-exception', 'deprecated': False},
'linux-syscall-note': {'id': 'Linux-syscall-note', 'deprecated': False},
'llgpl': {'id': 'LLGPL', 'deprecated': False},
'llvm-exception': {'id': 'LLVM-exception', 'deprecated': False},
'lzma-exception': {'id': 'LZMA-exception', 'deprecated': False},
'mif-exception': {'id': 'mif-exception', 'deprecated': False},
'mxml-exception': {'id': 'mxml-exception', 'deprecated': False},
'nokia-qt-exception-1.1': {'id': 'Nokia-Qt-exception-1.1', 'deprecated': True},
'ocaml-lgpl-linking-exception': {'id': 'OCaml-LGPL-linking-exception', 'deprecated': False},
'occt-exception-1.0': {'id': 'OCCT-exception-1.0', 'deprecated': False},
'openjdk-assembly-exception-1.0': {'id': 'OpenJDK-assembly-exception-1.0', 'deprecated': False},
'openvpn-openssl-exception': {'id': 'openvpn-openssl-exception', 'deprecated': False},
'pcre2-exception': {'id': 'PCRE2-exception', 'deprecated': False},
'polyparse-exception': {'id': 'polyparse-exception', 'deprecated': False},
'ps-or-pdf-font-exception-20170817': {'id': 'PS-or-PDF-font-exception-20170817', 'deprecated': False},
'qpl-1.0-inria-2004-exception': {'id': 'QPL-1.0-INRIA-2004-exception', 'deprecated': False},
'qt-gpl-exception-1.0': {'id': 'Qt-GPL-exception-1.0', 'deprecated': False},
'qt-lgpl-exception-1.1': {'id': 'Qt-LGPL-exception-1.1', 'deprecated': False},
'qwt-exception-1.0': {'id': 'Qwt-exception-1.0', 'deprecated': False},
'romic-exception': {'id': 'romic-exception', 'deprecated': False},
'rrdtool-floss-exception-2.0': {'id': 'RRDtool-FLOSS-exception-2.0', 'deprecated': False},
'sane-exception': {'id': 'SANE-exception', 'deprecated': False},
'shl-2.0': {'id': 'SHL-2.0', 'deprecated': False},
'shl-2.1': {'id': 'SHL-2.1', 'deprecated': False},
'stunnel-exception': {'id': 'stunnel-exception', 'deprecated': False},
'swi-exception': {'id': 'SWI-exception', 'deprecated': False},
'swift-exception': {'id': 'Swift-exception', 'deprecated': False},
'texinfo-exception': {'id': 'Texinfo-exception', 'deprecated': False},
'u-boot-exception-2.0': {'id': 'u-boot-exception-2.0', 'deprecated': False},
'ubdl-exception': {'id': 'UBDL-exception', 'deprecated': False},
'universal-foss-exception-1.0': {'id': 'Universal-FOSS-exception-1.0', 'deprecated': False},
'vsftpd-openssl-exception': {'id': 'vsftpd-openssl-exception', 'deprecated': False},
'wxwindows-exception-3.1': {'id': 'WxWindows-exception-3.1', 'deprecated': False},
'x11vnc-openssl-exception': {'id': 'x11vnc-openssl-exception', 'deprecated': False},
}
@@ -0,0 +1,576 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
from __future__ import annotations
import functools
import operator
import os
import platform
import sys
from collections.abc import Set as AbstractSet
from typing import TYPE_CHECKING, Callable, Literal, TypedDict, Union, cast
from ._parser import MarkerAtom, MarkerList, Op, Value, Variable
from ._parser import parse_marker as _parse_marker
from ._tokenizer import ParserSyntaxError
from .specifiers import InvalidSpecifier, Specifier
from .utils import canonicalize_name
if TYPE_CHECKING:
from collections.abc import Mapping
__all__ = [
"Environment",
"EvaluateContext",
"InvalidMarker",
"Marker",
"UndefinedComparison",
"UndefinedEnvironmentName",
"default_environment",
]
def __dir__() -> list[str]:
return __all__
Operator = Callable[[str, Union[str, AbstractSet[str]]], bool]
EvaluateContext = Literal["metadata", "lock_file", "requirement"]
"""A ``typing.Literal`` enumerating valid marker evaluation contexts.
Valid values for the ``context`` passed to :meth:`Marker.evaluate` are:
* ``"metadata"`` (for core metadata; default)
* ``"lock_file"`` (for lock files)
* ``"requirement"`` (i.e. all other situations)
.. versionadded:: 25.0
"""
MARKERS_ALLOWING_SET = {"extras", "dependency_groups"}
MARKERS_REQUIRING_VERSION = {
"implementation_version",
"platform_release",
"python_full_version",
"python_version",
}
class InvalidMarker(ValueError):
"""Raised when attempting to create a :class:`Marker` from invalid input.
This error indicates that the given marker string does not conform to the
:ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.
"""
class UndefinedComparison(ValueError):
"""Raised when evaluating an unsupported marker comparison.
This can happen when marker values are compared as versions but do not
conform to the :ref:`specification of version specifiers
<pypug:version-specifiers>`.
"""
class UndefinedEnvironmentName(KeyError):
"""Raised when evaluating a marker that references a missing environment key.
Subclasses :class:`KeyError` so that code catching the bare ``KeyError`` that
a missing environment lookup historically produced keeps working.
.. versionchanged:: 26.3
Now subclasses :class:`KeyError` (was :class:`ValueError`) and is raised by
:meth:`Marker.evaluate` for missing environment keys, where a bare
``KeyError`` was raised before.
"""
class Environment(TypedDict):
"""
A dictionary that represents a Python environment as captured by
:func:`default_environment`. All fields are required.
"""
implementation_name: str
"""The implementation's identifier, e.g. ``'cpython'``."""
implementation_version: str
"""
The implementation's version, e.g. ``'3.13.0a2'`` for CPython 3.13.0a2, or
``'7.3.13'`` for PyPy3.10 v7.3.13.
"""
os_name: str
"""
The value of :py:data:`os.name`. The name of the operating system dependent module
imported, e.g. ``'posix'``.
"""
platform_machine: str
"""
Returns the machine type, e.g. ``'i386'``.
An empty string if the value cannot be determined.
"""
platform_release: str
"""
The system's release, e.g. ``'2.2.0'`` or ``'NT'``.
An empty string if the value cannot be determined.
"""
platform_system: str
"""
The system/OS name, e.g. ``'Linux'``, ``'Windows'`` or ``'Java'``.
An empty string if the value cannot be determined.
"""
platform_version: str
"""
The system's release version, e.g. ``'#3 on degas'``.
An empty string if the value cannot be determined.
"""
python_full_version: str
"""
The Python version as string ``'major.minor.patchlevel'``.
Note that unlike the Python :py:data:`sys.version`, this value will always include
the patchlevel (it defaults to 0).
"""
platform_python_implementation: str
"""
A string identifying the Python implementation, e.g. ``'CPython'``.
"""
python_version: str
"""The Python version as string ``'major.minor'``."""
sys_platform: str
"""
This string contains a platform identifier that can be used to append
platform-specific components to :py:data:`sys.path`, for instance.
For Unix systems, except on Linux and AIX, this is the lowercased OS name as
returned by ``uname -s`` with the first part of the version as returned by
``uname -r`` appended, e.g. ``'sunos5'`` or ``'freebsd8'``, at the time when Python
was built.
"""
def _normalize_extras(
result: MarkerList | MarkerAtom | str,
) -> MarkerList | MarkerAtom | str:
if isinstance(result, list):
return [_normalize_extras(r) for r in result]
if not isinstance(result, tuple):
return result
lhs, op, rhs = result
if isinstance(lhs, Variable) and lhs.value == "extra" and isinstance(rhs, Value):
normalized_extra = canonicalize_name(rhs.value)
rhs = Value(normalized_extra)
elif isinstance(rhs, Variable) and rhs.value == "extra" and isinstance(lhs, Value):
normalized_extra = canonicalize_name(lhs.value)
lhs = Value(normalized_extra)
elif (
isinstance(rhs, Variable)
and rhs.value in MARKERS_ALLOWING_SET
and isinstance(lhs, Value)
):
# PEP 685 (extras) / PEP 735 (dependency_groups): the set-valued membership
# literal must also be normalized. evaluate() already canonicalizes both
# operands for these keys (see _normalize), so normalizing the literal at
# parse time keeps __str__/__eq__/__hash__ consistent with evaluate() -- e.g.
# Marker('"Foo" in extras') and Marker('"foo" in extras') must compare and
# hash equal (the membership variable is always the right-hand operand).
lhs = Value(canonicalize_name(lhs.value))
return lhs, op, rhs
def _normalize_extra_values(results: MarkerList) -> MarkerList:
"""
Normalize extra values.
"""
return [_normalize_extras(r) for r in results]
def _format_marker(
marker: list[str] | MarkerAtom | str, first: bool | None = True
) -> str:
assert isinstance(marker, (list, tuple, str))
# Unwrap a redundant [[...]] wrapper, but keep the nesting context so a
# nested group keeps the parentheses its and/or precedence needs.
if (
isinstance(marker, list)
and len(marker) == 1
and isinstance(marker[0], (list, tuple))
):
return _format_marker(marker[0], first=first)
if isinstance(marker, list):
inner = (_format_marker(m, first=False) for m in marker)
if first:
return " ".join(inner)
else:
return "(" + " ".join(inner) + ")"
elif isinstance(marker, tuple):
return " ".join([m.serialize() for m in marker])
else:
return marker
_operators: dict[str, Operator] = {
"in": lambda lhs, rhs: lhs in rhs,
"not in": lambda lhs, rhs: lhs not in rhs,
"<": lambda _lhs, _rhs: False,
"<=": operator.eq,
"==": operator.eq,
"!=": operator.ne,
">=": operator.eq,
">": lambda _lhs, _rhs: False,
}
def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str], *, key: str) -> bool:
op_str = op.serialize()
if key in MARKERS_REQUIRING_VERSION:
try:
spec = Specifier(f"{op_str}{rhs}")
except InvalidSpecifier:
pass
else:
return spec.contains(lhs, prereleases=True)
oper: Operator | None = _operators.get(op_str)
if oper is None:
raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.")
return oper(lhs, rhs)
def _normalize(
lhs: str, rhs: str | AbstractSet[str], key: str
) -> tuple[str, str | AbstractSet[str]]:
# PEP 685 - Comparison of extra names for optional distribution dependencies
# https://peps.python.org/pep-0685/
# > When comparing extra names, tools MUST normalize the names being
# > compared using the semantics outlined in PEP 503 for names
if key == "extra":
assert isinstance(rhs, str), "extra value must be a string"
# Both sides are normalized at this point already
return (lhs, rhs)
if key in MARKERS_ALLOWING_SET:
if isinstance(rhs, str): # pragma: no cover
return (canonicalize_name(lhs), canonicalize_name(rhs))
else:
return (canonicalize_name(lhs), {canonicalize_name(v) for v in rhs})
# other environment markers don't have such standards
return lhs, rhs
def _lookup_environment(
environment: dict[str, str | AbstractSet[str]], key: str
) -> str | AbstractSet[str]:
try:
return environment[key]
except KeyError:
raise UndefinedEnvironmentName(key) from None
def _evaluate_markers(
markers: MarkerList, environment: dict[str, str | AbstractSet[str]]
) -> bool:
groups: list[list[bool]] = [[]]
for marker in markers:
if isinstance(marker, list):
groups[-1].append(_evaluate_markers(marker, environment))
elif isinstance(marker, tuple):
lhs, op, rhs = marker
if isinstance(lhs, Variable):
environment_key = lhs.value
lhs_value = _lookup_environment(environment, environment_key)
rhs_value = rhs.value
else:
lhs_value = lhs.value
environment_key = rhs.value
rhs_value = _lookup_environment(environment, environment_key)
if not isinstance(lhs_value, str):
raise UndefinedComparison(
f"Set-valued marker {environment_key!r} can only be used "
f'with the membership form (e.g. "<name>" in '
f"{environment_key}); it cannot appear on the left-hand "
f"side of {op.serialize()!r}."
)
lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key)
groups[-1].append(_eval_op(lhs_value, op, rhs_value, key=environment_key))
elif marker == "or":
groups.append([])
elif marker == "and":
pass
else: # pragma: nocover
raise TypeError(f"Unexpected marker {marker!r}")
return any(all(item) for item in groups)
def _format_full_version(info: sys._version_info) -> str:
version = f"{info.major}.{info.minor}.{info.micro}"
kind = info.releaselevel
if kind != "final":
version += kind[0] + str(info.serial)
return version
@functools.cache
def _cached_default_environment() -> Environment:
"""Build the default marker environment for the current Python process.
The values are derived from process-constant data (the running interpreter
and the host platform), so this is cached and built only once. The result is
shared between callers and must never be mutated; :func:`default_environment`
returns a fresh copy.
"""
iver = _format_full_version(sys.implementation.version)
implementation_name = sys.implementation.name
return {
"implementation_name": implementation_name,
"implementation_version": iver,
"os_name": os.name,
"platform_machine": platform.machine(),
"platform_release": platform.release(),
"platform_system": platform.system(),
"platform_version": platform.version(),
"python_full_version": platform.python_version(),
"platform_python_implementation": platform.python_implementation(),
"python_version": ".".join(platform.python_version_tuple()[:2]),
"sys_platform": sys.platform,
}
def default_environment() -> Environment:
"""Return the default marker environment for the current Python process.
This is the base environment used by :meth:`Marker.evaluate`. A fresh copy
is returned on every call so callers may freely mutate the result; a shallow
copy suffices because all values are immutable strings.
.. versionchanged:: 26.3
The environment is computed once per process and cached, since it is
derived from process-constant data. Patching ``platform``/``sys``/``os``
after the first call has no effect; pass an explicit ``environment`` to
:meth:`Marker.evaluate` to evaluate against different values.
"""
return cast("Environment", dict(_cached_default_environment()))
class Marker:
"""Represents a parsed dependency marker expression.
Marker expressions are parsed according to the
:ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.
:param marker: The string representation of a marker expression.
:raises InvalidMarker: If ``marker`` cannot be parsed.
Instances are safe to serialize with :mod:`pickle`. They use a stable
format so the same pickle can be loaded in future packaging releases.
.. versionchanged:: 26.2
Added a stable pickle format. Pickles created with packaging 26.2+ can
be unpickled with future releases. Backward compatibility with pickles
from packaging < 26.2 is supported but may be removed in a future
release.
"""
__slots__ = ("_markers",)
def __init__(self, marker: str) -> None:
# Note: We create a Marker object without calling this constructor in
# packaging.requirements.Requirement. If any additional logic is
# added here, make sure to mirror/adapt Requirement.
# If this fails and throws an error, the repr still expects _markers to
# be defined.
self._markers: MarkerList = []
try:
self._markers = _normalize_extra_values(_parse_marker(marker))
# The attribute `_markers` can be described in terms of a recursive type:
# MarkerList = List[Union[Tuple[Node, ...], str, MarkerList]]
#
# For example, the following expression:
# python_version > "3.6" or (python_version == "3.6" and os_name == "unix")
#
# is parsed into:
# [
# (<Variable('python_version')>, <Op('>')>, <Value('3.6')>),
# 'and',
# [
# (<Variable('python_version')>, <Op('==')>, <Value('3.6')>),
# 'or',
# (<Variable('os_name')>, <Op('==')>, <Value('unix')>)
# ]
# ]
except ParserSyntaxError as e:
raise InvalidMarker(str(e)) from e
@classmethod
def _from_markers(cls, markers: MarkerList) -> Marker:
"""Create a Marker instance from a pre-parsed marker tree.
This avoids re-parsing serialised marker strings when combining markers.
"""
new = cls.__new__(cls)
new._markers = markers
return new
def __str__(self) -> str:
return _format_marker(self._markers)
def __repr__(self) -> str:
return f"<{self.__class__.__name__}({str(self)!r})>"
def __hash__(self) -> int:
return hash(str(self))
def __eq__(self, other: object) -> bool:
if not isinstance(other, Marker):
return NotImplemented
return str(self) == str(other)
def __getstate__(self) -> str:
# Return the marker expression string for compactness and stability.
# Internal Node objects are excluded; the string is re-parsed on load.
return str(self)
def __setstate__(self, state: object) -> None:
if isinstance(state, str):
# New format (26.2+): just the marker expression string.
try:
self._markers = _normalize_extra_values(_parse_marker(state))
except ParserSyntaxError as exc:
raise TypeError(f"Cannot restore Marker from {state!r}") from exc
return
if isinstance(state, dict) and "_markers" in state:
# Old format (packaging <= 26.1, no __slots__): plain __dict__.
markers = state["_markers"]
if isinstance(markers, list):
self._markers = markers
return
if isinstance(state, tuple) and len(state) == 2:
# Old format (packaging <= 26.1, __slots__): (None, {slot: value}).
_, slot_dict = state
if isinstance(slot_dict, dict) and "_markers" in slot_dict:
markers = slot_dict["_markers"]
if isinstance(markers, list):
self._markers = markers
return
raise TypeError(f"Cannot restore Marker from {state!r}")
def __and__(self, other: Marker) -> Marker:
"""Combine this marker with another using ``and``.
.. versionadded:: 26.1
"""
if not isinstance(other, Marker):
return NotImplemented
return self._from_markers([self._markers, "and", other._markers])
def __or__(self, other: Marker) -> Marker:
"""Combine this marker with another using ``or``.
.. versionadded:: 26.1
"""
if not isinstance(other, Marker):
return NotImplemented
return self._from_markers([self._markers, "or", other._markers])
def evaluate(
self,
environment: Mapping[str, str | AbstractSet[str]] | None = None,
context: EvaluateContext = "metadata",
) -> bool:
"""Evaluate a marker.
Return the boolean from evaluating this marker against the environment.
The environment is determined from the current Python process unless
passed in explicitly.
:param environment: Mapping containing keys and values to override the
detected environment.
:param EvaluateContext context: The context in which the marker is
evaluated, which influences what marker names are considered valid.
Accepted values are ``"metadata"`` (for core metadata; default),
``"lock_file"``, and ``"requirement"`` (i.e. all other situations).
:raises UndefinedComparison: If the marker uses a comparison on values
that are not valid versions per the :ref:`specification of version
specifiers <pypug:version-specifiers>`.
:raises UndefinedEnvironmentName: If the marker references a value that
is missing from the evaluation environment.
:returns: ``True`` if the marker matches, otherwise ``False``.
.. versionchanged:: 25.0
Added the ``context`` parameter, which influences which marker names
are considered valid.
"""
current_environment = cast(
"dict[str, str | AbstractSet[str]]", default_environment()
)
if context == "lock_file":
current_environment |= {
"extras": frozenset(),
"dependency_groups": frozenset(),
}
elif context == "metadata":
current_environment["extra"] = ""
if environment is not None:
current_environment |= environment
if "extra" in current_environment:
# The API used to allow setting extra to None. We need to handle
# this case for backwards compatibility. Also skip running
# normalize name if extra is empty.
extra = cast("str | None", current_environment["extra"])
current_environment["extra"] = canonicalize_name(extra) if extra else ""
return _evaluate_markers(
self._markers, _repair_python_full_version(current_environment)
)
def _pep440_python_full_version(python_full_version: str) -> str:
"""
Work around platform.python_version() returning something that is not PEP 440
compliant for non-tagged Python builds.
"""
if python_full_version.endswith("+"):
return f"{python_full_version}local"
return python_full_version
def _repair_python_full_version(
env: dict[str, str | AbstractSet[str]],
) -> dict[str, str | AbstractSet[str]]:
"""
Work around platform.python_version() returning something that is not PEP 440
compliant for non-tagged Python builds.
"""
python_full_version = cast("str", env["python_full_version"])
env["python_full_version"] = _pep440_python_full_version(python_full_version)
return env
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,943 @@
from __future__ import annotations
import dataclasses
import logging
import re
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from datetime import datetime
from typing import (
TYPE_CHECKING,
Any,
Callable,
Protocol,
TypeVar,
cast,
)
from urllib.parse import unquote, urlparse
from .markers import (
Environment,
Marker,
_pep440_python_full_version,
default_environment,
)
from .specifiers import SpecifierSet
from .tags import create_compatible_tags_selector, sys_tags
from .utils import (
NormalizedName,
is_normalized_name,
parse_sdist_filename,
parse_wheel_filename,
)
from .version import Version
if TYPE_CHECKING: # pragma: no cover
from collections.abc import Collection, Iterator
from pathlib import Path
from typing_extensions import Self
from .tags import Tag
_logger = logging.getLogger(__name__)
__all__ = [
"Package",
"PackageArchive",
"PackageDirectory",
"PackageSdist",
"PackageVcs",
"PackageWheel",
"Pylock",
"PylockSelectError",
"PylockUnsupportedVersionError",
"PylockValidationError",
"is_valid_pylock_path",
]
def __dir__() -> list[str]:
return __all__
_T = TypeVar("_T")
_T2 = TypeVar("_T2")
class _FromMappingProtocol(Protocol): # pragma: no cover
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self: ...
_FromMappingProtocolT = TypeVar("_FromMappingProtocolT", bound=_FromMappingProtocol)
_PYLOCK_FILE_NAME_RE = re.compile(r"^pylock\.([^.]+)\.toml$")
def is_valid_pylock_path(path: Path) -> bool:
"""Check if the given path is a valid pylock file path."""
return path.name == "pylock.toml" or bool(_PYLOCK_FILE_NAME_RE.match(path.name))
def _toml_key(key: str) -> str:
return key.replace("_", "-")
def _toml_value(key: str, value: Any) -> Any: # noqa: ANN401
if isinstance(value, (Version, Marker, SpecifierSet)):
return str(value)
if isinstance(value, Sequence) and key == "environments":
return [str(v) for v in value]
return value
def _toml_dict_factory(data: list[tuple[str, Any]]) -> dict[str, Any]:
return {
_toml_key(key): _toml_value(key, value)
for key, value in data
if value is not None
}
def _get(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T | None:
"""Get a value from the dictionary and verify it's the expected type."""
if (value := d.get(key)) is None:
return None
if not isinstance(value, expected_type) or (
# Special case: bool is a subclass of int, but TOML distinguishes the two
expected_type is int and isinstance(value, bool)
):
raise PylockValidationError(
f"Unexpected type {type(value).__name__} "
f"(expected {expected_type.__name__})",
context=key,
)
return value
def _get_required(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T:
"""Get a required value from the dictionary and verify it's the expected type."""
if (value := _get(d, expected_type, key)) is None:
raise _PylockRequiredKeyError(key)
return value
def _get_sequence(
d: Mapping[str, Any], expected_item_type: type[_T], key: str
) -> Sequence[_T] | None:
"""Get a list value from the dictionary and verify it's the expected items type."""
if (value := _get(d, Sequence, key)) is None: # type: ignore[type-abstract]
return None
if isinstance(value, (str, bytes)):
# special case: str and bytes are Sequences, but we want to reject it
raise PylockValidationError(
f"Unexpected type {type(value).__name__} (expected Sequence)",
context=key,
)
for i, item in enumerate(value):
if not isinstance(item, expected_item_type):
raise PylockValidationError(
f"Unexpected type {type(item).__name__} "
f"(expected {expected_item_type.__name__})",
context=f"{key}[{i}]",
)
return value
def _get_as(
d: Mapping[str, Any],
expected_type: type[_T],
target_type: Callable[[_T], _T2],
key: str,
) -> _T2 | None:
"""Get a value from the dictionary, verify it's the expected type,
and convert to the target type.
This assumes the target_type constructor accepts the value.
"""
if (value := _get(d, expected_type, key)) is None:
return None
try:
return target_type(value)
except Exception as e:
raise PylockValidationError(e, context=key) from e
def _get_required_as(
d: Mapping[str, Any],
expected_type: type[_T],
target_type: Callable[[_T], _T2],
key: str,
) -> _T2:
"""Get a required value from the dict, verify it's the expected type,
and convert to the target type."""
if (value := _get_as(d, expected_type, target_type, key)) is None:
raise _PylockRequiredKeyError(key)
return value
def _get_sequence_as(
d: Mapping[str, Any],
expected_item_type: type[_T],
target_item_type: Callable[[_T], _T2],
key: str,
) -> list[_T2] | None:
"""Get list value from dictionary and verify expected items type."""
if (value := _get_sequence(d, expected_item_type, key)) is None:
return None
result = []
try:
for item in value:
typed_item = target_item_type(item)
result.append(typed_item)
except Exception as e:
raise PylockValidationError(e, context=f"{key}[{len(result)}]") from e
return result
def _get_object(
d: Mapping[str, Any], target_type: type[_FromMappingProtocolT], key: str
) -> _FromMappingProtocolT | None:
"""Get a dictionary value from the dictionary and convert it to a dataclass."""
if (value := _get(d, Mapping, key)) is None: # type: ignore[type-abstract]
return None
try:
return target_type._from_dict(value)
except Exception as e:
raise PylockValidationError(e, context=key) from e
def _get_sequence_of_objects(
d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str
) -> list[_FromMappingProtocolT] | None:
"""Get a list value from the dictionary and convert its items to a dataclass."""
if (value := _get_sequence(d, Mapping, key)) is None: # type: ignore[type-abstract]
return None
result: list[_FromMappingProtocolT] = []
try:
for item in value:
typed_item = target_item_type._from_dict(item)
result.append(typed_item)
except Exception as e:
raise PylockValidationError(e, context=f"{key}[{len(result)}]") from e
return result
def _get_required_sequence_of_objects(
d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str
) -> Sequence[_FromMappingProtocolT]:
"""Get a required list value from the dictionary and convert its items to a
dataclass."""
if (result := _get_sequence_of_objects(d, target_item_type, key)) is None:
raise _PylockRequiredKeyError(key)
return result
def _validate_normalized_name(name: str) -> NormalizedName:
"""Validate that a string is a NormalizedName."""
if not is_normalized_name(name):
raise PylockValidationError(f"Name {name!r} is not normalized")
return NormalizedName(name)
def _validate_path_url(path: str | None, url: str | None) -> None:
if not path and not url:
raise PylockValidationError("path or url must be provided")
def _path_name(path: str | None) -> str | None:
if not path:
return None
# If the path is relative it MAY use POSIX-style path separators explicitly
# for portability
if "/" in path:
return path.rsplit("/", 1)[-1]
elif "\\" in path:
return path.rsplit("\\", 1)[-1]
else:
return path
def _url_name(url: str | None) -> str | None:
if not url:
return None
url_path = urlparse(url).path
# The last path component is percent-encoded, so decode it to the file name
return unquote(url_path.rsplit("/", 1)[-1])
def _validate_hashes(hashes: Mapping[str, Any]) -> Mapping[str, Any]:
if not hashes:
raise PylockValidationError("At least one hash must be provided")
if not all(isinstance(hash_val, str) for hash_val in hashes.values()):
raise PylockValidationError("Hash values must be strings")
return hashes
class PylockValidationError(Exception):
"""Raised when when input data is not spec-compliant."""
context: str | None = None
message: str
def __init__(
self,
cause: str | Exception,
*,
context: str | None = None,
) -> None:
if isinstance(cause, PylockValidationError):
if cause.context:
self.context = (
f"{context}.{cause.context}" if context else cause.context
)
else:
self.context = context
self.message = cause.message
else:
self.context = context
self.message = str(cause)
def __str__(self) -> str:
if self.context:
return f"{self.message} in {self.context!r}"
return self.message
class _PylockRequiredKeyError(PylockValidationError):
def __init__(self, key: str) -> None:
super().__init__("Missing required value", context=key)
class PylockUnsupportedVersionError(PylockValidationError):
"""Raised when encountering an unsupported `lock_version`."""
class PylockSelectError(Exception):
"""Base exception for errors raised by :meth:`Pylock.select`.
.. versionadded:: 26.1
"""
@dataclass(frozen=True, init=False)
class PackageVcs:
type: str
url: str | None = None
path: str | None = None
requested_revision: str | None = None
commit_id: str # type: ignore[misc]
subdirectory: str | None = None
def __init__(
self,
*,
type: str,
url: str | None = None,
path: str | None = None,
requested_revision: str | None = None,
commit_id: str,
subdirectory: str | None = None,
) -> None:
# In Python 3.10+ make dataclass kw_only=True and remove __init__
object.__setattr__(self, "type", type)
object.__setattr__(self, "url", url)
object.__setattr__(self, "path", path)
object.__setattr__(self, "requested_revision", requested_revision)
object.__setattr__(self, "commit_id", commit_id)
object.__setattr__(self, "subdirectory", subdirectory)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
package_vcs = cls(
type=_get_required(d, str, "type"),
url=_get(d, str, "url"),
path=_get(d, str, "path"),
requested_revision=_get(d, str, "requested-revision"),
commit_id=_get_required(d, str, "commit-id"),
subdirectory=_get(d, str, "subdirectory"),
)
_validate_path_url(package_vcs.path, package_vcs.url)
return package_vcs
@dataclass(frozen=True, init=False)
class PackageDirectory:
path: str
editable: bool | None = None
subdirectory: str | None = None
def __init__(
self,
*,
path: str,
editable: bool | None = None,
subdirectory: str | None = None,
) -> None:
# In Python 3.10+ make dataclass kw_only=True and remove __init__
object.__setattr__(self, "path", path)
object.__setattr__(self, "editable", editable)
object.__setattr__(self, "subdirectory", subdirectory)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
return cls(
path=_get_required(d, str, "path"),
editable=_get(d, bool, "editable"),
subdirectory=_get(d, str, "subdirectory"),
)
@dataclass(frozen=True, init=False)
class PackageArchive:
url: str | None = None
path: str | None = None
size: int | None = None
upload_time: datetime | None = None
hashes: Mapping[str, str] # type: ignore[misc]
subdirectory: str | None = None
def __init__(
self,
*,
url: str | None = None,
path: str | None = None,
size: int | None = None,
upload_time: datetime | None = None,
hashes: Mapping[str, str],
subdirectory: str | None = None,
) -> None:
# In Python 3.10+ make dataclass kw_only=True and remove __init__
object.__setattr__(self, "url", url)
object.__setattr__(self, "path", path)
object.__setattr__(self, "size", size)
object.__setattr__(self, "upload_time", upload_time)
object.__setattr__(self, "hashes", hashes)
object.__setattr__(self, "subdirectory", subdirectory)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
package_archive = cls(
url=_get(d, str, "url"),
path=_get(d, str, "path"),
size=_get(d, int, "size"),
upload_time=_get(d, datetime, "upload-time"),
hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
subdirectory=_get(d, str, "subdirectory"),
)
_validate_path_url(package_archive.path, package_archive.url)
return package_archive
@dataclass(frozen=True, init=False)
class PackageSdist:
name: str | None = None
upload_time: datetime | None = None
url: str | None = None
path: str | None = None
size: int | None = None
hashes: Mapping[str, str] # type: ignore[misc]
def __init__(
self,
*,
name: str | None = None,
upload_time: datetime | None = None,
url: str | None = None,
path: str | None = None,
size: int | None = None,
hashes: Mapping[str, str],
) -> None:
# In Python 3.10+ make dataclass kw_only=True and remove __init__
object.__setattr__(self, "name", name)
object.__setattr__(self, "upload_time", upload_time)
object.__setattr__(self, "url", url)
object.__setattr__(self, "path", path)
object.__setattr__(self, "size", size)
object.__setattr__(self, "hashes", hashes)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
package_sdist = cls(
name=_get(d, str, "name"),
upload_time=_get(d, datetime, "upload-time"),
url=_get(d, str, "url"),
path=_get(d, str, "path"),
size=_get(d, int, "size"),
hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
)
_validate_path_url(package_sdist.path, package_sdist.url)
return package_sdist
@property
def filename(self) -> str:
"""Get the filename of the sdist.
.. versionadded:: 26.1
"""
filename = self.name or _path_name(self.path) or _url_name(self.url)
if not filename:
raise PylockValidationError("Cannot determine sdist filename")
return filename
@dataclass(frozen=True, init=False)
class PackageWheel:
name: str | None = None
upload_time: datetime | None = None
url: str | None = None
path: str | None = None
size: int | None = None
hashes: Mapping[str, str] # type: ignore[misc]
def __init__(
self,
*,
name: str | None = None,
upload_time: datetime | None = None,
url: str | None = None,
path: str | None = None,
size: int | None = None,
hashes: Mapping[str, str],
) -> None:
# In Python 3.10+ make dataclass kw_only=True and remove __init__
object.__setattr__(self, "name", name)
object.__setattr__(self, "upload_time", upload_time)
object.__setattr__(self, "url", url)
object.__setattr__(self, "path", path)
object.__setattr__(self, "size", size)
object.__setattr__(self, "hashes", hashes)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
package_wheel = cls(
name=_get(d, str, "name"),
upload_time=_get(d, datetime, "upload-time"),
url=_get(d, str, "url"),
path=_get(d, str, "path"),
size=_get(d, int, "size"),
hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
)
_validate_path_url(package_wheel.path, package_wheel.url)
return package_wheel
@property
def filename(self) -> str:
"""Get the filename of the wheel."""
filename = self.name or _path_name(self.path) or _url_name(self.url)
if not filename:
raise PylockValidationError("Cannot determine wheel filename")
return filename
@dataclass(frozen=True, init=False)
class Package:
name: NormalizedName
version: Version | None = None
marker: Marker | None = None
requires_python: SpecifierSet | None = None
dependencies: Sequence[Mapping[str, Any]] | None = None
vcs: PackageVcs | None = None
directory: PackageDirectory | None = None
archive: PackageArchive | None = None
index: str | None = None
sdist: PackageSdist | None = None
wheels: Sequence[PackageWheel] | None = None
attestation_identities: Sequence[Mapping[str, Any]] | None = None
tool: Mapping[str, Any] | None = None
def __init__(
self,
*,
name: NormalizedName,
version: Version | None = None,
marker: Marker | None = None,
requires_python: SpecifierSet | None = None,
dependencies: Sequence[Mapping[str, Any]] | None = None,
vcs: PackageVcs | None = None,
directory: PackageDirectory | None = None,
archive: PackageArchive | None = None,
index: str | None = None,
sdist: PackageSdist | None = None,
wheels: Sequence[PackageWheel] | None = None,
attestation_identities: Sequence[Mapping[str, Any]] | None = None,
tool: Mapping[str, Any] | None = None,
) -> None:
# In Python 3.10+ make dataclass kw_only=True and remove __init__
object.__setattr__(self, "name", name)
object.__setattr__(self, "version", version)
object.__setattr__(self, "marker", marker)
object.__setattr__(self, "requires_python", requires_python)
object.__setattr__(self, "dependencies", dependencies)
object.__setattr__(self, "vcs", vcs)
object.__setattr__(self, "directory", directory)
object.__setattr__(self, "archive", archive)
object.__setattr__(self, "index", index)
object.__setattr__(self, "sdist", sdist)
object.__setattr__(self, "wheels", wheels)
object.__setattr__(self, "attestation_identities", attestation_identities)
object.__setattr__(self, "tool", tool)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
package = cls(
name=_get_required_as(d, str, _validate_normalized_name, "name"),
version=_get_as(d, str, Version, "version"),
requires_python=_get_as(d, str, SpecifierSet, "requires-python"),
dependencies=_get_sequence(d, Mapping, "dependencies"), # type: ignore[type-abstract]
marker=_get_as(d, str, Marker, "marker"),
vcs=_get_object(d, PackageVcs, "vcs"),
directory=_get_object(d, PackageDirectory, "directory"),
archive=_get_object(d, PackageArchive, "archive"),
index=_get(d, str, "index"),
sdist=_get_object(d, PackageSdist, "sdist"),
wheels=_get_sequence_of_objects(d, PackageWheel, "wheels"),
attestation_identities=_get_sequence(d, Mapping, "attestation-identities"), # type: ignore[type-abstract]
tool=_get(d, Mapping, "tool"), # type: ignore[type-abstract]
)
distributions = bool(package.sdist) + len(package.wheels or [])
direct_urls = (
bool(package.vcs) + bool(package.directory) + bool(package.archive)
)
if distributions > 0 and direct_urls > 0:
raise PylockValidationError(
"None of vcs, directory, archive must be set if sdist or wheels are set"
)
if distributions == 0 and direct_urls != 1:
raise PylockValidationError(
"Exactly one of vcs, directory, archive must be set "
"if sdist and wheels are not set"
)
for i, wheel in enumerate(package.wheels or []):
try:
(name, version, _, _) = parse_wheel_filename(wheel.filename)
except Exception as e:
raise PylockValidationError(
f"Invalid wheel filename {wheel.filename!r}",
context=f"wheels[{i}]",
) from e
if name != package.name:
raise PylockValidationError(
f"Name in {wheel.filename!r} is not consistent with "
f"package name {package.name!r}",
context=f"wheels[{i}]",
)
if package.version and version != package.version:
raise PylockValidationError(
f"Version in {wheel.filename!r} is not consistent with "
f"package version {str(package.version)!r}",
context=f"wheels[{i}]",
)
if package.sdist:
try:
name, version = parse_sdist_filename(package.sdist.filename)
except Exception as e:
raise PylockValidationError(
f"Invalid sdist filename {package.sdist.filename!r}",
context="sdist",
) from e
if name != package.name:
raise PylockValidationError(
f"Name in {package.sdist.filename!r} is not consistent with "
f"package name {package.name!r}",
context="sdist",
)
if package.version and version != package.version:
raise PylockValidationError(
f"Version in {package.sdist.filename!r} is not consistent with "
f"package version {str(package.version)!r}",
context="sdist",
)
try:
for i, attestation_identity in enumerate( # noqa: B007
package.attestation_identities or []
):
_get_required(attestation_identity, str, "kind")
except Exception as e:
raise PylockValidationError(
e, context=f"attestation-identities[{i}]"
) from e
return package
@property
def is_direct(self) -> bool:
return not (self.sdist or self.wheels)
@dataclass(frozen=True, init=False)
class Pylock:
"""A class representing a pylock file."""
lock_version: Version
environments: Sequence[Marker] | None = None
requires_python: SpecifierSet | None = None
extras: Sequence[NormalizedName] | None = None
dependency_groups: Sequence[str] | None = None
default_groups: Sequence[str] | None = None
created_by: str # type: ignore[misc]
packages: Sequence[Package] # type: ignore[misc]
tool: Mapping[str, Any] | None = None
def __init__(
self,
*,
lock_version: Version,
environments: Sequence[Marker] | None = None,
requires_python: SpecifierSet | None = None,
extras: Sequence[NormalizedName] | None = None,
dependency_groups: Sequence[str] | None = None,
default_groups: Sequence[str] | None = None,
created_by: str,
packages: Sequence[Package],
tool: Mapping[str, Any] | None = None,
) -> None:
# In Python 3.10+ make dataclass kw_only=True and remove __init__
object.__setattr__(self, "lock_version", lock_version)
object.__setattr__(self, "environments", environments)
object.__setattr__(self, "requires_python", requires_python)
object.__setattr__(self, "extras", extras)
object.__setattr__(self, "dependency_groups", dependency_groups)
object.__setattr__(self, "default_groups", default_groups)
object.__setattr__(self, "created_by", created_by)
object.__setattr__(self, "packages", packages)
object.__setattr__(self, "tool", tool)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
pylock = cls(
lock_version=_get_required_as(d, str, Version, "lock-version"),
environments=_get_sequence_as(d, str, Marker, "environments"),
extras=_get_sequence_as(d, str, _validate_normalized_name, "extras"),
dependency_groups=_get_sequence(d, str, "dependency-groups"),
default_groups=_get_sequence(d, str, "default-groups"),
created_by=_get_required(d, str, "created-by"),
requires_python=_get_as(d, str, SpecifierSet, "requires-python"),
packages=_get_required_sequence_of_objects(d, Package, "packages"),
tool=_get(d, Mapping, "tool"), # type: ignore[type-abstract]
)
if not Version("1") <= pylock.lock_version < Version("2"):
raise PylockUnsupportedVersionError(
f"pylock version {pylock.lock_version} is not supported"
)
if pylock.lock_version > Version("1.0"):
_logger.warning(
"pylock minor version %s is not supported", pylock.lock_version
)
return pylock
@classmethod
def from_dict(cls, d: Mapping[str, Any], /) -> Self:
"""Create and validate a Pylock instance from a TOML dictionary.
Raises :class:`PylockValidationError` if the input data is not
spec-compliant.
"""
return cls._from_dict(d)
def to_dict(self) -> Mapping[str, Any]:
"""Convert the Pylock instance to a TOML dictionary."""
return dataclasses.asdict(self, dict_factory=_toml_dict_factory)
def validate(self) -> None:
"""Validate the Pylock instance against the specification.
Raises :class:`PylockValidationError` otherwise."""
self.from_dict(self.to_dict())
def select(
self,
*,
environment: Environment | None = None,
tags: Sequence[Tag] | None = None,
extras: Collection[str] | None = None,
dependency_groups: Collection[str] | None = None,
prefer_sdist_predicate: Callable[[NormalizedName], bool] | None = None,
) -> Iterator[
tuple[
Package,
PackageVcs
| PackageDirectory
| PackageArchive
| PackageWheel
| PackageSdist,
]
]:
"""Select what to install from the lock file.
The *environment* and *tags* parameters represent the environment being
selected for. If unspecified, ``packaging.markers.default_environment()`` and
``packaging.tags.sys_tags()`` are used.
The *extras* parameter represents the extras to install.
The *dependency_groups* parameter represents the groups to install. If
unspecified, the default groups are used.
The *prefer_sdist_predicate* parameter is called for packages with a source
distribution. If it returns ``True``, the source distribution is selected
before attempting wheel compatibility. If no source distribution is
available, wheel selection proceeds as usual without calling the predicate.
This method must be used on valid Pylock instances (i.e. one obtained
from :meth:`Pylock.from_dict` or if constructed manually, after calling
:meth:`Pylock.validate`).
.. versionadded:: 26.1
.. versionchanged:: 26.3
Added the *prefer_sdist_predicate* parameter.
"""
compatible_tags_selector = create_compatible_tags_selector(
tags if tags is not None else sys_tags()
)
# #. Gather the extras and dependency groups to install and set ``extras`` and
# ``dependency_groups`` for marker evaluation, respectively.
#
# #. ``extras`` SHOULD be set to the empty set by default.
# #. ``dependency_groups`` SHOULD be the set created from
# :ref:`pylock-default-groups` by default.
env = cast(
"dict[str, str | frozenset[str]]",
dict(
environment or {}, # Marker.evaluate will fill-up
extras=frozenset(extras or []),
dependency_groups=frozenset(
(self.default_groups or [])
if dependency_groups is None # to allow selecting no group
else dependency_groups
),
),
)
env_python_full_version = _pep440_python_full_version(
environment["python_full_version"]
if environment
else default_environment()["python_full_version"]
)
# #. Check if the metadata version specified by :ref:`pylock-lock-version` is
# supported; an error or warning MUST be raised as appropriate.
# Covered by lock.validate() which is a precondition for this method.
# #. If :ref:`pylock-requires-python` is specified, check that the environment
# being installed for meets the requirement; an error MUST be raised if it is
# not met.
if self.requires_python and not self.requires_python.contains(
env_python_full_version,
):
raise PylockSelectError(
f"python_full_version {env_python_full_version!r} "
f"in provided environment does not satisfy the Python version "
f"requirement {str(self.requires_python)!r}"
)
# #. If :ref:`pylock-environments` is specified, check that at least one of the
# environment marker expressions is satisfied; an error MUST be raised if no
# expression is satisfied.
if self.environments:
for env_marker in self.environments:
if env_marker.evaluate(
cast("dict[str, str]", environment or {}), context="requirement"
):
break
else:
raise PylockSelectError(
"Provided environment does not satisfy any of the "
"environments specified in the lock file"
)
# #. For each package listed in :ref:`pylock-packages`:
selected_packages_by_name: dict[str, tuple[int, Package]] = {}
for package_index, package in enumerate(self.packages):
# #. If :ref:`pylock-packages-marker` is specified, check if it is
# satisfied;if it isn't, skip to the next package.
if package.marker and not package.marker.evaluate(env, context="lock_file"):
continue
# #. If :ref:`pylock-packages-requires-python` is specified, check if it is
# satisfied; an error MUST be raised if it isn't.
if package.requires_python and not package.requires_python.contains(
env_python_full_version,
):
raise PylockSelectError(
f"python_full_version {env_python_full_version!r} "
f"in provided environment does not satisfy the Python version "
f"requirement {str(package.requires_python)!r} for package "
f"{package.name!r} at packages[{package_index}]"
)
# #. Check that no other conflicting instance of the package has been slated
# to be installed; an error about the ambiguity MUST be raised otherwise.
if package.name in selected_packages_by_name:
raise PylockSelectError(
f"Multiple packages with the name {package.name!r} are "
f"selected at packages[{package_index}] and "
f"packages[{selected_packages_by_name[package.name][0]}]"
)
# #. Check that the source of the package is specified appropriately (i.e.
# there are no conflicting sources in the package entry);
# an error MUST be raised if any issues are found.
# Covered by lock.validate() which is a precondition for this method.
# #. Add the package to the set of packages to install.
selected_packages_by_name[package.name] = (package_index, package)
# #. For each package to be installed:
for package_index, package in selected_packages_by_name.values():
# - If :ref:`pylock-packages-vcs` is set:
if package.vcs is not None:
yield package, package.vcs
# - Else if :ref:`pylock-packages-directory` is set:
elif package.directory is not None:
yield package, package.directory
# - Else if :ref:`pylock-packages-archive` is set:
elif package.archive is not None:
yield package, package.archive
# - Else if source preference selects an available
# :ref:`pylock-packages-sdist`:
elif (
package.sdist is not None
and prefer_sdist_predicate is not None
and prefer_sdist_predicate(package.name)
):
yield package, package.sdist
# - Else if there are entries for :ref:`pylock-packages-wheels`:
elif package.wheels:
# #. Look for the appropriate wheel file based on
# :ref:`pylock-packages-wheels-name`; if one is not found then move
# on to :ref:`pylock-packages-sdist` or an error MUST be raised about
# a lack of source for the project.
best_wheel = next(
compatible_tags_selector(
(wheel, parse_wheel_filename(wheel.filename)[-1])
for wheel in package.wheels
),
None,
)
if best_wheel:
yield package, best_wheel
elif package.sdist is not None:
yield package, package.sdist
else:
raise PylockSelectError(
f"No wheel found matching the provided tags "
f"for package {package.name!r} "
f"at packages[{package_index}], "
f"and no sdist available as a fallback"
)
# - Else if no :ref:`pylock-packages-wheels` file is found or
# :ref:`pylock-packages-sdist` is solely set:
elif package.sdist is not None:
yield package, package.sdist
else:
# Covered by lock.validate() which is a precondition for this method.
raise NotImplementedError # pragma: no cover
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,192 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
from __future__ import annotations
from typing import TYPE_CHECKING
from ._parser import parse_requirement as _parse_requirement
from ._tokenizer import ParserSyntaxError
from .markers import Marker, _normalize_extra_values
from .specifiers import InvalidSpecifier, SpecifierSet
from .utils import canonicalize_name
if TYPE_CHECKING:
from collections.abc import Iterator
__all__ = [
"InvalidRequirement",
"Requirement",
]
def __dir__() -> list[str]:
return __all__
class InvalidRequirement(ValueError):
"""
An invalid requirement was found, users should refer to PEP 508.
.. versionadded:: 16.1
"""
class Requirement:
"""Parse a requirement.
Parse a given requirement string into its parts, such as name, specifier,
URL, and extras. Raises InvalidRequirement on a badly-formed requirement
string.
.. versionadded:: 16.1
.. versionchanged:: 22.0
Added equality (``__eq__``) and hashing (``__hash__``) so requirements
can be compared and stored in sets / dicts.
.. versionchanged:: 23.2
Equality and hashing began canonicalizing requirement names, so
requirements whose names differ only by normalization (e.g.
``Requirement("Foo")`` vs ``Requirement("foo")``) now compare and hash
equal.
Instances are safe to serialize with :mod:`pickle`. They use a stable
format so the same pickle can be loaded in future packaging releases.
.. versionchanged:: 26.2
Added a stable pickle format. Pickles created with packaging 26.2+ can
be unpickled with future releases. Backward compatibility with pickles
from packaging < 26.2 is supported but may be removed in a future
release.
.. versionchanged:: 26.3
The dedicated pickle support introduced in 26.2 did not preserve the
specifier's explicit :attr:`~packaging.specifiers.SpecifierSet.prereleases`
override; it is now included again.
Equality and hashing normalize requirement names, extras, and
equivalent specifiers. The string representation still preserves the
parsed name and extras spelling.
"""
# TODO: Can we test whether something is contained within a requirement?
# If so how do we do that? Do we need to test against the _name_ of
# the thing as well as the version? What about the markers?
# TODO: Can we normalize the name and extra name?
__slots__ = ("extras", "marker", "name", "specifier", "url")
def __init__(self, requirement_string: str) -> None:
try:
parsed = _parse_requirement(requirement_string)
except ParserSyntaxError as e:
raise InvalidRequirement(str(e)) from e
self.name: str = parsed.name
self.url: str | None = parsed.url or None
self.extras: set[str] = set(parsed.extras)
try:
self.specifier: SpecifierSet = SpecifierSet(parsed.specifier)
except InvalidSpecifier as e:
raise InvalidRequirement(str(e)) from e
self.marker: Marker | None = None
if parsed.marker is not None:
self.marker = Marker.__new__(Marker)
self.marker._markers = _normalize_extra_values(parsed.marker)
def _iter_parts(self, name: str) -> Iterator[str]:
yield name
if self.extras:
formatted_extras = ",".join(sorted(self.extras))
yield f"[{formatted_extras}]"
if self.specifier:
yield str(self.specifier)
if self.url:
yield f" @ {self.url}"
if self.marker:
yield " "
if self.marker:
yield f"; {self.marker}"
def __getstate__(self) -> tuple[str, bool | None]:
# Return the requirement string for compactness and stability, paired
# with the specifier's explicit prereleases override, which is not
# captured by the string form. Re-parsed on load to reconstruct all
# other fields.
return (str(self), self.specifier._prereleases)
def __setstate__(self, state: object) -> None:
if isinstance(state, str):
# Format (26.2): just the requirement string.
requirement_string: str = state
prereleases: bool | None = None
elif (
isinstance(state, tuple)
and len(state) == 2
and isinstance(state[0], str)
and (state[1] is None or isinstance(state[1], bool))
):
# New format (26.3+): (requirement string, specifier prereleases).
requirement_string, prereleases = state
elif isinstance(state, dict) and state.keys() >= set(self.__slots__):
# Old format (packaging <= 26.1, no __slots__): plain __dict__.
for key in self.__slots__:
setattr(self, key, state[key])
return
else:
raise TypeError(f"Cannot restore Requirement from {state!r}")
try:
tmp = Requirement(requirement_string)
except InvalidRequirement as exc:
raise TypeError(f"Cannot restore Requirement from {state!r}") from exc
self.name = tmp.name
self.url = tmp.url
self.extras = tmp.extras
self.specifier = tmp.specifier
self.specifier._prereleases = prereleases
self.marker = tmp.marker
def __str__(self) -> str:
return "".join(self._iter_parts(self.name))
def __repr__(self) -> str:
return f"<{self.__class__.__name__}({str(self)!r})>"
def __hash__(self) -> int:
# Mirror __eq__ by hashing the canonical specifier object rather than
# its raw string. ``_iter_parts`` yields ``str(self.specifier)``, which
# is non-canonical, so trailing-zero-equivalent requirements such as
# ``foo==1.0.0`` and ``foo==1.0.0.0`` (which compare equal) would
# otherwise hash differently, breaking the hash/__eq__ invariant.
return hash(
(
canonicalize_name(self.name),
frozenset(canonicalize_name(e) for e in self.extras),
self.specifier,
self.url,
self.marker,
)
)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Requirement):
return NotImplemented
# Extras must be normalized before comparison as per PEP 685.
self_extras = frozenset(canonicalize_name(e) for e in self.extras)
other_extras = frozenset(canonicalize_name(e) for e in other.extras)
return (
canonicalize_name(self.name) == canonicalize_name(other.name)
and self_extras == other_extras
and self.specifier == other.specifier
and self.url == other.url
and self.marker == other.marker
)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,370 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
from __future__ import annotations
import re
from typing import NewType, Union, cast
from .tags import InvalidTag, Tag, UnsortedTagsError, parse_tag
from .version import InvalidVersion, Version, _TrimmedRelease
__all__ = [
"BuildTag",
"InvalidName",
"InvalidSdistFilename",
"InvalidWheelFilename",
"NormalizedName",
"canonicalize_name",
"canonicalize_version",
"is_normalized_name",
"parse_sdist_filename",
"parse_wheel_filename",
]
def __dir__() -> list[str]:
return __all__
BuildTag = Union[tuple[()], tuple[int, str]]
"""
A wheel build tag: an empty tuple, or a ``(build number, build tag suffix)`` pair.
.. versionadded:: 20.9
"""
NormalizedName = NewType("NormalizedName", str)
"""
A :class:`typing.NewType` of :class:`str`, representing a normalized name.
.. versionadded:: 20.4
"""
class InvalidName(ValueError):
"""
An invalid distribution name; users should refer to the packaging user guide.
.. versionadded:: 23.2
"""
class InvalidWheelFilename(ValueError):
"""
An invalid wheel filename was found, users should refer to PEP 427.
.. versionadded:: 20.9
"""
class InvalidSdistFilename(ValueError):
"""
An invalid sdist filename was found, users should refer to the packaging user guide.
.. versionadded:: 20.9
"""
# Core metadata spec for `Name`
_validate_regex = re.compile(
r"[a-z0-9]|[a-z0-9][a-z0-9._-]*[a-z0-9]", re.IGNORECASE | re.ASCII
)
_normalized_regex = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*", re.ASCII)
# PEP 427: The build number must start with a digit.
_build_tag_regex = re.compile(r"(\d+)(.*)", re.ASCII)
# PEP 427: Valid characters for an escaped project name in a wheel filename.
# Requires at least one character so an empty project name is rejected.
_wheel_name_regex = re.compile(r"^[\w._]+\Z", re.UNICODE)
def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName:
"""
This function takes a valid Python package or extra name, and returns the
normalized form of it.
The return type is typed as :class:`NormalizedName`. This allows type
checkers to help require that a string has passed through this function
before use.
If **validate** is true, then the function will check if **name** is a valid
distribution name before normalizing.
:param str name: The name to normalize.
:param bool validate: Check whether the name is a valid distribution name.
:raises InvalidName: If **validate** is true and the name is not an
acceptable distribution name.
>>> from packaging.utils import canonicalize_name
>>> canonicalize_name("Django")
'django'
>>> canonicalize_name("oslo.concurrency")
'oslo-concurrency'
>>> canonicalize_name("requests")
'requests'
.. versionadded:: 16.2
.. versionchanged:: 20.4
The return type was changed to :class:`NormalizedName`.
.. versionchanged:: 23.2
Added the *validate* keyword parameter.
"""
if validate and not _validate_regex.fullmatch(name):
raise InvalidName(f"name is invalid: {name!r}")
# Ensure all ``.`` and ``_`` are ``-``
# Emulates ``re.sub(r"[-_.]+", "-", name).lower()`` from PEP 503
# Much faster than re, and even faster than str.translate
value = name.lower().replace("_", "-").replace(".", "-")
# Condense repeats (faster than regex)
while "--" in value:
value = value.replace("--", "-")
return cast("NormalizedName", value)
def is_normalized_name(name: str) -> bool:
"""
Check if a name is a normalized project name (i.e. a valid name that
:func:`canonicalize_name` would roundtrip to the same value).
The roundtrip only characterizes normalized names for *valid* names. A name
must start and end with an ASCII letter or digit, which
:func:`canonicalize_name` does not enforce: it leaves a leading or trailing
hyphen in place, so such a name roundtrips without being normalized.
:param str name: The name to check.
>>> from packaging.utils import canonicalize_name, is_normalized_name
>>> is_normalized_name("requests")
True
>>> is_normalized_name("Django")
False
>>> canonicalize_name("_not_legal")
'-not-legal'
>>> is_normalized_name("-not-legal") # roundtrips, but not a valid name
False
.. versionadded:: 23.2
"""
return _normalized_regex.fullmatch(name) is not None
def canonicalize_version(
version: Version | str, *, strip_trailing_zero: bool = True
) -> str:
"""Return a canonical form of a version as a string.
This function takes a string representing a package version (or a
:class:`~packaging.version.Version` instance), and returns the
normalized form of it. By default, it strips trailing zeros from
the release segment.
>>> from packaging.utils import canonicalize_version
>>> canonicalize_version('1.0.1')
'1.0.1'
Per PEP 625, versions may have multiple canonical forms, differing
only by trailing zeros.
>>> canonicalize_version('1.0.0')
'1'
>>> canonicalize_version('1.0.0', strip_trailing_zero=False)
'1.0.0'
Invalid versions are returned unaltered.
>>> canonicalize_version('foo bar baz')
'foo bar baz'
>>> canonicalize_version('1.4.0.0.0')
'1.4'
.. versionadded:: 17.1
.. versionchanged:: 21.0
The return type was narrowed to :class:`str`.
.. versionchanged:: 22.0
Added the *strip_trailing_zero* keyword parameter.
"""
if isinstance(version, str):
try:
version = Version(version)
except InvalidVersion:
return str(version)
return str(_TrimmedRelease(version) if strip_trailing_zero else version)
def parse_wheel_filename(
filename: str,
*,
validate_order: bool = False,
) -> tuple[NormalizedName, Version, BuildTag, frozenset[Tag]]:
"""
This function takes the filename of a wheel file, and parses it,
returning a tuple of name, version, build number, and tags.
The name part of the tuple is normalized and typed as
:class:`NormalizedName`. The version portion is an instance of
:class:`~packaging.version.Version`. The build number is ``()`` if
there is no build number in the wheel filename, otherwise a
two-item tuple of an integer for the leading digits and
a string for the rest of the build number. The tags portion is a
frozen set of :class:`~packaging.tags.Tag` instances (as the tag
string format allows multiple tags to be combined into a single
string).
If **validate_order** is true, compressed tag set components are
checked to be in sorted order as required by PEP 425.
:param str filename: The name of the wheel file.
:param bool validate_order: Check whether compressed tag set components
are in sorted order.
:raises InvalidWheelFilename: If the filename in question
does not follow the :ref:`wheel specification
<pypug:binary-distribution-format>`.
>>> from packaging.utils import parse_wheel_filename
>>> from packaging.tags import Tag
>>> from packaging.version import Version
>>> name, ver, build, tags = parse_wheel_filename("foo-1.0-py3-none-any.whl")
>>> name
'foo'
>>> ver == Version('1.0')
True
>>> tags == {Tag("py3", "none", "any")}
True
>>> not build
True
.. versionadded:: 20.9
.. versionchanged:: 23.2
Raises :class:`InvalidWheelFilename` when the version component is invalid.
.. versionadded:: 26.1
The *validate_order* parameter.
.. versionchanged:: 26.3
Raises :class:`InvalidWheelFilename` when an interpreter component is
not an identifier, a tag set component is empty, or the project name is
empty.
"""
if not filename.endswith(".whl"):
raise InvalidWheelFilename(
f"Invalid wheel filename (extension must be '.whl'): {filename!r}"
)
filename = filename[:-4]
dashes = filename.count("-")
if dashes not in (4, 5):
raise InvalidWheelFilename(
f"Invalid wheel filename (wrong number of parts): {filename!r}"
)
parts = filename.split("-", dashes - 2)
name_part = parts[0]
# See PEP 427 for the rules on escaping the project name.
if "__" in name_part or _wheel_name_regex.match(name_part) is None:
raise InvalidWheelFilename(f"Invalid project name: {filename!r}")
name = canonicalize_name(name_part)
try:
version = Version(parts[1])
except InvalidVersion as e:
raise InvalidWheelFilename(
f"Invalid wheel filename (invalid version): {filename!r}"
) from e
if dashes == 5:
build_part = parts[2]
build_match = _build_tag_regex.match(build_part)
if build_match is None:
raise InvalidWheelFilename(
f"Invalid build number: {build_part} in {filename!r}"
)
build = cast("BuildTag", (int(build_match.group(1)), build_match.group(2)))
else:
build = ()
tag_str = parts[-1]
try:
tags = parse_tag(tag_str, validate_order=validate_order)
except UnsortedTagsError:
raise InvalidWheelFilename(
f"Invalid wheel filename (compressed tag set components must be in "
f"sorted order per PEP 425): {filename!r}"
) from None
except InvalidTag:
raise InvalidWheelFilename(
f"Invalid wheel filename (invalid tag component): {filename!r}"
) from None
return (name, version, build, tags)
def parse_sdist_filename(filename: str) -> tuple[NormalizedName, Version]:
"""
This function takes the filename of a sdist file (as specified
in the `Source distribution format`_ documentation), and parses
it, returning a tuple of the normalized name and version as
represented by an instance of :class:`~packaging.version.Version`.
:param str filename: The name of the sdist file.
:raises InvalidSdistFilename: If the filename does not end
with an sdist extension (``.zip`` or ``.tar.gz``), if it does not
contain a dash separating the name and the version of the distribution,
if the project name is empty, or if the version portion is not a valid
version.
>>> from packaging.utils import parse_sdist_filename
>>> from packaging.version import Version
>>> name, ver = parse_sdist_filename("foo-1.0.tar.gz")
>>> name
'foo'
>>> ver == Version('1.0')
True
.. versionadded:: 20.9
.. versionchanged:: 21.0
Added support for ``.zip`` source distributions.
.. versionchanged:: 23.2
Raises :class:`InvalidSdistFilename` when the version component is invalid.
.. versionchanged:: 26.3
Raises :class:`InvalidSdistFilename` on an empty project name.
.. _Source distribution format: https://packaging.python.org/specifications/source-distribution-format/#source-distribution-file-name
"""
if filename.endswith(".tar.gz"):
file_stem = filename[: -len(".tar.gz")]
elif filename.endswith(".zip"):
file_stem = filename[: -len(".zip")]
else:
raise InvalidSdistFilename(
f"Invalid sdist filename (extension must be '.tar.gz' or '.zip'):"
f" {filename!r}"
)
# We are requiring a PEP 440 version, which cannot contain dashes,
# so we split on the last dash.
name_part, sep, version_part = file_stem.rpartition("-")
if not sep:
raise InvalidSdistFilename(f"Invalid sdist filename: {filename!r}")
if not name_part:
raise InvalidSdistFilename(
f"Invalid sdist filename (empty project name): {filename!r}"
)
name = canonicalize_name(name_part)
try:
version = Version(version_part)
except InvalidVersion as e:
raise InvalidSdistFilename(
f"Invalid sdist filename (invalid version): {filename!r}"
) from e
return (name, version)
File diff suppressed because it is too large Load Diff