Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/1152.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Assigning to a cardinality-many relationship (e.g. `node.members = [peer]`) now populates the relationship manager, and assigning a non-list value raises a clear error at assignment time instead of being silently accepted and failing later in `save()` with a confusing `'InfrahubNode' object has no attribute 'initialized'` message.
46 changes: 46 additions & 0 deletions infrahub_sdk/node/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,29 @@ def __setattr__(self, name: str, value: Any) -> None:
self._relationship_cardinality_one_data[name] = new_rel
return

if "_relationship_cardinality_many_data" in self.__dict__ and name in self._relationship_cardinality_many_data:
rel_schemas = [rel_schema for rel_schema in self._schema.relationships if rel_schema.name == name]
if not rel_schemas:
raise SchemaNotFoundError(
identifier=self._schema.kind,
message=f"Unable to find relationship schema for '{name}' on {self._schema.kind}",
)
rel_schema = rel_schemas[0]
# RelationshipManager validates that a cardinality-many value is a list and raises a
# helpful error otherwise, so assigning a single node fails here instead of silently
# corrupting the node and blowing up later in save().
new_many_rel = RelationshipManager(
name=rel_schema.name,
client=self._client,
node=self,
branch=self._branch,
schema=rel_schema,
data=value,
)
new_many_rel._has_update = True
self._relationship_cardinality_many_data[name] = new_many_rel
return

super().__setattr__(name, value)

async def generate(self, nodes: list[str] | None = None) -> None:
Expand Down Expand Up @@ -2169,6 +2192,29 @@ def __setattr__(self, name: str, value: Any) -> None:
self._relationship_cardinality_one_data[name] = new_rel
return

if "_relationship_cardinality_many_data" in self.__dict__ and name in self._relationship_cardinality_many_data:
rel_schemas = [rel_schema for rel_schema in self._schema.relationships if rel_schema.name == name]
if not rel_schemas:
raise SchemaNotFoundError(
identifier=self._schema.kind,
message=f"Unable to find relationship schema for '{name}' on {self._schema.kind}",
)
rel_schema = rel_schemas[0]
# RelationshipManagerSync validates that a cardinality-many value is a list and raises a
# helpful error otherwise, so assigning a single node fails here instead of silently
# corrupting the node and blowing up later in save().
new_many_rel = RelationshipManagerSync(
name=rel_schema.name,
client=self._client,
node=self,
branch=self._branch,
schema=rel_schema,
data=value,
)
new_many_rel._has_update = True
self._relationship_cardinality_many_data[name] = new_many_rel
return

super().__setattr__(name, value)

def generate(self, nodes: list[str] | None = None) -> None:
Expand Down
36 changes: 36 additions & 0 deletions tests/unit/sdk/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,42 @@ async def test_cardinality_many_accepts_list(
assert len(node.tags.peers) == 2


@pytest.mark.parametrize("client_type", client_types)
async def test_cardinality_many_assignment_requires_list(
client: InfrahubClient, location_schema: NodeSchemaAPI, client_type: str
) -> None:
"""Assigning a single node to a cardinality-many relationship must fail at assignment time.

Previously this was silently accepted and only blew up later in save() with a confusing
"'InfrahubNode' object has no attribute 'initialized'" error.
"""
if client_type == "standard":
node = InfrahubNode(client=client, schema=location_schema, data={"name": {"value": "JFK1"}})
else:
node = InfrahubNodeSync(client=client, schema=location_schema, data={"name": {"value": "JFK1"}})

with pytest.raises(ValueError, match=r"expects a list of nodes"):
node.tags = {"id": "pppppppp"}


@pytest.mark.parametrize("client_type", client_types)
async def test_cardinality_many_assignment_accepts_list(
client: InfrahubClient, location_schema: NodeSchemaAPI, client_type: str
) -> None:
"""Assigning a list to a cardinality-many relationship populates the manager and marks it updated."""
if client_type == "standard":
node = InfrahubNode(client=client, schema=location_schema, data={"name": {"value": "JFK1"}})
else:
node = InfrahubNodeSync(client=client, schema=location_schema, data={"name": {"value": "JFK1"}})

node.tags = [{"id": "aaaaaa"}, {"id": "bbbb"}]

assert isinstance(node.tags, RelationshipManagerBase)
assert node.tags.peer_ids == ["aaaaaa", "bbbb"]
# has_update must be set so save() actually persists the assignment instead of stripping it.
assert node.tags.has_update is True


@pytest.mark.parametrize("client_type", client_types)
async def test_query_data_no_filters_property(
clients: BothClients, location_schema: NodeSchemaAPI, client_type: str
Expand Down
Loading