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.
Files changed (49) hide show
  1. {didactic-0.3.1 → didactic-0.3.2}/PKG-INFO +2 -2
  2. {didactic-0.3.1 → didactic-0.3.2}/pyproject.toml +2 -2
  3. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/_self_describing.py +3 -3
  4. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/api.py +1 -2
  5. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/axioms/_axiom_enforcement.py +0 -1
  6. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/cli/_cli.py +9 -4
  7. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/codegen/_json_schema.py +8 -3
  8. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/codegen/_write.py +2 -8
  9. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/codegen/source.py +5 -7
  10. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/fields/_computed.py +0 -1
  11. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/fields/_derived.py +12 -6
  12. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/fields/_fields.py +18 -11
  13. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/fields/_unions.py +15 -9
  14. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/fields/_validators.py +8 -5
  15. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/lenses/_dependent_lens.py +7 -7
  16. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/lenses/_lens.py +11 -9
  17. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/lenses/_testing.py +2 -2
  18. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/migrations/_diff.py +22 -4
  19. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/migrations/_fingerprint.py +10 -8
  20. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/migrations/_synthesis.py +4 -7
  21. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/models/_config.py +0 -1
  22. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/models/_meta.py +35 -20
  23. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/models/_model.py +1 -4
  24. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/models/_root.py +6 -6
  25. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/theory/_theory.py +8 -49
  26. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/types/_types.py +18 -13
  27. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/types/_typing.py +2 -11
  28. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/vcs/_backref.py +8 -7
  29. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/vcs/_repo.py +13 -11
  30. {didactic-0.3.1 → didactic-0.3.2}/.gitignore +0 -0
  31. {didactic-0.3.1 → didactic-0.3.2}/README.md +0 -0
  32. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/axioms/__init__.py +0 -0
  33. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/axioms/_axioms.py +0 -0
  34. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/cli/__init__.py +0 -0
  35. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/codegen/__init__.py +0 -0
  36. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/codegen/_emitter.py +0 -0
  37. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/codegen/io.py +0 -0
  38. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/fields/__init__.py +0 -0
  39. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/fields/_refs.py +0 -0
  40. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/lenses/__init__.py +0 -0
  41. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/migrations/__init__.py +0 -0
  42. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/migrations/_migrations.py +0 -0
  43. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/models/__init__.py +0 -0
  44. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/models/_storage.py +0 -0
  45. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/py.typed +0 -0
  46. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/theory/__init__.py +0 -0
  47. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/types/__init__.py +0 -0
  48. {didactic-0.3.1 → didactic-0.3.2}/src/didactic/types/_types_lib.py +0 -0
  49. {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.1
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.1"
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.1"
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
- for change in report.get("breaking", []):
194
- print(f" {change}")
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
- type_str, format_str = _PRIMITIVE_TYPE_MAP.get(annotation, ("string", None))
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) # type: ignore[attr-defined]
100
- payload_bytes = (
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
- return registry.emit(protocol, schema)
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:
@@ -35,7 +35,6 @@ if TYPE_CHECKING:
35
35
 
36
36
  from didactic.types._typing import FieldValue
37
37
 
38
-
39
38
  _COMPUTED_MARKER_ATTR = "__didactic_computed__"
40
39
 
41
40
 
@@ -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
- fn.__didactic_derived__ = True # type: ignore[attr-defined]
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._derived_cache
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 cast("FieldValue", cache[name])
104
+ return cache[name]
101
105
 
102
106
  prop = property(_getter)
103
- prop.fget.__didactic_derived__ = True # type: ignore[union-attr]
104
- prop.fget.__wrapped_name__ = name # type: ignore[union-attr]
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
- # ``extras`` is the project's ``Mapping[str, Opaque]`` and pyright
33
- # doesn't carry the value-type through ``dict(extras)``.
34
- # Tracked in panproto/didactic#1.
35
- # pyright: reportUnknownVariableType=false
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( # pyright: ignore[reportInconsistentOverload]
243
+ def field(
239
244
  *,
240
- default: DefaultOrMissing = MISSING,
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: type
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(default_factory=dict)
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 didactic.types._typing import FieldValue, JsonObject, Opaque
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: JsonObject) -> Self:
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
- value = payload[disc]
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
- return variant.model_validate(payload) # type: ignore[return-value]
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
- isinstance(klass, type)
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: type) -> tuple[FieldValue, ...]:
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
- # mark the function so the metaclass can find it
154
- fn.__didactic_validator__ = {"fields": field_names, "mode": mode}
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
- return self._inner.instantiate(schema, protocol)
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
- self.identity: Callable[..., Iso[Opaque, Opaque]] = identity
382
- self.Lens: type[Lens[Opaque, Opaque, Opaque]] = Lens
383
- self.Iso: type[Iso[Opaque, Opaque]] = Iso
384
- self.Mapping: type[Mapping[Opaque, Opaque]] = Mapping
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",
@@ -102,8 +102,8 @@ def verify_iso[A](
102
102
  _check()
103
103
 
104
104
 
105
- def check_lens_laws[A, B](
106
- lens: Lens[A, B],
105
+ def check_lens_laws[A, B, C](
106
+ lens: Lens[A, B, C],
107
107
  strategy: SearchStrategy[A],
108
108
  *,
109
109
  max_examples: int = 100,
@@ -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
- compat = panproto.diff_and_classify(old_schema, new_schema, protocol)
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: tuple[JsonValue, ...]) -> JsonValue:
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
- return _replace_model_name(spec, name)
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
 
@@ -96,7 +96,6 @@ class ModelConfig:
96
96
  DEFAULT_CONFIG = ModelConfig()
97
97
  """The configuration applied to a Model that does not specify one."""
98
98
 
99
-
100
99
  __all__ = [
101
100
  "DEFAULT_CONFIG",
102
101
  "ExtraPolicy",
@@ -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
- translation = classify(annotation)
226
- annotated_meta = read_annotated_metadata(annotation)
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
- if cls.__dict__.get("__theory_cache__") is None:
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
- cls.__theory_cache__ = build_theory(cls)
314
- return cls.__theory_cache__
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: dict[str, JsonValue]
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
- merged_kwargs = {
418
- "extra": explicit.extra,
419
- "strict": explicit.strict,
420
- "populate_by_name": explicit.populate_by_name,
421
- "title": explicit.title,
422
- "description": explicit.description,
423
- }
424
- merged_kwargs.update(header_overrides)
425
- return ModelConfig(**merged_kwargs)
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) # type: ignore[no-any-return]
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
- return json.dumps(self._translation.encode(value))
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
- create_theory = panproto.create_theory
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(accumulator, next_theory, ancestor_theory)
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]``) plus PEP 604
64
+ # generics like ``tuple[int, ...]`` and ``dict[str, int]``), PEP 604
68
65
  # ``UnionType`` (``int | None``), which pyright does *not* narrow to
69
- # ``type``. Other typing special forms (``typing.Union[...]``,
70
- # ``Annotated[...]``, PEP 695 type aliases) are accepted at runtime but
71
- # fall outside this static union; callers that pass them rely on pyright's
72
- # legacy structural acceptance of those forms.
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 match any arm of "
987
- f"alias {alias_name!r}; expected one of {sorted(table)}."
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 _Missing
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 | _Missing
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[A: Model, B: Model](
49
- target: A,
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
- return list(self._by_class.get(cls, [])) # type: ignore[arg-type]
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[A: Model, B: Model](
184
+ def backrefs[B: Model](
184
185
  self,
185
- target: A,
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
- return self._inner.resolve_ref(ref)
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
- self._inner.add(target)
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