Component
Python SDK, infrahubctl
Infrahub SDK version
1.20.1
Current Behavior
RelatedNode.peer is annotated to return InfrahubNode (the dynamic, untyped node) rather than the typed peer. As a result, any relationship traversal through .peer loses all static type information: the next attribute access resolves to the catch-all union Attribute | RelationshipManager | RelatedNode, so node.rel.peer.<attr>.value fails under mypy with union-attr. Chains such as interface.device.peer.rack.peer.name.value produce a cascade of errors.
Root cause:
infrahub_sdk/node/related_node.py — def peer(self) -> InfrahubNode: ...
RelatedNode is not parameterized by the peer type, so peer cannot return anything more specific than InfrahubNode, and InfrahubNode.__getattr__ returns the union Attribute | RelationshipManager | RelatedNode. This affects every consumer that walks relationships using the generated typed protocols and runs mypy.
Reproduces on both mypy 2.1.0 and mypy 1.17.1 (not a mypy-version regression).
Expected Behavior
reveal_type(artifact.definition.peer) should be CoreArtifactDefinition (the typed peer), and artifact.definition.peer.name.value should type-check as str.
Steps to Reproduce
from infrahub_sdk import InfrahubClient
from infrahub_sdk.protocols import CoreArtifact
async def main(client: InfrahubClient) -> None:
artifact = await client.get(kind=CoreArtifact, id="x")
reveal_type(artifact.definition.peer) # -> infrahub_sdk.node.node.InfrahubNode
_ = artifact.definition.peer.name.value # union-attr
Run mypy (infrahub-sdk 1.20.1):
note: Revealed type is "infrahub_sdk.node.node.InfrahubNode"
error: Item "RelationshipManager" of "Attribute | RelationshipManager | RelatedNode" has no attribute "value" [union-attr]
error: Item "RelatedNode" of "Attribute | RelationshipManager | RelatedNode" has no attribute "value" [union-attr]
_ = artifact.definition.peer.name.value
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
reveal_type shows the peer comes back as InfrahubNode instead of CoreArtifactDefinition, so the typed schema is lost after the first hop.
Additional Information
Impact
- The single largest source of type errors for relationship-heavy code. In one downstream repo this one issue accounts for ~56 of ~101 mypy errors after the 1.16 → 1.20 upgrade.
- Forces
cast(...) at every .peer boundary or blanket # type: ignore[union-attr].
Suggested fix
Make RelatedNode generic over the peer type:
PeerT = TypeVar("PeerT", bound=CoreNode)
class RelatedNode(RelatedNodeBase, Protocol[PeerT]):
@property
def peer(self) -> PeerT: ...
and have infrahubctl protocols emit relationship attributes as RelatedNode[CoreArtifactDefinition] (and RelationshipManager[...] similarly) instead of bare RelatedNode. This restores type information across traversals and removes the need for downstream casts. This is likely the highest-value of the related typing fixes, since correct relationship typing is the main reason to use the generated protocols.
Downstream workaround (interim)
Cast at each .peer boundary to the known peer protocol (more truthful than a blanket ignore, and keeps downstream access checked):
from typing import cast
from infrahub_sdk.protocols import CoreArtifactDefinition
definition = cast("CoreArtifactDefinition", artifact.definition.peer)
_ = definition.name.value # now checks as str
For multi-hop chains, introduce a typed local per hop:
# instead of: src_interface.peer.device.peer.rack.peer.name.value
interface = cast("NetworkInterface", endpoint.peer)
device = cast("NetworkDevice", interface.device.peer)
rack_name = cast("LocationRack", device.rack.peer).name.value if device.rack.initialized else ""
Once RelatedNode is generic, all of these casts can be deleted.
Filed separately: companion issues for type-abstract on kind= and for relationship-attribute assignment being rejected (the assignment fix composes with the generic-RelatedNode change proposed here).
Component
Python SDK, infrahubctl
Infrahub SDK version
1.20.1
Current Behavior
RelatedNode.peeris annotated to returnInfrahubNode(the dynamic, untyped node) rather than the typed peer. As a result, any relationship traversal through.peerloses all static type information: the next attribute access resolves to the catch-all unionAttribute | RelationshipManager | RelatedNode, sonode.rel.peer.<attr>.valuefails under mypy withunion-attr. Chains such asinterface.device.peer.rack.peer.name.valueproduce a cascade of errors.Root cause:
infrahub_sdk/node/related_node.py—def peer(self) -> InfrahubNode: ...RelatedNodeis not parameterized by the peer type, sopeercannot return anything more specific thanInfrahubNode, andInfrahubNode.__getattr__returns the unionAttribute | RelationshipManager | RelatedNode. This affects every consumer that walks relationships using the generated typed protocols and runs mypy.Reproduces on both mypy 2.1.0 and mypy 1.17.1 (not a mypy-version regression).
Expected Behavior
reveal_type(artifact.definition.peer)should beCoreArtifactDefinition(the typed peer), andartifact.definition.peer.name.valueshould type-check asstr.Steps to Reproduce
Run mypy (infrahub-sdk 1.20.1):
reveal_typeshows the peer comes back asInfrahubNodeinstead ofCoreArtifactDefinition, so the typed schema is lost after the first hop.Additional Information
Impact
cast(...)at every.peerboundary or blanket# type: ignore[union-attr].Suggested fix
Make
RelatedNodegeneric over the peer type:and have
infrahubctl protocolsemit relationship attributes asRelatedNode[CoreArtifactDefinition](andRelationshipManager[...]similarly) instead of bareRelatedNode. This restores type information across traversals and removes the need for downstream casts. This is likely the highest-value of the related typing fixes, since correct relationship typing is the main reason to use the generated protocols.Downstream workaround (interim)
Cast at each
.peerboundary to the known peer protocol (more truthful than a blanket ignore, and keeps downstream access checked):For multi-hop chains, introduce a typed local per hop:
Once
RelatedNodeis generic, all of these casts can be deleted.Filed separately: companion issues for
type-abstractonkind=and for relationship-attribute assignment being rejected (the assignment fix composes with the generic-RelatedNodechange proposed here).