dbrownell-parserlib 0.8.4__tar.gz → 0.9.1__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.
Files changed (19) hide show
  1. {dbrownell_parserlib-0.8.4 → dbrownell_parserlib-0.9.1}/PKG-INFO +8 -10
  2. {dbrownell_parserlib-0.8.4 → dbrownell_parserlib-0.9.1}/README.md +7 -9
  3. {dbrownell_parserlib-0.8.4 → dbrownell_parserlib-0.9.1}/pyproject.toml +1 -1
  4. dbrownell_parserlib-0.9.1/src/dbrownell_ParserLib/__init__.py +6 -0
  5. dbrownell_parserlib-0.9.1/src/dbrownell_ParserLib/antlr/__init__.py +1 -0
  6. {dbrownell_parserlib-0.8.4 → dbrownell_parserlib-0.9.1}/src/dbrownell_ParserLib/antlr/antlr_parser.py +3 -14
  7. dbrownell_parserlib-0.9.1/src/dbrownell_ParserLib/errors.py +232 -0
  8. dbrownell_parserlib-0.8.4/src/dbrownell_ParserLib/__init__.py +0 -44
  9. dbrownell_parserlib-0.8.4/src/dbrownell_ParserLib/antlr/__init__.py +0 -20
  10. dbrownell_parserlib-0.8.4/src/dbrownell_ParserLib/errors.py +0 -112
  11. {dbrownell_parserlib-0.8.4 → dbrownell_parserlib-0.9.1}/src/dbrownell_ParserLib/antlr/antlr_visitor_mixins.py +0 -0
  12. {dbrownell_parserlib-0.8.4 → dbrownell_parserlib-0.9.1}/src/dbrownell_ParserLib/antlr/build_antlr_grammar/__init__.py +0 -0
  13. {dbrownell_parserlib-0.8.4 → dbrownell_parserlib-0.9.1}/src/dbrownell_ParserLib/antlr/build_antlr_grammar/antlr-4.13.2-complete.jar +0 -0
  14. {dbrownell_parserlib-0.8.4 → dbrownell_parserlib-0.9.1}/src/dbrownell_ParserLib/expression.py +0 -0
  15. {dbrownell_parserlib-0.8.4 → dbrownell_parserlib-0.9.1}/src/dbrownell_ParserLib/location.py +0 -0
  16. {dbrownell_parserlib-0.8.4 → dbrownell_parserlib-0.9.1}/src/dbrownell_ParserLib/py.typed +0 -0
  17. {dbrownell_parserlib-0.8.4 → dbrownell_parserlib-0.9.1}/src/dbrownell_ParserLib/region.py +0 -0
  18. {dbrownell_parserlib-0.8.4 → dbrownell_parserlib-0.9.1}/src/dbrownell_ParserLib/terminal_expression.py +0 -0
  19. {dbrownell_parserlib-0.8.4 → dbrownell_parserlib-0.9.1}/src/dbrownell_ParserLib/visitors.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dbrownell-parserlib
3
- Version: 0.8.4
3
+ Version: 0.9.1
4
4
  Summary: Functionality useful when creating parsers.
5
5
  Author: Dave Brownell
6
6
  Author-email: Dave Brownell <github@DavidBrownell.com>
@@ -85,7 +85,7 @@ Use `BuildAntlrGrammar` to generate the lexer and parser:
85
85
  ```python
86
86
  from pathlib import Path
87
87
 
88
- from dbrownell_ParserLib import BuildAntlrGrammar
88
+ from dbrownell_ParserLib.antlr.build_antlr_grammar import BuildAntlrGrammar
89
89
 
90
90
 
91
91
  BuildAntlrGrammar(
@@ -101,11 +101,9 @@ Implement a visitor that converts ANTLR parse trees to Expression objects:
101
101
  ```python
102
102
  from dataclasses import dataclass
103
103
 
104
- from dbrownell_ParserLib import (
105
- Expression,
106
- InsignificantWhitespaceAntlrVisitorMixin,
107
- TerminalExpression,
108
- )
104
+ from dbrownell_ParserLib.antlr.antlr_visitor_mixins import InsignificantWhitespaceAntlrVisitorMixin
105
+ from dbrownell_ParserLib.expression import Expression
106
+ from dbrownell_ParserLib.terminal_expression import TerminalExpression
109
107
 
110
108
  from CalculatorVisitor import CalculatorVisitor as GeneratedVisitor
111
109
 
@@ -147,7 +145,7 @@ class CalculatorVisitor(InsignificantWhitespaceAntlrVisitorMixin, GeneratedVisit
147
145
  Use `CreateAntlrParser` to build a callable parser:
148
146
 
149
147
  ```python
150
- from dbrownell_ParserLib import CreateAntlrParser
148
+ from dbrownell_ParserLib.antlr.antlr_parser import CreateAntlrParser
151
149
 
152
150
 
153
151
  parser = CreateAntlrParser(
@@ -162,7 +160,7 @@ parser = CreateAntlrParser(
162
160
  Parse single files, multiple files, or entire workspaces:
163
161
 
164
162
  ```python
165
- from dbrownell_ParserLib import Error
163
+ from dbrownell_ParserLib.errors import Error
166
164
 
167
165
 
168
166
  # Parse a single file
@@ -187,7 +185,7 @@ Use the visitor pattern to process your AST:
187
185
  ```python
188
186
  from contextlib import contextmanager
189
187
 
190
- from dbrownell_ParserLib import ExpressionVisitorHelper, VisitResult
188
+ from dbrownell_ParserLib.visitors import ExpressionVisitorHelper, VisitResult
191
189
 
192
190
 
193
191
  class EvaluatorVisitor(ExpressionVisitorHelper):
@@ -63,7 +63,7 @@ Use `BuildAntlrGrammar` to generate the lexer and parser:
63
63
  ```python
64
64
  from pathlib import Path
65
65
 
66
- from dbrownell_ParserLib import BuildAntlrGrammar
66
+ from dbrownell_ParserLib.antlr.build_antlr_grammar import BuildAntlrGrammar
67
67
 
68
68
 
69
69
  BuildAntlrGrammar(
@@ -79,11 +79,9 @@ Implement a visitor that converts ANTLR parse trees to Expression objects:
79
79
  ```python
80
80
  from dataclasses import dataclass
81
81
 
82
- from dbrownell_ParserLib import (
83
- Expression,
84
- InsignificantWhitespaceAntlrVisitorMixin,
85
- TerminalExpression,
86
- )
82
+ from dbrownell_ParserLib.antlr.antlr_visitor_mixins import InsignificantWhitespaceAntlrVisitorMixin
83
+ from dbrownell_ParserLib.expression import Expression
84
+ from dbrownell_ParserLib.terminal_expression import TerminalExpression
87
85
 
88
86
  from CalculatorVisitor import CalculatorVisitor as GeneratedVisitor
89
87
 
@@ -125,7 +123,7 @@ class CalculatorVisitor(InsignificantWhitespaceAntlrVisitorMixin, GeneratedVisit
125
123
  Use `CreateAntlrParser` to build a callable parser:
126
124
 
127
125
  ```python
128
- from dbrownell_ParserLib import CreateAntlrParser
126
+ from dbrownell_ParserLib.antlr.antlr_parser import CreateAntlrParser
129
127
 
130
128
 
131
129
  parser = CreateAntlrParser(
@@ -140,7 +138,7 @@ parser = CreateAntlrParser(
140
138
  Parse single files, multiple files, or entire workspaces:
141
139
 
142
140
  ```python
143
- from dbrownell_ParserLib import Error
141
+ from dbrownell_ParserLib.errors import Error
144
142
 
145
143
 
146
144
  # Parse a single file
@@ -165,7 +163,7 @@ Use the visitor pattern to process your AST:
165
163
  ```python
166
164
  from contextlib import contextmanager
167
165
 
168
- from dbrownell_ParserLib import ExpressionVisitorHelper, VisitResult
166
+ from dbrownell_ParserLib.visitors import ExpressionVisitorHelper, VisitResult
169
167
 
170
168
 
171
169
  class EvaluatorVisitor(ExpressionVisitorHelper):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dbrownell-parserlib"
3
- version = "0.8.4"
3
+ version = "0.9.1"
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:
@@ -0,0 +1,6 @@
1
+ # noqa: D104
2
+
3
+ from importlib.metadata import version
4
+
5
+
6
+ __version__ = version("dbrownell_ParserLib")
@@ -17,7 +17,7 @@ from dbrownell_Common import ExecuteTasks
17
17
  from dbrownell_Common.Streams.DoneManager import DoneManager # noqa: TC002
18
18
  from plum import dispatch, overload
19
19
 
20
- from dbrownell_ParserLib.errors import Error, PythonError
20
+ from dbrownell_ParserLib.errors import Error, ErrorException, PythonError
21
21
  from dbrownell_ParserLib.region import Location, Region
22
22
 
23
23
 
@@ -25,17 +25,6 @@ from dbrownell_ParserLib.region import Location, Region
25
25
  # |
26
26
  # | Public Types
27
27
  # |
28
- # ----------------------------------------------------------------------
29
- class AntlrParserException(Exception): # noqa: N818
30
- """Exception raised by the ANTLR parser or a visitor processing the AST."""
31
-
32
- # ----------------------------------------------------------------------
33
- def __init__(self, error: Error) -> None:
34
- super().__init__(str(error))
35
-
36
- self.error = error
37
-
38
-
39
28
  # ----------------------------------------------------------------------
40
29
  class AntlrParser(ABC):
41
30
  """Abstract base class for parsers created with `CreateAntlrParser`."""
@@ -451,7 +440,7 @@ class _AntlrParser(AntlrParser):
451
440
 
452
441
  result = visitor
453
442
 
454
- except AntlrParserException as ex:
443
+ except ErrorException as ex:
455
444
  result = ex.error
456
445
  except Exception as ex:
457
446
  result = PythonError.Create(ex)
@@ -486,7 +475,7 @@ class _ErrorListener(antlr4.DiagnosticErrorListener):
486
475
 
487
476
  location = Location(line, column + 1)
488
477
  error = Error(f"Syntax error: {msg}", Region(self._source, location, location))
489
- ex = AntlrParserException(error)
478
+ ex = ErrorException(error)
490
479
 
491
480
  if e is None:
492
481
  raise ex
@@ -0,0 +1,232 @@
1
+ """Contains functionality associated with errors and exceptions."""
2
+
3
+ import re
4
+ import textwrap
5
+ import traceback
6
+
7
+ from abc import abstractmethod
8
+ from dataclasses import dataclass, field, InitVar, make_dataclass
9
+ from enum import Enum
10
+ from functools import cached_property, singledispatch
11
+ from io import StringIO
12
+ from pathlib import Path
13
+ from typing import Self
14
+
15
+ from dbrownell_ParserLib.region import Location, Region
16
+
17
+
18
+ # ----------------------------------------------------------------------
19
+ # |
20
+ # | Public Types
21
+ # |
22
+ # ----------------------------------------------------------------------
23
+ @dataclass(frozen=True)
24
+ class Error:
25
+ """Base class for all errors generated by this library."""
26
+
27
+ # ----------------------------------------------------------------------
28
+ message: str
29
+
30
+ region_or_regions: InitVar[Region | list[Region]]
31
+ regions: list[Region] = field(init=False)
32
+
33
+ # ----------------------------------------------------------------------
34
+ @classmethod
35
+ def CreateAsException(cls, *args, **kwargs) -> ErrorException:
36
+ """Create an `ErrorException` with an `Error` based on the provided arguments."""
37
+
38
+ return ErrorException(cls(*args, **kwargs))
39
+
40
+ # ----------------------------------------------------------------------
41
+ def __post_init__(self, region_or_regions: Region | list[Region]) -> None:
42
+ regions = [region_or_regions] if isinstance(region_or_regions, Region) else region_or_regions
43
+
44
+ if not regions:
45
+ msg = "At least one region must be provided"
46
+ raise ValueError(msg)
47
+
48
+ object.__setattr__(self, "regions", regions)
49
+
50
+ # ----------------------------------------------------------------------
51
+ def __str__(self) -> str:
52
+ return self._string
53
+
54
+ # ----------------------------------------------------------------------
55
+ # ----------------------------------------------------------------------
56
+ # ----------------------------------------------------------------------
57
+ @cached_property
58
+ def _string(self) -> str:
59
+ if len(self.regions) == 1 and "\n" not in self.message:
60
+ return f"{self.message} ({self.regions[0]})"
61
+
62
+ return textwrap.dedent(
63
+ """\
64
+ {}
65
+
66
+ {}
67
+ """,
68
+ ).format(self.message.rstrip(), "\n".join(f" - {region}" for region in self.regions))
69
+
70
+
71
+ # ----------------------------------------------------------------------
72
+ @dataclass(frozen=True)
73
+ class PythonError(Error):
74
+ """An `Error` based on a Python exception."""
75
+
76
+ # ----------------------------------------------------------------------
77
+ ex: Exception
78
+
79
+ # ----------------------------------------------------------------------
80
+ @classmethod
81
+ def Create(
82
+ cls,
83
+ ex: Exception,
84
+ ) -> Self:
85
+ """Create a `PythonError` from a Python exception."""
86
+
87
+ regions: list[Region] = []
88
+
89
+ # I haven't been able to find a reliable way to get the exception's callstack so it is being
90
+ # parsed manually here. There has got to be a better way to do this.
91
+ sink = StringIO()
92
+
93
+ traceback.print_exception(ex, file=sink)
94
+ sink_str = sink.getvalue()
95
+
96
+ regex = re.compile(r"^\s*File \"(?P<filename>.+?)\", line (?P<line>\d+),.+$", re.MULTILINE)
97
+
98
+ for line in sink_str.splitlines():
99
+ match = regex.match(line)
100
+ if not match:
101
+ continue
102
+
103
+ line_number = int(match.group("line"))
104
+
105
+ regions.append(
106
+ Region(
107
+ Path(match.group("filename")),
108
+ Location(line_number, 1),
109
+ Location(line_number, 1),
110
+ ),
111
+ )
112
+
113
+ regions.reverse()
114
+
115
+ return cls(
116
+ textwrap.dedent(
117
+ """\
118
+ Python Exception
119
+ ----------------
120
+ {}
121
+ """,
122
+ ).format(str(ex)),
123
+ regions,
124
+ ex,
125
+ )
126
+
127
+
128
+ # ----------------------------------------------------------------------
129
+ class ErrorException(Exception): # noqa: N818
130
+ """Python exception that contains an `Error` object."""
131
+
132
+ # ----------------------------------------------------------------------
133
+ def __init__(self, error: Error) -> None:
134
+ super().__init__(str(error))
135
+ self.error = error
136
+
137
+
138
+ # ----------------------------------------------------------------------
139
+ # |
140
+ # | Public Functions
141
+ # |
142
+ # ----------------------------------------------------------------------
143
+ class _CreatedError(Error):
144
+ """Abstract base class for errors created via `CreateErrorType`."""
145
+
146
+ # ----------------------------------------------------------------------
147
+ @classmethod
148
+ @abstractmethod
149
+ def Create(cls, region_or_regions: Region | list[Region], *args, **kwargs) -> Self: ...
150
+
151
+
152
+ # ----------------------------------------------------------------------
153
+ def CreateErrorType(
154
+ message_template: str,
155
+ **error_attributes: type,
156
+ ) -> type[_CreatedError]:
157
+ """Create a new `Error` type with the provided message template and attributes.
158
+
159
+ Example:
160
+ MyError = CreateErrorType(
161
+ "This is an error with attribute {attr1} and {attr2}",
162
+ attr1=str,
163
+ attr2=int,
164
+ )
165
+
166
+ """
167
+
168
+ dynamic_fields_class = make_dataclass(
169
+ "DynamicFields",
170
+ error_attributes.items(),
171
+ bases=(_CreatedError,),
172
+ frozen=True,
173
+ )
174
+
175
+ # ----------------------------------------------------------------------
176
+ @dataclass(frozen=True)
177
+ class NewError(dynamic_fields_class): # ty: ignore[unsupported-base]
178
+ # ----------------------------------------------------------------------
179
+ @classmethod
180
+ def Create(
181
+ cls,
182
+ region_or_regions: Region | list[Region],
183
+ *args,
184
+ **kwargs,
185
+ ) -> Self:
186
+ """Create a new instance of this error type."""
187
+
188
+ kvps = {k: _ArgToString(v) for k, v in zip(error_attributes.keys(), args, strict=False)}
189
+
190
+ for k, v in kwargs.items():
191
+ kvps[k] = _ArgToString(v)
192
+
193
+ return cls(
194
+ message_template.format(**kvps), # ty: ignore[too-many-positional-arguments]
195
+ region_or_regions,
196
+ *args,
197
+ **kwargs,
198
+ )
199
+
200
+ # ----------------------------------------------------------------------
201
+ @classmethod
202
+ def CreateAsException(cls, *args, **kwargs) -> ErrorException:
203
+ return ErrorException(cls.Create(*args, **kwargs))
204
+
205
+ # ----------------------------------------------------------------------
206
+
207
+ return NewError
208
+
209
+
210
+ # ----------------------------------------------------------------------
211
+ # |
212
+ # | Private Functions
213
+ # |
214
+ # ----------------------------------------------------------------------
215
+ @singledispatch
216
+ def _ArgToString(value: object) -> str:
217
+ return str(value)
218
+
219
+
220
+ @_ArgToString.register
221
+ def _(value: str) -> str:
222
+ return value
223
+
224
+
225
+ @_ArgToString.register
226
+ def _(value: Enum) -> str:
227
+ return value.name
228
+
229
+
230
+ @_ArgToString.register
231
+ def _(value: list) -> str:
232
+ return "[{}]".format(", ".join("'{}'".format(_ArgToString(v)) for v in value))
@@ -1,44 +0,0 @@
1
- # noqa: D104
2
-
3
- from importlib.metadata import version
4
-
5
- from dbrownell_ParserLib.antlr import (
6
- AntlrParser,
7
- AntlrParserException,
8
- AntlrVisitorMixinBase,
9
- BuildAntlrGrammar,
10
- CreateAntlrParser,
11
- InsignificantWhitespaceAntlrVisitorMixin,
12
- SignificantWhitespaceAntlrVisitorMixin,
13
- )
14
- from dbrownell_ParserLib.errors import Error, PythonError
15
- from dbrownell_ParserLib.expression import Expression
16
- from dbrownell_ParserLib.location import Location
17
- from dbrownell_ParserLib.region import Region
18
- from dbrownell_ParserLib.terminal_expression import TerminalExpression
19
- from dbrownell_ParserLib.visitors import ExpressionVisitor, ExpressionVisitorHelper, VisitResult
20
-
21
-
22
- # ----------------------------------------------------------------------
23
- __all__ = [
24
- "AntlrParser",
25
- "AntlrParserException",
26
- "AntlrVisitorMixinBase",
27
- "BuildAntlrGrammar",
28
- "CreateAntlrParser",
29
- "Error",
30
- "Expression",
31
- "ExpressionVisitor",
32
- "ExpressionVisitorHelper",
33
- "InsignificantWhitespaceAntlrVisitorMixin",
34
- "Location",
35
- "PythonError",
36
- "Region",
37
- "SignificantWhitespaceAntlrVisitorMixin",
38
- "TerminalExpression",
39
- "VisitResult",
40
- "__version__",
41
- ]
42
-
43
-
44
- __version__ = version("dbrownell_ParserLib")
@@ -1,20 +0,0 @@
1
- # noqa: D104
2
- from dbrownell_ParserLib.antlr.build_antlr_grammar import BuildAntlrGrammar
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
- )
9
-
10
-
11
- # ----------------------------------------------------------------------
12
- __all__ = [
13
- "AntlrParser",
14
- "AntlrParserException",
15
- "AntlrVisitorMixinBase",
16
- "BuildAntlrGrammar",
17
- "CreateAntlrParser",
18
- "InsignificantWhitespaceAntlrVisitorMixin",
19
- "SignificantWhitespaceAntlrVisitorMixin",
20
- ]
@@ -1,112 +0,0 @@
1
- """Contains functionality associated with errors and exceptions."""
2
-
3
- import re
4
- import textwrap
5
- import traceback
6
-
7
- from dataclasses import dataclass, field, InitVar
8
- from functools import cached_property
9
- from io import StringIO
10
- from pathlib import Path
11
- from typing import Self
12
-
13
- from dbrownell_ParserLib.region import Location, Region
14
-
15
-
16
- # ----------------------------------------------------------------------
17
- @dataclass(frozen=True)
18
- class Error:
19
- """Base class for all errors generated by this library."""
20
-
21
- # ----------------------------------------------------------------------
22
- message: str
23
-
24
- region_or_regions: InitVar[Region | list[Region]]
25
- regions: list[Region] = field(init=False)
26
-
27
- # ----------------------------------------------------------------------
28
- def __post_init__(self, region_or_regions: Region | list[Region]) -> None:
29
- regions = [region_or_regions] if isinstance(region_or_regions, Region) else region_or_regions
30
-
31
- if not regions:
32
- msg = "At least one region must be provided"
33
- raise ValueError(msg)
34
-
35
- object.__setattr__(self, "regions", regions)
36
-
37
- # ----------------------------------------------------------------------
38
- def __str__(self) -> str:
39
- return self._string
40
-
41
- # ----------------------------------------------------------------------
42
- # ----------------------------------------------------------------------
43
- # ----------------------------------------------------------------------
44
- @cached_property
45
- def _string(self) -> str:
46
- if len(self.regions) == 1 and "\n" not in self.message:
47
- return f"{self.message} ({self.regions[0]})"
48
-
49
- return textwrap.dedent(
50
- """\
51
- {}
52
-
53
- {}
54
- """,
55
- ).format(self.message.rstrip(), "\n".join(f" - {region}" for region in self.regions))
56
-
57
-
58
- # ----------------------------------------------------------------------
59
- @dataclass(frozen=True)
60
- class PythonError(Error):
61
- """An `Error` based on a Python exception."""
62
-
63
- # ----------------------------------------------------------------------
64
- ex: Exception
65
-
66
- # ----------------------------------------------------------------------
67
- @classmethod
68
- def Create(
69
- cls,
70
- ex: Exception,
71
- ) -> Self:
72
- """Create a `PythonError` from a Python exception."""
73
-
74
- regions: list[Region] = []
75
-
76
- # I haven't been able to find a reliable way to get the exception's callstack so it is being
77
- # parsed manually here. There has got to be a better way to do this.
78
- sink = StringIO()
79
-
80
- traceback.print_exception(ex, file=sink)
81
- sink_str = sink.getvalue()
82
-
83
- regex = re.compile(r"^\s*File \"(?P<filename>.+?)\", line (?P<line>\d+),.+$", re.MULTILINE)
84
-
85
- for line in sink_str.splitlines():
86
- match = regex.match(line)
87
- if not match:
88
- continue
89
-
90
- line_number = int(match.group("line"))
91
-
92
- regions.append(
93
- Region(
94
- Path(match.group("filename")),
95
- Location(line_number, 1),
96
- Location(line_number, 1),
97
- ),
98
- )
99
-
100
- regions.reverse()
101
-
102
- return cls(
103
- textwrap.dedent(
104
- """\
105
- Python Exception
106
- ----------------
107
- {}
108
- """,
109
- ).format(str(ex)),
110
- regions,
111
- ex,
112
- )