morphic 0.2.0__tar.gz → 0.2.1__tar.gz
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.
- {morphic-0.2.0 → morphic-0.2.1}/.github/workflows/release.yml +0 -3
- {morphic-0.2.0 → morphic-0.2.1}/PKG-INFO +1 -1
- {morphic-0.2.0 → morphic-0.2.1}/src/morphic/__init__.py +2 -0
- morphic-0.2.1/src/morphic/classproperty.py +373 -0
- {morphic-0.2.0 → morphic-0.2.1}/src/morphic/typed.py +1 -40
- morphic-0.2.1/tests/test_classproperty.py +1531 -0
- {morphic-0.2.0 → morphic-0.2.1}/.cursor/rules/morphic-standards.mdc +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/.cursor/rules/typed-registry-examples.mdc +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/.github/workflows/docs.yml +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/.github/workflows/linting.yml +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/.github/workflows/tests.yml +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/.gitignore +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/LICENSE +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/README.md +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/docs/api/autoenum.md +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/docs/api/index.md +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/docs/api/registry.md +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/docs/api/string.md +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/docs/api/typed.md +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/docs/examples.md +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/docs/index.md +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/docs/installation.md +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/docs/stylesheets/extra.css +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/docs/user-guide/autoenum.md +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/docs/user-guide/getting-started.md +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/docs/user-guide/registry.md +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/docs/user-guide/string.md +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/docs/user-guide/typed-registry-integration.md +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/docs/user-guide/typed.md +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/mkdocs.yml +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/pyproject.toml +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/src/morphic/autoenum.py +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/src/morphic/function.py +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/src/morphic/imports.py +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/src/morphic/registry.py +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/src/morphic/string.py +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/src/morphic/string_data.py +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/src/morphic/structs.py +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/tests/__init__.py +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/tests/test_autoenum.py +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/tests/test_function.py +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/tests/test_imports.py +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/tests/test_registry.py +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/tests/test_string.py +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/tests/test_structs.py +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/tests/test_typed.py +0 -0
- {morphic-0.2.0 → morphic-0.2.1}/tests/test_typed_registry_integration.py +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Morphic: Dynamic Python utilities for class registration, creation, and type checking."""
|
|
2
2
|
|
|
3
3
|
from .autoenum import AutoEnum, alias, auto
|
|
4
|
+
from .classproperty import classproperty
|
|
4
5
|
from .function import (
|
|
5
6
|
FunctionSpec,
|
|
6
7
|
call_str_to_params,
|
|
@@ -98,6 +99,7 @@ __all__ = [
|
|
|
98
99
|
"MutableTyped",
|
|
99
100
|
"validate",
|
|
100
101
|
"ValidationError",
|
|
102
|
+
"classproperty",
|
|
101
103
|
# Import utilities
|
|
102
104
|
"optional_dependency",
|
|
103
105
|
# Language utilities
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""Descriptor that allows properties to be accessed at the class level.
|
|
2
|
+
|
|
3
|
+
Supports ``@abstractmethod`` stacking: when ``@abstractmethod`` wraps a
|
|
4
|
+
``@classproperty``, ABC enforcement works exactly as it does for regular
|
|
5
|
+
abstract methods — subclasses that fail to override the classproperty cannot
|
|
6
|
+
be instantiated.
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from morphic import classproperty
|
|
12
|
+
|
|
13
|
+
class Base(ABC):
|
|
14
|
+
@abstractmethod
|
|
15
|
+
@classproperty
|
|
16
|
+
def my_value(cls) -> int:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
class Concrete(Base):
|
|
20
|
+
@classproperty
|
|
21
|
+
def my_value(cls) -> int:
|
|
22
|
+
return 42
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class classproperty(property):
|
|
27
|
+
"""Descriptor that allows properties to be accessed at the class level.
|
|
28
|
+
|
|
29
|
+
A ``classproperty`` is the class-level analogue of Python's built-in
|
|
30
|
+
``@property``. Where ``@property`` computes a value from an *instance*
|
|
31
|
+
(``self``), ``@classproperty`` computes a value from the *class* (``cls``).
|
|
32
|
+
The decorated function receives the class as its first argument — not an
|
|
33
|
+
instance — and the result is available both on the class itself and on any
|
|
34
|
+
instance of that class.
|
|
35
|
+
|
|
36
|
+
``classproperty`` inherits from the built-in ``property`` type. The key
|
|
37
|
+
difference is in ``__get__``: when Python resolves attribute access,
|
|
38
|
+
``property.__get__(obj=None, objtype=cls)`` returns the descriptor object
|
|
39
|
+
itself (because there is no instance to pass to ``fget``), whereas
|
|
40
|
+
``classproperty.__get__`` passes the *class* to ``fget`` and returns the
|
|
41
|
+
computed value. This is what makes class-level access work.
|
|
42
|
+
|
|
43
|
+
Basic usage::
|
|
44
|
+
|
|
45
|
+
class Circle:
|
|
46
|
+
_pi = 3.14159
|
|
47
|
+
|
|
48
|
+
@classproperty
|
|
49
|
+
def pi(cls) -> float:
|
|
50
|
+
return cls._pi
|
|
51
|
+
|
|
52
|
+
Circle.pi # 3.14159 (class-level access)
|
|
53
|
+
Circle().pi # 3.14159 (instance-level access — same result)
|
|
54
|
+
|
|
55
|
+
Class-level and instance-level access
|
|
56
|
+
======================================
|
|
57
|
+
|
|
58
|
+
A ``classproperty`` is accessible from both the class and any instance of
|
|
59
|
+
that class. In both cases the decorated function receives the **class**
|
|
60
|
+
(not the instance) as its first argument, so the return value is always
|
|
61
|
+
the same regardless of which instance you access it through::
|
|
62
|
+
|
|
63
|
+
class Config:
|
|
64
|
+
_mode = "fast"
|
|
65
|
+
|
|
66
|
+
@classproperty
|
|
67
|
+
def mode(cls) -> str:
|
|
68
|
+
return cls._mode
|
|
69
|
+
|
|
70
|
+
# All three return the same value:
|
|
71
|
+
Config.mode # "fast" (class-level)
|
|
72
|
+
Config().mode # "fast" (instance-level)
|
|
73
|
+
Config().mode # "fast" (different instance, same result)
|
|
74
|
+
|
|
75
|
+
This holds true for all class types — plain classes, ``Typed``, ``Typed +
|
|
76
|
+
Registry``, and ABC subclasses::
|
|
77
|
+
|
|
78
|
+
from morphic import Typed
|
|
79
|
+
|
|
80
|
+
class Metric(Typed):
|
|
81
|
+
value: float
|
|
82
|
+
|
|
83
|
+
@classproperty
|
|
84
|
+
def display_range(cls) -> tuple:
|
|
85
|
+
return (0.0, 1.0)
|
|
86
|
+
|
|
87
|
+
Metric.display_range # (0.0, 1.0)
|
|
88
|
+
Metric(value=0.5).display_range # (0.0, 1.0)
|
|
89
|
+
|
|
90
|
+
When a subclass overrides a ``classproperty``, each class and its instances
|
|
91
|
+
see their own version::
|
|
92
|
+
|
|
93
|
+
class Accuracy(Metric):
|
|
94
|
+
@classproperty
|
|
95
|
+
def display_range(cls) -> tuple:
|
|
96
|
+
return (0.0, 100.0)
|
|
97
|
+
|
|
98
|
+
Metric.display_range # (0.0, 1.0)
|
|
99
|
+
Metric(value=0.5).display_range # (0.0, 1.0)
|
|
100
|
+
Accuracy.display_range # (0.0, 100.0)
|
|
101
|
+
Accuracy(value=95.0).display_range # (0.0, 100.0)
|
|
102
|
+
|
|
103
|
+
Subclass polymorphism::
|
|
104
|
+
|
|
105
|
+
class Base:
|
|
106
|
+
@classproperty
|
|
107
|
+
def tag(cls) -> str:
|
|
108
|
+
return cls.__name__.lower()
|
|
109
|
+
|
|
110
|
+
class Child(Base):
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
Base.tag # "base"
|
|
114
|
+
Child.tag # "child" (cls is Child, not Base)
|
|
115
|
+
|
|
116
|
+
Because the function receives the actual class through the MRO, subclasses
|
|
117
|
+
automatically get their own ``cls`` without overriding anything.
|
|
118
|
+
|
|
119
|
+
Overriding in subclasses::
|
|
120
|
+
|
|
121
|
+
class Metric:
|
|
122
|
+
@classproperty
|
|
123
|
+
def display_range(cls) -> tuple:
|
|
124
|
+
return (0.0, 1.0)
|
|
125
|
+
|
|
126
|
+
class Perplexity(Metric):
|
|
127
|
+
@classproperty
|
|
128
|
+
def display_range(cls) -> tuple:
|
|
129
|
+
return (0.0, float("inf"))
|
|
130
|
+
|
|
131
|
+
Metric.display_range # (0.0, 1.0)
|
|
132
|
+
Perplexity.display_range # (0.0, inf)
|
|
133
|
+
|
|
134
|
+
Abstract classproperties
|
|
135
|
+
========================
|
|
136
|
+
|
|
137
|
+
``classproperty`` supports the standard ``@abstractmethod`` decorator from
|
|
138
|
+
``abc``. The correct stacking order places ``@abstractmethod`` on the
|
|
139
|
+
outside::
|
|
140
|
+
|
|
141
|
+
from abc import ABC, abstractmethod
|
|
142
|
+
|
|
143
|
+
class Base(ABC):
|
|
144
|
+
@abstractmethod # outer — marks the descriptor as abstract
|
|
145
|
+
@classproperty # inner — creates the descriptor
|
|
146
|
+
def value(cls) -> int:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
With this stacking:
|
|
150
|
+
|
|
151
|
+
- ``Base()`` raises ``TypeError`` ("Can't instantiate abstract class …").
|
|
152
|
+
- A subclass that does **not** override ``value`` with a concrete
|
|
153
|
+
``@classproperty`` also raises ``TypeError`` on instantiation.
|
|
154
|
+
- A subclass that overrides ``value`` with a concrete ``@classproperty``
|
|
155
|
+
can be instantiated normally.
|
|
156
|
+
|
|
157
|
+
This works identically for plain classes, ``Typed`` classes, and
|
|
158
|
+
``Typed + Registry`` classes::
|
|
159
|
+
|
|
160
|
+
from morphic import Typed, Registry
|
|
161
|
+
|
|
162
|
+
class Metric(Typed, Registry, ABC):
|
|
163
|
+
value: float
|
|
164
|
+
|
|
165
|
+
@abstractmethod
|
|
166
|
+
@classproperty
|
|
167
|
+
def display_range(cls) -> tuple:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
class Accuracy(Metric):
|
|
171
|
+
aliases = ["acc"]
|
|
172
|
+
|
|
173
|
+
@classproperty
|
|
174
|
+
def display_range(cls) -> tuple:
|
|
175
|
+
return (0.0, 1.0)
|
|
176
|
+
|
|
177
|
+
Accuracy(value=0.95) # works
|
|
178
|
+
Metric.of("acc", value=0.95) # works via Registry
|
|
179
|
+
|
|
180
|
+
Intermediate abstract classes can add more abstract classproperties
|
|
181
|
+
without overriding existing ones::
|
|
182
|
+
|
|
183
|
+
class Metric(ABC):
|
|
184
|
+
@abstractmethod
|
|
185
|
+
@classproperty
|
|
186
|
+
def display_range(cls) -> tuple:
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
class RankedMetric(Metric, ABC):
|
|
190
|
+
@abstractmethod
|
|
191
|
+
@classproperty
|
|
192
|
+
def optimization_direction(cls) -> str:
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
class Accuracy(RankedMetric):
|
|
196
|
+
@classproperty
|
|
197
|
+
def display_range(cls) -> tuple:
|
|
198
|
+
return (0.0, 1.0)
|
|
199
|
+
|
|
200
|
+
@classproperty
|
|
201
|
+
def optimization_direction(cls) -> str:
|
|
202
|
+
return "maximize"
|
|
203
|
+
|
|
204
|
+
Grandchild classes that inherit from a concrete parent do **not** need to
|
|
205
|
+
re-override the classproperty::
|
|
206
|
+
|
|
207
|
+
class BinaryAccuracy(Accuracy):
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
BinaryAccuracy.display_range # (0.0, 1.0) — inherited
|
|
211
|
+
|
|
212
|
+
Concrete classproperties on ABC classes
|
|
213
|
+
=======================================
|
|
214
|
+
|
|
215
|
+
An ABC can have **both** abstract and concrete classproperties. A concrete
|
|
216
|
+
``@classproperty`` (without ``@abstractmethod``) on an ABC works exactly
|
|
217
|
+
like a classproperty on any other class: it is inherited by all subclasses
|
|
218
|
+
and is accessible from both the class and any instance. Only the abstract
|
|
219
|
+
classproperties require overriding::
|
|
220
|
+
|
|
221
|
+
class Metric(ABC):
|
|
222
|
+
@classproperty
|
|
223
|
+
def api_version(cls) -> int:
|
|
224
|
+
return 3 # concrete — inherited as-is
|
|
225
|
+
|
|
226
|
+
@abstractmethod
|
|
227
|
+
@classproperty
|
|
228
|
+
def display_range(cls) -> tuple:
|
|
229
|
+
pass # abstract — must override
|
|
230
|
+
|
|
231
|
+
class Accuracy(Metric):
|
|
232
|
+
@classproperty
|
|
233
|
+
def display_range(cls) -> tuple:
|
|
234
|
+
return (0.0, 1.0)
|
|
235
|
+
|
|
236
|
+
Accuracy.api_version # 3 (inherited, class-level)
|
|
237
|
+
Accuracy(value=0.95).api_version # 3 (inherited, instance-level)
|
|
238
|
+
Accuracy.display_range # (0.0, 1.0)
|
|
239
|
+
Accuracy(value=0.95).display_range # (0.0, 1.0)
|
|
240
|
+
|
|
241
|
+
Concrete classproperties on ABCs participate in normal MRO-based
|
|
242
|
+
polymorphism. A classproperty that reads ``cls.__name__`` will return the
|
|
243
|
+
actual subclass name when accessed on a child class or its instances::
|
|
244
|
+
|
|
245
|
+
class Base(ABC):
|
|
246
|
+
@classproperty
|
|
247
|
+
def label(cls) -> str:
|
|
248
|
+
return cls.__name__.lower()
|
|
249
|
+
|
|
250
|
+
@abstractmethod
|
|
251
|
+
def run(self) -> None:
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
class Worker(Base):
|
|
255
|
+
def run(self) -> None:
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
Worker.label # "worker" (class-level)
|
|
259
|
+
Worker().label # "worker" (instance-level)
|
|
260
|
+
Base.label # "base" (class-level on the ABC itself)
|
|
261
|
+
|
|
262
|
+
Subclasses can also override a concrete classproperty from an ABC parent::
|
|
263
|
+
|
|
264
|
+
class FastWorker(Worker):
|
|
265
|
+
@classproperty
|
|
266
|
+
def label(cls) -> str:
|
|
267
|
+
return "fast-" + cls.__name__.lower()
|
|
268
|
+
|
|
269
|
+
FastWorker.label # "fast-fastworker"
|
|
270
|
+
FastWorker().label # "fast-fastworker"
|
|
271
|
+
Worker.label # "worker" (unchanged)
|
|
272
|
+
|
|
273
|
+
Decorator stacking order
|
|
274
|
+
========================
|
|
275
|
+
|
|
276
|
+
Only one stacking order is correct::
|
|
277
|
+
|
|
278
|
+
@abstractmethod # outer
|
|
279
|
+
@classproperty # inner
|
|
280
|
+
def value(cls) -> int:
|
|
281
|
+
pass
|
|
282
|
+
|
|
283
|
+
The reverse order (``@classproperty`` wrapping ``@abstractmethod``) does
|
|
284
|
+
not raise an error, but silently defeats ABC enforcement: the
|
|
285
|
+
``classproperty`` executes ``fget`` on every ``getattr``, returning
|
|
286
|
+
``None`` from the abstract body, and ABCMeta sees a concrete value.
|
|
287
|
+
Subclasses that forget to override will not be caught.
|
|
288
|
+
|
|
289
|
+
Note:
|
|
290
|
+
``classproperty`` is used internally by ``Typed`` for built-in
|
|
291
|
+
class-level properties: ``class_name``, ``param_names``,
|
|
292
|
+
``param_default_values``, and ``_constructor``.
|
|
293
|
+
|
|
294
|
+
Reference:
|
|
295
|
+
Original ``classproperty`` pattern:
|
|
296
|
+
https://stackoverflow.com/a/13624858/4900327
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
# ======================
|
|
300
|
+
# Implementation details
|
|
301
|
+
# ======================
|
|
302
|
+
|
|
303
|
+
# Two problems prevent naive ``@abstractmethod`` + ``@classproperty``
|
|
304
|
+
# stacking from working with CPython's ``property``:
|
|
305
|
+
|
|
306
|
+
# **Problem 1 — read-only ``__isabstractmethod__``:**
|
|
307
|
+
# ``@abstractmethod`` stamps ``funcobj.__isabstractmethod__ = True`` on the
|
|
308
|
+
# object it wraps. CPython's ``property`` exposes ``__isabstractmethod__``
|
|
309
|
+
# as a read-only C-level descriptor (delegating to ``fget.__isabstractmethod__``).
|
|
310
|
+
# Writing to it raises ``AttributeError: attribute '__isabstractmethod__' of
|
|
311
|
+
# 'property' objects is not writable``.
|
|
312
|
+
|
|
313
|
+
# **Solution:** ``classproperty`` shadows the inherited C-level descriptor
|
|
314
|
+
# with a Python-level ``@property``/``@setter`` pair backed by the instance
|
|
315
|
+
# attribute ``_is_abstract``. This makes ``__isabstractmethod__`` writable,
|
|
316
|
+
# so ``@abstractmethod`` can set it.
|
|
317
|
+
|
|
318
|
+
# **Problem 2 — ``__get__`` defeats ABCMeta enforcement:**
|
|
319
|
+
# ABCMeta checks whether abstract methods are overridden by doing
|
|
320
|
+
# ``value = getattr(cls, name)`` followed by
|
|
321
|
+
# ``getattr(value, "__isabstractmethod__", False)``. For a regular
|
|
322
|
+
# ``property``, ``getattr(cls, name)`` with no instance returns the
|
|
323
|
+
# descriptor object itself (because ``property.__get__(obj=None)`` returns
|
|
324
|
+
# ``self``). ABCMeta then sees ``descriptor.__isabstractmethod__ == True``
|
|
325
|
+
# and knows the method is still abstract.
|
|
326
|
+
|
|
327
|
+
# But ``classproperty.__get__`` is designed to execute ``fget(cls)`` even
|
|
328
|
+
# when accessed on the class — that is its raison d'être. So
|
|
329
|
+
# ``getattr(SubClass, name)`` calls ``fget(SubClass)`` and returns a
|
|
330
|
+
# concrete value (e.g. ``None`` from the abstract body). ABCMeta sees a
|
|
331
|
+
# plain value with no ``__isabstractmethod__`` attribute, concludes the
|
|
332
|
+
# method is overridden, and allows instantiation. The abstract contract
|
|
333
|
+
# is silently broken.
|
|
334
|
+
|
|
335
|
+
# **Solution:** ``__get__`` checks ``self._is_abstract``. When ``True``,
|
|
336
|
+
# it returns ``self`` (the descriptor) instead of executing ``fget``. This
|
|
337
|
+
# gives ABCMeta the descriptor it needs to see the abstract flag. When a
|
|
338
|
+
# subclass provides a concrete ``@classproperty`` (which has
|
|
339
|
+
# ``_is_abstract = False``), ``__get__`` executes ``fget`` normally and
|
|
340
|
+
# returns the computed value.
|
|
341
|
+
|
|
342
|
+
# Instance-level storage for __isabstractmethod__.
|
|
343
|
+
# CPython's property exposes __isabstractmethod__ as a read-only C-level
|
|
344
|
+
# descriptor that delegates to fget.__isabstractmethod__. We shadow it
|
|
345
|
+
# with an instance attribute so that @abstractmethod (which does
|
|
346
|
+
# ``funcobj.__isabstractmethod__ = True``) can write to it.
|
|
347
|
+
_is_abstract: bool = False
|
|
348
|
+
|
|
349
|
+
@property # type: ignore[override]
|
|
350
|
+
def __isabstractmethod__(self) -> bool:
|
|
351
|
+
return self._is_abstract
|
|
352
|
+
|
|
353
|
+
@__isabstractmethod__.setter
|
|
354
|
+
def __isabstractmethod__(self, value: bool) -> None:
|
|
355
|
+
self._is_abstract = value
|
|
356
|
+
|
|
357
|
+
def __get__(self, obj, objtype=None):
|
|
358
|
+
# When the classproperty is abstract, return the descriptor itself
|
|
359
|
+
# rather than executing fget. This is required so that ABCMeta's
|
|
360
|
+
# enforcement loop (which does ``getattr(cls, name)`` and then checks
|
|
361
|
+
# ``value.__isabstractmethod__``) can see the flag and correctly track
|
|
362
|
+
# the method as unimplemented.
|
|
363
|
+
#
|
|
364
|
+
# For concrete (non-abstract) classproperties, execute fget as usual.
|
|
365
|
+
if self._is_abstract:
|
|
366
|
+
return self
|
|
367
|
+
return super(classproperty, self).__get__(objtype)
|
|
368
|
+
|
|
369
|
+
def __set__(self, obj, value):
|
|
370
|
+
super(classproperty, self).__set__(type(obj), value)
|
|
371
|
+
|
|
372
|
+
def __delete__(self, obj):
|
|
373
|
+
super(classproperty, self).__delete__(type(obj))
|
|
@@ -23,51 +23,12 @@ from pydantic.errors import PydanticSchemaGenerationError
|
|
|
23
23
|
from pydantic_core import PydanticUndefined
|
|
24
24
|
|
|
25
25
|
from .autoenum import AutoEnum
|
|
26
|
+
from .classproperty import classproperty
|
|
26
27
|
from .registry import Registry
|
|
27
28
|
from .string import format_exception_msg
|
|
28
29
|
from .structs import INBUILT_COLLECTIONS, map_collection
|
|
29
30
|
|
|
30
31
|
|
|
31
|
-
class classproperty(property):
|
|
32
|
-
"""
|
|
33
|
-
Descriptor that allows properties to be accessed at the class level.
|
|
34
|
-
|
|
35
|
-
Similar to the built-in `property` decorator, but works on classes rather than instances.
|
|
36
|
-
This allows defining computed properties that can be accessed directly on the class
|
|
37
|
-
without requiring an instance.
|
|
38
|
-
|
|
39
|
-
Examples:
|
|
40
|
-
```python
|
|
41
|
-
class MyClass:
|
|
42
|
-
_name = "Example"
|
|
43
|
-
|
|
44
|
-
@classproperty
|
|
45
|
-
def name(cls):
|
|
46
|
-
return cls._name
|
|
47
|
-
|
|
48
|
-
# Access directly on class
|
|
49
|
-
print(MyClass.name) # "Example"
|
|
50
|
-
|
|
51
|
-
# Also works on instances
|
|
52
|
-
instance = MyClass()
|
|
53
|
-
print(instance.name) # "Example"
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Note:
|
|
57
|
-
This is used internally by Typed for class-level properties like `class_name`
|
|
58
|
-
and `param_names`. Reference: https://stackoverflow.com/a/13624858/4900327
|
|
59
|
-
"""
|
|
60
|
-
|
|
61
|
-
def __get__(self, obj, objtype=None):
|
|
62
|
-
return super(classproperty, self).__get__(objtype)
|
|
63
|
-
|
|
64
|
-
def __set__(self, obj, value):
|
|
65
|
-
super(classproperty, self).__set__(type(obj), value)
|
|
66
|
-
|
|
67
|
-
def __delete__(self, obj):
|
|
68
|
-
super(classproperty, self).__delete__(type(obj))
|
|
69
|
-
|
|
70
|
-
|
|
71
32
|
def _Typed_pformat(data: Any) -> str:
|
|
72
33
|
"""
|
|
73
34
|
Pretty-format data structures for enhanced error messages.
|