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
@@ -0,0 +1,12 @@
1
+ import typing as ta
2
+
3
+
4
+ ##
5
+
6
+
7
+ def opt_repr(o: ta.Any) -> str | None:
8
+ return repr(o) if o is not None else None
9
+
10
+
11
+ def truthy_repr(o: ta.Any) -> str | None:
12
+ return repr(o) if o else None
@@ -8,9 +8,9 @@ import copy
8
8
  import dataclasses as dc
9
9
  import typing as ta
10
10
 
11
- from .. import lang
12
- from ..lite.dataclasses import is_immediate_dataclass
13
- from .impl.api import dataclass
11
+ from ... import lang
12
+ from ...lite.dataclasses import is_immediate_dataclass
13
+ from ..api.classes.decorator import dataclass
14
14
 
15
15
 
16
16
  ##
@@ -122,6 +122,27 @@ class Static(lang.Abstract):
122
122
  # Use a default_factory to allow unsafe (mutable) values.
123
123
  new_fld.default_factory = (lambda v2: lambda: v2)(v) # noqa
124
124
 
125
+ # FIXME
126
+ from ..api.fields.metadata import _ExtraFieldParamsMetadata # noqa
127
+ from ..specs import FieldSpec
128
+ try:
129
+ x_fs = fld.metadata[FieldSpec]
130
+ except KeyError:
131
+ pass
132
+ else:
133
+ n_md = {
134
+ k: v
135
+ for k, v in fld.metadata.items()
136
+ if k not in (FieldSpec, _ExtraFieldParamsMetadata)
137
+ }
138
+ n_md[_ExtraFieldParamsMetadata] = {
139
+ fs_f.name: getattr(x_fs, fs_f.name)
140
+ for fs_f in dc.fields(FieldSpec) # noqa
141
+ if fs_f not in dc.Field.__slots__ # type: ignore[attr-defined]
142
+ and fs_f.name not in ('default', 'default_factory')
143
+ }
144
+ new_fld.metadata = n_md # type: ignore[assignment]
145
+
125
146
  setattr(cls, fld.name, new_fld)
126
147
  new_anns[fld.name] = fld.type
127
148
 
@@ -144,7 +165,7 @@ class Static(lang.Abstract):
144
165
  )
145
166
 
146
167
  # Explicitly forbid dc transforms that rebuild the class, such as slots.
147
- if (dc_cls := dataclass(cls, frozen=True)) is not cls:
168
+ if (dc_cls := dataclass(frozen=True)(cls)) is not cls:
148
169
  raise TypeError(dc_cls)
149
170
 
150
171
  dc_flds = dc.fields(cls) # type: ignore[arg-type] # noqa
@@ -1,153 +1,98 @@
1
+ import ast
1
2
  import collections
2
- import dataclasses as dc
3
+ import functools
3
4
  import types
4
5
  import typing as ta
5
6
 
6
7
  from .. import check
7
- from .impl.metadata import METADATA_ATTR
8
- from .impl.metadata import UserMetadata
9
- from .impl.params import DEFAULT_FIELD_EXTRAS
10
- from .impl.params import FieldExtras
11
- from .impl.params import get_field_extras
12
8
 
13
9
 
14
10
  T = ta.TypeVar('T')
15
11
 
16
-
17
- ##
18
-
19
-
20
- def opt_repr(o: ta.Any) -> str | None:
21
- return repr(o) if o is not None else None
22
-
23
-
24
- def truthy_repr(o: ta.Any) -> str | None:
25
- return repr(o) if o else None
12
+ K = ta.TypeVar('K')
13
+ V = ta.TypeVar('V')
26
14
 
27
15
 
28
16
  ##
29
17
 
30
18
 
31
- def fields_dict(cls_or_instance: ta.Any) -> dict[str, dc.Field]:
32
- return {f.name: f for f in dc.fields(cls_or_instance)}
19
+ def repr_round_trip_value(v: T) -> T:
20
+ r = repr(v)
21
+ v2 = ast.literal_eval(r)
22
+ if v != v2:
23
+ raise ValueError(v)
24
+ return v2
33
25
 
34
26
 
35
27
  ##
36
28
 
37
29
 
38
- class field_modifier: # noqa
39
- def __init__(self, fn: ta.Callable[[dc.Field], dc.Field]) -> None:
40
- super().__init__()
41
- self.fn = fn
30
+ def set_qualname(cls: type, value: T) -> T:
31
+ if isinstance(value, types.FunctionType):
32
+ value.__qualname__ = f'{cls.__qualname__}.{value.__name__}'
33
+ return value
42
34
 
43
- def __ror__(self, other: T) -> T:
44
- return self(other)
45
35
 
46
- def __call__(self, f: T) -> T:
47
- return check.isinstance(self.fn(check.isinstance(f, dc.Field)), dc.Field) # type: ignore
36
+ def set_new_attribute(cls: type, name: str, value: ta.Any) -> bool:
37
+ if name in cls.__dict__:
38
+ return True
39
+ set_qualname(cls, value)
40
+ setattr(cls, name, value)
41
+ return False
48
42
 
49
43
 
50
- def chain_metadata(*mds: ta.Mapping) -> types.MappingProxyType:
51
- return types.MappingProxyType(collections.ChainMap(*mds)) # type: ignore # noqa
44
+ ##
52
45
 
53
46
 
54
- def update_class_metadata(cls: type[T], *args: ta.Any) -> type[T]:
55
- check.isinstance(cls, type)
56
- setattr(cls, METADATA_ATTR, md := getattr(cls, METADATA_ATTR, {}))
57
- md.setdefault(UserMetadata, []).extend(args)
58
- return cls
47
+ class SealableRegistry(ta.Generic[K, V]):
48
+ def __init__(self) -> None:
49
+ super().__init__()
59
50
 
51
+ self._dct: dict[K, V] = {}
52
+ self._sealed = False
60
53
 
61
- def update_field_metadata(f: dc.Field, nmd: ta.Mapping) -> dc.Field:
62
- check.isinstance(f, dc.Field)
63
- f.metadata = chain_metadata(nmd, f.metadata)
64
- return f
54
+ def seal(self) -> None:
55
+ self._sealed = True
65
56
 
57
+ def __setitem__(self, k: K, v: V) -> None:
58
+ check.state(not self._sealed)
59
+ check.not_in(k, self._dct)
60
+ self._dct[k] = v
66
61
 
67
- def update_field_extras(f: dc.Field, *, unless_non_default: bool = False, **kwargs: ta.Any) -> dc.Field:
68
- fe = get_field_extras(f)
69
- return update_field_metadata(f, {
70
- FieldExtras: dc.replace(fe, **{
71
- k: v
72
- for k, v in kwargs.items()
73
- if not unless_non_default or v != getattr(DEFAULT_FIELD_EXTRAS, k)
74
- }),
75
- })
62
+ def __getitem__(self, k: K) -> V:
63
+ self.seal()
64
+ return self._dct[k]
76
65
 
66
+ def items(self) -> ta.Iterator[tuple[K, V]]:
67
+ self.seal()
68
+ return iter(self._dct.items())
77
69
 
78
- def update_fields(
79
- fn: ta.Callable[[str, dc.Field], dc.Field],
80
- fields: ta.Iterable[str] | None = None,
81
- ) -> ta.Callable[[type[T]], type[T]]:
82
- def inner(cls):
83
- if fields is None:
84
- for a, v in list(cls.__dict__.items()):
85
- if isinstance(v, dc.Field):
86
- setattr(cls, a, fn(a, v))
87
70
 
88
- else:
89
- for a in fields:
90
- try:
91
- v = cls.__dict__[a]
92
- except KeyError:
93
- v = dc.field()
94
- else:
95
- if not isinstance(v, dc.Field):
96
- v = dc.field(default=v)
97
- setattr(cls, a, fn(a, v))
71
+ ##
98
72
 
99
- return cls
100
73
 
101
- check.not_isinstance(fields, str)
74
+ def class_decorator(fn):
75
+ @functools.wraps(fn)
76
+ def inner(cls=None, *args, **kwargs):
77
+ if cls is None:
78
+ return lambda cls: fn(cls, *args, **kwargs) # noqa
79
+ return fn(cls, *args, **kwargs)
102
80
  return inner
103
81
 
104
82
 
105
- def update_fields_metadata(
106
- nmd: ta.Mapping,
107
- fields: ta.Iterable[str] | None = None,
108
- ) -> ta.Callable[[type[T]], type[T]]:
109
- def inner(a: str, f: dc.Field) -> dc.Field:
110
- return update_field_metadata(f, nmd)
111
-
112
- return update_fields(inner, fields)
113
-
114
-
115
83
  ##
116
84
 
117
85
 
118
- def shallow_astuple(o: ta.Any) -> tuple[ta.Any, ...]:
119
- return tuple(getattr(o, f.name) for f in dc.fields(o))
86
+ _EMPTY_MAPPING_PROXY: ta.Mapping = types.MappingProxyType({})
120
87
 
121
88
 
122
- def shallow_asdict(o: ta.Any) -> dict[str, ta.Any]:
123
- return {f.name: getattr(o, f.name) for f in dc.fields(o)}
124
-
125
-
126
- ##
127
-
128
-
129
- def deep_replace(o: T, *args: str | ta.Callable[[ta.Any], ta.Mapping[str, ta.Any]]) -> T:
130
- if not args:
131
- return o
132
- elif len(args) == 1:
133
- return dc.replace(o, **args[0](o)) # type: ignore
89
+ def chain_mapping_proxy(*ms: ta.Mapping) -> types.MappingProxyType:
90
+ m: ta.Any
91
+ if len(ms) > 1:
92
+ m = collections.ChainMap(*ms) # type: ignore[arg-type]
93
+ elif ms:
94
+ [m] = ms
134
95
  else:
135
- return dc.replace(o, **{args[0]: deep_replace(getattr(o, args[0]), *args[1:])}) # type: ignore
136
-
137
-
138
- ##
139
-
140
-
141
- def iter_items(obj: ta.Any) -> ta.Iterator[tuple[str, ta.Any]]:
142
- for f in dc.fields(obj):
143
- yield (f.name, getattr(obj, f.name))
144
-
145
-
146
- def iter_keys(obj: ta.Any) -> ta.Iterator[str]:
147
- for f in dc.fields(obj):
148
- yield f.name
149
-
96
+ m = _EMPTY_MAPPING_PROXY
150
97
 
151
- def iter_values(obj: ta.Any) -> ta.Iterator[ta.Any]:
152
- for f in dc.fields(obj):
153
- yield getattr(obj, f.name)
98
+ return types.MappingProxyType(m)
omlish/diag/__init__.py CHANGED
@@ -17,10 +17,10 @@ Debuggers
17
17
 
18
18
  CPU Profilers
19
19
  - cProfile - https://docs.python.org/3/library/profile.html
20
- - pyinstrument - https://github.com/joerick/pyinstrument
21
- - py-pspy - https://github.com/benfred/py-spy
22
- - austin-dist - https://github.com/P403n1x87/austin
23
- - yappi - https://github.com/sumerc/yappi
20
+ - yappi - https://github.com/sumerc/yappi - tracing
21
+ - pyinstrument - https://github.com/joerick/pyinstrument - pretty
22
+ - py-spy - https://github.com/benfred/py-spy - busted on mac
23
+ - austin-dist - https://github.com/P403n1x87/austin - old
24
24
 
25
25
  Memory Profilers
26
26
  - tracemalloc - https://docs.python.org/3/library/tracemalloc.html
@@ -26,7 +26,7 @@ ORIGIN_IGNORED_PACKAGES = frozenset([
26
26
  lang.functions.__name__,
27
27
 
28
28
  dc.__name__,
29
- dc.impl.__name__, # noqa
29
+ *[m for m in sys.modules if m.startswith(dc.__name__ + '.')],
30
30
  ])
31
31
 
32
32
 
@@ -151,11 +151,12 @@ def _make_cache_key_maker(
151
151
 
152
152
 
153
153
  class _CachedFunction(ta.Generic[T], Abstract):
154
- @dc.dataclass(frozen=True)
154
+ @dc.dataclass(frozen=True, kw_only=True)
155
155
  class Opts:
156
156
  map_maker: ta.Callable[[], ta.MutableMapping] = dict
157
157
  lock: DefaultLockable = None
158
158
  transient: bool = False
159
+ no_wrapper_update: bool = False
159
160
 
160
161
  def __init__(
161
162
  self,
@@ -175,7 +176,8 @@ class _CachedFunction(ta.Generic[T], Abstract):
175
176
  self._lock = default_lock(opts.lock, False)() if opts.lock is not None else None
176
177
  self._values = values if values is not None else opts.map_maker()
177
178
  self._value_fn = value_fn if value_fn is not None else fn
178
- functools.update_wrapper(self, fn)
179
+ if not self._opts.no_wrapper_update:
180
+ functools.update_wrapper(self, fn)
179
181
 
180
182
  @property
181
183
  def _fn(self):
omlish/lang/maybes.py CHANGED
@@ -1,6 +1,9 @@
1
1
  import abc
2
2
  import typing as ta
3
3
 
4
+ from ..lite.maybes import Maybe as _LiteMaybe
5
+ from ..lite.maybes import as_maybe as _as_lite_maybe
6
+
4
7
 
5
8
  T = ta.TypeVar('T')
6
9
  U = ta.TypeVar('U')
@@ -123,6 +126,9 @@ class _Maybe(Maybe[T], tuple):
123
126
  raise exception_supplier()
124
127
 
125
128
 
129
+ #
130
+
131
+
126
132
  def just(v: T) -> Maybe[T]:
127
133
  return tuple.__new__(_Maybe, (v,)) # noqa
128
134
 
@@ -138,3 +144,14 @@ def maybe(o: T | None) -> Maybe[T]:
138
144
  if o is None:
139
145
  return _empty # noqa
140
146
  return just(o)
147
+
148
+
149
+ ##
150
+
151
+
152
+ @_as_lite_maybe.register
153
+ def _(obj: Maybe) -> _LiteMaybe:
154
+ if obj.present:
155
+ return _LiteMaybe.just(obj.must())
156
+ else:
157
+ return _LiteMaybe.empty()
omlish/lite/maybes.py CHANGED
@@ -1,10 +1,14 @@
1
1
  import abc
2
+ import functools
2
3
  import typing as ta
3
4
 
4
5
 
5
6
  T = ta.TypeVar('T')
6
7
 
7
8
 
9
+ ##
10
+
11
+
8
12
  class Maybe(ta.Generic[T]):
9
13
  @property
10
14
  @abc.abstractmethod
@@ -43,3 +47,16 @@ class _Maybe(Maybe[T], tuple):
43
47
 
44
48
 
45
49
  Maybe._empty = tuple.__new__(_Maybe, ()) # noqa
50
+
51
+
52
+ ##
53
+
54
+
55
+ @functools.singledispatch
56
+ def as_maybe(obj: ta.Any) -> Maybe:
57
+ raise TypeError(obj)
58
+
59
+
60
+ @as_maybe.register
61
+ def _(obj: Maybe) -> Maybe:
62
+ return obj
@@ -34,11 +34,7 @@ from .unmarshal import ObjectUnmarshaler
34
34
 
35
35
 
36
36
  def get_dataclass_metadata(ty: type) -> ObjectMetadata:
37
- return check.opt_single(
38
- e
39
- for e in dc.get_merged_metadata(ty).get(dc.UserMetadata, [])
40
- if isinstance(e, ObjectMetadata)
41
- ) or ObjectMetadata()
37
+ return check.single(dc.reflect(ty).spec.metadata_by_type.get(ObjectMetadata) or [ObjectMetadata()])
42
38
 
43
39
 
44
40
  def get_dataclass_field_infos(
@@ -76,10 +72,10 @@ def get_dataclass_field_infos(
76
72
 
77
73
  f_ty: ta.Any
78
74
  if (
79
- ((cpx := dc_rfl.cls_params_extras) is not None and cpx.generic_init) or
75
+ dc_rfl.spec.generic_init or
80
76
  (fmd is not None and fmd.options.generic_replace)
81
77
  ):
82
- f_ty = rfl.to_annotation(dc_rfl.generic_replaced_field_type(field.name))
78
+ f_ty = rfl.to_annotation(dc_rfl.fields_inspection.generic_replaced_field_type(field.name))
83
79
  else:
84
80
  f_ty = type_hints[field.name]
85
81
 
@@ -14,7 +14,7 @@ T = ta.TypeVar('T')
14
14
  def update_field_metadata(**kwargs: ta.Any) -> dc.field_modifier:
15
15
  @dc.field_modifier
16
16
  def inner(f: dc.Field) -> dc.Field:
17
- return dc.update_field_metadata(f, {
17
+ return dc.set_field_metadata(f, {
18
18
  FieldMetadata: f.metadata.get(FieldMetadata, FieldMetadata()).update(**kwargs),
19
19
  })
20
20
  return inner
@@ -25,7 +25,7 @@ def update_fields_metadata(
25
25
  **kwargs: ta.Any,
26
26
  ) -> ta.Callable[[type[T]], type[T]]:
27
27
  def inner(a: str, f: dc.Field) -> dc.Field:
28
- return dc.update_field_metadata(f, {
28
+ return dc.set_field_metadata(f, {
29
29
  FieldMetadata: f.metadata.get(FieldMetadata, FieldMetadata()).update(**kwargs),
30
30
  })
31
31
 
@@ -37,7 +37,7 @@ def update_object_metadata(
37
37
  **kwargs: ta.Any,
38
38
  ):
39
39
  def inner(cls):
40
- return dc.update_class_metadata(cls, ObjectMetadata(**kwargs))
40
+ return dc.append_class_metadata(cls, ObjectMetadata(**kwargs))
41
41
 
42
42
  if cls is not None:
43
43
  inner(cls)
omlish/secrets/marshal.py CHANGED
@@ -44,7 +44,7 @@ class StrOrSecretRefMarshalerUnmarshaler(msh.Marshaler, msh.Unmarshaler):
44
44
  def marshal_secret_field(f: dc.Field) -> dc.Field:
45
45
  """Mostly obsolete with auto-registration below."""
46
46
 
47
- return dc.update_field_metadata(f, {
47
+ return dc.set_field_metadata(f, {
48
48
  msh.FieldMetadata: dc.replace(
49
49
  f.metadata.get(msh.FieldMetadata, msh.FieldMetadata()),
50
50
  marshaler=StrOrSecretRefMarshalerUnmarshaler(),
omlish/secrets/secrets.py CHANGED
@@ -79,7 +79,7 @@ def secret_repr(o: SecretRefOrStr | None) -> str | None:
79
79
 
80
80
  @dc.field_modifier
81
81
  def secret_field(f: dc.Field) -> dc.Field:
82
- return dc.update_field_extras(
82
+ return dc.update_extra_field_params(
83
83
  f,
84
84
  repr_fn=secret_repr,
85
85
  unless_non_default=True,
@@ -29,7 +29,7 @@ class Node(
29
29
  lang.Abstract,
30
30
  eq=False,
31
31
  confer=frozenset([
32
- *dc.get_metaclass_params(dc.Frozen).confer,
32
+ *dc.get_metaclass_spec(dc.Frozen).confer,
33
33
  'eq',
34
34
  ]),
35
35
  ):
omlish/text/minja.py CHANGED
@@ -5,12 +5,14 @@ TODO:
5
5
  - raw
6
6
  - blocks / inheritance
7
7
  """
8
+ import dataclasses as dc
8
9
  import io
9
10
  import re
10
11
  import typing as ta
11
12
 
12
13
  from ..lite.cached import cached_nullary
13
14
  from ..lite.check import check
15
+ from ..lite.maybes import Maybe
14
16
 
15
17
 
16
18
  ##
@@ -29,6 +31,34 @@ class MinjaTemplate:
29
31
  ##
30
32
 
31
33
 
34
+ @dc.dataclass(frozen=True)
35
+ class MinjaTemplateParam:
36
+ name: str
37
+ default: Maybe[ta.Any] = Maybe.empty()
38
+
39
+ def __post_init__(self) -> None:
40
+ check.arg(self.name.isidentifier())
41
+
42
+ @classmethod
43
+ def of(cls, obj: ta.Union[str, 'MinjaTemplateParam']) -> 'MinjaTemplateParam':
44
+ if isinstance(obj, MinjaTemplateParam):
45
+ return obj
46
+ elif isinstance(obj, str):
47
+ return MinjaTemplateParam.new(obj)
48
+ else:
49
+ raise TypeError(obj)
50
+
51
+ @classmethod
52
+ def new(cls, name: str, *defaults: ta.Any) -> 'MinjaTemplateParam':
53
+ dfl: Maybe[ta.Any]
54
+ if defaults:
55
+ [dv] = defaults
56
+ dfl = Maybe.just(dv)
57
+ else:
58
+ dfl = Maybe.empty()
59
+ return cls(name, dfl)
60
+
61
+
32
62
  class MinjaTemplateCompiler:
33
63
  """
34
64
  Compiles a template string into a Python function. The returned function takes a dictionary 'context' and returns
@@ -43,23 +73,28 @@ class MinjaTemplateCompiler:
43
73
  - {# comment #}: Ignored completely.
44
74
  """
45
75
 
46
- DEFAULT_INDENT: str = ' '
76
+ DEFAULT_INDENT: str = ' ' * 4
47
77
 
48
78
  def __init__(
49
79
  self,
50
80
  src: str,
51
- args: ta.Sequence[str],
81
+ params: ta.Sequence[ta.Union[str, MinjaTemplateParam]],
52
82
  *,
53
83
  indent: str = DEFAULT_INDENT,
54
84
  ) -> None:
55
85
  super().__init__()
56
86
 
87
+ check.not_isinstance(params, str)
88
+
57
89
  self._src = check.isinstance(src, str)
58
- self._args = check.not_isinstance(args, str)
90
+ self._params = [
91
+ MinjaTemplateParam.of(p)
92
+ for p in params
93
+ ]
94
+ check.unique(p.name for p in self._params)
59
95
  self._indent_str: str = check.non_empty_str(indent)
60
96
 
61
97
  self._stack: ta.List[ta.Literal['for', 'if']] = []
62
- self._lines: ta.List[str] = []
63
98
 
64
99
  #
65
100
 
@@ -124,41 +159,63 @@ class MinjaTemplateCompiler:
124
159
 
125
160
  _RENDER_FN_NAME = '__render'
126
161
 
162
+ class Rendered(ta.NamedTuple):
163
+ src: str
164
+ ns: ta.Dict[str, ta.Any]
165
+
127
166
  @cached_nullary
128
- def render(self) -> ta.Tuple[str, ta.Mapping[str, ta.Any]]:
167
+ def render(self) -> Rendered:
168
+ lines: ta.List[str] = []
169
+
170
+ ns: ta.Dict[str, ta.Any] = {
171
+ '__StringIO': io.StringIO,
172
+ }
173
+
129
174
  parts = self._split_tags(self._src)
130
175
 
131
- self._lines.append(f'def {self._RENDER_FN_NAME}({", ".join(self._args)}):')
132
- self._lines.append(self._indent('__output = __StringIO()'))
176
+ if not self._params:
177
+ lines.append(f'def {self._RENDER_FN_NAME}():')
178
+ else:
179
+ lines.append(f'def {self._RENDER_FN_NAME}(')
180
+ for p in self._params:
181
+ if p.default.present:
182
+ check.not_in(p.name, ns)
183
+ ns[p.name] = p.default.must()
184
+ lines.append(self._indent(f'{p.name}={p.name},'))
185
+ else:
186
+ lines.append(self._indent(f'{p.name},'))
187
+ lines.append('):')
188
+
189
+ lines.append(self._indent('__output = __StringIO()'))
133
190
 
134
191
  for g, s in parts:
135
192
  if g == '{':
136
193
  expr = s.strip()
137
- self._lines.append(self._indent(f'__output.write(str({expr}))'))
194
+ lines.append(self._indent(f'__output.write(str({expr}))'))
138
195
 
139
196
  elif g == '%':
140
197
  stmt = s.strip()
141
198
 
142
199
  if stmt.startswith('for '):
143
- self._lines.append(self._indent(stmt + ':'))
200
+ lines.append(self._indent(stmt + ':'))
144
201
  self._stack.append('for')
145
202
  elif stmt.startswith('endfor'):
146
203
  check.equal(self._stack.pop(), 'for')
147
204
 
148
205
  elif stmt.startswith('if '):
149
- self._lines.append(self._indent(stmt + ':'))
206
+ lines.append(self._indent(stmt + ':'))
150
207
  self._stack.append('if')
151
208
  elif stmt.startswith('elif '):
152
209
  check.equal(self._stack[-1], 'if')
153
- self._lines.append(self._indent(stmt + ':', -1))
210
+ lines.append(self._indent(stmt + ':', -1))
154
211
  elif stmt.strip() == 'else':
155
212
  check.equal(self._stack[-1], 'if')
156
- self._lines.append(self._indent('else:', -1))
213
+ lines.append(self._indent('else:', -1))
157
214
  elif stmt.startswith('endif'):
158
215
  check.equal(self._stack.pop(), 'if')
159
216
 
160
217
  else:
161
- self._lines.append(self._indent(stmt))
218
+ lines.append(self._indent(stmt))
162
219
 
163
220
  elif g == '#':
164
221
  pass
@@ -166,20 +223,16 @@ class MinjaTemplateCompiler:
166
223
  elif not g:
167
224
  if s:
168
225
  safe_text = s.replace('"""', '\\"""')
169
- self._lines.append(self._indent(f'__output.write("""{safe_text}""")'))
226
+ lines.append(self._indent(f'__output.write("""{safe_text}""")'))
170
227
 
171
228
  else:
172
229
  raise KeyError(g)
173
230
 
174
231
  check.empty(self._stack)
175
232
 
176
- self._lines.append(self._indent('return __output.getvalue()'))
177
-
178
- ns = {
179
- '__StringIO': io.StringIO,
180
- }
233
+ lines.append(self._indent('return __output.getvalue()'))
181
234
 
182
- return ('\n'.join(self._lines), ns)
235
+ return self.Rendered('\n'.join(lines), ns)
183
236
 
184
237
  #
185
238
 
@@ -200,12 +253,12 @@ class MinjaTemplateCompiler:
200
253
 
201
254
  @cached_nullary
202
255
  def compile(self) -> MinjaTemplate:
203
- render_src, render_ns = self.render()
256
+ rendered = self.render()
204
257
 
205
258
  render_fn = self._make_fn(
206
259
  self._RENDER_FN_NAME,
207
- render_src,
208
- render_ns,
260
+ rendered.src,
261
+ rendered.ns,
209
262
  )
210
263
 
211
264
  return MinjaTemplate(render_fn)
@@ -214,8 +267,11 @@ class MinjaTemplateCompiler:
214
267
  ##
215
268
 
216
269
 
217
- def compile_minja_template(src: str, args: ta.Sequence[str] = ()) -> MinjaTemplate:
218
- return MinjaTemplateCompiler(src, args).compile()
270
+ def compile_minja_template(
271
+ src: str,
272
+ params: ta.Sequence[ta.Union[str, MinjaTemplateParam]] = (),
273
+ ) -> MinjaTemplate:
274
+ return MinjaTemplateCompiler(src, params).compile()
219
275
 
220
276
 
221
277
  def render_minja_template(src: str, **kwargs: ta.Any) -> str: