dbrownell-parserlib 0.8.1__tar.gz → 0.8.3__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 (16) hide show
  1. {dbrownell_parserlib-0.8.1 → dbrownell_parserlib-0.8.3}/PKG-INFO +146 -3
  2. {dbrownell_parserlib-0.8.1 → dbrownell_parserlib-0.8.3}/README.md +145 -2
  3. {dbrownell_parserlib-0.8.1 → dbrownell_parserlib-0.8.3}/pyproject.toml +1 -1
  4. {dbrownell_parserlib-0.8.1 → dbrownell_parserlib-0.8.3}/src/dbrownell_ParserLib/antlr/antlr_parser.py +1 -1
  5. {dbrownell_parserlib-0.8.1 → dbrownell_parserlib-0.8.3}/src/dbrownell_ParserLib/antlr/antlr_visitor_mixins.py +57 -72
  6. {dbrownell_parserlib-0.8.1 → dbrownell_parserlib-0.8.3}/src/dbrownell_ParserLib/__init__.py +0 -0
  7. {dbrownell_parserlib-0.8.1 → dbrownell_parserlib-0.8.3}/src/dbrownell_ParserLib/antlr/__init__.py +0 -0
  8. {dbrownell_parserlib-0.8.1 → dbrownell_parserlib-0.8.3}/src/dbrownell_ParserLib/antlr/build_antlr_grammar/__init__.py +0 -0
  9. {dbrownell_parserlib-0.8.1 → dbrownell_parserlib-0.8.3}/src/dbrownell_ParserLib/antlr/build_antlr_grammar/antlr-4.13.2-complete.jar +0 -0
  10. {dbrownell_parserlib-0.8.1 → dbrownell_parserlib-0.8.3}/src/dbrownell_ParserLib/errors.py +0 -0
  11. {dbrownell_parserlib-0.8.1 → dbrownell_parserlib-0.8.3}/src/dbrownell_ParserLib/expression.py +0 -0
  12. {dbrownell_parserlib-0.8.1 → dbrownell_parserlib-0.8.3}/src/dbrownell_ParserLib/location.py +0 -0
  13. {dbrownell_parserlib-0.8.1 → dbrownell_parserlib-0.8.3}/src/dbrownell_ParserLib/py.typed +0 -0
  14. {dbrownell_parserlib-0.8.1 → dbrownell_parserlib-0.8.3}/src/dbrownell_ParserLib/region.py +0 -0
  15. {dbrownell_parserlib-0.8.1 → dbrownell_parserlib-0.8.3}/src/dbrownell_ParserLib/terminal_expression.py +0 -0
  16. {dbrownell_parserlib-0.8.1 → dbrownell_parserlib-0.8.3}/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.1
3
+ Version: 0.8.3
4
4
  Summary: Functionality useful when creating parsers.
5
5
  Author: Dave Brownell
6
6
  Author-email: Dave Brownell <github@DavidBrownell.com>
@@ -47,10 +47,153 @@ Description-Content-Type: text/markdown
47
47
  - [License](#license)
48
48
 
49
49
  ## Overview
50
- TODO: Complete this section
50
+ `dbrownell_ParserLib` is a Python library that provides utility functionality for creating parsers, with a primary focus on [ANTLR](https://antlr.org) (Another Tool for Language Recognition) integration. The library simplifies the process of building parsers that produce strongly-typed abstract syntax trees (ASTs) with comprehensive source location tracking.
51
+
52
+ ### Key Features
53
+ - **ANTLR Integration**: Streamlined wrapper around ANTLR for Python, handling grammar compilation and parser generation
54
+ - **AST Representation**: Expression-based AST nodes with automatic parent-child relationship management
55
+ - **Source Location Tracking**: Every AST node tracks its source file location (filename, line, column ranges) for detailed error reporting
56
+ - **Visitor Pattern**: Flexible visitor pattern implementation with fine-grained traversal control
57
+ - **Error Handling**: Rich error objects that associate messages with source locations and extract Python exception tracebacks
58
+ - **Multi-threaded Parsing**: Built-in support for parsing multiple files concurrently
59
+ - **Workspace Support**: Parse entire directory trees with customizable file filtering
60
+ - **Whitespace Handling**: Mixins for both insignificant and significant whitespace grammars (e.g., Python-like indentation)
51
61
 
52
62
  ### How to use `dbrownell_ParserLib`
53
- TODO: Complete this section
63
+
64
+ The typical workflow for creating a parser with this library involves:
65
+
66
+ #### 1. Define an ANTLR Grammar
67
+ Create a `.g4` grammar file defining your language syntax:
68
+ ```antlr
69
+ grammar Calculator;
70
+
71
+ expr : expr op=('*'|'/') expr # BinaryOp
72
+ | expr op=('+'|'-') expr # BinaryOp
73
+ | INT # Number
74
+ ;
75
+
76
+ INT : [0-9]+ ;
77
+ WS : [ \t\n\r]+ -> skip ;
78
+ ```
79
+
80
+ #### 2. Build the Grammar
81
+ Use `BuildAntlrGrammar` to generate the lexer and parser:
82
+ ```python
83
+ from dbrownell_ParserLib import BuildAntlrGrammar
84
+ from pathlib import Path
85
+
86
+ BuildAntlrGrammar(
87
+ dm, # DoneManager instance
88
+ Path("Calculator.g4"),
89
+ Path("output_dir"),
90
+ )
91
+ ```
92
+
93
+ #### 3. Create Custom Visitor
94
+ Implement a visitor that converts ANTLR parse trees to Expression objects:
95
+ ```python
96
+ from dbrownell_ParserLib import (
97
+ Expression,
98
+ InsignificantWhitespaceAntlrVisitorMixin,
99
+ TerminalExpression,
100
+ )
101
+ from dataclasses import dataclass
102
+
103
+ from CalculatorVisitor import CalculatorVisitor as GeneratedVisitor
104
+
105
+
106
+
107
+ @dataclass(eq=False)
108
+ class BinaryExpression(Expression):
109
+ left: Expression
110
+ operator: TerminalExpression[str]
111
+ right: Expression
112
+
113
+ def _GenerateAcceptDetails(self):
114
+ yield "left", self.left
115
+ yield "operator", self.operator
116
+ yield "right", self.right
117
+
118
+ class CalculatorVisitor(InsignificantWhitespaceAntlrVisitorMixin, GeneratedVisitor):
119
+
120
+ def visitBinaryOp(self, ctx):
121
+ children = self.GetChildren(ctx)
122
+ self._stack.append(
123
+ BinaryExpression(
124
+ self.CreateRegion(ctx),
125
+ children[0],
126
+ TerminalExpression[str](self.CreateRegion(ctx.children[1]), ctx.children[1].getText()),
127
+ children[1],
128
+ )
129
+ )
130
+
131
+ def visitNumber(self, ctx):
132
+ self._stack.append(
133
+ TerminalExpression(self.CreateRegion(ctx), int(ctx.INT().getText()))
134
+ )
135
+ ```
136
+
137
+ #### 4. Create Parser Instance
138
+ Use `CreateAntlrParser` to build a callable parser:
139
+ ```python
140
+ from dbrownell_ParserLib import CreateAntlrParser
141
+
142
+ parser = CreateAntlrParser(
143
+ CalculatorLexer,
144
+ CalculatorParser,
145
+ CalculatorVisitor,
146
+ lambda p: p.expr(), # Entry point rule
147
+ )
148
+ ```
149
+ from pathlib import Path
150
+
151
+ from dbrownell_ParserLib import Error
152
+
153
+
154
+
155
+ #### 5. Parse Files
156
+ Parse single files, multiple files, or entire workspaces:
157
+ ```python
158
+ # Parse a single file
159
+ results = parser(dm, Path("input.txt"), None)
160
+
161
+ # Parse multiple files
162
+ results = parser(dm, [Path("file1.txt"), Path("file2.txt")], None)
163
+
164
+ # Check results
165
+ for filepath, result in results.items():
166
+ if isinstance(result, Error):
167
+ print(f"Parse error in {filepath}: {result}")
168
+ else:
169
+ # result is the visitor containing the parsed AST
170
+ ast = result._stack[0]
171
+ # Process the AST
172
+ ```
173
+
174
+ from contextlib import contextmanager
175
+
176
+ #### 6. Traverse the AST
177
+ Use the visitor pattern to process your AST:
178
+ ```python
179
+ @contextmanager
180
+
181
+ from dbrownell_ParserLib import ExpressionVisitorHelper, VisitResult
182
+
183
+ class EvaluatorVisitor(ExpressionVisitorHelper):
184
+ def OnBinaryExpression(self, expression):
185
+ @contextmanager
186
+
187
+ # Process binary expressions
188
+ yield VisitResult.Continue
189
+
190
+ # Process terminal values
191
+ yield VisitResult.Continue
192
+
193
+ ast.Accept(EvaluatorVisitor())
194
+ ```
195
+
196
+ For detailed examples, see the test files in the repository, particularly the end-to-end tests that demonstrate complete parsing workflows.
54
197
 
55
198
  <!-- 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. -->
56
199
 
@@ -25,10 +25,153 @@
25
25
  - [License](#license)
26
26
 
27
27
  ## Overview
28
- TODO: Complete this section
28
+ `dbrownell_ParserLib` is a Python library that provides utility functionality for creating parsers, with a primary focus on [ANTLR](https://antlr.org) (Another Tool for Language Recognition) integration. The library simplifies the process of building parsers that produce strongly-typed abstract syntax trees (ASTs) with comprehensive source location tracking.
29
+
30
+ ### Key Features
31
+ - **ANTLR Integration**: Streamlined wrapper around ANTLR for Python, handling grammar compilation and parser generation
32
+ - **AST Representation**: Expression-based AST nodes with automatic parent-child relationship management
33
+ - **Source Location Tracking**: Every AST node tracks its source file location (filename, line, column ranges) for detailed error reporting
34
+ - **Visitor Pattern**: Flexible visitor pattern implementation with fine-grained traversal control
35
+ - **Error Handling**: Rich error objects that associate messages with source locations and extract Python exception tracebacks
36
+ - **Multi-threaded Parsing**: Built-in support for parsing multiple files concurrently
37
+ - **Workspace Support**: Parse entire directory trees with customizable file filtering
38
+ - **Whitespace Handling**: Mixins for both insignificant and significant whitespace grammars (e.g., Python-like indentation)
29
39
 
30
40
  ### How to use `dbrownell_ParserLib`
31
- TODO: Complete this section
41
+
42
+ The typical workflow for creating a parser with this library involves:
43
+
44
+ #### 1. Define an ANTLR Grammar
45
+ Create a `.g4` grammar file defining your language syntax:
46
+ ```antlr
47
+ grammar Calculator;
48
+
49
+ expr : expr op=('*'|'/') expr # BinaryOp
50
+ | expr op=('+'|'-') expr # BinaryOp
51
+ | INT # Number
52
+ ;
53
+
54
+ INT : [0-9]+ ;
55
+ WS : [ \t\n\r]+ -> skip ;
56
+ ```
57
+
58
+ #### 2. Build the Grammar
59
+ Use `BuildAntlrGrammar` to generate the lexer and parser:
60
+ ```python
61
+ from dbrownell_ParserLib import BuildAntlrGrammar
62
+ from pathlib import Path
63
+
64
+ BuildAntlrGrammar(
65
+ dm, # DoneManager instance
66
+ Path("Calculator.g4"),
67
+ Path("output_dir"),
68
+ )
69
+ ```
70
+
71
+ #### 3. Create Custom Visitor
72
+ Implement a visitor that converts ANTLR parse trees to Expression objects:
73
+ ```python
74
+ from dbrownell_ParserLib import (
75
+ Expression,
76
+ InsignificantWhitespaceAntlrVisitorMixin,
77
+ TerminalExpression,
78
+ )
79
+ from dataclasses import dataclass
80
+
81
+ from CalculatorVisitor import CalculatorVisitor as GeneratedVisitor
82
+
83
+
84
+
85
+ @dataclass(eq=False)
86
+ class BinaryExpression(Expression):
87
+ left: Expression
88
+ operator: TerminalExpression[str]
89
+ right: Expression
90
+
91
+ def _GenerateAcceptDetails(self):
92
+ yield "left", self.left
93
+ yield "operator", self.operator
94
+ yield "right", self.right
95
+
96
+ class CalculatorVisitor(InsignificantWhitespaceAntlrVisitorMixin, GeneratedVisitor):
97
+
98
+ def visitBinaryOp(self, ctx):
99
+ children = self.GetChildren(ctx)
100
+ self._stack.append(
101
+ BinaryExpression(
102
+ self.CreateRegion(ctx),
103
+ children[0],
104
+ TerminalExpression[str](self.CreateRegion(ctx.children[1]), ctx.children[1].getText()),
105
+ children[1],
106
+ )
107
+ )
108
+
109
+ def visitNumber(self, ctx):
110
+ self._stack.append(
111
+ TerminalExpression(self.CreateRegion(ctx), int(ctx.INT().getText()))
112
+ )
113
+ ```
114
+
115
+ #### 4. Create Parser Instance
116
+ Use `CreateAntlrParser` to build a callable parser:
117
+ ```python
118
+ from dbrownell_ParserLib import CreateAntlrParser
119
+
120
+ parser = CreateAntlrParser(
121
+ CalculatorLexer,
122
+ CalculatorParser,
123
+ CalculatorVisitor,
124
+ lambda p: p.expr(), # Entry point rule
125
+ )
126
+ ```
127
+ from pathlib import Path
128
+
129
+ from dbrownell_ParserLib import Error
130
+
131
+
132
+
133
+ #### 5. Parse Files
134
+ Parse single files, multiple files, or entire workspaces:
135
+ ```python
136
+ # Parse a single file
137
+ results = parser(dm, Path("input.txt"), None)
138
+
139
+ # Parse multiple files
140
+ results = parser(dm, [Path("file1.txt"), Path("file2.txt")], None)
141
+
142
+ # Check results
143
+ for filepath, result in results.items():
144
+ if isinstance(result, Error):
145
+ print(f"Parse error in {filepath}: {result}")
146
+ else:
147
+ # result is the visitor containing the parsed AST
148
+ ast = result._stack[0]
149
+ # Process the AST
150
+ ```
151
+
152
+ from contextlib import contextmanager
153
+
154
+ #### 6. Traverse the AST
155
+ Use the visitor pattern to process your AST:
156
+ ```python
157
+ @contextmanager
158
+
159
+ from dbrownell_ParserLib import ExpressionVisitorHelper, VisitResult
160
+
161
+ class EvaluatorVisitor(ExpressionVisitorHelper):
162
+ def OnBinaryExpression(self, expression):
163
+ @contextmanager
164
+
165
+ # Process binary expressions
166
+ yield VisitResult.Continue
167
+
168
+ # Process terminal values
169
+ yield VisitResult.Continue
170
+
171
+ ast.Accept(EvaluatorVisitor())
172
+ ```
173
+
174
+ For detailed examples, see the test files in the repository, particularly the end-to-end tests that demonstrate complete parsing workflows.
32
175
 
33
176
  <!-- 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. -->
34
177
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dbrownell-parserlib"
3
- version = "0.8.1"
3
+ version = "0.8.3"
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:
@@ -485,7 +485,7 @@ class _ErrorListener(antlr4.DiagnosticErrorListener):
485
485
  return
486
486
 
487
487
  location = Location(line, column + 1)
488
- error = Error(msg, Region(self._source, location, location))
488
+ error = Error(f"Syntax error: {msg}", Region(self._source, location, location))
489
489
  ex = AntlrParserException(error)
490
490
 
491
491
  if e is None:
@@ -1,5 +1,7 @@
1
1
  """Various ANTLR visitor mixins that can be used to add functionality to the generated visitor classes."""
2
2
 
3
+ import re
4
+
3
5
  from abc import ABC, abstractmethod
4
6
  from typing import cast, override, TYPE_CHECKING
5
7
 
@@ -35,14 +37,36 @@ class AntlrVisitorMixinBase(ABC):
35
37
  self._current_line: int = 0
36
38
 
37
39
  # ----------------------------------------------------------------------
38
- @abstractmethod
39
40
  def CreateRegion(
40
41
  self,
41
- ctx: antlr4.ParserRuleContext,
42
+ ctx: antlr4.TerminalNode | antlr4.ParserRuleContext,
42
43
  ) -> Region:
43
44
  """Create a `Region` from the given context."""
44
45
 
45
- raise NotImplementedError() # pragma: no cover
46
+ if isinstance(ctx, antlr4.TerminalNode):
47
+ token = cast(antlr4.Token, ctx.symbol) # ty: ignore[unresolved-attribute]
48
+
49
+ assert token.column is not None
50
+
51
+ start_line = token.line
52
+ start_col = token.column + 1
53
+ end_line = token.line
54
+ end_col = token.column + 1 + len(token.text)
55
+
56
+ assert start_line is not None
57
+ assert end_line is not None
58
+
59
+ region = Region(
60
+ self.filename,
61
+ Location(start_line, start_col),
62
+ Location(end_line, end_col),
63
+ )
64
+ else:
65
+ region = self._CreateRegionFromRule(ctx)
66
+
67
+ self._OnProgress(region.end.line)
68
+
69
+ return region
46
70
 
47
71
  # ----------------------------------------------------------------------
48
72
  def GetChildren(self, ctx: antlr4.ParserRuleContext) -> list[object]:
@@ -64,49 +88,28 @@ class AntlrVisitorMixinBase(ABC):
64
88
  self._current_line = end_line
65
89
  self._on_progress_func(end_line)
66
90
 
91
+ # ----------------------------------------------------------------------
92
+ @abstractmethod
93
+ def _CreateRegionFromRule(self, ctx: antlr4.ParserRuleContext) -> Region: ...
94
+
67
95
 
68
96
  # ----------------------------------------------------------------------
69
97
  class InsignificantWhitespaceAntlrVisitorMixin(AntlrVisitorMixinBase):
70
98
  """Mixin that adds functionality used for grammars that ignore insignificant whitespace."""
71
99
 
72
100
  # ----------------------------------------------------------------------
73
- @override
74
- def CreateRegion(
75
- self,
76
- ctx: antlr4.ParserRuleContext | antlr4.TerminalNode,
77
- ) -> Region:
78
- """Create a `Region` from the given context."""
79
-
80
- if isinstance(ctx, antlr4.ParserRuleContext):
81
- start_token = ctx.start
82
- stop_token = ctx.stop
83
-
84
- if start_token is None or stop_token is None:
85
- msg = "Context does not have start or stop token"
86
- raise ValueError(msg)
101
+ def _CreateRegionFromRule(self, ctx: antlr4.ParserRuleContext) -> Region:
102
+ start_token = ctx.start
103
+ stop_token = ctx.stop
87
104
 
88
- start_line = start_token.line
89
- start_col = start_token.column + 1
90
- end_line = stop_token.line
91
- end_col = stop_token.column + 1
92
-
93
- if start_line == end_line:
94
- end_col += len(stop_token.text)
95
- else:
96
- end_col += 1
97
-
98
- elif isinstance(ctx, antlr4.TerminalNode):
99
- token = cast(antlr4.Token, ctx.symbol) # ty: ignore[unresolved-attribute]
100
-
101
- assert token.column is not None
102
-
103
- start_line = token.line
104
- start_col = token.column + 1
105
- end_line = token.line
106
- end_col = token.column + 1 + len(token.text)
105
+ if start_token is None or stop_token is None:
106
+ msg = "Context does not have start or stop token"
107
+ raise ValueError(msg)
107
108
 
108
- else:
109
- assert False, ctx # noqa: B011, PT015 # pragma: no cover
109
+ start_line = start_token.line
110
+ start_col = start_token.column + 1
111
+ end_line = stop_token.line
112
+ end_col = stop_token.column + 1 + len(stop_token.text)
110
113
 
111
114
  assert start_line is not None
112
115
  assert end_line is not None
@@ -133,52 +136,34 @@ class SignificantWhitespaceAntlrVisitorMixin(AntlrVisitorMixinBase):
133
136
  self._newline_token = newline_token
134
137
  self._newline_token_string = newline_token_string
135
138
 
139
+ self._newline_indent_regex = re.compile(r"^\r?\n[ \t]*$")
140
+
136
141
  # ----------------------------------------------------------------------
137
142
  @override
138
- def CreateRegion(
143
+ def _CreateRegionFromRule(
139
144
  self,
140
145
  ctx: antlr4.ParserRuleContext,
141
146
  ) -> Region:
142
147
  assert isinstance(ctx.start, antlr4.Token), ctx.start
143
148
  assert isinstance(ctx.stop, antlr4.Token), ctx.stop
144
149
 
145
- if ctx.stop.type == self._dedent_token:
146
- stop_line = ctx.stop.line
147
- stop_col = ctx.stop.column
148
-
149
- elif ctx.stop.type == self._newline_token and ctx.stop.text == self._newline_token_string:
150
- if ctx.stop.line == ctx.start.line:
151
- # This is the scenario where the statement is followed by a dedent followed by another
152
- # statement. We don't want the range of this item to overlap with the range of the
153
- # next item, so use the values as they are, even though it means that a statement
154
- # that terminates with a newline will not have the newline here.
155
- stop_line = ctx.stop.line
156
- stop_col = ctx.stop.column
157
- else:
158
- stop_line = ctx.stop.line
159
- stop_col = ctx.stop.column if ctx.stop.column == 0 else ctx.start.column
160
-
161
- else:
162
- stop_line = ctx.stop.line
163
-
164
- content = ctx.stop.text
165
- lines = content.split("\n")
166
-
167
- if ctx.stop.type == self._newline_token:
168
- assert content.startswith("\n"), content
169
- assert len(lines) == 2, lines # noqa: PLR2004
170
-
171
- stop_col = len(lines[-1])
172
-
173
- if stop_line == ctx.stop.line:
174
- stop_col += ctx.stop.column
150
+ if ctx.stop.text == self._newline_token_string or self._newline_indent_regex.match(ctx.stop.text):
151
+ assert ctx.stop.line == ctx.start.line or ctx.stop.line - 1 == ctx.start.line, (
152
+ ctx.start.line,
153
+ ctx.stop.line,
154
+ )
175
155
 
176
- stop_line += len(lines) - 1
156
+ stop_line = ctx.start.line
157
+ stop_column = ctx.start.column + 1 + len(ctx.start.text)
177
158
 
178
- self._OnProgress(stop_line)
159
+ return Region(
160
+ self.filename,
161
+ Location(ctx.start.line, ctx.start.column + 1),
162
+ Location(stop_line, stop_column),
163
+ )
179
164
 
180
165
  return Region(
181
166
  self.filename,
182
167
  Location(ctx.start.line, ctx.start.column + 1),
183
- Location(stop_line, stop_col),
168
+ Location(ctx.stop.line, ctx.stop.column + 1),
184
169
  )