_parser.py 12 KB

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