Skip to content

Feature: Add clear() / replace() method to RelationshipManager that sets _has_update #1031

@PhillSimonds

Description

@PhillSimonds

Component

Python SDK

Describe the Feature Request

Add a method to RelationshipManagerBase (and the RelationshipManager / RelationshipManagerSync subclasses) that wipes all peers from a many-relationship and sets _has_update = True so that node.save() actually persists the change.

Concretely, either:

  • a clear() method that drops all peers, or
  • a replace(iterable) helper that drops all peers and replaces them with the supplied set in one call,

or both. Today the manager exposes add, extend, and remove, but nothing for "wipe everything," which forces callers into patterns that are either kludgy or quietly incorrect.

Describe the Use Case

A very common pattern when syncing data into Infrahub is "replace the entire peer set of a relationship with a fresh list." Today the only ways to do this are:

  1. Directly assign node.my_rel.peers = [] and then extend(...). This is terse, but bypasses _has_update. It works only if the subsequent extend is non-empty; if the new list happens to be empty, the wipe is silently dropped in _strip_unmodified (see infrahub_sdk/node/node.py:457) and the old peers remain on the server. This is a silent-no-op footgun.
  2. Loop over list(rm.peers) and call remove() on each peer. This is verbose and surprising for callers who reasonably expect a list-like API to support clearing.

Neither pattern is great. A first-class clear() (and ideally replace(iterable)) would:

  • be the obvious idiomatic way to wipe a many-relationship
  • correctly set _has_update = True so save() persists the change
  • eliminate the silent-no-op footgun where assigning peers = [] followed by an empty extend() does nothing
  • match the mental model that callers already have when they reach for .peers = []

Example of the kind of caller code this would simplify:

node.destination_services.replace(
    [{"hfid": [peer]} for peer in data.destination_services]
)
node.save()

…vs. today's pattern, which either has the empty-list bug:

node.destination_services.peers = []
node.destination_services.extend(
    [{"hfid": [peer]} for peer in data.destination_services]
)
node.save()

…or the verbose-but-correct alternative:

rm = node.destination_services
for peer in list(rm.peers):
    rm.remove(peer)
rm.extend([{"hfid": [peer]} for peer in data.destination_services])
node.save()

Additional Information

Relevant code paths in stable:

  • RelationshipManagerBase defines the public mutation API: add, extend, remove (infrahub_sdk/node/relationship.py). Both add and remove set self._has_update = True on success; there is no method for wiping all peers.
  • node._strip_unmodified (infrahub_sdk/node/node.py:447-460) explicitly drops any relationship from the GraphQL mutation payload when relationship_property.has_update is False:
) or (isinstance(relationship_property, RelationshipManagerBase) and not relationship_property.has_update):
    data.pop(relationship)

This is what makes the "assign peers = [] then save" pattern a silent no-op when no follow-up add/extend runs.

Suggested implementation (sketch, on RelationshipManagerBase or on each subclass to match the existing add/remove placement):

def clear(self) -> None:
    """Remove all peers from this relationship."""
    if not self.initialized:
        raise UninitializedError("Must call fetch() on RelationshipManager before editing members")
    if self.peers:
        self.peers = []
        self._has_update = True

def replace(self, data: Iterable[str | RelatedNode | dict]) -> None:
    """Replace the full set of peers with the supplied iterable."""
    self.clear()
    self.extend(data)

This change is fully backward-compatible — it only adds methods, and direct manipulation of .peers continues to work the way it does today (footgun included, until callers migrate).

Metadata

Metadata

Assignees

No one assigned

    Labels

    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