dbrownell-parserlib 0.6.0__tar.gz → 0.7.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dbrownell-parserlib
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Functionality useful when creating parsers.
5
5
  Author: Dave Brownell
6
6
  Author-email: Dave Brownell <github@DavidBrownell.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dbrownell-parserlib"
3
- version = "0.6.0"
3
+ version = "0.7.0"
4
4
  # ^^^^^
5
5
  # Wheel names will be generated according to this value. Do not manually modify this value; instead
6
6
  # update it according to committed changes by running this command from the root of the repository:
@@ -2,7 +2,15 @@
2
2
 
3
3
  from importlib.metadata import version
4
4
 
5
- from dbrownell_ParserLib.antlr import AntlrParser, AntlrParserException, BuildAntlrGrammar, CreateAntlrParser
5
+ from dbrownell_ParserLib.antlr import (
6
+ AntlrParser,
7
+ AntlrParserException,
8
+ AntlrVisitorMixinBase,
9
+ BuildAntlrGrammar,
10
+ CreateAntlrParser,
11
+ InsignificantWhitespaceAntlrVisitorMixin,
12
+ SignificantWhitespaceAntlrVisitorMixin,
13
+ )
6
14
  from dbrownell_ParserLib.errors import Error, PythonError
7
15
  from dbrownell_ParserLib.location import Location
8
16
  from dbrownell_ParserLib.region import Region
@@ -12,12 +20,15 @@ from dbrownell_ParserLib.region import Region
12
20
  __all__ = [
13
21
  "AntlrParser",
14
22
  "AntlrParserException",
23
+ "AntlrVisitorMixinBase",
15
24
  "BuildAntlrGrammar",
16
25
  "CreateAntlrParser",
17
26
  "Error",
27
+ "InsignificantWhitespaceAntlrVisitorMixin",
18
28
  "Location",
19
29
  "PythonError",
20
30
  "Region",
31
+ "SignificantWhitespaceAntlrVisitorMixin",
21
32
  "__version__",
22
33
  ]
23
34
 
@@ -1,12 +1,20 @@
1
1
  # noqa: D104
2
2
  from dbrownell_ParserLib.antlr.build_antlr_grammar import BuildAntlrGrammar
3
3
  from dbrownell_ParserLib.antlr.antlr_parser import AntlrParser, AntlrParserException, CreateAntlrParser
4
+ from dbrownell_ParserLib.antlr.antlr_visitor_mixins import (
5
+ AntlrVisitorMixinBase,
6
+ InsignificantWhitespaceAntlrVisitorMixin,
7
+ SignificantWhitespaceAntlrVisitorMixin,
8
+ )
4
9
 
5
10
 
6
11
  # ----------------------------------------------------------------------
7
12
  __all__ = [
8
13
  "AntlrParser",
9
14
  "AntlrParserException",
15
+ "AntlrVisitorMixinBase",
10
16
  "BuildAntlrGrammar",
11
17
  "CreateAntlrParser",
18
+ "InsignificantWhitespaceAntlrVisitorMixin",
19
+ "SignificantWhitespaceAntlrVisitorMixin",
12
20
  ]
@@ -0,0 +1,155 @@
1
+ """Various ANTLR visitor mixins that can be used to add functionality to the generated visitor classes."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import cast, TYPE_CHECKING
5
+
6
+ import antlr4
7
+
8
+ from dbrownell_ParserLib.region import Location, Region
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Callable
12
+ from pathlib import Path
13
+
14
+
15
+ # ----------------------------------------------------------------------
16
+ class AntlrVisitorMixinBase(ABC):
17
+ """Base class for visitor mixins. This class is not meant to be used directly, but rather to be inherited by other mixin classes."""
18
+
19
+ # ----------------------------------------------------------------------
20
+ def __init__(
21
+ self,
22
+ filename: Path,
23
+ on_progress_func: Callable[[int], None],
24
+ *,
25
+ is_included_file: bool,
26
+ ) -> None:
27
+ self.filename = filename
28
+ self.is_included_file = is_included_file
29
+
30
+ # Protected data
31
+ self._on_progress_func = on_progress_func
32
+ self._stack: list[object] = []
33
+
34
+ # Private data
35
+ self._current_line: int = 0
36
+
37
+ # ----------------------------------------------------------------------
38
+ @abstractmethod
39
+ def CreateRegion(
40
+ self,
41
+ ctx: antlr4.ParserRuleContext,
42
+ ) -> Region:
43
+ """Create a `Region` from the given context."""
44
+
45
+ raise NotImplementedError() # pragma: no cover
46
+
47
+ # ----------------------------------------------------------------------
48
+ def GetChildren(self, ctx: antlr4.ParserRuleContext) -> list[object]:
49
+ """Return the results of visiting the children of the given context."""
50
+
51
+ prev_num_stack_items = len(self._stack)
52
+
53
+ cast(antlr4.ParseTreeVisitor, self).visitChildren(ctx)
54
+
55
+ results = self._stack[prev_num_stack_items:]
56
+ del self._stack[prev_num_stack_items:]
57
+
58
+ return results
59
+
60
+ # ----------------------------------------------------------------------
61
+ # | Protected Methods
62
+ def _OnProgress(self, end_line: int) -> None:
63
+ if end_line > self._current_line:
64
+ self._current_line = end_line
65
+ self._on_progress_func(end_line)
66
+
67
+
68
+ # ----------------------------------------------------------------------
69
+ class InsignificantWhitespaceAntlrVisitorMixin(AntlrVisitorMixinBase):
70
+ """Mixin that adds functionality used for grammars that ignore insignificant whitespace."""
71
+
72
+ # ----------------------------------------------------------------------
73
+ def CreateRegion(
74
+ self,
75
+ ctx: antlr4.ParserRuleContext,
76
+ ) -> Region:
77
+ """Create a `Region` from the given context."""
78
+
79
+ start_token = ctx.start
80
+ stop_token = ctx.stop
81
+
82
+ if start_token is None or stop_token is None:
83
+ msg = "Context does not have start or stop token"
84
+ raise ValueError(msg)
85
+
86
+ return Region(
87
+ self.filename,
88
+ Location(start_token.line, start_token.column),
89
+ Location(stop_token.line, stop_token.column + len(stop_token.text)),
90
+ )
91
+
92
+
93
+ # ----------------------------------------------------------------------
94
+ class SignificantWhitespaceAntlrVisitorMixin(AntlrVisitorMixinBase):
95
+ """Mixin that adds functionality used for grammars that treat whitespace as significant."""
96
+
97
+ # ----------------------------------------------------------------------
98
+ def __init__(
99
+ self,
100
+ dedent_token: int,
101
+ newline_token: int,
102
+ newline_token_string: str,
103
+ ) -> None:
104
+ self._dedent_token = dedent_token
105
+ self._newline_token = newline_token
106
+ self._newline_token_string = newline_token_string
107
+
108
+ # ----------------------------------------------------------------------
109
+ def CreateRegion( # noqa: D102
110
+ self,
111
+ ctx: antlr4.ParserRuleContext,
112
+ ) -> Region:
113
+ assert isinstance(ctx.start, antlr4.Token), ctx.start
114
+ assert isinstance(ctx.stop, antlr4.Token), ctx.stop
115
+
116
+ if ctx.stop.type == self._dedent_token:
117
+ stop_line = ctx.stop.line
118
+ stop_col = ctx.stop.column
119
+
120
+ elif ctx.stop.type == self._newline_token and ctx.stop.text == self._newline_token_string:
121
+ if ctx.stop.line == ctx.start.line:
122
+ # This is the scenario where the statement is followed by a dedent followed by another
123
+ # statement. We don't want the range of this item to overlap with the range of the
124
+ # next item, so use the values as they are, even though it means that a statement
125
+ # that terminates with a newline will not have the newline here.
126
+ stop_line = ctx.stop.line
127
+ stop_col = ctx.stop.column
128
+ else:
129
+ stop_line = ctx.stop.line
130
+ stop_col = ctx.stop.column if ctx.stop.column == 0 else ctx.start.column
131
+
132
+ else:
133
+ stop_line = ctx.stop.line
134
+
135
+ content = ctx.stop.text
136
+ lines = content.split("\n")
137
+
138
+ if ctx.stop.type == self._newline_token:
139
+ assert content.startswith("\n"), content
140
+ assert len(lines) == 2, lines # noqa: PLR2004
141
+
142
+ stop_col = len(lines[-1])
143
+
144
+ if stop_line == ctx.stop.line:
145
+ stop_col += ctx.stop.column
146
+
147
+ stop_line += len(lines) - 1
148
+
149
+ self._OnProgress(stop_line)
150
+
151
+ return Region(
152
+ self.filename,
153
+ Location(ctx.start.line, ctx.start.column + 1),
154
+ Location(stop_line, stop_col),
155
+ )