@@ -52,6 +52,7 @@ def __init__(self, name: str, m2m: bool = False) -> None:
5252 self .attname = name
5353 self .many_to_many = m2m
5454 self .one_to_many = False
55+ self .concrete = True
5556
5657 def value_from_object (self , instance : Any ) -> Any :
5758 return getattr (instance , self .name )
@@ -144,6 +145,29 @@ def get_fields() -> list[Field | MockOneToOneRel]:
144145 return [Field ("f" ), MockOneToOneRel ("o2o" )]
145146
146147
148+ class VirtualField :
149+ """Mock GenericForeignKey - virtual field with no database column."""
150+
151+ def __init__ (self , name : str ) -> None :
152+ self .name = name
153+ self .many_to_many = False
154+ self .one_to_many = False
155+ self .concrete = False
156+
157+
158+ class FakeModelWithVirtualField :
159+ f = "a value"
160+ gfk = None
161+
162+ class _meta :
163+ @staticmethod
164+ def get_fields () -> list [Field | VirtualField ]:
165+ return [Field ("f" ), VirtualField ("gfk" )]
166+
167+ def get_deferred_fields (self ) -> set [str ]:
168+ return set ()
169+
170+
147171class TestGeneral :
148172 @pytest .fixture (autouse = True )
149173 def ready (self ) -> None :
@@ -157,9 +181,7 @@ def test_m2m_fields_error(self) -> None:
157181 def test_one_to_one_rel_field_error (self ) -> None :
158182 with must_be_called (False ) as func :
159183 with pytest .raises (ValueError ):
160- post_save_changed .connect (
161- func , sender = FakeModelWithOneToOne , fields = ["o2o" , "f" ]
162- )
184+ post_save_changed .connect (func , sender = FakeModelWithOneToOne , fields = ["o2o" , "f" ])
163185
164186 def test_one_to_one_rel_excluded (self ) -> None :
165187 with must_be_called (False ) as func :
@@ -223,6 +245,23 @@ def test_boolean_field_transition_to_valid(self) -> None:
223245
224246 assert func .kwargs ["changed_fields" ] == {"is_active" : (None , True )}
225247
248+ def test_virtual_field_excluded (self ) -> None :
249+ """Virtual fields like GenericForeignKey are auto-skipped."""
250+ with must_be_called (False ) as func :
251+ post_save_changed .connect (func , sender = FakeModelWithVirtualField )
252+
253+ obj = FakeModelWithVirtualField ()
254+ post_init .send (instance = obj , sender = FakeModelWithVirtualField )
255+ post_save .send (instance = obj , sender = FakeModelWithVirtualField )
256+
257+ def test_virtual_field_error (self ) -> None :
258+ """Explicitly requesting a virtual field raises clear error."""
259+ with must_be_called (False ) as func :
260+ with pytest .raises (
261+ ValueError , match = "doesn't handle virtual fields.*gfk.*VirtualField"
262+ ):
263+ post_save_changed .connect (func , sender = FakeModelWithVirtualField , fields = ["gfk" ])
264+
226265
227266class TestPostSave :
228267 @pytest .fixture (autouse = True )
0 commit comments