omlish 0.0.0.dev284__py3-none-any.whl → 0.0.0.dev286__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.
Files changed (107) hide show
  1. omlish/__about__.py +6 -2
  2. omlish/dataclasses/__init__.py +58 -60
  3. omlish/dataclasses/api/__init__.py +25 -0
  4. omlish/dataclasses/api/classes/__init__.py +0 -0
  5. omlish/dataclasses/api/classes/conversion.py +30 -0
  6. omlish/dataclasses/api/classes/decorator.py +145 -0
  7. omlish/dataclasses/api/classes/make.py +109 -0
  8. omlish/dataclasses/api/classes/metadata.py +133 -0
  9. omlish/dataclasses/api/classes/params.py +78 -0
  10. omlish/dataclasses/api/fields/__init__.py +0 -0
  11. omlish/dataclasses/api/fields/building.py +120 -0
  12. omlish/dataclasses/api/fields/constructor.py +56 -0
  13. omlish/dataclasses/api/fields/conversion.py +191 -0
  14. omlish/dataclasses/api/fields/metadata.py +94 -0
  15. omlish/dataclasses/concerns/__init__.py +17 -0
  16. omlish/dataclasses/concerns/abc.py +15 -0
  17. omlish/dataclasses/concerns/copy.py +63 -0
  18. omlish/dataclasses/concerns/doc.py +53 -0
  19. omlish/dataclasses/concerns/eq.py +60 -0
  20. omlish/dataclasses/concerns/fields.py +119 -0
  21. omlish/dataclasses/concerns/frozen.py +133 -0
  22. omlish/dataclasses/concerns/hash.py +165 -0
  23. omlish/dataclasses/concerns/init.py +453 -0
  24. omlish/dataclasses/concerns/matchargs.py +27 -0
  25. omlish/dataclasses/concerns/mro.py +16 -0
  26. omlish/dataclasses/concerns/order.py +87 -0
  27. omlish/dataclasses/concerns/override.py +98 -0
  28. omlish/dataclasses/concerns/params.py +14 -0
  29. omlish/dataclasses/concerns/replace.py +48 -0
  30. omlish/dataclasses/concerns/repr.py +95 -0
  31. omlish/dataclasses/{impl → concerns}/slots.py +25 -1
  32. omlish/dataclasses/debug.py +2 -0
  33. omlish/dataclasses/errors.py +115 -0
  34. omlish/dataclasses/generation/__init__.py +0 -0
  35. omlish/dataclasses/generation/base.py +38 -0
  36. omlish/dataclasses/generation/compilation.py +258 -0
  37. omlish/dataclasses/generation/execution.py +195 -0
  38. omlish/dataclasses/generation/globals.py +83 -0
  39. omlish/dataclasses/generation/idents.py +6 -0
  40. omlish/dataclasses/generation/mangling.py +18 -0
  41. omlish/dataclasses/generation/manifests.py +20 -0
  42. omlish/dataclasses/generation/ops.py +97 -0
  43. omlish/dataclasses/generation/plans.py +35 -0
  44. omlish/dataclasses/generation/processor.py +179 -0
  45. omlish/dataclasses/generation/registry.py +42 -0
  46. omlish/dataclasses/generation/utils.py +83 -0
  47. omlish/dataclasses/{impl/reflect.py → inspect.py} +53 -90
  48. omlish/dataclasses/{impl/internals.py → internals.py} +26 -32
  49. omlish/dataclasses/metaclass/__init__.py +0 -0
  50. omlish/dataclasses/metaclass/bases.py +69 -0
  51. omlish/dataclasses/metaclass/confer.py +65 -0
  52. omlish/dataclasses/metaclass/meta.py +115 -0
  53. omlish/dataclasses/metaclass/specs.py +38 -0
  54. omlish/dataclasses/processing/__init__.py +0 -0
  55. omlish/dataclasses/processing/base.py +83 -0
  56. omlish/dataclasses/processing/driving.py +49 -0
  57. omlish/dataclasses/processing/priority.py +13 -0
  58. omlish/dataclasses/processing/registry.py +81 -0
  59. omlish/dataclasses/reflection.py +81 -0
  60. omlish/dataclasses/specs.py +224 -0
  61. omlish/dataclasses/tools/__init__.py +0 -0
  62. omlish/dataclasses/{impl → tools}/as_.py +23 -8
  63. omlish/dataclasses/tools/iter.py +27 -0
  64. omlish/dataclasses/tools/modifiers.py +52 -0
  65. omlish/dataclasses/tools/replace.py +17 -0
  66. omlish/dataclasses/tools/repr.py +12 -0
  67. omlish/dataclasses/{static.py → tools/static.py} +25 -4
  68. omlish/dataclasses/utils.py +54 -109
  69. omlish/diag/__init__.py +4 -4
  70. omlish/inject/impl/origins.py +1 -1
  71. omlish/lang/cached/function.py +4 -2
  72. omlish/lang/maybes.py +17 -0
  73. omlish/lite/maybes.py +17 -0
  74. omlish/marshal/objects/dataclasses.py +3 -7
  75. omlish/marshal/objects/helpers.py +3 -3
  76. omlish/secrets/marshal.py +1 -1
  77. omlish/secrets/secrets.py +1 -1
  78. omlish/sql/queries/base.py +1 -1
  79. omlish/text/minja.py +81 -25
  80. omlish/text/templating.py +116 -0
  81. omlish/typedvalues/marshal.py +2 -2
  82. {omlish-0.0.0.dev284.dist-info → omlish-0.0.0.dev286.dist-info}/METADATA +4 -1
  83. {omlish-0.0.0.dev284.dist-info → omlish-0.0.0.dev286.dist-info}/RECORD +87 -46
  84. omlish/dataclasses/impl/LICENSE +0 -279
  85. omlish/dataclasses/impl/__init__.py +0 -33
  86. omlish/dataclasses/impl/api.py +0 -278
  87. omlish/dataclasses/impl/copy.py +0 -30
  88. omlish/dataclasses/impl/errors.py +0 -53
  89. omlish/dataclasses/impl/fields.py +0 -245
  90. omlish/dataclasses/impl/frozen.py +0 -93
  91. omlish/dataclasses/impl/hashing.py +0 -86
  92. omlish/dataclasses/impl/init.py +0 -199
  93. omlish/dataclasses/impl/main.py +0 -93
  94. omlish/dataclasses/impl/metaclass.py +0 -235
  95. omlish/dataclasses/impl/metadata.py +0 -75
  96. omlish/dataclasses/impl/order.py +0 -57
  97. omlish/dataclasses/impl/overrides.py +0 -53
  98. omlish/dataclasses/impl/params.py +0 -128
  99. omlish/dataclasses/impl/processing.py +0 -24
  100. omlish/dataclasses/impl/replace.py +0 -40
  101. omlish/dataclasses/impl/repr.py +0 -66
  102. omlish/dataclasses/impl/simple.py +0 -50
  103. omlish/dataclasses/impl/utils.py +0 -167
  104. {omlish-0.0.0.dev284.dist-info → omlish-0.0.0.dev286.dist-info}/WHEEL +0 -0
  105. {omlish-0.0.0.dev284.dist-info → omlish-0.0.0.dev286.dist-info}/entry_points.txt +0 -0
  106. {omlish-0.0.0.dev284.dist-info → omlish-0.0.0.dev286.dist-info}/licenses/LICENSE +0 -0
  107. {omlish-0.0.0.dev284.dist-info → omlish-0.0.0.dev286.dist-info}/top_level.txt +0 -0
@@ -1,75 +0,0 @@
1
- import types
2
- import typing as ta
3
-
4
- from ... import lang
5
-
6
-
7
- METADATA_ATTR = '__dataclass_metadata__'
8
-
9
- Metadata: ta.TypeAlias = ta.Mapping[ta.Any, ta.Any]
10
-
11
- EMPTY_METADATA: Metadata = types.MappingProxyType({})
12
-
13
- _CLASS_MERGED_KEYS: set[str] = set()
14
- CLASS_MERGED_KEYS: ta.AbstractSet = _CLASS_MERGED_KEYS
15
-
16
-
17
- def _class_merged(o):
18
- _CLASS_MERGED_KEYS.add(o)
19
- return o
20
-
21
-
22
- def get_merged_metadata(obj: ta.Any) -> Metadata:
23
- cls = obj if isinstance(obj, type) else type(obj)
24
- dct: dict[ta.Any, ta.Any] = {}
25
- for cur in cls.__mro__[::-1]:
26
- if not (smd := cur.__dict__.get(METADATA_ATTR)):
27
- continue
28
- for k, v in smd.items():
29
- if k in CLASS_MERGED_KEYS:
30
- dct.setdefault(k, []).extend(v)
31
- else:
32
- dct[k] = v
33
- return dct
34
-
35
-
36
- def _append_cls_md(k, v):
37
- lang.get_caller_cls_dct(1).setdefault(METADATA_ATTR, {}).setdefault(k, []).append(v)
38
-
39
-
40
- ##
41
-
42
-
43
- @_class_merged
44
- class UserMetadata(lang.Marker):
45
- pass
46
-
47
-
48
- @lang.cls_dct_fn()
49
- def metadata(cls_dct, *args) -> None:
50
- cls_dct.setdefault(METADATA_ATTR, {}).setdefault(UserMetadata, []).extend(args)
51
-
52
-
53
- ##
54
-
55
-
56
- @_class_merged
57
- class Validate(lang.Marker):
58
- pass
59
-
60
-
61
- def validate(fn: ta.Callable[..., bool] | staticmethod) -> None:
62
- _append_cls_md(Validate, fn)
63
-
64
-
65
- ##
66
-
67
-
68
- @_class_merged
69
- class Init(lang.Marker):
70
- pass
71
-
72
-
73
- def init(obj):
74
- _append_cls_md(Init, obj)
75
- return obj
@@ -1,57 +0,0 @@
1
- import typing as ta
2
-
3
- from .processing import Processor
4
- from .utils import Namespace
5
- from .utils import create_fn
6
- from .utils import set_new_attribute
7
- from .utils import tuple_str
8
-
9
-
10
- def cmp_fn(
11
- name: str,
12
- op: str,
13
- self_tuple: str,
14
- other_tuple: str,
15
- globals: Namespace, # noqa
16
- ) -> ta.Callable:
17
- return create_fn(
18
- name,
19
- ('self', 'other'),
20
- [
21
- 'if other.__class__ is self.__class__:',
22
- f' return {self_tuple}{op}{other_tuple}',
23
- 'return NotImplemented',
24
- ],
25
- globals=globals,
26
- )
27
-
28
-
29
- class OrderProcessor(Processor):
30
- def _process(self) -> None:
31
- if not self._info.params.order:
32
- return
33
-
34
- flds = [f for f in self._info.instance_fields if f.compare]
35
- self_tuple = tuple_str('self', flds)
36
- other_tuple = tuple_str('other', flds)
37
- for name, op in [
38
- ('__lt__', '<'),
39
- ('__le__', '<='),
40
- ('__gt__', '>'),
41
- ('__ge__', '>='),
42
- ]:
43
- if set_new_attribute(
44
- self._cls, # noqa
45
- name,
46
- cmp_fn(
47
- name,
48
- op,
49
- self_tuple,
50
- other_tuple,
51
- globals=self._info.globals,
52
- ),
53
- ):
54
- raise TypeError(
55
- f'Cannot overwrite attribute {name} in class {self._cls.__name__}. '
56
- f'Consider using functools.total_ordering',
57
- )
@@ -1,53 +0,0 @@
1
- from ... import lang
2
- from .fields import field_assign
3
- from .params import get_field_extras
4
- from .processing import Processor
5
- from .utils import create_fn
6
- from .utils import set_new_attribute
7
-
8
-
9
- class OverridesProcessor(Processor):
10
- def _process(self) -> None:
11
- for f in self._info.instance_fields:
12
- fx = get_field_extras(f)
13
- if not (fx.override or self._info.params_extras.override):
14
- continue
15
-
16
- if self._info.params.slots:
17
- raise TypeError
18
-
19
- self_name = '__dataclass_self__' if 'self' in self._info.fields else 'self'
20
-
21
- getter = create_fn(
22
- f.name,
23
- (self_name,),
24
- [f'return {self_name}.__dict__[{f.name!r}]'],
25
- globals=self._info.globals,
26
- return_type=lang.just(f.type),
27
- )
28
- prop = property(getter)
29
-
30
- if not self._info.params.frozen:
31
- setter = create_fn(
32
- f.name,
33
- (self_name, f'{f.name}: __dataclass_type_{f.name}__'),
34
- [
35
- field_assign(
36
- self._info.params.frozen,
37
- f.name,
38
- f.name,
39
- self_name,
40
- True,
41
- ),
42
- ],
43
- globals=self._info.globals,
44
- locals={f'__dataclass_type_{f.name}__': f.type},
45
- return_type=lang.just(None),
46
- )
47
- prop = prop.setter(setter)
48
-
49
- set_new_attribute(
50
- self._cls,
51
- f.name,
52
- prop,
53
- )
@@ -1,128 +0,0 @@
1
- """
2
- @dc.dataclass(frozen=True)
3
- class Field_:
4
- name: str | None = None
5
- type: Any = None
6
- default: Any | MISSING = MISSING
7
- default_factory: Any | MISSING = MISSING
8
- repr: bool = True
9
- hash: bool | None = None
10
- init: bool = True
11
- compare: bool = True
12
- metadata: Metadata | None = None
13
- kw_only: bool | MISSING = MISSING
14
-
15
- if sys.version_info >= (3, 13):
16
- doc: str | None = None
17
-
18
- _field_type: Any = None
19
-
20
-
21
- @dc.dataclass(frozen=True)
22
- class Params_:
23
- init: bool = True
24
- repr: bool = True
25
- eq: bool = True
26
- order: bool = False
27
- unsafe_hash: bool = False
28
- frozen: bool = False
29
-
30
- match_args: bool = True
31
- kw_only: bool = False
32
- slots: bool = False
33
- weakref_slot: bool = False
34
- """
35
- import dataclasses as dc
36
- import typing as ta
37
-
38
- from ... import lang
39
- from .internals import PARAMS_ATTR
40
- from .internals import Params
41
- from .metadata import EMPTY_METADATA
42
- from .metadata import METADATA_ATTR
43
-
44
-
45
- ##
46
-
47
-
48
- @dc.dataclass(frozen=True, kw_only=True)
49
- class FieldExtras(lang.Final):
50
- derive: ta.Callable[..., ta.Any] | None = None # TODO
51
- coerce: bool | ta.Callable[[ta.Any], ta.Any] | None = None
52
- validate: ta.Callable[[ta.Any], bool] | None = None
53
- check_type: bool | type | tuple[type | None, ...] | None = None
54
- override: bool = False
55
- repr_fn: ta.Callable[[ta.Any], str | None] | None = None
56
- repr_priority: int | None = None
57
- frozen: bool | None = None # TODO
58
-
59
-
60
- DEFAULT_FIELD_EXTRAS = FieldExtras()
61
-
62
-
63
- def get_field_extras(f: dc.Field) -> FieldExtras:
64
- if not isinstance(f, dc.Field):
65
- raise TypeError(f)
66
- return f.metadata.get(FieldExtras, DEFAULT_FIELD_EXTRAS)
67
-
68
-
69
- ##
70
-
71
-
72
- def get_params_cls(obj: ta.Any) -> type | None:
73
- if not isinstance(obj, type):
74
- obj = type(obj)
75
- for cur in obj.__mro__:
76
- if PARAMS_ATTR in cur.__dict__:
77
- return cur
78
- return None
79
-
80
-
81
- def get_params(obj: ta.Any) -> Params:
82
- if not hasattr(obj, PARAMS_ATTR):
83
- raise TypeError(obj)
84
- return getattr(obj, PARAMS_ATTR)
85
-
86
-
87
- ##
88
-
89
-
90
- @dc.dataclass(frozen=True, kw_only=True)
91
- class ParamsExtras(lang.Final):
92
- reorder: bool = False
93
- cache_hash: bool = False
94
- generic_init: bool = False
95
- override: bool = False
96
- repr_id: bool = False
97
-
98
-
99
- DEFAULT_PARAMS_EXTRAS = ParamsExtras()
100
-
101
-
102
- def get_params_extras(obj: ta.Any) -> ParamsExtras:
103
- if (pcls := get_params_cls(obj)) is None:
104
- raise TypeError(pcls)
105
-
106
- md = pcls.__dict__.get(METADATA_ATTR, EMPTY_METADATA)
107
- return md.get(ParamsExtras, DEFAULT_PARAMS_EXTRAS)
108
-
109
-
110
- ##
111
-
112
-
113
- @dc.dataclass(frozen=True)
114
- class MetaclassParams:
115
- confer: frozenset[str] = frozenset()
116
- final_subclasses: bool = False
117
- abstract_immediate_subclasses: bool = False
118
-
119
-
120
- DEFAULT_METACLASS_PARAMS = MetaclassParams()
121
-
122
-
123
- def get_metaclass_params(obj: ta.Any) -> MetaclassParams:
124
- if (pcls := get_params_cls(obj)) is None:
125
- raise TypeError(pcls)
126
-
127
- md = pcls.__dict__.get(METADATA_ATTR, EMPTY_METADATA)
128
- return md.get(MetaclassParams, DEFAULT_METACLASS_PARAMS)
@@ -1,24 +0,0 @@
1
- import typing as ta
2
-
3
- from ... import lang
4
-
5
-
6
- if ta.TYPE_CHECKING:
7
- from .reflect import ClassInfo
8
-
9
-
10
- class Processor(lang.Abstract):
11
- def __init__(self, info: 'ClassInfo') -> None:
12
- super().__init__()
13
- self._cls = info.cls
14
- self._info = info
15
-
16
- def check(self) -> None:
17
- pass
18
-
19
- @lang.cached_function
20
- def process(self) -> None:
21
- self._process()
22
-
23
- def _process(self) -> None:
24
- raise NotImplementedError
@@ -1,40 +0,0 @@
1
- import dataclasses as dc
2
-
3
- from .fields import field_type
4
- from .internals import FIELDS_ATTR
5
- from .internals import FieldType
6
- from .internals import is_dataclass_instance
7
- from .processing import Processor
8
- from .utils import set_new_attribute
9
-
10
-
11
- MISSING = dc.MISSING
12
-
13
-
14
- def replace(obj, /, **changes): # noqa
15
- if not is_dataclass_instance(obj):
16
- raise TypeError('replace() should be called on dataclass instances')
17
- return _replace(obj, **changes)
18
-
19
-
20
- def _replace(obj, /, **changes):
21
- for f in getattr(obj, FIELDS_ATTR).values():
22
- if (ft := field_type(f)) is FieldType.CLASS:
23
- continue
24
-
25
- if not f.init:
26
- if f.name in changes:
27
- raise TypeError(f'field {f.name} is declared with init=False, it cannot be specified with replace()')
28
- continue
29
-
30
- if f.name not in changes:
31
- if ft is FieldType.INIT and f.default is MISSING:
32
- raise TypeError(f'InitVar {f.name!r} must be specified with replace()')
33
- changes[f.name] = getattr(obj, f.name)
34
-
35
- return obj.__class__(**changes)
36
-
37
-
38
- class ReplaceProcessor(Processor):
39
- def _process(self) -> None:
40
- set_new_attribute(self._cls, '__replace__', _replace)
@@ -1,66 +0,0 @@
1
- import dataclasses as dc
2
- import reprlib
3
- import typing as ta
4
-
5
- from .params import get_field_extras
6
- from .processing import Processor
7
- from .utils import Namespace
8
- from .utils import create_fn
9
- from .utils import set_new_attribute
10
-
11
-
12
- def repr_fn(
13
- fields: ta.Sequence[dc.Field],
14
- globals: Namespace, # noqa
15
- *,
16
- repr_id: bool = False,
17
- ) -> ta.Callable:
18
- locals: dict[str, ta.Any] = {} # noqa
19
-
20
- fields = sorted(fields, key=lambda f: get_field_extras(f).repr_priority or 0)
21
-
22
- prefix_src = '{self.__class__.__qualname__}'
23
- if repr_id:
24
- prefix_src += '@{hex(id(self))[2:]}'
25
-
26
- if any(get_field_extras(f).repr_fn is not None for f in fields):
27
- lst: list[str] = []
28
- for f in fields:
29
- if (fex := get_field_extras(f)).repr_fn is not None:
30
- locals[fn_name := f'__repr_fn__{f.name}'] = fex.repr_fn
31
- lst.append(f"if (r := {fn_name}(self.{f.name})) is not None: l.append(f'{f.name}={{r}}')")
32
- else:
33
- lst.append(f"l.append(f'{f.name}={{self.{f.name}!r}}')")
34
-
35
- src = [
36
- 'l = []',
37
- *lst,
38
- f'return f"{prefix_src}({{", ".join(l)}})"',
39
- ]
40
-
41
- else:
42
- src = [
43
- f'return f"{prefix_src}(' +
44
- ', '.join([f'{f.name}={{self.{f.name}!r}}' for f in fields]) +
45
- ')"',
46
- ]
47
-
48
- fn = create_fn(
49
- '__repr__',
50
- ('self',),
51
- src,
52
- globals=globals,
53
- locals=locals,
54
- )
55
-
56
- return reprlib.recursive_repr()(fn)
57
-
58
-
59
- class ReprProcessor(Processor):
60
- def _process(self) -> None:
61
- if not self._info.params.repr:
62
- return
63
-
64
- flds = [f for f in self._info.instance_fields if f.repr]
65
- rfn = repr_fn(flds, self._info.globals, repr_id=self._info.params_extras.repr_id)
66
- set_new_attribute(self._cls, '__repr__', rfn) # noqa
@@ -1,50 +0,0 @@
1
- import inspect
2
-
3
- from .init import get_init_fields
4
- from .processing import Processor
5
- from .utils import create_fn
6
- from .utils import set_new_attribute
7
-
8
-
9
- class EqProcessor(Processor):
10
- def _process(self) -> None:
11
- if not self._info.params.eq:
12
- return
13
-
14
- # flds = [f for f in self._info.instance_fields if f.compare]
15
- # self_tuple = tuple_str('self', flds)
16
- # other_tuple = tuple_str('other', flds)
17
- # set_new_attribute(cls, '__eq__', _cmp_fn('__eq__', '==', self_tuple, other_tuple, globals=globals))
18
- cmp_fields = [field for field in self._info.instance_fields if field.compare]
19
- terms = [f'self.{field.name} == other.{field.name}' for field in cmp_fields]
20
- field_comparisons = ' and '.join(terms) or 'True'
21
- body = [
22
- f'if self is other:',
23
- f' return True',
24
- f'if other.__class__ is self.__class__:',
25
- f' return {field_comparisons}',
26
- f'return NotImplemented',
27
- ]
28
- func = create_fn('__eq__', ('self', 'other'), body, globals=self._info.globals)
29
- set_new_attribute(self._cls, '__eq__', func)
30
-
31
-
32
- class DocProcessor(Processor):
33
- def _process(self) -> None:
34
- if getattr(self._cls, '__doc__'):
35
- return
36
-
37
- try:
38
- text_sig = str(inspect.signature(self._cls)).replace(' -> None', '')
39
- except (TypeError, ValueError):
40
- text_sig = ''
41
- self._cls.__doc__ = (self._cls.__name__ + text_sig)
42
-
43
-
44
- class MatchArgsProcessor(Processor):
45
- def _process(self) -> None:
46
- if not self._info.params.match_args:
47
- return
48
-
49
- ifs = get_init_fields(self._info.fields.values())
50
- set_new_attribute(self._cls, '__match_args__', tuple(f.name for f in ifs.std))
@@ -1,167 +0,0 @@
1
- import dataclasses as dc
2
- import textwrap
3
- import types
4
- import typing as ta
5
-
6
- from ... import check
7
- from ... import lang
8
-
9
-
10
- T = ta.TypeVar('T')
11
- Namespace: ta.TypeAlias = ta.MutableMapping[str, ta.Any]
12
-
13
-
14
- def create_fn(
15
- name: str,
16
- args: ta.Sequence[str],
17
- body: ta.Sequence[str],
18
- *,
19
- globals: Namespace | None = None, # noqa
20
- locals: Namespace | None = None, # noqa
21
- return_type: lang.Maybe[ta.Any] = lang.empty(),
22
- ) -> ta.Callable:
23
- check.not_isinstance(args, str)
24
- check.not_isinstance(body, str)
25
-
26
- if locals is None:
27
- locals = {} # noqa
28
- return_annotation = ''
29
- if return_type.present:
30
- locals['__dataclass_return_type__'] = return_type()
31
- return_annotation = '->__dataclass_return_type__'
32
- args = ','.join(args)
33
- body = '\n'.join(f' {b}' for b in body)
34
-
35
- txt = f' def {name}({args}){return_annotation}:\n{body}'
36
-
37
- local_vars = ', '.join(locals.keys())
38
- txt = f'def __create_fn__({local_vars}):\n{txt}\n return {name}'
39
- ns: dict[str, ta.Any] = {}
40
- exec(txt, globals, ns) # type: ignore
41
- return ns['__create_fn__'](**locals)
42
-
43
-
44
- # TODO: https://github.com/python/cpython/commit/8945b7ff55b87d11c747af2dad0e3e4d631e62d6
45
- class FuncBuilder:
46
- def __init__(self, globals: Namespace) -> None: # noqa
47
- super().__init__()
48
-
49
- self.names: list[str] = []
50
- self.src: list[str] = []
51
- self.globals = globals
52
- self.locals: Namespace = {}
53
- self.overwrite_errors: dict[str, bool] = {}
54
- self.unconditional_adds: dict[str, bool] = {}
55
-
56
- def add_fn(
57
- self,
58
- name: str,
59
- args: ta.Sequence[str],
60
- body: ta.Sequence[str],
61
- *,
62
- locals: Namespace | None = None, # noqa
63
- return_type: lang.Maybe[ta.Any] = lang.empty(),
64
- overwrite_error: bool = False,
65
- unconditional_add: bool = False,
66
- decorator: str | None = None,
67
- ) -> None:
68
- if locals is not None:
69
- self.locals.update(locals)
70
-
71
- # Keep track if this method is allowed to be overwritten if it already exists in the class. The error is
72
- # method-specific, so keep it with the name. We'll use this when we generate all of the functions in the
73
- # add_fns_to_class call. overwrite_error is either True, in which case we'll raise an error, or it's a string,
74
- # in which case we'll raise an error and append this string.
75
- if overwrite_error:
76
- self.overwrite_errors[name] = overwrite_error
77
-
78
- # Should this function always overwrite anything that's already in the class? The default is to not overwrite a
79
- # function that already exists.
80
- if unconditional_add:
81
- self.unconditional_adds[name] = True
82
-
83
- self.names.append(name)
84
-
85
- if return_type.present:
86
- self.locals[f'__dataclass_{name}_return_type__'] = return_type()
87
- return_annotation = f' -> __dataclass_{name}_return_type__'
88
- else:
89
- return_annotation = ''
90
- args = ', '.join(args)
91
- body = textwrap.indent('\n'.join(body), ' ')
92
-
93
- # Compute the text of the entire function, add it to the text we're generating.
94
- deco_str = ' {decorator}\n' if decorator else ''
95
- self.src.append(f'{deco_str} def {name}({args}){return_annotation}:\n{body}')
96
-
97
- def add_fns_to_class(self, cls: type) -> None:
98
- # The source to all of the functions we're generating.
99
- fns_src = '\n'.join(self.src)
100
-
101
- # The locals they use.
102
- local_vars = ','.join(self.locals)
103
-
104
- # The names of all of the functions, used for the return value of the outer function. Need to handle the
105
- # 0-tuple specially.
106
- if not self.names:
107
- return_names = '()'
108
- else:
109
- return_names = f'({",".join(self.names)},)'
110
-
111
- # txt is the entire function we're going to execute, including the bodies of the functions we're defining.
112
- # Here's a greatly simplified version:
113
- # def __create_fn__():
114
- # def __init__(self, x, y):
115
- # self.x = x
116
- # self.y = y
117
- # @recursive_repr
118
- # def __repr__(self):
119
- # return f'cls(x={self.x!r},y={self.y!r})'
120
- # return __init__,__repr__
121
-
122
- txt = f'def __create_fn__({local_vars}):\n{fns_src}\n return {return_names}'
123
- ns: dict[str, ta.Any] = {}
124
- exec(txt, self.globals, ns) # type: ignore
125
- fns = ns['__create_fn__'](**self.locals)
126
-
127
- # Now that we've generated the functions, assign them into cls.
128
- for name, fn in zip(self.names, fns):
129
- fn.__qualname__ = f'{cls.__qualname__}.{fn.__name__}'
130
- if self.unconditional_adds.get(name, False):
131
- setattr(cls, name, fn)
132
- else:
133
- already_exists = set_new_attribute(cls, name, fn)
134
-
135
- # See if it's an error to overwrite this particular function.
136
- if already_exists and (msg_extra := self.overwrite_errors.get(name)):
137
- error_msg = (f'Cannot overwrite attribute {fn.__name__} in class {cls.__name__}')
138
- if msg_extra:
139
- error_msg = f'{error_msg} {msg_extra}'
140
-
141
- raise TypeError(error_msg)
142
-
143
-
144
- def set_qualname(cls: type, value: T) -> T:
145
- if isinstance(value, types.FunctionType):
146
- value.__qualname__ = f'{cls.__qualname__}.{value.__name__}'
147
- return value
148
-
149
-
150
- def set_new_attribute(cls: type, name: str, value: ta.Any) -> bool:
151
- if name in cls.__dict__:
152
- return True
153
- set_qualname(cls, value)
154
- setattr(cls, name, value)
155
- return False
156
-
157
-
158
- def tuple_str(obj_name: str, fields: ta.Iterable[dc.Field]) -> str:
159
- # Return a string representing each field of obj_name as a tuple member. So, if fields is ['x', 'y'] and obj_name
160
- # is "self", return "(self.x,self.y)".
161
-
162
- # Special case for the 0-tuple.
163
- if not fields:
164
- return '()'
165
-
166
- # Note the trailing comma, needed if this turns out to be a 1-tuple.
167
- return f'({",".join([f"{obj_name}.{f.name}" for f in fields])},)'