Skip to content

bug: assigning to a relationship attribute fails mypy (assignment) for id strings and nodes #1064

@FragmentedPacket

Description

@FragmentedPacket

Component

Python SDK, infrahubctl

Infrahub SDK version

1.20.1

Current Behavior

Generated protocols type relationship attributes as RelatedNode (the read type). At runtime the SDK accepts assigning either an id string or a node object to set a relationship (node.rel = "<id>" or node.rel = other_node). Under mypy both forms fail with assignment, because the annotation only models what you read back, not what is assignable.

Root cause — generated protocols declare relationships as a single read type, e.g. in infrahub_sdk/protocols.py:

class BuiltinIPAddress(CoreNode):
    ip_namespace: RelatedNode
    ip_prefix: RelatedNode

A bare attribute annotation uses the same type for get and set, but the runtime __setattr__ accepts str | CoreNode | RelatedNode. mypy therefore rejects the runtime-valid assignment forms. This affects every consumer that sets relationships and runs mypy.

Reproduces on both mypy 2.1.0 and mypy 1.17.1 (not a mypy-version regression).

Expected Behavior

Setting a relationship via an id string or a node — the documented way to establish relationships before save() — should type-check.

Steps to Reproduce

from infrahub_sdk import InfrahubClient
from infrahub_sdk.protocols import BuiltinIPAddress


async def main(client: InfrahubClient) -> None:
    addr = await client.get(kind=BuiltinIPAddress, id="x")
    addr.ip_prefix = "some-prefix-id"            # set by id string

    other = await client.get(kind=BuiltinIPAddress, id="y")
    addr.ip_namespace = other                    # set by node

Run mypy (infrahub-sdk 1.20.1):

error: Incompatible types in assignment (expression has type "str", variable has type "RelatedNode")  [assignment]
    addr.ip_prefix = "some-prefix-id"
                     ^~~~~~~~~~~~~~~~
error: Incompatible types in assignment (expression has type "BuiltinIPAddress", variable has type "RelatedNode")  [assignment]
    addr.ip_namespace = other
                        ^~~~~

(The same error also commonly appears when assigning RelatedNode.id, i.e. str | None, to another relationship attribute.)

Additional Information

Impact

  • Affects all relationship-setting code (generators, data loaders, migrations).
  • Forces # type: ignore[assignment] on every relationship assignment.

Suggested fix

Generate relationship fields using a descriptor that separates get and set types:

T = TypeVar("T", bound=CoreNode)

class RelationshipField(Generic[T]):
    def __get__(self, obj: object, owner: type | None = None) -> RelatedNode[T]: ...
    def __set__(self, obj: object, value: str | T | RelatedNode[T]) -> None: ...

so generated protocols can declare ip_prefix: RelationshipField[BuiltinIPPrefix], allowing assignment of an id string or a node while still reading back a typed RelatedNode. (This composes naturally with the generic-RelatedNode change proposed in the .peer issue.)

A lighter-weight alternative is widening the annotation to a documented assignable union (RelatedNode | str | CoreNode), at the cost of a looser read type.

Downstream workaround (interim)

Suppress per line, with a comment noting it's a stub limitation:

addr.ip_prefix = "some-prefix-id"  # type: ignore[assignment]  # SDK stub types rel as read-only RelatedNode
addr.ip_namespace = other          # type: ignore[assignment]

There is no clean cast-based alternative, because the mismatch is on the assignment target (the attribute's declared type), not on the value being assigned. Once relationships are generated with separate get/set types, these ignores can be removed.


Filed separately: companion issues for type-abstract on kind= and for RelatedNode.peer losing the peer type on traversal.

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