Skip to content

bug: passing a generated protocol as kind= triggers mypy type-abstract on every client call #1062

@FragmentedPacket

Description

@FragmentedPacket

Component

Python SDK, infrahubctl

Infrahub SDK version

1.20.1

Current Behavior

The documented way to call client.get / client.filters / client.all / client.allocate_next_ip_prefix is to pass a generated protocol class (e.g. CoreIPPrefixPool, NetworkPod) as the kind= argument. Under mypy this emits a type-abstract error on every such call.

The generated protocols ultimately subclass CoreNode, which is a typing.Protocol, and kind is typed type[SchemaType]. mypy forbids passing an abstract/Protocol type where type[X] (an instantiable type) is expected.

Root cause:

  • infrahub_sdk/protocols_base.pyclass CoreNode(CoreNodeBase, Protocol): ...
  • infrahub_sdk/client.pySchemaType = TypeVar("SchemaType", bound=CoreNode), and def get(self, kind: type[SchemaType], ...)

Because CoreNode is a Protocol, every generated subclass is non-instantiable from mypy's perspective, so passing it where type[SchemaType] is expected is exactly what type-abstract flags. This affects any downstream project that follows the documented typed-client pattern and runs mypy — it appears on essentially every client call.

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

Expected Behavior

Calling the client with a generated protocol as kind= — the documented, intended API — should type-check cleanly without per-call suppressions.

Steps to Reproduce

from infrahub_sdk import InfrahubClient
from infrahub_sdk.protocols import CoreIPPrefixPool


async def main(client: InfrahubClient) -> None:
    # Documented usage: pass a generated protocol as `kind=`.
    await client.get(kind=CoreIPPrefixPool, name__value="pool")

Run mypy (infrahub-sdk 1.20.1):

error: Only concrete class can be given where "type[CoreIPPrefixPool]" is expected  [type-abstract]
    await client.get(kind=CoreIPPrefixPool, name__value="pool")
                          ^~~~~~~~~~~~~~~~

Additional Information

Impact

  • Affects all SDK consumers using the typed client + mypy.
  • Forces either ~one # type: ignore[type-abstract] per client call, or a project-wide disable_error_code = ["type-abstract"], which also masks genuine abstract-instantiation mistakes elsewhere.

Suggested fixes (for discussion)

  1. Document the recommended disable_error_code = ["type-abstract"] (or per-call ignore) as the supported pattern, with rationale, in the typed-client docs.
  2. Typing-level: provide client method overloads whose kind parameter accepts protocol types without tripping type-abstract, if a clean expression is achievable.
  3. Generation-level: reconsider whether generated node classes must be Protocol (structural) vs concrete nominal classes — concrete classes would not trigger type-abstract. (Broader implications; raised for completeness.)

Option 1 is the cheapest and unblocks consumers immediately.

Downstream workaround (interim)

Per-call suppression (surgical):

pool = await client.get(kind=CoreIPPrefixPool, name__value="pool")  # type: ignore[type-abstract]

Or project-wide (one line, but also silences genuine abstract-instantiation mistakes):

# pyproject.toml
[tool.mypy]
disable_error_code = ["type-abstract"]

Two related typing issues were found in the same downstream upgrade (filed separately): RelatedNode.peer returning InfrahubNode (loses peer type on traversal), and relationship-attribute assignment being rejected.

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