]>
Commit | Line | Data |
---|---|---|
53e6db90 DC |
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. | |
4 | ||
5 | from typing import Any, Iterator, Optional, Set | |
6 | ||
7 | from ._parser import parse_requirement as _parse_requirement | |
8 | from ._tokenizer import ParserSyntaxError | |
9 | from .markers import Marker, _normalize_extra_values | |
10 | from .specifiers import SpecifierSet | |
11 | from .utils import canonicalize_name | |
12 | ||
13 | ||
14 | class InvalidRequirement(ValueError): | |
15 | """ | |
16 | An invalid requirement was found, users should refer to PEP 508. | |
17 | """ | |
18 | ||
19 | ||
20 | class Requirement: | |
21 | """Parse a requirement. | |
22 | ||
23 | Parse a given requirement string into its parts, such as name, specifier, | |
24 | URL, and extras. Raises InvalidRequirement on a badly-formed requirement | |
25 | string. | |
26 | """ | |
27 | ||
28 | # TODO: Can we test whether something is contained within a requirement? | |
29 | # If so how do we do that? Do we need to test against the _name_ of | |
30 | # the thing as well as the version? What about the markers? | |
31 | # TODO: Can we normalize the name and extra name? | |
32 | ||
33 | def __init__(self, requirement_string: str) -> None: | |
34 | try: | |
35 | parsed = _parse_requirement(requirement_string) | |
36 | except ParserSyntaxError as e: | |
37 | raise InvalidRequirement(str(e)) from e | |
38 | ||
39 | self.name: str = parsed.name | |
40 | self.url: Optional[str] = parsed.url or None | |
41 | self.extras: Set[str] = set(parsed.extras if parsed.extras else []) | |
42 | self.specifier: SpecifierSet = SpecifierSet(parsed.specifier) | |
43 | self.marker: Optional[Marker] = None | |
44 | if parsed.marker is not None: | |
45 | self.marker = Marker.__new__(Marker) | |
46 | self.marker._markers = _normalize_extra_values(parsed.marker) | |
47 | ||
48 | def _iter_parts(self, name: str) -> Iterator[str]: | |
49 | yield name | |
50 | ||
51 | if self.extras: | |
52 | formatted_extras = ",".join(sorted(self.extras)) | |
53 | yield f"[{formatted_extras}]" | |
54 | ||
55 | if self.specifier: | |
56 | yield str(self.specifier) | |
57 | ||
58 | if self.url: | |
59 | yield f"@ {self.url}" | |
60 | if self.marker: | |
61 | yield " " | |
62 | ||
63 | if self.marker: | |
64 | yield f"; {self.marker}" | |
65 | ||
66 | def __str__(self) -> str: | |
67 | return "".join(self._iter_parts(self.name)) | |
68 | ||
69 | def __repr__(self) -> str: | |
70 | return f"<Requirement('{self}')>" | |
71 | ||
72 | def __hash__(self) -> int: | |
73 | return hash( | |
74 | ( | |
75 | self.__class__.__name__, | |
76 | *self._iter_parts(canonicalize_name(self.name)), | |
77 | ) | |
78 | ) | |
79 | ||
80 | def __eq__(self, other: Any) -> bool: | |
81 | if not isinstance(other, Requirement): | |
82 | return NotImplemented | |
83 | ||
84 | return ( | |
85 | canonicalize_name(self.name) == canonicalize_name(other.name) | |
86 | and self.extras == other.extras | |
87 | and self.specifier == other.specifier | |
88 | and self.url == other.url | |
89 | and self.marker == other.marker | |
90 | ) |