overture-schema-codegen 0.1.1.dev0__py3-none-any.whl
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.
- overture/schema/codegen/__init__.py +1 -0
- overture/schema/codegen/cli.py +228 -0
- overture/schema/codegen/extraction/__init__.py +0 -0
- overture/schema/codegen/extraction/docstring.py +46 -0
- overture/schema/codegen/extraction/enum_extraction.py +40 -0
- overture/schema/codegen/extraction/examples.py +367 -0
- overture/schema/codegen/extraction/field.py +172 -0
- overture/schema/codegen/extraction/field_constraints.py +185 -0
- overture/schema/codegen/extraction/field_walk.py +275 -0
- overture/schema/codegen/extraction/length_constraints.py +49 -0
- overture/schema/codegen/extraction/literal_alternatives.py +26 -0
- overture/schema/codegen/extraction/model_constraints.py +252 -0
- overture/schema/codegen/extraction/model_extraction.py +240 -0
- overture/schema/codegen/extraction/newtype_extraction.py +73 -0
- overture/schema/codegen/extraction/numeric_extraction.py +74 -0
- overture/schema/codegen/extraction/pydantic_extraction.py +33 -0
- overture/schema/codegen/extraction/specs.py +295 -0
- overture/schema/codegen/extraction/type_analyzer.py +693 -0
- overture/schema/codegen/extraction/type_registry.py +137 -0
- overture/schema/codegen/extraction/union_extraction.py +270 -0
- overture/schema/codegen/layout/__init__.py +0 -0
- overture/schema/codegen/layout/module_layout.py +139 -0
- overture/schema/codegen/layout/type_collection.py +122 -0
- overture/schema/codegen/markdown/__init__.py +0 -0
- overture/schema/codegen/markdown/link_computation.py +70 -0
- overture/schema/codegen/markdown/path_assignment.py +114 -0
- overture/schema/codegen/markdown/pipeline.py +198 -0
- overture/schema/codegen/markdown/renderer.py +641 -0
- overture/schema/codegen/markdown/reverse_references.py +169 -0
- overture/schema/codegen/markdown/templates/_used_by.md.jinja2 +10 -0
- overture/schema/codegen/markdown/templates/enum.md.jinja2 +13 -0
- overture/schema/codegen/markdown/templates/feature.md.jinja2 +45 -0
- overture/schema/codegen/markdown/templates/geometric.md.jinja2 +11 -0
- overture/schema/codegen/markdown/templates/newtype.md.jinja2 +17 -0
- overture/schema/codegen/markdown/templates/numeric.md.jinja2 +27 -0
- overture/schema/codegen/markdown/templates/pydantic_type.md.jinja2 +8 -0
- overture/schema/codegen/markdown/type_format.py +383 -0
- overture/schema/codegen/py.typed +0 -0
- overture/schema/codegen/pyspark/__init__.py +1 -0
- overture/schema/codegen/pyspark/_primitive_fill.py +23 -0
- overture/schema/codegen/pyspark/_render_common.py +477 -0
- overture/schema/codegen/pyspark/check_builder.py +961 -0
- overture/schema/codegen/pyspark/check_ir.py +223 -0
- overture/schema/codegen/pyspark/constraint_dispatch.py +753 -0
- overture/schema/codegen/pyspark/pipeline.py +220 -0
- overture/schema/codegen/pyspark/renderer.py +816 -0
- overture/schema/codegen/pyspark/schema_builder.py +187 -0
- overture/schema/codegen/pyspark/templates/_check_function.py.jinja2 +10 -0
- overture/schema/codegen/pyspark/templates/model_module.py.jinja2 +83 -0
- overture/schema/codegen/pyspark/templates/test_module.py.jinja2 +129 -0
- overture/schema/codegen/pyspark/test_data/__init__.py +9 -0
- overture/schema/codegen/pyspark/test_data/base_row.py +835 -0
- overture/schema/codegen/pyspark/test_data/constraint_values.py +203 -0
- overture/schema/codegen/pyspark/test_data/invalid_value.py +105 -0
- overture/schema/codegen/pyspark/test_data/scaffold.py +390 -0
- overture/schema/codegen/pyspark/test_renderer.py +708 -0
- overture/schema/codegen/spec_discovery.py +66 -0
- overture_schema_codegen-0.1.1.dev0.dist-info/METADATA +13 -0
- overture_schema_codegen-0.1.1.dev0.dist-info/RECORD +61 -0
- overture_schema_codegen-0.1.1.dev0.dist-info/WHEEL +4 -0
- overture_schema_codegen-0.1.1.dev0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Compute reverse references from types to their referrers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from ..extraction.field import (
|
|
12
|
+
FieldShape,
|
|
13
|
+
ModelRef,
|
|
14
|
+
NewTypeShape,
|
|
15
|
+
Primitive,
|
|
16
|
+
UnionRef,
|
|
17
|
+
)
|
|
18
|
+
from ..extraction.field_walk import all_constraints, walk_shape
|
|
19
|
+
from ..extraction.specs import (
|
|
20
|
+
FieldSpec,
|
|
21
|
+
ModelSpec,
|
|
22
|
+
NewTypeSpec,
|
|
23
|
+
RecordSpec,
|
|
24
|
+
SupplementarySpec,
|
|
25
|
+
TypeIdentity,
|
|
26
|
+
UnionSpec,
|
|
27
|
+
is_pydantic_sourced,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"UsedByEntry",
|
|
32
|
+
"UsedByKind",
|
|
33
|
+
"compute_reverse_references",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class UsedByKind(Enum):
|
|
38
|
+
"""Kind of referrer in a 'used by' entry."""
|
|
39
|
+
|
|
40
|
+
MODEL = 0
|
|
41
|
+
NEWTYPE = 1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True)
|
|
45
|
+
class UsedByEntry:
|
|
46
|
+
"""A single 'used by' entry pointing to a referrer."""
|
|
47
|
+
|
|
48
|
+
identity: TypeIdentity
|
|
49
|
+
kind: UsedByKind
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def compute_reverse_references(
|
|
53
|
+
model_specs: Sequence[ModelSpec],
|
|
54
|
+
all_specs: Mapping[TypeIdentity, SupplementarySpec],
|
|
55
|
+
) -> dict[TypeIdentity, list[UsedByEntry]]:
|
|
56
|
+
"""Compute reverse references from types to their referrers.
|
|
57
|
+
|
|
58
|
+
Returns a dict mapping TypeIdentity to lists of UsedByEntry, sorted with
|
|
59
|
+
models before NewTypes, alphabetical within each group.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
model_specs
|
|
64
|
+
Feature-level specs (RecordSpec or UnionSpec).
|
|
65
|
+
all_specs
|
|
66
|
+
Supplementary types (enums, newtypes, sub-models).
|
|
67
|
+
"""
|
|
68
|
+
# An insertion-ordered set (dict keys) per target: dedups like a set but
|
|
69
|
+
# iterates deterministically, so sorted()'s stable order breaks ties by
|
|
70
|
+
# insertion rather than by nondeterministic set-hash order.
|
|
71
|
+
references: dict[TypeIdentity, dict[UsedByEntry, None]] = {}
|
|
72
|
+
|
|
73
|
+
def add_reference(
|
|
74
|
+
target: TypeIdentity, referrer: TypeIdentity, kind: UsedByKind
|
|
75
|
+
) -> None:
|
|
76
|
+
if target == referrer or target not in all_specs:
|
|
77
|
+
return
|
|
78
|
+
references.setdefault(target, {})[UsedByEntry(referrer, kind)] = None
|
|
79
|
+
|
|
80
|
+
def collect_from_shape(
|
|
81
|
+
shape: FieldShape,
|
|
82
|
+
referrer: TypeIdentity,
|
|
83
|
+
referrer_kind: UsedByKind,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Walk a shape and add references for every type it touches."""
|
|
86
|
+
|
|
87
|
+
def _visit(node: FieldShape) -> None:
|
|
88
|
+
match node:
|
|
89
|
+
case NewTypeShape(name=name, ref=ref):
|
|
90
|
+
add_reference(TypeIdentity(ref, name), referrer, referrer_kind)
|
|
91
|
+
case ModelRef(model=m) if m.source_type is not None:
|
|
92
|
+
add_reference(
|
|
93
|
+
TypeIdentity.of(m.source_type), referrer, referrer_kind
|
|
94
|
+
)
|
|
95
|
+
case UnionRef(union=u):
|
|
96
|
+
for member_cls in u.members:
|
|
97
|
+
add_reference(
|
|
98
|
+
TypeIdentity.of(member_cls), referrer, referrer_kind
|
|
99
|
+
)
|
|
100
|
+
case Primitive(source_type=cls) if cls is not None:
|
|
101
|
+
if isinstance(cls, type) and (
|
|
102
|
+
issubclass(cls, Enum)
|
|
103
|
+
or issubclass(cls, BaseModel)
|
|
104
|
+
or is_pydantic_sourced(cls)
|
|
105
|
+
):
|
|
106
|
+
add_reference(TypeIdentity.of(cls), referrer, referrer_kind)
|
|
107
|
+
|
|
108
|
+
walk_shape(shape, _visit)
|
|
109
|
+
|
|
110
|
+
def collect_from_fields(
|
|
111
|
+
fields: list[FieldSpec],
|
|
112
|
+
referrer: TypeIdentity,
|
|
113
|
+
referrer_kind: UsedByKind,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Collect references from each field's shape."""
|
|
116
|
+
for field_spec in fields:
|
|
117
|
+
collect_from_shape(field_spec.shape, referrer, referrer_kind)
|
|
118
|
+
|
|
119
|
+
def collect_from_model_spec(spec: RecordSpec, referrer: TypeIdentity) -> None:
|
|
120
|
+
collect_from_fields(spec.fields, referrer, UsedByKind.MODEL)
|
|
121
|
+
|
|
122
|
+
def collect_from_union_spec(spec: UnionSpec) -> None:
|
|
123
|
+
referrer = spec.identity
|
|
124
|
+
# Union features reference their members
|
|
125
|
+
for member_cls in spec.members:
|
|
126
|
+
add_reference(TypeIdentity.of(member_cls), referrer, UsedByKind.MODEL)
|
|
127
|
+
collect_from_fields(spec.fields, referrer, UsedByKind.MODEL)
|
|
128
|
+
|
|
129
|
+
def collect_from_newtype_spec(spec: NewTypeSpec, referrer: TypeIdentity) -> None:
|
|
130
|
+
# The NewType's own identity isn't added here (self-reference).
|
|
131
|
+
# spec.shape already has the outer NewTypeShape stripped.
|
|
132
|
+
collect_from_shape(spec.shape, referrer, UsedByKind.NEWTYPE)
|
|
133
|
+
|
|
134
|
+
# Inherited NewTypes from constraint sources at every layer
|
|
135
|
+
# (array / map / scalar), not just the terminal scalar -- a
|
|
136
|
+
# NewType chaining through an array NewType carries the inner
|
|
137
|
+
# NewType's provenance on the array layer.
|
|
138
|
+
for cs in all_constraints(spec.shape):
|
|
139
|
+
if cs.source_ref is not None and cs.source_name is not None:
|
|
140
|
+
add_reference(
|
|
141
|
+
TypeIdentity(cs.source_ref, cs.source_name),
|
|
142
|
+
referrer,
|
|
143
|
+
UsedByKind.NEWTYPE,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Collect from features
|
|
147
|
+
for spec in model_specs:
|
|
148
|
+
if isinstance(spec, RecordSpec):
|
|
149
|
+
collect_from_model_spec(spec, spec.identity)
|
|
150
|
+
elif isinstance(spec, UnionSpec):
|
|
151
|
+
collect_from_union_spec(spec)
|
|
152
|
+
|
|
153
|
+
# Collect from supplementary specs (enums have no outgoing references)
|
|
154
|
+
for tid, supp_spec in all_specs.items():
|
|
155
|
+
if isinstance(supp_spec, NewTypeSpec):
|
|
156
|
+
collect_from_newtype_spec(supp_spec, tid)
|
|
157
|
+
elif isinstance(supp_spec, RecordSpec):
|
|
158
|
+
collect_from_model_spec(supp_spec, tid)
|
|
159
|
+
|
|
160
|
+
# Sort into deterministic lists.
|
|
161
|
+
result: dict[TypeIdentity, list[UsedByEntry]] = {}
|
|
162
|
+
for target, ref_map in references.items():
|
|
163
|
+
entries = sorted(
|
|
164
|
+
ref_map,
|
|
165
|
+
key=lambda e: (e.kind.value, e.identity.name, e.identity.module),
|
|
166
|
+
)
|
|
167
|
+
result[target] = entries
|
|
168
|
+
|
|
169
|
+
return result
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# {{ enum.name }}
|
|
2
|
+
{% if enum.description %}
|
|
3
|
+
|
|
4
|
+
{{ enum.description | linkify_urls }}
|
|
5
|
+
{% endif %}
|
|
6
|
+
|
|
7
|
+
## Values
|
|
8
|
+
|
|
9
|
+
{% for member in enum.members -%}
|
|
10
|
+
- `{{ member.value }}`{% if member.description %} - {{ member.description }}{% endif %}
|
|
11
|
+
|
|
12
|
+
{% endfor %}
|
|
13
|
+
{% include '_used_by.md.jinja2' %}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# {{ model.name }}
|
|
2
|
+
{% if model.description %}
|
|
3
|
+
|
|
4
|
+
{{ model.description | linkify_urls }}
|
|
5
|
+
{% endif %}
|
|
6
|
+
|
|
7
|
+
## Fields
|
|
8
|
+
|
|
9
|
+
| Name | Type | Description |
|
|
10
|
+
| -----: | :----: | ------------- |
|
|
11
|
+
{% for field in fields -%}
|
|
12
|
+
| {% if field.pre_formatted %}{{ field.name }}{% else %}`{{ field.name }}`{% endif %} | {{ field.type_str }} | {% if field.description %}{{ field.description }} {% endif %}|
|
|
13
|
+
{% endfor %}
|
|
14
|
+
{% if constraints %}
|
|
15
|
+
|
|
16
|
+
## Constraints
|
|
17
|
+
|
|
18
|
+
{% for c in constraints %}
|
|
19
|
+
- {{ c }}
|
|
20
|
+
{% endfor %}
|
|
21
|
+
{% endif %}
|
|
22
|
+
{% if examples %}
|
|
23
|
+
|
|
24
|
+
## Examples
|
|
25
|
+
{% if examples|length == 1 %}
|
|
26
|
+
|
|
27
|
+
| Column | Value |
|
|
28
|
+
| -------: | ------- |
|
|
29
|
+
{% for row in examples[0] -%}
|
|
30
|
+
| `{{ row.column }}` | {{ row.value }} |
|
|
31
|
+
{% endfor %}
|
|
32
|
+
{% else %}
|
|
33
|
+
{% for example in examples %}
|
|
34
|
+
|
|
35
|
+
### Example {{ loop.index }}
|
|
36
|
+
|
|
37
|
+
| Column | Value |
|
|
38
|
+
| -------: | ------- |
|
|
39
|
+
{% for row in example -%}
|
|
40
|
+
| `{{ row.column }}` | {{ row.value }} |
|
|
41
|
+
{% endfor %}
|
|
42
|
+
{% endfor %}
|
|
43
|
+
{% endif %}
|
|
44
|
+
{% endif %}
|
|
45
|
+
{% include '_used_by.md.jinja2' %}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Geometry Types
|
|
2
|
+
|
|
3
|
+
Spatial types for representing geographic features.
|
|
4
|
+
|
|
5
|
+
## Types
|
|
6
|
+
|
|
7
|
+
| Type | Description |
|
|
8
|
+
| -----: | ------------- |
|
|
9
|
+
| `Geometry` | GeoJSON geometry value (Point, LineString, Polygon, etc.) |
|
|
10
|
+
| `BBox` | Bounding box as 4 or 6 coordinate values: [west, south, east, north] or [west, south, min-altitude, east, north, max-altitude] |
|
|
11
|
+
| `GeometryType` | Enumeration of geometry types: {{ geometry_types }} |
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# {{ newtype.name }}
|
|
2
|
+
{% if newtype.description %}
|
|
3
|
+
|
|
4
|
+
{{ newtype.description | linkify_urls }}
|
|
5
|
+
{% endif %}
|
|
6
|
+
|
|
7
|
+
Underlying type: {{ underlying_type }}
|
|
8
|
+
{% if constraints %}
|
|
9
|
+
|
|
10
|
+
## Constraints
|
|
11
|
+
|
|
12
|
+
{% for c in constraints -%}
|
|
13
|
+
- {{ c.display }}{% if c.source_link %} (from [`{{ c.source }}`]({{ c.source_link }})){% endif %}
|
|
14
|
+
|
|
15
|
+
{% endfor %}
|
|
16
|
+
{% endif %}
|
|
17
|
+
{% include '_used_by.md.jinja2' %}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Numeric Types
|
|
2
|
+
|
|
3
|
+
Numeric types used for schema field definitions.
|
|
4
|
+
|
|
5
|
+
## Integer Types
|
|
6
|
+
|
|
7
|
+
| Type | Range | Description |
|
|
8
|
+
| -----: | :-----: | ------------- |
|
|
9
|
+
{% for t in signed_ints -%}
|
|
10
|
+
| `{{ t.name }}` | {{ t.range }} | {{ t.description }} |
|
|
11
|
+
{% endfor %}
|
|
12
|
+
|
|
13
|
+
## Unsigned Integer Types
|
|
14
|
+
|
|
15
|
+
| Type | Range | Description |
|
|
16
|
+
| -----: | :-----: | ------------- |
|
|
17
|
+
{% for t in unsigned_ints -%}
|
|
18
|
+
| `{{ t.name }}` | {{ t.range }} | {{ t.description }} |
|
|
19
|
+
{% endfor %}
|
|
20
|
+
|
|
21
|
+
## Floating Point Types
|
|
22
|
+
|
|
23
|
+
| Type | Precision | Description |
|
|
24
|
+
| -----: | :---------: | ------------- |
|
|
25
|
+
{% for t in floats -%}
|
|
26
|
+
| `{{ t.name }}` | {{ t.precision }} | {{ t.description }} |
|
|
27
|
+
{% endfor %}
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"""Format `FieldShape` trees as markdown type strings with cross-page links."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from typing_extensions import assert_never
|
|
10
|
+
|
|
11
|
+
from ..extraction.field import (
|
|
12
|
+
AnyScalar,
|
|
13
|
+
ArrayOf,
|
|
14
|
+
FieldShape,
|
|
15
|
+
LiteralScalar,
|
|
16
|
+
MapOf,
|
|
17
|
+
ModelRef,
|
|
18
|
+
NewTypeShape,
|
|
19
|
+
Primitive,
|
|
20
|
+
Scalar,
|
|
21
|
+
UnionRef,
|
|
22
|
+
)
|
|
23
|
+
from ..extraction.specs import FieldSpec, TypeIdentity, is_pydantic_sourced
|
|
24
|
+
from ..extraction.type_registry import (
|
|
25
|
+
get_type_mapping,
|
|
26
|
+
is_semantic_newtype,
|
|
27
|
+
resolve_type_name,
|
|
28
|
+
)
|
|
29
|
+
from .link_computation import LinkContext
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"format_type",
|
|
33
|
+
"format_underlying_type",
|
|
34
|
+
"resolve_type_link",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _code_link(name: str, href: str) -> str:
|
|
39
|
+
"""Format a markdown link with inline-code text: `[``name``](href)`."""
|
|
40
|
+
return f"[`{name}`]({href})"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def resolve_type_link(identity: TypeIdentity, ctx: LinkContext | None = None) -> str:
|
|
44
|
+
"""Resolve a `TypeIdentity` to a linked or plain code span.
|
|
45
|
+
|
|
46
|
+
With `ctx`, links only to types in the registry (types without
|
|
47
|
+
pages render as inline code). Without context, renders as inline
|
|
48
|
+
code -- producing a link requires a placement registry to compute
|
|
49
|
+
correct relative paths.
|
|
50
|
+
"""
|
|
51
|
+
if ctx:
|
|
52
|
+
href = ctx.resolve_link(identity)
|
|
53
|
+
if href:
|
|
54
|
+
return _code_link(identity.name, href)
|
|
55
|
+
return f"`{identity.name}`"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _wrap_list_n(inner: str, depth: int) -> str:
|
|
59
|
+
"""Wrap an inner type string in `list<...>` markdown syntax *depth* times.
|
|
60
|
+
|
|
61
|
+
Builds a single broken-backtick wrapper rather than nesting
|
|
62
|
+
iteratively, since iterative nesting creates adjacent backticks
|
|
63
|
+
that CommonMark interprets as multi-backtick code span delimiters.
|
|
64
|
+
"""
|
|
65
|
+
return f"`{'list<' * depth}`{inner}`{'>' * depth}`"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _plain_list_type(base: str, depth: int) -> str:
|
|
69
|
+
"""Format a plain (unlinked) list type string for *depth* nesting levels."""
|
|
70
|
+
return f"`{'list<' * depth}{base}{'>' * depth}`"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _peel_arrays(shape: FieldShape) -> tuple[int, FieldShape]:
|
|
74
|
+
"""Strip outer `ArrayOf` layers; return (count, inner)."""
|
|
75
|
+
depth = 0
|
|
76
|
+
while isinstance(shape, ArrayOf):
|
|
77
|
+
depth += 1
|
|
78
|
+
shape = shape.element
|
|
79
|
+
return depth, shape
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _format_literal(values: tuple[object, ...]) -> str:
|
|
83
|
+
"""Format Literal values for display."""
|
|
84
|
+
if len(values) == 1:
|
|
85
|
+
return f'`"{values[0]}"`'
|
|
86
|
+
return r" \| ".join(f'`"{v}"`' for v in values)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _format_union_members(
|
|
90
|
+
members: Sequence[type[BaseModel]],
|
|
91
|
+
ctx: LinkContext | None,
|
|
92
|
+
separator: str = r" \| ",
|
|
93
|
+
) -> str:
|
|
94
|
+
r"""Format union members as individually linked / backticked names.
|
|
95
|
+
|
|
96
|
+
Each member is resolved independently so members with pages get
|
|
97
|
+
linked while others render as plain code spans. `separator` is
|
|
98
|
+
inserted between members (default is `\|` for table-cell safety).
|
|
99
|
+
"""
|
|
100
|
+
return separator.join(resolve_type_link(TypeIdentity.of(m), ctx) for m in members)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _model_ref_identity(model_ref: ModelRef) -> TypeIdentity | None:
|
|
104
|
+
"""Return a linkable identity for a `ModelRef`, or None when unsourced.
|
|
105
|
+
|
|
106
|
+
A `ModelRef` links by its `source_type` (the original Python class)
|
|
107
|
+
paired with the model name. Returns None when `source_type` is absent
|
|
108
|
+
-- a synthesized spec with no backing class has no page to link to.
|
|
109
|
+
"""
|
|
110
|
+
src = model_ref.model.source_type
|
|
111
|
+
if src is None:
|
|
112
|
+
return None
|
|
113
|
+
return TypeIdentity(src, model_ref.model.name)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _model_link(model_ref: ModelRef, ctx: LinkContext | None) -> str:
|
|
117
|
+
"""Resolve a `ModelRef` to a markdown link or fallback code span."""
|
|
118
|
+
identity = _model_ref_identity(model_ref)
|
|
119
|
+
if identity is not None:
|
|
120
|
+
return resolve_type_link(identity, ctx)
|
|
121
|
+
return f"`{model_ref.model.name}`"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _scalar_identity(scalar: Primitive) -> TypeIdentity | None:
|
|
125
|
+
"""Return a linkable identity for a `Primitive`'s `source_type`, if any.
|
|
126
|
+
|
|
127
|
+
Enum / BaseModel / Pydantic-sourced types link by their own
|
|
128
|
+
identity and class name. Class-based registered primitives
|
|
129
|
+
(`Geometry`, `BBox`) are plain classes -- not BaseModel, not
|
|
130
|
+
Pydantic-sourced -- so they link by object identity to their
|
|
131
|
+
aggregate page under the markdown registry name (`geometry`, `bbox`).
|
|
132
|
+
"""
|
|
133
|
+
src = scalar.source_type
|
|
134
|
+
if not isinstance(src, type):
|
|
135
|
+
return None
|
|
136
|
+
if issubclass(src, Enum) or issubclass(src, BaseModel) or is_pydantic_sourced(src):
|
|
137
|
+
return TypeIdentity.of(src)
|
|
138
|
+
if get_type_mapping(src.__name__) is not None:
|
|
139
|
+
return TypeIdentity(src, _registry_name(scalar))
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _scalar_display(scalar: Scalar, ctx: LinkContext | None) -> tuple[str, bool]:
|
|
144
|
+
"""Render a `Scalar` variant as a markdown string; second value is True if linked.
|
|
145
|
+
|
|
146
|
+
Linked when the scalar is a `Primitive` with an Enum / BaseModel /
|
|
147
|
+
Pydantic-sourced `source_type` whose identity resolves to a page.
|
|
148
|
+
Otherwise renders as the registry-resolved markdown name.
|
|
149
|
+
"""
|
|
150
|
+
if isinstance(scalar, Primitive):
|
|
151
|
+
identity = _scalar_identity(scalar)
|
|
152
|
+
if identity is not None and ctx:
|
|
153
|
+
href = ctx.resolve_link(identity)
|
|
154
|
+
if href:
|
|
155
|
+
return _code_link(identity.name, href), True
|
|
156
|
+
if identity is not None:
|
|
157
|
+
return f"`{identity.name}`", False
|
|
158
|
+
return f"`{_registry_name(scalar)}`", False
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _registry_name(scalar: Scalar) -> str:
|
|
162
|
+
"""Resolve a scalar to its markdown registry name (e.g. `int64`)."""
|
|
163
|
+
if isinstance(scalar, LiteralScalar):
|
|
164
|
+
return "Literal"
|
|
165
|
+
if isinstance(scalar, AnyScalar):
|
|
166
|
+
return "Any"
|
|
167
|
+
mapping = get_type_mapping(scalar.base_type)
|
|
168
|
+
if mapping is None and scalar.source_type is not None:
|
|
169
|
+
mapping = get_type_mapping(scalar.source_type.__name__)
|
|
170
|
+
if mapping is not None:
|
|
171
|
+
return mapping.markdown
|
|
172
|
+
return scalar.base_type
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _format_map(shape: MapOf, ctx: LinkContext | None) -> str:
|
|
176
|
+
"""Format a `MapOf` as a `map<K, V>` code span, linking key/value types.
|
|
177
|
+
|
|
178
|
+
Semantic NewTypes and Enum / BaseModel-sourced key/value types link
|
|
179
|
+
to their pages; primitives stay bare. Output is identical whether the
|
|
180
|
+
map is rendered in a field cell or as a NewType's underlying type --
|
|
181
|
+
both paths route through here.
|
|
182
|
+
|
|
183
|
+
A link has to break out of the surrounding code span, so any bare side
|
|
184
|
+
is folded into the adjacent `map<...>` span rather than wrapped in its
|
|
185
|
+
own backticks. Two backtick spans must never abut: CommonMark reads the
|
|
186
|
+
resulting `` as a two-backtick delimiter and swallows the link.
|
|
187
|
+
"""
|
|
188
|
+
key_str, key_linked = _map_side(shape.key, ctx)
|
|
189
|
+
val_str, val_linked = _map_side(shape.value, ctx)
|
|
190
|
+
if not key_linked and not val_linked:
|
|
191
|
+
return f"`map<{key_str}, {val_str}>`"
|
|
192
|
+
if key_linked and val_linked:
|
|
193
|
+
return f"`map<`{key_str}`,`{val_str}`>`"
|
|
194
|
+
if key_linked:
|
|
195
|
+
return f"`map<`{key_str}`,{val_str}>`"
|
|
196
|
+
return f"`map<{key_str},`{val_str}`>`"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _map_side(shape: FieldShape, ctx: LinkContext | None) -> tuple[str, bool]:
|
|
200
|
+
"""Render one map key/value as (text, is_link).
|
|
201
|
+
|
|
202
|
+
Returns a page link when the side resolves to one, else its
|
|
203
|
+
container-aware bare name (so a `list<...>` / `map<...>` wrapper
|
|
204
|
+
survives instead of collapsing to its element). The flag tells
|
|
205
|
+
`_format_map` whether the side breaks out of the surrounding code span.
|
|
206
|
+
"""
|
|
207
|
+
link = _map_side_link(shape, ctx)
|
|
208
|
+
if link is not None:
|
|
209
|
+
return link, True
|
|
210
|
+
return _bare_map_side_name(shape), False
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _map_side_link(shape: FieldShape, ctx: LinkContext | None) -> str | None:
|
|
214
|
+
"""Return a markdown link for a map key/value that has its own page.
|
|
215
|
+
|
|
216
|
+
Links a semantic NewType, a model (`ModelRef`), or a primitive whose
|
|
217
|
+
`source_type` is a linkable identity (`_scalar_identity`), when `ctx`
|
|
218
|
+
resolves a page for it. NewType and primitive sides link through
|
|
219
|
+
`list<...>` layers; a model side links only when it is the direct map
|
|
220
|
+
side (`depth == 0`), so a `list<Model>`-valued map keeps its `list<...>`
|
|
221
|
+
wrapper from `_bare_map_side_name` rather than collapsing to a bare model
|
|
222
|
+
link. Returns None when the side has no page; the caller renders a bare
|
|
223
|
+
name instead.
|
|
224
|
+
"""
|
|
225
|
+
identity: TypeIdentity | None = None
|
|
226
|
+
depth, cur = _peel_arrays(shape)
|
|
227
|
+
if isinstance(cur, NewTypeShape) and is_semantic_newtype(shape):
|
|
228
|
+
identity = TypeIdentity(cur.ref, cur.name)
|
|
229
|
+
elif depth == 0 and isinstance(cur, ModelRef):
|
|
230
|
+
identity = _model_ref_identity(cur)
|
|
231
|
+
elif isinstance(cur, Primitive):
|
|
232
|
+
identity = _scalar_identity(cur)
|
|
233
|
+
if identity and ctx:
|
|
234
|
+
href = ctx.resolve_link(identity)
|
|
235
|
+
if href:
|
|
236
|
+
return _code_link(identity.name, href)
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _bare_map_side_name(shape: FieldShape) -> str:
|
|
241
|
+
r"""Bare markdown name for a map key/value, recursing through containers.
|
|
242
|
+
|
|
243
|
+
Every variant resolves to a real name: `list<...>` / `map<...>`
|
|
244
|
+
wrappers recurse, scalars use their registry name (so `Any` is `Any`,
|
|
245
|
+
not `?`), semantic NewTypes and models use their type name, and a
|
|
246
|
+
pass-through NewType resolves through the registry like the scalar it
|
|
247
|
+
aliases. There is no `?` fallback -- a side that can't be named is a
|
|
248
|
+
bug, not a placeholder.
|
|
249
|
+
|
|
250
|
+
A union-valued map is the one shape left unrendered: no schema field
|
|
251
|
+
uses one, and its `\|`-separated members do not compose cleanly into
|
|
252
|
+
a bare `map<...>` span. It raises so the gap surfaces loudly when a
|
|
253
|
+
field first needs it, rather than shipping a half-rendered value.
|
|
254
|
+
"""
|
|
255
|
+
match shape:
|
|
256
|
+
case ArrayOf(element=element):
|
|
257
|
+
return f"list<{_bare_map_side_name(element)}>"
|
|
258
|
+
case MapOf(key=key, value=value):
|
|
259
|
+
return f"map<{_bare_map_side_name(key)}, {_bare_map_side_name(value)}>"
|
|
260
|
+
case NewTypeShape(name=name) if is_semantic_newtype(shape):
|
|
261
|
+
return name
|
|
262
|
+
case NewTypeShape():
|
|
263
|
+
return resolve_type_name(shape)
|
|
264
|
+
case ModelRef(model=model):
|
|
265
|
+
return model.name
|
|
266
|
+
case Primitive() | LiteralScalar() | AnyScalar():
|
|
267
|
+
return _registry_name(shape)
|
|
268
|
+
case UnionRef():
|
|
269
|
+
raise NotImplementedError(
|
|
270
|
+
"union-typed map key/value is not rendered in markdown; "
|
|
271
|
+
"add handling here when a schema field first needs one"
|
|
272
|
+
)
|
|
273
|
+
case _:
|
|
274
|
+
assert_never(shape)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def format_type(field: FieldSpec, ctx: LinkContext | None = None) -> str:
|
|
278
|
+
"""Format a field's type for markdown display, with links and qualifiers."""
|
|
279
|
+
qualifiers: list[str] = []
|
|
280
|
+
display = _format_shape(field.shape, ctx, qualifiers)
|
|
281
|
+
if not field.is_required:
|
|
282
|
+
qualifiers.append("optional")
|
|
283
|
+
if qualifiers:
|
|
284
|
+
return f"{display} ({', '.join(qualifiers)})"
|
|
285
|
+
return display
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _format_shape(
|
|
289
|
+
shape: FieldShape, ctx: LinkContext | None, qualifiers: list[str]
|
|
290
|
+
) -> str:
|
|
291
|
+
"""Format a `FieldShape`, possibly appending qualifiers like `list`, `map`."""
|
|
292
|
+
outer_depth, inner = _peel_arrays(shape)
|
|
293
|
+
|
|
294
|
+
match inner:
|
|
295
|
+
case LiteralScalar(values=values):
|
|
296
|
+
if outer_depth > 0:
|
|
297
|
+
inside = " | ".join(f'"{v}"' for v in values)
|
|
298
|
+
return _plain_list_type(inside, outer_depth)
|
|
299
|
+
return _format_literal(values)
|
|
300
|
+
|
|
301
|
+
case UnionRef(union=u):
|
|
302
|
+
if outer_depth > 0:
|
|
303
|
+
qualifiers.append("list")
|
|
304
|
+
return _format_union_members(u.members, ctx)
|
|
305
|
+
|
|
306
|
+
case MapOf() as m:
|
|
307
|
+
map_str = _format_map(m, ctx)
|
|
308
|
+
if outer_depth > 0:
|
|
309
|
+
return _wrap_list_n(map_str.strip("`"), outer_depth)
|
|
310
|
+
return map_str
|
|
311
|
+
|
|
312
|
+
case ModelRef() as m:
|
|
313
|
+
link = _model_link(m, ctx)
|
|
314
|
+
if outer_depth > 0:
|
|
315
|
+
return _wrap_list_n(link, outer_depth)
|
|
316
|
+
return link
|
|
317
|
+
|
|
318
|
+
case NewTypeShape(name=name, ref=ref, inner=nt_inner):
|
|
319
|
+
link = resolve_type_link(TypeIdentity(ref, name), ctx)
|
|
320
|
+
if outer_depth > 0:
|
|
321
|
+
return _wrap_list_n(link, outer_depth)
|
|
322
|
+
if isinstance(nt_inner, ArrayOf):
|
|
323
|
+
qualifiers.append("list")
|
|
324
|
+
elif isinstance(nt_inner, MapOf):
|
|
325
|
+
qualifiers.append("map")
|
|
326
|
+
return link
|
|
327
|
+
|
|
328
|
+
case Primitive() | AnyScalar() as s:
|
|
329
|
+
text, linked = _scalar_display(s, ctx)
|
|
330
|
+
if outer_depth > 0:
|
|
331
|
+
if linked:
|
|
332
|
+
return _wrap_list_n(text, outer_depth)
|
|
333
|
+
return _plain_list_type(text.strip("`"), outer_depth)
|
|
334
|
+
return text
|
|
335
|
+
|
|
336
|
+
raise TypeError(f"Unhandled FieldShape: {shape!r}")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# ---- Underlying-type rendering for NewType pages ----
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _peel_to_terminal(shape: FieldShape) -> FieldShape:
|
|
343
|
+
"""Strip `NewTypeShape` / `ArrayOf` layers to find the terminal shape."""
|
|
344
|
+
while True:
|
|
345
|
+
if isinstance(shape, NewTypeShape):
|
|
346
|
+
shape = shape.inner
|
|
347
|
+
elif isinstance(shape, ArrayOf):
|
|
348
|
+
shape = shape.element
|
|
349
|
+
else:
|
|
350
|
+
return shape
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def format_underlying_type(shape: FieldShape, ctx: LinkContext | None = None) -> str:
|
|
354
|
+
"""Format a NewType's underlying type for the page header, with links."""
|
|
355
|
+
terminal = _peel_to_terminal(shape)
|
|
356
|
+
if isinstance(terminal, UnionRef):
|
|
357
|
+
return _format_union_members(terminal.union.members, ctx, separator=" | ")
|
|
358
|
+
|
|
359
|
+
if isinstance(terminal, MapOf):
|
|
360
|
+
return _format_map(terminal, ctx)
|
|
361
|
+
|
|
362
|
+
# Link by the terminal primitive's identity, not the enclosing NewType's:
|
|
363
|
+
# this shape belongs to the NewType being rendered, so linking its own
|
|
364
|
+
# identity would self-link. The terminal's identity is always its
|
|
365
|
+
# underlying primitive (Geometry/BBox, a pydantic type, etc.).
|
|
366
|
+
identity: TypeIdentity | None = None
|
|
367
|
+
if isinstance(terminal, Primitive):
|
|
368
|
+
identity = _scalar_identity(terminal)
|
|
369
|
+
|
|
370
|
+
depth, _ = _peel_arrays(shape)
|
|
371
|
+
|
|
372
|
+
if identity and ctx:
|
|
373
|
+
href = ctx.resolve_link(identity)
|
|
374
|
+
if href:
|
|
375
|
+
linked = _code_link(identity.name, href)
|
|
376
|
+
if depth > 0:
|
|
377
|
+
return _wrap_list_n(linked, depth)
|
|
378
|
+
return linked
|
|
379
|
+
|
|
380
|
+
base = identity.name if identity else resolve_type_name(shape)
|
|
381
|
+
if depth > 0:
|
|
382
|
+
return _plain_list_type(base, depth)
|
|
383
|
+
return f"`{base}`"
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""PySpark codegen pipeline: ModelSpec to expression and test modules."""
|