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.
@@ -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, # type: ignore [assignment]
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 attr
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[attr.AttrsInstance]) -> Constructor[attr.AttrsInstance]:
71
- fields = attr.fields(type_)
72
- defaults = {f.name: empty_gen(f.type) for f in fields if f.default is attr.NOTHING}
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 = attrs.fields(type_) # type: ignore [misc]
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 = attr.fields(type_)
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 # type: ignore
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
@@ -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 = attrs.fields(type_) # type: ignore
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 # type: ignore [attr-defined]
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
 
@@ -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 = attr.fields(type_) # type: ignore [arg-type,misc]
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
- def gen_namedtuple(random_gen, type_: Type[T]) -> Gen[T]:
52
- field_names = type_._fields # type: ignore [attr-defined]
53
- field_types = (type_.__annotations__[name] for name in field_names) # type: ignore [attr-defined]
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(
@@ -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) # type: ignore [return-value]
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),
@@ -2,13 +2,13 @@ import collections
2
2
  import enum
3
3
  import inspect
4
4
  import typing
5
- from typing import Callable, List, Mapping, Optional, Tuple, Type, TypeVar, Union
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) -> bool:
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) -> bool:
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) -> bool:
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) -> bool:
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) -> bool:
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) -> bool:
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.6.20251124154148
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 = NewType("ID", int)
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(ID, lambda: next(ids))
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=ill0gCf2EWe7Lx9vz5xSqotmj6jh3vIt7wB3stwPI7o,2852
4
- thds/attrs_utils/empty.py,sha256=96oeOFfovDmQgOHLqcwvNmd9c8-MTwAzZ_lP8XVIZc4,4633
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=XVdz9MAlsSYPxHub1jcgMhns8Z_kNcsstI76OHwZV_k,1051
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=Lr9BXO4VpNQMMFa--8ruQplsmP1YieAxizxl3O0LFp4,7400
10
- thds/attrs_utils/type_utils.py,sha256=368WqiE0ojWFevt1iTVehsEQ8Jtyp68wbSAQiPQmXbQ,4924
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=CIpHTf3GTtAmUcxU-FncsmYVe_9eoPqpFK7wSqgHCu4,7560
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=zfyfT8Wm0wbfodxgUAbAZTYmxdmapRXiw-DA2I8pIys,3455
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=GZPm-RkcC-F5FFTN7btUnCMVN4DDvF02xfnvI4J8WAA,12895
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=fLed-aLcCqZ2gLhHQogY5sn32A4HE_LlpobHuj8qvL8,1695
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=kp0XZoFHL4lkzZvxWZJ9bMGLDHAaNqTTioAk31-mKF4,4737
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.6.20251124154148.dist-info/METADATA,sha256=QG-3u3NKwagAPjV9QpinZZOfrSGYpLPYQ5rRtjMKcAw,8685
34
- thds_attrs_utils-1.6.20251124154148.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
- thds_attrs_utils-1.6.20251124154148.dist-info/top_level.txt,sha256=LTZaE5SkWJwv9bwOlMbIhiS-JWQEEIcjVYnJrt-CriY,5
36
- thds_attrs_utils-1.6.20251124154148.dist-info/RECORD,,
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,,