temparse 1.0.0__tar.gz → 1.1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: temparse
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: temparse is template + parse
5
5
  Author: jiang-zhexin
6
6
  Author-email: jiang-zhexin <162887873+jiang-zhexin@users.noreply.github.com>
@@ -116,6 +116,59 @@ assert parser.parse("foo + 3 = 3.14") == ("foo", 3, 3.14)
116
116
  assert parser.parse("bar + 7 = 2.72") == ("bar", 7, 2.72)
117
117
  ```
118
118
 
119
+ ## `Matcher` (Pattern Matching & Structured Parsing)
120
+
121
+ Subclass `Matcher` with a `Parser` to match strings in `match/case` statements or parse strings directly into structured instances:
122
+
123
+ ```python
124
+ from temparse import Matcher, MatchStr, Parser
125
+
126
+
127
+ class OkResp(Matcher, parser=Parser(t'code={int};message="{str}";')):
128
+ code: int
129
+ message: str
130
+
131
+
132
+ class ErrResp(Matcher, parser=Parser(t'code={int};error="{str}";')):
133
+ code: int
134
+ error: str
135
+
136
+
137
+ def handle_response(raw: str) -> str:
138
+ # Wrap the string with MatchStr to enable pattern matching
139
+ match MatchStr(raw):
140
+ case OkResp(code, message):
141
+ return f"OK ({code}): {message}"
142
+ case ErrResp(code, error):
143
+ return f"Error ({code}): {error}"
144
+ case _:
145
+ return "Unknown response"
146
+
147
+
148
+ assert handle_response('code=200;message="success";') == "OK (200): success"
149
+ assert handle_response('code=400;error="bad request";') == "Error (400): bad request"
150
+ ```
151
+
152
+ `Matcher` subclasses are also dataclasses that support keyword matching and `.parse()`:
153
+
154
+ ```python
155
+ from temparse import Matcher, MatchStr, Parser
156
+
157
+
158
+ class Point(Matcher, parser=Parser(t"x={int}, y={int}")):
159
+ x: int
160
+ y: int
161
+
162
+
163
+ p = Point.parse("x=1, y=2")
164
+ assert p == Point(x=1, y=2)
165
+
166
+ # Wrap the string with MatchStr to enable pattern matching
167
+ match MatchStr("x=0, y=5"):
168
+ case Point(x=0, y=y):
169
+ assert y == 5
170
+ ```
171
+
119
172
  ## License
120
173
 
121
174
  MIT
@@ -100,6 +100,59 @@ assert parser.parse("foo + 3 = 3.14") == ("foo", 3, 3.14)
100
100
  assert parser.parse("bar + 7 = 2.72") == ("bar", 7, 2.72)
101
101
  ```
102
102
 
103
+ ## `Matcher` (Pattern Matching & Structured Parsing)
104
+
105
+ Subclass `Matcher` with a `Parser` to match strings in `match/case` statements or parse strings directly into structured instances:
106
+
107
+ ```python
108
+ from temparse import Matcher, MatchStr, Parser
109
+
110
+
111
+ class OkResp(Matcher, parser=Parser(t'code={int};message="{str}";')):
112
+ code: int
113
+ message: str
114
+
115
+
116
+ class ErrResp(Matcher, parser=Parser(t'code={int};error="{str}";')):
117
+ code: int
118
+ error: str
119
+
120
+
121
+ def handle_response(raw: str) -> str:
122
+ # Wrap the string with MatchStr to enable pattern matching
123
+ match MatchStr(raw):
124
+ case OkResp(code, message):
125
+ return f"OK ({code}): {message}"
126
+ case ErrResp(code, error):
127
+ return f"Error ({code}): {error}"
128
+ case _:
129
+ return "Unknown response"
130
+
131
+
132
+ assert handle_response('code=200;message="success";') == "OK (200): success"
133
+ assert handle_response('code=400;error="bad request";') == "Error (400): bad request"
134
+ ```
135
+
136
+ `Matcher` subclasses are also dataclasses that support keyword matching and `.parse()`:
137
+
138
+ ```python
139
+ from temparse import Matcher, MatchStr, Parser
140
+
141
+
142
+ class Point(Matcher, parser=Parser(t"x={int}, y={int}")):
143
+ x: int
144
+ y: int
145
+
146
+
147
+ p = Point.parse("x=1, y=2")
148
+ assert p == Point(x=1, y=2)
149
+
150
+ # Wrap the string with MatchStr to enable pattern matching
151
+ match MatchStr("x=0, y=5"):
152
+ case Point(x=0, y=y):
153
+ assert y == 5
154
+ ```
155
+
103
156
  ## License
104
157
 
105
158
  MIT
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "temparse"
3
+ version = "1.1.0"
4
+ description = "temparse is template + parse"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ license = "MIT"
8
+ classifiers = [
9
+ "Environment :: Web Environment",
10
+ "Intended Audience :: Developers",
11
+ "Programming Language :: Python :: 3",
12
+ "Topic :: Software Development :: Code Generators",
13
+ "Topic :: Software Development :: Libraries :: Python Modules",
14
+ ]
15
+ dependencies = []
16
+
17
+ [[project.authors]]
18
+ name = "jiang-zhexin"
19
+ email = "162887873+jiang-zhexin@users.noreply.github.com"
20
+
21
+ [project.urls]
22
+ homepage = "https://github.com/jiang-zhexin/temparse"
23
+
24
+ [dependency-groups]
25
+ dev = [
26
+ "pytest>=9.1.1",
27
+ "ruff>=0.17.2",
28
+ "ty>=0.0.92",
29
+ ]
30
+
31
+ [build-system]
32
+ requires = ["uv_build>=0.13.0,<0.14"]
33
+ build-backend = "uv_build"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "temparse"
3
- version = "1.0.0"
3
+ version = "1.1.0"
4
4
  description = "temparse is template + parse"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -24,10 +24,10 @@ homepage = "https://github.com/jiang-zhexin/temparse"
24
24
  [dependency-groups]
25
25
  dev = [
26
26
  "pytest>=9.1.1",
27
- "ruff>=0.15.22",
28
- "ty>=0.0.61",
27
+ "ruff>=0.17.2",
28
+ "ty>=0.0.92",
29
29
  ]
30
30
 
31
31
  [build-system]
32
- requires = ["uv_build>=0.11.15,<0.12.0"]
32
+ requires = ["uv_build>=0.13.0,<0.14"]
33
33
  build-backend = "uv_build"
@@ -0,0 +1,276 @@
1
+ import builtins
2
+ import dataclasses
3
+ import datetime
4
+ import functools
5
+ import json
6
+ import re
7
+ from collections.abc import Callable
8
+ from string.templatelib import Interpolation, Template
9
+ from typing import (
10
+ Any,
11
+ Literal,
12
+ Self,
13
+ cast,
14
+ dataclass_transform,
15
+ final,
16
+ get_origin,
17
+ is_typeddict,
18
+ overload,
19
+ )
20
+
21
+ __all__ = ["Conversion", "FormatConversion", "MatchStr", "Matcher", "Parser", "parse"]
22
+
23
+
24
+ @final
25
+ class Parser[*T]:
26
+ """
27
+ A compiled template parser that can be reused to parse multiple strings.
28
+
29
+ example:
30
+ ```python
31
+ parser = Parser[str, int, float](t"{str} + {int} = {float}")
32
+
33
+ parser.parse("foo + 3 = 3.14")
34
+ ```
35
+ """
36
+
37
+ def __init__(self, template: Template, flags: re.RegexFlag = re.NOFLAG) -> None:
38
+ if len(template.interpolations) == 0:
39
+ raise ValueError("template must contain at least one interpolation")
40
+
41
+ self._template = template
42
+ self._flags = flags
43
+ self._expression = "".join(
44
+ re.escape(item) if isinstance(item, str) else r"(.+?)" for item in template
45
+ )
46
+
47
+ @functools.cached_property
48
+ def _match_re(self) -> re.Pattern[str]:
49
+ return re.compile(self._expression, self._flags)
50
+
51
+ @overload
52
+ def parse(self, s: str, *, return_none: Literal[False] = False) -> tuple[*T]: ...
53
+
54
+ @overload
55
+ def parse(self, s: str, *, return_none: bool) -> tuple[*T] | None: ...
56
+
57
+ def parse(self, s: str, *, return_none: bool = False) -> tuple[*T] | None:
58
+ m = self._match_re.fullmatch(s)
59
+ if m is None:
60
+ if return_none:
61
+ return None
62
+ raise ValueError(f"invalid format: {s!r}")
63
+
64
+ return cast(
65
+ tuple[*T],
66
+ tuple(
67
+ _convert(text, interpolation)
68
+ for text, interpolation in zip(
69
+ m.groups(), self._template.interpolations
70
+ )
71
+ ),
72
+ )
73
+
74
+
75
+ @final
76
+ class parse[*T]:
77
+ """
78
+ Parse string by template.
79
+
80
+ example:
81
+ ```python
82
+ city, year = parse[str, int](
83
+ t"I live in {str}, since {int}", "I live in Tokyo, since 2010"
84
+ )
85
+ assert city == "Tokyo"
86
+ assert year == 2010
87
+ ```
88
+ """
89
+
90
+ def __new__(cls, t: Template, s: str, flags: re.RegexFlag = re.NOFLAG) -> tuple[*T]:
91
+ """
92
+ Used to replace this function until [PEP 718](https://peps.python.org/pep-0718/) is accepted.
93
+
94
+ ```python
95
+ def parse[*T](t: Template, s: str, flags: re.RegexFlag = re.NOFLAG) -> tuple[*T]:
96
+ return Parser(t).parse(s)
97
+ ```
98
+ """
99
+ return Parser[*T](t, flags).parse(s)
100
+
101
+
102
+ @dataclass_transform()
103
+ class _MatcherMeta(type):
104
+ _parser: Parser
105
+ __match_args__: tuple[str, ...]
106
+
107
+ def __instancecheck__(cls, instance: object) -> bool:
108
+ if super().__instancecheck__(instance):
109
+ return True
110
+ return isinstance(instance, MatchStr) and instance._match(cls)
111
+
112
+
113
+ class Matcher(metaclass=_MatcherMeta):
114
+ """
115
+ Base class for structural pattern matching and structured template parsing.
116
+
117
+ example:
118
+ ```python
119
+ class OkResp(Matcher, parser=Parser(t'code={int};message="{str}";')):
120
+ code: int
121
+ message: str
122
+
123
+
124
+ class ErrResp(Matcher, parser=Parser(t'code={int};error="{str}";')):
125
+ code: int
126
+ error: str
127
+
128
+
129
+ match MatchStr('code=400;error="bad request";'):
130
+ case OkResp(code, message):
131
+ print(f"OK ({code}): {message}")
132
+ case ErrResp(code, error):
133
+ print(f"Error ({code}): {error}")
134
+ ```
135
+ """
136
+
137
+ def __init__(self, *args: Any, **kwargs: Any) -> None: ...
138
+
139
+ def __init_subclass__(cls, parser: Parser) -> None:
140
+ super().__init_subclass__()
141
+ dataclasses.dataclass(cls)
142
+ cls._parser = parser
143
+ if len(cls.__match_args__) != len(parser._template.interpolations):
144
+ raise ValueError("number of fields must match template interpolations")
145
+
146
+ @classmethod
147
+ def parse(cls, s: str) -> Self:
148
+ """
149
+ Parse a string using the class's parser and return an instance of the class.
150
+
151
+ example:
152
+ ```python
153
+ p = Point.parse("x=1, y=2")
154
+ assert p == Point(x=1, y=2)
155
+ ```
156
+ """
157
+ return cls(*cls._parser.parse(s))
158
+
159
+
160
+ class MatchStr(str):
161
+ """
162
+ A `str` subclass that enables structural pattern matching with `Matcher` subclasses.
163
+
164
+ example:
165
+ ```python
166
+ match MatchStr("x=1, y=2"):
167
+ case Point(x, y):
168
+ assert x == 1 and y == 2
169
+ ```
170
+ """
171
+
172
+ def __init__(self, _: str, /) -> None:
173
+ self._cache: dict[_MatcherMeta, dict[str, Any] | None] = {}
174
+
175
+ def _match(self, cls: _MatcherMeta) -> bool:
176
+ if cls not in self._cache:
177
+ res = cls._parser.parse(self, return_none=True)
178
+ self._cache[cls] = (
179
+ dict(zip(cls.__match_args__, res)) if res is not None else None
180
+ )
181
+ if (fields := self._cache[cls]) is None:
182
+ return False
183
+ self.__dict__.update(fields)
184
+ return True
185
+
186
+
187
+ @final
188
+ class Conversion[T]:
189
+ """
190
+ Convert the string to your object.
191
+
192
+ example:
193
+ ```python
194
+ @Conversion
195
+ def percent(s: str) -> float:
196
+ return float(s.rstrip("%")) / 100
197
+
198
+
199
+ (result,) = parse[float](t"x = {percent}", "x = 30%")
200
+ assert result == 0.3
201
+ ```
202
+ """
203
+
204
+ def __init__(self, func: Callable[[str], T]) -> None:
205
+ self._convert = func
206
+ functools.update_wrapper(self, func)
207
+
208
+ def __call__(self, s: str) -> T:
209
+ return self._convert(s)
210
+
211
+
212
+ @final
213
+ class FormatConversion[T]:
214
+ """
215
+ Convert the string to your object with format.
216
+
217
+ example:
218
+ ```python
219
+ @FormatConversion
220
+ def between(s: str, spec: str) -> str:
221
+ lo, hi = spec.split(",")
222
+ return s[int(lo) : int(hi)]
223
+
224
+ (result,) = parse[str](t"{between:2,5}", "abcdefg")
225
+ assert result == "cde"
226
+ ```
227
+ """
228
+
229
+ def __init__(self, func: Callable[[str, str], T]) -> None:
230
+ self._convert = func
231
+ functools.update_wrapper(self, func)
232
+
233
+ def __call__(self, s: str, format: str) -> T:
234
+ return self._convert(s, format)
235
+
236
+
237
+ def _convert(s: str, interp: Interpolation[Any]) -> Any:
238
+ origin = get_origin(interp.value) or interp.value
239
+ match origin:
240
+ case Conversion():
241
+ return interp.value(s)
242
+
243
+ case FormatConversion():
244
+ return interp.value(s, interp.format_spec)
245
+
246
+ case datetime.datetime:
247
+ return datetime.datetime.strptime(s, interp.format_spec).astimezone(
248
+ datetime.UTC
249
+ )
250
+
251
+ case datetime.date:
252
+ return datetime.date.strptime(s, interp.format_spec)
253
+
254
+ case datetime.time:
255
+ return datetime.time.strptime(s, interp.format_spec)
256
+
257
+ case builtins.int:
258
+ return int(s, base=int(interp.format_spec) if interp.format_spec else 0)
259
+
260
+ case builtins.float:
261
+ return float(s)
262
+
263
+ case builtins.complex:
264
+ return complex(s)
265
+
266
+ case builtins.str:
267
+ return s
268
+
269
+ case builtins.list | builtins.dict:
270
+ return json.loads(s)
271
+
272
+ case _ if origin is json or is_typeddict(origin):
273
+ return json.loads(s)
274
+
275
+ case _:
276
+ raise TypeError(f"unkown object: {interp.value}")
@@ -1,174 +0,0 @@
1
- import datetime
2
- import functools
3
- import json
4
- import re
5
- from collections.abc import Callable
6
- from string.templatelib import Interpolation, Template
7
- from typing import Any, cast, final, get_origin, is_typeddict
8
-
9
- __all__ = ["Conversion", "FormatConversion", "Parser", "parse"]
10
-
11
-
12
- @final
13
- class Parser[*T]:
14
- """
15
- A compiled template parser that can be reused to parse multiple strings.
16
-
17
- example:
18
- ```python
19
- parser = Parser[str, int, float](t"{str} + {int} = {float}")
20
-
21
- parser.parse("foo + 3 = 3.14")
22
- ```
23
- """
24
-
25
- def __init__(self, template: Template, flags: re.RegexFlag = re.NOFLAG) -> None:
26
- if len(template.interpolations) == 0:
27
- raise ValueError("template must contain at least one interpolation")
28
-
29
- self._template = template
30
- self._flags = flags
31
- self._expression = "".join(
32
- re.escape(item) if isinstance(item, str) else r"(.+?)" for item in template
33
- )
34
-
35
- @functools.cached_property
36
- def _match_re(self) -> re.Pattern[str]:
37
- return re.compile(self._expression, self._flags)
38
-
39
- def parse(self, s: str) -> tuple[*T]:
40
- m = self._match_re.fullmatch(s)
41
- if m is None:
42
- raise ValueError(f"invalid format: {s!r}")
43
-
44
- return cast(
45
- tuple[*T],
46
- tuple(
47
- _convert(text, interpolation)
48
- for text, interpolation in zip(
49
- m.groups(), self._template.interpolations
50
- )
51
- ),
52
- )
53
-
54
-
55
- @final
56
- class parse[*T]:
57
- """
58
- Parse string by template.
59
-
60
- example:
61
- ```python
62
- city, year = parse[str, int](
63
- t"I live in {str}, since {int}", "I live in Tokyo, since 2010"
64
- )
65
- assert city == "Tokyo"
66
- assert year == 2010
67
- ```
68
- """
69
-
70
- def __new__(cls, t: Template, s: str, flags: re.RegexFlag = re.NOFLAG) -> tuple[*T]:
71
- """
72
- Used to replace this function until [PEP 718](https://peps.python.org/pep-0718/) is accepted.
73
-
74
- ```python
75
- def parse[*T](t: Template, s: str, flags: re.RegexFlag = re.NOFLAG) -> tuple[*T]:
76
- return Parser(t).parse(s)
77
- ```
78
- """
79
- return Parser[*T](t, flags).parse(s)
80
-
81
-
82
- @final
83
- class Conversion[T]:
84
- """
85
- Convert the string to your object.
86
-
87
- example:
88
- ```python
89
- @Conversion
90
- def percent(s: str) -> float:
91
- return float(s.rstrip("%")) / 100
92
-
93
-
94
- (result,) = parse[float](t"x = {percent}", "x = 30%")
95
- assert result == 0.3
96
- ```
97
- """
98
-
99
- __name__: str
100
- __qualname__: str
101
-
102
- def __init__(self, func: Callable[[str], T]) -> None:
103
- self._convert = func
104
- functools.update_wrapper(self, func)
105
-
106
- def __call__(self, s: str) -> T:
107
- return self._convert(s)
108
-
109
-
110
- @final
111
- class FormatConversion[T]:
112
- """
113
- Convert the string to your object with format.
114
-
115
- example:
116
- ```python
117
- @FormatConversion
118
- def between(s: str, spec: str) -> str:
119
- lo, hi = spec.split(",")
120
- return s[int(lo) : int(hi)]
121
-
122
- (result,) = parse[str](t"{between:2,5}", "abcdefg")
123
- assert result == "cde"
124
- ```
125
- """
126
-
127
- __name__: str
128
- __qualname__: str
129
-
130
- def __init__(self, func: Callable[[str, str], T]) -> None:
131
- self._convert = func
132
- functools.update_wrapper(self, func)
133
-
134
- def __call__(self, s: str, format: str) -> T:
135
- return self._convert(s, format)
136
-
137
-
138
- def _convert(s: str, i: Interpolation[Any]) -> Any:
139
- origin = get_origin(i.value) or i.value
140
- match origin:
141
- case Conversion():
142
- return i.value(s)
143
-
144
- case FormatConversion():
145
- return i.value(s, i.format_spec)
146
-
147
- case datetime.datetime:
148
- return datetime.datetime.strptime(s, i.format_spec).astimezone(datetime.UTC)
149
-
150
- case datetime.date:
151
- return datetime.date.strptime(s, i.format_spec)
152
-
153
- case datetime.time:
154
- return datetime.time.strptime(s, i.format_spec)
155
-
156
- case _ if origin is int:
157
- return int(s, base=int(i.format_spec) if i.format_spec else 0)
158
-
159
- case _ if origin is float:
160
- return float(s)
161
-
162
- case _ if origin is complex:
163
- return complex(s)
164
-
165
- case _ if origin is str:
166
- return s
167
-
168
- case _ if (
169
- origin is list or origin is dict or origin is json or is_typeddict(origin)
170
- ):
171
- return json.loads(s)
172
-
173
- case _:
174
- raise TypeError(f"unkown object: {i.value}")
File without changes