_tokenizer.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. from __future__ import annotations
  2. import contextlib
  3. import re
  4. from dataclasses import dataclass
  5. from typing import TYPE_CHECKING, NoReturn
  6. from .specifiers import Specifier
  7. if TYPE_CHECKING:
  8. from collections.abc import Generator, Mapping
  9. @dataclass
  10. class Token:
  11. __slots__ = ("name", "position", "text")
  12. name: str
  13. text: str
  14. position: int
  15. class ParserSyntaxError(Exception):
  16. """The provided source text could not be parsed correctly."""
  17. def __init__(
  18. self,
  19. message: str,
  20. *,
  21. source: str,
  22. span: tuple[int, int],
  23. ) -> None:
  24. self.span = span
  25. self.message = message
  26. self.source = source
  27. super().__init__()
  28. def __str__(self) -> str:
  29. marker = " " * self.span[0] + "~" * (self.span[1] - self.span[0]) + "^"
  30. return f"{self.message}\n {self.source}\n {marker}"
  31. DEFAULT_RULES: dict[str, re.Pattern[str]] = {
  32. "LEFT_PARENTHESIS": re.compile(r"\("),
  33. "RIGHT_PARENTHESIS": re.compile(r"\)"),
  34. "LEFT_BRACKET": re.compile(r"\["),
  35. "RIGHT_BRACKET": re.compile(r"\]"),
  36. "SEMICOLON": re.compile(r";"),
  37. "COMMA": re.compile(r","),
  38. "QUOTED_STRING": re.compile(
  39. r"""
  40. (
  41. ('[^']*')
  42. |
  43. ("[^"]*")
  44. )
  45. """,
  46. re.VERBOSE,
  47. ),
  48. "OP": re.compile(r"(===|==|~=|!=|<=|>=|<|>)"),
  49. "BOOLOP": re.compile(r"\b(or|and)\b"),
  50. "IN": re.compile(r"\bin\b"),
  51. "NOT": re.compile(r"\bnot\b"),
  52. "VARIABLE": re.compile(
  53. r"""
  54. \b(
  55. python_version
  56. |python_full_version
  57. |os[._]name
  58. |sys[._]platform
  59. |platform_(release|system)
  60. |platform[._](version|machine|python_implementation)
  61. |python_implementation
  62. |implementation_(name|version)
  63. |extras?
  64. |dependency_groups
  65. )\b
  66. """,
  67. re.VERBOSE,
  68. ),
  69. "SPECIFIER": re.compile(
  70. Specifier._specifier_regex_str,
  71. re.VERBOSE | re.IGNORECASE,
  72. ),
  73. "AT": re.compile(r"\@"),
  74. "URL": re.compile(r"[^ \t]+"),
  75. "IDENTIFIER": re.compile(r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b"),
  76. "VERSION_PREFIX_TRAIL": re.compile(r"\.\*"),
  77. "VERSION_LOCAL_LABEL_TRAIL": re.compile(r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*"),
  78. "WS": re.compile(r"[ \t]+"),
  79. "END": re.compile(r"\Z"),
  80. }
  81. class Tokenizer:
  82. """Context-sensitive token parsing.
  83. Provides methods to examine the input stream to check whether the next token
  84. matches.
  85. """
  86. __slots__ = ("next_token", "position", "rules", "source")
  87. def __init__(
  88. self,
  89. source: str,
  90. *,
  91. rules: Mapping[str, re.Pattern[str]],
  92. ) -> None:
  93. self.source = source
  94. self.rules = rules
  95. self.next_token: Token | None = None
  96. self.position = 0
  97. def consume(self, name: str) -> None:
  98. """Move beyond provided token name, if at current position."""
  99. if self.check(name):
  100. self.read()
  101. def check(self, name: str, *, peek: bool = False) -> bool:
  102. """Check whether the next token has the provided name.
  103. By default, if the check succeeds, the token *must* be read before
  104. another check. If `peek` is set to `True`, the token is not loaded and
  105. would need to be checked again.
  106. """
  107. assert self.next_token is None, (
  108. f"Cannot check for {name!r}, already have {self.next_token!r}"
  109. )
  110. assert name in self.rules, f"Unknown token name: {name!r}"
  111. expression = self.rules[name]
  112. match = expression.match(self.source, self.position)
  113. if match is None:
  114. return False
  115. if not peek:
  116. self.next_token = Token(name, match[0], self.position)
  117. return True
  118. def expect(self, name: str, *, expected: str) -> Token:
  119. """Expect a certain token name next, failing with a syntax error otherwise.
  120. The token is read and returned.
  121. """
  122. if not self.check(name):
  123. raise self.raise_syntax_error(f"Expected {expected}")
  124. return self.read()
  125. def read(self) -> Token:
  126. """Consume the next token and return it."""
  127. token = self.next_token
  128. assert token is not None
  129. self.position += len(token.text)
  130. self.next_token = None
  131. return token
  132. def raise_syntax_error(
  133. self,
  134. message: str,
  135. *,
  136. span_start: int | None = None,
  137. span_end: int | None = None,
  138. ) -> NoReturn:
  139. """Raise ParserSyntaxError at the given position."""
  140. span = (
  141. self.position if span_start is None else span_start,
  142. self.position if span_end is None else span_end,
  143. )
  144. raise ParserSyntaxError(
  145. message,
  146. source=self.source,
  147. span=span,
  148. )
  149. @contextlib.contextmanager
  150. def enclosing_tokens(
  151. self, open_token: str, close_token: str, *, around: str
  152. ) -> Generator[None, None, None]:
  153. if self.check(open_token):
  154. open_position = self.position
  155. self.read()
  156. else:
  157. open_position = None
  158. yield
  159. if open_position is None:
  160. return
  161. if not self.check(close_token):
  162. self.raise_syntax_error(
  163. f"Expected matching {close_token} for {open_token}, after {around}",
  164. span_start=open_position,
  165. )
  166. self.read()