Repository: FactoryBoy/factory_boy
Pull request #1004 (2023-03-06 14:26:59)

Fix mute signals for factories with post_generation hooks
In my factories `mute_signals` was not working.

I investigated the issue and found out that there's another `save` inside `_after_postgeneration` method.

This PR fixes issue.

francoisfreitag: Thanks for the PR! :tada: 
Could you include a test to reproduce the issue and prevent regressions?
last-partizan: Sure, turned out it was trickier than expected.

This bug is present only for sub-factories (#424), and now it's fixed.

Patch:
diff --git a/Makefile b/Makefile
index df330d0..ad35a6f 100644
--- a/Makefile
+++ b/Makefile
@@ -52,6 +52,7 @@ testall:
 	tox
 
 # DOC: Run tests for the currently installed version
+# Remove cgi warning when dropping support for Django<=4.1.
 test:
 	python \
 		-b \
@@ -62,7 +63,6 @@ test:
 		-Wdefault:"Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.9 it will stop working":DeprecationWarning:: \
 		-Wdefault:"set_output_charset() is deprecated":DeprecationWarning:: \
 		-Wdefault:"parameter codeset is deprecated":DeprecationWarning:: \
-		# Remove cgi warning when dropping support for Django<=4.1.
 		-Wdefault:"'cgi' is deprecated and slated for removal in Python 3.13":DeprecationWarning:: \
 		-m unittest
 
diff --git a/docs/orms.rst b/docs/orms.rst
index 3d255a3..3839707 100644
--- a/docs/orms.rst
+++ b/docs/orms.rst
@@ -463,8 +463,7 @@ A (very) simple example:
 .. code-block:: python
 
     from sqlalchemy import Column, Integer, Unicode, create_engine
-    from sqlalchemy.ext.declarative import declarative_base
-    from sqlalchemy.orm import scoped_session, sessionmaker
+    from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker
 
     engine = create_engine('sqlite://')
     session = scoped_session(sessionmaker(bind=engine))
diff --git a/factory/django.py b/factory/django.py
index db37d04..dee35d6 100644
--- a/factory/django.py
+++ b/factory/django.py
@@ -327,6 +327,9 @@ class mute_signals:
             # Retrieve __func__, the *actual* callable object.
             callable_obj._create = self.wrap_method(callable_obj._create.__func__)
             callable_obj._generate = self.wrap_method(callable_obj._generate.__func__)
+            callable_obj._after_postgeneration = self.wrap_method(
+                callable_obj._after_postgeneration.__func__
+            )
             return callable_obj
 
         else:


Test patch:
diff --git a/tests/alchemyapp/models.py b/tests/alchemyapp/models.py
index 2ed443d..20e60aa 100644
--- a/tests/alchemyapp/models.py
+++ b/tests/alchemyapp/models.py
@@ -5,8 +5,7 @@
 import os
 
 from sqlalchemy import Column, Integer, Unicode, create_engine
-from sqlalchemy.ext.declarative import declarative_base
-from sqlalchemy.orm import scoped_session, sessionmaker
+from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker
 
 try:
     import psycopg2  # noqa: F401
diff --git a/tests/test_django.py b/tests/test_django.py
index fd4fe3c..df92984 100644
--- a/tests/test_django.py
+++ b/tests/test_django.py
@@ -20,6 +20,7 @@ from django.db import IntegrityError, connections
 from django.db.models import signals
 from django.test import utils as django_test_utils
 
+import factory
 import factory.django
 
 from . import testdata
@@ -989,6 +990,40 @@ class PreventSignalsTestCase(django_test.TestCase):
 
         self.assertSignalsReactivated()
 
+    def test_class_decorator_related_model_with_post_hook(self):
+        """
+        Related factory with post_generation hook should not call disabled signals.
+
+        Refs https://github.com/FactoryBoy/factory_boy/issues/424
+        """
+
+        @factory.django.mute_signals(signals.post_save)
+        class PointedFactory(factory.django.DjangoModelFactory):
+            class Meta:
+                model = models.PointedModel
+
+            @factory.post_generation
+            def post_action(obj, create, extracted, **kwargs):
+                pass
+
+        class PointerFactory(factory.django.DjangoModelFactory):
+            pointed = factory.SubFactory(PointedFactory)
+
+            class Meta:
+                model = models.PointerModel
+
+        PointerFactory.create()
+
+        self.handlers.post_save.assert_called_once_with(
+            signal=mock.ANY,
+            sender=models.PointerModel,
+            instance=mock.ANY,
+            created=True,
+            update_fields=None,
+            raw=False,
+            using="default",
+        )
+
     def test_class_decorator_build(self):
         @factory.django.mute_signals(signals.pre_save, signals.post_save)
         class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory):