@@ -1332,25 +1332,189 @@ def test_model_from_dict_includes_relationship_attributes() -> None:
13321332 assert "books" in attrs_keys , "books relationship should be in attrs.keys()"
13331333
13341334
1335- def test_model_from_dict_with_relationship_data () -> None :
1336- """Test that model_from_dict can handle relationship data."""
1337- from tests .fixtures .uuid .models import UUIDAuthor , UUIDBook
1335+ # Tests for write_only relationship handling in update method (issue #524)
13381336
1339- # Create test data with relationship
1340- book1 = UUIDBook (title = "Test Book 1" , author_id = "dummy-uuid" )
1341- book2 = UUIDBook (title = "Test Book 2" , author_id = "dummy-uuid" )
13421337
1343- author_data = {"name" : "Test Author" , "string_field" : "test value" , "books" : [book1 , book2 ]}
1338+ async def test_update_skips_write_only_relationships (
1339+ mock_repo : SQLAlchemyAsyncRepository [Any ],
1340+ mocker : MockerFixture ,
1341+ ) -> None :
1342+ """Test that update method skips write_only relationships without error."""
1343+ id_ = 3
1344+ mock_instance = MagicMock ()
1345+ existing_instance = MagicMock ()
13441346
1345- # model_from_dict should handle the relationship attribute
1346- author = model_from_dict (UUIDAuthor , ** author_data )
1347+ # Mock the mapper and relationship
1348+ mock_mapper = MagicMock ()
1349+ mock_relationship = MagicMock ()
1350+ mock_relationship .key = "items"
1351+ mock_relationship .lazy = "write_only"
1352+ mock_relationship .viewonly = False
1353+ mock_mapper .mapper .columns = []
1354+ mock_mapper .mapper .relationships = [mock_relationship ]
13471355
1348- assert author .name == "Test Author"
1349- assert author .string_field == "test value"
1350- assert hasattr (author , "books" ), "Author should have books attribute"
1351- assert len (author .books ) == 2
1352- assert author .books [0 ].title == "Test Book 1"
1353- assert author .books [1 ].title == "Test Book 2"
1356+ # Mock the data object to have the write_only relationship attribute
1357+ mock_instance .items = MagicMock () # This would be a WriteOnlyCollection in reality
1358+
1359+ mocker .patch .object (mock_repo , "get_id_attribute_value" , return_value = id_ )
1360+ mocker .patch .object (mock_repo , "get" , return_value = existing_instance )
1361+ mocker .patch ("advanced_alchemy.repository._async.inspect" , return_value = mock_mapper )
1362+ mock_repo .session .merge .return_value = existing_instance # pyright: ignore[reportFunctionMemberAccess]
1363+
1364+ # This should not raise an error even though items is a write_only relationship
1365+ instance = await maybe_async (mock_repo .update (mock_instance ))
1366+
1367+ # Verify the relationship was not processed (no merge attempted for relationships)
1368+ mock_repo .session .merge .assert_called_once_with (existing_instance , load = True ) # pyright: ignore[reportFunctionMemberAccess]
1369+ assert instance is existing_instance
1370+
1371+
1372+ async def test_update_skips_dynamic_relationships (
1373+ mock_repo : SQLAlchemyAsyncRepository [Any ],
1374+ mocker : MockerFixture ,
1375+ ) -> None :
1376+ """Test that update method skips dynamic relationships without error."""
1377+ id_ = 3
1378+ mock_instance = MagicMock ()
1379+ existing_instance = MagicMock ()
1380+
1381+ # Mock the mapper and relationship
1382+ mock_mapper = MagicMock ()
1383+ mock_relationship = MagicMock ()
1384+ mock_relationship .key = "items"
1385+ mock_relationship .lazy = "dynamic"
1386+ mock_relationship .viewonly = False
1387+ mock_mapper .mapper .columns = []
1388+ mock_mapper .mapper .relationships = [mock_relationship ]
1389+
1390+ # Mock the data object to have the dynamic relationship attribute
1391+ mock_instance .items = MagicMock () # This would be an AppenderQuery in reality
1392+
1393+ mocker .patch .object (mock_repo , "get_id_attribute_value" , return_value = id_ )
1394+ mocker .patch .object (mock_repo , "get" , return_value = existing_instance )
1395+ mocker .patch ("advanced_alchemy.repository._async.inspect" , return_value = mock_mapper )
1396+ mock_repo .session .merge .return_value = existing_instance # pyright: ignore[reportFunctionMemberAccess]
1397+
1398+ # This should not raise an error even though items is a dynamic relationship
1399+ instance = await maybe_async (mock_repo .update (mock_instance ))
1400+
1401+ # Verify the relationship was not processed (no merge attempted for relationships)
1402+ mock_repo .session .merge .assert_called_once_with (existing_instance , load = True ) # pyright: ignore[reportFunctionMemberAccess]
1403+ assert instance is existing_instance
1404+
1405+
1406+ async def test_update_skips_viewonly_relationships (
1407+ mock_repo : SQLAlchemyAsyncRepository [Any ],
1408+ mocker : MockerFixture ,
1409+ ) -> None :
1410+ """Test that update method skips viewonly relationships without error."""
1411+ id_ = 3
1412+ mock_instance = MagicMock ()
1413+ existing_instance = MagicMock ()
1414+
1415+ # Mock the mapper and relationship
1416+ mock_mapper = MagicMock ()
1417+ mock_relationship = MagicMock ()
1418+ mock_relationship .key = "readonly_items"
1419+ mock_relationship .lazy = "select" # Normal lazy loading
1420+ mock_relationship .viewonly = True # But marked as view-only
1421+ mock_mapper .mapper .columns = []
1422+ mock_mapper .mapper .relationships = [mock_relationship ]
1423+
1424+ # Mock the data object to have the viewonly relationship attribute
1425+ mock_instance .readonly_items = [MagicMock ()]
1426+
1427+ mocker .patch .object (mock_repo , "get_id_attribute_value" , return_value = id_ )
1428+ mocker .patch .object (mock_repo , "get" , return_value = existing_instance )
1429+ mocker .patch ("advanced_alchemy.repository._async.inspect" , return_value = mock_mapper )
1430+ mock_repo .session .merge .return_value = existing_instance # pyright: ignore[reportFunctionMemberAccess]
1431+
1432+ # This should not raise an error even though readonly_items is viewonly
1433+ instance = await maybe_async (mock_repo .update (mock_instance ))
1434+
1435+ # Verify the relationship was not processed (no merge attempted for relationships)
1436+ mock_repo .session .merge .assert_called_once_with (existing_instance , load = True ) # pyright: ignore[reportFunctionMemberAccess]
1437+ assert instance is existing_instance
1438+
1439+
1440+ async def test_update_skips_raise_lazy_relationships (
1441+ mock_repo : SQLAlchemyAsyncRepository [Any ],
1442+ mocker : MockerFixture ,
1443+ ) -> None :
1444+ """Test that update method skips raise lazy strategy relationships without error."""
1445+ id_ = 3
1446+ mock_instance = MagicMock ()
1447+ existing_instance = MagicMock ()
1448+
1449+ # Mock the mapper and relationship
1450+ mock_mapper = MagicMock ()
1451+ mock_relationship = MagicMock ()
1452+ mock_relationship .key = "items"
1453+ mock_relationship .lazy = "raise"
1454+ mock_relationship .viewonly = False
1455+ mock_mapper .mapper .columns = []
1456+ mock_mapper .mapper .relationships = [mock_relationship ]
1457+
1458+ # Mock the data object to have the raise relationship attribute
1459+ mock_instance .items = MagicMock ()
1460+
1461+ mocker .patch .object (mock_repo , "get_id_attribute_value" , return_value = id_ )
1462+ mocker .patch .object (mock_repo , "get" , return_value = existing_instance )
1463+ mocker .patch ("advanced_alchemy.repository._async.inspect" , return_value = mock_mapper )
1464+ mock_repo .session .merge .return_value = existing_instance # pyright: ignore[reportFunctionMemberAccess]
1465+
1466+ # This should not raise an error even though items has lazy="raise"
1467+ instance = await maybe_async (mock_repo .update (mock_instance ))
1468+
1469+ # Verify the relationship was not processed (no merge attempted for relationships)
1470+ mock_repo .session .merge .assert_called_once_with (existing_instance , load = True ) # pyright: ignore[reportFunctionMemberAccess]
1471+ assert instance is existing_instance
1472+
1473+
1474+ async def test_update_processes_normal_relationships (
1475+ mock_repo : SQLAlchemyAsyncRepository [Any ],
1476+ mocker : MockerFixture ,
1477+ ) -> None :
1478+ """Test that update method still processes normal relationships correctly."""
1479+ id_ = 3
1480+ mock_instance = MagicMock ()
1481+ existing_instance = MagicMock ()
1482+ related_item = MagicMock ()
1483+ merged_related_item = MagicMock ()
1484+
1485+ # Mock the mapper and relationship
1486+ mock_mapper = MagicMock ()
1487+ mock_relationship = MagicMock ()
1488+ mock_relationship .key = "items"
1489+ mock_relationship .lazy = "select" # Normal lazy loading
1490+ mock_relationship .viewonly = False
1491+ mock_mapper .mapper .columns = []
1492+ mock_mapper .mapper .relationships = [mock_relationship ]
1493+
1494+ # Mock the data object to have a normal relationship with items
1495+ mock_instance .items = [related_item ]
1496+
1497+ mocker .patch .object (mock_repo , "get_id_attribute_value" , return_value = id_ )
1498+ mocker .patch .object (mock_repo , "get" , return_value = existing_instance )
1499+ mocker .patch ("advanced_alchemy.repository._async.inspect" , return_value = mock_mapper )
1500+
1501+ # Mock session.merge to return different objects for main instance vs related items
1502+ async def mock_merge (obj : Any , load : bool = True ) -> Any :
1503+ if obj is existing_instance :
1504+ return existing_instance
1505+ if obj is related_item :
1506+ return merged_related_item
1507+ return obj
1508+
1509+ mock_repo .session .merge .side_effect = mock_merge
1510+
1511+ # This should process the normal relationship correctly
1512+ instance = await maybe_async (mock_repo .update (mock_instance ))
1513+
1514+ # Verify the relationship was processed - at minimum the main instance should be merged
1515+ assert mock_repo .session .merge .call_count >= 1 # At least the main instance
1516+ # The main point is that normal relationships don't cause errors
1517+ assert instance is existing_instance
13541518
13551519
13561520def test_model_from_dict_backward_compatibility () -> None :
0 commit comments