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:
- 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.
- 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).
Component
Python SDK
Describe the Feature Request
Add a method to
RelationshipManagerBase(and theRelationshipManager/RelationshipManagerSyncsubclasses) that wipes all peers from a many-relationship and sets_has_update = Trueso thatnode.save()actually persists the change.Concretely, either:
clear()method that drops all peers, orreplace(iterable)helper that drops all peers and replaces them with the supplied set in one call,or both. Today the manager exposes
add,extend, andremove, 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:
node.my_rel.peers = []and thenextend(...). This is terse, but bypasses_has_update. It works only if the subsequentextendis non-empty; if the new list happens to be empty, the wipe is silently dropped in_strip_unmodified(seeinfrahub_sdk/node/node.py:457) and the old peers remain on the server. This is a silent-no-op footgun.list(rm.peers)and callremove()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 ideallyreplace(iterable)) would:_has_update = Truesosave()persists the changepeers = []followed by an emptyextend()does nothing.peers = []Example of the kind of caller code this would simplify:
…vs. today's pattern, which either has the empty-list bug:
…or the verbose-but-correct alternative:
Additional Information
Relevant code paths in
stable:RelationshipManagerBasedefines the public mutation API:add,extend,remove(infrahub_sdk/node/relationship.py). Bothaddandremovesetself._has_update = Trueon 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 whenrelationship_property.has_updateisFalse:This is what makes the "assign
peers = []then save" pattern a silent no-op when no follow-upadd/extendruns.Suggested implementation (sketch, on
RelationshipManagerBaseor on each subclass to match the existingadd/removeplacement):This change is fully backward-compatible — it only adds methods, and direct manipulation of
.peerscontinues to work the way it does today (footgun included, until callers migrate).