didactic 0.3.0__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.0 → didactic-0.3.2}/PKG-INFO +2 -2
- {didactic-0.3.0 → didactic-0.3.2}/pyproject.toml +2 -2
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/_self_describing.py +14 -7
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/api.py +1 -2
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/axioms/_axiom_enforcement.py +0 -1
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/cli/_cli.py +9 -4
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/codegen/_json_schema.py +8 -3
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/codegen/_write.py +2 -8
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/codegen/source.py +5 -7
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/fields/_computed.py +0 -1
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/fields/_derived.py +12 -6
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/fields/_fields.py +18 -11
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/fields/_unions.py +15 -9
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/fields/_validators.py +8 -5
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/lenses/_dependent_lens.py +7 -7
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/lenses/_lens.py +11 -9
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/lenses/_testing.py +2 -2
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/migrations/_diff.py +22 -4
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/migrations/_fingerprint.py +10 -8
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/migrations/_synthesis.py +4 -7
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/models/_config.py +0 -1
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/models/_meta.py +35 -20
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/models/_model.py +8 -13
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/models/_root.py +6 -6
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/theory/_theory.py +8 -49
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/types/_types.py +65 -31
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/types/_typing.py +2 -11
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/vcs/_backref.py +8 -7
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/vcs/_repo.py +13 -11
- {didactic-0.3.0 → didactic-0.3.2}/.gitignore +0 -0
- {didactic-0.3.0 → didactic-0.3.2}/README.md +0 -0
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/axioms/__init__.py +0 -0
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/axioms/_axioms.py +0 -0
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/cli/__init__.py +0 -0
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/codegen/__init__.py +0 -0
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/codegen/_emitter.py +0 -0
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/codegen/io.py +0 -0
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/fields/__init__.py +0 -0
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/fields/_refs.py +0 -0
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/lenses/__init__.py +0 -0
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/migrations/__init__.py +0 -0
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/migrations/_migrations.py +0 -0
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/models/__init__.py +0 -0
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/models/_storage.py +0 -0
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/py.typed +0 -0
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/theory/__init__.py +0 -0
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/types/__init__.py +0 -0
- {didactic-0.3.0 → didactic-0.3.2}/src/didactic/types/_types_lib.py +0 -0
- {didactic-0.3.0 → 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,12 +31,9 @@ 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
|
-
from typing import TYPE_CHECKING
|
|
36
|
+
from typing import TYPE_CHECKING, cast
|
|
40
37
|
|
|
41
38
|
if TYPE_CHECKING:
|
|
42
39
|
from didactic.models._model import Model
|
|
@@ -67,7 +64,7 @@ def schema_uri(cls: type[Model]) -> str:
|
|
|
67
64
|
|
|
68
65
|
|
|
69
66
|
def embed_schema_uri(instance: Model) -> JsonObject:
|
|
70
|
-
"""Return ``instance
|
|
67
|
+
"""Return the JSON-shape dump of ``instance`` with a ``$schema`` URI prepended.
|
|
71
68
|
|
|
72
69
|
Parameters
|
|
73
70
|
----------
|
|
@@ -77,16 +74,23 @@ def embed_schema_uri(instance: Model) -> JsonObject:
|
|
|
77
74
|
Returns
|
|
78
75
|
-------
|
|
79
76
|
dict
|
|
80
|
-
The dump dict, with ``"$schema"`` set to the Model's
|
|
77
|
+
The JSON-safe dump dict, with ``"$schema"`` set to the Model's
|
|
81
78
|
canonical URI as the first key.
|
|
82
79
|
|
|
83
80
|
Notes
|
|
84
81
|
-----
|
|
82
|
+
Routes through ``model_dump_json`` (not the bare ``model_dump``)
|
|
83
|
+
so any nested ``tuple[Embed[T], ...]`` or ``dict[str, Embed[T]]``
|
|
84
|
+
fields get the JSON-safe walk; the returned dict is always
|
|
85
|
+
serialisable with ``json.dumps``.
|
|
86
|
+
|
|
85
87
|
A consumer that knows how to resolve ``didactic://v1/<fp>`` URIs
|
|
86
88
|
can fetch the Theory by fingerprint and validate the payload
|
|
87
89
|
without knowing the original Python class.
|
|
88
90
|
"""
|
|
89
|
-
|
|
91
|
+
import json # noqa: PLC0415
|
|
92
|
+
|
|
93
|
+
payload = cast("JsonObject", json.loads(instance.model_dump_json()))
|
|
90
94
|
return {"$schema": schema_uri(type(instance)), **payload}
|
|
91
95
|
|
|
92
96
|
|
|
@@ -188,6 +192,9 @@ def validate_with_uri_lookup(
|
|
|
188
192
|
raise KeyError(msg)
|
|
189
193
|
|
|
190
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)
|
|
191
198
|
cls = registry.lookup(uri)
|
|
192
199
|
if cls is None:
|
|
193
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
|
|