From e46fdce7e36b7a11f702a63ed14c3e0094559f58 Mon Sep 17 00:00:00 2001 From: Adam Byczkowski Date: Thu, 27 Aug 2026 00:12:24 -0500 Subject: [PATCH 1/7] fix: duck-type get_kind() instead of isinstance against write-side schema classes client.schema.all() returns infrahub-sdk's read-side schema objects (AttributeSchemaAPI/RelationshipSchemaAPI). At infrahub-sdk 1.18.1 those subclassed AttributeSchema/RelationshipSchema, so get_kind()'s isinstance checks happened to work; infrahub-sdk 1.23.0 split the class hierarchy so they no longer do, and every attribute/relationship silently fell through to the "str" default -- dropping optional/None/list[...] handling. Checking for a `cardinality` attribute instead of a specific base class is robust to that kind of SDK refactor. Co-Authored-By: Claude Sonnet 5 --- infrahub_sync/generator/__init__.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/infrahub_sync/generator/__init__.py b/infrahub_sync/generator/__init__.py index be82a795..b37d1209 100644 --- a/infrahub_sync/generator/__init__.py +++ b/infrahub_sync/generator/__init__.py @@ -118,8 +118,16 @@ def get_children(node: NodeSchema, config: SyncConfig) -> str | None: def get_kind(item: Union[RelationshipSchema, AttributeSchema]) -> str: + # Duck-typed on `cardinality` rather than `isinstance(item, (AttributeSchema, + # RelationshipSchema))`: the SDK's read-side schema objects returned by + # `client.schema.all()` (e.g. AttributeSchemaAPI/RelationshipSchemaAPI) are not + # guaranteed to subclass these write-side classes across infrahub-sdk versions + # -- that inheritance held at 1.18.1 but was removed by 1.23.0, which silently + # made every attribute/relationship fall through to the "str" default below. kind = "str" - if isinstance(item, AttributeSchema): + is_relationship = hasattr(item, "cardinality") + + if not is_relationship: kind = ATTRIBUTE_KIND_MAP.get(item.kind, "str") if item.optional: kind = f"{kind} | None" @@ -134,11 +142,11 @@ def get_kind(item: Union[RelationshipSchema, AttributeSchema]) -> str: else: kind += " = None" - elif isinstance(item, RelationshipSchema) and item.cardinality == "one": + elif item.cardinality == "one": if item.optional: kind = f"{kind} | None = None" - elif isinstance(item, RelationshipSchema) and item.cardinality == "many": + elif item.cardinality == "many": kind = "list[str]" if item.optional: kind = f"{kind} | None" From e3241d8c30516b1a347e7a6219be79b13cb8c25d Mon Sep 17 00:00:00 2001 From: Adam Byczkowski Date: Thu, 27 Aug 2026 00:35:37 -0500 Subject: [PATCH 2/7] fix: satisfy ty by casting after the cardinality duck-type check hasattr()-based narrowing doesn't inform ty's type narrowing the way isinstance() does, so it still saw item as RelationshipSchema | AttributeSchema inside each branch and flagged the cross-branch attribute accesses (default_value on the relationship branch, cardinality on the attribute branch). Cast to the appropriate type in each branch instead, matching the runtime check we already made. Co-Authored-By: Claude Sonnet 5 --- infrahub_sync/generator/__init__.py | 39 ++++++++++++++++------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/infrahub_sync/generator/__init__.py b/infrahub_sync/generator/__init__.py index b37d1209..f0932422 100644 --- a/infrahub_sync/generator/__init__.py +++ b/infrahub_sync/generator/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Union +from typing import TYPE_CHECKING, Any, Union, cast import jinja2 from infrahub_sdk.schema import ( @@ -128,29 +128,32 @@ def get_kind(item: Union[RelationshipSchema, AttributeSchema]) -> str: is_relationship = hasattr(item, "cardinality") if not is_relationship: - kind = ATTRIBUTE_KIND_MAP.get(item.kind, "str") - if item.optional: + attr = cast("AttributeSchema", item) + kind = ATTRIBUTE_KIND_MAP.get(attr.kind, "str") + if attr.optional: kind = f"{kind} | None" - if item.default_value is not None: + if attr.default_value is not None: # Format the default value based on its type - if isinstance(item.default_value, str): - kind += f' = "{item.default_value}"' - elif isinstance(item.default_value, (int, float, bool)): - kind += f" = {item.default_value}" + if isinstance(attr.default_value, str): + kind += f' = "{attr.default_value}"' + elif isinstance(attr.default_value, (int, float, bool)): + kind += f" = {attr.default_value}" else: - kind += f" = {item.default_value!r}" + kind += f" = {attr.default_value!r}" else: kind += " = None" - elif item.cardinality == "one": - if item.optional: - kind = f"{kind} | None = None" - - elif item.cardinality == "many": - kind = "list[str]" - if item.optional: - kind = f"{kind} | None" - kind += " = []" + else: + rel = cast("RelationshipSchema", item) + if rel.cardinality == "one": + if rel.optional: + kind = f"{kind} | None = None" + + elif rel.cardinality == "many": + kind = "list[str]" + if rel.optional: + kind = f"{kind} | None" + kind += " = []" return kind From b253bb31500e7f20a0f1b1efdc9f25ad13c47938 Mon Sep 17 00:00:00 2001 From: Adam Byczkowski Date: Thu, 27 Aug 2026 00:47:11 -0500 Subject: [PATCH 3/7] test: cover get_kind() duck-typing and give it an honest Protocol signature Adds tests/test_generator_get_kind.py, which exercises get_kind() against schema-shaped objects that deliberately do NOT subclass AttributeSchema/ RelationshipSchema, plus the real infrahub-sdk read-side classes (AttributeSchemaAPI/RelationshipSchemaAPI). Confirmed these tests fail against the pre-fix isinstance-based get_kind() (9/14 failures) and pass against the duck-typed version, so they guard the regression from #187 regardless of which infrahub-sdk version a future lockfile resolves. Also replaces get_kind()'s `Union[RelationshipSchema, AttributeSchema]` parameter type with a `_AttributeLike | _RelationshipLike` Protocol union, since the isinstance-free implementation no longer actually requires either concrete class -- the old annotation was a check `ty` could no longer verify once real call sites (including these tests) pass API-side schema objects. Co-Authored-By: Claude Sonnet 5 --- infrahub_sync/generator/__init__.py | 27 ++++++- tests/test_generator_get_kind.py | 108 ++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 tests/test_generator_get_kind.py diff --git a/infrahub_sync/generator/__init__.py b/infrahub_sync/generator/__init__.py index f0932422..c5591eba 100644 --- a/infrahub_sync/generator/__init__.py +++ b/infrahub_sync/generator/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Union, cast +from typing import TYPE_CHECKING, Any, Protocol, Union, cast import jinja2 from infrahub_sdk.schema import ( @@ -117,13 +117,36 @@ def get_children(node: NodeSchema, config: SyncConfig) -> str | None: return "{" + ", ".join(children_list) + "}" -def get_kind(item: Union[RelationshipSchema, AttributeSchema]) -> str: +class _AttributeLike(Protocol): + """Structural shape get_kind() needs from an attribute-schema object. + + `kind` is typed `Any` rather than `str`: real schema classes type it as + an `AttributeKind` str-enum, and Protocol attribute matching is invariant, + so a `str`-typed member here would reject them despite being usable as a + ATTRIBUTE_KIND_MAP lookup key. + """ + + kind: Any + optional: bool + default_value: Any + + +class _RelationshipLike(Protocol): + """Structural shape get_kind() needs from a relationship-schema object.""" + + cardinality: str + optional: bool + + +def get_kind(item: Union[_AttributeLike, _RelationshipLike]) -> str: # Duck-typed on `cardinality` rather than `isinstance(item, (AttributeSchema, # RelationshipSchema))`: the SDK's read-side schema objects returned by # `client.schema.all()` (e.g. AttributeSchemaAPI/RelationshipSchemaAPI) are not # guaranteed to subclass these write-side classes across infrahub-sdk versions # -- that inheritance held at 1.18.1 but was removed by 1.23.0, which silently # made every attribute/relationship fall through to the "str" default below. + # The parameter type reflects that: any object with the right shape works, + # not just AttributeSchema/RelationshipSchema instances. kind = "str" is_relationship = hasattr(item, "cardinality") diff --git a/tests/test_generator_get_kind.py b/tests/test_generator_get_kind.py new file mode 100644 index 00000000..0d713097 --- /dev/null +++ b/tests/test_generator_get_kind.py @@ -0,0 +1,108 @@ +"""Tests for infrahub_sync.generator.get_kind. + +get_kind() decides the pydantic type annotation for each generated DiffSync +model field. It must work on whatever object `client.schema.all()` hands back +-- infrahub-sdk's read-side schema classes (AttributeSchemaAPI / +RelationshipSchemaAPI) -- not just on the write-side AttributeSchema / +RelationshipSchema classes used to build schema payloads. + +Those two class families are not guaranteed to share a common base: at +infrahub-sdk 1.18.1 the read-side classes happened to subclass the write-side +ones, so an `isinstance(item, AttributeSchema)` check worked; at 1.23.0 that +inheritance was removed, and the check silently stopped matching anything, +making get_kind() fall through to its "str" default for every attribute and +relationship (see https://github.com/opsmill/infrahub-sync/issues/187). + +`_FakeAttribute`/`_FakeRelationship` below deliberately do NOT inherit from +either SDK schema family. They exist so this suite fails if get_kind() is +ever changed back to an isinstance-based check -- regardless of which +infrahub-sdk version the lockfile happens to resolve at the time. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +import pytest +from infrahub_sdk.schema import AttributeSchema, RelationshipSchema +from infrahub_sdk.schema.main import AttributeKind, AttributeSchemaAPI, RelationshipSchemaAPI + +from infrahub_sync.generator import get_kind + + +@dataclass +class _FakeAttribute: + """Duck-types an attribute schema without subclassing any SDK class.""" + + kind: str + optional: bool = False + default_value: Any = None + + +@dataclass +class _FakeRelationship: + """Duck-types a relationship schema without subclassing any SDK class.""" + + cardinality: str + optional: bool = False + + +def test_fake_schema_objects_do_not_subclass_sdk_schema_classes() -> None: + """Pins down the premise the other tests rely on. + + If this starts failing, the fakes below stopped simulating the + decoupled-class scenario and no longer guard the regression. + """ + assert not isinstance(_FakeAttribute(kind="Text"), AttributeSchema) + assert not isinstance(_FakeRelationship(cardinality="one"), RelationshipSchema) + + +@pytest.mark.parametrize( + ("attribute", "expected"), + [ + (_FakeAttribute(kind="Text"), "str"), + (_FakeAttribute(kind="Number"), "int"), + (_FakeAttribute(kind="Boolean"), "bool"), + (_FakeAttribute(kind="SomeUnmappedKind"), "str"), + (_FakeAttribute(kind="Text", optional=True), "str | None = None"), + (_FakeAttribute(kind="Text", optional=True, default_value="foo"), 'str | None = "foo"'), + (_FakeAttribute(kind="Number", optional=True, default_value=5), "int | None = 5"), + (_FakeAttribute(kind="Boolean", optional=True, default_value=True), "bool | None = True"), + ], +) +def test_get_kind_for_attribute_shaped_objects(attribute: _FakeAttribute, expected: str) -> None: + assert get_kind(attribute) == expected + + +@pytest.mark.parametrize( + ("relationship", "expected"), + [ + (_FakeRelationship(cardinality="one"), "str"), + (_FakeRelationship(cardinality="one", optional=True), "str | None = None"), + (_FakeRelationship(cardinality="many"), "list[str] = []"), + (_FakeRelationship(cardinality="many", optional=True), "list[str] | None = []"), + ], +) +def test_get_kind_for_relationship_shaped_objects(relationship: _FakeRelationship, expected: str) -> None: + assert get_kind(relationship) == expected + + +def test_get_kind_against_real_sdk_read_side_schema_classes() -> None: + """Sanity check against the actual classes `client.schema.all()` returns. + + Complements the fakes above with the real infrahub-sdk read-side types, + so a shape drift in the SDK itself (a renamed/removed field) is caught + here too, not just a reintroduced isinstance check. + """ + optional_attr = AttributeSchemaAPI(id="1", name="description", kind=AttributeKind.TEXT, optional=True) + assert get_kind(optional_attr) == "str | None = None" + + required_attr = AttributeSchemaAPI(id="2", name="name", kind=AttributeKind.TEXT, optional=False) + assert get_kind(required_attr) == "str" + + many_rel = RelationshipSchemaAPI(id="3", name="tags", peer="BuiltinTag", cardinality="many", optional=True) + assert get_kind(many_rel) == "list[str] | None = []" + + one_rel = RelationshipSchemaAPI(id="4", name="status", peer="StatusGeneric", cardinality="one", optional=False) + assert get_kind(one_rel) == "str" From 7d184759b7b81c407b3c778c9c24da4ce0835fa3 Mon Sep 17 00:00:00 2001 From: Adam Byczkowski Date: Thu, 27 Aug 2026 00:53:11 -0500 Subject: [PATCH 4/7] style: drop the rationale comment inside get_kind() Co-Authored-By: Claude Sonnet 5 --- infrahub_sync/generator/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/infrahub_sync/generator/__init__.py b/infrahub_sync/generator/__init__.py index c5591eba..868e0e2b 100644 --- a/infrahub_sync/generator/__init__.py +++ b/infrahub_sync/generator/__init__.py @@ -139,14 +139,6 @@ class _RelationshipLike(Protocol): def get_kind(item: Union[_AttributeLike, _RelationshipLike]) -> str: - # Duck-typed on `cardinality` rather than `isinstance(item, (AttributeSchema, - # RelationshipSchema))`: the SDK's read-side schema objects returned by - # `client.schema.all()` (e.g. AttributeSchemaAPI/RelationshipSchemaAPI) are not - # guaranteed to subclass these write-side classes across infrahub-sdk versions - # -- that inheritance held at 1.18.1 but was removed by 1.23.0, which silently - # made every attribute/relationship fall through to the "str" default below. - # The parameter type reflects that: any object with the right shape works, - # not just AttributeSchema/RelationshipSchema instances. kind = "str" is_relationship = hasattr(item, "cardinality") From 0400759383f8a90b8e27d79b2afcfa5d807224ca Mon Sep 17 00:00:00 2001 From: Adam Byczkowski Date: Thu, 27 Aug 2026 00:55:10 -0500 Subject: [PATCH 5/7] style: trim _AttributeLike docstring to one line Co-Authored-By: Claude Sonnet 5 --- infrahub_sync/generator/__init__.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/infrahub_sync/generator/__init__.py b/infrahub_sync/generator/__init__.py index 868e0e2b..aa20502a 100644 --- a/infrahub_sync/generator/__init__.py +++ b/infrahub_sync/generator/__init__.py @@ -118,13 +118,7 @@ def get_children(node: NodeSchema, config: SyncConfig) -> str | None: class _AttributeLike(Protocol): - """Structural shape get_kind() needs from an attribute-schema object. - - `kind` is typed `Any` rather than `str`: real schema classes type it as - an `AttributeKind` str-enum, and Protocol attribute matching is invariant, - so a `str`-typed member here would reject them despite being usable as a - ATTRIBUTE_KIND_MAP lookup key. - """ + """Structural shape get_kind() needs from an attribute-schema object.""" kind: Any optional: bool From 7ca68fec1e7fb0ea248fa1c67a84ef03a3d27b9e Mon Sep 17 00:00:00 2001 From: Adam Byczkowski Date: Thu, 27 Aug 2026 14:34:39 -0500 Subject: [PATCH 6/7] fix: split get_kind() into per-context attribute/relationship filters Templates already know whether a field is an attribute or relationship, so the hasattr(item, "cardinality") duck-typing check in get_kind() was unnecessary indirection. get_attribute_type_annotation() and get_relationship_type_annotation() are called from the exact template site that knows which one applies, removing the runtime branch. Co-Authored-By: Claude Sonnet 5 --- infrahub_sync/generator/__init__.py | 71 ++++++++++--------- .../generator/templates/diffsync_models.j2 | 4 +- ...enerator_get_kind.py => test_generator.py} | 50 +++++-------- 3 files changed, 59 insertions(+), 66 deletions(-) rename tests/{test_generator_get_kind.py => test_generator.py} (60%) diff --git a/infrahub_sync/generator/__init__.py b/infrahub_sync/generator/__init__.py index aa20502a..3a6cb67b 100644 --- a/infrahub_sync/generator/__init__.py +++ b/infrahub_sync/generator/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Protocol, Union, cast +from typing import TYPE_CHECKING, Any, Protocol, cast import jinja2 from infrahub_sdk.schema import ( @@ -118,7 +118,7 @@ def get_children(node: NodeSchema, config: SyncConfig) -> str | None: class _AttributeLike(Protocol): - """Structural shape get_kind() needs from an attribute-schema object.""" + """Structural shape get_attribute_type_annotation() needs from an attribute-schema object.""" kind: Any optional: bool @@ -126,45 +126,49 @@ class _AttributeLike(Protocol): class _RelationshipLike(Protocol): - """Structural shape get_kind() needs from a relationship-schema object.""" + """Structural shape get_relationship_type_annotation() needs from a relationship-schema object.""" cardinality: str optional: bool -def get_kind(item: Union[_AttributeLike, _RelationshipLike]) -> str: - kind = "str" - is_relationship = hasattr(item, "cardinality") - - if not is_relationship: - attr = cast("AttributeSchema", item) - kind = ATTRIBUTE_KIND_MAP.get(attr.kind, "str") - if attr.optional: - kind = f"{kind} | None" - if attr.default_value is not None: - # Format the default value based on its type - if isinstance(attr.default_value, str): - kind += f' = "{attr.default_value}"' - elif isinstance(attr.default_value, (int, float, bool)): - kind += f" = {attr.default_value}" - else: - kind += f" = {attr.default_value!r}" +def get_attribute_type_annotation(item: _AttributeLike) -> str: + """Return type annotation of schema attribute for Diffsync model.""" + annotation = "str" + + attr = cast("AttributeSchema", item) + annotation = ATTRIBUTE_KIND_MAP.get(attr.kind, "str") + if attr.optional: + annotation = f"{annotation} | None" + if attr.default_value is not None: + # Format the default value based on its type + if isinstance(attr.default_value, str): + annotation += f' = "{attr.default_value}"' + elif isinstance(attr.default_value, (int, float, bool)): + annotation += f" = {attr.default_value}" else: - kind += " = None" + annotation += f" = {attr.default_value!r}" + else: + annotation += " = None" - else: - rel = cast("RelationshipSchema", item) - if rel.cardinality == "one": - if rel.optional: - kind = f"{kind} | None = None" + return annotation - elif rel.cardinality == "many": - kind = "list[str]" - if rel.optional: - kind = f"{kind} | None" - kind += " = []" - return kind +def get_relationship_type_annotation(item: _RelationshipLike) -> str: + """Return type annotation of schema relationship for Diffsync model.""" + annotation = "str" + rel = cast("RelationshipSchema", item) + if rel.cardinality == "one": + if rel.optional: + annotation = f"{annotation} | None = None" + + elif rel.cardinality == "many": + annotation = "list[str]" + if rel.optional: + annotation = f"{annotation} | None" + annotation += " = []" + + return annotation def has_children(node: NodeSchema, config: SyncConfig) -> bool: @@ -185,7 +189,8 @@ def render_template(template_file: Path, output_dir: Path, output_file: Path, co template_env.filters["has_node"] = has_node template_env.filters["has_field"] = has_field template_env.filters["has_children"] = has_children - template_env.filters["get_kind"] = get_kind + template_env.filters["get_attribute_type_annotation"] = get_attribute_type_annotation + template_env.filters["get_relationship_type_annotation"] = get_relationship_type_annotation template = template_env.get_template(str(template_file)) diff --git a/infrahub_sync/generator/templates/diffsync_models.j2 b/infrahub_sync/generator/templates/diffsync_models.j2 index 577bff86..37321ff5 100644 --- a/infrahub_sync/generator/templates/diffsync_models.j2 +++ b/infrahub_sync/generator/templates/diffsync_models.j2 @@ -35,12 +35,12 @@ class {{ nodekind }}(_ModelBaseClass): {%- for attr in node.attributes -%} {%- if config | has_field(node.kind, attr.name) %} - {{ attr.name }}: {{ attr | get_kind }} + {{ attr.name }}: {{ attr | get_attribute_type_annotation }} {%- endif -%} {%- endfor -%} {%- for rel in node.relationships -%} {%- if config | has_field(node.kind, rel.name) %} - {{ rel.name }}: {{ rel | get_kind }} + {{ rel.name }}: {{ rel | get_relationship_type_annotation }} {%- endif -%} {%- endfor %} diff --git a/tests/test_generator_get_kind.py b/tests/test_generator.py similarity index 60% rename from tests/test_generator_get_kind.py rename to tests/test_generator.py index 0d713097..0c2267e9 100644 --- a/tests/test_generator_get_kind.py +++ b/tests/test_generator.py @@ -1,22 +1,10 @@ -"""Tests for infrahub_sync.generator.get_kind. +"""Tests for infrahub_sync.generator.get_attribute_type_annotation / get_relationship_type_annotation. -get_kind() decides the pydantic type annotation for each generated DiffSync -model field. It must work on whatever object `client.schema.all()` hands back +These functions decide the pydantic type annotation for each generated DiffSync +model field. They must work on whatever object `client.schema.all()` hands back -- infrahub-sdk's read-side schema classes (AttributeSchemaAPI / RelationshipSchemaAPI) -- not just on the write-side AttributeSchema / RelationshipSchema classes used to build schema payloads. - -Those two class families are not guaranteed to share a common base: at -infrahub-sdk 1.18.1 the read-side classes happened to subclass the write-side -ones, so an `isinstance(item, AttributeSchema)` check worked; at 1.23.0 that -inheritance was removed, and the check silently stopped matching anything, -making get_kind() fall through to its "str" default for every attribute and -relationship (see https://github.com/opsmill/infrahub-sync/issues/187). - -`_FakeAttribute`/`_FakeRelationship` below deliberately do NOT inherit from -either SDK schema family. They exist so this suite fails if get_kind() is -ever changed back to an isinstance-based check -- regardless of which -infrahub-sdk version the lockfile happens to resolve at the time. """ from __future__ import annotations @@ -28,7 +16,7 @@ from infrahub_sdk.schema import AttributeSchema, RelationshipSchema from infrahub_sdk.schema.main import AttributeKind, AttributeSchemaAPI, RelationshipSchemaAPI -from infrahub_sync.generator import get_kind +from infrahub_sync.generator import get_attribute_type_annotation, get_relationship_type_annotation @dataclass @@ -71,8 +59,8 @@ def test_fake_schema_objects_do_not_subclass_sdk_schema_classes() -> None: (_FakeAttribute(kind="Boolean", optional=True, default_value=True), "bool | None = True"), ], ) -def test_get_kind_for_attribute_shaped_objects(attribute: _FakeAttribute, expected: str) -> None: - assert get_kind(attribute) == expected +def test_get_attribute_type_annotation_for_attribute_shaped_objects(attribute: _FakeAttribute, expected: str) -> None: + assert get_attribute_type_annotation(attribute) == expected @pytest.mark.parametrize( @@ -84,25 +72,25 @@ def test_get_kind_for_attribute_shaped_objects(attribute: _FakeAttribute, expect (_FakeRelationship(cardinality="many", optional=True), "list[str] | None = []"), ], ) -def test_get_kind_for_relationship_shaped_objects(relationship: _FakeRelationship, expected: str) -> None: - assert get_kind(relationship) == expected - +def test_get_relationship_type_annotation_for_relationship_shaped_objects( + relationship: _FakeRelationship, expected: str +) -> None: + assert get_relationship_type_annotation(relationship) == expected -def test_get_kind_against_real_sdk_read_side_schema_classes() -> None: - """Sanity check against the actual classes `client.schema.all()` returns. - Complements the fakes above with the real infrahub-sdk read-side types, - so a shape drift in the SDK itself (a renamed/removed field) is caught - here too, not just a reintroduced isinstance check. - """ +def test_get_attribute_type_annotation_against_real_sdk_read_side_schema_classes() -> None: + """Sanity check against the actual classes `client.schema.all()` returns.""" optional_attr = AttributeSchemaAPI(id="1", name="description", kind=AttributeKind.TEXT, optional=True) - assert get_kind(optional_attr) == "str | None = None" + assert get_attribute_type_annotation(optional_attr) == "str | None = None" required_attr = AttributeSchemaAPI(id="2", name="name", kind=AttributeKind.TEXT, optional=False) - assert get_kind(required_attr) == "str" + assert get_attribute_type_annotation(required_attr) == "str" + +def test_get_relationship_type_annotation_against_real_sdk_read_side_schema_classes() -> None: + """Sanity check against the actual classes `client.schema.all()` returns.""" many_rel = RelationshipSchemaAPI(id="3", name="tags", peer="BuiltinTag", cardinality="many", optional=True) - assert get_kind(many_rel) == "list[str] | None = []" + assert get_relationship_type_annotation(many_rel) == "list[str] | None = []" one_rel = RelationshipSchemaAPI(id="4", name="status", peer="StatusGeneric", cardinality="one", optional=False) - assert get_kind(one_rel) == "str" + assert get_relationship_type_annotation(one_rel) == "str" From 6b0513f395e711474ae1c8578dba7e2d33141230 Mon Sep 17 00:00:00 2001 From: Adam Byczkowski Date: Thu, 27 Aug 2026 14:40:15 -0500 Subject: [PATCH 7/7] Removed unneeded type casting --- infrahub_sync/generator/__init__.py | 32 ++++++++++++----------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/infrahub_sync/generator/__init__.py b/infrahub_sync/generator/__init__.py index 3a6cb67b..1cf6c9d6 100644 --- a/infrahub_sync/generator/__init__.py +++ b/infrahub_sync/generator/__init__.py @@ -1,13 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Protocol, cast +from typing import TYPE_CHECKING, Any, Protocol import jinja2 from infrahub_sdk.schema import ( - AttributeSchema, NodeSchema, RelationshipKind, - RelationshipSchema, ) if TYPE_CHECKING: @@ -134,20 +132,17 @@ class _RelationshipLike(Protocol): def get_attribute_type_annotation(item: _AttributeLike) -> str: """Return type annotation of schema attribute for Diffsync model.""" - annotation = "str" - - attr = cast("AttributeSchema", item) - annotation = ATTRIBUTE_KIND_MAP.get(attr.kind, "str") - if attr.optional: + annotation = ATTRIBUTE_KIND_MAP.get(item.kind, "str") + if item.optional: annotation = f"{annotation} | None" - if attr.default_value is not None: + if item.default_value is not None: # Format the default value based on its type - if isinstance(attr.default_value, str): - annotation += f' = "{attr.default_value}"' - elif isinstance(attr.default_value, (int, float, bool)): - annotation += f" = {attr.default_value}" + if isinstance(item.default_value, str): + annotation += f' = "{item.default_value}"' + elif isinstance(item.default_value, (int, float, bool)): + annotation += f" = {item.default_value}" else: - annotation += f" = {attr.default_value!r}" + annotation += f" = {item.default_value!r}" else: annotation += " = None" @@ -157,14 +152,13 @@ def get_attribute_type_annotation(item: _AttributeLike) -> str: def get_relationship_type_annotation(item: _RelationshipLike) -> str: """Return type annotation of schema relationship for Diffsync model.""" annotation = "str" - rel = cast("RelationshipSchema", item) - if rel.cardinality == "one": - if rel.optional: + if item.cardinality == "one": + if item.optional: annotation = f"{annotation} | None = None" - elif rel.cardinality == "many": + elif item.cardinality == "many": annotation = "list[str]" - if rel.optional: + if item.optional: annotation = f"{annotation} | None" annotation += " = []"