Skip to content

bug: RelatedNode.peer is typed InfrahubNode, losing the peer type during relationship traversal #1063

@FragmentedPacket

Description

@FragmentedPacket

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.pydef 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    type/bugSomething isn't working as expected

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions