31
31
import keyword
32
32
import re
33
33
import types
34
+ from typing import Final
34
35
from typing import Literal
35
36
from typing import NoReturn
36
37
from typing import overload
39
40
40
41
__all__ = [
41
42
"Expression" ,
42
- "ParseError " ,
43
+ "ExpressionMatcher " ,
43
44
]
44
45
45
46
47
+ FILE_NAME : Final = "<pytest match expression>"
48
+
49
+
46
50
class TokenType (enum .Enum ):
47
51
LPAREN = "left parenthesis"
48
52
RPAREN = "right parenthesis"
@@ -64,25 +68,11 @@ class Token:
64
68
pos : int
65
69
66
70
67
- class ParseError (Exception ):
68
- """The :class:`Expression` contains invalid syntax.
69
-
70
- :param column: The column in the line where the error occurred (1-based).
71
- :param message: A description of the error.
72
- """
73
-
74
- def __init__ (self , column : int , message : str ) -> None :
75
- self .column = column
76
- self .message = message
77
-
78
- def __str__ (self ) -> str :
79
- return f"at column { self .column } : { self .message } "
80
-
81
-
82
71
class Scanner :
83
- __slots__ = ("current" , "tokens" )
72
+ __slots__ = ("current" , "input" , " tokens" )
84
73
85
74
def __init__ (self , input : str ) -> None :
75
+ self .input = input
86
76
self .tokens = self .lex (input )
87
77
self .current = next (self .tokens )
88
78
@@ -106,15 +96,15 @@ def lex(self, input: str) -> Iterator[Token]:
106
96
elif (quote_char := input [pos ]) in ("'" , '"' ):
107
97
end_quote_pos = input .find (quote_char , pos + 1 )
108
98
if end_quote_pos == - 1 :
109
- raise ParseError (
110
- pos + 1 ,
99
+ raise SyntaxError (
111
100
f'closing quote "{ quote_char } " is missing' ,
101
+ (FILE_NAME , 1 , pos + 1 , input ),
112
102
)
113
103
value = input [pos : end_quote_pos + 1 ]
114
104
if (backslash_pos := input .find ("\\ " )) != - 1 :
115
- raise ParseError (
116
- backslash_pos + 1 ,
105
+ raise SyntaxError (
117
106
r'escaping with "\" not supported in marker expression' ,
107
+ (FILE_NAME , 1 , backslash_pos + 1 , input ),
118
108
)
119
109
yield Token (TokenType .STRING , value , pos )
120
110
pos += len (value )
@@ -132,9 +122,9 @@ def lex(self, input: str) -> Iterator[Token]:
132
122
yield Token (TokenType .IDENT , value , pos )
133
123
pos += len (value )
134
124
else :
135
- raise ParseError (
136
- pos + 1 ,
125
+ raise SyntaxError (
137
126
f'unexpected character "{ input [pos ]} "' ,
127
+ (FILE_NAME , 1 , pos + 1 , input ),
138
128
)
139
129
yield Token (TokenType .EOF , "" , pos )
140
130
@@ -157,12 +147,12 @@ def accept(self, type: TokenType, *, reject: bool = False) -> Token | None:
157
147
return None
158
148
159
149
def reject (self , expected : Sequence [TokenType ]) -> NoReturn :
160
- raise ParseError (
161
- self .current .pos + 1 ,
150
+ raise SyntaxError (
162
151
"expected {}; got {}" .format (
163
152
" OR " .join (type .value for type in expected ),
164
153
self .current .type .value ,
165
154
),
155
+ (FILE_NAME , 1 , self .current .pos + 1 , self .input ),
166
156
)
167
157
168
158
@@ -223,14 +213,14 @@ def not_expr(s: Scanner) -> ast.expr:
223
213
def single_kwarg (s : Scanner ) -> ast .keyword :
224
214
keyword_name = s .accept (TokenType .IDENT , reject = True )
225
215
if not keyword_name .value .isidentifier ():
226
- raise ParseError (
227
- keyword_name .pos + 1 ,
216
+ raise SyntaxError (
228
217
f"not a valid python identifier { keyword_name .value } " ,
218
+ (FILE_NAME , 1 , keyword_name .pos + 1 , s .input ),
229
219
)
230
220
if keyword .iskeyword (keyword_name .value ):
231
- raise ParseError (
232
- keyword_name .pos + 1 ,
221
+ raise SyntaxError (
233
222
f"unexpected reserved python keyword `{ keyword_name .value } `" ,
223
+ (FILE_NAME , 1 , keyword_name .pos + 1 , s .input ),
234
224
)
235
225
s .accept (TokenType .EQUAL , reject = True )
236
226
@@ -245,9 +235,9 @@ def single_kwarg(s: Scanner) -> ast.keyword:
245
235
elif value_token .value in BUILTIN_MATCHERS :
246
236
value = BUILTIN_MATCHERS [value_token .value ]
247
237
else :
248
- raise ParseError (
249
- value_token .pos + 1 ,
238
+ raise SyntaxError (
250
239
f'unexpected character/s "{ value_token .value } "' ,
240
+ (FILE_NAME , 1 , value_token .pos + 1 , s .input ),
251
241
)
252
242
253
243
ret = ast .keyword (keyword_name .value , ast .Constant (value ))
@@ -333,7 +323,7 @@ def compile(cls, input: str) -> Expression:
333
323
334
324
:param input: The input expression - one line.
335
325
336
- :raises ParseError : If the expression is malformed.
326
+ :raises SyntaxError : If the expression is malformed.
337
327
"""
338
328
astexpr = expression (Scanner (input ))
339
329
code = compile (
0 commit comments