into-obj 0.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.
- into_obj-0.1.0/PKG-INFO +284 -0
- into_obj-0.1.0/README.md +262 -0
- into_obj-0.1.0/pyproject.toml +46 -0
- into_obj-0.1.0/pyproject.toml.orig +43 -0
- into_obj-0.1.0/src/into_obj/__init__.py +52 -0
- into_obj-0.1.0/src/into_obj/into.py +381 -0
- into_obj-0.1.0/src/into_obj/py.typed +0 -0
into_obj-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: into-obj
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Rust-style `Into` & `TryInto` conversions for Python
|
|
5
|
+
Keywords: rust,rs,convertions,cast,type,into
|
|
6
|
+
Author: Sequence
|
|
7
|
+
Author-email: Sequence <3863887959@qq.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Natural Language :: English
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Dist: rezult>=0.2.0
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# into-obj
|
|
24
|
+
|
|
25
|
+
Rust-style `Into` & `TryInto` conversions for Python, with explicit,
|
|
26
|
+
type-checked converter registries.
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from into_obj import Reg, into, register_into
|
|
30
|
+
|
|
31
|
+
register_into(Celsius, Fahrenheit, lambda c: Fahrenheit(c.degrees * 9 / 5 + 32))
|
|
32
|
+
|
|
33
|
+
into(Reg(Celsius(100)), Fahrenheit) # 212.0
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- `into(from_, Target)` for infallible conversions and `try_into(from_, Target)`
|
|
39
|
+
for fallible ones that return `Result`.
|
|
40
|
+
- Two conversion mechanisms:
|
|
41
|
+
- **Magic methods** — a type converts itself via `__into__` / `__try_into__`.
|
|
42
|
+
- **Converter registries** — converters keyed by `(source type, target type)`,
|
|
43
|
+
usable without touching the source type.
|
|
44
|
+
- `Reg` wrapper makes registry-based conversions visible in the type signature,
|
|
45
|
+
so type checkers reject unregistered, non-converting sources.
|
|
46
|
+
- MRO-aware dispatch: a converter registered for a base class also serves its
|
|
47
|
+
subclasses, and `object` works as a wildcard target.
|
|
48
|
+
- Errors are explicit: `into` raises, `try_into` returns `Err`.
|
|
49
|
+
|
|
50
|
+
## Requirements
|
|
51
|
+
|
|
52
|
+
- Python 3.12+ (the package uses PEP 695 generics)
|
|
53
|
+
- `rezult >= 0.2.0`
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv add into-obj
|
|
59
|
+
# or
|
|
60
|
+
pip install into-obj
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick start
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from rezult import Err, Ok
|
|
67
|
+
|
|
68
|
+
from into_obj import Reg, into, register_into, register_try_into, try_into
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Version:
|
|
72
|
+
def __init__(self, number: int) -> None:
|
|
73
|
+
self.number = number
|
|
74
|
+
|
|
75
|
+
def __into__(self, into: type[str]) -> str:
|
|
76
|
+
return f"v{self.number}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Celsius:
|
|
80
|
+
def __init__(self, degrees: float) -> None:
|
|
81
|
+
self.degrees = degrees
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Fahrenheit(float):
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
register_into(
|
|
89
|
+
Celsius,
|
|
90
|
+
Fahrenheit,
|
|
91
|
+
lambda celsius: Fahrenheit(celsius.degrees * 9 / 5 + 32),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
register_try_into(
|
|
95
|
+
str,
|
|
96
|
+
int,
|
|
97
|
+
lambda text: Ok(int(text)) if text.isdigit() else Err(ValueError(text)),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
assert into(Version(3), str) == "v3"
|
|
101
|
+
assert into(Reg(Celsius(100)), Fahrenheit) == 212.0
|
|
102
|
+
assert try_into(Reg("42"), int) == Ok(42)
|
|
103
|
+
assert try_into(Reg("abc"), int).is_err()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## How it works
|
|
107
|
+
|
|
108
|
+
Every call resolves a conversion by walking a fixed order. A magic method that
|
|
109
|
+
raises `NotImplementedError` counts as missing, and resolution continues.
|
|
110
|
+
|
|
111
|
+
| Call | Resolution order |
|
|
112
|
+
| --------------------- | ----------------------------------------------------------------------------------------- |
|
|
113
|
+
| `into(x, T)` | `x.__into__(T)` |
|
|
114
|
+
| `into(Reg(x), T)` | `x.__into__(T)` → `INTO_CONVERTERS` |
|
|
115
|
+
| `try_into(x, T)` | `x.__try_into__(T)` |
|
|
116
|
+
| `try_into(Reg(x), T)` | `x.__try_into__(T)` → `TRY_INTO_CONVERTERS` → `x.__into__(T)` → `INTO_CONVERTERS` |
|
|
117
|
+
|
|
118
|
+
### Magic methods
|
|
119
|
+
|
|
120
|
+
Implement `__into__` for infallible conversions, or `__try_into__` when the
|
|
121
|
+
conversion can fail.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from rezult import Err, Ok, Result
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Port:
|
|
128
|
+
def __init__(self, text: str) -> None:
|
|
129
|
+
self.text = text
|
|
130
|
+
|
|
131
|
+
def __try_into__(self, into: type[int]) -> Result[int, str]:
|
|
132
|
+
if into is not int:
|
|
133
|
+
return Err("only int is supported")
|
|
134
|
+
if not self.text.isdigit():
|
|
135
|
+
return Err(f"invalid port: {self.text}")
|
|
136
|
+
return Ok(int(self.text))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
assert try_into(Port("8080"), int) == Ok(8080)
|
|
140
|
+
assert try_into(Port("http"), int) == Err("invalid port: http")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`Into[T]` and `TryInto[T, E]` are protocols, so implementing the method is
|
|
144
|
+
enough — no inheritance required.
|
|
145
|
+
|
|
146
|
+
### Registered converters
|
|
147
|
+
|
|
148
|
+
Register a converter when you cannot (or do not want to) change the source
|
|
149
|
+
type. Wrap the source value in `Reg` to select the registry path.
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
class Meters:
|
|
153
|
+
def __init__(self, value: float) -> None:
|
|
154
|
+
self.value = value
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class Kilometers:
|
|
158
|
+
def __init__(self, value: float) -> None:
|
|
159
|
+
self.value = value
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
register_into(Meters, Kilometers, lambda meters: Kilometers(meters.value / 1000))
|
|
163
|
+
|
|
164
|
+
assert into(Reg(Meters(1500)), Kilometers).value == 1.5
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
A fallible converter returns a `Result`. Returning a plain value is treated as
|
|
168
|
+
`Ok(value)`, and raising an `Exception` becomes `Err(exception)`.
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
register_try_into(
|
|
172
|
+
str,
|
|
173
|
+
int,
|
|
174
|
+
lambda text: Ok(int(text)) if text.isdigit() else Err(ValueError(text)),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
assert try_into(Reg("7"), int) == Ok(7)
|
|
178
|
+
assert isinstance(try_into(Reg("seven"), int), Err)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Converters can be removed again, which is useful in tests:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from into_obj import unregister_into, unregister_try_into
|
|
185
|
+
|
|
186
|
+
unregister_into(Meters, Kilometers)
|
|
187
|
+
unregister_try_into(str, int)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### MRO-aware lookup
|
|
191
|
+
|
|
192
|
+
Registry keys are `(source type, target type)`. Lookup walks the source MRO
|
|
193
|
+
first, then the target MRO, so the nearest source class wins.
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
class SubMeters(Meters):
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
register_into(Meters, Kilometers, lambda meters: Kilometers(meters.value / 1000))
|
|
201
|
+
register_into(SubMeters, Kilometers, lambda meters: Kilometers(-1))
|
|
202
|
+
|
|
203
|
+
into(Reg(Meters(2000)), Kilometers).value # 2.0 (base converter)
|
|
204
|
+
into(Reg(SubMeters(2000)), Kilometers).value # -1 (nearest source wins)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Registering `object` as the target makes a converter apply to every target
|
|
208
|
+
type:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
register_into(Meters, object, lambda meters: meters.value)
|
|
212
|
+
|
|
213
|
+
into(Reg(Meters(3)), int) # 3
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## API reference
|
|
217
|
+
|
|
218
|
+
### Functions
|
|
219
|
+
|
|
220
|
+
| Function | Description |
|
|
221
|
+
| --------------------------------------- | ------------------------------------------------------------------ |
|
|
222
|
+
| `into(from_, target)` | Convert `from_` into `target`, raising on failure. |
|
|
223
|
+
| `try_into(from_, target)` | Convert `from_` into `target`, returning `Result[T, E]`. |
|
|
224
|
+
| `register_into(source, target, conv)` | Register an infallible converter for `(source, target)`. |
|
|
225
|
+
| `register_try_into(source, target, conv)` | Register a fallible converter for `(source, target)`. |
|
|
226
|
+
| `unregister_into(source, target)` | Remove the infallible converter for the pair, if any. |
|
|
227
|
+
| `unregister_try_into(source, target)` | Remove the fallible converter for the pair, if any. |
|
|
228
|
+
|
|
229
|
+
`register_*` validates its arguments and raises `TypeError` if `source` or
|
|
230
|
+
`target` is not a class, or if the converter is not callable.
|
|
231
|
+
|
|
232
|
+
### Types
|
|
233
|
+
|
|
234
|
+
| Name | Description |
|
|
235
|
+
| ------------------------ | ---------------------------------------------------------------------- |
|
|
236
|
+
| `Reg[T]` | Wrapper marking a value as converted by a registered converter. |
|
|
237
|
+
| `Into[T]` | Protocol for types implementing `__into__`. |
|
|
238
|
+
| `TryInto[T, E]` | Protocol for types implementing `__try_into__`. |
|
|
239
|
+
| `IntoSource[S, T]` | `Into[T] \| Reg[S]` — accepted by `into`. |
|
|
240
|
+
| `TryIntoSource[S, T, E]` | `TryInto[T, E] \| Reg[S]` — accepted by `try_into`. |
|
|
241
|
+
| `Converter[T, U]` | `Callable[[T], U]`. |
|
|
242
|
+
| `TryConverter[T, U, E]` | `Callable[[T], Result[U, E]]`. |
|
|
243
|
+
| `INTO_CONVERTERS` | Registry used by `into`. |
|
|
244
|
+
| `TRY_INTO_CONVERTERS` | Registry used by `try_into`. |
|
|
245
|
+
|
|
246
|
+
### Type signatures
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
def into[S, T](from_: IntoSource[S, T], into: type[T]) -> T: ...
|
|
250
|
+
def try_into[S, T, E](from_: TryIntoSource[S, T, E], into: type[T]) -> Result[T, E]: ...
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`S` is the wrapped source type and `T` the target type, which is why `Reg` is
|
|
254
|
+
parameterized separately: `into(Reg(Celsius(100)), Fahrenheit)` infers
|
|
255
|
+
`S = Celsius` and `T = Fahrenheit`. Passing a registered source without `Reg`
|
|
256
|
+
is rejected by type checkers.
|
|
257
|
+
|
|
258
|
+
### Error handling
|
|
259
|
+
|
|
260
|
+
- `into` raises `TypeError` when neither a magic method nor a converter is
|
|
261
|
+
available, and re-raises `NotImplementedError` from `__into__` in that case.
|
|
262
|
+
- `try_into` never raises for conversion failures: exceptions from the
|
|
263
|
+
registry converter or the fallback path are returned as `Err`.
|
|
264
|
+
|
|
265
|
+
## Notes
|
|
266
|
+
|
|
267
|
+
- Registries are process-global dictionaries. Register converters once at
|
|
268
|
+
import time or application startup.
|
|
269
|
+
- Registering the same `(source, target)` pair twice replaces the converter.
|
|
270
|
+
- `Reg` is transparent at runtime: it is unwrapped before resolution, so a
|
|
271
|
+
magic method still takes precedence over a registered converter.
|
|
272
|
+
|
|
273
|
+
## Development
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
uv sync
|
|
277
|
+
uv run pytest
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Tests live in `tests/test_into.py` and isolate the global registries with an
|
|
281
|
+
autouse fixture.
|
|
282
|
+
|
|
283
|
+
## License
|
|
284
|
+
MIT
|
into_obj-0.1.0/README.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# into-obj
|
|
2
|
+
|
|
3
|
+
Rust-style `Into` & `TryInto` conversions for Python, with explicit,
|
|
4
|
+
type-checked converter registries.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from into_obj import Reg, into, register_into
|
|
8
|
+
|
|
9
|
+
register_into(Celsius, Fahrenheit, lambda c: Fahrenheit(c.degrees * 9 / 5 + 32))
|
|
10
|
+
|
|
11
|
+
into(Reg(Celsius(100)), Fahrenheit) # 212.0
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- `into(from_, Target)` for infallible conversions and `try_into(from_, Target)`
|
|
17
|
+
for fallible ones that return `Result`.
|
|
18
|
+
- Two conversion mechanisms:
|
|
19
|
+
- **Magic methods** — a type converts itself via `__into__` / `__try_into__`.
|
|
20
|
+
- **Converter registries** — converters keyed by `(source type, target type)`,
|
|
21
|
+
usable without touching the source type.
|
|
22
|
+
- `Reg` wrapper makes registry-based conversions visible in the type signature,
|
|
23
|
+
so type checkers reject unregistered, non-converting sources.
|
|
24
|
+
- MRO-aware dispatch: a converter registered for a base class also serves its
|
|
25
|
+
subclasses, and `object` works as a wildcard target.
|
|
26
|
+
- Errors are explicit: `into` raises, `try_into` returns `Err`.
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- Python 3.12+ (the package uses PEP 695 generics)
|
|
31
|
+
- `rezult >= 0.2.0`
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv add into-obj
|
|
37
|
+
# or
|
|
38
|
+
pip install into-obj
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from rezult import Err, Ok
|
|
45
|
+
|
|
46
|
+
from into_obj import Reg, into, register_into, register_try_into, try_into
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Version:
|
|
50
|
+
def __init__(self, number: int) -> None:
|
|
51
|
+
self.number = number
|
|
52
|
+
|
|
53
|
+
def __into__(self, into: type[str]) -> str:
|
|
54
|
+
return f"v{self.number}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Celsius:
|
|
58
|
+
def __init__(self, degrees: float) -> None:
|
|
59
|
+
self.degrees = degrees
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Fahrenheit(float):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
register_into(
|
|
67
|
+
Celsius,
|
|
68
|
+
Fahrenheit,
|
|
69
|
+
lambda celsius: Fahrenheit(celsius.degrees * 9 / 5 + 32),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
register_try_into(
|
|
73
|
+
str,
|
|
74
|
+
int,
|
|
75
|
+
lambda text: Ok(int(text)) if text.isdigit() else Err(ValueError(text)),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
assert into(Version(3), str) == "v3"
|
|
79
|
+
assert into(Reg(Celsius(100)), Fahrenheit) == 212.0
|
|
80
|
+
assert try_into(Reg("42"), int) == Ok(42)
|
|
81
|
+
assert try_into(Reg("abc"), int).is_err()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## How it works
|
|
85
|
+
|
|
86
|
+
Every call resolves a conversion by walking a fixed order. A magic method that
|
|
87
|
+
raises `NotImplementedError` counts as missing, and resolution continues.
|
|
88
|
+
|
|
89
|
+
| Call | Resolution order |
|
|
90
|
+
| --------------------- | ----------------------------------------------------------------------------------------- |
|
|
91
|
+
| `into(x, T)` | `x.__into__(T)` |
|
|
92
|
+
| `into(Reg(x), T)` | `x.__into__(T)` → `INTO_CONVERTERS` |
|
|
93
|
+
| `try_into(x, T)` | `x.__try_into__(T)` |
|
|
94
|
+
| `try_into(Reg(x), T)` | `x.__try_into__(T)` → `TRY_INTO_CONVERTERS` → `x.__into__(T)` → `INTO_CONVERTERS` |
|
|
95
|
+
|
|
96
|
+
### Magic methods
|
|
97
|
+
|
|
98
|
+
Implement `__into__` for infallible conversions, or `__try_into__` when the
|
|
99
|
+
conversion can fail.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from rezult import Err, Ok, Result
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Port:
|
|
106
|
+
def __init__(self, text: str) -> None:
|
|
107
|
+
self.text = text
|
|
108
|
+
|
|
109
|
+
def __try_into__(self, into: type[int]) -> Result[int, str]:
|
|
110
|
+
if into is not int:
|
|
111
|
+
return Err("only int is supported")
|
|
112
|
+
if not self.text.isdigit():
|
|
113
|
+
return Err(f"invalid port: {self.text}")
|
|
114
|
+
return Ok(int(self.text))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
assert try_into(Port("8080"), int) == Ok(8080)
|
|
118
|
+
assert try_into(Port("http"), int) == Err("invalid port: http")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`Into[T]` and `TryInto[T, E]` are protocols, so implementing the method is
|
|
122
|
+
enough — no inheritance required.
|
|
123
|
+
|
|
124
|
+
### Registered converters
|
|
125
|
+
|
|
126
|
+
Register a converter when you cannot (or do not want to) change the source
|
|
127
|
+
type. Wrap the source value in `Reg` to select the registry path.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
class Meters:
|
|
131
|
+
def __init__(self, value: float) -> None:
|
|
132
|
+
self.value = value
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class Kilometers:
|
|
136
|
+
def __init__(self, value: float) -> None:
|
|
137
|
+
self.value = value
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
register_into(Meters, Kilometers, lambda meters: Kilometers(meters.value / 1000))
|
|
141
|
+
|
|
142
|
+
assert into(Reg(Meters(1500)), Kilometers).value == 1.5
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
A fallible converter returns a `Result`. Returning a plain value is treated as
|
|
146
|
+
`Ok(value)`, and raising an `Exception` becomes `Err(exception)`.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
register_try_into(
|
|
150
|
+
str,
|
|
151
|
+
int,
|
|
152
|
+
lambda text: Ok(int(text)) if text.isdigit() else Err(ValueError(text)),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
assert try_into(Reg("7"), int) == Ok(7)
|
|
156
|
+
assert isinstance(try_into(Reg("seven"), int), Err)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Converters can be removed again, which is useful in tests:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from into_obj import unregister_into, unregister_try_into
|
|
163
|
+
|
|
164
|
+
unregister_into(Meters, Kilometers)
|
|
165
|
+
unregister_try_into(str, int)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### MRO-aware lookup
|
|
169
|
+
|
|
170
|
+
Registry keys are `(source type, target type)`. Lookup walks the source MRO
|
|
171
|
+
first, then the target MRO, so the nearest source class wins.
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
class SubMeters(Meters):
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
register_into(Meters, Kilometers, lambda meters: Kilometers(meters.value / 1000))
|
|
179
|
+
register_into(SubMeters, Kilometers, lambda meters: Kilometers(-1))
|
|
180
|
+
|
|
181
|
+
into(Reg(Meters(2000)), Kilometers).value # 2.0 (base converter)
|
|
182
|
+
into(Reg(SubMeters(2000)), Kilometers).value # -1 (nearest source wins)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Registering `object` as the target makes a converter apply to every target
|
|
186
|
+
type:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
register_into(Meters, object, lambda meters: meters.value)
|
|
190
|
+
|
|
191
|
+
into(Reg(Meters(3)), int) # 3
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## API reference
|
|
195
|
+
|
|
196
|
+
### Functions
|
|
197
|
+
|
|
198
|
+
| Function | Description |
|
|
199
|
+
| --------------------------------------- | ------------------------------------------------------------------ |
|
|
200
|
+
| `into(from_, target)` | Convert `from_` into `target`, raising on failure. |
|
|
201
|
+
| `try_into(from_, target)` | Convert `from_` into `target`, returning `Result[T, E]`. |
|
|
202
|
+
| `register_into(source, target, conv)` | Register an infallible converter for `(source, target)`. |
|
|
203
|
+
| `register_try_into(source, target, conv)` | Register a fallible converter for `(source, target)`. |
|
|
204
|
+
| `unregister_into(source, target)` | Remove the infallible converter for the pair, if any. |
|
|
205
|
+
| `unregister_try_into(source, target)` | Remove the fallible converter for the pair, if any. |
|
|
206
|
+
|
|
207
|
+
`register_*` validates its arguments and raises `TypeError` if `source` or
|
|
208
|
+
`target` is not a class, or if the converter is not callable.
|
|
209
|
+
|
|
210
|
+
### Types
|
|
211
|
+
|
|
212
|
+
| Name | Description |
|
|
213
|
+
| ------------------------ | ---------------------------------------------------------------------- |
|
|
214
|
+
| `Reg[T]` | Wrapper marking a value as converted by a registered converter. |
|
|
215
|
+
| `Into[T]` | Protocol for types implementing `__into__`. |
|
|
216
|
+
| `TryInto[T, E]` | Protocol for types implementing `__try_into__`. |
|
|
217
|
+
| `IntoSource[S, T]` | `Into[T] \| Reg[S]` — accepted by `into`. |
|
|
218
|
+
| `TryIntoSource[S, T, E]` | `TryInto[T, E] \| Reg[S]` — accepted by `try_into`. |
|
|
219
|
+
| `Converter[T, U]` | `Callable[[T], U]`. |
|
|
220
|
+
| `TryConverter[T, U, E]` | `Callable[[T], Result[U, E]]`. |
|
|
221
|
+
| `INTO_CONVERTERS` | Registry used by `into`. |
|
|
222
|
+
| `TRY_INTO_CONVERTERS` | Registry used by `try_into`. |
|
|
223
|
+
|
|
224
|
+
### Type signatures
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
def into[S, T](from_: IntoSource[S, T], into: type[T]) -> T: ...
|
|
228
|
+
def try_into[S, T, E](from_: TryIntoSource[S, T, E], into: type[T]) -> Result[T, E]: ...
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`S` is the wrapped source type and `T` the target type, which is why `Reg` is
|
|
232
|
+
parameterized separately: `into(Reg(Celsius(100)), Fahrenheit)` infers
|
|
233
|
+
`S = Celsius` and `T = Fahrenheit`. Passing a registered source without `Reg`
|
|
234
|
+
is rejected by type checkers.
|
|
235
|
+
|
|
236
|
+
### Error handling
|
|
237
|
+
|
|
238
|
+
- `into` raises `TypeError` when neither a magic method nor a converter is
|
|
239
|
+
available, and re-raises `NotImplementedError` from `__into__` in that case.
|
|
240
|
+
- `try_into` never raises for conversion failures: exceptions from the
|
|
241
|
+
registry converter or the fallback path are returned as `Err`.
|
|
242
|
+
|
|
243
|
+
## Notes
|
|
244
|
+
|
|
245
|
+
- Registries are process-global dictionaries. Register converters once at
|
|
246
|
+
import time or application startup.
|
|
247
|
+
- Registering the same `(source, target)` pair twice replaces the converter.
|
|
248
|
+
- `Reg` is transparent at runtime: it is unwrapped before resolution, so a
|
|
249
|
+
magic method still takes precedence over a registered converter.
|
|
250
|
+
|
|
251
|
+
## Development
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
uv sync
|
|
255
|
+
uv run pytest
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Tests live in `tests/test_into.py` and isolate the global registries with an
|
|
259
|
+
autouse fixture.
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
MIT
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "into-obj"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Rust-style `Into` & `TryInto` conversions for Python"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
dependencies = ["rezult>=0.2.0"]
|
|
9
|
+
keywords = [
|
|
10
|
+
"rust",
|
|
11
|
+
"rs",
|
|
12
|
+
"convertions",
|
|
13
|
+
"cast",
|
|
14
|
+
"type",
|
|
15
|
+
"into",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Natural Language :: English",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Topic :: Utilities",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[[project.authors]]
|
|
31
|
+
name = "Sequence"
|
|
32
|
+
email = "3863887959@qq.com"
|
|
33
|
+
|
|
34
|
+
[[tool.uv.index]]
|
|
35
|
+
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
36
|
+
default = true
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["uv_build>=0.13.6,<0.14.0"]
|
|
43
|
+
build-backend = "uv_build"
|
|
44
|
+
|
|
45
|
+
[dependency-groups]
|
|
46
|
+
dev = ["pytest>=9.2.0"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "into-obj"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Rust-style `Into` & `TryInto` conversions for Python"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Sequence", email = "3863887959@qq.com" }
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"rezult>=0.2.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
keywords = ["rust", "rs", "convertions", "cast", "type", "into"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Natural Language :: English",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Topic :: Utilities",
|
|
26
|
+
"Typing :: Typed"
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[[tool.uv.index]]
|
|
30
|
+
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
31
|
+
default = true
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["uv_build>=0.13.6,<0.14.0"]
|
|
35
|
+
build-backend = "uv_build"
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = [
|
|
39
|
+
"pytest>=9.2.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Rust-style conversions for Python, built on ``rezult``.
|
|
2
|
+
|
|
3
|
+
The public API is re-exported from `into_obj.into`:
|
|
4
|
+
|
|
5
|
+
- `into` / `try_into`: convert a value into a target type.
|
|
6
|
+
- `Into` / `TryInto`: protocols implemented by self-converting types.
|
|
7
|
+
- `Reg`: wrapper marking values converted by a registered converter.
|
|
8
|
+
- `register_into` / `register_try_into`: register converters.
|
|
9
|
+
- `unregister_into` / `unregister_try_into`: remove converters.
|
|
10
|
+
- `INTO_CONVERTERS` / `TRY_INTO_CONVERTERS`: the converter registries.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .into import (
|
|
14
|
+
INTO_CONVERTERS,
|
|
15
|
+
TRY_INTO_CONVERTERS,
|
|
16
|
+
Converter,
|
|
17
|
+
Into,
|
|
18
|
+
IntoSource,
|
|
19
|
+
Reg,
|
|
20
|
+
TryConverter,
|
|
21
|
+
TryInto,
|
|
22
|
+
TryIntoSource,
|
|
23
|
+
into,
|
|
24
|
+
register_into,
|
|
25
|
+
register_try_into,
|
|
26
|
+
try_into,
|
|
27
|
+
unregister_into,
|
|
28
|
+
unregister_try_into,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"INTO_CONVERTERS",
|
|
33
|
+
"TRY_INTO_CONVERTERS",
|
|
34
|
+
"Converter",
|
|
35
|
+
"Into",
|
|
36
|
+
"IntoSource",
|
|
37
|
+
"Reg",
|
|
38
|
+
"TryConverter",
|
|
39
|
+
"TryInto",
|
|
40
|
+
"TryIntoSource",
|
|
41
|
+
"into",
|
|
42
|
+
"register_into",
|
|
43
|
+
"register_try_into",
|
|
44
|
+
"try_into",
|
|
45
|
+
"unregister_into",
|
|
46
|
+
"unregister_try_into",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main() -> None:
|
|
51
|
+
"""Run the ``into-obj`` console script."""
|
|
52
|
+
print("Hello from into-obj!")
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
# pyright: reportUnnecessaryIsInstance = false
|
|
2
|
+
"""Conversion helpers built on magic methods and registered converters.
|
|
3
|
+
|
|
4
|
+
Two mechanisms are provided:
|
|
5
|
+
|
|
6
|
+
1. Magic methods -- a type implements ``__into__`` (see `Into`) or
|
|
7
|
+
``__try_into__`` (see `TryInto`) and converts itself.
|
|
8
|
+
2. Converter registries -- infallible converters live in `INTO_CONVERTERS`
|
|
9
|
+
and fallible ones in `TRY_INTO_CONVERTERS`, keyed by
|
|
10
|
+
``(source type, target type)``. Values converted this way must be wrapped
|
|
11
|
+
in `Reg`, which keeps the intent visible in the type signature.
|
|
12
|
+
|
|
13
|
+
Resolution order:
|
|
14
|
+
|
|
15
|
+
- `into` prefers ``__into__`` and falls back to `INTO_CONVERTERS`.
|
|
16
|
+
- `try_into` prefers ``__try_into__``, then `TRY_INTO_CONVERTERS`, then the
|
|
17
|
+
same path as `into`.
|
|
18
|
+
|
|
19
|
+
A magic method that raises ``NotImplementedError`` is treated as missing, so
|
|
20
|
+
resolution continues in the registry.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
>>> register_into(int, str, lambda value: f"#{value}")
|
|
24
|
+
>>> into(Reg(42), str)
|
|
25
|
+
'#42'
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from collections.abc import Callable
|
|
29
|
+
from typing import Any, Protocol, cast
|
|
30
|
+
|
|
31
|
+
from rezult import Err, Ok, Result
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Into[T](Protocol):
|
|
35
|
+
"""Protocol for values that convert themselves into another type.
|
|
36
|
+
|
|
37
|
+
Implementing ``__into__`` lets a type be passed to `into` directly,
|
|
38
|
+
without a `Reg` wrapper and without registering a converter.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __into__(self, into: type[T]) -> T:
|
|
42
|
+
"""Convert this value into the given target type.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
into: Target type of the conversion.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
The converted value.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
NotImplementedError: If this type cannot convert into the given
|
|
52
|
+
target type.
|
|
53
|
+
"""
|
|
54
|
+
raise NotImplementedError
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TryInto[T, E](Protocol):
|
|
58
|
+
"""Protocol for values that fallibly convert themselves.
|
|
59
|
+
|
|
60
|
+
Implementing ``__try_into__`` lets a type be passed to `try_into`
|
|
61
|
+
directly, without a `Reg` wrapper and without registering a converter.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __try_into__(self, into: type[T]) -> Result[T, E]:
|
|
65
|
+
"""Try to convert this value into the given target type.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
into: Target type of the conversion.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
``Ok`` with the converted value, or ``Err`` describing why the
|
|
72
|
+
conversion failed.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
NotImplementedError: If this type cannot convert into the given
|
|
76
|
+
target type.
|
|
77
|
+
"""
|
|
78
|
+
raise NotImplementedError
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Reg[T]:
|
|
82
|
+
"""Explicit marker for values converted by a registered converter.
|
|
83
|
+
|
|
84
|
+
`into` and `try_into` only accept registered-converter sources when they
|
|
85
|
+
are wrapped in ``Reg``, which makes the conversion path visible in the
|
|
86
|
+
type signature (``Into[T] | Reg[S]`` and ``TryInto[T, E] | Reg[S]``).
|
|
87
|
+
|
|
88
|
+
The wrapper is transparent at runtime: the value is unwrapped and
|
|
89
|
+
resolved normally, so an implemented magic method still wins over a
|
|
90
|
+
registered converter.
|
|
91
|
+
|
|
92
|
+
Attributes:
|
|
93
|
+
value: The wrapped source value.
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
>>> into(Reg(Meters(1500)), Kilometers)
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
__slots__ = ("value",)
|
|
100
|
+
__match_args__ = ("value",)
|
|
101
|
+
|
|
102
|
+
def __init__(self, value: T) -> None:
|
|
103
|
+
"""Initialize the wrapper.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
value: Source value to convert through a registered converter.
|
|
107
|
+
"""
|
|
108
|
+
self.value: T = value
|
|
109
|
+
|
|
110
|
+
def __repr__(self) -> str:
|
|
111
|
+
"""Return a debug representation such as ``Reg(42)``."""
|
|
112
|
+
return f"Reg({self.value!r})"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
#: Source accepted by `into`: a self-converting value or a `Reg` wrapper.
|
|
116
|
+
type IntoSource[S, T] = Into[T] | Reg[S]
|
|
117
|
+
|
|
118
|
+
#: Source accepted by `try_into`: a fallibly self-converting value or `Reg`.
|
|
119
|
+
type TryIntoSource[S, T, E] = TryInto[T, E] | Reg[S]
|
|
120
|
+
|
|
121
|
+
#: Infallible converter from ``T`` to ``U``.
|
|
122
|
+
type Converter[T, U] = Callable[[T], U]
|
|
123
|
+
|
|
124
|
+
#: Fallible converter from ``T`` to ``U``, reporting failures as ``Result``.
|
|
125
|
+
type TryConverter[T, U, E] = Callable[[T], Result[U, E]]
|
|
126
|
+
|
|
127
|
+
#: Registry used by `into`; keys are ``(source class, target class)``.
|
|
128
|
+
INTO_CONVERTERS: dict[tuple[type, type], Converter[Any, Any]] = {}
|
|
129
|
+
|
|
130
|
+
#: Registry used by `try_into`; keys are ``(source class, target class)``.
|
|
131
|
+
TRY_INTO_CONVERTERS: dict[tuple[type, type], TryConverter[Any, Any, Any]] = {}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def register_into[T, U](
|
|
135
|
+
source: type[T],
|
|
136
|
+
target: type[U],
|
|
137
|
+
converter: Converter[T, U],
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Register an infallible converter for `into`.
|
|
140
|
+
|
|
141
|
+
Keys are matched through both MROs: the nearest source class wins, then
|
|
142
|
+
the nearest target class. Registering ``object`` as the target therefore
|
|
143
|
+
applies the converter to every target type. Registering an existing pair
|
|
144
|
+
replaces the previous converter.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
source: Type of the value being converted.
|
|
148
|
+
target: Type produced by ``converter``.
|
|
149
|
+
converter: Callable receiving the source value and returning the
|
|
150
|
+
converted value.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
TypeError: If ``source`` or ``target`` is not a class, or if
|
|
154
|
+
``converter`` is not callable.
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
>>> register_into(Meters, Kilometers, lambda m: Kilometers(m.value / 1000))
|
|
158
|
+
>>> into(Reg(Meters(1500)), Kilometers).value
|
|
159
|
+
1.5
|
|
160
|
+
"""
|
|
161
|
+
_validate(source, target, converter)
|
|
162
|
+
INTO_CONVERTERS[source, target] = converter
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def register_try_into[T, U, E](
|
|
166
|
+
source: type[T],
|
|
167
|
+
target: type[U],
|
|
168
|
+
converter: TryConverter[T, U, E],
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Register a fallible converter for `try_into`.
|
|
171
|
+
|
|
172
|
+
The converter should return a ``Result``; a plain value is wrapped in
|
|
173
|
+
``Ok`` and a raised ``Exception`` becomes ``Err``. Matching follows the
|
|
174
|
+
same MRO rules as `register_into`.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
source: Type of the value being converted.
|
|
178
|
+
target: Type produced by ``converter``.
|
|
179
|
+
converter: Callable receiving the source value and returning a
|
|
180
|
+
``Result`` (or a plain value, which is wrapped in ``Ok``).
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
TypeError: If ``source`` or ``target`` is not a class, or if
|
|
184
|
+
``converter`` is not callable.
|
|
185
|
+
|
|
186
|
+
Example:
|
|
187
|
+
>>> register_try_into(
|
|
188
|
+
... str,
|
|
189
|
+
... int,
|
|
190
|
+
... lambda text: Ok(int(text)) if text.isdigit() else Err(ValueError(text)),
|
|
191
|
+
... )
|
|
192
|
+
>>> try_into(Reg("42"), int)
|
|
193
|
+
Ok(42)
|
|
194
|
+
"""
|
|
195
|
+
_validate(source, target, converter)
|
|
196
|
+
TRY_INTO_CONVERTERS[source, target] = converter
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def unregister_into(source: type, target: type) -> None:
|
|
200
|
+
"""Remove the converter registered for ``source`` -> ``target``.
|
|
201
|
+
|
|
202
|
+
Does nothing when the pair is not registered.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
source: Source type of the registration to remove.
|
|
206
|
+
target: Target type of the registration to remove.
|
|
207
|
+
"""
|
|
208
|
+
INTO_CONVERTERS.pop((source, target), None)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def unregister_try_into(source: type, target: type) -> None:
|
|
212
|
+
"""Remove the try converter registered for ``source`` -> ``target``.
|
|
213
|
+
|
|
214
|
+
Does nothing when the pair is not registered.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
source: Source type of the registration to remove.
|
|
218
|
+
target: Target type of the registration to remove.
|
|
219
|
+
"""
|
|
220
|
+
TRY_INTO_CONVERTERS.pop((source, target), None)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def into[S, T](from_: IntoSource[S, T], into: type[T]) -> T:
|
|
224
|
+
"""Convert ``from_`` into the given target type.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
from_: Value implementing `Into`, or a `Reg` wrapper around a value
|
|
228
|
+
whose converter is registered in `INTO_CONVERTERS`.
|
|
229
|
+
into: Target type of the conversion.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
The converted value.
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
NotImplementedError: If ``__into__`` raises it and no converter is
|
|
236
|
+
registered for the pair.
|
|
237
|
+
TypeError: If the value has no ``__into__`` and no converter is
|
|
238
|
+
registered for the pair.
|
|
239
|
+
|
|
240
|
+
Example:
|
|
241
|
+
>>> into(Version(3), str)
|
|
242
|
+
'v3'
|
|
243
|
+
>>> into(Reg(Meters(1500)), Kilometers).value
|
|
244
|
+
1.5
|
|
245
|
+
"""
|
|
246
|
+
return cast(T, _convert(_unwrap(from_), into))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def try_into[S, T, E](from_: TryIntoSource[S, T, E], into: type[T]) -> Result[T, E]:
|
|
250
|
+
"""Try to convert ``from_`` into the given target type.
|
|
251
|
+
|
|
252
|
+
Resolution order is ``__try_into__``, `TRY_INTO_CONVERTERS`, then the
|
|
253
|
+
infallible path used by `into` (``__into__`` and `INTO_CONVERTERS`).
|
|
254
|
+
Any exception raised along the way becomes ``Err``.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
from_: Value implementing `TryInto`, or a `Reg` wrapper around a value
|
|
258
|
+
whose converter is registered in `TRY_INTO_CONVERTERS` or
|
|
259
|
+
`INTO_CONVERTERS`.
|
|
260
|
+
into: Target type of the conversion.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
``Ok`` with the converted value, or ``Err`` describing why the
|
|
264
|
+
conversion failed.
|
|
265
|
+
|
|
266
|
+
Example:
|
|
267
|
+
>>> try_into(Reg("42"), int)
|
|
268
|
+
Ok(42)
|
|
269
|
+
"""
|
|
270
|
+
value = _unwrap(from_)
|
|
271
|
+
|
|
272
|
+
magic = getattr(value, "__try_into__", None)
|
|
273
|
+
if magic is not None:
|
|
274
|
+
try:
|
|
275
|
+
return cast(Result[T, E], magic(into))
|
|
276
|
+
except NotImplementedError:
|
|
277
|
+
pass
|
|
278
|
+
|
|
279
|
+
converter = _lookup(TRY_INTO_CONVERTERS, value, into)
|
|
280
|
+
if converter is not None:
|
|
281
|
+
try:
|
|
282
|
+
result = converter(value)
|
|
283
|
+
except Exception as error:
|
|
284
|
+
return Err(cast(E, error))
|
|
285
|
+
if isinstance(result, Result):
|
|
286
|
+
return cast(Result[T, E], result)
|
|
287
|
+
return Ok(cast(T, result))
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
return Ok(_convert(value, into))
|
|
291
|
+
except Exception as error:
|
|
292
|
+
return Err(cast(E, error))
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _unwrap(from_: object) -> object:
|
|
296
|
+
"""Return the value inside a `Reg` wrapper, or ``from_`` unchanged.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
from_: Possibly wrapped source value.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
The unwrapped source value.
|
|
303
|
+
"""
|
|
304
|
+
return from_.value if isinstance(from_, Reg) else from_ # type: ignore
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _convert(from_: object, target: type) -> Any:
|
|
308
|
+
"""Resolve and run an infallible conversion.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
from_: Unwrapped source value.
|
|
312
|
+
target: Target type of the conversion.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
The converted value.
|
|
316
|
+
|
|
317
|
+
Raises:
|
|
318
|
+
NotImplementedError: If ``__into__`` raises it and no converter is
|
|
319
|
+
registered for the pair.
|
|
320
|
+
TypeError: If the value has no ``__into__`` and no converter is
|
|
321
|
+
registered for the pair.
|
|
322
|
+
"""
|
|
323
|
+
magic = getattr(from_, "__into__", None)
|
|
324
|
+
if magic is not None:
|
|
325
|
+
try:
|
|
326
|
+
return magic(target)
|
|
327
|
+
except NotImplementedError:
|
|
328
|
+
converter = _lookup(INTO_CONVERTERS, from_, target)
|
|
329
|
+
if converter is None:
|
|
330
|
+
raise
|
|
331
|
+
return converter(from_)
|
|
332
|
+
|
|
333
|
+
converter = _lookup(INTO_CONVERTERS, from_, target)
|
|
334
|
+
if converter is None:
|
|
335
|
+
raise TypeError(
|
|
336
|
+
f"no conversion from {type(from_).__qualname__!r} "
|
|
337
|
+
f"to {getattr(target, '__qualname__', target)!r}"
|
|
338
|
+
)
|
|
339
|
+
return converter(from_)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _lookup[T](
|
|
343
|
+
registry: dict[tuple[type, type], T],
|
|
344
|
+
value: object,
|
|
345
|
+
target: type,
|
|
346
|
+
) -> T | None:
|
|
347
|
+
"""Find a converter by walking the source and target MROs.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
registry: Registry to search.
|
|
351
|
+
value: Source value whose type starts the source lookup.
|
|
352
|
+
target: Target type that starts the target lookup.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
The first matching converter, or ``None`` when nothing matches.
|
|
356
|
+
"""
|
|
357
|
+
target_mro = getattr(target, "__mro__", (target,))
|
|
358
|
+
for source_cls in type(value).__mro__:
|
|
359
|
+
for target_cls in target_mro:
|
|
360
|
+
converter = registry.get((source_cls, target_cls))
|
|
361
|
+
if converter is not None:
|
|
362
|
+
return converter
|
|
363
|
+
return None
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _validate(source: type, target: type, converter: Callable[..., Any]) -> None:
|
|
367
|
+
"""Validate the arguments of a registration helper.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
source: Type of the value being converted.
|
|
371
|
+
target: Type produced by ``converter``.
|
|
372
|
+
converter: Callable performing the conversion.
|
|
373
|
+
|
|
374
|
+
Raises:
|
|
375
|
+
TypeError: If ``source`` or ``target`` is not a class, or if
|
|
376
|
+
``converter`` is not callable.
|
|
377
|
+
"""
|
|
378
|
+
if not isinstance(source, type) or not isinstance(target, type):
|
|
379
|
+
raise TypeError("source and target must be classes")
|
|
380
|
+
if not callable(converter):
|
|
381
|
+
raise TypeError("converter must be callable")
|
|
File without changes
|