1# This file is dual licensed under the terms of the Apache License, Version
2# 2.0, and the BSD License. See the LICENSE file in the root of this repository
3# for complete details.
4from __future__ import annotations
5
6from typing import TYPE_CHECKING
7
8from ._parser import parse_requirement as _parse_requirement
9from ._tokenizer import ParserSyntaxError
10from .markers import Marker, _normalize_extra_values
11from .specifiers import InvalidSpecifier, SpecifierSet
12from .utils import canonicalize_name
13
14if TYPE_CHECKING:
15 from collections.abc import Iterator
16
17__all__ = [
18 "InvalidRequirement",
19 "Requirement",
20]
21
22
23def __dir__() -> list[str]:
24 return __all__
25
26
27class InvalidRequirement(ValueError):
28 """
29 An invalid requirement was found, users should refer to PEP 508.
30
31 .. versionadded:: 16.1
32 """
33
34
35class Requirement:
36 """Parse a requirement.
37
38 Parse a given requirement string into its parts, such as name, specifier,
39 URL, and extras. Raises InvalidRequirement on a badly-formed requirement
40 string.
41
42 .. versionadded:: 16.1
43
44 .. versionchanged:: 22.0
45 Added equality (``__eq__``) and hashing (``__hash__``) so requirements
46 can be compared and stored in sets / dicts.
47
48 .. versionchanged:: 23.2
49 Equality and hashing began canonicalizing requirement names, so
50 requirements whose names differ only by normalization (e.g.
51 ``Requirement("Foo")`` vs ``Requirement("foo")``) now compare and hash
52 equal.
53
54 Instances are safe to serialize with :mod:`pickle`. They use a stable
55 format so the same pickle can be loaded in future packaging releases.
56
57 .. versionchanged:: 26.2
58
59 Added a stable pickle format. Pickles created with packaging 26.2+ can
60 be unpickled with future releases. Backward compatibility with pickles
61 from pip._vendor.packaging < 26.2 is supported but may be removed in a future
62 release.
63
64 .. versionchanged:: 26.3
65
66 The dedicated pickle support introduced in 26.2 did not preserve the
67 specifier's explicit :attr:`~packaging.specifiers.SpecifierSet.prereleases`
68 override; it is now included again.
69
70 Equality and hashing normalize requirement names, extras, and
71 equivalent specifiers. The string representation still preserves the
72 parsed name and extras spelling.
73 """
74
75 # TODO: Can we test whether something is contained within a requirement?
76 # If so how do we do that? Do we need to test against the _name_ of
77 # the thing as well as the version? What about the markers?
78 # TODO: Can we normalize the name and extra name?
79
80 __slots__ = ("extras", "marker", "name", "specifier", "url")
81
82 def __init__(self, requirement_string: str) -> None:
83 try:
84 parsed = _parse_requirement(requirement_string)
85 except ParserSyntaxError as e:
86 raise InvalidRequirement(str(e)) from e
87
88 self.name: str = parsed.name
89 self.url: str | None = parsed.url or None
90 self.extras: set[str] = set(parsed.extras)
91 try:
92 self.specifier: SpecifierSet = SpecifierSet(parsed.specifier)
93 except InvalidSpecifier as e:
94 raise InvalidRequirement(str(e)) from e
95 self.marker: Marker | None = None
96 if parsed.marker is not None:
97 self.marker = Marker.__new__(Marker)
98 self.marker._markers = _normalize_extra_values(parsed.marker)
99
100 def _iter_parts(self, name: str) -> Iterator[str]:
101 yield name
102
103 if self.extras:
104 formatted_extras = ",".join(sorted(self.extras))
105 yield f"[{formatted_extras}]"
106
107 if self.specifier:
108 yield str(self.specifier)
109
110 if self.url:
111 yield f" @ {self.url}"
112 if self.marker:
113 yield " "
114
115 if self.marker:
116 yield f"; {self.marker}"
117
118 def __getstate__(self) -> tuple[str, bool | None]:
119 # Return the requirement string for compactness and stability, paired
120 # with the specifier's explicit prereleases override, which is not
121 # captured by the string form. Re-parsed on load to reconstruct all
122 # other fields.
123 return (str(self), self.specifier._prereleases)
124
125 def __setstate__(self, state: object) -> None:
126 if isinstance(state, str):
127 # Format (26.2): just the requirement string.
128 requirement_string: str = state
129 prereleases: bool | None = None
130 elif (
131 isinstance(state, tuple)
132 and len(state) == 2
133 and isinstance(state[0], str)
134 and (state[1] is None or isinstance(state[1], bool))
135 ):
136 # New format (26.3+): (requirement string, specifier prereleases).
137 requirement_string, prereleases = state
138 elif isinstance(state, dict) and state.keys() >= set(self.__slots__):
139 # Old format (packaging <= 26.1, no __slots__): plain __dict__.
140 for key in self.__slots__:
141 setattr(self, key, state[key])
142 return
143 else:
144 raise TypeError(f"Cannot restore Requirement from {state!r}")
145
146 try:
147 tmp = Requirement(requirement_string)
148 except InvalidRequirement as exc:
149 raise TypeError(f"Cannot restore Requirement from {state!r}") from exc
150 self.name = tmp.name
151 self.url = tmp.url
152 self.extras = tmp.extras
153 self.specifier = tmp.specifier
154 self.specifier._prereleases = prereleases
155 self.marker = tmp.marker
156
157 def __str__(self) -> str:
158 return "".join(self._iter_parts(self.name))
159
160 def __repr__(self) -> str:
161 return f"<{self.__class__.__name__}({str(self)!r})>"
162
163 def __hash__(self) -> int:
164 # Mirror __eq__ by hashing the canonical specifier object rather than
165 # its raw string. ``_iter_parts`` yields ``str(self.specifier)``, which
166 # is non-canonical, so trailing-zero-equivalent requirements such as
167 # ``foo==1.0.0`` and ``foo==1.0.0.0`` (which compare equal) would
168 # otherwise hash differently, breaking the hash/__eq__ invariant.
169 return hash(
170 (
171 canonicalize_name(self.name),
172 frozenset(canonicalize_name(e) for e in self.extras),
173 self.specifier,
174 self.url,
175 self.marker,
176 )
177 )
178
179 def __eq__(self, other: object) -> bool:
180 if not isinstance(other, Requirement):
181 return NotImplemented
182
183 # Extras must be normalized before comparison as per PEP 685.
184 self_extras = frozenset(canonicalize_name(e) for e in self.extras)
185 other_extras = frozenset(canonicalize_name(e) for e in other.extras)
186 return (
187 canonicalize_name(self.name) == canonicalize_name(other.name)
188 and self_extras == other_extras
189 and self.specifier == other.specifier
190 and self.url == other.url
191 and self.marker == other.marker
192 )