thds.attrs-utils 1.6.20251124154148__py3-none-any.whl → 1.7.20251204022717__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.
- thds/attrs_utils/cattrs/converter.py +2 -2
- thds/attrs_utils/docs.py +2 -1
- thds/attrs_utils/empty.py +5 -4
- thds/attrs_utils/isinstance/check.py +2 -1
- thds/attrs_utils/jsonschema/jsonschema.py +3 -2
- thds/attrs_utils/params/__init__.py +10 -0
- thds/attrs_utils/params/parameterize.py +90 -0
- thds/attrs_utils/params/records.py +98 -0
- thds/attrs_utils/params/utils.py +27 -0
- thds/attrs_utils/random/attrs.py +4 -3
- thds/attrs_utils/random/gen.py +9 -5
- thds/attrs_utils/registry.py +1 -1
- thds/attrs_utils/type_recursion.py +3 -0
- thds/attrs_utils/type_utils.py +12 -21
- thds/attrs_utils/utils.py +12 -0
- {thds_attrs_utils-1.6.20251124154148.dist-info → thds_attrs_utils-1.7.20251204022717.dist-info}/METADATA +10 -7
- {thds_attrs_utils-1.6.20251124154148.dist-info → thds_attrs_utils-1.7.20251204022717.dist-info}/RECORD +19 -14
- {thds_attrs_utils-1.6.20251124154148.dist-info → thds_attrs_utils-1.7.20251204022717.dist-info}/WHEEL +0 -0
- {thds_attrs_utils-1.6.20251124154148.dist-info → thds_attrs_utils-1.7.20251204022717.dist-info}/top_level.txt +0 -0
|
@@ -15,7 +15,7 @@ UnStruct = Callable[[Any], T]
|
|
|
15
15
|
StructFactory = Callable[[Any], Struct[T]]
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
PREJSON_UNSTRUCTURE_COLLECTION_OVERRIDES = {
|
|
18
|
+
PREJSON_UNSTRUCTURE_COLLECTION_OVERRIDES: ty.Mapping[Type, Callable[[], ty.Collection]] = {
|
|
19
19
|
ty.Set: list,
|
|
20
20
|
ty.FrozenSet: list,
|
|
21
21
|
ty.AbstractSet: list,
|
|
@@ -173,7 +173,7 @@ def default_converter(
|
|
|
173
173
|
prefer_attrib_converters: bool = True,
|
|
174
174
|
unstruct_collection_overrides: ty.Mapping[
|
|
175
175
|
Type, Callable[[], ty.Collection]
|
|
176
|
-
] = PREJSON_UNSTRUCTURE_COLLECTION_OVERRIDES,
|
|
176
|
+
] = PREJSON_UNSTRUCTURE_COLLECTION_OVERRIDES,
|
|
177
177
|
) -> Converter:
|
|
178
178
|
return GenConverter(
|
|
179
179
|
unstruct_collection_overrides=unstruct_collection_overrides,
|
thds/attrs_utils/docs.py
CHANGED
|
@@ -7,7 +7,7 @@ from typing import Callable, Dict, List, Literal, Optional, Set, Type
|
|
|
7
7
|
|
|
8
8
|
from docstring_parser import Docstring, DocstringMeta, DocstringParam, Style, parse
|
|
9
9
|
|
|
10
|
-
from .type_utils import bases
|
|
10
|
+
from .type_utils import bases, get_origin
|
|
11
11
|
|
|
12
12
|
DocCombineSpec = Literal["first", "join"]
|
|
13
13
|
|
|
@@ -29,6 +29,7 @@ def record_class_docs(
|
|
|
29
29
|
join_sep: str = "\n\n",
|
|
30
30
|
require_complete: bool = False,
|
|
31
31
|
) -> Docstring:
|
|
32
|
+
cls = get_origin(cls) or cls # unwrap parameterized generics
|
|
32
33
|
base_clss = [c for c in bases(cls, filter_bases) if c is not object]
|
|
33
34
|
|
|
34
35
|
docs = [parse(c.__doc__, style=style) for c in base_clss if c.__doc__]
|
thds/attrs_utils/empty.py
CHANGED
|
@@ -6,9 +6,10 @@ import uuid
|
|
|
6
6
|
import warnings
|
|
7
7
|
from functools import partial
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
import attrs
|
|
10
10
|
|
|
11
11
|
from . import recursion, type_recursion, type_utils
|
|
12
|
+
from .params import attrs_fields_parameterized
|
|
12
13
|
from .registry import Registry
|
|
13
14
|
from .type_recursion import Constructor
|
|
14
15
|
|
|
@@ -67,9 +68,9 @@ def _record_constructor(
|
|
|
67
68
|
return f
|
|
68
69
|
|
|
69
70
|
|
|
70
|
-
def empty_attrs(empty_gen, type_: ty.Type[
|
|
71
|
-
fields =
|
|
72
|
-
defaults = {f.name: empty_gen(f.type) for f in fields if f.default is
|
|
71
|
+
def empty_attrs(empty_gen, type_: ty.Type[attrs.AttrsInstance]) -> Constructor[attrs.AttrsInstance]:
|
|
72
|
+
fields = attrs_fields_parameterized(type_)
|
|
73
|
+
defaults = {f.name: empty_gen(f.type) for f in fields if f.default is attrs.NOTHING}
|
|
73
74
|
# keep the original defaults and default factories
|
|
74
75
|
return _record_constructor(type_, defaults)
|
|
75
76
|
|
|
@@ -3,6 +3,7 @@ from typing import Any, NamedTuple, Type, cast, get_args, get_origin
|
|
|
3
3
|
import attrs
|
|
4
4
|
|
|
5
5
|
from .. import recursion, type_utils
|
|
6
|
+
from ..params import attrs_fields_parameterized
|
|
6
7
|
from ..type_recursion import TypeRecursion
|
|
7
8
|
from . import util
|
|
8
9
|
from .registry import ISINSTANCE_REGISTRY
|
|
@@ -26,7 +27,7 @@ def check_literal(instancecheck, type_: Type):
|
|
|
26
27
|
|
|
27
28
|
def check_attrs(instancecheck, type_: Type[attrs.AttrsInstance]):
|
|
28
29
|
# this _should_ typecheck according to my understanding of attrs.AttrsInstance but it is not
|
|
29
|
-
fields =
|
|
30
|
+
fields = attrs_fields_parameterized(type_)
|
|
30
31
|
names = tuple(f.name for f in fields)
|
|
31
32
|
types = (f.type for f in fields)
|
|
32
33
|
return util.check_attrs(
|
|
@@ -13,6 +13,7 @@ from thds.core import scope
|
|
|
13
13
|
from thds.core.stack_context import StackContext
|
|
14
14
|
|
|
15
15
|
from ..cattrs import DEFAULT_JSON_CONVERTER
|
|
16
|
+
from ..params import attrs_fields_parameterized
|
|
16
17
|
from ..recursion import RecF, value_error
|
|
17
18
|
from ..registry import Registry
|
|
18
19
|
from ..type_recursion import TypeRecursion
|
|
@@ -181,7 +182,7 @@ def gen_jsonschema_attrs(
|
|
|
181
182
|
all_attributes_required = AllAttributesRequired()
|
|
182
183
|
|
|
183
184
|
type_ = attr.resolve_types(type_)
|
|
184
|
-
attrs =
|
|
185
|
+
attrs = attrs_fields_parameterized(type_)
|
|
185
186
|
properties: Dict[str, Any] = {}
|
|
186
187
|
required: List[str] = []
|
|
187
188
|
|
|
@@ -213,7 +214,7 @@ def gen_jsonschema_attrs(
|
|
|
213
214
|
default = None if null_default else at.default
|
|
214
215
|
attr_schema[DEFAULT] = serializer(default)
|
|
215
216
|
|
|
216
|
-
properties[at.name] = attr_schema
|
|
217
|
+
properties[at.name] = attr_schema
|
|
217
218
|
|
|
218
219
|
return object_(properties=properties, required=required, additionalProperties=False)
|
|
219
220
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"attrs_fields_parameterized",
|
|
3
|
+
"dataclass_fields_parameterized",
|
|
4
|
+
"field_origins",
|
|
5
|
+
"parameterize",
|
|
6
|
+
"parameterized_mro",
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
from .parameterize import parameterize, parameterized_mro
|
|
10
|
+
from .records import attrs_fields_parameterized, dataclass_fields_parameterized, field_origins
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Utilities for parameterizing generic types and analyzing parameterized generic types."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import itertools
|
|
5
|
+
import typing as ty
|
|
6
|
+
|
|
7
|
+
import typing_inspect as ti
|
|
8
|
+
|
|
9
|
+
from ..utils import signature_preserving_cache
|
|
10
|
+
from .utils import TypeOrVar, origin_params_and_args
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@ty.overload
|
|
14
|
+
def parameterize(
|
|
15
|
+
type_: ty.TypeVar,
|
|
16
|
+
params: ty.Union[ty.Mapping[ty.TypeVar, TypeOrVar], ty.Type],
|
|
17
|
+
) -> TypeOrVar: ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@ty.overload
|
|
21
|
+
def parameterize(
|
|
22
|
+
type_: ty.Type,
|
|
23
|
+
params: ty.Union[ty.Mapping[ty.TypeVar, TypeOrVar], ty.Type],
|
|
24
|
+
) -> ty.Type: ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parameterize(
|
|
28
|
+
type_: TypeOrVar,
|
|
29
|
+
params: ty.Union[ty.Mapping[ty.TypeVar, TypeOrVar], ty.Type],
|
|
30
|
+
) -> TypeOrVar:
|
|
31
|
+
"""Given a generic type (or type variable) `type_` and a mapping from type variables to concrete types, return a new
|
|
32
|
+
type where the type variables have been substituted according to the mapping. If `params` is itself a parameterized
|
|
33
|
+
generic type, extract its type parameters and arguments and use those to build the mapping. If `type_` is not
|
|
34
|
+
generic, return it unchanged.
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
>>> from typing import Generic, TypeVar, List, Dict
|
|
38
|
+
>>> T = TypeVar('T')
|
|
39
|
+
>>> U = TypeVar('U')
|
|
40
|
+
>>> V = TypeVar('V')
|
|
41
|
+
>>> parameterize(int, {T: str}) == int
|
|
42
|
+
True
|
|
43
|
+
>>> parameterize(T, {T: int}) == int
|
|
44
|
+
True
|
|
45
|
+
>>> parameterize(List[T], {T: str}) == List[str]
|
|
46
|
+
True
|
|
47
|
+
>>> parameterize(Dict[T, U], {T: int, U: str, V: float}) == Dict[int, str]
|
|
48
|
+
True
|
|
49
|
+
>>> class MyGeneric(Generic[T, U]): ...
|
|
50
|
+
>>> parameterize(MyGeneric, MyGeneric[int, str]) == MyGeneric[int, str]
|
|
51
|
+
True
|
|
52
|
+
>>> parameterize(List[T], MyGeneric[int, str]) == List[int]
|
|
53
|
+
True
|
|
54
|
+
"""
|
|
55
|
+
if not isinstance(params, ty.Mapping):
|
|
56
|
+
_, params_, args, _ = origin_params_and_args(params)
|
|
57
|
+
return parameterize(type_, dict(zip(params_, args)))
|
|
58
|
+
elif isinstance(type_, ty.TypeVar):
|
|
59
|
+
return params.get(type_, type_)
|
|
60
|
+
elif tparams := ti.get_parameters(type_):
|
|
61
|
+
new_args = tuple(params.get(t, t) for t in tparams)
|
|
62
|
+
return type_[new_args]
|
|
63
|
+
else:
|
|
64
|
+
return type_
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@signature_preserving_cache
|
|
68
|
+
def parameterized_mro(type_: ty.Type) -> ty.Tuple[ty.Type, ...]:
|
|
69
|
+
"""All generic bases of a generic type, with type parameters substituted according to their specification in `type_`,
|
|
70
|
+
in python method resolution order. In case `type_` is not generic, just return the standard MRO.
|
|
71
|
+
"""
|
|
72
|
+
# property-based test idea:
|
|
73
|
+
# map(partial(parameterize, args), parameterized_bases(t)) == parameterized_bases(t[args])
|
|
74
|
+
|
|
75
|
+
def inner(type_: ty.Type, visited: ty.Set[ty.Type]) -> ty.Iterator[ty.Type]:
|
|
76
|
+
origin, params, args, parameterized = origin_params_and_args(type_)
|
|
77
|
+
if origin not in visited and origin is not ty.Generic:
|
|
78
|
+
mapping = dict(zip(params, args))
|
|
79
|
+
yield type_ if parameterized else parameterize(origin, mapping)
|
|
80
|
+
visited.add(origin)
|
|
81
|
+
for base in ti.get_generic_bases(origin):
|
|
82
|
+
if ti.get_origin(base) is not ty.Generic:
|
|
83
|
+
yield from inner(parameterize(base, mapping), visited)
|
|
84
|
+
|
|
85
|
+
if not ti.is_generic_type(type_):
|
|
86
|
+
return inspect.getmro(type_)
|
|
87
|
+
else:
|
|
88
|
+
mro = dict(zip(inspect.getmro(ti.get_origin(type_) or type_), itertools.count()))
|
|
89
|
+
pmro = sorted(inner(type_, set()), key=lambda type_: mro[ti.get_origin(type_) or type_])
|
|
90
|
+
return tuple(pmro)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Special handling of typevar resolution for `attrs` and `dataclasses` record types."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import dataclasses
|
|
5
|
+
import typing as ty
|
|
6
|
+
|
|
7
|
+
import attrs
|
|
8
|
+
import typing_inspect as ti
|
|
9
|
+
|
|
10
|
+
from ..utils import signature_preserving_cache
|
|
11
|
+
from .parameterize import parameterize, parameterized_mro
|
|
12
|
+
from .utils import TypeOrVar
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def field_origins(
|
|
16
|
+
cls: ty.Type,
|
|
17
|
+
) -> ty.Dict[str, ty.Type]:
|
|
18
|
+
"""Map field names of a record type (e.g. `dataclasses` or `attrs`) to the parameterized base class where the field
|
|
19
|
+
is first defined."""
|
|
20
|
+
|
|
21
|
+
# find first class in MRO where each field is defined
|
|
22
|
+
field_origins: ty.Dict[str, ty.Type] = dict()
|
|
23
|
+
for base in parameterized_mro(cls):
|
|
24
|
+
origin = ti.get_origin(base) or base
|
|
25
|
+
for name in getattr(origin, "__annotations__", {}).keys():
|
|
26
|
+
if name not in field_origins:
|
|
27
|
+
field_origins[name] = base
|
|
28
|
+
|
|
29
|
+
return field_origins
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _replace_dataclass_field_type(
|
|
33
|
+
dataclass_field: dataclasses.Field, new_type: TypeOrVar
|
|
34
|
+
) -> dataclasses.Field:
|
|
35
|
+
new_field = copy.copy(dataclass_field)
|
|
36
|
+
new_field.type = new_type # type: ignore[assignment]
|
|
37
|
+
# ^ NOTE: mypy thinks dataclasses.Field.type is always 'type', but it can be any type hint
|
|
38
|
+
return new_field
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_FT = ty.TypeVar("_FT", attrs.Attribute, dataclasses.Field)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _parameterize_attrs_or_dataclass_field(
|
|
45
|
+
field_origins: ty.Mapping[str, ty.Type],
|
|
46
|
+
field: _FT,
|
|
47
|
+
) -> _FT:
|
|
48
|
+
if (parameterized_origin := field_origins.get(field.name)) is not None:
|
|
49
|
+
field_type = type(None) if field.type is None else field.type
|
|
50
|
+
# shouldn't happen in pracice after type resolution, but we keep it here to make mypy happy
|
|
51
|
+
concrete_type = parameterize(field_type, parameterized_origin)
|
|
52
|
+
if concrete_type == field.type:
|
|
53
|
+
return field
|
|
54
|
+
elif isinstance(field, attrs.Attribute):
|
|
55
|
+
return field.evolve(type=concrete_type)
|
|
56
|
+
else:
|
|
57
|
+
# dataclasses.Field
|
|
58
|
+
return _replace_dataclass_field_type(field, concrete_type)
|
|
59
|
+
else:
|
|
60
|
+
return field
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@signature_preserving_cache
|
|
64
|
+
def attrs_fields_parameterized(
|
|
65
|
+
attrs_cls: ty.Type[attrs.AttrsInstance],
|
|
66
|
+
) -> ty.Sequence[attrs.Attribute]:
|
|
67
|
+
"""`attrs.fields` does not resolve typevars in the field types when base classes provide type parameters.
|
|
68
|
+
This appears to be true even for classes decorated with `attrs.resolve_types`, which may only resolve ForwardRefs.
|
|
69
|
+
This function has the same signature as `attrs.fields` but returns `Attribute`s with fully resolved `type` attributes.
|
|
70
|
+
"""
|
|
71
|
+
attrs.resolve_types(
|
|
72
|
+
ti.get_origin(attrs_cls) or attrs_cls, include_extras=True
|
|
73
|
+
) # this mutates in place
|
|
74
|
+
origins = field_origins(attrs_cls)
|
|
75
|
+
return [_parameterize_attrs_or_dataclass_field(origins, field) for field in attrs.fields(attrs_cls)]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _resolve_dataclass_fields(
|
|
79
|
+
dataclass_cls: ty.Type,
|
|
80
|
+
) -> ty.Tuple[dataclasses.Field, ...]:
|
|
81
|
+
resolved_annotations = ty.get_type_hints(dataclass_cls, include_extras=True)
|
|
82
|
+
fields = dataclasses.fields(dataclass_cls)
|
|
83
|
+
return tuple(_replace_dataclass_field_type(f, resolved_annotations[f.name]) for f in fields)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@signature_preserving_cache
|
|
87
|
+
def dataclass_fields_parameterized(
|
|
88
|
+
dataclass_cls: ty.Type,
|
|
89
|
+
) -> ty.Sequence[dataclasses.Field]:
|
|
90
|
+
"""`dataclasses.fields` does not resolve typevars in the field types when base classes provide type parameters.
|
|
91
|
+
This function has the same signature as `dataclasses.fields` but returns `Field`s with fully resolved `type` attributes.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
origins = field_origins(dataclass_cls)
|
|
95
|
+
return [
|
|
96
|
+
_parameterize_attrs_or_dataclass_field(origins, field)
|
|
97
|
+
for field in _resolve_dataclass_fields(ti.get_origin(dataclass_cls) or dataclass_cls)
|
|
98
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import typing as ty
|
|
2
|
+
|
|
3
|
+
import typing_inspect as ti
|
|
4
|
+
|
|
5
|
+
TypeOrVar = ty.Union[ty.Type, ty.TypeVar]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def origin_params_and_args(
|
|
9
|
+
type_: ty.Type,
|
|
10
|
+
) -> ty.Tuple[ty.Type, ty.Tuple[ty.TypeVar, ...], ty.Tuple[TypeOrVar, ...], bool]:
|
|
11
|
+
"""Get the origin, parameters, and arguments of a generic type.
|
|
12
|
+
When a generic type has not been parameterized, the arguments will be the same as the parameters.
|
|
13
|
+
Examples:
|
|
14
|
+
- For `List[int]`, returns `(list, (T,), (int,))`
|
|
15
|
+
- For `Dict[str, U]`, returns `(dict, (K, V), (str, U))`
|
|
16
|
+
- For just `List`, returns `(list, (T,), (T,))`
|
|
17
|
+
"""
|
|
18
|
+
if args := ti.get_args(type_):
|
|
19
|
+
origin = ti.get_origin(type_)
|
|
20
|
+
params = ti.get_parameters(origin)
|
|
21
|
+
parameterized = True
|
|
22
|
+
else:
|
|
23
|
+
origin = type_
|
|
24
|
+
params = ti.get_parameters(origin)
|
|
25
|
+
args = params
|
|
26
|
+
parameterized = False
|
|
27
|
+
return origin, params, args, parameterized
|
thds/attrs_utils/random/attrs.py
CHANGED
|
@@ -2,6 +2,7 @@ from typing import Callable, Dict, Sequence, Type, TypeVar
|
|
|
2
2
|
|
|
3
3
|
import attrs
|
|
4
4
|
|
|
5
|
+
from ..params import attrs_fields_parameterized
|
|
5
6
|
from ..registry import Registry
|
|
6
7
|
from ..type_utils import is_namedtuple_type
|
|
7
8
|
from .util import Gen, T
|
|
@@ -25,10 +26,10 @@ def random_attrs(
|
|
|
25
26
|
|
|
26
27
|
def _register_random_gen_by_field(type_: Type[T], **gens: Gen):
|
|
27
28
|
if attrs.has(type_):
|
|
28
|
-
fields =
|
|
29
|
-
names = [f.name for f in fields]
|
|
29
|
+
fields = attrs_fields_parameterized(type_)
|
|
30
|
+
names: Sequence[str] = [f.name for f in fields]
|
|
30
31
|
elif is_namedtuple_type(type_):
|
|
31
|
-
names = type_._fields
|
|
32
|
+
names = type_._fields
|
|
32
33
|
else:
|
|
33
34
|
raise TypeError(f"Don't know how to interpret {type_} as a record type")
|
|
34
35
|
|
thds/attrs_utils/random/gen.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import collections
|
|
2
2
|
import enum
|
|
3
3
|
from functools import partial
|
|
4
|
-
from typing import DefaultDict, Iterable, Tuple, Type, cast, get_args
|
|
4
|
+
from typing import DefaultDict, Iterable, NamedTuple, Tuple, Type, TypeVar, cast, get_args
|
|
5
5
|
|
|
6
6
|
import attr
|
|
7
7
|
|
|
8
8
|
from .. import recursion, type_recursion, type_utils
|
|
9
|
+
from ..params import attrs_fields_parameterized
|
|
9
10
|
from . import attrs, collection, optional, tuple, union
|
|
10
11
|
from .registry import GEN_REGISTRY
|
|
11
12
|
from .util import Gen, T, U, choice_gen, juxtapose_gen, repeat_gen
|
|
@@ -29,7 +30,7 @@ def gen_enum(random_gen, type_: Type[T]) -> Gen[T]:
|
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
def gen_attrs(random_gen, type_: Type[attr.AttrsInstance]) -> Gen[attr.AttrsInstance]:
|
|
32
|
-
fields =
|
|
33
|
+
fields = attrs_fields_parameterized(type_)
|
|
33
34
|
kw_only_fields = [f for f in fields if f.kw_only]
|
|
34
35
|
overrides = attrs.CUSTOM_ATTRS_BY_FIELD_REGISTRY.get(type_)
|
|
35
36
|
|
|
@@ -48,9 +49,12 @@ def gen_attrs(random_gen, type_: Type[attr.AttrsInstance]) -> Gen[attr.AttrsInst
|
|
|
48
49
|
return tuple.random_namedtuple_gen(type_, *field_gens)
|
|
49
50
|
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
NT = TypeVar("NT", bound=NamedTuple)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def gen_namedtuple(random_gen, type_: Type[NT]) -> Gen[NT]:
|
|
56
|
+
field_names = type_._fields
|
|
57
|
+
field_types = (type_.__annotations__[name] for name in field_names)
|
|
54
58
|
overrides = attrs.CUSTOM_ATTRS_BY_FIELD_REGISTRY.get(type_)
|
|
55
59
|
if overrides:
|
|
56
60
|
return tuple.random_namedtuple_gen(
|
thds/attrs_utils/registry.py
CHANGED
|
@@ -30,7 +30,7 @@ class Registry(Dict[T, U]):
|
|
|
30
30
|
|
|
31
31
|
def cache(self, func: Callable[[T], U]) -> Callable[[T], U]:
|
|
32
32
|
cached = partial(_check_cache, self, func)
|
|
33
|
-
return wraps(func)(cached)
|
|
33
|
+
return wraps(func)(cached)
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def _check_cache(cache: Dict[T, U], func: Callable[[T], U], key: T) -> U:
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import dataclasses
|
|
1
2
|
import typing
|
|
2
3
|
from functools import partial
|
|
3
4
|
from typing import List, Optional, Tuple, Type, TypeVar
|
|
@@ -70,6 +71,7 @@ class TypeRecursion(StructuredRecursion[Type, Params, U]):
|
|
|
70
71
|
*,
|
|
71
72
|
first: Optional[Tuple[Predicate[Type], RecF[Type, Params, U]]] = None,
|
|
72
73
|
attrs: Optional[RecF[Type, Params, U]] = None,
|
|
74
|
+
dataclass: Optional[RecF[Type, Params, U]] = None,
|
|
73
75
|
namedtuple: Optional[RecF[Type, Params, U]] = None,
|
|
74
76
|
optional: Optional[RecF[Type, Params, U]] = None,
|
|
75
77
|
union: Optional[RecF[Type, Params, U]] = None,
|
|
@@ -96,6 +98,7 @@ class TypeRecursion(StructuredRecursion[Type, Params, U]):
|
|
|
96
98
|
(type_utils.is_literal_type, literal),
|
|
97
99
|
(type_utils.is_enum_type, enum),
|
|
98
100
|
(attr.has, attrs),
|
|
101
|
+
(dataclasses.is_dataclass, dataclass),
|
|
99
102
|
(type_utils.is_optional_type, optional),
|
|
100
103
|
(is_union_type, union),
|
|
101
104
|
(type_utils.is_set_type, set),
|
thds/attrs_utils/type_utils.py
CHANGED
|
@@ -2,13 +2,13 @@ import collections
|
|
|
2
2
|
import enum
|
|
3
3
|
import inspect
|
|
4
4
|
import typing
|
|
5
|
-
from typing import Callable, List,
|
|
5
|
+
from typing import Callable, List, Optional, Tuple, Type, TypeVar, Union
|
|
6
6
|
|
|
7
7
|
import attr
|
|
8
|
+
from typing_extensions import TypeIs
|
|
8
9
|
from typing_inspect import (
|
|
9
10
|
get_args,
|
|
10
11
|
get_origin,
|
|
11
|
-
get_parameters,
|
|
12
12
|
is_literal_type,
|
|
13
13
|
is_new_type,
|
|
14
14
|
is_optional_type,
|
|
@@ -97,7 +97,7 @@ def enum_base(type_: Type) -> Type:
|
|
|
97
97
|
def unwrap_optional(type_: Type) -> Type:
|
|
98
98
|
if is_optional_type(type_):
|
|
99
99
|
args = get_args(type_)
|
|
100
|
-
return Union[tuple(a for a in args if a is not type(None))] # type: ignore # noqa:E721
|
|
100
|
+
return Union[tuple(a for a in args if a is not type(None))] # type: ignore[return-value] # noqa:E721
|
|
101
101
|
else:
|
|
102
102
|
return type_
|
|
103
103
|
|
|
@@ -116,30 +116,30 @@ def unwrap_annotated(type_: Type) -> Type:
|
|
|
116
116
|
return type_
|
|
117
117
|
|
|
118
118
|
|
|
119
|
-
def is_enum_type(type_: Type) ->
|
|
119
|
+
def is_enum_type(type_: Type) -> TypeIs[Type[enum.Enum]]:
|
|
120
120
|
return isinstance(type_, type) and issubclass(type_, enum.Enum)
|
|
121
121
|
|
|
122
122
|
|
|
123
|
-
def is_collection_type(type_: Type) ->
|
|
123
|
+
def is_collection_type(type_: Type) -> TypeIs[Type[typing.Collection]]:
|
|
124
124
|
origin = get_origin(type_)
|
|
125
125
|
return (type_ in COLLECTION_TYPES) if origin is None else (origin in COLLECTION_TYPES)
|
|
126
126
|
|
|
127
127
|
|
|
128
|
-
def is_mapping_type(type_: Type) ->
|
|
128
|
+
def is_mapping_type(type_: Type) -> TypeIs[Type[typing.Mapping]]:
|
|
129
129
|
origin = get_origin(type_)
|
|
130
130
|
return (type_ in MAPPING_TYPES) if origin is None else (origin in MAPPING_TYPES)
|
|
131
131
|
|
|
132
132
|
|
|
133
|
-
def is_set_type(type_: Type) ->
|
|
133
|
+
def is_set_type(type_: Type) -> TypeIs[Type[typing.AbstractSet]]:
|
|
134
134
|
origin = get_origin(type_)
|
|
135
135
|
return (type_ in UNIQUE_COLLECTION_TYPES) if origin is None else (origin in UNIQUE_COLLECTION_TYPES)
|
|
136
136
|
|
|
137
137
|
|
|
138
|
-
def is_namedtuple_type(type_: Type) ->
|
|
138
|
+
def is_namedtuple_type(type_: Type) -> TypeIs[Type[typing.NamedTuple]]:
|
|
139
139
|
return getattr(type_, "__bases__", None) == (tuple,) and hasattr(type_, "_fields")
|
|
140
140
|
|
|
141
141
|
|
|
142
|
-
def is_variadic_tuple_type(type_: Type) ->
|
|
142
|
+
def is_variadic_tuple_type(type_: Type) -> TypeIs[Type[typing.Tuple[typing.Any, ...]]]:
|
|
143
143
|
if is_tuple_type(type_):
|
|
144
144
|
args = get_args(type_)
|
|
145
145
|
return len(args) == 2 and args[-1] is Ellipsis
|
|
@@ -153,23 +153,14 @@ def is_builtin_type(type_: Type) -> bool:
|
|
|
153
153
|
|
|
154
154
|
def concrete_constructor(type_: Type[T]) -> Callable[..., T]:
|
|
155
155
|
if is_namedtuple_type(type_):
|
|
156
|
-
return type_
|
|
156
|
+
return type_ # type: ignore[return-value]
|
|
157
157
|
origin = get_origin(type_)
|
|
158
158
|
return ORIGIN_TO_CONSTRUCTOR[type_] if origin is None else ORIGIN_TO_CONSTRUCTOR[origin]
|
|
159
159
|
|
|
160
160
|
|
|
161
|
-
def parameterize(
|
|
162
|
-
type_: Union[Type, TypeVar], params: Mapping[TypeVar, Union[TypeVar, Type]]
|
|
163
|
-
) -> Union[Type, TypeVar]:
|
|
164
|
-
if isinstance(type_, TypeVar):
|
|
165
|
-
return params.get(type_, type_)
|
|
166
|
-
else:
|
|
167
|
-
tparams = get_parameters(type_)
|
|
168
|
-
new_args = tuple(params.get(t, t) for t in tparams)
|
|
169
|
-
return type_[new_args]
|
|
170
|
-
|
|
171
|
-
|
|
172
161
|
def bases(type_: Type, predicate: Optional[Callable[[Type], bool]] = None) -> List[Type]:
|
|
162
|
+
if get_args(type_):
|
|
163
|
+
type_ = get_origin(type_)
|
|
173
164
|
if not inspect.isclass(type_):
|
|
174
165
|
raise TypeError(
|
|
175
166
|
f"{bases.__module__}.{bases.__name__} can be called only on concrete classes; got {type_}"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import typing as ty
|
|
3
|
+
|
|
4
|
+
F = ty.TypeVar("F", bound=ty.Callable)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def signature_preserving_cache(func: F, size: ty.Optional[int] = None) -> F:
|
|
8
|
+
"""Decorator to apply `functools.lru_cache` while preserving the decorated function's signature
|
|
9
|
+
(which is otherwise lost when using `lru_cache` directly)"""
|
|
10
|
+
|
|
11
|
+
cached_func = functools.lru_cache(size)(func)
|
|
12
|
+
return ty.cast(F, cached_func)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thds.attrs-utils
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.20251204022717
|
|
4
4
|
Summary: Utilities for attrs record classes.
|
|
5
5
|
Author-email: Trillianth Health <info@trillianthealth.com>
|
|
6
6
|
Project-URL: Repository, https://github.com/TrilliantHealth/ds-monorepo
|
|
@@ -31,7 +31,8 @@ transforming, checking, generating, or anything else you'd want to do with types
|
|
|
31
31
|
- Heterogeneous tuple types, e.g. `typing.Tuple[A, B, C]`
|
|
32
32
|
- Variadic tuple types, e.g. `Tuple[T, ...]`
|
|
33
33
|
- `typing.NamedTuple` record types
|
|
34
|
-
- `attrs`-defined record types
|
|
34
|
+
- `attrs`-defined record types, including generics with type variables
|
|
35
|
+
- `dataclasses`-defined record types, including generics with type variables
|
|
35
36
|
- Union types using `typing.Union`
|
|
36
37
|
- `typing.Literal`
|
|
37
38
|
- `typing.Annotated`
|
|
@@ -92,7 +93,7 @@ You can create a callable to generate instances of a given type as follows:
|
|
|
92
93
|
|
|
93
94
|
```python
|
|
94
95
|
import itertools
|
|
95
|
-
from typing import Dict, Literal, NewType, Optional, Tuple
|
|
96
|
+
from typing import Dict, Generic, Literal, NewType, Optional, Tuple, TypeVar
|
|
96
97
|
import attr
|
|
97
98
|
from thds.attrs_utils.random.builtin import random_bool_gen, random_int_gen, random_str_gen
|
|
98
99
|
from thds.attrs_utils.random.tuple import random_tuple_gen
|
|
@@ -108,18 +109,20 @@ class Record1:
|
|
|
108
109
|
a: Optional[str]
|
|
109
110
|
b: Tuple[int, bool]
|
|
110
111
|
|
|
111
|
-
ID =
|
|
112
|
+
ID = TypeVar("ID")
|
|
112
113
|
Key = Literal["foo", "bar", "baz"]
|
|
113
114
|
|
|
114
115
|
@attr.define
|
|
115
|
-
class Record2:
|
|
116
|
+
class Record2(Generic[ID]):
|
|
116
117
|
id: ID
|
|
117
118
|
records: Dict[Key, Record1]
|
|
118
119
|
|
|
120
|
+
MyID = NewType("MyID", int)
|
|
121
|
+
|
|
119
122
|
ids = itertools.count(1)
|
|
120
|
-
random_gen.register(
|
|
123
|
+
random_gen.register(MyID, lambda: next(ids))
|
|
121
124
|
|
|
122
|
-
random_record = random_gen(Record2)
|
|
125
|
+
random_record = random_gen(Record2[MyID])
|
|
123
126
|
|
|
124
127
|
print(random_record())
|
|
125
128
|
print(random_record())
|
|
@@ -1,36 +1,41 @@
|
|
|
1
1
|
thds/attrs_utils/__init__.py,sha256=7hP-VWe-nq56jMk4jqsDanussQDzTUgOG94zUn7GgwA,122
|
|
2
2
|
thds/attrs_utils/defaults.py,sha256=SjxB44PsArwJmtqDZ70iShdYZ1BxSlW8kQ2DnFCkeEg,1019
|
|
3
|
-
thds/attrs_utils/docs.py,sha256=
|
|
4
|
-
thds/attrs_utils/empty.py,sha256=
|
|
3
|
+
thds/attrs_utils/docs.py,sha256=Ul-0o9uQPl8sm7XOMpsmTsu43CfzTOta2n7WavjbvRs,2930
|
|
4
|
+
thds/attrs_utils/empty.py,sha256=qNbKrs7bljPd7aVS6D7bodWFIRJ-NUnzcNZ5rw8h7qI,4699
|
|
5
5
|
thds/attrs_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
thds/attrs_utils/recursion.py,sha256=qQjygpMnFtR2oNmTn6S24YojsJzc7lPzHBh0_DJsdKs,1607
|
|
7
|
-
thds/attrs_utils/registry.py,sha256=
|
|
7
|
+
thds/attrs_utils/registry.py,sha256=1ez9XCYgN842anLLm98qruCllzgJfhLuBDxuOc5Eee4,1020
|
|
8
8
|
thds/attrs_utils/type_cache.py,sha256=t--FCqzJcwKhy-WJHqOxYr0dhaMOLvgViU13WZHGPzk,4008
|
|
9
|
-
thds/attrs_utils/type_recursion.py,sha256=
|
|
10
|
-
thds/attrs_utils/type_utils.py,sha256=
|
|
9
|
+
thds/attrs_utils/type_recursion.py,sha256=f0-5ANqfI9tTv0vgSX_0Oqph1cQ1XsBPaIWhsYBHfIw,7533
|
|
10
|
+
thds/attrs_utils/type_utils.py,sha256=v1N0oGNCICjS6S0nfVnW-RNZGDWMHyTRMTwoNiSoPLA,4855
|
|
11
|
+
thds/attrs_utils/utils.py,sha256=o5suGo9L0i4i77QUjjglW9QnklR-jbA1pQQIaEYgsR8,407
|
|
11
12
|
thds/attrs_utils/cattrs/__init__.py,sha256=635CMqhH1Bzd-p1-c6l0MJrxKgmlk45EmA-IbGGbfC8,822
|
|
12
|
-
thds/attrs_utils/cattrs/converter.py,sha256=
|
|
13
|
+
thds/attrs_utils/cattrs/converter.py,sha256=jPRsBJehZtgsjWLYH5XvA_AW-w7pkyCrKkTAmFNXAWI,7578
|
|
13
14
|
thds/attrs_utils/cattrs/errors.py,sha256=w7O7hRr8LcI17av1MTEnu-Im4jESECnpK30QqNVUqE8,1899
|
|
14
15
|
thds/attrs_utils/isinstance/__init__.py,sha256=EAGULDzlI85Mgedsy7FY8lbTpMa-qJHsh_dL-adrv3E,248
|
|
15
|
-
thds/attrs_utils/isinstance/check.py,sha256=
|
|
16
|
+
thds/attrs_utils/isinstance/check.py,sha256=cncosTpHKlmo4WacpUHgFQQsbiyt1rxxv3Uir1UedcM,3494
|
|
16
17
|
thds/attrs_utils/isinstance/registry.py,sha256=WumuN5F156fv3K39nbdvEVnDilKxD-UwKLla5GqghDs,251
|
|
17
18
|
thds/attrs_utils/isinstance/util.py,sha256=iAK7_eFKDBUclX-UTYk4XGNB-T4cfX8c6A9xHMcka8U,2956
|
|
18
19
|
thds/attrs_utils/jsonschema/__init__.py,sha256=UV5P8-Dnw4LlC6-xRbhmxPdqqeORnWT8Pjp3RqwSCxo,221
|
|
19
20
|
thds/attrs_utils/jsonschema/constructors.py,sha256=reSbWiHYgWqVL6iWB_XSWB5nLPesq1joa-dfSLtFzj0,1839
|
|
20
|
-
thds/attrs_utils/jsonschema/jsonschema.py,sha256=
|
|
21
|
+
thds/attrs_utils/jsonschema/jsonschema.py,sha256=MA2WX2UkyiJZS9cz27_iddj7eEiChuTjJqE_JSers0Q,12942
|
|
21
22
|
thds/attrs_utils/jsonschema/str_formats.py,sha256=P_7IbvEvsJ5hEMfkwmiI85n4pBBfCNH5uv6nklG4O9A,3361
|
|
22
23
|
thds/attrs_utils/jsonschema/util.py,sha256=oa-cbIqplkriTIFTiW2OsVNyYpgFoK8sYMG8OJEIK3c,3400
|
|
24
|
+
thds/attrs_utils/params/__init__.py,sha256=sdBdSHBD47qo7DWZNG0taewzqhfxtwL0hxaMcmdA3LM,306
|
|
25
|
+
thds/attrs_utils/params/parameterize.py,sha256=MxLBcQREeYQcFUwFpV2_8F_0Rrtcj6-WX4wfFWMFQVI,3473
|
|
26
|
+
thds/attrs_utils/params/records.py,sha256=jsZNr9B3eNs3u2h66TOxrbEh7SeJiRvlx8VZeTxDgOY,3744
|
|
27
|
+
thds/attrs_utils/params/utils.py,sha256=Oz72dwXMEjLWFFMqilj6DlCiSa7SR3b9rvkFL-GnWIw,916
|
|
23
28
|
thds/attrs_utils/random/__init__.py,sha256=A1n-9fispK5EkZFpwOhNJpO1kY_7xDZ5zec5ckflpmY,160
|
|
24
|
-
thds/attrs_utils/random/attrs.py,sha256=
|
|
29
|
+
thds/attrs_utils/random/attrs.py,sha256=N-qGxtQhb6wr_ML3aFb4Gh3beQsZxgFa7Y_gpACfCPQ,1725
|
|
25
30
|
thds/attrs_utils/random/builtin.py,sha256=p16zK3CJJITfCpe2gg4f1TkRXi25y4m97o5CvZKmfWA,2854
|
|
26
31
|
thds/attrs_utils/random/collection.py,sha256=gSa_-478_KUcqzYt-01OREB3nJ7bcfGxwnFjSB7JJ30,1116
|
|
27
|
-
thds/attrs_utils/random/gen.py,sha256=
|
|
32
|
+
thds/attrs_utils/random/gen.py,sha256=tqF-c5WRqfze87gOsBV-0CtOY55XeqNUYZETjD9rsV4,4768
|
|
28
33
|
thds/attrs_utils/random/optional.py,sha256=Mm0WnJ6VeUUUhYelrOK7ZAYRJePlBalHc0wQECyO9uM,386
|
|
29
34
|
thds/attrs_utils/random/registry.py,sha256=IcC2ucJtrnOG0RH43VK8lrMha1uhYflz8_pZJZj7jJ4,1056
|
|
30
35
|
thds/attrs_utils/random/tuple.py,sha256=YI_wzYFzprUG9iuxwloWz9OqQDmVJuhp9epu_pnUI2w,783
|
|
31
36
|
thds/attrs_utils/random/union.py,sha256=FEWB4SByQFVr0vCGeWUor4W3bTB3sj0MEdavGxrBc98,1242
|
|
32
37
|
thds/attrs_utils/random/util.py,sha256=tQKjJFP58NEhX5UNYuHeAUOiyf1SE39EQeZgj2O1uKk,1557
|
|
33
|
-
thds_attrs_utils-1.
|
|
34
|
-
thds_attrs_utils-1.
|
|
35
|
-
thds_attrs_utils-1.
|
|
36
|
-
thds_attrs_utils-1.
|
|
38
|
+
thds_attrs_utils-1.7.20251204022717.dist-info/METADATA,sha256=vN81Ow2jIo_5j1wvBq2CBt-UxKySX14TSCli1scgzKc,8865
|
|
39
|
+
thds_attrs_utils-1.7.20251204022717.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
40
|
+
thds_attrs_utils-1.7.20251204022717.dist-info/top_level.txt,sha256=LTZaE5SkWJwv9bwOlMbIhiS-JWQEEIcjVYnJrt-CriY,5
|
|
41
|
+
thds_attrs_utils-1.7.20251204022717.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|