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