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,187 @@
|
|
|
1
|
+
"""Build StructType schema source from ModelSpec field trees."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from ..extraction.field import (
|
|
8
|
+
AnyScalar,
|
|
9
|
+
ArrayOf,
|
|
10
|
+
FieldShape,
|
|
11
|
+
LiteralScalar,
|
|
12
|
+
MapOf,
|
|
13
|
+
ModelRef,
|
|
14
|
+
NewTypeShape,
|
|
15
|
+
Primitive,
|
|
16
|
+
Scalar,
|
|
17
|
+
UnionRef,
|
|
18
|
+
)
|
|
19
|
+
from ..extraction.field_walk import enum_source
|
|
20
|
+
from ..extraction.specs import FieldSpec, ModelSpec, UnionSpec
|
|
21
|
+
from ..extraction.type_registry import get_type_mapping
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"SHARED_TYPE_REFS",
|
|
25
|
+
"SchemaField",
|
|
26
|
+
"build_schema",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# Types whose base_type name maps to a _schema_structs.py StructType constant.
|
|
30
|
+
# Reserved for types the codegen cannot walk (BBox is a plain class, not a
|
|
31
|
+
# Pydantic BaseModel). Pydantic BaseModels are inlined.
|
|
32
|
+
SHARED_TYPE_REFS: dict[str, str] = {
|
|
33
|
+
"BBox": "BBOX_STRUCT",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Literal and Enum fields both serialize as strings in Parquet.
|
|
37
|
+
_STRING_FALLBACK = "StringType()"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class SchemaField:
|
|
42
|
+
"""One field in the generated StructType.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
name
|
|
47
|
+
Column name.
|
|
48
|
+
type_expr
|
|
49
|
+
Spark type expression string (e.g. `"StringType()"`) or
|
|
50
|
+
a `_schema_structs.py` constant name.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
name: str
|
|
54
|
+
type_expr: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _spark_for_base(base_type: str, source_type: type | None) -> str:
|
|
58
|
+
"""Return a Spark type expression for a primitive base type.
|
|
59
|
+
|
|
60
|
+
Tries `base_type` first, then falls back to `source_type.__name__`.
|
|
61
|
+
Returns `StringType()` when neither maps to a known Spark type.
|
|
62
|
+
"""
|
|
63
|
+
mapping = get_type_mapping(base_type)
|
|
64
|
+
if mapping is not None and mapping.spark is not None:
|
|
65
|
+
return mapping.spark
|
|
66
|
+
if source_type is not None:
|
|
67
|
+
fallback = get_type_mapping(source_type.__name__)
|
|
68
|
+
if fallback is not None and fallback.spark is not None:
|
|
69
|
+
return fallback.spark
|
|
70
|
+
return _STRING_FALLBACK
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _spark_for_scalar(scalar: Scalar) -> str:
|
|
74
|
+
"""Map a `Scalar` variant to a Spark type expression.
|
|
75
|
+
|
|
76
|
+
`LiteralScalar` and `AnyScalar` serialize as strings. `Primitive`
|
|
77
|
+
scalars look up the type registry; enum primitives and BBox short-
|
|
78
|
+
circuit to strings / shared constants before the registry.
|
|
79
|
+
"""
|
|
80
|
+
if isinstance(scalar, (LiteralScalar, AnyScalar)):
|
|
81
|
+
return _STRING_FALLBACK
|
|
82
|
+
if scalar.base_type in SHARED_TYPE_REFS:
|
|
83
|
+
return SHARED_TYPE_REFS[scalar.base_type]
|
|
84
|
+
if enum_source(scalar) is not None:
|
|
85
|
+
return _STRING_FALLBACK
|
|
86
|
+
return _spark_for_base(scalar.base_type, scalar.source_type)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _deduplicate_by_name(fields: list[FieldSpec]) -> list[FieldSpec]:
|
|
90
|
+
"""Keep one FieldSpec per name, requiring every arm to agree on Spark type.
|
|
91
|
+
|
|
92
|
+
Union annotated_fields may contain the same field name declared by
|
|
93
|
+
multiple arms with different `FieldSpec`s -- a `Literal` discriminator
|
|
94
|
+
whose value differs per arm, or a field whose per-arm constraints
|
|
95
|
+
diverge (see `union_extraction.extract_union`). A columnar sink stores
|
|
96
|
+
one type per column name, so the schema needs exactly one entry. Two
|
|
97
|
+
same-named fields are compatible when they resolve to the SAME Spark
|
|
98
|
+
type -- the first-seen `FieldSpec`'s shape is kept (arbitrarily; the
|
|
99
|
+
column type is identical either way). Two same-named fields that resolve
|
|
100
|
+
to DIFFERENT Spark types cannot share one generated column, so this
|
|
101
|
+
always raises, whether the mismatch is numeric (a narrower int type vs a
|
|
102
|
+
float) or not.
|
|
103
|
+
|
|
104
|
+
Widening the two to their common type would often work in practice --
|
|
105
|
+
Spark and Parquet can promote a narrower numeric column to a wider one
|
|
106
|
+
(reading an int where the schema declares a double, say). It is forbidden
|
|
107
|
+
anyway: a widened column makes the union's type an implicit property
|
|
108
|
+
inferred from whichever arms happen to disagree, rather than a decision
|
|
109
|
+
stated in the model. Raising forces that decision to the surface at
|
|
110
|
+
generation instead of leaving it as a silent compatibility trap.
|
|
111
|
+
"""
|
|
112
|
+
seen: dict[str, FieldSpec] = {}
|
|
113
|
+
for f in fields:
|
|
114
|
+
existing = seen.get(f.name)
|
|
115
|
+
if existing is None:
|
|
116
|
+
seen[f.name] = f
|
|
117
|
+
continue
|
|
118
|
+
spark_f, spark_existing = (
|
|
119
|
+
_shape_to_spark(f.shape),
|
|
120
|
+
_shape_to_spark(existing.shape),
|
|
121
|
+
)
|
|
122
|
+
if spark_f != spark_existing:
|
|
123
|
+
raise ValueError(
|
|
124
|
+
f"Union field {f.name!r} resolves to incompatible Spark "
|
|
125
|
+
f"types across arms ({spark_existing} vs {spark_f}); a "
|
|
126
|
+
"single Parquet column cannot represent both."
|
|
127
|
+
)
|
|
128
|
+
return list(seen.values())
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _struct_type_expr(fields: list[FieldSpec]) -> str:
|
|
132
|
+
"""Build an inline `StructType([...])` expression from a list of fields."""
|
|
133
|
+
if not fields:
|
|
134
|
+
raise ValueError(
|
|
135
|
+
"Cannot build a StructType for a model with no fields; an empty "
|
|
136
|
+
"struct column cannot carry data and signals an upstream "
|
|
137
|
+
"extraction problem."
|
|
138
|
+
)
|
|
139
|
+
parts = [
|
|
140
|
+
f'StructField("{f.name}", {_shape_to_spark(f.shape)}, True)' for f in fields
|
|
141
|
+
]
|
|
142
|
+
return f"StructType([{', '.join(parts)}])"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _shape_to_spark(shape: FieldShape) -> str:
|
|
146
|
+
"""Convert a FieldShape to a Spark type expression string."""
|
|
147
|
+
match shape:
|
|
148
|
+
case ArrayOf(element=element):
|
|
149
|
+
return f"ArrayType({_shape_to_spark(element)}, True)"
|
|
150
|
+
case NewTypeShape(inner=inner):
|
|
151
|
+
return _shape_to_spark(inner)
|
|
152
|
+
case ModelRef(model=m):
|
|
153
|
+
return _struct_type_expr(m.fields)
|
|
154
|
+
case UnionRef(union=u):
|
|
155
|
+
return _struct_type_expr(_deduplicate_by_name(u.fields))
|
|
156
|
+
case MapOf(key=k, value=v):
|
|
157
|
+
return f"MapType({_shape_to_spark(k)}, {_shape_to_spark(v)}, True)"
|
|
158
|
+
case Primitive() | LiteralScalar() | AnyScalar() as s:
|
|
159
|
+
return _spark_for_scalar(s)
|
|
160
|
+
raise TypeError(f"Unhandled FieldShape: {shape!r}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def build_schema(spec: ModelSpec) -> list[SchemaField]:
|
|
164
|
+
"""Build schema fields for a feature spec.
|
|
165
|
+
|
|
166
|
+
Walks the field tree and maps types to Spark type expressions.
|
|
167
|
+
Recognizes shared types and emits fields in model order.
|
|
168
|
+
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
spec
|
|
172
|
+
The feature spec to build schema fields for.
|
|
173
|
+
|
|
174
|
+
Returns
|
|
175
|
+
-------
|
|
176
|
+
list[SchemaField]
|
|
177
|
+
One entry per schema column in model order.
|
|
178
|
+
"""
|
|
179
|
+
source_fields = (
|
|
180
|
+
_deduplicate_by_name(spec.fields)
|
|
181
|
+
if isinstance(spec, UnionSpec)
|
|
182
|
+
else spec.fields
|
|
183
|
+
)
|
|
184
|
+
return [
|
|
185
|
+
SchemaField(name=f.name, type_expr=_shape_to_spark(f.shape))
|
|
186
|
+
for f in source_fields
|
|
187
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{%- macro check_function(c) -%}
|
|
2
|
+
def {{ c.func_name }}() -> Check:
|
|
3
|
+
return Check(
|
|
4
|
+
field={{ c.field | py_literal }},
|
|
5
|
+
name={{ c.check_name | py_literal }},
|
|
6
|
+
expr={{ c.expr }},
|
|
7
|
+
shape=CheckShape.{{ c.shape }},
|
|
8
|
+
read_columns={{ c.read_columns | py_literal }},
|
|
9
|
+
)
|
|
10
|
+
{% endmacro %}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{% from '_check_function.py.jinja2' import check_function -%}
|
|
2
|
+
# This file is auto-generated by overture-schema-codegen. Do not edit.
|
|
3
|
+
|
|
4
|
+
"""{{ model_title }} validation expression builders."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pyspark.sql import functions as F
|
|
9
|
+
{% if spark_types %}
|
|
10
|
+
from pyspark.sql.types import (
|
|
11
|
+
{% for t in spark_types %}
|
|
12
|
+
{{ t }},
|
|
13
|
+
{% endfor %}
|
|
14
|
+
)
|
|
15
|
+
{% endif %}
|
|
16
|
+
{% if geometry_type %}
|
|
17
|
+
from overture.schema.system.geometric import GeometryType
|
|
18
|
+
|
|
19
|
+
{% endif %}
|
|
20
|
+
from overture.schema.pyspark.check import Check, CheckShape, ModelValidation
|
|
21
|
+
{% if schema_struct_refs %}
|
|
22
|
+
from overture.schema.pyspark.expressions._schema_structs import (
|
|
23
|
+
{% for r in schema_struct_refs %}
|
|
24
|
+
{{ r }},
|
|
25
|
+
{% endfor %}
|
|
26
|
+
)
|
|
27
|
+
{% endif %}
|
|
28
|
+
{% if column_pattern_fns %}
|
|
29
|
+
from overture.schema.pyspark.expressions.column_patterns import (
|
|
30
|
+
{% for f in column_pattern_fns %}
|
|
31
|
+
{{ f }},
|
|
32
|
+
{% endfor %}
|
|
33
|
+
)
|
|
34
|
+
{% endif %}
|
|
35
|
+
{% if constraint_expr_fns %}
|
|
36
|
+
from overture.schema.pyspark.expressions.constraint_expressions import (
|
|
37
|
+
{% for f in constraint_expr_fns %}
|
|
38
|
+
{{ f }},
|
|
39
|
+
{% endfor %}
|
|
40
|
+
)
|
|
41
|
+
{% endif %}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
{% for c in check_functions %}
|
|
45
|
+
{{ check_function(c) }}
|
|
46
|
+
{% endfor %}
|
|
47
|
+
|
|
48
|
+
def {{ model_name }}_checks() -> list[Check]:
|
|
49
|
+
"""All validation checks for {{ model_name }}."""
|
|
50
|
+
{% if check_functions %}
|
|
51
|
+
return [
|
|
52
|
+
{% for c in check_functions %}
|
|
53
|
+
{{ c.func_name }}(),
|
|
54
|
+
{% endfor %}
|
|
55
|
+
]
|
|
56
|
+
{% else %}
|
|
57
|
+
return []
|
|
58
|
+
{% endif %}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
{{ schema_const_name }} = StructType(
|
|
62
|
+
[
|
|
63
|
+
{%- for sf in schema_fields %}
|
|
64
|
+
StructField("{{ sf.name }}", {{ sf.type_expr }}, True),
|
|
65
|
+
{%- endfor %}
|
|
66
|
+
]
|
|
67
|
+
)
|
|
68
|
+
{% if geometry_types_literal %}
|
|
69
|
+
|
|
70
|
+
GEOMETRY_TYPES: tuple[GeometryType, ...] = {{ geometry_types_literal }}
|
|
71
|
+
{% endif %}
|
|
72
|
+
|
|
73
|
+
ENTRY_POINT = "{{ entry_point }}"
|
|
74
|
+
|
|
75
|
+
PARTITIONS: dict[str, str] = {{ partitions | py_literal }}
|
|
76
|
+
|
|
77
|
+
MODEL_VALIDATION = ModelValidation(
|
|
78
|
+
schema={{ schema_const_name }},
|
|
79
|
+
checks={{ model_name }}_checks,
|
|
80
|
+
{%- if geometry_types_literal %}
|
|
81
|
+
geometry_types=GEOMETRY_TYPES,
|
|
82
|
+
{%- endif %}
|
|
83
|
+
)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Auto-generated — do not edit.
|
|
2
|
+
|
|
3
|
+
"""Generated conformance tests for {{ model_name }}."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from {{ expression_import }} import (
|
|
9
|
+
{{ model_name | upper }}_SCHEMA,
|
|
10
|
+
{{ model_name }}_checks,
|
|
11
|
+
)
|
|
12
|
+
from pyspark.sql import SparkSession
|
|
13
|
+
|
|
14
|
+
from _support.harness import (
|
|
15
|
+
ValidationResults,
|
|
16
|
+
run_validation_pipeline,
|
|
17
|
+
)
|
|
18
|
+
{% if mutation_imports %}
|
|
19
|
+
from _support.mutations import {{ mutation_imports | join(", ") }}
|
|
20
|
+
{% endif %}
|
|
21
|
+
{% if needs_set_at_path %}
|
|
22
|
+
from _support.helpers import set_at_path
|
|
23
|
+
{% endif %}
|
|
24
|
+
from _support.scenarios import Scenario
|
|
25
|
+
|
|
26
|
+
BASE_ROW_SPARSE: dict = {{ base_row_sparse }}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
BASE_ROW_POPULATED: dict = {{ base_row_populated }}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
SCENARIOS: list[Scenario] = [
|
|
33
|
+
{% for entry in scenarios %}
|
|
34
|
+
Scenario(
|
|
35
|
+
{% for k, v in entry %}
|
|
36
|
+
{{ k }}={{ v }},
|
|
37
|
+
{% endfor %}
|
|
38
|
+
),
|
|
39
|
+
{% endfor %}
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.fixture(scope="module")
|
|
44
|
+
def checks() -> list:
|
|
45
|
+
return {{ model_name }}_checks()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.fixture(scope="module")
|
|
49
|
+
def sparse_results(spark: SparkSession, checks: list) -> ValidationResults:
|
|
50
|
+
return run_validation_pipeline(
|
|
51
|
+
spark,
|
|
52
|
+
{{ schema_name }},
|
|
53
|
+
checks,
|
|
54
|
+
BASE_ROW_SPARSE,
|
|
55
|
+
SCENARIOS,
|
|
56
|
+
model_name="{{ model_name }}",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.fixture(scope="module")
|
|
61
|
+
def populated_results(spark: SparkSession, checks: list) -> ValidationResults:
|
|
62
|
+
return run_validation_pipeline(
|
|
63
|
+
spark,
|
|
64
|
+
{{ schema_name }},
|
|
65
|
+
checks,
|
|
66
|
+
BASE_ROW_POPULATED,
|
|
67
|
+
SCENARIOS,
|
|
68
|
+
model_name="{{ model_name }}",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_baseline_sparse(sparse_results: ValidationResults) -> None:
|
|
73
|
+
"""Sparse base row passes every check the codegen produced.
|
|
74
|
+
|
|
75
|
+
Catches drift between base_row synthesis, schema_builder, and
|
|
76
|
+
check_builder -- if any of those produce output inconsistent with
|
|
77
|
+
the others (e.g. a check that rejects values the synthesizer emits
|
|
78
|
+
for required-only fields), the baseline fails here before any
|
|
79
|
+
scenario runs.
|
|
80
|
+
"""
|
|
81
|
+
baseline = sparse_results.violations.get("{{ model_name }}::baseline", set())
|
|
82
|
+
assert baseline == set(), f"Sparse baseline has violations: {baseline}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_baseline_populated(populated_results: ValidationResults) -> None:
|
|
86
|
+
"""Fully-populated base row passes every check the codegen produced.
|
|
87
|
+
|
|
88
|
+
Mirrors `test_baseline_sparse` but with all optional fields
|
|
89
|
+
filled, exercising codegen paths that only fire when a value is
|
|
90
|
+
present.
|
|
91
|
+
"""
|
|
92
|
+
baseline = populated_results.violations.get("{{ model_name }}::baseline", set())
|
|
93
|
+
assert baseline == set(), f"Populated baseline has violations: {baseline}"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@pytest.mark.parametrize("scenario", SCENARIOS, ids=lambda s: s.id)
|
|
97
|
+
def test_scenario_sparse(
|
|
98
|
+
scenario: Scenario,
|
|
99
|
+
sparse_results: ValidationResults,
|
|
100
|
+
) -> None:
|
|
101
|
+
_assert_scenario(scenario, sparse_results)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@pytest.mark.parametrize("scenario", SCENARIOS, ids=lambda s: s.id)
|
|
105
|
+
def test_scenario_populated(
|
|
106
|
+
scenario: Scenario,
|
|
107
|
+
populated_results: ValidationResults,
|
|
108
|
+
) -> None:
|
|
109
|
+
_assert_scenario(scenario, populated_results)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _assert_scenario(
|
|
113
|
+
scenario: Scenario,
|
|
114
|
+
validation_results: ValidationResults,
|
|
115
|
+
) -> None:
|
|
116
|
+
expected = (scenario.expected_field, scenario.expected_check)
|
|
117
|
+
if scenario.id in validation_results.skipped:
|
|
118
|
+
# An unbuildable scenario exercises nothing; fail loud rather than skip
|
|
119
|
+
# (a skip reads as a pass and hides codegen/scaffold gaps).
|
|
120
|
+
pytest.fail(
|
|
121
|
+
f"unbuildable scenario {scenario.id!r}: "
|
|
122
|
+
f"{validation_results.skipped[scenario.id]}"
|
|
123
|
+
)
|
|
124
|
+
valid_violations = validation_results.violations.get(f"{scenario.id}::valid", set())
|
|
125
|
+
assert expected not in valid_violations
|
|
126
|
+
invalid_violations = validation_results.violations.get(
|
|
127
|
+
f"{scenario.id}::invalid", set()
|
|
128
|
+
)
|
|
129
|
+
assert expected in invalid_violations
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Test-data generation for the rendered PySpark conformance tests.
|
|
2
|
+
|
|
3
|
+
Three modules cover three flavors of data:
|
|
4
|
+
|
|
5
|
+
- `invalid_value`: constraint-violating values for triggering each check.
|
|
6
|
+
- `base_row`: minimal and fully populated valid rows.
|
|
7
|
+
- `scaffold`: sparse path scaffolds that supply the nested intermediates
|
|
8
|
+
(optional structs, arrays) a check's field path requires.
|
|
9
|
+
"""
|