_parser.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. """Handwritten parser of dependency specifiers.
  2. The docstring for each __parse_* function contains EBNF-inspired grammar representing
  3. the implementation.
  4. """
  5. from __future__ import annotations
  6. import ast
  7. from typing import List, Literal, NamedTuple, Sequence, Tuple, Union
  8. from ._tokenizer import DEFAULT_RULES, Tokenizer
  9. class Node:
  10. __slots__ = ("value",)
  11. def __init__(self, value: str) -> None:
  12. self.value = value
  13. def __str__(self) -> str:
  14. return self.value
  15. def __repr__(self) -> str:
  16. return f"<{self.__class__.__name__}({self.value!r})>"
  17. def serialize(self) -> str:
  18. raise NotImplementedError
  19. def __getstate__(self) -> str:
  20. # Return just the value string for compactness and stability.
  21. return self.value
  22. def _restore_value(self, value: object) -> None:
  23. if not isinstance(value, str):
  24. raise TypeError(
  25. f"Cannot restore {self.__class__.__name__} value from {value!r}"
  26. )
  27. self.value = value
  28. def __setstate__(self, state: object) -> None:
  29. if isinstance(state, str):
  30. # New format (26.2+): just the value string.
  31. self._restore_value(state)
  32. return
  33. if isinstance(state, tuple) and len(state) == 2:
  34. # Old format (packaging <= 26.0, __slots__): (None, {slot: value}).
  35. _, slot_dict = state
  36. if isinstance(slot_dict, dict) and "value" in slot_dict:
  37. self._restore_value(slot_dict["value"])
  38. return
  39. if isinstance(state, dict) and "value" in state:
  40. # Old format (packaging <= 25.0, no __slots__): plain __dict__.
  41. self._restore_value(state["value"])
  42. return
  43. raise TypeError(f"Cannot restore {self.__class__.__name__} from {state!r}")
  44. class Variable(Node):
  45. __slots__ = ()
  46. def serialize(self) -> str:
  47. return str(self)
  48. class Value(Node):
  49. __slots__ = ()
  50. def serialize(self) -> str:
  51. return f'"{self}"'
  52. class Op(Node):
  53. __slots__ = ()
  54. def serialize(self) -> str:
  55. return str(self)
  56. MarkerLogical = Literal["and", "or"]
  57. MarkerVar = Union[Variable, Value]
  58. MarkerItem = Tuple[MarkerVar, Op, MarkerVar]
  59. MarkerAtom = Union[MarkerItem, Sequence["MarkerAtom"]]
  60. MarkerList = List[Union["MarkerList", MarkerAtom, MarkerLogical]]
  61. class ParsedRequirement(NamedTuple):
  62. name: str
  63. url: str
  64. extras: list[str]
  65. specifier: str
  66. marker: MarkerList | None
  67. # --------------------------------------------------------------------------------------
  68. # Recursive descent parser for dependency specifier
  69. # --------------------------------------------------------------------------------------
  70. def parse_requirement(source: str) -> ParsedRequirement:
  71. return _parse_requirement(Tokenizer(source, rules=DEFAULT_RULES))
  72. def _parse_requirement(tokenizer: Tokenizer) -> ParsedRequirement:
  73. """
  74. requirement = WS? IDENTIFIER WS? extras WS? requirement_details
  75. """
  76. tokenizer.consume("WS")
  77. name_token = tokenizer.expect(
  78. "IDENTIFIER", expected="package name at the start of dependency specifier"
  79. )
  80. name = name_token.text
  81. tokenizer.consume("WS")
  82. extras = _parse_extras(tokenizer)
  83. tokenizer.consume("WS")
  84. url, specifier, marker = _parse_requirement_details(tokenizer)
  85. tokenizer.expect("END", expected="end of dependency specifier")
  86. return ParsedRequirement(name, url, extras, specifier, marker)
  87. def _parse_requirement_details(
  88. tokenizer: Tokenizer,
  89. ) -> tuple[str, str, MarkerList | None]:
  90. """
  91. requirement_details = AT URL (WS requirement_marker?)?
  92. | specifier WS? (requirement_marker)?
  93. """
  94. specifier = ""
  95. url = ""
  96. marker = None
  97. if tokenizer.check("AT"):
  98. tokenizer.read()
  99. tokenizer.consume("WS")
  100. url_start = tokenizer.position
  101. url = tokenizer.expect("URL", expected="URL after @").text
  102. if tokenizer.check("END", peek=True):
  103. return (url, specifier, marker)
  104. tokenizer.expect("WS", expected="whitespace after URL")
  105. # The input might end after whitespace.
  106. if tokenizer.check("END", peek=True):
  107. return (url, specifier, marker)
  108. marker = _parse_requirement_marker(
  109. tokenizer,
  110. span_start=url_start,
  111. expected="semicolon (after URL and whitespace)",
  112. )
  113. else:
  114. specifier_start = tokenizer.position
  115. specifier = _parse_specifier(tokenizer)
  116. tokenizer.consume("WS")
  117. if tokenizer.check("END", peek=True):
  118. return (url, specifier, marker)
  119. marker = _parse_requirement_marker(
  120. tokenizer,
  121. span_start=specifier_start,
  122. expected=(
  123. "comma (within version specifier), semicolon (after version specifier)"
  124. if specifier
  125. else "semicolon (after name with no version specifier)"
  126. ),
  127. )
  128. return (url, specifier, marker)
  129. def _parse_requirement_marker(
  130. tokenizer: Tokenizer, *, span_start: int, expected: str
  131. ) -> MarkerList:
  132. """
  133. requirement_marker = SEMICOLON marker WS?
  134. """
  135. if not tokenizer.check("SEMICOLON"):
  136. tokenizer.raise_syntax_error(
  137. f"Expected {expected} or end",
  138. span_start=span_start,
  139. span_end=None,
  140. )
  141. tokenizer.read()
  142. marker = _parse_marker(tokenizer)
  143. tokenizer.consume("WS")
  144. return marker
  145. def _parse_extras(tokenizer: Tokenizer) -> list[str]:
  146. """
  147. extras = (LEFT_BRACKET wsp* extras_list? wsp* RIGHT_BRACKET)?
  148. """
  149. if not tokenizer.check("LEFT_BRACKET", peek=True):
  150. return []
  151. with tokenizer.enclosing_tokens(
  152. "LEFT_BRACKET",
  153. "RIGHT_BRACKET",
  154. around="extras",
  155. ):
  156. tokenizer.consume("WS")
  157. extras = _parse_extras_list(tokenizer)
  158. tokenizer.consume("WS")
  159. return extras
  160. def _parse_extras_list(tokenizer: Tokenizer) -> list[str]:
  161. """
  162. extras_list = identifier (wsp* ',' wsp* identifier)*
  163. """
  164. extras: list[str] = []
  165. if not tokenizer.check("IDENTIFIER"):
  166. return extras
  167. extras.append(tokenizer.read().text)
  168. while True:
  169. tokenizer.consume("WS")
  170. if tokenizer.check("IDENTIFIER", peek=True):
  171. tokenizer.raise_syntax_error("Expected comma between extra names")
  172. elif not tokenizer.check("COMMA"):
  173. break
  174. tokenizer.read()
  175. tokenizer.consume("WS")
  176. extra_token = tokenizer.expect("IDENTIFIER", expected="extra name after comma")
  177. extras.append(extra_token.text)
  178. return extras
  179. def _parse_specifier(tokenizer: Tokenizer) -> str:
  180. """
  181. specifier = LEFT_PARENTHESIS WS? version_many WS? RIGHT_PARENTHESIS
  182. | WS? version_many WS?
  183. """
  184. with tokenizer.enclosing_tokens(
  185. "LEFT_PARENTHESIS",
  186. "RIGHT_PARENTHESIS",
  187. around="version specifier",
  188. ):
  189. tokenizer.consume("WS")
  190. parsed_specifiers = _parse_version_many(tokenizer)
  191. tokenizer.consume("WS")
  192. return parsed_specifiers
  193. def _parse_version_many(tokenizer: Tokenizer) -> str:
  194. """
  195. version_many = (SPECIFIER (WS? COMMA WS? SPECIFIER)*)?
  196. """
  197. parsed_specifiers = ""
  198. while tokenizer.check("SPECIFIER"):
  199. span_start = tokenizer.position
  200. parsed_specifiers += tokenizer.read().text
  201. if tokenizer.check("VERSION_PREFIX_TRAIL", peek=True):
  202. tokenizer.raise_syntax_error(
  203. ".* suffix can only be used with `==` or `!=` operators",
  204. span_start=span_start,
  205. span_end=tokenizer.position + 1,
  206. )
  207. if tokenizer.check("VERSION_LOCAL_LABEL_TRAIL", peek=True):
  208. tokenizer.raise_syntax_error(
  209. "Local version label can only be used with `==` or `!=` operators",
  210. span_start=span_start,
  211. span_end=tokenizer.position,
  212. )
  213. tokenizer.consume("WS")
  214. if not tokenizer.check("COMMA"):
  215. break
  216. parsed_specifiers += tokenizer.read().text
  217. tokenizer.consume("WS")
  218. return parsed_specifiers
  219. # --------------------------------------------------------------------------------------
  220. # Recursive descent parser for marker expression
  221. # --------------------------------------------------------------------------------------
  222. def parse_marker(source: str) -> MarkerList:
  223. return _parse_full_marker(Tokenizer(source, rules=DEFAULT_RULES))
  224. def _parse_full_marker(tokenizer: Tokenizer) -> MarkerList:
  225. retval = _parse_marker(tokenizer)
  226. tokenizer.expect("END", expected="end of marker expression")
  227. return retval
  228. def _parse_marker(tokenizer: Tokenizer) -> MarkerList:
  229. """
  230. marker = marker_atom (BOOLOP marker_atom)+
  231. """
  232. expression = [_parse_marker_atom(tokenizer)]
  233. while tokenizer.check("BOOLOP"):
  234. token = tokenizer.read()
  235. expr_right = _parse_marker_atom(tokenizer)
  236. expression.extend((token.text, expr_right))
  237. return expression
  238. def _parse_marker_atom(tokenizer: Tokenizer) -> MarkerAtom:
  239. """
  240. marker_atom = WS? LEFT_PARENTHESIS WS? marker WS? RIGHT_PARENTHESIS WS?
  241. | WS? marker_item WS?
  242. """
  243. tokenizer.consume("WS")
  244. if tokenizer.check("LEFT_PARENTHESIS", peek=True):
  245. with tokenizer.enclosing_tokens(
  246. "LEFT_PARENTHESIS",
  247. "RIGHT_PARENTHESIS",
  248. around="marker expression",
  249. ):
  250. tokenizer.consume("WS")
  251. marker: MarkerAtom = _parse_marker(tokenizer)
  252. tokenizer.consume("WS")
  253. else:
  254. marker = _parse_marker_item(tokenizer)
  255. tokenizer.consume("WS")
  256. return marker
  257. def _parse_marker_item(tokenizer: Tokenizer) -> MarkerItem:
  258. """
  259. marker_item = WS? marker_var WS? marker_op WS? marker_var WS?
  260. """
  261. tokenizer.consume("WS")
  262. marker_var_left = _parse_marker_var(tokenizer)
  263. tokenizer.consume("WS")
  264. marker_op = _parse_marker_op(tokenizer)
  265. tokenizer.consume("WS")
  266. marker_var_right = _parse_marker_var(tokenizer)
  267. tokenizer.consume("WS")
  268. return (marker_var_left, marker_op, marker_var_right)
  269. def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar: # noqa: RET503
  270. """
  271. marker_var = VARIABLE | QUOTED_STRING
  272. """
  273. if tokenizer.check("VARIABLE"):
  274. return process_env_var(tokenizer.read().text.replace(".", "_"))
  275. elif tokenizer.check("QUOTED_STRING"):
  276. return process_python_str(tokenizer.read().text)
  277. else:
  278. tokenizer.raise_syntax_error(
  279. message="Expected a marker variable or quoted string"
  280. )
  281. def process_env_var(env_var: str) -> Variable:
  282. if env_var in ("platform_python_implementation", "python_implementation"):
  283. return Variable("platform_python_implementation")
  284. else:
  285. return Variable(env_var)
  286. def process_python_str(python_str: str) -> Value:
  287. value = ast.literal_eval(python_str)
  288. return Value(str(value))
  289. def _parse_marker_op(tokenizer: Tokenizer) -> Op:
  290. """
  291. marker_op = IN | NOT IN | OP
  292. """
  293. if tokenizer.check("IN"):
  294. tokenizer.read()
  295. return Op("in")
  296. elif tokenizer.check("NOT"):
  297. tokenizer.read()
  298. tokenizer.expect("WS", expected="whitespace after 'not'")
  299. tokenizer.expect("IN", expected="'in' after 'not'")
  300. return Op("not in")
  301. elif tokenizer.check("OP"):
  302. return Op(tokenizer.read().text)
  303. else:
  304. return tokenizer.raise_syntax_error(
  305. "Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in"
  306. )