Coverage for /pythoncovmergedfiles/medio/medio/src/black/src/blib2to3/pgen2/driver.py: 87%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved.
2# Licensed to PSF under a Contributor Agreement.
4# Modifications:
5# Copyright 2006 Google, Inc. All Rights Reserved.
6# Licensed to PSF under a Contributor Agreement.
8"""Parser driver.
10This provides a high-level interface to parse a file into a syntax tree.
12"""
14__author__ = "Guido van Rossum <guido@python.org>"
16__all__ = ["Driver", "load_grammar"]
18# Python imports
19import logging
20import os
21import pickle
22import sys
23from collections.abc import Iterable, Iterator
24from contextlib import contextmanager
25from dataclasses import dataclass, field
26from logging import Logger
27from typing import Any, Union, cast
29from blib2to3.pgen2.grammar import Grammar
30from blib2to3.pgen2.tokenize import TokenInfo
31from blib2to3.pytree import NL
33# Pgen imports
34from . import grammar, parse, pgen, token, tokenize
36Path = Union[str, "os.PathLike[str]"]
39@dataclass
40class ReleaseRange:
41 start: int
42 end: int | None = None
43 tokens: list[Any] = field(default_factory=list)
45 def lock(self) -> None:
46 total_eaten = len(self.tokens)
47 self.end = self.start + total_eaten
50class TokenProxy:
51 def __init__(self, generator: Any) -> None:
52 self._tokens = generator
53 self._counter = 0
54 self._release_ranges: list[ReleaseRange] = []
56 @contextmanager
57 def release(self) -> Iterator["TokenProxy"]:
58 release_range = ReleaseRange(self._counter)
59 self._release_ranges.append(release_range)
60 try:
61 yield self
62 finally:
63 # Lock the last release range to the final position that
64 # has been eaten.
65 release_range.lock()
67 def eat(self, point: int) -> Any:
68 eaten_tokens = self._release_ranges[-1].tokens
69 if point < len(eaten_tokens):
70 return eaten_tokens[point]
71 else:
72 while point >= len(eaten_tokens):
73 token = next(self._tokens)
74 eaten_tokens.append(token)
75 return token
77 def __iter__(self) -> "TokenProxy":
78 return self
80 def __next__(self) -> Any:
81 # If the current position is already compromised (looked up)
82 # return the eaten token, if not just go further on the given
83 # token producer.
84 # Ranges are appended in counter order and the counter only moves
85 # forward, so any range we've already advanced past can never match
86 # again. Drop that exhausted prefix; otherwise the scan below grows
87 # with the number of soft-keyword lookaheads and parsing a file full
88 # of them (e.g. many `match`/`case` blocks) becomes quadratic.
89 ranges = self._release_ranges
90 while ranges and ranges[0].end is not None and ranges[0].end <= self._counter:
91 ranges.pop(0)
93 for release_range in ranges:
94 assert release_range.end is not None
96 start, end = release_range.start, release_range.end
97 if start <= self._counter < end:
98 token = release_range.tokens[self._counter - start]
99 break
100 else:
101 token = next(self._tokens)
102 self._counter += 1
103 return token
105 def can_advance(self, to: int) -> bool:
106 # Try to eat, fail if it can't. The eat operation is cached
107 # so there won't be any additional cost of eating here
108 try:
109 self.eat(to)
110 except StopIteration:
111 return False
112 else:
113 return True
116class Driver:
117 def __init__(self, grammar: Grammar, logger: Logger | None = None) -> None:
118 self.grammar = grammar
119 if logger is None:
120 logger = logging.getLogger(__name__)
121 self.logger = logger
123 def parse_tokens(self, tokens: Iterable[TokenInfo], debug: bool = False) -> NL:
124 """Parse a series of tokens and return the syntax tree."""
125 # XXX Move the prefix computation into a wrapper around tokenize.
126 proxy = TokenProxy(tokens)
128 p = parse.Parser(self.grammar)
129 p.setup(proxy=proxy)
131 lineno = 1
132 column = 0
133 indent_columns: list[int] = []
134 type = value = start = end = line_text = None
135 prefix = ""
137 for quintuple in proxy:
138 type, value, start, end, line_text = quintuple
139 if start != (lineno, column):
140 assert (lineno, column) <= start, ((lineno, column), start)
141 s_lineno, s_column = start
142 if lineno < s_lineno:
143 prefix += "\n" * (s_lineno - lineno)
144 lineno = s_lineno
145 column = 0
146 if column < s_column:
147 prefix += line_text[column:s_column]
148 column = s_column
149 if type in (tokenize.COMMENT, tokenize.NL):
150 prefix += value
151 lineno, column = end
152 if value.endswith("\n"):
153 lineno += 1
154 column = 0
155 continue
156 if type == token.OP:
157 type = grammar.opmap[value]
158 if debug:
159 assert type is not None
160 self.logger.debug(
161 "%s %r (prefix=%r)", token.tok_name[type], value, prefix
162 )
163 if type == token.INDENT:
164 indent_columns.append(len(value))
165 _prefix = prefix + value
166 prefix = ""
167 value = ""
168 elif type == token.DEDENT:
169 _indent_col = indent_columns.pop()
170 prefix, _prefix = self._partially_consume_prefix(prefix, _indent_col)
171 if p.addtoken(cast(int, type), value, (prefix, start)):
172 if debug:
173 self.logger.debug("Stop.")
174 break
175 prefix = ""
176 if type in {token.INDENT, token.DEDENT}:
177 prefix = _prefix
178 lineno, column = end
179 # FSTRING_MIDDLE and TSTRING_MIDDLE are the only token that can end with a
180 # newline, and `end` will point to the next line. For that case, don't
181 # increment lineno.
182 if value.endswith("\n") and type not in (
183 token.FSTRING_MIDDLE,
184 token.TSTRING_MIDDLE,
185 ):
186 lineno += 1
187 column = 0
188 else:
189 # We never broke out -- EOF is too soon (how can this happen???)
190 assert start is not None
191 raise parse.ParseError("incomplete input", type, value, (prefix, start))
192 assert p.rootnode is not None
193 return p.rootnode
195 def parse_file(
196 self, filename: Path, encoding: str | None = None, debug: bool = False
197 ) -> NL:
198 """Parse a file and return the syntax tree."""
199 with open(filename, encoding=encoding) as stream:
200 text = stream.read()
201 return self.parse_string(text, debug)
203 def parse_string(self, text: str, debug: bool = False) -> NL:
204 """Parse a string and return the syntax tree."""
205 tokens = tokenize.tokenize(text, grammar=self.grammar)
206 return self.parse_tokens(tokens, debug)
208 def _partially_consume_prefix(self, prefix: str, column: int) -> tuple[str, str]:
209 lines: list[str] = []
210 current_line = ""
211 current_column = 0
212 wait_for_nl = False
213 for char in prefix:
214 current_line += char
215 if wait_for_nl:
216 if char == "\n":
217 if current_line.strip() and current_column < column:
218 res = "".join(lines)
219 return res, prefix[len(res) :]
221 lines.append(current_line)
222 current_line = ""
223 current_column = 0
224 wait_for_nl = False
225 elif char in " \t":
226 current_column += 1
227 elif char == "\n":
228 # unexpected empty line
229 current_column = 0
230 elif char == "\f":
231 current_column = 0
232 else:
233 # indent is finished
234 wait_for_nl = True
235 return "".join(lines), current_line
238def _generate_pickle_name(gt: Path, cache_dir: Path | None = None) -> str:
239 head, tail = os.path.splitext(gt)
240 if tail == ".txt":
241 tail = ""
242 name = head + tail + ".".join(map(str, sys.version_info)) + ".pickle"
243 if cache_dir:
244 return os.path.join(cache_dir, os.path.basename(name))
245 else:
246 return name
249def load_grammar(
250 gt: str = "Grammar.txt",
251 gp: str | None = None,
252 save: bool = True,
253 force: bool = False,
254 logger: Logger | None = None,
255) -> Grammar:
256 """Load the grammar (maybe from a pickle)."""
257 if logger is None:
258 logger = logging.getLogger(__name__)
260 gp = _generate_pickle_name(gt) if gp is None else gp
262 if not force and _newer(gp, gt):
263 try:
264 g_unpickled = grammar.Grammar()
265 g_unpickled.load(gp)
266 return g_unpickled
267 except (
268 pickle.UnpicklingError,
269 EOFError,
270 ValueError,
271 IndexError,
272 PermissionError,
273 ):
274 # Ignore error and continue on to generate a new grammar
275 pass
277 g_generated: grammar.Grammar = pgen.generate_grammar(gt)
278 if save:
279 try:
280 g_generated.dump(gp)
281 except OSError:
282 # Ignore error, caching is not vital.
283 pass
284 return g_generated
287def _newer(a: str, b: str) -> bool:
288 """Inquire whether file a was written since file b."""
289 if not os.path.exists(a):
290 return False
291 if not os.path.exists(b):
292 return True
293 return os.path.getmtime(a) >= os.path.getmtime(b)
296def load_packaged_grammar(
297 grammar_source: str, cache_dir: Path | None = None
298) -> grammar.Grammar:
299 """Loads a grammar by doing `load_grammar(grammar_source)`.
300 This facilitates using a packaged grammar file when needed but preserves
301 load_grammar's automatic regeneration behavior when possible.
302 """
303 gp = _generate_pickle_name(grammar_source, cache_dir) if cache_dir else None
304 return load_grammar(grammar_source, gp=gp)
307def main(*args: str) -> bool:
308 """Main program, when run as a script: produce grammar pickle files.
310 Calls load_grammar for each argument, a path to a grammar text file.
311 """
312 if not args:
313 args = tuple(sys.argv[1:])
314 logging.basicConfig(level=logging.INFO, stream=sys.stdout, format="%(message)s")
315 for gt in args:
316 load_grammar(gt, save=True, force=True)
317 return True
320if __name__ == "__main__":
321 sys.exit(int(not main()))