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 5from typing import Any, Iterator, Optional, Set 6 7from ._parser import parse_requirement as _parse_requirement 8from ._tokenizer import ParserSyntaxError 9from .markers import Marker, _normalize_extra_values 10from .specifiers import SpecifierSet 11from .utils import canonicalize_name 12 13 14class InvalidRequirement(ValueError): 15 """ 16 An invalid requirement was found, users should refer to PEP 508. 17 """ 18 19 20class 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 ) 91