From 01ed925b72970ee504475c2cac43ce0c02ca8e9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 15 Nov 2025 00:43:13 +0000 Subject: [PATCH 1/2] Initial plan From f0ee3cd55f5e9b575b501cd7b59d66208fafb587 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 15 Nov 2025 00:54:04 +0000 Subject: [PATCH 2/2] Fix optional attributes not being set when using object templates - Modified _generate_input_data() to skip adding None for optional relationships on new nodes - For existing nodes, still include None to allow clearing relationships (preserves PR #515 behavior) - Updated tests to reflect new behavior - Added test to verify existing nodes still work correctly Co-authored-by: minitriga <26367336+minitriga@users.noreply.github.com> --- infrahub_sdk/node/node.py | 7 +++++-- tests/unit/sdk/test_node.py | 37 ++++++++++++++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/infrahub_sdk/node/node.py b/infrahub_sdk/node/node.py index 72624467..16e55ebf 100644 --- a/infrahub_sdk/node/node.py +++ b/infrahub_sdk/node/node.py @@ -192,7 +192,7 @@ def is_resource_pool(self) -> bool: def get_raw_graphql_data(self) -> dict | None: return self._data - def _generate_input_data( # noqa: C901 + def _generate_input_data( # noqa: C901, PLR0915 self, exclude_unmodified: bool = False, exclude_hfid: bool = False, @@ -235,7 +235,10 @@ def _generate_input_data( # noqa: C901 rel: RelatedNodeBase | RelationshipManagerBase = getattr(self, item_name) if rel_schema.cardinality == RelationshipCardinality.ONE and rel_schema.optional and not rel.initialized: - data[item_name] = None + # Only include None for existing nodes to allow clearing relationships + # For new nodes, omit the field to allow object template defaults to be applied + if self._existing: + data[item_name] = None continue if rel is None or not rel.initialized: diff --git a/tests/unit/sdk/test_node.py b/tests/unit/sdk/test_node.py index e4192871..7572de32 100644 --- a/tests/unit/sdk/test_node.py +++ b/tests/unit/sdk/test_node.py @@ -1365,7 +1365,6 @@ async def test_create_input_data(client, location_schema: NodeSchemaAPI, client_ "name": {"value": "JFK1"}, "description": {"value": "JFK Airport"}, "type": {"value": "SITE"}, - "primary_tag": None, } } @@ -1393,6 +1392,38 @@ async def test_create_input_data_with_dropdown(client, location_schema_with_drop "description": {"value": "JFK Airport"}, "type": {"value": "SITE"}, "status": {"value": None}, + } + } + + +@pytest.mark.parametrize("client_type", client_types) +async def test_update_input_data_existing_node_with_optional_relationship( + client, location_schema: NodeSchemaAPI, client_type +) -> None: + """Validate that existing nodes include None for uninitialized optional relationships. + + This ensures that we can explicitly clear optional relationships when updating existing nodes. + """ + # Simulate an existing node by including an id + data = { + "id": "existing-node-id", + "name": {"value": "JFK1"}, + "description": {"value": "JFK Airport"}, + "type": {"value": "SITE"}, + } + + if client_type == "standard": + node = InfrahubNode(client=client, schema=location_schema, data=data) + else: + node = InfrahubNodeSync(client=client, schema=location_schema, data=data) + + # For existing nodes, optional uninitialized relationships should include None + assert node._generate_input_data()["data"] == { + "data": { + "id": "existing-node-id", + "name": {"value": "JFK1"}, + "description": {"value": "JFK Airport"}, + "type": {"value": "SITE"}, "primary_tag": None, } } @@ -1641,7 +1672,7 @@ async def test_create_input_data_with_IPHost_attribute(client, ipaddress_schema, ip_address = InfrahubNodeSync(client=client, schema=ipaddress_schema, data=data) assert ip_address._generate_input_data()["data"] == { - "data": {"address": {"value": "1.1.1.1/24", "is_protected": True}, "interface": None} + "data": {"address": {"value": "1.1.1.1/24", "is_protected": True}} } @@ -1655,7 +1686,7 @@ async def test_create_input_data_with_IPNetwork_attribute(client, ipnetwork_sche ip_network = InfrahubNodeSync(client=client, schema=ipnetwork_schema, data=data) assert ip_network._generate_input_data()["data"] == { - "data": {"network": {"value": "1.1.1.0/24", "is_protected": True}, "site": None} + "data": {"network": {"value": "1.1.1.0/24", "is_protected": True}} }