nab-python 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. nab_python/__init__.py +1 -0
  2. nab_python/_build/__init__.py +1 -0
  3. nab_python/_build/env.py +364 -0
  4. nab_python/_build/errors.py +17 -0
  5. nab_python/_build/runner.py +254 -0
  6. nab_python/_lockfile/__init__.py +1 -0
  7. nab_python/_lockfile/builder.py +339 -0
  8. nab_python/_lockfile/disjointness.py +207 -0
  9. nab_python/_lockfile/pylock.py +323 -0
  10. nab_python/_lockfile/requirements.py +121 -0
  11. nab_python/_packaging_provider.py +98 -0
  12. nab_python/_provider/__init__.py +1 -0
  13. nab_python/_provider/build_remote.py +95 -0
  14. nab_python/_provider/extras.py +231 -0
  15. nab_python/_provider/listing.py +442 -0
  16. nab_python/_provider/lookahead.py +156 -0
  17. nab_python/_provider/metadata_resolver.py +450 -0
  18. nab_python/_provider/priority.py +174 -0
  19. nab_python/_provider/sources.py +215 -0
  20. nab_python/_testing/__init__.py +1 -0
  21. nab_python/_testing/coordinator_fake.py +240 -0
  22. nab_python/_vcs_admission.py +209 -0
  23. nab_python/_vendor/__init__.py +6 -0
  24. nab_python/_vendor/packaging/LICENSE +3 -0
  25. nab_python/_vendor/packaging/LICENSE.APACHE +177 -0
  26. nab_python/_vendor/packaging/LICENSE.BSD +23 -0
  27. nab_python/_vendor/packaging/PROVENANCE.md +73 -0
  28. nab_python/_vendor/packaging/__init__.py +15 -0
  29. nab_python/_vendor/packaging/_elffile.py +108 -0
  30. nab_python/_vendor/packaging/_manylinux.py +265 -0
  31. nab_python/_vendor/packaging/_musllinux.py +88 -0
  32. nab_python/_vendor/packaging/_parser.py +394 -0
  33. nab_python/_vendor/packaging/_structures.py +33 -0
  34. nab_python/_vendor/packaging/_tokenizer.py +196 -0
  35. nab_python/_vendor/packaging/dependency_groups.py +302 -0
  36. nab_python/_vendor/packaging/direct_url.py +325 -0
  37. nab_python/_vendor/packaging/errors.py +94 -0
  38. nab_python/_vendor/packaging/licenses/__init__.py +186 -0
  39. nab_python/_vendor/packaging/licenses/_spdx.py +799 -0
  40. nab_python/_vendor/packaging/markers.py +506 -0
  41. nab_python/_vendor/packaging/metadata.py +964 -0
  42. nab_python/_vendor/packaging/py.typed +0 -0
  43. nab_python/_vendor/packaging/pylock.py +910 -0
  44. nab_python/_vendor/packaging/ranges.py +1803 -0
  45. nab_python/_vendor/packaging/requirements.py +132 -0
  46. nab_python/_vendor/packaging/specifiers.py +1141 -0
  47. nab_python/_vendor/packaging/tags.py +929 -0
  48. nab_python/_vendor/packaging/utils.py +296 -0
  49. nab_python/_vendor/packaging/version.py +1230 -0
  50. nab_python/build_backend.py +184 -0
  51. nab_python/config.py +805 -0
  52. nab_python/download.py +170 -0
  53. nab_python/fetch.py +827 -0
  54. nab_python/lockfile.py +238 -0
  55. nab_python/metadata.py +145 -0
  56. nab_python/provider.py +1235 -0
  57. nab_python/py.typed +0 -0
  58. nab_python/requirements_file.py +180 -0
  59. nab_python/resolve.py +497 -0
  60. nab_python/universal/__init__.py +1 -0
  61. nab_python/universal/matrix.py +235 -0
  62. nab_python/universal/provider.py +214 -0
  63. nab_python/universal/reresolve.py +310 -0
  64. nab_python/universal/resolve.py +508 -0
  65. nab_python/universal/validate.py +439 -0
  66. nab_python/universal/wheel_selection.py +327 -0
  67. nab_python/workspace.py +214 -0
  68. nab_python-0.0.1.dist-info/METADATA +49 -0
  69. nab_python-0.0.1.dist-info/RECORD +71 -0
  70. nab_python-0.0.1.dist-info/WHEEL +4 -0
  71. nab_python-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,394 @@
1
+ """Handwritten parser of dependency specifiers.
2
+
3
+ The docstring for each __parse_* function contains EBNF-inspired grammar representing
4
+ the implementation.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import ast
10
+ from collections.abc import Sequence
11
+ from typing import Literal, NamedTuple, Union
12
+
13
+ from ._tokenizer import DEFAULT_RULES, Tokenizer
14
+
15
+
16
+ class Node:
17
+ __slots__ = ("value",)
18
+
19
+ def __init__(self, value: str) -> None:
20
+ self.value = value
21
+
22
+ def __str__(self) -> str:
23
+ return self.value
24
+
25
+ def __repr__(self) -> str:
26
+ return f"<{self.__class__.__name__}({self.value!r})>"
27
+
28
+ def serialize(self) -> str:
29
+ raise NotImplementedError
30
+
31
+ def __getstate__(self) -> str:
32
+ # Return just the value string for compactness and stability.
33
+ return self.value
34
+
35
+ def _restore_value(self, value: object) -> None:
36
+ if not isinstance(value, str):
37
+ raise TypeError(
38
+ f"Cannot restore {self.__class__.__name__} value from {value!r}"
39
+ )
40
+ self.value = value
41
+
42
+ def __setstate__(self, state: object) -> None:
43
+ if isinstance(state, str):
44
+ # New format (26.2+): just the value string.
45
+ self._restore_value(state)
46
+ return
47
+ if isinstance(state, tuple) and len(state) == 2:
48
+ # Old format (packaging <= 26.0, __slots__): (None, {slot: value}).
49
+ _, slot_dict = state
50
+ if isinstance(slot_dict, dict) and "value" in slot_dict:
51
+ self._restore_value(slot_dict["value"])
52
+ return
53
+ if isinstance(state, dict) and "value" in state:
54
+ # Old format (packaging <= 25.0, no __slots__): plain __dict__.
55
+ self._restore_value(state["value"])
56
+ return
57
+ raise TypeError(f"Cannot restore {self.__class__.__name__} from {state!r}")
58
+
59
+
60
+ class Variable(Node):
61
+ __slots__ = ()
62
+
63
+ def serialize(self) -> str:
64
+ return str(self)
65
+
66
+
67
+ class Value(Node):
68
+ __slots__ = ()
69
+
70
+ def serialize(self) -> str:
71
+ return f'"{self}"'
72
+
73
+
74
+ class Op(Node):
75
+ __slots__ = ()
76
+
77
+ def serialize(self) -> str:
78
+ return str(self)
79
+
80
+
81
+ MarkerLogical = Literal["and", "or"]
82
+ MarkerVar = Union[Variable, Value]
83
+ MarkerItem = tuple[MarkerVar, Op, MarkerVar]
84
+ MarkerAtom = Union[MarkerItem, Sequence["MarkerAtom"]]
85
+ MarkerList = list[Union["MarkerList", MarkerAtom, MarkerLogical]]
86
+
87
+
88
+ class ParsedRequirement(NamedTuple):
89
+ name: str
90
+ url: str
91
+ extras: list[str]
92
+ specifier: str
93
+ marker: MarkerList | None
94
+
95
+
96
+ # --------------------------------------------------------------------------------------
97
+ # Recursive descent parser for dependency specifier
98
+ # --------------------------------------------------------------------------------------
99
+ def parse_requirement(source: str) -> ParsedRequirement:
100
+ return _parse_requirement(Tokenizer(source, rules=DEFAULT_RULES))
101
+
102
+
103
+ def _parse_requirement(tokenizer: Tokenizer) -> ParsedRequirement:
104
+ """
105
+ requirement = WS? IDENTIFIER WS? extras WS? requirement_details
106
+ """
107
+ tokenizer.consume("WS")
108
+
109
+ name_token = tokenizer.expect(
110
+ "IDENTIFIER", expected="package name at the start of dependency specifier"
111
+ )
112
+ name = name_token.text
113
+ tokenizer.consume("WS")
114
+
115
+ extras = _parse_extras(tokenizer)
116
+ tokenizer.consume("WS")
117
+
118
+ url, specifier, marker = _parse_requirement_details(tokenizer)
119
+ tokenizer.expect("END", expected="end of dependency specifier")
120
+
121
+ return ParsedRequirement(name, url, extras, specifier, marker)
122
+
123
+
124
+ def _parse_requirement_details(
125
+ tokenizer: Tokenizer,
126
+ ) -> tuple[str, str, MarkerList | None]:
127
+ """
128
+ requirement_details = AT URL (WS requirement_marker?)?
129
+ | specifier WS? (requirement_marker)?
130
+ """
131
+
132
+ specifier = ""
133
+ url = ""
134
+ marker = None
135
+
136
+ if tokenizer.check("AT"):
137
+ tokenizer.read()
138
+ tokenizer.consume("WS")
139
+
140
+ url_start = tokenizer.position
141
+ url = tokenizer.expect("URL", expected="URL after @").text
142
+ if tokenizer.check("END", peek=True):
143
+ return (url, specifier, marker)
144
+
145
+ tokenizer.expect("WS", expected="whitespace after URL")
146
+
147
+ # The input might end after whitespace.
148
+ if tokenizer.check("END", peek=True):
149
+ return (url, specifier, marker)
150
+
151
+ marker = _parse_requirement_marker(
152
+ tokenizer,
153
+ span_start=url_start,
154
+ expected="semicolon (after URL and whitespace)",
155
+ )
156
+ else:
157
+ specifier_start = tokenizer.position
158
+ specifier = _parse_specifier(tokenizer)
159
+ tokenizer.consume("WS")
160
+
161
+ if tokenizer.check("END", peek=True):
162
+ return (url, specifier, marker)
163
+
164
+ marker = _parse_requirement_marker(
165
+ tokenizer,
166
+ span_start=specifier_start,
167
+ expected=(
168
+ "comma (within version specifier), semicolon (after version specifier)"
169
+ if specifier
170
+ else "semicolon (after name with no version specifier)"
171
+ ),
172
+ )
173
+
174
+ return (url, specifier, marker)
175
+
176
+
177
+ def _parse_requirement_marker(
178
+ tokenizer: Tokenizer, *, span_start: int, expected: str
179
+ ) -> MarkerList:
180
+ """
181
+ requirement_marker = SEMICOLON marker WS?
182
+ """
183
+
184
+ if not tokenizer.check("SEMICOLON"):
185
+ tokenizer.raise_syntax_error(
186
+ f"Expected {expected} or end",
187
+ span_start=span_start,
188
+ span_end=None,
189
+ )
190
+ tokenizer.read()
191
+
192
+ marker = _parse_marker(tokenizer)
193
+ tokenizer.consume("WS")
194
+
195
+ return marker
196
+
197
+
198
+ def _parse_extras(tokenizer: Tokenizer) -> list[str]:
199
+ """
200
+ extras = (LEFT_BRACKET wsp* extras_list? wsp* RIGHT_BRACKET)?
201
+ """
202
+ if not tokenizer.check("LEFT_BRACKET", peek=True):
203
+ return []
204
+
205
+ with tokenizer.enclosing_tokens(
206
+ "LEFT_BRACKET",
207
+ "RIGHT_BRACKET",
208
+ around="extras",
209
+ ):
210
+ tokenizer.consume("WS")
211
+ extras = _parse_extras_list(tokenizer)
212
+ tokenizer.consume("WS")
213
+
214
+ return extras
215
+
216
+
217
+ def _parse_extras_list(tokenizer: Tokenizer) -> list[str]:
218
+ """
219
+ extras_list = identifier (wsp* ',' wsp* identifier)*
220
+ """
221
+ extras: list[str] = []
222
+
223
+ if not tokenizer.check("IDENTIFIER"):
224
+ return extras
225
+
226
+ extras.append(tokenizer.read().text)
227
+
228
+ while True:
229
+ tokenizer.consume("WS")
230
+ if tokenizer.check("IDENTIFIER", peek=True):
231
+ tokenizer.raise_syntax_error("Expected comma between extra names")
232
+ elif not tokenizer.check("COMMA"):
233
+ break
234
+
235
+ tokenizer.read()
236
+ tokenizer.consume("WS")
237
+
238
+ extra_token = tokenizer.expect("IDENTIFIER", expected="extra name after comma")
239
+ extras.append(extra_token.text)
240
+
241
+ return extras
242
+
243
+
244
+ def _parse_specifier(tokenizer: Tokenizer) -> str:
245
+ """
246
+ specifier = LEFT_PARENTHESIS WS? version_many WS? RIGHT_PARENTHESIS
247
+ | WS? version_many WS?
248
+ """
249
+ with tokenizer.enclosing_tokens(
250
+ "LEFT_PARENTHESIS",
251
+ "RIGHT_PARENTHESIS",
252
+ around="version specifier",
253
+ ):
254
+ tokenizer.consume("WS")
255
+ parsed_specifiers = _parse_version_many(tokenizer)
256
+ tokenizer.consume("WS")
257
+
258
+ return parsed_specifiers
259
+
260
+
261
+ def _parse_version_many(tokenizer: Tokenizer) -> str:
262
+ """
263
+ version_many = (SPECIFIER (WS? COMMA WS? SPECIFIER)*)?
264
+ """
265
+ parsed_specifiers = ""
266
+ while tokenizer.check("SPECIFIER"):
267
+ span_start = tokenizer.position
268
+ parsed_specifiers += tokenizer.read().text
269
+ if tokenizer.check("VERSION_PREFIX_TRAIL", peek=True):
270
+ tokenizer.raise_syntax_error(
271
+ ".* suffix can only be used with `==` or `!=` operators",
272
+ span_start=span_start,
273
+ span_end=tokenizer.position + 1,
274
+ )
275
+ if tokenizer.check("VERSION_LOCAL_LABEL_TRAIL", peek=True):
276
+ tokenizer.raise_syntax_error(
277
+ "Local version label can only be used with `==` or `!=` operators",
278
+ span_start=span_start,
279
+ span_end=tokenizer.position,
280
+ )
281
+ tokenizer.consume("WS")
282
+ if not tokenizer.check("COMMA"):
283
+ break
284
+ parsed_specifiers += tokenizer.read().text
285
+ tokenizer.consume("WS")
286
+
287
+ return parsed_specifiers
288
+
289
+
290
+ # --------------------------------------------------------------------------------------
291
+ # Recursive descent parser for marker expression
292
+ # --------------------------------------------------------------------------------------
293
+ def parse_marker(source: str) -> MarkerList:
294
+ return _parse_full_marker(Tokenizer(source, rules=DEFAULT_RULES))
295
+
296
+
297
+ def _parse_full_marker(tokenizer: Tokenizer) -> MarkerList:
298
+ retval = _parse_marker(tokenizer)
299
+ tokenizer.expect("END", expected="end of marker expression")
300
+ return retval
301
+
302
+
303
+ def _parse_marker(tokenizer: Tokenizer) -> MarkerList:
304
+ """
305
+ marker = marker_atom (BOOLOP marker_atom)+
306
+ """
307
+ expression = [_parse_marker_atom(tokenizer)]
308
+ while tokenizer.check("BOOLOP"):
309
+ token = tokenizer.read()
310
+ expr_right = _parse_marker_atom(tokenizer)
311
+ expression.extend((token.text, expr_right))
312
+ return expression
313
+
314
+
315
+ def _parse_marker_atom(tokenizer: Tokenizer) -> MarkerAtom:
316
+ """
317
+ marker_atom = WS? LEFT_PARENTHESIS WS? marker WS? RIGHT_PARENTHESIS WS?
318
+ | WS? marker_item WS?
319
+ """
320
+
321
+ tokenizer.consume("WS")
322
+ if tokenizer.check("LEFT_PARENTHESIS", peek=True):
323
+ with tokenizer.enclosing_tokens(
324
+ "LEFT_PARENTHESIS",
325
+ "RIGHT_PARENTHESIS",
326
+ around="marker expression",
327
+ ):
328
+ tokenizer.consume("WS")
329
+ marker: MarkerAtom = _parse_marker(tokenizer)
330
+ tokenizer.consume("WS")
331
+ else:
332
+ marker = _parse_marker_item(tokenizer)
333
+ tokenizer.consume("WS")
334
+ return marker
335
+
336
+
337
+ def _parse_marker_item(tokenizer: Tokenizer) -> MarkerItem:
338
+ """
339
+ marker_item = WS? marker_var WS? marker_op WS? marker_var WS?
340
+ """
341
+ tokenizer.consume("WS")
342
+ marker_var_left = _parse_marker_var(tokenizer)
343
+ tokenizer.consume("WS")
344
+ marker_op = _parse_marker_op(tokenizer)
345
+ tokenizer.consume("WS")
346
+ marker_var_right = _parse_marker_var(tokenizer)
347
+ tokenizer.consume("WS")
348
+ return (marker_var_left, marker_op, marker_var_right)
349
+
350
+
351
+ def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar: # noqa: RET503
352
+ """
353
+ marker_var = VARIABLE | QUOTED_STRING
354
+ """
355
+ if tokenizer.check("VARIABLE"):
356
+ return process_env_var(tokenizer.read().text.replace(".", "_"))
357
+ elif tokenizer.check("QUOTED_STRING"):
358
+ return process_python_str(tokenizer.read().text)
359
+ else:
360
+ tokenizer.raise_syntax_error(
361
+ message="Expected a marker variable or quoted string"
362
+ )
363
+
364
+
365
+ def process_env_var(env_var: str) -> Variable:
366
+ if env_var in ("platform_python_implementation", "python_implementation"):
367
+ return Variable("platform_python_implementation")
368
+ else:
369
+ return Variable(env_var)
370
+
371
+
372
+ def process_python_str(python_str: str) -> Value:
373
+ value = ast.literal_eval(python_str)
374
+ return Value(str(value))
375
+
376
+
377
+ def _parse_marker_op(tokenizer: Tokenizer) -> Op:
378
+ """
379
+ marker_op = IN | NOT IN | OP
380
+ """
381
+ if tokenizer.check("IN"):
382
+ tokenizer.read()
383
+ return Op("in")
384
+ elif tokenizer.check("NOT"):
385
+ tokenizer.read()
386
+ tokenizer.expect("WS", expected="whitespace after 'not'")
387
+ tokenizer.expect("IN", expected="'in' after 'not'")
388
+ return Op("not in")
389
+ elif tokenizer.check("OP"):
390
+ return Op(tokenizer.read().text)
391
+ else:
392
+ return tokenizer.raise_syntax_error(
393
+ "Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in"
394
+ )
@@ -0,0 +1,33 @@
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
+
5
+ """Backward-compatibility shim for unpickling Version objects serialized before
6
+ packaging 26.1.
7
+
8
+ Old pickles reference ``packaging._structures.InfinityType`` and
9
+ ``packaging._structures.NegativeInfinityType``. This module provides minimal
10
+ stand-in classes so that ``pickle.loads()`` can resolve those references.
11
+ The deserialized objects are not used for comparisons — ``Version.__setstate__``
12
+ discards the stale ``_key`` cache and recomputes it from the core version fields.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+
18
+ class InfinityType:
19
+ """Stand-in for the removed ``InfinityType`` used in old comparison keys."""
20
+
21
+ def __repr__(self) -> str:
22
+ return "Infinity"
23
+
24
+
25
+ class NegativeInfinityType:
26
+ """Stand-in for the removed ``NegativeInfinityType`` used in old comparison keys."""
27
+
28
+ def __repr__(self) -> str:
29
+ return "-Infinity"
30
+
31
+
32
+ Infinity = InfinityType()
33
+ NegativeInfinity = NegativeInfinityType()
@@ -0,0 +1,196 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import re
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, NoReturn
7
+
8
+ from .specifiers import Specifier
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Generator, Mapping
12
+
13
+
14
+ @dataclass
15
+ class Token:
16
+ name: str
17
+ text: str
18
+ position: int
19
+
20
+
21
+ class ParserSyntaxError(Exception):
22
+ """The provided source text could not be parsed correctly."""
23
+
24
+ def __init__(
25
+ self,
26
+ message: str,
27
+ *,
28
+ source: str,
29
+ span: tuple[int, int],
30
+ ) -> None:
31
+ self.span = span
32
+ self.message = message
33
+ self.source = source
34
+
35
+ super().__init__()
36
+
37
+ def __str__(self) -> str:
38
+ marker = " " * self.span[0] + "~" * (self.span[1] - self.span[0]) + "^"
39
+ return f"{self.message}\n {self.source}\n {marker}"
40
+
41
+
42
+ DEFAULT_RULES: dict[str, re.Pattern[str]] = {
43
+ "LEFT_PARENTHESIS": re.compile(r"\("),
44
+ "RIGHT_PARENTHESIS": re.compile(r"\)"),
45
+ "LEFT_BRACKET": re.compile(r"\["),
46
+ "RIGHT_BRACKET": re.compile(r"\]"),
47
+ "SEMICOLON": re.compile(r";"),
48
+ "COMMA": re.compile(r","),
49
+ "QUOTED_STRING": re.compile(
50
+ r"""
51
+ (
52
+ ('[^']*')
53
+ |
54
+ ("[^"]*")
55
+ )
56
+ """,
57
+ re.VERBOSE,
58
+ ),
59
+ "OP": re.compile(r"(===|==|~=|!=|<=|>=|<|>)"),
60
+ "BOOLOP": re.compile(r"\b(or|and)\b"),
61
+ "IN": re.compile(r"\bin\b"),
62
+ "NOT": re.compile(r"\bnot\b"),
63
+ "VARIABLE": re.compile(
64
+ r"""
65
+ \b(
66
+ python_version
67
+ |python_full_version
68
+ |os[._]name
69
+ |sys[._]platform
70
+ |platform_(release|system)
71
+ |platform[._](version|machine|python_implementation)
72
+ |python_implementation
73
+ |implementation_(name|version)
74
+ |extras?
75
+ |dependency_groups
76
+ )\b
77
+ """,
78
+ re.VERBOSE,
79
+ ),
80
+ "SPECIFIER": re.compile(
81
+ Specifier._specifier_regex_str,
82
+ re.VERBOSE | re.IGNORECASE,
83
+ ),
84
+ "AT": re.compile(r"\@"),
85
+ "URL": re.compile(r"[^ \t]+"),
86
+ "IDENTIFIER": re.compile(r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b"),
87
+ "VERSION_PREFIX_TRAIL": re.compile(r"\.\*"),
88
+ "VERSION_LOCAL_LABEL_TRAIL": re.compile(r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*"),
89
+ "WS": re.compile(r"[ \t]+"),
90
+ "END": re.compile(r"$"),
91
+ }
92
+
93
+
94
+ class Tokenizer:
95
+ """Context-sensitive token parsing.
96
+
97
+ Provides methods to examine the input stream to check whether the next token
98
+ matches.
99
+ """
100
+
101
+ def __init__(
102
+ self,
103
+ source: str,
104
+ *,
105
+ rules: Mapping[str, re.Pattern[str]],
106
+ ) -> None:
107
+ self.source = source
108
+ self.rules = rules
109
+ self.next_token: Token | None = None
110
+ self.position = 0
111
+
112
+ def consume(self, name: str) -> None:
113
+ """Move beyond provided token name, if at current position."""
114
+ if self.check(name):
115
+ self.read()
116
+
117
+ def check(self, name: str, *, peek: bool = False) -> bool:
118
+ """Check whether the next token has the provided name.
119
+
120
+ By default, if the check succeeds, the token *must* be read before
121
+ another check. If `peek` is set to `True`, the token is not loaded and
122
+ would need to be checked again.
123
+ """
124
+ assert self.next_token is None, (
125
+ f"Cannot check for {name!r}, already have {self.next_token!r}"
126
+ )
127
+ assert name in self.rules, f"Unknown token name: {name!r}"
128
+
129
+ expression = self.rules[name]
130
+
131
+ match = expression.match(self.source, self.position)
132
+ if match is None:
133
+ return False
134
+ if not peek:
135
+ self.next_token = Token(name, match[0], self.position)
136
+ return True
137
+
138
+ def expect(self, name: str, *, expected: str) -> Token:
139
+ """Expect a certain token name next, failing with a syntax error otherwise.
140
+
141
+ The token is *not* read.
142
+ """
143
+ if not self.check(name):
144
+ raise self.raise_syntax_error(f"Expected {expected}")
145
+ return self.read()
146
+
147
+ def read(self) -> Token:
148
+ """Consume the next token and return it."""
149
+ token = self.next_token
150
+ assert token is not None
151
+
152
+ self.position += len(token.text)
153
+ self.next_token = None
154
+
155
+ return token
156
+
157
+ def raise_syntax_error(
158
+ self,
159
+ message: str,
160
+ *,
161
+ span_start: int | None = None,
162
+ span_end: int | None = None,
163
+ ) -> NoReturn:
164
+ """Raise ParserSyntaxError at the given position."""
165
+ span = (
166
+ self.position if span_start is None else span_start,
167
+ self.position if span_end is None else span_end,
168
+ )
169
+ raise ParserSyntaxError(
170
+ message,
171
+ source=self.source,
172
+ span=span,
173
+ )
174
+
175
+ @contextlib.contextmanager
176
+ def enclosing_tokens(
177
+ self, open_token: str, close_token: str, *, around: str
178
+ ) -> Generator[None, None, None]:
179
+ if self.check(open_token):
180
+ open_position = self.position
181
+ self.read()
182
+ else:
183
+ open_position = None
184
+
185
+ yield
186
+
187
+ if open_position is None:
188
+ return
189
+
190
+ if not self.check(close_token):
191
+ self.raise_syntax_error(
192
+ f"Expected matching {close_token} for {open_token}, after {around}",
193
+ span_start=open_position,
194
+ )
195
+
196
+ self.read()