didactic 0.4.2__tar.gz → 0.4.3__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.4.2 → didactic-0.4.3}/PKG-INFO +1 -1
  2. {didactic-0.4.2 → didactic-0.4.3}/pyproject.toml +1 -1
  3. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/api.py +1 -1
  4. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/fields/_unions.py +56 -3
  5. {didactic-0.4.2 → didactic-0.4.3}/.gitignore +0 -0
  6. {didactic-0.4.2 → didactic-0.4.3}/README.md +0 -0
  7. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/_self_describing.py +0 -0
  8. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/axioms/__init__.py +0 -0
  9. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/axioms/_axiom_enforcement.py +0 -0
  10. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/axioms/_axioms.py +0 -0
  11. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/cli/__init__.py +0 -0
  12. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/cli/_cli.py +0 -0
  13. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/codegen/__init__.py +0 -0
  14. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/codegen/_emitter.py +0 -0
  15. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/codegen/_json_schema.py +0 -0
  16. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/codegen/_write.py +0 -0
  17. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/codegen/io.py +0 -0
  18. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/codegen/source.py +0 -0
  19. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/fields/__init__.py +0 -0
  20. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/fields/_computed.py +0 -0
  21. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/fields/_derived.py +0 -0
  22. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/fields/_fields.py +0 -0
  23. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/fields/_refs.py +0 -0
  24. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/fields/_validators.py +0 -0
  25. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/lenses/__init__.py +0 -0
  26. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/lenses/_dependent_lens.py +0 -0
  27. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/lenses/_lens.py +0 -0
  28. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/lenses/_testing.py +0 -0
  29. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/migrations/__init__.py +0 -0
  30. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/migrations/_diff.py +0 -0
  31. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/migrations/_fingerprint.py +0 -0
  32. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/migrations/_migrations.py +0 -0
  33. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/migrations/_synthesis.py +0 -0
  34. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/models/__init__.py +0 -0
  35. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/models/_config.py +0 -0
  36. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/models/_meta.py +0 -0
  37. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/models/_model.py +0 -0
  38. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/models/_root.py +0 -0
  39. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/models/_storage.py +0 -0
  40. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/py.typed +0 -0
  41. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/theory/__init__.py +0 -0
  42. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/theory/_theory.py +0 -0
  43. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/types/__init__.py +0 -0
  44. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/types/_types.py +0 -0
  45. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/types/_types_lib.py +0 -0
  46. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/types/_typing.py +0 -0
  47. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/vcs/__init__.py +0 -0
  48. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/vcs/_backref.py +0 -0
  49. {didactic-0.4.2 → didactic-0.4.3}/src/didactic/vcs/_repo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: didactic
3
- Version: 0.4.2
3
+ Version: 0.4.3
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "didactic"
7
- version = "0.4.2"
7
+ version = "0.4.3"
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"
@@ -68,7 +68,7 @@ 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.4.2"
71
+ __version__ = "0.4.3"
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
@@ -44,7 +44,16 @@ didactic.models._meta.ModelMeta : the metaclass; unchanged.
44
44
  from __future__ import annotations
45
45
 
46
46
  import annotationlib
47
- from typing import TYPE_CHECKING, ClassVar, Literal, Self, cast, get_args, get_origin
47
+ import sys
48
+ from typing import (
49
+ TYPE_CHECKING,
50
+ ClassVar,
51
+ Literal,
52
+ Self,
53
+ cast,
54
+ get_args,
55
+ get_origin,
56
+ )
48
57
 
49
58
  from didactic.fields._validators import ValidationError, ValidationErrorEntry
50
59
  from didactic.models._model import Model
@@ -139,6 +148,12 @@ class TaggedUnion(Model):
139
148
  # NOTE: we read annotations directly here rather than through the
140
149
  # metaclass's `__field_specs__` because `__init_subclass__` fires
141
150
  # *during* the metaclass's __new__, before __field_specs__ is set.
151
+ # FORWARDREF gives us un-evaluated forms when the class uses
152
+ # ``from __future__ import annotations``; the discriminator field
153
+ # is then re-resolved in the class's defining module so qualified
154
+ # spellings (``typing.Literal[...]``) and aliased imports
155
+ # (``from typing import Literal as L``) are handled the same as
156
+ # the bare ``Literal[...]`` form.
142
157
  annotations = annotationlib.get_annotations(
143
158
  cls,
144
159
  format=annotationlib.Format.FORWARDREF,
@@ -150,12 +165,16 @@ class TaggedUnion(Model):
150
165
  )
151
166
  raise TypeError(msg)
152
167
 
168
+ disc_annotation = _resolve_discriminator_annotation(
169
+ cls, annotations[disc_field]
170
+ )
171
+
153
172
  # extract the Literal value(s)
154
- values = _literal_values(annotations[disc_field])
173
+ values = _literal_values(disc_annotation)
155
174
  if not values:
156
175
  msg = (
157
176
  f"variant {cls.__name__}.{disc_field} must be annotated as "
158
- f"Literal[...] with at least one value; got {annotations[disc_field]!r}"
177
+ f"Literal[...] with at least one value; got {disc_annotation!r}"
159
178
  )
160
179
  raise TypeError(msg)
161
180
 
@@ -252,6 +271,40 @@ def _literal_values(annotation: Opaque) -> tuple[FieldValue, ...]:
252
271
  return ()
253
272
 
254
273
 
274
+ def _resolve_discriminator_annotation(cls: type, raw: Opaque) -> Opaque:
275
+ """Return the live annotation object for ``cls.disc_field``.
276
+
277
+ Under ``from __future__ import annotations`` (or any context where
278
+ the class's ``__annotations__`` carries strings), the raw entry
279
+ will not satisfy ``get_origin(...) is Literal``. Evaluate the
280
+ string in the class's defining module so qualified spellings
281
+ (``typing.Literal[...]``), aliased imports
282
+ (``from typing import Literal as L``), and bare ``Literal[...]``
283
+ all reach the same live ``Literal`` object.
284
+
285
+ Only the discriminator's own annotation is evaluated; other
286
+ fields on the class may legitimately carry forward references
287
+ that don't resolve at class-creation time, and resolving them
288
+ here would mask those into spurious failures of this check.
289
+
290
+ The raw annotation is returned unchanged when evaluation fails;
291
+ the caller surfaces the original value in the error message,
292
+ which is friendlier than a stray ``NameError``.
293
+ """
294
+ if get_origin(raw) is Literal:
295
+ return raw
296
+ if not isinstance(raw, str):
297
+ return raw
298
+ module = sys.modules.get(getattr(cls, "__module__", ""))
299
+ if module is None:
300
+ return raw
301
+ try:
302
+ evaluated = eval(raw, vars(module))
303
+ except Exception:
304
+ return raw
305
+ return cast("Opaque", evaluated)
306
+
307
+
255
308
  __all__ = [
256
309
  "TaggedUnion",
257
310
  ]
File without changes
File without changes
File without changes