Conversation
| if TYPE_CHECKING: | ||
| from fglatch.registry._record_model import LatchRecordModel |
| def _is_nullable(annotation: Any) -> bool: | ||
| """True if the annotation is a `T | None` or `Optional[T]` / `Union[..., None]`.""" | ||
| origin = get_origin(annotation) | ||
| if origin is not Union and origin is not UnionType: | ||
| return False | ||
| return type(None) in get_args(annotation) |
There was a problem hiding this comment.
Is this the same as https://github.com/fg-labs/fgmetric/blob/9af9921a5a6dcb5b4f45fdf6259d1b2d8eb1887d/fgmetric/_typing_extensions.py#L24-L58 ?
Can we reuse? I can export is_optional in fgmetric to make it part of the public API.
| def _unwrap_none(annotation: Any) -> Any: | ||
| """ | ||
| Given `T | None`, return `T`. | ||
|
|
||
| Returns the annotation unchanged when it is not a simple `T | None` (e.g. tagged | ||
| unions like `A | B | None`, which are out of scope for v1). | ||
| """ | ||
| if not _is_nullable(annotation): | ||
| return annotation | ||
| non_none_args = [a for a in get_args(annotation) if a is not type(None)] | ||
| if len(non_none_args) != 1: | ||
| return annotation | ||
| return non_none_args[0] |
| def _describe_type(annotation: Any) -> str: | ||
| """Produce a short human-readable string for a type annotation.""" | ||
| if isinstance(annotation, type): | ||
| return annotation.__name__ | ||
| return str(annotation).replace("typing.", "") |
There was a problem hiding this comment.
| def _describe_type(annotation: Any) -> str: | |
| """Produce a short human-readable string for a type annotation.""" | |
| if isinstance(annotation, type): | |
| return annotation.__name__ | |
| return str(annotation).replace("typing.", "") |
Remove this - I'd rather have the literal annotation preserved. Explicit is better than implicit and I don't want the annotations formatted inconsistently depending on their parent module.
| expected_type: Any, | ||
| actual_type: Any, | ||
| *, | ||
| message: str, |
There was a problem hiding this comment.
I think in #47 we are going to make this a property on SchemaMismatch - unless we need to override it in some cases?
There was a problem hiding this comment.
Perhaps custom formatting could be dispatched through the kind Enum being added in #47?
|
|
||
|
|
||
| def _validate_table_schema( | ||
| model_cls: "type[LatchRecordModel]", |
| `_validate_table_schema`. | ||
| """ | ||
| model_is_nullable = _is_nullable(model_annotation) | ||
| column_is_nullable: bool = column.upstream_type["allowEmpty"] |
| field_name: str, | ||
| expected_type: Any, | ||
| actual_type: Any, | ||
| ) -> list[SchemaMismatch]: |
Adds the schema-validation entry point and the enumeration-only error paths (`missing_on_table`, `missing_on_model`). Per-field type comparison is layered on in the next commit; this PR is intentionally scoped to "do columns line up", not "do their types agree". - `_validate_table_schema(model_cls, table, *, allow_extra_columns)` iterates the model's `model_fields` (excluding the base `id` / `name`) against the table's columns. Reports a `MISSING_ON_TABLE` mismatch for any model field with no matching column. When `allow_extra_columns=False`, reports `MISSING_ON_MODEL` for any column the model does not declare. Fields present on both sides are silently passed through — the next commit fills in the comparison. - `model_type` is sourced from `info.annotation` and is typed `TypeAnnotation` (from fgmetric) — no string-stringification. `column_type` is the SDK-resolved Python type on the `Column`. Per @msto's review: this PR (originally #48) is now split. The internal helper still returns `list[SchemaMismatch]` for test introspection; the classmethod that raises lands in #49 and is a thin wrapper that calls the helper and raises if the list is non-empty. Related to #42. Tracked at #53.
80a88d3 to
2255051
Compare
Adds the schema-validation entry point and the enumeration-only error paths (`missing_on_table`, `missing_on_model`). Per-field type comparison is layered on in the next commit; this PR is intentionally scoped to "do columns line up", not "do their types agree". - `_validate_table_schema(model_cls, table, *, allow_extra_columns)` iterates the model's `model_fields` (excluding the base `id` / `name`) against the table's columns. Reports a `MISSING_ON_TABLE` mismatch for any model field with no matching column. When `allow_extra_columns=False`, reports `MISSING_ON_MODEL` for any column the model does not declare. Fields present on both sides are silently passed through — the next commit fills in the comparison. - `model_type` is sourced from `info.annotation` and is typed `TypeAnnotation` (from fgmetric) — no string-stringification. `column_type` is the SDK-resolved Python type on the `Column`. Per @msto's review: this PR (originally #48) is now split. The internal helper still returns `list[SchemaMismatch]` for test introspection; the classmethod that raises lands in #49 and is a thin wrapper that calls the helper and raises if the list is non-empty. Related to #42. Tracked at #53.
2255051 to
478b377
Compare
Adds the schema-validation entry point and the enumeration-only error paths (`missing_on_table`, `missing_on_model`). Per-field type comparison is layered on in the next commit; this PR is intentionally scoped to "do columns line up", not "do their types agree". - `_validate_table_schema(model_cls, table, *, allow_extra_columns)` iterates the model's `model_fields` (excluding the base `id` / `name`) against the table's columns. Reports a `MISSING_ON_TABLE` mismatch for any model field with no matching column. When `allow_extra_columns=False`, reports `MISSING_ON_MODEL` for any column the model does not declare. Fields present on both sides are silently passed through — the next commit fills in the comparison. - `model_type` is sourced from `info.annotation` and is typed `TypeAnnotation` (from fgmetric) — no string-stringification. `column_type` is the SDK-resolved Python type on the `Column`. Per @msto's review: this PR (originally #48) is now split. The internal helper still returns `list[SchemaMismatch]` for test introspection; the classmethod that raises lands in #49 and is a thin wrapper that calls the helper and raises if the list is non-empty. Related to #42. Tracked at #53.
478b377 to
5d9ae53
Compare
Summary
Related to #42. Stacked on #47. Tracked at #53.
Adds the schema-validation entry point and the enumeration-only error
paths (
MISSING_ON_TABLE,MISSING_ON_MODEL). Per-field typecomparison is layered on in the next stacked PR (#48b); this PR is
intentionally scoped to "do columns line up", not "do their types
agree".
_validate_table_schema(model_cls, table, *, allow_extra_columns)iterates the model's
model_fields(excluding the baseid/name) against the table's columns. Reports aMISSING_ON_TABLEmismatch for any model field with no matching column. When
allow_extra_columns=False, reportsMISSING_ON_MODELfor anycolumn the model does not declare. Fields present on both sides are
passed through silently — the next PR fills in the comparison.
model_typeis sourced frominfo.annotationand typedTypeAnnotation(fromfgmetric._typing_extensions), so theliteral annotation is preserved end-to-end.
column_typeis theSDK-resolved Python type on the
Column.Design note (re your "why two layers" comment on the previous
revision): the internal helper still returns
list[SchemaMismatch]because test introspection is much cleaner that way (no
pytest.raisesceremony to inspect per-field details). The classmethodthat raises lands in #49 and is a thin wrapper.
Test plan
MISSING_ON_TABLEpopulatesmodel_field+model_typeand leavescolumn_*None.MISSING_ON_MODELpopulatescolumn_name+column_typeandleaves
model_*None.allow_extra_columns=TruesilencesMISSING_ON_MODEL.id/namemodel fields are skipped.Co-Authored-By: Claude noreply@anthropic.com