Component: Python SDK, CI/CD
Why this is needed
pyproject.toml carries a growing list of [[tool.ty.overrides]] blocks that disable type checking rules for test files. These are documented as tech debt ("Fix these incrementally by addressing violations and removing the override"), and nearly all of them exist for the same two reasons:
- Dynamic node attribute access. Tests read
node.name.value / node.primary_tag.peer on plain InfrahubNode objects. That access goes through InfrahubNode.__getattr__, which is typed as returning Attribute | RelationshipManager | RelatedNode, so every such expression raises possibly-missing-attribute. Affected overrides: test_node.py, test_hierarchical_nodes.py, test_schema.py, test_store_branch.py, test_repository.py, tests/integration/**.
- Untyped schema construction. Test schemas are defined as inline dicts in
tests/unit/sdk/conftest.py and built with NodeSchema(**data), which loses all types at the constructor boundary (invalid-argument-type, ~434 violations in the conftest.py override alone).
The SDK already has the intended answer to problem 1: generated protocol classes plus the kind= overloads on client.get() / store.get(), which return a typed SchemaType. Tests can't use it today because the conftest test schemas (BuiltinLocation, InfraLocation, ...) have no protocol definitions - only kinds in the real core schema exist in infrahub_sdk/protocols.py.
tests/unit/sdk/test_store_merge.py shows the workaround: hand-written protocol classes for its fixture schemas. It type-checks clean with no override, proving the approach, but hand-written protocols duplicate the schema and will drift silently when a fixture changes. They also can't express everything the generator can (see RelationshipAttribute below).
Prior art: how the main Infrahub repo solves this
The main repo generates protocols for its test schemas and gates them in CI:
- Test schemas are defined once, as schema model objects, in
backend/tests/helpers/schema/ (e.g. CAR, PERSON, TAG), and the package exports a test_models dict.
invoke backend.generate renders backend/tests/protocols.py from those models with the same Jinja template as the production protocols, after extending the test models with the core models so every referenced peer kind is defined.
invoke backend.validate-generated runs the generator and then git diff --exit-code backend/infrahub/core/protocols.py backend/tests/protocols.py - CI fails if a schema changed without regenerating.
Recommended approach for the SDK
The SDK already ships the generator: infrahub_sdk/protocols_generator/generator.py (CodeGenerator), used by infrahubctl protocols. It accepts exactly what the conftest fixtures produce (dict[str, MainSchemaTypesAll], e.g. NodeSchema(...).convert_api()). A prototype run against the location_schema fixture produced:
class BuiltinLocation(CoreNode):
description: StringOptional
name: String
type: String
primary_tag: RelationshipAttribute[BuiltinTag]
tags: RelationshipManager[BuiltinTag]
(plus the CoreNodeSync twin from render(sync=True)). Note RelationshipAttribute[BuiltinTag]: the generated form keeps the peer type through .peer and accepts node.primary_tag = "some-id" assignment - hand-written primary_tag: RelatedNode annotations can express neither, which is the same family of problem as #1062 / #1063 / #1064.
Suggested steps:
- Centralize the test schemas (the bulk of the work). Lift the ~30 inline schema dicts out of
tests/unit/sdk/conftest.py into a module such as tests/helpers/schema/, defined as NodeSchema objects. The existing fixtures become thin wrappers returning .convert_api() of the shared objects, so no test changes are needed beyond imports.
- Add an invoke task (e.g.
generate-test-protocols, wired into the existing generate/validate tasks in tasks.py) that feeds those schemas through CodeGenerator and writes a committed tests/helpers/protocols.py (async + sync variants, ruff-formatted, with the standard "generated, do not edit" header).
- Gate it in CI the same way
docs-validate works: a validate step that regenerates and runs git diff --exit-code tests/helpers/protocols.py, so any schema fixture change that isn't regenerated fails the pipeline.
- Adopt in tests incrementally: replace the hand-written protocols in
test_store_merge.py with the generated ones, then migrate the other override-listed test files to typed store.get(key=..., kind=<Protocol>) / client.get(kind=<Protocol>) lookups and delete their [[tool.ty.overrides]] blocks one file at a time.
Known wrinkles (found while prototyping)
- Deprecated attribute kind. The conftest schemas use
kind="String", which ATTRIBUTE_KIND_MAP in infrahub_sdk/protocols_generator/constants.py does not map - the generator raises KeyError: 'String'. The centralized schemas must migrate to "Text" (consistent with the AttributeKind.STRING deprecation). This is invisible to the tests themselves (schema.kind only drives the IP-type value mapper).
- Peer kinds must be in the generation set. The template renders a self-contained module, so peers referenced by relationships (
BuiltinTag, CoreGroup, ...) must be included, mirroring the main repo's test_models["nodes"].extend(core_models["nodes"]), or the annotations become undefined names.
- Protocol attribute gaps.
protocols_base.Attribute doesn't declare SDK internals such as is_fetched, value_has_been_mutated, source/owner. Tests asserting on internals should keep going through node._attribute_data["name"] (typed as the real Attribute), as test_store_merge.py does; extending the base protocol is a separate decision.
- Schema fixture construction. While centralizing, build schemas with explicit keywords /
AttributeSchema(...) objects rather than NodeSchema(**data) - that alone removes the invalid-argument-type half of the conftest override.
References
Component: Python SDK, CI/CD
Why this is needed
pyproject.tomlcarries a growing list of[[tool.ty.overrides]]blocks that disable type checking rules for test files. These are documented as tech debt ("Fix these incrementally by addressing violations and removing the override"), and nearly all of them exist for the same two reasons:node.name.value/node.primary_tag.peeron plainInfrahubNodeobjects. That access goes throughInfrahubNode.__getattr__, which is typed as returningAttribute | RelationshipManager | RelatedNode, so every such expression raisespossibly-missing-attribute. Affected overrides:test_node.py,test_hierarchical_nodes.py,test_schema.py,test_store_branch.py,test_repository.py,tests/integration/**.tests/unit/sdk/conftest.pyand built withNodeSchema(**data), which loses all types at the constructor boundary (invalid-argument-type, ~434 violations in theconftest.pyoverride alone).The SDK already has the intended answer to problem 1: generated protocol classes plus the
kind=overloads onclient.get()/store.get(), which return a typedSchemaType. Tests can't use it today because the conftest test schemas (BuiltinLocation,InfraLocation, ...) have no protocol definitions - only kinds in the real core schema exist ininfrahub_sdk/protocols.py.tests/unit/sdk/test_store_merge.pyshows the workaround: hand-written protocol classes for its fixture schemas. It type-checks clean with no override, proving the approach, but hand-written protocols duplicate the schema and will drift silently when a fixture changes. They also can't express everything the generator can (seeRelationshipAttributebelow).Prior art: how the main Infrahub repo solves this
The main repo generates protocols for its test schemas and gates them in CI:
backend/tests/helpers/schema/(e.g.CAR,PERSON,TAG), and the package exports atest_modelsdict.invoke backend.generaterendersbackend/tests/protocols.pyfrom those models with the same Jinja template as the production protocols, after extending the test models with the core models so every referenced peer kind is defined.invoke backend.validate-generatedruns the generator and thengit diff --exit-code backend/infrahub/core/protocols.py backend/tests/protocols.py- CI fails if a schema changed without regenerating.Recommended approach for the SDK
The SDK already ships the generator:
infrahub_sdk/protocols_generator/generator.py(CodeGenerator), used byinfrahubctl protocols. It accepts exactly what the conftest fixtures produce (dict[str, MainSchemaTypesAll], e.g.NodeSchema(...).convert_api()). A prototype run against thelocation_schemafixture produced:(plus the
CoreNodeSynctwin fromrender(sync=True)). NoteRelationshipAttribute[BuiltinTag]: the generated form keeps the peer type through.peerand acceptsnode.primary_tag = "some-id"assignment - hand-writtenprimary_tag: RelatedNodeannotations can express neither, which is the same family of problem as #1062 / #1063 / #1064.Suggested steps:
tests/unit/sdk/conftest.pyinto a module such astests/helpers/schema/, defined asNodeSchemaobjects. The existing fixtures become thin wrappers returning.convert_api()of the shared objects, so no test changes are needed beyond imports.generate-test-protocols, wired into the existing generate/validate tasks intasks.py) that feeds those schemas throughCodeGeneratorand writes a committedtests/helpers/protocols.py(async + sync variants, ruff-formatted, with the standard "generated, do not edit" header).docs-validateworks: avalidatestep that regenerates and runsgit diff --exit-code tests/helpers/protocols.py, so any schema fixture change that isn't regenerated fails the pipeline.test_store_merge.pywith the generated ones, then migrate the other override-listed test files to typedstore.get(key=..., kind=<Protocol>)/client.get(kind=<Protocol>)lookups and delete their[[tool.ty.overrides]]blocks one file at a time.Known wrinkles (found while prototyping)
kind="String", whichATTRIBUTE_KIND_MAPininfrahub_sdk/protocols_generator/constants.pydoes not map - the generator raisesKeyError: 'String'. The centralized schemas must migrate to"Text"(consistent with theAttributeKind.STRINGdeprecation). This is invisible to the tests themselves (schema.kindonly drives the IP-type value mapper).BuiltinTag,CoreGroup, ...) must be included, mirroring the main repo'stest_models["nodes"].extend(core_models["nodes"]), or the annotations become undefined names.protocols_base.Attributedoesn't declare SDK internals such asis_fetched,value_has_been_mutated,source/owner. Tests asserting on internals should keep going throughnode._attribute_data["name"](typed as the realAttribute), astest_store_merge.pydoes; extending the base protocol is a separate decision.AttributeSchema(...)objects rather thanNodeSchema(**data)- that alone removes theinvalid-argument-typehalf of the conftest override.References
tests/unit/sdk/test_store_merge.py(hand-written protocols + typed store lookups)infrahub_sdk/protocols_generator/generator.py, consumed ininfrahub_sdk/ctl/cli_commands.py[[tool.ty.overrides]]blocks inpyproject.tomlbackend/tests/helpers/schema/,backend/tests/protocols.py, andvalidate_generatedintasks/backend.py(opsmill/infrahub)