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.
@@ -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
@@ -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