didactic 0.3.1__tar.gz → 0.3.2__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.
- {didactic-0.3.1 → didactic-0.3.2}/PKG-INFO +2 -2
- {didactic-0.3.1 → didactic-0.3.2}/pyproject.toml +2 -2
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/_self_describing.py +3 -3
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/api.py +1 -2
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/axioms/_axiom_enforcement.py +0 -1
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/cli/_cli.py +9 -4
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/codegen/_json_schema.py +8 -3
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/codegen/_write.py +2 -8
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/codegen/source.py +5 -7
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/fields/_computed.py +0 -1
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/fields/_derived.py +12 -6
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/fields/_fields.py +18 -11
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/fields/_unions.py +15 -9
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/fields/_validators.py +8 -5
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/lenses/_dependent_lens.py +7 -7
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/lenses/_lens.py +11 -9
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/lenses/_testing.py +2 -2
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/migrations/_diff.py +22 -4
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/migrations/_fingerprint.py +10 -8
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/migrations/_synthesis.py +4 -7
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/models/_config.py +0 -1
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/models/_meta.py +35 -20
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/models/_model.py +1 -4
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/models/_root.py +6 -6
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/theory/_theory.py +8 -49
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/types/_types.py +18 -13
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/types/_typing.py +2 -11
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/vcs/_backref.py +8 -7
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/vcs/_repo.py +13 -11
- {didactic-0.3.1 → didactic-0.3.2}/.gitignore +0 -0
- {didactic-0.3.1 → didactic-0.3.2}/README.md +0 -0
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/axioms/__init__.py +0 -0
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/axioms/_axioms.py +0 -0
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/cli/__init__.py +0 -0
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/codegen/__init__.py +0 -0
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/codegen/_emitter.py +0 -0
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/codegen/io.py +0 -0
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/fields/__init__.py +0 -0
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/fields/_refs.py +0 -0
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/lenses/__init__.py +0 -0
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/migrations/__init__.py +0 -0
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/migrations/_migrations.py +0 -0
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/models/__init__.py +0 -0
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/models/_storage.py +0 -0
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/py.typed +0 -0
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/theory/__init__.py +0 -0
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/types/__init__.py +0 -0
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/types/_types_lib.py +0 -0
- {didactic-0.3.1 → didactic-0.3.2}/src/didactic/vcs/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: didactic
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Pydantic-class API on top of panproto: GATs, lenses, and VCS.
|
|
5
5
|
Project-URL: Homepage, https://github.com/panproto/didactic
|
|
6
6
|
Project-URL: Repository, https://github.com/panproto/didactic
|
|
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
15
15
|
Classifier: Typing :: Typed
|
|
16
16
|
Requires-Python: >=3.14
|
|
17
17
|
Requires-Dist: annotated-types>=0.7
|
|
18
|
-
Requires-Dist: panproto>=0.43
|
|
18
|
+
Requires-Dist: panproto>=0.43.1
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
|
|
21
21
|
# didactic
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "didactic"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Pydantic-class API on top of panproto: GATs, lenses, and VCS."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.14"
|
|
@@ -22,7 +22,7 @@ classifiers = [
|
|
|
22
22
|
"Typing :: Typed",
|
|
23
23
|
]
|
|
24
24
|
dependencies = [
|
|
25
|
-
"panproto>=0.43",
|
|
25
|
+
"panproto>=0.43.1",
|
|
26
26
|
"annotated-types>=0.7",
|
|
27
27
|
]
|
|
28
28
|
|
|
@@ -31,9 +31,6 @@ didactic.migrations._fingerprint.structural_fingerprint : the underlying address
|
|
|
31
31
|
|
|
32
32
|
# Cross-translation between ``FieldValue`` and ``JsonValue`` shapes
|
|
33
33
|
# in the schema-URI registry layer.
|
|
34
|
-
# Tracked in panproto/didactic#1.
|
|
35
|
-
# pyright: reportArgumentType=false, reportReturnType=false
|
|
36
|
-
|
|
37
34
|
from __future__ import annotations
|
|
38
35
|
|
|
39
36
|
from typing import TYPE_CHECKING, cast
|
|
@@ -195,6 +192,9 @@ def validate_with_uri_lookup(
|
|
|
195
192
|
raise KeyError(msg)
|
|
196
193
|
|
|
197
194
|
uri = payload["$schema"]
|
|
195
|
+
if not isinstance(uri, str):
|
|
196
|
+
msg = "validate_with_uri_lookup: $schema value must be a string"
|
|
197
|
+
raise TypeError(msg)
|
|
198
198
|
cls = registry.lookup(uri)
|
|
199
199
|
if cls is None:
|
|
200
200
|
msg = f"validate_with_uri_lookup: no registered model for URI {uri!r}"
|
|
@@ -68,14 +68,13 @@ from didactic.types import _types_lib as types
|
|
|
68
68
|
from didactic.vcs._backref import ModelPool, resolve_backrefs
|
|
69
69
|
from didactic.vcs._repo import Repository
|
|
70
70
|
|
|
71
|
-
__version__ = "0.3.
|
|
71
|
+
__version__ = "0.3.2"
|
|
72
72
|
|
|
73
73
|
#: Conventional namespace for lens utilities (`dx.lens.identity(...)`,
|
|
74
74
|
#: `dx.lens.Lens`, etc.). The ``lens`` name doubles as a decorator
|
|
75
75
|
#: (``@dx.lens(A, B)``) and as a module-style namespace. The attributes
|
|
76
76
|
#: are bound by ``_LensNamespace.__init__`` in :mod:`didactic.lenses._lens`.
|
|
77
77
|
|
|
78
|
-
|
|
79
78
|
__all__ = [
|
|
80
79
|
"DEFAULT_CONFIG",
|
|
81
80
|
"Axiom",
|
|
@@ -26,7 +26,6 @@ if TYPE_CHECKING:
|
|
|
26
26
|
from didactic.axioms._axioms import Axiom
|
|
27
27
|
from didactic.types._typing import FieldValue, JsonValue
|
|
28
28
|
|
|
29
|
-
|
|
30
29
|
# A panproto Expr ``to_dict()`` AST node is one of any JsonValue-shaped
|
|
31
30
|
# payload — at the leaves of dispatch we narrow with ``match`` /
|
|
32
31
|
# ``isinstance``. Using ``JsonValue`` (rather than the narrower
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
# ``.breaking`` shape is a ``JsonValue`` union; pyright can't narrow
|
|
3
3
|
# the iteration target without per-branch ``isinstance``. Tracked in
|
|
4
4
|
# panproto/didactic#1.
|
|
5
|
-
# pyright: reportGeneralTypeIssues=false, reportOptionalIterable=false, reportUnknownVariableType=false
|
|
6
5
|
"""``didactic`` CLI front-end.
|
|
7
6
|
|
|
8
7
|
Subcommands
|
|
@@ -117,6 +116,11 @@ def _resolve_model(spec: str) -> type[Model]:
|
|
|
117
116
|
return cls
|
|
118
117
|
|
|
119
118
|
|
|
119
|
+
# Public re-export so tests can introspect the resolver without tripping
|
|
120
|
+
# pyright's reportPrivateUsage.
|
|
121
|
+
resolve_model = _resolve_model
|
|
122
|
+
|
|
123
|
+
|
|
120
124
|
def _cmd_schema_show(args: argparse.Namespace) -> int:
|
|
121
125
|
cls = _resolve_model(args.model)
|
|
122
126
|
from didactic.migrations._fingerprint import structural_fingerprint # noqa: PLC0415
|
|
@@ -190,8 +194,10 @@ def _cmd_check_breaking(args: argparse.Namespace) -> int:
|
|
|
190
194
|
print(f"compatible: {args.old} -> {args.new}")
|
|
191
195
|
return 0
|
|
192
196
|
print(f"BREAKING: {args.old} -> {args.new}")
|
|
193
|
-
|
|
194
|
-
|
|
197
|
+
breaking = report.get("breaking")
|
|
198
|
+
if isinstance(breaking, list):
|
|
199
|
+
for change in breaking:
|
|
200
|
+
print(f" {change}")
|
|
195
201
|
return 2
|
|
196
202
|
|
|
197
203
|
|
|
@@ -235,7 +241,6 @@ _SUBCOMMANDS = {
|
|
|
235
241
|
"version": _cmd_version,
|
|
236
242
|
}
|
|
237
243
|
|
|
238
|
-
|
|
239
244
|
__all__ = [
|
|
240
245
|
"main",
|
|
241
246
|
]
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# ``annotated_metadata`` lives on ``spec.extras`` as ``Opaque`` and
|
|
2
2
|
# the per-marker iteration takes the marker as an opaque value to
|
|
3
3
|
# duck-type. Tracked in panproto/didactic#1.
|
|
4
|
-
# pyright: reportUnknownVariableType=false, reportUnknownArgumentType=false
|
|
5
4
|
"""JSON Schema (Draft 2020-12) generation from a Model class.
|
|
6
5
|
|
|
7
6
|
The single user-facing entry point is
|
|
@@ -147,7 +146,13 @@ def _schema_for_field(spec: FieldSpec) -> JsonSchemaProperty:
|
|
|
147
146
|
annotation = spec.annotation
|
|
148
147
|
out: dict[str, JsonValue] = {}
|
|
149
148
|
|
|
150
|
-
|
|
149
|
+
# The map is keyed on bare ``type`` instances (``str``, ``int``,
|
|
150
|
+
# etc.); ``TypeVar`` / ``ForwardRef`` annotations fall through to
|
|
151
|
+
# the ``("string", None)`` default.
|
|
152
|
+
if isinstance(annotation, type):
|
|
153
|
+
type_str, format_str = _PRIMITIVE_TYPE_MAP.get(annotation, ("string", None))
|
|
154
|
+
else:
|
|
155
|
+
type_str, format_str = "string", None
|
|
151
156
|
out["type"] = type_str
|
|
152
157
|
if format_str is not None:
|
|
153
158
|
out["format"] = format_str
|
|
@@ -174,7 +179,7 @@ def _schema_for_field(spec: FieldSpec) -> JsonSchemaProperty:
|
|
|
174
179
|
# as ``Opaque`` (a marker Protocol); we narrow with isinstance.
|
|
175
180
|
metadata = extras.get("annotated_metadata", ()) or ()
|
|
176
181
|
if isinstance(metadata, (tuple, list)):
|
|
177
|
-
for entry in metadata:
|
|
182
|
+
for entry in cast("tuple[Opaque, ...] | list[Opaque]", metadata):
|
|
178
183
|
_apply_annotated_constraint(out, entry)
|
|
179
184
|
|
|
180
185
|
return cast("JsonSchemaProperty", out)
|
|
@@ -17,9 +17,6 @@ Examples
|
|
|
17
17
|
|
|
18
18
|
# Defensive ``isinstance(payload, bytes)`` on a parameter pyright
|
|
19
19
|
# narrows to ``bytes`` already; runtime callers may bypass.
|
|
20
|
-
# Tracked in panproto/didactic#1.
|
|
21
|
-
# pyright: reportUnnecessaryIsInstance=false
|
|
22
|
-
|
|
23
20
|
from __future__ import annotations
|
|
24
21
|
|
|
25
22
|
from pathlib import Path
|
|
@@ -96,11 +93,8 @@ def write(
|
|
|
96
93
|
ext=ext,
|
|
97
94
|
)
|
|
98
95
|
path = out_path / filename_str
|
|
99
|
-
payload = model.emit_as(target)
|
|
100
|
-
|
|
101
|
-
payload if isinstance(payload, bytes) else str(payload).encode("utf-8")
|
|
102
|
-
)
|
|
103
|
-
path.write_bytes(payload_bytes)
|
|
96
|
+
payload = model.emit_as(target)
|
|
97
|
+
path.write_bytes(payload)
|
|
104
98
|
written[f"{target}/{model.__name__}"] = path.resolve()
|
|
105
99
|
return written
|
|
106
100
|
|
|
@@ -29,14 +29,9 @@ didactic.codegen.io : instance-level codecs.
|
|
|
29
29
|
didactic.Model.emit_as : the unified entry point.
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
-
# ``schema: object`` (panproto handle carve-out) handed back to
|
|
33
|
-
# panproto's ``emit(schema: Schema)``.
|
|
34
|
-
# Tracked in panproto/didactic#1.
|
|
35
|
-
# pyright: reportArgumentType=false
|
|
36
|
-
|
|
37
32
|
from __future__ import annotations
|
|
38
33
|
|
|
39
|
-
from typing import TYPE_CHECKING
|
|
34
|
+
from typing import TYPE_CHECKING, cast
|
|
40
35
|
|
|
41
36
|
if TYPE_CHECKING:
|
|
42
37
|
from collections.abc import Callable
|
|
@@ -112,7 +107,10 @@ def emit(schema: object, *, protocol: str) -> bytes:
|
|
|
112
107
|
import panproto # noqa: PLC0415
|
|
113
108
|
|
|
114
109
|
registry = panproto.AstParserRegistry()
|
|
115
|
-
|
|
110
|
+
# ``schema`` is typed ``object`` in the public signature (carve-out
|
|
111
|
+
# for the opaque panproto handle); the runtime contract is that it
|
|
112
|
+
# came from a previous ``parse`` call and is a real ``Schema``.
|
|
113
|
+
return registry.emit(protocol, cast("panproto.Schema", schema))
|
|
116
114
|
|
|
117
115
|
|
|
118
116
|
def parse(source: bytes, *, protocol: str, file_path: str = "<source>") -> object:
|
|
@@ -47,7 +47,6 @@ didactic.computed : evaluated every read; not cached.
|
|
|
47
47
|
# ``_`` prefix is conventional for "implementation detail of the
|
|
48
48
|
# Model layer"; the derived-decorator integration is part of that
|
|
49
49
|
# layer. Tracked in panproto/didactic#1.
|
|
50
|
-
# pyright: reportPrivateUsage=false
|
|
51
50
|
|
|
52
51
|
from __future__ import annotations
|
|
53
52
|
|
|
@@ -90,18 +89,25 @@ def derived(fn: Callable[..., FieldValue]) -> property:
|
|
|
90
89
|
>>> Box(w=3, h=4).area
|
|
91
90
|
12
|
|
92
91
|
"""
|
|
93
|
-
|
|
92
|
+
# ``__didactic_derived__`` is a marker attribute consumed by
|
|
93
|
+
# ``derived_field_names`` to discover wrapped derived methods. The
|
|
94
|
+
# function-object protocol allows arbitrary attribute writes at
|
|
95
|
+
# runtime; pyright models ``Callable`` as opaque so we set through
|
|
96
|
+
# ``setattr`` to keep the boundary explicit.
|
|
97
|
+
setattr(fn, "__didactic_derived__", True) # noqa: B010
|
|
94
98
|
name = fn.__name__
|
|
95
99
|
|
|
96
100
|
def _getter(self: Model) -> FieldValue:
|
|
97
|
-
cache = self
|
|
101
|
+
cache = cast("dict[str, FieldValue]", getattr(self, "_derived_cache")) # noqa: B009
|
|
98
102
|
if name not in cache:
|
|
99
103
|
cache[name] = fn(self)
|
|
100
|
-
return
|
|
104
|
+
return cache[name]
|
|
101
105
|
|
|
102
106
|
prop = property(_getter)
|
|
103
|
-
|
|
104
|
-
|
|
107
|
+
fget = prop.fget
|
|
108
|
+
if fget is not None:
|
|
109
|
+
setattr(fget, "__didactic_derived__", True) # noqa: B010
|
|
110
|
+
setattr(fget, "__wrapped_name__", name) # noqa: B010
|
|
105
111
|
return prop
|
|
106
112
|
|
|
107
113
|
|
|
@@ -29,11 +29,14 @@ didactic.types._types : the translation primitives FieldSpec uses.
|
|
|
29
29
|
didactic.models._meta : the metaclass that consumes FieldSpec.
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
-
# ``
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
|
|
32
|
+
# ``field()`` uses the field-specifier overload pattern from
|
|
33
|
+
# ``CONTRIBUTING.md`` carve-out 1: typed overloads return ``T`` so
|
|
34
|
+
# call sites like ``email: str = dx.field(description="...")`` type-check,
|
|
35
|
+
# while the implementation returns ``Field``. Pyright in strict mode
|
|
36
|
+
# rejects this inconsistency between the unconstrained-``T`` overload
|
|
37
|
+
# returns and the concrete ``Field`` impl return; no structural fix
|
|
38
|
+
# preserves the surface ergonomics. Confined to this file.
|
|
39
|
+
# pyright: reportInconsistentOverload=false
|
|
37
40
|
from __future__ import annotations
|
|
38
41
|
|
|
39
42
|
from dataclasses import dataclass
|
|
@@ -43,8 +46,11 @@ from typing import (
|
|
|
43
46
|
Annotated,
|
|
44
47
|
Any,
|
|
45
48
|
Final,
|
|
49
|
+
ForwardRef,
|
|
46
50
|
Literal,
|
|
47
51
|
Self,
|
|
52
|
+
TypeVar,
|
|
53
|
+
cast,
|
|
48
54
|
get_args,
|
|
49
55
|
get_origin,
|
|
50
56
|
overload,
|
|
@@ -53,7 +59,7 @@ from typing import (
|
|
|
53
59
|
if TYPE_CHECKING:
|
|
54
60
|
from collections.abc import Callable, Mapping
|
|
55
61
|
|
|
56
|
-
from didactic.types._types import TypeTranslation
|
|
62
|
+
from didactic.types._types import TypeForm, TypeTranslation
|
|
57
63
|
from didactic.types._typing import (
|
|
58
64
|
DefaultOrMissing,
|
|
59
65
|
FieldValue,
|
|
@@ -110,7 +116,6 @@ MISSING: Final[_Missing] = _Missing()
|
|
|
110
116
|
#: ``isinstance(value, MissingType)`` without crossing a private boundary.
|
|
111
117
|
MissingType = _Missing
|
|
112
118
|
|
|
113
|
-
|
|
114
119
|
# ---------------------------------------------------------------------------
|
|
115
120
|
# The Field descriptor class
|
|
116
121
|
# ---------------------------------------------------------------------------
|
|
@@ -235,9 +240,9 @@ def field(
|
|
|
235
240
|
usage_mode: Literal["readwrite", "computed", "materialised"] = ...,
|
|
236
241
|
extras: Mapping[str, Opaque] | None = ...,
|
|
237
242
|
) -> Any: ... # documented escape hatch for required-with-metadata fields
|
|
238
|
-
def field(
|
|
243
|
+
def field(
|
|
239
244
|
*,
|
|
240
|
-
default:
|
|
245
|
+
default: FieldValue | _Missing = MISSING,
|
|
241
246
|
default_factory: Callable[[], FieldValue] | None = None,
|
|
242
247
|
converter: Callable[[FieldValue], FieldValue] | None = None,
|
|
243
248
|
alias: str | None = None,
|
|
@@ -370,7 +375,7 @@ class FieldSpec:
|
|
|
370
375
|
"""
|
|
371
376
|
|
|
372
377
|
name: str
|
|
373
|
-
annotation:
|
|
378
|
+
annotation: TypeForm | TypeVar | ForwardRef
|
|
374
379
|
translation: TypeTranslation
|
|
375
380
|
default: DefaultOrMissing = MISSING
|
|
376
381
|
default_factory: Callable[[], FieldValue] | None = None
|
|
@@ -382,7 +387,9 @@ class FieldSpec:
|
|
|
382
387
|
nominal: bool = False
|
|
383
388
|
usage_mode: Literal["readwrite", "computed", "materialised"] = "readwrite"
|
|
384
389
|
axioms: tuple[str, ...] = ()
|
|
385
|
-
extras: Mapping[str, Opaque] = _dc_field(
|
|
390
|
+
extras: Mapping[str, Opaque] = _dc_field(
|
|
391
|
+
default_factory=lambda: cast("dict[str, Opaque]", {})
|
|
392
|
+
)
|
|
386
393
|
|
|
387
394
|
@property
|
|
388
395
|
def is_required(self) -> bool:
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
# narrower payload type; the runtime accepts the base shape too. The
|
|
3
3
|
# ``Literal[...]`` discriminator extraction goes through annotation
|
|
4
4
|
# walking that pyright can't follow. Tracked in panproto/didactic#1.
|
|
5
|
-
# pyright: reportArgumentType=false, reportIncompatibleMethodOverride=false, reportUnnecessaryIsInstance=false
|
|
6
5
|
"""Tagged (discriminated) unions over [Model][didactic.api.Model] subclasses.
|
|
7
6
|
|
|
8
7
|
Pydantic-shaped discriminated unions: declare a base class with a
|
|
@@ -49,13 +48,15 @@ didactic.models._meta.ModelMeta : the metaclass; unchanged.
|
|
|
49
48
|
from __future__ import annotations
|
|
50
49
|
|
|
51
50
|
import annotationlib
|
|
52
|
-
from typing import TYPE_CHECKING, ClassVar, Literal, Self, get_args, get_origin
|
|
51
|
+
from typing import TYPE_CHECKING, ClassVar, Literal, Self, cast, get_args, get_origin
|
|
53
52
|
|
|
54
53
|
from didactic.fields._validators import ValidationError, ValidationErrorEntry
|
|
55
54
|
from didactic.models._model import Model
|
|
56
55
|
|
|
57
56
|
if TYPE_CHECKING:
|
|
58
|
-
from
|
|
57
|
+
from collections.abc import Mapping
|
|
58
|
+
|
|
59
|
+
from didactic.types._typing import FieldValue, JsonValue, Opaque
|
|
59
60
|
|
|
60
61
|
|
|
61
62
|
class TaggedUnion(Model):
|
|
@@ -173,7 +174,7 @@ class TaggedUnion(Model):
|
|
|
173
174
|
root.__variants__[value] = cls
|
|
174
175
|
|
|
175
176
|
@classmethod
|
|
176
|
-
def model_validate(cls, payload:
|
|
177
|
+
def model_validate(cls, payload: Mapping[str, FieldValue | JsonValue]) -> Self:
|
|
177
178
|
"""Dispatch on the discriminator and validate as the matched variant.
|
|
178
179
|
|
|
179
180
|
Parameters
|
|
@@ -212,7 +213,10 @@ class TaggedUnion(Model):
|
|
|
212
213
|
)
|
|
213
214
|
raise ValidationError(entries=(entry,), model=cls)
|
|
214
215
|
|
|
215
|
-
|
|
216
|
+
# ``payload`` is widened to ``Mapping[str, FieldValue | JsonValue]``
|
|
217
|
+
# to match the base ``model_validate`` signature; the discriminator
|
|
218
|
+
# value is recorded in ``__variants__`` under a ``FieldValue`` key.
|
|
219
|
+
value = cast("FieldValue", payload[disc])
|
|
216
220
|
variant = cls.__variants__.get(value)
|
|
217
221
|
if variant is None:
|
|
218
222
|
entry = ValidationErrorEntry(
|
|
@@ -225,22 +229,24 @@ class TaggedUnion(Model):
|
|
|
225
229
|
)
|
|
226
230
|
raise ValidationError(entries=(entry,), model=cls)
|
|
227
231
|
|
|
228
|
-
|
|
232
|
+
# ``variant`` is a concrete subclass of ``cls``; the return type
|
|
233
|
+
# is bound to ``Self`` of the union root by design (the dispatch
|
|
234
|
+
# contract is "give me a value of *some* variant").
|
|
235
|
+
return cast("Self", variant.model_validate(payload))
|
|
229
236
|
|
|
230
237
|
|
|
231
238
|
def _find_union_root(cls: type[TaggedUnion]) -> type[TaggedUnion] | None:
|
|
232
239
|
"""Walk MRO to find the nearest ancestor with a non-None ``__discriminator__``."""
|
|
233
240
|
for klass in cls.__mro__[1:]:
|
|
234
241
|
if (
|
|
235
|
-
|
|
236
|
-
and issubclass(klass, TaggedUnion)
|
|
242
|
+
issubclass(klass, TaggedUnion)
|
|
237
243
|
and klass.__dict__.get("__discriminator__") is not None
|
|
238
244
|
):
|
|
239
245
|
return klass
|
|
240
246
|
return None
|
|
241
247
|
|
|
242
248
|
|
|
243
|
-
def _literal_values(annotation:
|
|
249
|
+
def _literal_values(annotation: Opaque) -> tuple[FieldValue, ...]:
|
|
244
250
|
"""Extract the values from a ``Literal[...]`` annotation.
|
|
245
251
|
|
|
246
252
|
Returns an empty tuple when the annotation isn't a Literal.
|
|
@@ -22,9 +22,6 @@ didactic.models._meta : the metaclass that records validators on the class.
|
|
|
22
22
|
|
|
23
23
|
# Stamps ``__didactic_validator__`` onto a function; pyright won't
|
|
24
24
|
# let arbitrary attribute assignment on a ``FunctionType``.
|
|
25
|
-
# Tracked in panproto/didactic#1.
|
|
26
|
-
# pyright: reportFunctionMemberAccess=false
|
|
27
|
-
|
|
28
25
|
from __future__ import annotations
|
|
29
26
|
|
|
30
27
|
from dataclasses import dataclass, field
|
|
@@ -150,8 +147,14 @@ def validates(
|
|
|
150
147
|
def decorator(
|
|
151
148
|
fn: Callable[..., FieldValue],
|
|
152
149
|
) -> Callable[..., FieldValue]:
|
|
153
|
-
#
|
|
154
|
-
|
|
150
|
+
# Mark the function so the metaclass can find it. ``setattr``
|
|
151
|
+
# writes through the function-object's ``__dict__`` without
|
|
152
|
+
# tripping pyright's narrowed FunctionType view.
|
|
153
|
+
setattr( # noqa: B010
|
|
154
|
+
fn,
|
|
155
|
+
"__didactic_validator__",
|
|
156
|
+
{"fields": field_names, "mode": mode},
|
|
157
|
+
)
|
|
155
158
|
return fn
|
|
156
159
|
|
|
157
160
|
return decorator
|
|
@@ -42,14 +42,9 @@ didactic.Lens : the schema-fixed lens type.
|
|
|
42
42
|
didactic.Iso : the isomorphism subcase.
|
|
43
43
|
"""
|
|
44
44
|
|
|
45
|
-
# ``ProtolensChain.instantiate(src, tgt)`` accepts the project's
|
|
46
|
-
# ``Schema`` Protocol; pyright sees the panproto stub class instead.
|
|
47
|
-
# Tracked in panproto/didactic#1.
|
|
48
|
-
# pyright: reportArgumentType=false
|
|
49
|
-
|
|
50
45
|
from __future__ import annotations
|
|
51
46
|
|
|
52
|
-
from typing import TYPE_CHECKING
|
|
47
|
+
from typing import TYPE_CHECKING, cast
|
|
53
48
|
|
|
54
49
|
if TYPE_CHECKING:
|
|
55
50
|
import panproto
|
|
@@ -280,7 +275,12 @@ class DependentLens:
|
|
|
280
275
|
panproto.LensError
|
|
281
276
|
If the chain cannot be instantiated against ``schema``.
|
|
282
277
|
"""
|
|
283
|
-
|
|
278
|
+
# ``ProtolensChain.instantiate``'s shipped stub claims
|
|
279
|
+
# ``(src: Schema, tgt: Schema)``; the runtime is
|
|
280
|
+
# ``(schema, protocol)`` with the second arg being a
|
|
281
|
+
# ``panproto.Protocol``. Cast at the boundary so we type-match
|
|
282
|
+
# the stub while passing the runtime's expected value.
|
|
283
|
+
return self._inner.instantiate(schema, cast("panproto.Schema", protocol))
|
|
284
284
|
|
|
285
285
|
# serialisation -------------------------------------------------
|
|
286
286
|
|
|
@@ -51,9 +51,6 @@ didactic.theory._theory : the bridge that will compile lenses to panproto.Lens.
|
|
|
51
51
|
|
|
52
52
|
# ``lens`` doubles as a callable and a namespace (``dx.lens.identity``);
|
|
53
53
|
# the namespace attributes are stamped on the function object.
|
|
54
|
-
# Tracked in panproto/didactic#1.
|
|
55
|
-
# pyright: reportAttributeAccessIssue=false
|
|
56
|
-
|
|
57
54
|
from __future__ import annotations
|
|
58
55
|
|
|
59
56
|
import functools
|
|
@@ -70,7 +67,6 @@ from didactic.types._typing import Opaque
|
|
|
70
67
|
if TYPE_CHECKING:
|
|
71
68
|
from collections.abc import Callable
|
|
72
69
|
|
|
73
|
-
|
|
74
70
|
# ---------------------------------------------------------------------------
|
|
75
71
|
# Mapping (one-way)
|
|
76
72
|
# ---------------------------------------------------------------------------
|
|
@@ -377,11 +373,18 @@ class _LensNamespace:
|
|
|
377
373
|
``dx.lens.Lens``, ``dx.lens.Iso``, ``dx.lens.Mapping``).
|
|
378
374
|
"""
|
|
379
375
|
|
|
376
|
+
identity: Callable[..., Iso[Opaque, Opaque]]
|
|
377
|
+
Lens: type[Lens[Opaque, Opaque, Opaque]]
|
|
378
|
+
Iso: type[Iso[Opaque, Opaque]]
|
|
379
|
+
Mapping: type[Mapping[Opaque, Opaque]]
|
|
380
|
+
|
|
380
381
|
def __init__(self) -> None:
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
self.
|
|
384
|
-
self.
|
|
382
|
+
# ``identity`` is parameterised on ``A``; the namespace exposes the
|
|
383
|
+
# erased ``Opaque, Opaque`` shape since callers re-bind on use.
|
|
384
|
+
self.identity = cast("Callable[..., Iso[Opaque, Opaque]]", identity)
|
|
385
|
+
self.Lens = Lens
|
|
386
|
+
self.Iso = Iso
|
|
387
|
+
self.Mapping = Mapping
|
|
385
388
|
|
|
386
389
|
def __call__[A, B](
|
|
387
390
|
self, source: type[A], target: type[B]
|
|
@@ -434,7 +437,6 @@ class _LensNamespace:
|
|
|
434
437
|
|
|
435
438
|
lens = _LensNamespace()
|
|
436
439
|
|
|
437
|
-
|
|
438
440
|
__all__ = [
|
|
439
441
|
"Iso",
|
|
440
442
|
"Lens",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# ``diff_and_classify`` returns a ``CompatReport`` whose ``.to_dict()``
|
|
2
2
|
# we re-emit as ``JsonObject``; pyright's stub doesn't narrow the
|
|
3
3
|
# panproto-side types tightly enough. Tracked in panproto/didactic#1.
|
|
4
|
-
# pyright: reportReturnType=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportCallIssue=false
|
|
5
4
|
"""Schema diff and breaking-change detection.
|
|
6
5
|
|
|
7
6
|
Two thin functions over panproto's ``diff_schemas`` and
|
|
@@ -22,9 +21,11 @@ panproto.diff_schemas : the runtime call.
|
|
|
22
21
|
|
|
23
22
|
from __future__ import annotations
|
|
24
23
|
|
|
25
|
-
from typing import TYPE_CHECKING
|
|
24
|
+
from typing import TYPE_CHECKING, Protocol, cast
|
|
26
25
|
|
|
27
26
|
if TYPE_CHECKING:
|
|
27
|
+
import panproto
|
|
28
|
+
|
|
28
29
|
from didactic.models._model import Model
|
|
29
30
|
from didactic.types._typing import JsonObject
|
|
30
31
|
|
|
@@ -65,7 +66,7 @@ def diff(old: type[Model], new: type[Model]) -> JsonObject:
|
|
|
65
66
|
old_schema = schema_from_model(old)
|
|
66
67
|
new_schema = schema_from_model(new)
|
|
67
68
|
schema_diff = panproto.diff_schemas(old_schema, new_schema)
|
|
68
|
-
return schema_diff.to_dict()
|
|
69
|
+
return cast("JsonObject", schema_diff.to_dict())
|
|
69
70
|
|
|
70
71
|
|
|
71
72
|
def classify_change(old: type[Model], new: type[Model]) -> JsonObject:
|
|
@@ -111,7 +112,24 @@ def classify_change(old: type[Model], new: type[Model]) -> JsonObject:
|
|
|
111
112
|
schema_theory=build_theory(new),
|
|
112
113
|
obj_kinds=["object"],
|
|
113
114
|
)
|
|
114
|
-
|
|
115
|
+
|
|
116
|
+
# ``panproto.diff_and_classify`` accepts a third positional ``protocol``
|
|
117
|
+
# argument at runtime; the upstream stub still lists only two. Cast
|
|
118
|
+
# to a Protocol that exposes the runtime arity and return shape.
|
|
119
|
+
class _CompatReportLike(Protocol):
|
|
120
|
+
def to_dict(self) -> JsonObject: ...
|
|
121
|
+
|
|
122
|
+
class _DiffAndClassifyLike(Protocol):
|
|
123
|
+
def __call__(
|
|
124
|
+
self,
|
|
125
|
+
old: panproto.Schema,
|
|
126
|
+
new: panproto.Schema,
|
|
127
|
+
protocol: panproto.Protocol,
|
|
128
|
+
/,
|
|
129
|
+
) -> _CompatReportLike: ...
|
|
130
|
+
|
|
131
|
+
diff_and_classify = cast("_DiffAndClassifyLike", panproto.diff_and_classify)
|
|
132
|
+
compat = diff_and_classify(old_schema, new_schema, protocol)
|
|
115
133
|
return compat.to_dict()
|
|
116
134
|
|
|
117
135
|
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
# back into ``JsonObject`` (``dict[str, JsonValue]``); the dict
|
|
3
3
|
# invariance bites. ``_default`` narrowing on tuple inputs is
|
|
4
4
|
# pyright-flagged as redundant. Tracked in panproto/didactic#1.
|
|
5
|
-
# pyright: reportReturnType=false, reportArgumentType=false, reportUnnecessaryIsInstance=false
|
|
6
5
|
"""Stable fingerprints for didactic Theory specs.
|
|
7
6
|
|
|
8
7
|
didactic stores migration registry entries by a fingerprint that is
|
|
@@ -45,12 +44,11 @@ from __future__ import annotations
|
|
|
45
44
|
|
|
46
45
|
import hashlib
|
|
47
46
|
import json
|
|
48
|
-
from typing import TYPE_CHECKING
|
|
47
|
+
from typing import TYPE_CHECKING, cast
|
|
49
48
|
|
|
50
49
|
if TYPE_CHECKING:
|
|
51
50
|
from didactic.theory._theory import TheorySpec
|
|
52
|
-
from didactic.types._typing import JsonObject, JsonValue
|
|
53
|
-
|
|
51
|
+
from didactic.types._typing import JsonObject, JsonValue, Opaque
|
|
54
52
|
|
|
55
53
|
# canonical placeholder for the model's own display name; chosen to be
|
|
56
54
|
# unambiguous and unlikely to clash with any user-supplied identifier
|
|
@@ -89,7 +87,7 @@ def canonical_json_bytes(spec: JsonValue) -> bytes:
|
|
|
89
87
|
).encode("utf-8")
|
|
90
88
|
|
|
91
89
|
|
|
92
|
-
def _default(obj:
|
|
90
|
+
def _default(obj: Opaque) -> JsonValue:
|
|
93
91
|
"""Fallback encoder for tuples (json default would already accept them).
|
|
94
92
|
|
|
95
93
|
The callback runs on every non-JSON-native value ``json.dumps``
|
|
@@ -99,7 +97,7 @@ def _default(obj: tuple[JsonValue, ...]) -> JsonValue:
|
|
|
99
97
|
raises so the spec author sees it immediately.
|
|
100
98
|
"""
|
|
101
99
|
if isinstance(obj, tuple):
|
|
102
|
-
return list(obj)
|
|
100
|
+
return list(cast("tuple[JsonValue, ...]", obj))
|
|
103
101
|
msg = f"non-JSON-encodable value in didactic spec: {type(obj).__name__}"
|
|
104
102
|
raise TypeError(msg)
|
|
105
103
|
|
|
@@ -163,7 +161,11 @@ def structural_spec(spec: JsonObject) -> JsonObject:
|
|
|
163
161
|
if not isinstance(name, str):
|
|
164
162
|
msg = "structural_spec(): spec['name'] must be a string"
|
|
165
163
|
raise TypeError(msg)
|
|
166
|
-
|
|
164
|
+
rewritten = _replace_model_name(spec, name)
|
|
165
|
+
if not isinstance(rewritten, dict):
|
|
166
|
+
msg = "structural_spec(): top-level rewrite must remain a dict"
|
|
167
|
+
raise TypeError(msg)
|
|
168
|
+
return rewritten
|
|
167
169
|
|
|
168
170
|
|
|
169
171
|
def _replace_model_name(value: JsonValue, name: str) -> JsonValue:
|
|
@@ -209,7 +211,7 @@ def structural_fingerprint(spec: TheorySpec) -> str:
|
|
|
209
211
|
structurally-identical Models should share one entry regardless of
|
|
210
212
|
their class names.
|
|
211
213
|
"""
|
|
212
|
-
return fingerprint(structural_spec(spec))
|
|
214
|
+
return fingerprint(structural_spec(cast("JsonObject", spec)))
|
|
213
215
|
|
|
214
216
|
|
|
215
217
|
__all__ = [
|
|
@@ -27,15 +27,10 @@ didactic.register_migration : the manual-authoring counterpart.
|
|
|
27
27
|
panproto.auto_generate_lens : the runtime call.
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
|
-
# panproto's ``auto_generate_lens`` returns ``tuple[..., dict[str, object]]``
|
|
31
|
-
# and we re-emit as ``tuple[JsonObject, ...]``; dict invariance bites.
|
|
32
|
-
# Tracked in panproto/didactic#1.
|
|
33
|
-
# pyright: reportArgumentType=false
|
|
34
|
-
|
|
35
30
|
from __future__ import annotations
|
|
36
31
|
|
|
37
32
|
from dataclasses import dataclass
|
|
38
|
-
from typing import TYPE_CHECKING
|
|
33
|
+
from typing import TYPE_CHECKING, cast
|
|
39
34
|
|
|
40
35
|
if TYPE_CHECKING:
|
|
41
36
|
from didactic.models._model import Model
|
|
@@ -125,10 +120,12 @@ def synthesise_migration(
|
|
|
125
120
|
)
|
|
126
121
|
|
|
127
122
|
lens, score, proposals = result
|
|
123
|
+
# ``proposals`` comes back from panproto as ``list[dict[str, object]]``;
|
|
124
|
+
# narrow at the boundary to didactic's stricter ``JsonObject`` shape.
|
|
128
125
|
return SynthesisResult(
|
|
129
126
|
lens=lens,
|
|
130
127
|
score=float(score),
|
|
131
|
-
proposals=tuple(proposals),
|
|
128
|
+
proposals=cast("tuple[JsonObject, ...]", tuple(proposals)),
|
|
132
129
|
)
|
|
133
130
|
|
|
134
131
|
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
# accepts string forward refs and TypeVar instances), but pyright
|
|
6
6
|
# can't follow the case split through the dataclass_transform
|
|
7
7
|
# layer. Tracked in panproto/didactic#1.
|
|
8
|
-
# pyright: reportArgumentType=false, reportReturnType=false, reportCallIssue=false
|
|
9
8
|
"""The ``ModelMeta`` metaclass: class-as-Theory derivation.
|
|
10
9
|
|
|
11
10
|
``ModelMeta`` runs at class-creation time. It reads each annotated field,
|
|
@@ -33,7 +32,7 @@ from __future__ import annotations
|
|
|
33
32
|
import annotationlib
|
|
34
33
|
import contextlib
|
|
35
34
|
import sys
|
|
36
|
-
from typing import TYPE_CHECKING, ForwardRef, TypeVar, dataclass_transform
|
|
35
|
+
from typing import TYPE_CHECKING, ForwardRef, TypeVar, cast, dataclass_transform
|
|
37
36
|
|
|
38
37
|
from didactic.axioms._axioms import collect_class_axioms
|
|
39
38
|
from didactic.fields._computed import computed_field_names
|
|
@@ -44,10 +43,12 @@ from didactic.fields._fields import (
|
|
|
44
43
|
field,
|
|
45
44
|
read_annotated_metadata,
|
|
46
45
|
)
|
|
47
|
-
from didactic.models._config import DEFAULT_CONFIG, ModelConfig
|
|
48
|
-
from didactic.types._types import classify
|
|
46
|
+
from didactic.models._config import DEFAULT_CONFIG, ExtraPolicy, ModelConfig
|
|
47
|
+
from didactic.types._types import TypeForm, classify
|
|
49
48
|
|
|
50
49
|
if TYPE_CHECKING:
|
|
50
|
+
from collections.abc import Mapping
|
|
51
|
+
|
|
51
52
|
import panproto
|
|
52
53
|
|
|
53
54
|
from didactic.axioms._axioms import Axiom
|
|
@@ -222,8 +223,14 @@ def _build_field_spec(
|
|
|
222
223
|
default=raw_default if not isinstance(raw_default, Field) else MISSING,
|
|
223
224
|
usage_mode="readwrite",
|
|
224
225
|
)
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
# Above branches return for ``TypeVar`` / ``ForwardRef`` cases; the
|
|
227
|
+
# remaining annotation is a real ``TypeForm`` (a class, ``UnionType``,
|
|
228
|
+
# ``TypeAliasType``, ``GenericAlias``, or an ``Annotated`` form). Cast
|
|
229
|
+
# to drop the residual ``TypeVar | ForwardRef`` arms from pyright's
|
|
230
|
+
# view.
|
|
231
|
+
type_form = cast("TypeForm", annotation)
|
|
232
|
+
translation = classify(type_form)
|
|
233
|
+
annotated_meta = read_annotated_metadata(type_form)
|
|
227
234
|
|
|
228
235
|
# default & metadata sourced from a Field instance, if present
|
|
229
236
|
if isinstance(raw_default, Field):
|
|
@@ -307,11 +314,13 @@ class ModelMeta(type):
|
|
|
307
314
|
[build_theory][didactic.theory._theory.build_theory]; cached
|
|
308
315
|
thereafter. Subsequent accesses return the same object.
|
|
309
316
|
"""
|
|
310
|
-
|
|
317
|
+
cached = cls.__dict__.get("__theory_cache__")
|
|
318
|
+
if cached is None:
|
|
311
319
|
from didactic.theory._theory import build_theory # noqa: PLC0415
|
|
312
320
|
|
|
313
|
-
|
|
314
|
-
|
|
321
|
+
cached = build_theory(cls)
|
|
322
|
+
cls.__theory_cache__ = cached
|
|
323
|
+
return cached
|
|
315
324
|
|
|
316
325
|
# config keys we accept on the class header.
|
|
317
326
|
# e.g. ``class Foo(dx.Model, extra="forbid"): ...``
|
|
@@ -368,7 +377,7 @@ class ModelMeta(type):
|
|
|
368
377
|
|
|
369
378
|
@staticmethod
|
|
370
379
|
def _resolve_config(
|
|
371
|
-
target: type, header_overrides:
|
|
380
|
+
target: type, header_overrides: Mapping[str, Opaque]
|
|
372
381
|
) -> ModelConfig:
|
|
373
382
|
"""Pick the ModelConfig for a class.
|
|
374
383
|
|
|
@@ -413,16 +422,22 @@ class ModelMeta(type):
|
|
|
413
422
|
if not header_overrides:
|
|
414
423
|
return explicit
|
|
415
424
|
|
|
416
|
-
# apply overrides via dataclasses.replace-shaped construction
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
425
|
+
# apply overrides via dataclasses.replace-shaped construction. The
|
|
426
|
+
# ``header_overrides`` mapping is keyed on a known small set
|
|
427
|
+
# (``_CONFIG_HEADER_KEYS``); each value is taken at face value and
|
|
428
|
+
# validated by ``ModelConfig.__post_init__`` rather than statically.
|
|
429
|
+
def _pick(key: str, fallback: Opaque) -> Opaque:
|
|
430
|
+
return header_overrides.get(key, fallback)
|
|
431
|
+
|
|
432
|
+
return ModelConfig(
|
|
433
|
+
extra=cast("ExtraPolicy", _pick("extra", explicit.extra)),
|
|
434
|
+
strict=cast("bool", _pick("strict", explicit.strict)),
|
|
435
|
+
populate_by_name=cast(
|
|
436
|
+
"bool", _pick("populate_by_name", explicit.populate_by_name)
|
|
437
|
+
),
|
|
438
|
+
title=cast("str | None", _pick("title", explicit.title)),
|
|
439
|
+
description=cast("str | None", _pick("description", explicit.description)),
|
|
440
|
+
)
|
|
426
441
|
|
|
427
442
|
@staticmethod
|
|
428
443
|
def collect_field_specs(target: type) -> dict[str, FieldSpec]:
|
|
@@ -23,9 +23,6 @@ didactic.models._storage : the pluggable storage backend.
|
|
|
23
23
|
|
|
24
24
|
# ``__init__(**kwargs: FieldValue | JsonValue)`` accepts JSON-shape
|
|
25
25
|
# dicts but pyright doesn't carry the union through ``_encode_field``.
|
|
26
|
-
# Tracked in panproto/didactic#1.
|
|
27
|
-
# pyright: reportArgumentType=false
|
|
28
|
-
|
|
29
26
|
from __future__ import annotations
|
|
30
27
|
|
|
31
28
|
import json
|
|
@@ -136,7 +133,7 @@ class Model(metaclass=ModelMeta):
|
|
|
136
133
|
value = default
|
|
137
134
|
|
|
138
135
|
try:
|
|
139
|
-
encoded_value = _encode_field(spec, value)
|
|
136
|
+
encoded_value = _encode_field(spec, cast("FieldValue", value))
|
|
140
137
|
except (TypeError, ValueError) as exc:
|
|
141
138
|
errors.append(
|
|
142
139
|
ValidationErrorEntry(
|
|
@@ -27,12 +27,9 @@ Examples
|
|
|
27
27
|
|
|
28
28
|
# ``TypeAdapter`` round-trips a generic ``T`` through the field
|
|
29
29
|
# translation layer; pyright doesn't bind ``T`` to ``FieldValue``.
|
|
30
|
-
# Tracked in panproto/didactic#1.
|
|
31
|
-
# pyright: reportArgumentType=false
|
|
32
|
-
|
|
33
30
|
from __future__ import annotations
|
|
34
31
|
|
|
35
|
-
from typing import TYPE_CHECKING
|
|
32
|
+
from typing import TYPE_CHECKING, cast
|
|
36
33
|
|
|
37
34
|
from didactic.models._model import Model
|
|
38
35
|
|
|
@@ -109,13 +106,16 @@ class TypeAdapter[T]:
|
|
|
109
106
|
If the value cannot be coerced to the target type.
|
|
110
107
|
"""
|
|
111
108
|
encoded = self._translation.encode(value)
|
|
112
|
-
return self._translation.decode(encoded)
|
|
109
|
+
return cast("T", self._translation.decode(encoded))
|
|
113
110
|
|
|
114
111
|
def dump_json(self, value: T) -> str:
|
|
115
112
|
"""Encode ``value`` to a JSON string."""
|
|
116
113
|
import json # noqa: PLC0415
|
|
117
114
|
|
|
118
|
-
|
|
115
|
+
# ``T`` is unconstrained at the API surface; the translation
|
|
116
|
+
# accepts any ``FieldValue`` shape at runtime, so we cast at the
|
|
117
|
+
# boundary rather than constrain ``T``.
|
|
118
|
+
return json.dumps(self._translation.encode(cast("FieldValue", value)))
|
|
119
119
|
|
|
120
120
|
|
|
121
121
|
__all__ = [
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
# panproto 0.43's ``_native.pyi`` stub for ``create_theory``/``colimit_theories``
|
|
2
|
-
# disagrees with the runtime: ``create_theory`` declares ``dict[str, object]``
|
|
3
|
-
# (rejecting our ``TheorySpec`` TypedDict, which IS a dict at runtime), and
|
|
4
|
-
# ``colimit_theories`` is stubbed as ``(Sequence[Theory], /)`` while the
|
|
5
|
-
# runtime is ``(t1, t2, shared)``. Tracked in panproto/didactic#1; will be
|
|
6
|
-
# removed once panproto ships corrected stubs.
|
|
7
|
-
# pyright: reportArgumentType=false, reportCallIssue=false, reportUnknownVariableType=false
|
|
8
1
|
"""Bridge between didactic FieldSpecs and ``panproto.Theory``.
|
|
9
2
|
|
|
10
3
|
This module is the seam where the didactic-side metaclass output
|
|
@@ -35,8 +28,6 @@ didactic.fields._fields.FieldSpec : the per-field record consumed here.
|
|
|
35
28
|
|
|
36
29
|
# ``_class_axiom_eq`` is a stub for the eqs-emission path that's
|
|
37
30
|
# registered later; the local symbol is referenced by its qualname.
|
|
38
|
-
# Tracked in panproto/didactic#1.
|
|
39
|
-
# pyright: reportUnusedFunction=false
|
|
40
31
|
|
|
41
32
|
from __future__ import annotations
|
|
42
33
|
|
|
@@ -45,7 +36,6 @@ from typing import TYPE_CHECKING, TypedDict, cast
|
|
|
45
36
|
if TYPE_CHECKING:
|
|
46
37
|
import panproto
|
|
47
38
|
|
|
48
|
-
from didactic.axioms._axioms import Axiom
|
|
49
39
|
from didactic.fields._fields import FieldSpec
|
|
50
40
|
from didactic.models._model import Model
|
|
51
41
|
from didactic.types._typing import JsonValue
|
|
@@ -257,36 +247,6 @@ def _edge_accessor(
|
|
|
257
247
|
}
|
|
258
248
|
|
|
259
249
|
|
|
260
|
-
def _class_axiom_eq(ax: Axiom, schema_kind: str, idx: int) -> dict[str, JsonValue]:
|
|
261
|
-
"""Build an Equation dict for a class-level axiom.
|
|
262
|
-
|
|
263
|
-
Parameters
|
|
264
|
-
----------
|
|
265
|
-
ax
|
|
266
|
-
An [Axiom][didactic.axioms._axioms.Axiom] instance.
|
|
267
|
-
schema_kind
|
|
268
|
-
The model's primary sort name; used as a default name prefix.
|
|
269
|
-
idx
|
|
270
|
-
Zero-based position within the class's ``__axioms__`` list.
|
|
271
|
-
|
|
272
|
-
Returns
|
|
273
|
-
-------
|
|
274
|
-
dict
|
|
275
|
-
An equation dict with name + the raw expression text. The
|
|
276
|
-
panproto-side parser will translate the expression once the
|
|
277
|
-
runtime hookup lands; for now we carry the surface form
|
|
278
|
-
verbatim under an ``expr`` key.
|
|
279
|
-
"""
|
|
280
|
-
name = ax.name or f"{schema_kind}_axiom_{idx}"
|
|
281
|
-
body: dict[str, JsonValue] = {
|
|
282
|
-
"name": name,
|
|
283
|
-
"expr": ax.expr,
|
|
284
|
-
}
|
|
285
|
-
if ax.message:
|
|
286
|
-
body["message"] = ax.message
|
|
287
|
-
return body
|
|
288
|
-
|
|
289
|
-
|
|
290
250
|
def _embed_accessor(
|
|
291
251
|
field_name: str, parent_sort: str, target_sort: str
|
|
292
252
|
) -> dict[str, JsonValue]:
|
|
@@ -472,24 +432,23 @@ def _build_colimit_theory(cls: type, parents: list[type]) -> panproto.Theory:
|
|
|
472
432
|
"""
|
|
473
433
|
import panproto # noqa: PLC0415
|
|
474
434
|
|
|
475
|
-
|
|
476
|
-
colimit_theories = panproto.colimit_theories
|
|
477
|
-
|
|
478
|
-
accumulator = create_theory(build_theory_spec(parents[0]))
|
|
435
|
+
accumulator = panproto.create_theory(build_theory_spec(parents[0]))
|
|
479
436
|
accumulator_cls: type = parents[0]
|
|
480
437
|
|
|
481
438
|
for parent in parents[1:]:
|
|
482
439
|
ancestor = _lowest_common_model_ancestor([accumulator_cls, parent])
|
|
483
|
-
ancestor_theory = create_theory(build_theory_spec(ancestor))
|
|
484
|
-
next_theory = create_theory(build_theory_spec(parent))
|
|
485
|
-
accumulator = colimit_theories(
|
|
440
|
+
ancestor_theory = panproto.create_theory(build_theory_spec(ancestor))
|
|
441
|
+
next_theory = panproto.create_theory(build_theory_spec(parent))
|
|
442
|
+
accumulator = panproto.colimit_theories(
|
|
443
|
+
accumulator, next_theory, ancestor_theory
|
|
444
|
+
)
|
|
486
445
|
accumulator_cls = parent
|
|
487
446
|
|
|
488
447
|
# finally fold in any cls-only fields by colimiting against the
|
|
489
448
|
# immediate spec of cls; the shared ancestor is the accumulated
|
|
490
449
|
# theory we just built
|
|
491
|
-
cls_theory = create_theory(build_theory_spec(cls))
|
|
492
|
-
return colimit_theories(accumulator, cls_theory, accumulator)
|
|
450
|
+
cls_theory = panproto.create_theory(build_theory_spec(cls))
|
|
451
|
+
return panproto.colimit_theories(accumulator, cls_theory, accumulator)
|
|
493
452
|
|
|
494
453
|
|
|
495
454
|
__all__ = [
|
|
@@ -28,9 +28,6 @@ didactic.models._meta : the metaclass that orchestrates field translation.
|
|
|
28
28
|
|
|
29
29
|
# An ``items()`` over an ``Annotated``-stripped value whose type
|
|
30
30
|
# narrows to the wider ``object`` carve-out branch.
|
|
31
|
-
# Tracked in panproto/didactic#1.
|
|
32
|
-
# pyright: reportUnknownArgumentType=false
|
|
33
|
-
|
|
34
31
|
from __future__ import annotations
|
|
35
32
|
|
|
36
33
|
import json
|
|
@@ -38,7 +35,7 @@ from dataclasses import dataclass, field
|
|
|
38
35
|
from datetime import date, datetime, time
|
|
39
36
|
from decimal import Decimal
|
|
40
37
|
from functools import reduce
|
|
41
|
-
from types import EllipsisType, NoneType, UnionType
|
|
38
|
+
from types import EllipsisType, GenericAlias, NoneType, UnionType
|
|
42
39
|
from typing import (
|
|
43
40
|
TYPE_CHECKING,
|
|
44
41
|
Annotated,
|
|
@@ -64,13 +61,13 @@ type SpecRecord = dict[str, "JsonValue"]
|
|
|
64
61
|
|
|
65
62
|
# The widest annotation form ``classify`` accepts. Includes the nominal
|
|
66
63
|
# ``type`` (covering bare classes and pyright's special-cased parameterised
|
|
67
|
-
# generics like ``tuple[int, ...]`` and ``dict[str, int]``)
|
|
64
|
+
# generics like ``tuple[int, ...]`` and ``dict[str, int]``), PEP 604
|
|
68
65
|
# ``UnionType`` (``int | None``), which pyright does *not* narrow to
|
|
69
|
-
# ``type
|
|
70
|
-
# ``
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
TypeForm = type | UnionType
|
|
66
|
+
# ``type``, and PEP 695 ``TypeAliasType`` instances. ``typing.Annotated``
|
|
67
|
+
# values and ``typing.Union[...]`` are also accepted at runtime; callers
|
|
68
|
+
# that pass them rely on pyright's legacy structural acceptance of those
|
|
69
|
+
# forms or use ``cast`` at the boundary.
|
|
70
|
+
TypeForm = type | UnionType | TypeAliasType | GenericAlias
|
|
74
71
|
|
|
75
72
|
# ---------------------------------------------------------------------------
|
|
76
73
|
# Public types
|
|
@@ -983,8 +980,9 @@ def _alias_sum_translation(alias: TypeAliasType) -> TypeTranslation:
|
|
|
983
980
|
}
|
|
984
981
|
return {f"{alias_name}_dict": inner_obj}
|
|
985
982
|
msg = (
|
|
986
|
-
f"value of type {type(value).__name__} does not
|
|
987
|
-
f"alias {alias_name!r}; expected one of
|
|
983
|
+
f"value of type {type(cast('Opaque', value)).__name__} does not "
|
|
984
|
+
f"match any arm of alias {alias_name!r}; expected one of "
|
|
985
|
+
f"{sorted(table)}."
|
|
988
986
|
)
|
|
989
987
|
raise TypeError(msg)
|
|
990
988
|
|
|
@@ -1289,6 +1287,13 @@ def _expand_type_alias(typ: TypeForm) -> TypeForm:
|
|
|
1289
1287
|
return cast("TypeForm", substitution.get(value, value))
|
|
1290
1288
|
|
|
1291
1289
|
|
|
1290
|
+
# Public re-export under a non-underscored name, intended for tests and
|
|
1291
|
+
# tooling that need to introspect a PEP 695 alias substitution. The
|
|
1292
|
+
# underscored ``_expand_type_alias`` name is retained for backwards
|
|
1293
|
+
# compatibility within this module.
|
|
1294
|
+
expand_type_alias = _expand_type_alias
|
|
1295
|
+
|
|
1296
|
+
|
|
1292
1297
|
def unwrap_annotated(typ: TypeForm) -> tuple[TypeForm, tuple[Opaque, ...]]:
|
|
1293
1298
|
"""Split ``Annotated[T, x, y, z]`` into ``(T, (x, y, z))``.
|
|
1294
1299
|
|
|
@@ -1782,7 +1787,7 @@ def _embed_translation(base: TypeForm, metadata: tuple[Opaque, ...]) -> TypeTran
|
|
|
1782
1787
|
f"got {type(items).__name__}"
|
|
1783
1788
|
)
|
|
1784
1789
|
raise TypeError(msg)
|
|
1785
|
-
return target_cls.from_storage_dict(items)
|
|
1790
|
+
return target_cls.from_storage_dict(cast("dict[str, str]", items))
|
|
1786
1791
|
|
|
1787
1792
|
def from_json(v: JsonValue) -> FieldValue:
|
|
1788
1793
|
# the JSON form is the model_dump dict (decoded values), not
|
|
@@ -19,9 +19,6 @@ didactic.types._types : the translation layer that consumes these aliases.
|
|
|
19
19
|
|
|
20
20
|
# References ``_Missing`` (the sentinel singleton class) by name in
|
|
21
21
|
# the ``DefaultOrMissing`` alias; the ``_`` is conventional.
|
|
22
|
-
# Tracked in panproto/didactic#1.
|
|
23
|
-
# pyright: reportPrivateUsage=false
|
|
24
|
-
|
|
25
22
|
from __future__ import annotations
|
|
26
23
|
|
|
27
24
|
from datetime import date, datetime, time
|
|
@@ -30,10 +27,9 @@ from typing import TYPE_CHECKING, ForwardRef, Protocol, runtime_checkable
|
|
|
30
27
|
from uuid import UUID
|
|
31
28
|
|
|
32
29
|
if TYPE_CHECKING:
|
|
33
|
-
from didactic.fields._fields import
|
|
30
|
+
from didactic.fields._fields import MissingType
|
|
34
31
|
from didactic.models._model import Model
|
|
35
32
|
|
|
36
|
-
|
|
37
33
|
# ---------------------------------------------------------------------------
|
|
38
34
|
# Structural "anything" Protocol
|
|
39
35
|
# ---------------------------------------------------------------------------
|
|
@@ -70,7 +66,6 @@ type JsonValue = (
|
|
|
70
66
|
#: Convenience alias for an object-shaped JsonValue (``dict[str, JsonValue]``).
|
|
71
67
|
type JsonObject = dict[str, JsonValue]
|
|
72
68
|
|
|
73
|
-
|
|
74
69
|
# ---------------------------------------------------------------------------
|
|
75
70
|
# Field-space values
|
|
76
71
|
# ---------------------------------------------------------------------------
|
|
@@ -95,7 +90,6 @@ type FieldValue = (
|
|
|
95
90
|
| Model
|
|
96
91
|
)
|
|
97
92
|
|
|
98
|
-
|
|
99
93
|
# ---------------------------------------------------------------------------
|
|
100
94
|
# Encoded / storage values
|
|
101
95
|
# ---------------------------------------------------------------------------
|
|
@@ -105,7 +99,6 @@ type FieldValue = (
|
|
|
105
99
|
#: where "this is the encoded representation, not arbitrary text" matters.
|
|
106
100
|
type Encoded = str
|
|
107
101
|
|
|
108
|
-
|
|
109
102
|
# ---------------------------------------------------------------------------
|
|
110
103
|
# Class / forward-reference targets
|
|
111
104
|
# ---------------------------------------------------------------------------
|
|
@@ -115,15 +108,13 @@ type Encoded = str
|
|
|
115
108
|
#: ``typing.ForwardRef`` proxy.
|
|
116
109
|
type ClassTarget = type | str | ForwardRef
|
|
117
110
|
|
|
118
|
-
|
|
119
111
|
# ---------------------------------------------------------------------------
|
|
120
112
|
# Field default sentinel
|
|
121
113
|
# ---------------------------------------------------------------------------
|
|
122
114
|
|
|
123
115
|
#: A field default may be either a real value or the
|
|
124
116
|
#: [MISSING][didactic.fields._fields.MISSING] sentinel.
|
|
125
|
-
type DefaultOrMissing = FieldValue |
|
|
126
|
-
|
|
117
|
+
type DefaultOrMissing = FieldValue | MissingType
|
|
127
118
|
|
|
128
119
|
__all__ = [
|
|
129
120
|
"ClassTarget",
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
# type; pyright wants generics to bind multiple sites. The runtime
|
|
3
3
|
# uses ``A`` for the call-site type binding documentation. Tracked
|
|
4
4
|
# in panproto/didactic#1.
|
|
5
|
-
# pyright: reportInvalidTypeVarUse=false
|
|
6
5
|
"""In-memory Backref resolution.
|
|
7
6
|
|
|
8
7
|
[Backref][didactic.api.Backref] is the inverse of [Ref][didactic.api.Ref]: if
|
|
@@ -37,7 +36,7 @@ didactic.Repository : the eventual Repository-backed resolution path.
|
|
|
37
36
|
from __future__ import annotations
|
|
38
37
|
|
|
39
38
|
from collections import defaultdict
|
|
40
|
-
from typing import TYPE_CHECKING
|
|
39
|
+
from typing import TYPE_CHECKING, cast
|
|
41
40
|
|
|
42
41
|
if TYPE_CHECKING:
|
|
43
42
|
from collections.abc import Iterable
|
|
@@ -45,8 +44,8 @@ if TYPE_CHECKING:
|
|
|
45
44
|
from didactic.models._model import Model
|
|
46
45
|
|
|
47
46
|
|
|
48
|
-
def resolve_backrefs[
|
|
49
|
-
target:
|
|
47
|
+
def resolve_backrefs[B: Model](
|
|
48
|
+
target: Model,
|
|
50
49
|
candidates: Iterable[B],
|
|
51
50
|
*,
|
|
52
51
|
via: str,
|
|
@@ -178,11 +177,13 @@ class ModelPool:
|
|
|
178
177
|
instances are stored under their own concrete class, not
|
|
179
178
|
under ``cls``). Order is registration order.
|
|
180
179
|
"""
|
|
181
|
-
|
|
180
|
+
# ``_by_class`` is keyed on ``type[Model]``; ``cls`` narrows
|
|
181
|
+
# the value type to ``list[M]`` only at the call site.
|
|
182
|
+
return cast("list[M]", list(self._by_class.get(cls, [])))
|
|
182
183
|
|
|
183
|
-
def backrefs[
|
|
184
|
+
def backrefs[B: Model](
|
|
184
185
|
self,
|
|
185
|
-
target:
|
|
186
|
+
target: Model,
|
|
186
187
|
candidate_cls: type[B],
|
|
187
188
|
*,
|
|
188
189
|
via: str,
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
# Wraps panproto's ``Repository`` whose method signatures
|
|
2
|
-
# (``log() -> list[dict[str, object]]``, ``resolve_ref() -> str | None``,
|
|
3
|
-
# ``add(target: Schema)``) don't line up exactly with didactic's
|
|
4
|
-
# narrower public surface. The runtime contract is honoured; the
|
|
5
|
-
# stub-side narrowing is deferred. Tracked in panproto/didactic#1.
|
|
6
|
-
# pyright: reportReturnType=false, reportArgumentType=false
|
|
7
1
|
"""Filesystem-backed VCS for panproto schemas.
|
|
8
2
|
|
|
9
3
|
Wraps ``panproto.Repository`` with a didactic-shaped public surface.
|
|
@@ -36,7 +30,7 @@ panproto.Repository : the wrapped runtime type.
|
|
|
36
30
|
|
|
37
31
|
from __future__ import annotations
|
|
38
32
|
|
|
39
|
-
from typing import TYPE_CHECKING
|
|
33
|
+
from typing import TYPE_CHECKING, cast
|
|
40
34
|
|
|
41
35
|
if TYPE_CHECKING:
|
|
42
36
|
from os import PathLike
|
|
@@ -199,7 +193,7 @@ class Repository:
|
|
|
199
193
|
The exact shape is panproto-defined; callers that depend
|
|
200
194
|
on specific keys should consult panproto's documentation.
|
|
201
195
|
"""
|
|
202
|
-
return list(self._inner.log())
|
|
196
|
+
return cast("list[JsonObject]", list(self._inner.log()))
|
|
203
197
|
|
|
204
198
|
def resolve_ref(self, ref: str) -> str:
|
|
205
199
|
"""Resolve a ref expression to a commit id.
|
|
@@ -217,9 +211,15 @@ class Repository:
|
|
|
217
211
|
Raises
|
|
218
212
|
------
|
|
219
213
|
panproto.VcsError
|
|
220
|
-
If ``ref`` does not resolve to a commit
|
|
214
|
+
If ``ref`` does not resolve to a commit, or returns ``None``.
|
|
221
215
|
"""
|
|
222
|
-
|
|
216
|
+
result = self._inner.resolve_ref(ref)
|
|
217
|
+
if result is None:
|
|
218
|
+
import panproto # noqa: PLC0415
|
|
219
|
+
|
|
220
|
+
msg = f"ref {ref!r} did not resolve to a commit"
|
|
221
|
+
raise panproto.VcsError(msg)
|
|
222
|
+
return result
|
|
223
223
|
|
|
224
224
|
# mutation ------------------------------------------------------
|
|
225
225
|
|
|
@@ -244,7 +244,9 @@ class Repository:
|
|
|
244
244
|
if isinstance(target, type) and issubclass(target, Model):
|
|
245
245
|
self._inner.add(schema_from_model(target))
|
|
246
246
|
return
|
|
247
|
-
|
|
247
|
+
# already-narrowed by the isinstance branch above; only the
|
|
248
|
+
# ``Schema`` arm of the union is left.
|
|
249
|
+
self._inner.add(cast("panproto.Schema", target))
|
|
248
250
|
|
|
249
251
|
def commit(
|
|
250
252
|
self,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|