@@ -41,14 +41,24 @@ def connect( # type: ignore[override]
4141 """
4242
4343 if not apps .models_ready :
44+ # We require access to Model._meta.get_fields(), which isn't available yet.
45+ # (This error would be raised below anyway, but we want a more meaningful message)
4446 raise AppRegistryNotReady (
4547 "django-fieldsignals signals must be connected after the app cache is ready. "
4648 "Connect the signal in your AppConfig.ready() handler."
4749 )
4850
51+ # Validate arguments
52+
4953 if kwargs .get ("weak" , False ):
54+ # TODO: weak refs? I'm hella confused.
55+ # We can't go passing our proxy receivers around as weak refs, since they're
56+ # defined as closures and hence don't exist by the time they're called.
57+ # However, we can probably make _make_proxy_receiver() create weakrefs to
58+ # the original receiver if required. Patches welcome
5059 raise NotImplementedError ("This signal doesn't yet handle weak refs" )
5160
61+ # Check it's a class, don't check if it's a model class (useful for tests)
5262 if not isinstance (sender , type ):
5363 raise ValueError ("sender should be a model class" )
5464
@@ -74,6 +84,7 @@ def is_reverse_rel(f: Any) -> bool:
7484
7585 super ().connect (proxy_receiver , sender = sender , weak = False , dispatch_uid = dispatch_uid )
7686
87+ ### post_init : initialize the list of fields for each instance
7788 def post_init_closure (sender : type , instance : models .Model , ** kwargs : Any ) -> None :
7889 self .get_and_update_changed_fields (receiver , instance , resolved_fields )
7990
@@ -89,7 +100,10 @@ def connect_source_signals(self, sender: type) -> None:
89100 """
90101 Connects the source signals required to trigger updates for this
91102 ChangedSignal.
103+
104+ (post_init has already been connected during __init__)
92105 """
106+ # override in subclasses
93107 pass
94108
95109 def _make_proxy_receiver (
@@ -100,7 +114,8 @@ def _make_proxy_receiver(
100114 ) -> Callable [..., Any ]:
101115 """
102116 Takes a receiver function and creates a closure around it that knows what fields
103- to watch.
117+ to watch. The original receiver is called for an instance iff the value of
118+ at least one of the fields has changed since the last time it was called.
104119 """
105120
106121 def pr (instance : Any , * args : Any , ** kwargs : Any ) -> None :
@@ -115,9 +130,9 @@ def pr(instance: Any, *args: Any, **kwargs: Any) -> None:
115130
116131 pr ._original_receiver = receiver # type: ignore[attr-defined]
117132 pr ._fields = fields # type: ignore[attr-defined]
133+
118134 pr .__doc__ = receiver .__doc__
119135 pr .__name__ = receiver .__name__
120-
121136 return pr
122137
123138 def get_and_update_changed_fields (
@@ -128,8 +143,18 @@ def get_and_update_changed_fields(
128143 ) -> dict [str , tuple [Any , Any ]]:
129144 """
130145 Takes a receiver and a model instance, and a list of field instances.
131- Returns a dict of changed fields: {"fieldname": ("old value", "new value")}
146+ Gets the old and new values for each of the given fields, and stores their
147+ new values for next time.
148+
149+ Returns a dict like this:
150+ {
151+ "fieldname1" : ("old value", "new value"),
152+ }
132153 """
154+ # instance._fieldsignals_originals looks like this:
155+ # {
156+ # (id(<signal instance>), id(<receiver>)) : {"field_name": "old value",},
157+ # }
133158 key = (id (self ), id (receiver ))
134159 if not hasattr (instance , "_fieldsignals_originals" ):
135160 instance ._fieldsignals_originals = {}
@@ -143,13 +168,18 @@ def get_and_update_changed_fields(
143168 for field in fields :
144169 if field .attname in deferred_fields :
145170 continue
171+ # using value_from_object instead of getattr() means we don't traverse foreignkeys
146172 new_value = field .to_python (field .value_from_object (instance ))
147173 old_value = originals .get (field .name , None )
148174 if old_value != new_value :
149175 if not isinstance (new_value , IMMUTABLE_TYPES_WHITELIST ):
176+ # For mutable types, make a copy of the value before storing it.
177+ # Otherwise, the 'originals' dict may well get modified elsewhere, and
178+ # that's going to make change detection impossible
150179 new_value = deepcopy (new_value )
151180
152181 changed_fields [field .name ] = (old_value , new_value )
182+ # now update, for next time
153183 originals [field .name ] = new_value
154184 return changed_fields
155185
@@ -187,5 +217,9 @@ def connect_source_signals(self, sender: type) -> None:
187217 )
188218
189219
220+ ### API:
221+
190222pre_save_changed = PreSaveChangedSignal ()
191223post_save_changed = PostSaveChangedSignal ()
224+
225+ # TODO other signals?
0 commit comments