diff --git a/examples/django/article/tests.py b/examples/django/article/tests.py index 8f7febb..fdd1612 100644 --- a/examples/django/article/tests.py +++ b/examples/django/article/tests.py @@ -48,3 +48,38 @@ def test_sub_factory(self) -> None: articles = ArticleFactory.create_batch(5) self.assertEqual(len(articles), 5) self.assertIsInstance(articles[0], Article) + + def test_sub_factory_nested(self) -> None: + article = ArticleFactory(author__username="admin") + + # Testing SubFactory + self.assertIsInstance(article.author, User) + self.assertIsInstance(article.author.id, int) + self.assertIsInstance(article.author.is_staff, bool) + self.assertIsInstance(article.author.date_joined, datetime) + self.assertEqual(article.author.username, "admin") + + # Testing Factory + self.assertIsInstance(article.id, int) + self.assertIsInstance(article.slug, str) + + # Testing hooks + self.assertTrue( + hasattr(article, "pre_save_called") and article.pre_save_called + ) + self.assertTrue( + hasattr(article, "post_save_called") and article.post_save_called + ) + self.assertTrue( + hasattr(article.author, "pre_save_called") + and article.author.pre_save_called + ) + self.assertTrue( + hasattr(article.author, "post_save_called") + and article.author.post_save_called + ) + + # Testing batch creation + articles = ArticleFactory.create_batch(5) + self.assertEqual(len(articles), 5) + self.assertIsInstance(articles[0], Article) diff --git a/examples/tortoise/article/tests.py b/examples/tortoise/article/tests.py index a239a4a..0ee9c00 100644 --- a/examples/tortoise/article/tests.py +++ b/examples/tortoise/article/tests.py @@ -25,35 +25,73 @@ def tearDown(self): FILE_REGISTRY.clean_up() def test_sub_factory(self) -> None: - article = ArticleFactory() - - # Testing SubFactory - self.assertIsInstance(article.author, User) - self.assertIsInstance(article.author.id, int) - self.assertIsInstance(article.author.is_staff, bool) - self.assertIsInstance(article.author.date_joined, datetime) - - # Testing Factory - self.assertIsInstance(article.id, int) - self.assertIsInstance(article.slug, str) - - # Testing hooks - self.assertTrue( - hasattr(article, "pre_save_called") and article.pre_save_called - ) - self.assertTrue( - hasattr(article, "post_save_called") and article.post_save_called - ) - self.assertTrue( - hasattr(article.author, "pre_save_called") - and article.author.pre_save_called - ) - self.assertTrue( - hasattr(article.author, "post_save_called") - and article.author.post_save_called - ) - - # Testing batch creation - articles = ArticleFactory.create_batch(5) - self.assertEqual(len(articles), 5) - self.assertIsInstance(articles[0], Article) + with self.subTest("SubFactory"): + article = ArticleFactory() + + # Testing SubFactory + self.assertIsInstance(article.author, User) + self.assertIsInstance(article.author.id, int) + self.assertIsInstance(article.author.is_staff, bool) + self.assertIsInstance(article.author.date_joined, datetime) + + # Testing Factory + self.assertIsInstance(article.id, int) + self.assertIsInstance(article.slug, str) + + # Testing hooks + self.assertTrue( + hasattr(article, "pre_save_called") and article.pre_save_called + ) + self.assertTrue( + hasattr(article, "post_save_called") + and article.post_save_called + ) + self.assertTrue( + hasattr(article.author, "pre_save_called") + and article.author.pre_save_called + ) + self.assertTrue( + hasattr(article.author, "post_save_called") + and article.author.post_save_called + ) + + # Testing batch creation + articles = ArticleFactory.create_batch(5) + self.assertEqual(len(articles), 5) + self.assertIsInstance(articles[0], Article) + + with self.subTest("SubFactory nested params"): + article = ArticleFactory(author__username="admin") + + # Testing SubFactory + self.assertIsInstance(article.author, User) + self.assertIsInstance(article.author.id, int) + self.assertIsInstance(article.author.is_staff, bool) + self.assertIsInstance(article.author.date_joined, datetime) + self.assertEqual(article.author.username, "admin") + + # Testing Factory + self.assertIsInstance(article.id, int) + self.assertIsInstance(article.slug, str) + + # Testing hooks + self.assertTrue( + hasattr(article, "pre_save_called") and article.pre_save_called + ) + self.assertTrue( + hasattr(article, "post_save_called") + and article.post_save_called + ) + self.assertTrue( + hasattr(article.author, "pre_save_called") + and article.author.pre_save_called + ) + self.assertTrue( + hasattr(article.author, "post_save_called") + and article.author.post_save_called + ) + + # Testing batch creation + articles = ArticleFactory.create_batch(5) + self.assertEqual(len(articles), 5) + self.assertIsInstance(articles[0], Article) diff --git a/fake.py b/fake.py index 37eb382..2563b89 100644 --- a/fake.py +++ b/fake.py @@ -1720,8 +1720,6 @@ def create(cls, **kwargs): if instance: return instance - # Create a new instance if none found - # return super().create(**kwargs) model_data = { field: value for field, value in cls.__dict__.items() @@ -1741,7 +1739,7 @@ def create(cls, **kwargs): else direct_attrs[field] ) - # Create instance + # Create a new instance if none found instance = cls.Meta.model(**model_data) # Handle nested attributes @@ -1806,8 +1804,61 @@ async def async_filter(): if instance: return instance + model_data = { + field: value + for field, value in cls.__dict__.items() + if not field.startswith("_") and not field == "Meta" + } + + # Separate nested attributes and direct attributes + nested_attrs = {k: v for k, v in kwargs.items() if "__" in k} + direct_attrs = {k: v for k, v in kwargs.items() if "__" not in k} + + # Update direct attributes with callable results + for field, value in model_data.items(): + if isinstance(value, (FactoryMethod, SubFactory)): + model_data[field] = ( + value() + if field not in direct_attrs + else direct_attrs[field] + ) + # Create a new instance if none found - return super().create(**kwargs) + instance = cls.Meta.model(**model_data) + + # Handle nested attributes + for attr, value in nested_attrs.items(): + field_name, nested_attr = attr.split("__", 1) + if isinstance(getattr(cls, field_name, None), SubFactory): + + async def async_related_instance(): + return await getattr(cls, field_name).factory_class.create( + **{nested_attr: value} + ) + + related_instance = asyncio.run(async_related_instance()) + setattr(instance, field_name, related_instance) + + # Run pre-save hooks + pre_save_hooks = [ + method + for method in dir(cls) + if getattr(getattr(cls, method), "is_pre_save", False) + ] + cls._run_hooks(pre_save_hooks, instance) + + # Save instance + cls.save(instance) + + # Run post-save hooks + post_save_hooks = [ + method + for method in dir(cls) + if getattr(getattr(cls, method), "is_post_save", False) + ] + cls._run_hooks(post_save_hooks, instance) + + return instance # TODO: Remove once Python 3.8 support is dropped @@ -2519,6 +2570,10 @@ def __post_save_method(instance): self.assertIsInstance(django_article.author.id, int) self.assertIsInstance(django_article.author.is_staff, bool) self.assertIsInstance(django_article.author.date_joined, datetime) + # Since we're mimicking Django's behaviour, the following line would + # fail on test, however would pass when testing against real Django + # model (as done in the examples). + # self.assertEqual(django_article.author.username, "admin") # Testing Factory self.assertIsInstance(django_article.id, int) @@ -2539,6 +2594,91 @@ def __post_save_method(instance): self.assertEqual(len(django_articles), 5) self.assertIsInstance(django_articles[0], Article) + # ********************************** + # ******* TortoiseModelFactory ******* + # ********************************** + + class TortoiseUserFactory(TortoiseModelFactory): + id = FACTORY.pyint() + username = FACTORY.username() + first_name = FACTORY.first_name() + last_name = FACTORY.last_name() + email = FACTORY.email() + last_login = FACTORY.date_time() + is_superuser = False + is_staff = False + is_active = FACTORY.pybool() + date_joined = FACTORY.date_time() + + class Meta: + model = User + get_or_create = ("username",) + + @staticmethod + @pre_save + def __pre_save_method(instance): + instance.pre_save_called = True + + @staticmethod + @post_save + def __post_save_method(instance): + instance.post_save_called = True + + class TortoiseArticleFactory(TortoiseModelFactory): + id = FACTORY.pyint() + title = FACTORY.sentence() + slug = FACTORY.slug() + content = FACTORY.text() + image = FACTORY.png_file(storage=STORAGE) + pub_date = FACTORY.date() + safe_for_work = FACTORY.pybool() + minutes_to_read = FACTORY.pyint(min_value=1, max_value=10) + author = SubFactory(TortoiseUserFactory) + + class Meta: + model = Article + + @staticmethod + @pre_save + def __pre_save_method(instance): + instance.pre_save_called = True + + @staticmethod + @post_save + def __post_save_method(instance): + instance.post_save_called = True + + tortoise_article = TortoiseArticleFactory(author__username="admin") + + # Testing SubFactory + self.assertIsInstance(tortoise_article.author, User) + self.assertIsInstance(tortoise_article.author.id, int) + self.assertIsInstance(tortoise_article.author.is_staff, bool) + self.assertIsInstance(tortoise_article.author.date_joined, datetime) + # Since we're mimicking Tortoise's behaviour, the following line would + # fail on test, however would pass when testing against real Tortoise + # model (as done in the examples). + # self.assertEqual(tortoise_article.author.username, "admin") + + # Testing Factory + self.assertIsInstance(tortoise_article.id, int) + self.assertIsInstance(tortoise_article.slug, str) + + # Testing hooks + self.assertTrue( + hasattr(tortoise_article, "pre_save_called") + and tortoise_article.pre_save_called + ) + self.assertTrue( + hasattr(tortoise_article, "post_save_called") + and tortoise_article.post_save_called + ) + + # Testing batch creation + tortoise_articles = TortoiseArticleFactory.create_batch(5) + self.assertEqual(len(tortoise_articles), 5) + self.assertIsInstance(tortoise_articles[0], Article) + def test_registry_integration(self) -> None: """Test `add`.""" # Create a TXT file.