didactic 0.7.2__tar.gz → 0.7.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 (50) hide show
  1. {didactic-0.7.2 → didactic-0.7.3}/PKG-INFO +3 -3
  2. {didactic-0.7.2 → didactic-0.7.3}/README.md +1 -1
  3. {didactic-0.7.2 → didactic-0.7.3}/pyproject.toml +2 -2
  4. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/api.py +9 -1
  5. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/codegen/source.py +5 -2
  6. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/lenses/__init__.py +8 -0
  7. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/lenses/_dependent_lens.py +5 -3
  8. didactic-0.7.3/src/didactic/lenses/_morphisms.py +200 -0
  9. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/theory/_theory.py +34 -5
  10. {didactic-0.7.2 → didactic-0.7.3}/.gitignore +0 -0
  11. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/_self_describing.py +0 -0
  12. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/axioms/__init__.py +0 -0
  13. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/axioms/_axiom_enforcement.py +0 -0
  14. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/axioms/_axioms.py +0 -0
  15. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/cli/__init__.py +0 -0
  16. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/cli/_cli.py +0 -0
  17. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/codegen/__init__.py +0 -0
  18. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/codegen/_emitter.py +0 -0
  19. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/codegen/_json_schema.py +0 -0
  20. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/codegen/_write.py +0 -0
  21. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/codegen/io.py +0 -0
  22. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/fields/__init__.py +0 -0
  23. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/fields/_computed.py +0 -0
  24. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/fields/_derived.py +0 -0
  25. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/fields/_fields.py +0 -0
  26. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/fields/_refs.py +0 -0
  27. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/fields/_unions.py +0 -0
  28. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/fields/_validators.py +0 -0
  29. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/lenses/_lens.py +0 -0
  30. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/lenses/_testing.py +0 -0
  31. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/migrations/__init__.py +0 -0
  32. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/migrations/_diff.py +0 -0
  33. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/migrations/_fingerprint.py +0 -0
  34. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/migrations/_migrations.py +0 -0
  35. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/migrations/_synthesis.py +0 -0
  36. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/models/__init__.py +0 -0
  37. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/models/_config.py +0 -0
  38. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/models/_meta.py +0 -0
  39. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/models/_model.py +0 -0
  40. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/models/_root.py +0 -0
  41. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/models/_storage.py +0 -0
  42. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/py.typed +0 -0
  43. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/theory/__init__.py +0 -0
  44. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/types/__init__.py +0 -0
  45. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/types/_types.py +0 -0
  46. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/types/_types_lib.py +0 -0
  47. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/types/_typing.py +0 -0
  48. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/vcs/__init__.py +0 -0
  49. {didactic-0.7.2 → didactic-0.7.3}/src/didactic/vcs/_backref.py +0 -0
  50. {didactic-0.7.2 → didactic-0.7.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.7.2
3
+ Version: 0.7.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
@@ -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.48.3
18
+ Requires-Dist: panproto>=0.52.0
19
19
  Description-Content-Type: text/markdown
20
20
 
21
21
  # didactic
@@ -39,7 +39,7 @@ contribute submodules under `didactic.<name>`.
39
39
 
40
40
  ## Install
41
41
 
42
- didactic targets Python 3.14 and panproto 0.43+.
42
+ didactic targets Python 3.14 and panproto 0.52+.
43
43
 
44
44
  ```sh
45
45
  pip install didactic
@@ -19,7 +19,7 @@ contribute submodules under `didactic.<name>`.
19
19
 
20
20
  ## Install
21
21
 
22
- didactic targets Python 3.14 and panproto 0.43+.
22
+ didactic targets Python 3.14 and panproto 0.52+.
23
23
 
24
24
  ```sh
25
25
  pip install didactic
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "didactic"
7
- version = "0.7.2"
7
+ version = "0.7.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"
@@ -22,7 +22,7 @@ classifiers = [
22
22
  "Typing :: Typed",
23
23
  ]
24
24
  dependencies = [
25
- "panproto>=0.48.3",
25
+ "panproto>=0.52.0",
26
26
  "annotated-types>=0.7",
27
27
  ]
28
28
 
@@ -54,6 +54,11 @@ from didactic.fields._validators import (
54
54
  from didactic.lenses import _testing as testing
55
55
  from didactic.lenses._dependent_lens import DependentLens
56
56
  from didactic.lenses._lens import Iso, Lens, Mapping, lens
57
+ from didactic.lenses._morphisms import (
58
+ Correspondence,
59
+ best_correspondence,
60
+ find_correspondences,
61
+ )
57
62
  from didactic.migrations._diff import classify_change, diff, is_breaking_change
58
63
  from didactic.migrations._migrations import (
59
64
  load_registry,
@@ -69,7 +74,7 @@ from didactic.types import _types_lib as types
69
74
  from didactic.vcs._backref import ModelPool, resolve_backrefs
70
75
  from didactic.vcs._repo import Repository
71
76
 
72
- __version__ = "0.7.1"
77
+ __version__ = "0.7.3"
73
78
 
74
79
  #: Conventional namespace for lens utilities (`dx.lens.identity(...)`,
75
80
  #: `dx.lens.Lens`, etc.). The ``lens`` name doubles as a decorator
@@ -81,6 +86,7 @@ __all__ = [
81
86
  "Axiom",
82
87
  "Backref",
83
88
  "BaseModel",
89
+ "Correspondence",
84
90
  "DependentLens",
85
91
  "Embed",
86
92
  "ExtraPolicy",
@@ -103,6 +109,7 @@ __all__ = [
103
109
  "ValidationErrorEntry",
104
110
  "__version__",
105
111
  "axiom",
112
+ "best_correspondence",
106
113
  "classify_change",
107
114
  "codegen",
108
115
  "computed",
@@ -110,6 +117,7 @@ __all__ = [
110
117
  "diff",
111
118
  "embed_schema_uri",
112
119
  "field",
120
+ "find_correspondences",
113
121
  "is_breaking_change",
114
122
  "lens",
115
123
  "load_registry",
@@ -4,8 +4,11 @@ For every tree-sitter grammar panproto bundles, didactic can:
4
4
 
5
5
  - **De-novo emit** a [Model][didactic.api.Model] class as fresh source via
6
6
  [emit_pretty][didactic.codegen.source.emit_pretty]. Walks the
7
- grammar's production rules; output is syntactically valid for any
8
- grammar that ships a ``grammar.json``.
7
+ grammar's production rules; spacing and indentation derive from
8
+ grammar-classified token roles. Emit round-trips are checked
9
+ against each grammar's upstream corpus in panproto's test suite.
10
+ Use [available_targets][didactic.codegen.source.available_targets]
11
+ to enumerate what the running build supports.
9
12
  - **Edit-pipeline emit**: parse real source, transform the schema,
10
13
  re-emit the bytes. Uses [emit][didactic.codegen.source.emit].
11
14
  - **Parse** source bytes into a panproto Schema for inspection.
@@ -4,12 +4,20 @@ from didactic.lenses import _testing as testing
4
4
  from didactic.lenses._dependent_lens import DependentLens
5
5
  from didactic.lenses._lens import Iso, Lens, Mapping, identity
6
6
  from didactic.lenses._lens import lens as lens
7
+ from didactic.lenses._morphisms import (
8
+ Correspondence,
9
+ best_correspondence,
10
+ find_correspondences,
11
+ )
7
12
 
8
13
  __all__ = [
14
+ "Correspondence",
9
15
  "DependentLens",
10
16
  "Iso",
11
17
  "Lens",
12
18
  "Mapping",
19
+ "best_correspondence",
20
+ "find_correspondences",
13
21
  "identity",
14
22
  "lens",
15
23
  "testing",
@@ -146,7 +146,7 @@ class DependentLens:
146
146
  src_schema: panproto.Schema,
147
147
  tgt_schema: panproto.Schema,
148
148
  protocol: panproto.Protocol,
149
- hints: object,
149
+ hints: dict[str, str],
150
150
  *,
151
151
  stringency: str | None = None,
152
152
  ) -> DependentLens:
@@ -161,8 +161,10 @@ class DependentLens:
161
161
  protocol
162
162
  The panproto protocol both schemas conform to.
163
163
  hints
164
- Vertex-correspondence hints. Their exact shape is
165
- panproto-defined.
164
+ Vertex-correspondence hints mapping source-schema vertex
165
+ IDs to target-schema vertex IDs. A discovered
166
+ [Correspondence][didactic.api.Correspondence] supplies this
167
+ shape via its ``vertex_map``.
166
168
  stringency
167
169
  Optional stringency hint. ``None`` uses panproto's default.
168
170
 
@@ -0,0 +1,200 @@
1
+ """Vertex-correspondence discovery via panproto's hom search.
2
+
3
+ Given two panproto Schemas, panproto can enumerate the structure-
4
+ preserving maps (schema morphisms) between them and score each one.
5
+ didactic wraps that search as
6
+ [find_correspondences][didactic.api.find_correspondences] and
7
+ [best_correspondence][didactic.api.best_correspondence], returning
8
+ plain [Correspondence][didactic.api.Correspondence] records.
9
+
10
+ The discovered ``vertex_map`` has exactly the shape that
11
+ [DependentLens.auto_generate_with_hints][didactic.api.DependentLens.auto_generate_with_hints]
12
+ takes as ``hints``, so the two compose into a discover-then-derive
13
+ pipeline: search for the best correspondence, then derive a chain
14
+ that respects it.
15
+
16
+ Examples
17
+ --------
18
+ >>> import didactic.api as dx
19
+ >>> import panproto
20
+ >>>
21
+ >>> proto = panproto.get_builtin_protocol("openapi")
22
+ >>> # ... build src_schema and tgt_schema via proto.schema() ...
23
+ >>>
24
+ >>> best = dx.best_correspondence(src_schema, tgt_schema) # doctest: +SKIP
25
+ >>> best.vertex_map # doctest: +SKIP
26
+ {'post:body.text': 'post:body.content', ...}
27
+ >>> chain = dx.DependentLens.auto_generate_with_hints( # doctest: +SKIP
28
+ ... src_schema,
29
+ ... tgt_schema,
30
+ ... proto,
31
+ ... best.vertex_map,
32
+ ... )
33
+
34
+ See Also
35
+ --------
36
+ didactic.lenses._dependent_lens : the hint-consuming chain derivation.
37
+ panproto.find_morphisms : the runtime search.
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ from dataclasses import dataclass
43
+ from typing import TYPE_CHECKING
44
+
45
+ if TYPE_CHECKING:
46
+ import panproto
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class Correspondence:
51
+ """One discovered schema morphism, scored.
52
+
53
+ Parameters
54
+ ----------
55
+ vertex_map
56
+ Mapping from source-schema vertex IDs to target-schema vertex
57
+ IDs. Feed directly as the ``hints`` argument of
58
+ [DependentLens.auto_generate_with_hints][didactic.api.DependentLens.auto_generate_with_hints].
59
+ quality
60
+ Alignment quality in ``[0.0, 1.0]``. Higher is better.
61
+ """
62
+
63
+ vertex_map: dict[str, str]
64
+ quality: float
65
+
66
+
67
+ def find_correspondences(
68
+ src_schema: panproto.Schema,
69
+ tgt_schema: panproto.Schema,
70
+ *,
71
+ anchors: dict[str, str] | None = None,
72
+ monic: bool = False,
73
+ epic: bool = False,
74
+ iso: bool = False,
75
+ max_results: int = 0,
76
+ relax_edge_name_pruning: bool = False,
77
+ ) -> list[Correspondence]:
78
+ """Enumerate scored vertex correspondences between two schemas.
79
+
80
+ Parameters
81
+ ----------
82
+ src_schema
83
+ Source schema.
84
+ tgt_schema
85
+ Target schema.
86
+ anchors
87
+ Vertex pairs (source ID to target ID) the search must respect.
88
+ Use to pin known correspondences and let the search fill in
89
+ the rest.
90
+ monic
91
+ Require the morphism to be injective on vertices (no two
92
+ source vertices map to the same target vertex).
93
+ epic
94
+ Require the morphism to be surjective on vertices (every
95
+ target vertex is hit).
96
+ iso
97
+ Require a bijection. Implies ``monic`` and ``epic``.
98
+ max_results
99
+ Upper bound on the number of morphisms returned. ``0`` means
100
+ unbounded.
101
+ relax_edge_name_pruning
102
+ Keep kind-compatible candidate targets that share no outgoing
103
+ edge name with the source vertex. By default the search prunes
104
+ such candidates for object vertices with large candidate
105
+ domains, which can discard a correct pairing when every child
106
+ was renamed. Naturality is still enforced.
107
+
108
+ Returns
109
+ -------
110
+ list of Correspondence
111
+ Discovered correspondences. Empty when no structure-preserving
112
+ map exists under the given constraints.
113
+
114
+ Notes
115
+ -----
116
+ The schemas didactic builds from Model classes are single-vertex
117
+ (the structure lives in the Theory), so searching between two
118
+ Models degenerates to the root pairing. The search is informative
119
+ on multi-vertex schemas: protocol schemas built by hand and
120
+ schemas recovered by [didactic.codegen.source.parse][].
121
+ """
122
+ import panproto # noqa: PLC0415
123
+
124
+ found = panproto.find_morphisms(
125
+ src_schema,
126
+ tgt_schema,
127
+ anchors=anchors,
128
+ monic=monic,
129
+ epic=epic,
130
+ iso=iso,
131
+ max_results=max_results,
132
+ relax_edge_name_pruning=relax_edge_name_pruning,
133
+ )
134
+ return [
135
+ Correspondence(vertex_map=m.vertex_map, quality=float(m.quality)) for m in found
136
+ ]
137
+
138
+
139
+ def best_correspondence(
140
+ src_schema: panproto.Schema,
141
+ tgt_schema: panproto.Schema,
142
+ *,
143
+ anchors: dict[str, str] | None = None,
144
+ monic: bool = False,
145
+ epic: bool = False,
146
+ iso: bool = False,
147
+ relax_edge_name_pruning: bool = False,
148
+ ) -> Correspondence | None:
149
+ """Return the highest-quality correspondence, or ``None``.
150
+
151
+ Parameters
152
+ ----------
153
+ src_schema
154
+ Source schema.
155
+ tgt_schema
156
+ Target schema.
157
+ anchors
158
+ Vertex pairs (source ID to target ID) the search must respect.
159
+ monic
160
+ Require injectivity on vertices.
161
+ epic
162
+ Require surjectivity on vertices.
163
+ iso
164
+ Require a bijection. Implies ``monic`` and ``epic``.
165
+ relax_edge_name_pruning
166
+ Keep kind-compatible candidate targets that share no outgoing
167
+ edge name with the source vertex. See
168
+ [find_correspondences][didactic.api.find_correspondences].
169
+
170
+ Returns
171
+ -------
172
+ Correspondence or None
173
+ The best-scoring correspondence, or ``None`` when no
174
+ structure-preserving map exists under the given constraints.
175
+
176
+ See Also
177
+ --------
178
+ find_correspondences : the full enumeration.
179
+ """
180
+ import panproto # noqa: PLC0415
181
+
182
+ found = panproto.find_best_morphism(
183
+ src_schema,
184
+ tgt_schema,
185
+ anchors=anchors,
186
+ monic=monic,
187
+ epic=epic,
188
+ iso=iso,
189
+ relax_edge_name_pruning=relax_edge_name_pruning,
190
+ )
191
+ if found is None:
192
+ return None
193
+ return Correspondence(vertex_map=found.vertex_map, quality=float(found.quality))
194
+
195
+
196
+ __all__ = [
197
+ "Correspondence",
198
+ "best_correspondence",
199
+ "find_correspondences",
200
+ ]
@@ -34,6 +34,8 @@ from __future__ import annotations
34
34
  from typing import TYPE_CHECKING, TypedDict, cast
35
35
 
36
36
  if TYPE_CHECKING:
37
+ from collections.abc import Mapping
38
+
37
39
  import panproto
38
40
 
39
41
  from didactic.fields._fields import FieldSpec
@@ -58,6 +60,31 @@ class TheorySpec(TypedDict):
58
60
  policies: list[dict[str, JsonValue]]
59
61
 
60
62
 
63
+ def _spec_payload(spec: TheorySpec) -> Mapping[str, JsonValue]:
64
+ """Narrow a ``TheorySpec`` to the mapping shape ``create_theory`` takes.
65
+
66
+ Parameters
67
+ ----------
68
+ spec
69
+ A spec dict from ``build_theory_spec``.
70
+
71
+ Returns
72
+ -------
73
+ Mapping
74
+ The same dict, typed as ``Mapping[str, JsonValue]``.
75
+
76
+ Notes
77
+ -----
78
+ The typing spec makes a ``TypedDict`` assignable to
79
+ ``Mapping[str, object]`` and to no mapping with a narrower value
80
+ type, so handing a ``TheorySpec`` to ``panproto.create_theory``
81
+ (whose parameter is ``Mapping[str, JsonValue]``) needs a cast at
82
+ the boundary. Every ``TheorySpec`` field type is a ``JsonValue``,
83
+ so the cast is sound.
84
+ """
85
+ return cast("Mapping[str, JsonValue]", spec)
86
+
87
+
61
88
  # ---------------------------------------------------------------------------
62
89
  # Spec construction (no panproto runtime required)
63
90
  # ---------------------------------------------------------------------------
@@ -347,7 +374,7 @@ def build_theory(cls: type) -> panproto.Theory:
347
374
  if len(parents) <= 1:
348
375
  # single (or no) Model inheritance: the flat spec is correct
349
376
  spec = build_theory_spec(cls)
350
- return panproto.create_theory(spec)
377
+ return panproto.create_theory(_spec_payload(spec))
351
378
 
352
379
  # multiple Model inheritance: compute the colimit of the parent
353
380
  # theories over their lowest common ancestor in the Model lineage
@@ -432,13 +459,15 @@ def _build_colimit_theory(cls: type, parents: list[type]) -> panproto.Theory:
432
459
  """
433
460
  import panproto # noqa: PLC0415
434
461
 
435
- accumulator = panproto.create_theory(build_theory_spec(parents[0]))
462
+ accumulator = panproto.create_theory(_spec_payload(build_theory_spec(parents[0])))
436
463
  accumulator_cls: type = parents[0]
437
464
 
438
465
  for parent in parents[1:]:
439
466
  ancestor = _lowest_common_model_ancestor([accumulator_cls, parent])
440
- ancestor_theory = panproto.create_theory(build_theory_spec(ancestor))
441
- next_theory = panproto.create_theory(build_theory_spec(parent))
467
+ ancestor_theory = panproto.create_theory(
468
+ _spec_payload(build_theory_spec(ancestor))
469
+ )
470
+ next_theory = panproto.create_theory(_spec_payload(build_theory_spec(parent)))
442
471
  accumulator = panproto.colimit_theories(
443
472
  accumulator, next_theory, ancestor_theory
444
473
  )
@@ -447,7 +476,7 @@ def _build_colimit_theory(cls: type, parents: list[type]) -> panproto.Theory:
447
476
  # finally fold in any cls-only fields by colimiting against the
448
477
  # immediate spec of cls; the shared ancestor is the accumulated
449
478
  # theory we just built
450
- cls_theory = panproto.create_theory(build_theory_spec(cls))
479
+ cls_theory = panproto.create_theory(_spec_payload(build_theory_spec(cls)))
451
480
  return panproto.colimit_theories(accumulator, cls_theory, accumulator)
452
481
 
453
482
 
File without changes
File without changes