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.
Files changed (17) hide show
  1. {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/PKG-INFO +31 -26
  2. {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/README.md +30 -25
  3. {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/pyproject.toml +1 -1
  4. {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/__init__.py +3 -3
  5. {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/antlr/__init__.py +1 -2
  6. {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/antlr/antlr_parser.py +3 -14
  7. dbrownell_parserlib-0.9.0/src/dbrownell_ParserLib/errors.py +232 -0
  8. dbrownell_parserlib-0.8.3/src/dbrownell_ParserLib/errors.py +0 -112
  9. {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/antlr/antlr_visitor_mixins.py +0 -0
  10. {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/antlr/build_antlr_grammar/__init__.py +0 -0
  11. {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
  12. {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/expression.py +0 -0
  13. {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/location.py +0 -0
  14. {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/py.typed +0 -0
  15. {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/region.py +0 -0
  16. {dbrownell_parserlib-0.8.3 → dbrownell_parserlib-0.9.0}/src/dbrownell_ParserLib/terminal_expression.py +0 -0
  17. {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.8.3
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 op=('*'|'/') expr # BinaryOp
72
- | expr op=('+'|'-') expr # BinaryOp
73
- | INT # Number
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
- def visitBinaryOp(self, ctx):
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.INT().getText()))
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
- @contextmanager
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 op=('*'|'/') expr # BinaryOp
50
- | expr op=('+'|'-') expr # BinaryOp
51
- | INT # Number
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
- def visitBinaryOp(self, ctx):
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.INT().getText()))
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
- @contextmanager
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.8.3"
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",
@@ -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, AntlrParserException, CreateAntlrParser
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 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,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
- )