Skip to content

Generated sync protocols inherit async base classes #1276

Description

@ogenstad

Summary

Protocols generated for a sync client can inherit an async base class. The generator switches a
relationship's peer to its *Sync variant whenever one exists, but for an inheritance list it only
switches the three names in CORE_BASE_CLASS_TO_SYNCIFY (CoreProfile, CoreObjectTemplate,
CoreNode). Every other core kind in an inherit_from list keeps its async name, so the generated
sync class ends up with CoreNode in its MRO instead of CoreNodeSync.

The user-visible effect is that type checking a sync codebase fails on correct code.

Reproduction

infrahubctl protocols --sync against any schema whose nodes inherit a core generic, or a schema
with profiles or object templates. With the schema in tests/fixtures/schema_05.json the generated
sync file contains:

class IpamIPAddress(BuiltinIPAddress):        # should be BuiltinIPAddressSync
class IpamPrefix(BuiltinIPPrefix):            # should be BuiltinIPPrefixSync
class TemplateInfraDevice(LineageSource, CoreObjectTemplateSync, CoreNodeSync):   # LineageSourceSync
class ProfileLocationSite(LineageSource, CoreProfileSync, CoreNodeSync):          # LineageSourceSync

14 inheritance references are affected in that one fixture: BuiltinIPAddress and
BuiltinIPPrefix once each, and LineageSource twelve times. Note the mixed bases on the last two,
where the same class correctly gets CoreProfileSync and CoreNodeSync alongside an async
LineageSource.

Then type check code that uses one of them with a sync client:

from infrahub_sdk.protocols import BuiltinIPAddress


class IpamIPAddress(BuiltinIPAddress):   # what the generator emits into the sync file
    pass


def f(node: IpamIPAddress) -> None:
    node.save()
$ uv run mypy repro.py
repro.py:9: error: Value of type "Coroutine[Any, Any, None]" must be used  [unused-coroutine]
repro.py:9: note: Are you missing an await?

node.save resolves to CoreNode.save, which is async def, so a correct sync call is reported as
a missing await. Swap the base for BuiltinIPAddressSync and it type checks clean. Only typing is
affected; nothing changes at runtime, because these classes are never instantiated.

Root cause

infrahub_sdk/protocols_generator/generator.py, in _jinja2_filter_syncify. The list branch tests
membership of _inherited_sync_names, which for a user schema is CORE_BASE_CLASS_TO_SYNCIFY:

if isinstance(value, list):
    return [f"{item}Sync" if item in self._inherited_sync_names else item for item in value]

_jinja2_filter_render_relationship uses the other set, _peer_sync_names, which is every base
protocol that has a Sync counterpart. That is why peers are switched correctly and bases are not.
The two sets are deliberately separate today to preserve existing output; see the comment where they
are built.

Suggested fix

Use the _peer_sync_names rule for inheritance lists as well, so any base protocol with a *Sync
counterpart is switched. CORE_BASE_CLASS_TO_SYNCIFY is then only needed for its other job,
ordering the bases so the core ones come last, and the two name sets in CodeGenerator collapse
into one.

Worth checking while in there: whether ordering still needs its own list, and whether
_inherited_sync_names can be deleted outright.

Notes for whoever picks this up

  • This changes the output of infrahubctl protocols, so tests/fixtures/protocols_generator/user_schema_sync.txt
    will move. That fixture is a full-file golden, so its diff is the review surface: expect exactly
    the 14 references above to change and nothing else. user_schema_async.txt must not change at all.
  • Users regenerating after the fix may see new type errors in their own code, since a sync class that
    previously exposed async save/delete/create will start exposing the sync ones. That is the
    point of the fix, but it belongs in the changelog as a fixed entry with that consequence spelled
    out.
  • Found while working on IFC-3054 and deliberately left out of feat(protocols): support generating the SDK core protocols module [IFC-3054] #1273,
    which had to guarantee byte-identical output for user schemas.

Metadata

Metadata

Assignees

Labels

effort/lowThis issue should be completed in a couple of hoursstate/need-triageThis issue needs to be triagedtype/bugSomething isn't working as expected

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions