typewire 0.1.0__py3-none-any.whl

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.
typewire/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .caster import as_type, is_iterable, is_mapping, is_union, TypeHint
2
+
3
+ __all__ = ["as_type", "is_iterable", "is_mapping", "is_union", "TypeHint"]
typewire/caster.py ADDED
@@ -0,0 +1,181 @@
1
+ from collections.abc import Iterable, Mapping
2
+ from contextlib import suppress
3
+ import inspect
4
+ import types
5
+ from typing import Annotated, Any, get_args, get_origin, Literal, TypeAlias, TypeVar, Union
6
+
7
+ TypeHint: TypeAlias = Any
8
+
9
+
10
+ def is_union(type_hint: TypeHint) -> bool:
11
+ """Determine whether the given type represents a union type."""
12
+ return type_hint is Union or (hasattr(types, "UnionType") and isinstance(type_hint, types.UnionType))
13
+
14
+
15
+ def is_mapping(type_hint: TypeHint) -> bool:
16
+ """Determine whether the given type represents a mapping type."""
17
+ origin = get_origin(type_hint)
18
+ real_type = origin if origin is not None else type_hint
19
+ return isinstance(real_type, type) and issubclass(real_type, Mapping)
20
+
21
+
22
+ def is_iterable(type_hint: TypeHint) -> bool:
23
+ """Determine whether the given type represents an iterable type."""
24
+ origin = get_origin(type_hint)
25
+ real_type = origin if origin is not None else type_hint
26
+ return isinstance(real_type, type) and issubclass(real_type, Iterable) and real_type not in (str, bytes)
27
+
28
+
29
+ def as_type(value: Any, to: TypeHint, *, transparent_int: bool = False, semantic_bool: bool = False) -> Any:
30
+ """Cast a value to the given type hint.
31
+
32
+ :param value:
33
+ The raw input value to cast.
34
+ :param to:
35
+ The type hint to cast to.
36
+ :param transparent_int:
37
+ Whether to allow more transparent casting to int.
38
+ For example, int("1.0") raises a ValueError, so as_type("1.0", int) raises a ValueError as well.
39
+ However, as_type("1.0", int, transparent_int=True) will return 1.
40
+ This passes the conversion to float, then int, so as_type("1.3", int, transparent_int=True) returns 1.
41
+ :param semantic_bool:
42
+ Whether to allow for more semantic casting to bool.
43
+ For example, bool("false") returns True, so as_type("false", bool) returns True.
44
+ However, as_type("false", bool, semantic_bool=True) returns False.
45
+
46
+ :return: The casted value.
47
+ """
48
+
49
+ # We can't cast to Any or an unbound TypeVar, so just return the value as-is
50
+ if to is Any or isinstance(to, TypeVar):
51
+ return value
52
+
53
+ origin: Any = get_origin(to)
54
+ args: Any = get_args(to)
55
+
56
+ # reach into Annotated
57
+ if origin is Annotated:
58
+ to = get_args(to)[0]
59
+ origin = get_origin(to)
60
+ args = get_args(to)
61
+
62
+ # handle unions
63
+ if is_union(to):
64
+ for type_hint in get_args(to):
65
+ if type_hint is type(None) and value is None:
66
+ return None
67
+
68
+ with suppress(ValueError, TypeError):
69
+ return as_type(value, type_hint, transparent_int=transparent_int, semantic_bool=semantic_bool)
70
+ else:
71
+ raise ValueError(f"Value {value!r} does not match any type in {to}")
72
+
73
+ # handle literals
74
+ if origin is Literal:
75
+ if value in args:
76
+ return value
77
+
78
+ raise ValueError(f"Value {value!r} does not match any literal in {to}")
79
+
80
+ # If `to` is a plain type (e.g., int), then origin is None. But we want something we can actually call.
81
+ real_type = origin if origin is not None else to
82
+
83
+ # handle unions (e.g., int | float | None)
84
+ if is_union(real_type):
85
+ for type_hint in args:
86
+ # if value is None, then we can allow None in the union
87
+ if type_hint is type(None):
88
+ if value is None:
89
+ return None
90
+ continue
91
+
92
+ # otherwise, try to find the first matching type
93
+ with suppress(ValueError, TypeError):
94
+ return as_type(value, type_hint, transparent_int=transparent_int, semantic_bool=semantic_bool)
95
+ else:
96
+ # none of the types match
97
+ raise ValueError(f"Value {value!r} does not match any type in {to}")
98
+
99
+ # handle mappings
100
+ if is_mapping(real_type):
101
+ if not isinstance(value, Mapping):
102
+ # input is a list of pairs like [("a", 1), ("b", 2)]
103
+ try:
104
+ value = dict(value)
105
+ except ValueError:
106
+ raise ValueError(f"Value {value!r} is not a mapping")
107
+
108
+ key_type = args[0] if args else Any
109
+ val_type = args[1] if len(args) > 1 else Any
110
+
111
+ dct = {
112
+ as_type(key, key_type, transparent_int=transparent_int, semantic_bool=semantic_bool): as_type(
113
+ val, val_type, transparent_int=transparent_int, semantic_bool=semantic_bool
114
+ )
115
+ for key, val in value.items()
116
+ }
117
+
118
+ if inspect.isabstract(real_type) and isinstance(value, real_type):
119
+ # We can't cast to an abstract container, so just return the dict that we have
120
+ return dct
121
+
122
+ return real_type(dct)
123
+
124
+ # handle containers
125
+ if is_iterable(real_type):
126
+ if isinstance(value, (str, bytes)) and isinstance(value, real_type):
127
+ # specifically handle Iterable[str] and Iterable[bytes] as simply str and bytes
128
+ return value
129
+
130
+ # default to str if the inner type is not set, e.g. x: list
131
+ inner_type = args[0] if args else Any
132
+
133
+ # if tuple[T, T] fixed length
134
+ if origin is tuple and args and Ellipsis not in args:
135
+ if len(args) != len(value):
136
+ raise ValueError(f"Expected tuple of length {len(args)}, got {len(value)}")
137
+
138
+ return tuple(
139
+ as_type(v, t, transparent_int=transparent_int, semantic_bool=semantic_bool) for v, t in zip(value, args)
140
+ )
141
+
142
+ # otherwise, it's a variadic container
143
+ vals = (
144
+ as_type(
145
+ v,
146
+ inner_type,
147
+ transparent_int=transparent_int,
148
+ semantic_bool=semantic_bool,
149
+ )
150
+ for v in value
151
+ )
152
+
153
+ if inspect.isabstract(real_type):
154
+ # We can't cast to an abstract container, so just return the value as a list
155
+ return list(vals)
156
+
157
+ return real_type(vals)
158
+
159
+ # handle possible semantic conversions
160
+ if to is int and transparent_int:
161
+ with suppress(ValueError, TypeError):
162
+ return int(float(value))
163
+
164
+ if to is bool and semantic_bool and isinstance(value, str):
165
+ normalized = value.lower()
166
+
167
+ if normalized in ("true", "yes", "1", "on"):
168
+ return True
169
+
170
+ if normalized in ("false", "no", "0", "off"):
171
+ return False
172
+
173
+ if isinstance(real_type, type) and callable(real_type):
174
+ if inspect.isabstract(real_type):
175
+ # We can't instantiate an abstract class, so just return the value
176
+ return value
177
+
178
+ return real_type(value)
179
+
180
+ # fallback
181
+ return to(value)
typewire/py.typed ADDED
File without changes
@@ -0,0 +1,186 @@
1
+ Metadata-Version: 2.4
2
+ Name: typewire
3
+ Version: 0.1.0
4
+ Summary: A single-file utility to allow better runtime handling of types by providing a predictable way of transforming data into the shape of a given type hint.
5
+ Project-URL: repository, https://github.com/lilellia/typewire
6
+ Project-URL: Bug Tracker, https://github.com/lilellia/typewire/issues
7
+ Author-email: Lily Ellington <lilell_@outlook.com>
8
+ License-File: LICENSE
9
+ Keywords: annotated,casting,runtime-types,type-casting,type-hints,typing
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+
26
+ # typewire
27
+
28
+ A single-file utility to allow better runtime handling of types by providing a predictable way of transforming data into the shape of a given type hint.
29
+
30
+ ## Why?
31
+
32
+ Python's standard library provides tools for describing types via type hints, but it doesn't provide a unified way of actually enforcing those type hints at runtime, even buried deep in the `typing` module.
33
+
34
+ Our goal is to allow for `x: T` to behave transparently as usual whilst also allowing the user to convert to that type, regardless of whether that is `x: int` or `x: float | None` or `x: dict[str, list[int | dict[float, User]]]`. Just like `int(x)` will (to the best of its ability) turn `x` into an `int`, `typewire.as_type(x, int)` will do the same, but with the added benefit of working on type hints that aren't callable (like `list[float]`).
35
+
36
+ ```py
37
+ >>> from typewire import as_type
38
+
39
+ >>> as_type("3.2", float)
40
+ 3.2
41
+
42
+ >>> as_type(78.1, int)
43
+ 78
44
+
45
+ >>> as_type("3.2", int, transparent_int=True)
46
+ 3
47
+
48
+ >>> as_type(["13.2", "18", "-1.2"], list[int | float])
49
+ [13.2, 18, -1.2]
50
+
51
+ >>> as_type([("a", "1"), ("b", "2"), ("c", "3"), ("z", "26")], dict[str, float])
52
+ {'a': 1.0, 'b': 2.0, 'c': 3.0, 'z': 26.0}
53
+
54
+ >>> from pathlib import Path
55
+ >>> data = {"logs": ["/tmp/app.log", "123"]}
56
+ >>> hint = dict[str, list[Path | int]]
57
+ >>> as_type(data, hint)
58
+ {'logs': [Path('/tmp/app.log'), 123]}
59
+ ```
60
+
61
+ ## Installation
62
+
63
+ `typewire` is supported on Python 3.10 and onward and can be easily installed with a package manager such as:
64
+
65
+ ```bash
66
+ # using pip
67
+ $ pip install typewire
68
+
69
+ # using uv
70
+ $ uv add typewire
71
+ ```
72
+
73
+ `typewire` does not have any additional dependencies.
74
+
75
+ ## Documentation
76
+
77
+ ### `TypeHint`
78
+
79
+ `TypeHint` is provided as a top-level alias for `typing.Any`.
80
+
81
+ ### `is_union`, `is_mapping`, `is_iterable`
82
+
83
+ These three functions check whether a given type hint is a union type (e.g., `int | str | bytes`), a mapping type (e.g., `dict[str, Any]`), or an iterable type (`e.g., list[str]`).
84
+
85
+ Note that `is_iterable` specifically excludes `str` and `bytes`: `is_iterable(str) == False` as, while `str` does support iteration, for the purposes of type casting, it's not really an iterable/container type.
86
+
87
+ ### `as_type`
88
+
89
+ The signature is
90
+
91
+ ```py
92
+ def as_type(value: Any, to: TypeHint, *, transparent_int: bool = False, semantic_bool: False = False) -> Any:
93
+ ...
94
+ ```
95
+
96
+ In particular, it casts the given `value` to the given `to` type, regardless of whether `to` is:
97
+
98
+ ```py
99
+
100
+ # a plain type
101
+ >>> as_type(3.2, int)
102
+ 3
103
+
104
+ # typing.Literal, returning the value as-is if it's a valid entry
105
+ >>> as_type("abc", Literal["abc", "def"])
106
+ 'abc'
107
+
108
+ >>> as_type("80", Literal[80, 443])
109
+ ValueError(...)
110
+
111
+ # a union type, casting to the first valid type
112
+ >>> as_type("3", float | int)
113
+ 3.0
114
+
115
+ >>> as_type("3", int | float)
116
+ 3
117
+
118
+ # an optional type
119
+ >>> as_type(43, int | None)
120
+ 43
121
+
122
+ >>> as_type(None, int | None)
123
+ None
124
+
125
+ # a mapping type
126
+ >>> as_type({"a": "1", "b": "2.0"}, dict[str, float])
127
+ {'a': 1.0, 'b': 2.0}
128
+
129
+ # a container/iterable type
130
+ >>> as_type([1.2, -3, 449], list[str])
131
+ ['1.2', '-3', '449']
132
+
133
+ >>> as_type([1.2, -3, 449], tuple[str, ...])
134
+ ('1.2', '-3', '449')
135
+
136
+ # typing.Annotated, treating it as the bare type
137
+ >>> as_type("3", Annotated[int, "some metadata"])
138
+ 3
139
+
140
+ # an abstract collections.abc.Iterable/Mapping, cast as concrete list/dict
141
+ >>> as_type([1.2, -3, 449], Iterable[str])
142
+ ['1.2', '-3', '449']
143
+
144
+ >>> as_type({"a": "1", "b": "2.0"}, Mapping[str, float])
145
+ {'a': 1.0, 'b': 2.0}
146
+
147
+ # ...unless it's a string being cast as Iterable[str]
148
+ >>> as_type("hello world", Iterable[str])
149
+ 'hello world'
150
+ ```
151
+
152
+ On a failure, `ValueError` is raised.
153
+
154
+ #### `transparent_int`
155
+
156
+ This flag (default = False) allows for a nonstrict cast to `int`.
157
+
158
+ ```py
159
+ >>> int("3.2", int)
160
+ ValueError # invalid literal for int() with base 10: '3.2'
161
+
162
+ >>> as_type("3.2", int)
163
+ ValueError # invalid literal for int() with base 10: '3.2'
164
+
165
+ >>> as_type("3.2", int, transparent_int = True)
166
+ 3
167
+ ```
168
+
169
+ In practice, this flag results in a call of `int(float(value))` instead of just `int(value)`.
170
+
171
+ #### `semantic_bool`
172
+
173
+ This flag (default = False) allows for a nonstrict cast to `bool`.
174
+
175
+ ```py
176
+ >>> bool("false") # non-empty string
177
+ True
178
+
179
+ >>> as_type("false", bool)
180
+ True
181
+
182
+ >>> as_type("false", bool, semantic_bool = True)
183
+ False
184
+ ```
185
+
186
+ In practice, if `value` is a string and is one of `["false", "no", "0", "off"]` (case-insensitive), then it will be cast as `False` with this flag enabled.
@@ -0,0 +1,7 @@
1
+ typewire/__init__.py,sha256=j6T9wa5TbKAfZmB_7PqbYGQ3Miu63JRzvHaEoKIgMiE,149
2
+ typewire/caster.py,sha256=RaiIZg5ClhnJDBFwh1GXsnGKN7ZglWx1EwANF2KsXic,6613
3
+ typewire/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ typewire-0.1.0.dist-info/METADATA,sha256=kXGOByc2kqf21ZpN_cHNYNgR0zTle0af8x6B2HVZZxo,5541
5
+ typewire-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
+ typewire-0.1.0.dist-info/licenses/LICENSE,sha256=jYMMQTZU3uN4XkybqyCDbjVfv9wWQRGPDkBebAxS-SQ,1065
7
+ typewire-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 lilellia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.