dbrownell-parserlib 0.8.3__tar.gz → 0.9.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.
- {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/PKG-INFO +31 -26
- {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/README.md +30 -25
- {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/pyproject.toml +1 -1
- {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/__init__.py +3 -3
- {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/antlr/__init__.py +1 -2
- {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/antlr/antlr_parser.py +3 -14
- dbrownell_parserlib-0.9.0/src/dbrownell_ParserLib/errors.py +232 -0
- dbrownell_parserlib-0.8.3/src/dbrownell_ParserLib/errors.py +0 -112
- {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/antlr/antlr_visitor_mixins.py +0 -0
- {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/antlr/build_antlr_grammar/__init__.py +0 -0
- {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/antlr/build_antlr_grammar/antlr-4.13.2-complete.jar +0 -0
- {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/expression.py +0 -0
- {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/location.py +0 -0
- {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/py.typed +0 -0
- {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/region.py +0 -0
- {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/terminal_expression.py +0 -0
- {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/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.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Functionality useful when creating parsers.
|
|
5
5
|
Author: Dave Brownell
|
|
6
6
|
Author-email: Dave Brownell <github@DavidBrownell.com>
|
|
@@ -61,16 +61,18 @@ Description-Content-Type: text/markdown
|
|
|
61
61
|
|
|
62
62
|
### How to use `dbrownell_ParserLib`
|
|
63
63
|
|
|
64
|
-
The typical workflow for creating a parser with this library involves
|
|
64
|
+
The typical workflow for creating a parser with this library involves the following steps. For detailed examples, see the test files in the repository, particularly the end-to-end tests that demonstrate complete parsing workflows.
|
|
65
|
+
|
|
65
66
|
|
|
66
67
|
#### 1. Define an ANTLR Grammar
|
|
67
68
|
Create a `.g4` grammar file defining your language syntax:
|
|
69
|
+
|
|
68
70
|
```antlr
|
|
69
71
|
grammar Calculator;
|
|
70
72
|
|
|
71
|
-
expr : expr
|
|
72
|
-
| expr
|
|
73
|
-
| INT
|
|
73
|
+
expr : expr ('*'|'/') expr # BinaryOp
|
|
74
|
+
| expr ('+'|'-') expr # BinaryOp
|
|
75
|
+
| INT # Number
|
|
74
76
|
;
|
|
75
77
|
|
|
76
78
|
INT : [0-9]+ ;
|
|
@@ -79,10 +81,13 @@ WS : [ \t\n\r]+ -> skip ;
|
|
|
79
81
|
|
|
80
82
|
#### 2. Build the Grammar
|
|
81
83
|
Use `BuildAntlrGrammar` to generate the lexer and parser:
|
|
84
|
+
|
|
82
85
|
```python
|
|
83
|
-
from dbrownell_ParserLib import BuildAntlrGrammar
|
|
84
86
|
from pathlib import Path
|
|
85
87
|
|
|
88
|
+
from dbrownell_ParserLib import BuildAntlrGrammar
|
|
89
|
+
|
|
90
|
+
|
|
86
91
|
BuildAntlrGrammar(
|
|
87
92
|
dm, # DoneManager instance
|
|
88
93
|
Path("Calculator.g4"),
|
|
@@ -92,18 +97,19 @@ BuildAntlrGrammar(
|
|
|
92
97
|
|
|
93
98
|
#### 3. Create Custom Visitor
|
|
94
99
|
Implement a visitor that converts ANTLR parse trees to Expression objects:
|
|
100
|
+
|
|
95
101
|
```python
|
|
102
|
+
from dataclasses import dataclass
|
|
103
|
+
|
|
96
104
|
from dbrownell_ParserLib import (
|
|
97
105
|
Expression,
|
|
98
106
|
InsignificantWhitespaceAntlrVisitorMixin,
|
|
99
107
|
TerminalExpression,
|
|
100
108
|
)
|
|
101
|
-
from dataclasses import dataclass
|
|
102
109
|
|
|
103
110
|
from CalculatorVisitor import CalculatorVisitor as GeneratedVisitor
|
|
104
111
|
|
|
105
112
|
|
|
106
|
-
|
|
107
113
|
@dataclass(eq=False)
|
|
108
114
|
class BinaryExpression(Expression):
|
|
109
115
|
left: Expression
|
|
@@ -115,30 +121,35 @@ class BinaryExpression(Expression):
|
|
|
115
121
|
yield "operator", self.operator
|
|
116
122
|
yield "right", self.right
|
|
117
123
|
|
|
118
|
-
class CalculatorVisitor(InsignificantWhitespaceAntlrVisitorMixin, GeneratedVisitor):
|
|
119
124
|
|
|
120
|
-
|
|
125
|
+
class CalculatorVisitor(InsignificantWhitespaceAntlrVisitorMixin, GeneratedVisitor):
|
|
126
|
+
def visitExpr(self, ctx):
|
|
121
127
|
children = self.GetChildren(ctx)
|
|
128
|
+
assert len(children) == 2, children
|
|
129
|
+
|
|
122
130
|
self._stack.append(
|
|
123
131
|
BinaryExpression(
|
|
124
132
|
self.CreateRegion(ctx),
|
|
125
133
|
children[0],
|
|
126
|
-
TerminalExpression[str](self.CreateRegion(ctx.children[1]), ctx.children[1].getText()),
|
|
134
|
+
TerminalExpression[str](self.CreateRegion(ctx.children[1]), ctx.children[1].getText()),
|
|
135
|
+
|
|
127
136
|
children[1],
|
|
128
137
|
)
|
|
129
138
|
)
|
|
130
139
|
|
|
131
140
|
def visitNumber(self, ctx):
|
|
132
141
|
self._stack.append(
|
|
133
|
-
TerminalExpression(self.CreateRegion(ctx), int(ctx.
|
|
142
|
+
TerminalExpression(self.CreateRegion(ctx), int(ctx.getText()))
|
|
134
143
|
)
|
|
135
144
|
```
|
|
136
145
|
|
|
137
146
|
#### 4. Create Parser Instance
|
|
138
147
|
Use `CreateAntlrParser` to build a callable parser:
|
|
148
|
+
|
|
139
149
|
```python
|
|
140
150
|
from dbrownell_ParserLib import CreateAntlrParser
|
|
141
151
|
|
|
152
|
+
|
|
142
153
|
parser = CreateAntlrParser(
|
|
143
154
|
CalculatorLexer,
|
|
144
155
|
CalculatorParser,
|
|
@@ -146,15 +157,14 @@ parser = CreateAntlrParser(
|
|
|
146
157
|
lambda p: p.expr(), # Entry point rule
|
|
147
158
|
)
|
|
148
159
|
```
|
|
149
|
-
from pathlib import Path
|
|
150
|
-
|
|
151
|
-
from dbrownell_ParserLib import Error
|
|
152
|
-
|
|
153
|
-
|
|
154
160
|
|
|
155
161
|
#### 5. Parse Files
|
|
156
162
|
Parse single files, multiple files, or entire workspaces:
|
|
163
|
+
|
|
157
164
|
```python
|
|
165
|
+
from dbrownell_ParserLib import Error
|
|
166
|
+
|
|
167
|
+
|
|
158
168
|
# Parse a single file
|
|
159
169
|
results = parser(dm, Path("input.txt"), None)
|
|
160
170
|
|
|
@@ -171,30 +181,25 @@ for filepath, result in results.items():
|
|
|
171
181
|
# Process the AST
|
|
172
182
|
```
|
|
173
183
|
|
|
174
|
-
from contextlib import contextmanager
|
|
175
|
-
|
|
176
184
|
#### 6. Traverse the AST
|
|
177
185
|
Use the visitor pattern to process your AST:
|
|
186
|
+
|
|
178
187
|
```python
|
|
179
|
-
|
|
188
|
+
from contextlib import contextmanager
|
|
180
189
|
|
|
181
190
|
from dbrownell_ParserLib import ExpressionVisitorHelper, VisitResult
|
|
182
191
|
|
|
192
|
+
|
|
183
193
|
class EvaluatorVisitor(ExpressionVisitorHelper):
|
|
184
|
-
def OnBinaryExpression(self, expression):
|
|
185
194
|
@contextmanager
|
|
186
|
-
|
|
195
|
+
def OnBinaryExpression(self, expression):
|
|
187
196
|
# Process binary expressions
|
|
188
197
|
yield VisitResult.Continue
|
|
189
198
|
|
|
190
|
-
# Process terminal values
|
|
191
|
-
yield VisitResult.Continue
|
|
192
199
|
|
|
193
200
|
ast.Accept(EvaluatorVisitor())
|
|
194
201
|
```
|
|
195
202
|
|
|
196
|
-
For detailed examples, see the test files in the repository, particularly the end-to-end tests that demonstrate complete parsing workflows.
|
|
197
|
-
|
|
198
203
|
<!-- Content below this delimiter will be copied to the generated README.md file. DO NOT REMOVE THIS COMMENT, as it will cause regeneration to fail. -->
|
|
199
204
|
|
|
200
205
|
## Installation
|
|
@@ -39,16 +39,18 @@
|
|
|
39
39
|
|
|
40
40
|
### How to use `dbrownell_ParserLib`
|
|
41
41
|
|
|
42
|
-
The typical workflow for creating a parser with this library involves
|
|
42
|
+
The typical workflow for creating a parser with this library involves the following steps. For detailed examples, see the test files in the repository, particularly the end-to-end tests that demonstrate complete parsing workflows.
|
|
43
|
+
|
|
43
44
|
|
|
44
45
|
#### 1. Define an ANTLR Grammar
|
|
45
46
|
Create a `.g4` grammar file defining your language syntax:
|
|
47
|
+
|
|
46
48
|
```antlr
|
|
47
49
|
grammar Calculator;
|
|
48
50
|
|
|
49
|
-
expr : expr
|
|
50
|
-
| expr
|
|
51
|
-
| INT
|
|
51
|
+
expr : expr ('*'|'/') expr # BinaryOp
|
|
52
|
+
| expr ('+'|'-') expr # BinaryOp
|
|
53
|
+
| INT # Number
|
|
52
54
|
;
|
|
53
55
|
|
|
54
56
|
INT : [0-9]+ ;
|
|
@@ -57,10 +59,13 @@ WS : [ \t\n\r]+ -> skip ;
|
|
|
57
59
|
|
|
58
60
|
#### 2. Build the Grammar
|
|
59
61
|
Use `BuildAntlrGrammar` to generate the lexer and parser:
|
|
62
|
+
|
|
60
63
|
```python
|
|
61
|
-
from dbrownell_ParserLib import BuildAntlrGrammar
|
|
62
64
|
from pathlib import Path
|
|
63
65
|
|
|
66
|
+
from dbrownell_ParserLib import BuildAntlrGrammar
|
|
67
|
+
|
|
68
|
+
|
|
64
69
|
BuildAntlrGrammar(
|
|
65
70
|
dm, # DoneManager instance
|
|
66
71
|
Path("Calculator.g4"),
|
|
@@ -70,18 +75,19 @@ BuildAntlrGrammar(
|
|
|
70
75
|
|
|
71
76
|
#### 3. Create Custom Visitor
|
|
72
77
|
Implement a visitor that converts ANTLR parse trees to Expression objects:
|
|
78
|
+
|
|
73
79
|
```python
|
|
80
|
+
from dataclasses import dataclass
|
|
81
|
+
|
|
74
82
|
from dbrownell_ParserLib import (
|
|
75
83
|
Expression,
|
|
76
84
|
InsignificantWhitespaceAntlrVisitorMixin,
|
|
77
85
|
TerminalExpression,
|
|
78
86
|
)
|
|
79
|
-
from dataclasses import dataclass
|
|
80
87
|
|
|
81
88
|
from CalculatorVisitor import CalculatorVisitor as GeneratedVisitor
|
|
82
89
|
|
|
83
90
|
|
|
84
|
-
|
|
85
91
|
@dataclass(eq=False)
|
|
86
92
|
class BinaryExpression(Expression):
|
|
87
93
|
left: Expression
|
|
@@ -93,30 +99,35 @@ class BinaryExpression(Expression):
|
|
|
93
99
|
yield "operator", self.operator
|
|
94
100
|
yield "right", self.right
|
|
95
101
|
|
|
96
|
-
class CalculatorVisitor(InsignificantWhitespaceAntlrVisitorMixin, GeneratedVisitor):
|
|
97
102
|
|
|
98
|
-
|
|
103
|
+
class CalculatorVisitor(InsignificantWhitespaceAntlrVisitorMixin, GeneratedVisitor):
|
|
104
|
+
def visitExpr(self, ctx):
|
|
99
105
|
children = self.GetChildren(ctx)
|
|
106
|
+
assert len(children) == 2, children
|
|
107
|
+
|
|
100
108
|
self._stack.append(
|
|
101
109
|
BinaryExpression(
|
|
102
110
|
self.CreateRegion(ctx),
|
|
103
111
|
children[0],
|
|
104
|
-
TerminalExpression[str](self.CreateRegion(ctx.children[1]), ctx.children[1].getText()),
|
|
112
|
+
TerminalExpression[str](self.CreateRegion(ctx.children[1]), ctx.children[1].getText()),
|
|
113
|
+
|
|
105
114
|
children[1],
|
|
106
115
|
)
|
|
107
116
|
)
|
|
108
117
|
|
|
109
118
|
def visitNumber(self, ctx):
|
|
110
119
|
self._stack.append(
|
|
111
|
-
TerminalExpression(self.CreateRegion(ctx), int(ctx.
|
|
120
|
+
TerminalExpression(self.CreateRegion(ctx), int(ctx.getText()))
|
|
112
121
|
)
|
|
113
122
|
```
|
|
114
123
|
|
|
115
124
|
#### 4. Create Parser Instance
|
|
116
125
|
Use `CreateAntlrParser` to build a callable parser:
|
|
126
|
+
|
|
117
127
|
```python
|
|
118
128
|
from dbrownell_ParserLib import CreateAntlrParser
|
|
119
129
|
|
|
130
|
+
|
|
120
131
|
parser = CreateAntlrParser(
|
|
121
132
|
CalculatorLexer,
|
|
122
133
|
CalculatorParser,
|
|
@@ -124,15 +135,14 @@ parser = CreateAntlrParser(
|
|
|
124
135
|
lambda p: p.expr(), # Entry point rule
|
|
125
136
|
)
|
|
126
137
|
```
|
|
127
|
-
from pathlib import Path
|
|
128
|
-
|
|
129
|
-
from dbrownell_ParserLib import Error
|
|
130
|
-
|
|
131
|
-
|
|
132
138
|
|
|
133
139
|
#### 5. Parse Files
|
|
134
140
|
Parse single files, multiple files, or entire workspaces:
|
|
141
|
+
|
|
135
142
|
```python
|
|
143
|
+
from dbrownell_ParserLib import Error
|
|
144
|
+
|
|
145
|
+
|
|
136
146
|
# Parse a single file
|
|
137
147
|
results = parser(dm, Path("input.txt"), None)
|
|
138
148
|
|
|
@@ -149,30 +159,25 @@ for filepath, result in results.items():
|
|
|
149
159
|
# Process the AST
|
|
150
160
|
```
|
|
151
161
|
|
|
152
|
-
from contextlib import contextmanager
|
|
153
|
-
|
|
154
162
|
#### 6. Traverse the AST
|
|
155
163
|
Use the visitor pattern to process your AST:
|
|
164
|
+
|
|
156
165
|
```python
|
|
157
|
-
|
|
166
|
+
from contextlib import contextmanager
|
|
158
167
|
|
|
159
168
|
from dbrownell_ParserLib import ExpressionVisitorHelper, VisitResult
|
|
160
169
|
|
|
170
|
+
|
|
161
171
|
class EvaluatorVisitor(ExpressionVisitorHelper):
|
|
162
|
-
def OnBinaryExpression(self, expression):
|
|
163
172
|
@contextmanager
|
|
164
|
-
|
|
173
|
+
def OnBinaryExpression(self, expression):
|
|
165
174
|
# Process binary expressions
|
|
166
175
|
yield VisitResult.Continue
|
|
167
176
|
|
|
168
|
-
# Process terminal values
|
|
169
|
-
yield VisitResult.Continue
|
|
170
177
|
|
|
171
178
|
ast.Accept(EvaluatorVisitor())
|
|
172
179
|
```
|
|
173
180
|
|
|
174
|
-
For detailed examples, see the test files in the repository, particularly the end-to-end tests that demonstrate complete parsing workflows.
|
|
175
|
-
|
|
176
181
|
<!-- Content below this delimiter will be copied to the generated README.md file. DO NOT REMOVE THIS COMMENT, as it will cause regeneration to fail. -->
|
|
177
182
|
|
|
178
183
|
## Installation
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dbrownell-parserlib"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.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:
|
|
@@ -4,14 +4,13 @@ from importlib.metadata import version
|
|
|
4
4
|
|
|
5
5
|
from dbrownell_ParserLib.antlr import (
|
|
6
6
|
AntlrParser,
|
|
7
|
-
AntlrParserException,
|
|
8
7
|
AntlrVisitorMixinBase,
|
|
9
8
|
BuildAntlrGrammar,
|
|
10
9
|
CreateAntlrParser,
|
|
11
10
|
InsignificantWhitespaceAntlrVisitorMixin,
|
|
12
11
|
SignificantWhitespaceAntlrVisitorMixin,
|
|
13
12
|
)
|
|
14
|
-
from dbrownell_ParserLib.errors import Error, PythonError
|
|
13
|
+
from dbrownell_ParserLib.errors import CreateErrorType, Error, ErrorException, PythonError
|
|
15
14
|
from dbrownell_ParserLib.expression import Expression
|
|
16
15
|
from dbrownell_ParserLib.location import Location
|
|
17
16
|
from dbrownell_ParserLib.region import Region
|
|
@@ -22,11 +21,12 @@ from dbrownell_ParserLib.visitors import ExpressionVisitor, ExpressionVisitorHel
|
|
|
22
21
|
# ----------------------------------------------------------------------
|
|
23
22
|
__all__ = [
|
|
24
23
|
"AntlrParser",
|
|
25
|
-
"AntlrParserException",
|
|
26
24
|
"AntlrVisitorMixinBase",
|
|
27
25
|
"BuildAntlrGrammar",
|
|
28
26
|
"CreateAntlrParser",
|
|
27
|
+
"CreateErrorType",
|
|
29
28
|
"Error",
|
|
29
|
+
"ErrorException",
|
|
30
30
|
"Expression",
|
|
31
31
|
"ExpressionVisitor",
|
|
32
32
|
"ExpressionVisitorHelper",
|
{dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/antlr/__init__.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# noqa: D104
|
|
2
2
|
from dbrownell_ParserLib.antlr.build_antlr_grammar import BuildAntlrGrammar
|
|
3
|
-
from dbrownell_ParserLib.antlr.antlr_parser import AntlrParser,
|
|
3
|
+
from dbrownell_ParserLib.antlr.antlr_parser import AntlrParser, CreateAntlrParser
|
|
4
4
|
from dbrownell_ParserLib.antlr.antlr_visitor_mixins import (
|
|
5
5
|
AntlrVisitorMixinBase,
|
|
6
6
|
InsignificantWhitespaceAntlrVisitorMixin,
|
|
@@ -11,7 +11,6 @@ from dbrownell_ParserLib.antlr.antlr_visitor_mixins import (
|
|
|
11
11
|
# ----------------------------------------------------------------------
|
|
12
12
|
__all__ = [
|
|
13
13
|
"AntlrParser",
|
|
14
|
-
"AntlrParserException",
|
|
15
14
|
"AntlrVisitorMixinBase",
|
|
16
15
|
"BuildAntlrGrammar",
|
|
17
16
|
"CreateAntlrParser",
|
|
@@ -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
|
|
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 =
|
|
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,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
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/expression.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|