diff --git a/.secrets.baseline b/.secrets.baseline index 39a309e..f505083 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -118,16 +118,16 @@ "filename": "Makefile", "hashed_secret": "ee783f2421477b5483c23f47eca1f69a1f2bf4fb", "is_verified": true, - "line_number": 86 + "line_number": 92 }, { "type": "Secret Keyword", "filename": "Makefile", "hashed_secret": "1457a35245051927fac6fa556074300f4162ed66", "is_verified": true, - "line_number": 89 + "line_number": 95 } ] }, - "generated_at": "2023-12-10T22:39:54Z" + "generated_at": "2023-12-12T23:38:33Z" } diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8d39ad7..1ba3bde 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,14 @@ are used for versioning (schema follows below): 0.3.4 to 0.4). - All backwards incompatible changes are mentioned in this document. +0.6.3 +----- +2023-12-13 + +- Add ``LazyAttribute`` and ``LazyFunction``. +- Improve package portability (tests). +- Improve tests. + 0.6.2 ----- 2023-12-11 diff --git a/Makefile b/Makefile index 1abff74..80c0e18 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # Update version ONLY here -VERSION := 0.6.2 +VERSION := 0.6.3 SHELL := /bin/bash # Makefile for project VENV := ~/.virtualenvs/fake.py/bin/activate @@ -37,7 +37,7 @@ serve_docs: install: source $(VENV) && pip install -e .[all] -test: +test: clean source $(VENV) && pytest -vrx -s customization-test: @@ -52,6 +52,9 @@ django-test: hypothesis-test: source $(VENV) && cd examples/hypothesis/ && ./manage.py test +lazyfuzzy-test: + source $(VENV) && cd examples/lazyfuzzy/ && ./manage.py test + pydantic-test: source $(VENV) && cd examples/pydantic/ && python manage.py test @@ -73,6 +76,9 @@ dataclasses-shell: django-shell: source $(VENV) && python examples/django/manage.py shell +lazyfuzzy-shell: + source $(VENV) && cd examples/lazyfuzzy/ && python manage.py shell + pydantic-shell: source $(VENV) && cd examples/pydantic/ && python manage.py shell diff --git a/__copy_fake.py b/__copy_fake.py new file mode 120000 index 0000000..56494d4 --- /dev/null +++ b/__copy_fake.py @@ -0,0 +1 @@ +fake.py \ No newline at end of file diff --git a/docs/factories.rst b/docs/factories.rst index 64f8a3f..3cdfef2 100644 --- a/docs/factories.rst +++ b/docs/factories.rst @@ -15,6 +15,8 @@ Django example title = models.CharField(max_length=255) slug = models.SlugField(unique=True) content = models.TextField() + headline = models.TextField() + category = models.CharField() image = models.ImageField(null=True, blank=True) pub_date = models.DateTimeField(default=timezone.now) safe_for_work = models.BooleanField(default=False) @@ -30,12 +32,17 @@ Django example .. code-block:: python + import random + from functools import partial + from django.conf import settings from django.contrib.auth.models import User from fake import ( FACTORY, DjangoModelFactory, FileSystemStorage, + LazyAttribute, + LazyFunction, SubFactory, pre_save, trait, @@ -48,6 +55,11 @@ Django example # custom `FileSystemStorage` class and pass it to the file factory as # `storage` argument. STORAGE = FileSystemStorage(root_path=settings.MEDIA_ROOT, rel_path="tmp") + CATEGORIES = ( + "art", + "technology", + "literature", + ) class UserFactory(DjangoModelFactory): @@ -80,6 +92,8 @@ Django example title = FACTORY.sentence() slug = FACTORY.slug() content = FACTORY.text() + headline = LazyAttribute(lambda o: o.content[:25]) + category = LazyFunction(partial(random.choice, CATEGORIES)) image = FACTORY.png_file(storage=STORAGE) pub_date = FACTORY.date() safe_for_work = FACTORY.pybool() @@ -140,6 +154,8 @@ Pydantic example title: str = Field(..., max_length=255) slug: str = Field(..., max_length=255, unique=True) content: str + headline: str + category: str image: Optional[str] = None # Use str to represent the image path or URL pub_date: datetime = Field(default_factory=datetime.now) safe_for_work: bool = False @@ -153,6 +169,8 @@ Pydantic example .. code-block:: python + import random + from functools import partial from pathlib import Path from fake import FACTORY, FileSystemStorage, ModelFactory, SubFactory @@ -161,8 +179,12 @@ Pydantic example BASE_DIR = Path(__file__).resolve().parent.parent MEDIA_ROOT = BASE_DIR / "media" - STORAGE = FileSystemStorage(root_path=MEDIA_ROOT, rel_path="tmp") + CATEGORIES = ( + "art", + "technology", + "literature", + ) class UserFactory(ModelFactory): id = FACTORY.pyint() @@ -194,6 +216,8 @@ Pydantic example title = FACTORY.sentence() slug = FACTORY.slug() content = FACTORY.text() + headline = LazyAttribute(lambda o: o.content[:25]) + category = LazyFunction(partial(random.choice, CATEGORIES)) image = FACTORY.png_file(storage=STORAGE) pub_date = FACTORY.date() safe_for_work = FACTORY.pybool() @@ -240,6 +264,8 @@ TortoiseORM example title = fields.CharField(max_length=255) slug = fields.CharField(max_length=255, unique=True) content = fields.TextField() + headline = fields.TextField() + category = fields.CharField(max_length=255) image = fields.TextField(null=True, blank=True) pub_date = fields.DatetimeField(default=datetime.now) safe_for_work = fields.BooleanField(default=False) @@ -253,6 +279,8 @@ TortoiseORM example .. code-block:: python + import random + from functools import partial from pathlib import Path from fake import FACTORY, FileSystemStorage, SubFactory, TortoiseModelFactory @@ -261,8 +289,12 @@ TortoiseORM example BASE_DIR = Path(__file__).resolve().parent.parent MEDIA_ROOT = BASE_DIR / "media" - STORAGE = FileSystemStorage(root_path=MEDIA_ROOT, rel_path="tmp") + CATEGORIES = ( + "art", + "technology", + "literature", + ) class UserFactory(TortoiseModelFactory): """User factory.""" @@ -297,6 +329,8 @@ TortoiseORM example title = FACTORY.sentence() slug = FACTORY.slug() content = FACTORY.text() + headline = LazyAttribute(lambda o: o.content[:25]) + category = LazyFunction(partial(random.choice, CATEGORIES)) image = FACTORY.png_file(storage=STORAGE) pub_date = FACTORY.date_time() safe_for_work = FACTORY.pybool() @@ -315,8 +349,10 @@ Dataclasses example .. code-block:: python + import random from dataclasses import dataclass from datetime import datetime + from functools import partial from typing import Optional @dataclass @@ -342,6 +378,8 @@ Dataclasses example title: str slug: str content: str + headline: str + category: str author: User image: Optional[str] = None # Use str to represent the image path or URL pub_date: datetime = datetime.now() @@ -355,6 +393,8 @@ Dataclasses example .. code-block:: python + import random + from functools import partial from pathlib import Path from fake import FACTORY, FileSystemStorage, ModelFactory, SubFactory @@ -363,8 +403,12 @@ Dataclasses example BASE_DIR = Path(__file__).resolve().parent.parent MEDIA_ROOT = BASE_DIR / "media" - STORAGE = FileSystemStorage(root_path=MEDIA_ROOT, rel_path="tmp") + CATEGORIES = ( + "art", + "technology", + "literature", + ) class UserFactory(ModelFactory): id = FACTORY.pyint() @@ -396,6 +440,8 @@ Dataclasses example title = FACTORY.sentence() slug = FACTORY.slug() content = FACTORY.text() + headline = LazyAttribute(lambda o: o.content[:25]) + category = LazyFunction(partial(random.choice, CATEGORIES)) image = FACTORY.png_file(storage=STORAGE) pub_date = FACTORY.date() safe_for_work = FACTORY.pybool() @@ -469,6 +515,8 @@ SQLAlchemy example title = Column(String(255)) slug = Column(String(255), unique=True) content = Column(Text) + headline = Column(Text) + category = Column(String(255)) image = Column(Text, nullable=True) pub_date = Column(DateTime, default=datetime.utcnow) safe_for_work = Column(Boolean, default=False) @@ -484,6 +532,8 @@ method. .. code-block:: python + import random + from functools import partial from pathlib import Path from fake import ( @@ -502,6 +552,11 @@ method. BASE_DIR = Path(__file__).resolve().parent.parent MEDIA_ROOT = BASE_DIR / "media" STORAGE = FileSystemStorage(root_path=MEDIA_ROOT, rel_path="tmp") + CATEGORIES = ( + "art", + "technology", + "literature", + ) def get_session(): return SESSION() @@ -542,6 +597,8 @@ method. title = FACTORY.sentence() slug = FACTORY.slug() content = FACTORY.text() + headline = LazyAttribute(lambda o: o.content[:25]) + category = LazyFunction(partial(random.choice, CATEGORIES)) image = FACTORY.png_file(storage=STORAGE) pub_date = FACTORY.date() safe_for_work = FACTORY.pybool() diff --git a/examples/lazyfuzzy/article/__init__.py b/examples/lazyfuzzy/article/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/lazyfuzzy/article/factories.py b/examples/lazyfuzzy/article/factories.py new file mode 100644 index 0000000..db07649 --- /dev/null +++ b/examples/lazyfuzzy/article/factories.py @@ -0,0 +1,92 @@ +import random +from datetime import datetime +from functools import partial +from pathlib import Path + +from fake import ( + FACTORY, + FileSystemStorage, + LazyAttribute, + LazyFunction, + ModelFactory, + SubFactory, + post_save, + pre_save, + trait, +) + +from article.models import Article, User + +__author__ = "Artur Barseghyan " +__copyright__ = "2023 Artur Barseghyan" +__license__ = "MIT" +__all__ = ( + "ArticleFactory", + "UserFactory", +) + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent +MEDIA_ROOT = BASE_DIR / "media" + +STORAGE = FileSystemStorage(root_path=MEDIA_ROOT, rel_path="tmp") +CATEGORIES = ( + "art", + "technology", + "literature", +) + + +class UserFactory(ModelFactory): + id = FACTORY.pyint() + username = FACTORY.username() + first_name = FACTORY.first_name() + last_name = FACTORY.last_name() + email = LazyAttribute(lambda o: f"{o.username}@example.com") + last_login = FACTORY.date_time() + is_superuser = False + is_staff = False + is_active = FACTORY.pybool() + date_joined = LazyFunction(datetime.now) + + class Meta: + model = User + + @trait + def is_admin_user(self, instance: User) -> None: + instance.is_superuser = True + instance.is_staff = True + instance.is_active = True + + @pre_save + def _pre_save_method(self, instance): + instance.pre_save_called = True + + @post_save + def _post_save_method(self, instance): + instance.post_save_called = True + + +class ArticleFactory(ModelFactory): + id = FACTORY.pyint() + title = FACTORY.sentence() + slug = FACTORY.slug() + content = FACTORY.text() + headline = LazyAttribute(lambda o: o.content[:25]) + category = LazyFunction(partial(random.choice, CATEGORIES)) + 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(UserFactory) + + class Meta: + model = Article + + @pre_save + def _pre_save_method(self, instance): + instance.pre_save_called = True + + @post_save + def _post_save_method(self, instance): + instance.post_save_called = True diff --git a/examples/lazyfuzzy/article/models.py b/examples/lazyfuzzy/article/models.py new file mode 100644 index 0000000..d4c8864 --- /dev/null +++ b/examples/lazyfuzzy/article/models.py @@ -0,0 +1,47 @@ +from dataclasses import dataclass +from datetime import date, datetime +from typing import Optional + +__author__ = "Artur Barseghyan " +__copyright__ = "2023 Artur Barseghyan" +__license__ = "MIT" +__all__ = ( + "Article", + "User", +) + + +@dataclass +class User: + id: int + username: str + first_name: str + last_name: str + email: str + last_login: Optional[datetime] + date_joined: Optional[datetime] + password: Optional[str] = None + is_superuser: bool = False + is_staff: bool = False + is_active: bool = True + + def __str__(self): + return self.username + + +@dataclass +class Article: + id: int + title: str + slug: str + content: str + headline: str + category: str + author: User + image: Optional[str] = None # Use str to represent the image path or URL + pub_date: datetime = date.today() + safe_for_work: bool = False + minutes_to_read: int = 5 + + def __str__(self): + return self.title diff --git a/examples/lazyfuzzy/article/tests.py b/examples/lazyfuzzy/article/tests.py new file mode 100644 index 0000000..3994cc5 --- /dev/null +++ b/examples/lazyfuzzy/article/tests.py @@ -0,0 +1,51 @@ +import unittest +from datetime import datetime + +from fake import FILE_REGISTRY + +from article.factories import ArticleFactory +from article.models import Article, User + +__author__ = "Artur Barseghyan " +__copyright__ = "2023 Artur Barseghyan" +__license__ = "MIT" +__all__ = ("FactoriesTestCase",) + + +class FactoriesTestCase(unittest.TestCase): + 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) diff --git a/examples/lazyfuzzy/manage.py b/examples/lazyfuzzy/manage.py new file mode 100755 index 0000000..e71fa1d --- /dev/null +++ b/examples/lazyfuzzy/manage.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +import argparse +import os +import sys +import unittest + +import IPython + + +def run_tests(): + """Function to run tests in the article directory.""" + loader = unittest.TestLoader() + suite = loader.discover(start_dir="./article", pattern="tests.py") + runner = unittest.TextTestRunner() + runner.run(suite) + + +def main(): + """Run administrative tasks based on command line arguments.""" + sys.path.insert(0, os.path.abspath(os.path.join("..", ".."))) + sys.path.insert(0, os.path.abspath(".")) + parser = argparse.ArgumentParser( + description="Management script for the project." + ) + parser.add_argument("command", help="The command to run (test or shell)") + + args = parser.parse_args() + + if args.command == "test": + run_tests() + elif args.command == "shell": + IPython.embed() + else: + print("Unknown command. Use 'test' or 'shell'.") + + +if __name__ == "__main__": + main() diff --git a/fake.py b/fake.py index 4f17bf5..1a21743 100644 --- a/fake.py +++ b/fake.py @@ -20,6 +20,7 @@ from dataclasses import dataclass from datetime import date, datetime, timedelta, timezone from decimal import Decimal +from functools import partial from pathlib import Path from tempfile import NamedTemporaryFile, gettempdir from threading import Lock @@ -37,7 +38,7 @@ ) __title__ = "fake.py" -__version__ = "0.6.2" +__version__ = "0.6.3" __author__ = "Artur Barseghyan " __copyright__ = "2023 Artur Barseghyan" __license__ = "MIT" @@ -795,7 +796,7 @@ def create( if texts: nb_pages = len(texts) else: - texts = self.faker.sentences(nb=nb_pages) # type: ignore + texts = self.faker.sentences(nb=nb_pages) if metadata: metadata.add_content(texts) # type: ignore @@ -1738,7 +1739,10 @@ def faker(self, value): def _add_provider_methods(self, faker_instance): for class_name, methods in PROVIDER_REGISTRY.items(): - if class_name == "fake.Faker" or class_name == self.faker.uid: + if ( + class_name == f"{__name__}.{Faker.__name__}" + or class_name == self.faker.uid + ): for method_name in methods: if hasattr(faker_instance, method_name): bound_method = create_factory_method(method_name) @@ -1763,6 +1767,31 @@ def trait(func): return func +class LazyAttribute: + def __init__(self, func): + self.func = func + + def __get__(self, obj, objtype=None): + if obj is None: + return self + value = self.func(obj) + setattr(obj, self.func.__name__, value) + return value + + +class LazyFunction: + def __init__(self, func): + self.func = func + + def __call__(self): + return self.func() + + def __get__(self, obj, objtype=None): + if obj is None: + return self + return self.func() + + class ModelFactory: """ModelFactory.""" @@ -1774,8 +1803,8 @@ def __init_subclass__(cls, **kwargs): cls.__bases__[0], "_meta", { - attr: getattr(cls.__bases__[0].Meta, attr) - for attr in dir(cls.__bases__[0].Meta) + attr: getattr(cls.__bases__[0].Meta, attr) # type: ignore + for attr in dir(cls.__bases__[0].Meta) # type: ignore if not attr.startswith("_") }, ) @@ -1785,7 +1814,7 @@ def __init_subclass__(cls, **kwargs): if not attr.startswith("_") } - cls._meta = {**base_meta, **cls_meta} + cls._meta = {**base_meta, **cls_meta} # type: ignore @classmethod def _run_hooks(cls, hooks, instance): @@ -1798,8 +1827,16 @@ def _apply_traits(cls, instance, **kwargs) -> None: if getattr(method, "is_trait", False) and kwargs.get(name, False): method(cls, instance) + @classmethod + def _apply_lazy_attributes(cls, instance, model_data): + for field, value in model_data.items(): + if isinstance(value, LazyAttribute): + # Trigger computation and setting of the attribute + setattr(instance, field, value.__get__(instance, cls)) + @classmethod def create(cls, **kwargs): + model = cls.Meta.model # type: ignore trait_keys = { name for name, method in cls.__dict__.items() @@ -1809,7 +1846,7 @@ def create(cls, **kwargs): model_data = { field: ( value() - if isinstance(value, (FactoryMethod, SubFactory)) + if isinstance(value, (FactoryMethod, SubFactory, LazyFunction)) else value ) for field, value in cls.__dict__.items() @@ -1826,9 +1863,15 @@ def create(cls, **kwargs): {k: v for k, v in kwargs.items() if k not in trait_keys} ) - instance = cls.Meta.model(**model_data) + # Create a new instance + instance = model(**model_data) + + # Apply traits cls._apply_traits(instance, **kwargs) + # Apply LazyAttribute values + cls._apply_lazy_attributes(instance, model_data) + pre_save_hooks = [ method for method in dir(cls) @@ -1868,8 +1911,8 @@ def save(cls, instance): @classmethod def create(cls, **kwargs): - model = cls.Meta.model - unique_fields = cls._meta.get("get_or_create", ["id"]) + model = cls.Meta.model # type: ignore + unique_fields = cls._meta.get("get_or_create", ["id"]) # type: ignore # Construct a query for unique fields query = { @@ -1908,9 +1951,14 @@ def create(cls, **kwargs): ) # Create a new instance if none found - instance = cls.Meta.model(**model_data) + instance = model(**model_data) + + # Apply traits cls._apply_traits(instance, **kwargs) + # Apply LazyAttribute values + cls._apply_lazy_attributes(instance, model_data) + # Handle nested attributes for attr, value in nested_attrs.items(): field_name, nested_attr = attr.split("__", 1) @@ -1971,8 +2019,8 @@ async def async_save(): @classmethod def create(cls, **kwargs): - model = cls.Meta.model - unique_fields = cls._meta.get("get_or_create", ["id"]) + model = cls.Meta.model # type: ignore + unique_fields = cls._meta.get("get_or_create", ["id"]) # type: ignore # Construct a query for unique fields query = { @@ -2016,9 +2064,14 @@ async def async_filter(): ) # Create a new instance if none found - instance = cls.Meta.model(**model_data) + instance = model(**model_data) + + # Apply traits cls._apply_traits(instance, **kwargs) + # Apply LazyAttribute values + cls._apply_lazy_attributes(instance, model_data) + # Handle nested attributes for attr, value in nested_attrs.items(): field_name, nested_attr = attr.split("__", 1) @@ -2059,16 +2112,16 @@ class SQLAlchemyModelFactory(ModelFactory): @classmethod def save(cls, instance): - session = cls.MetaSQLAlchemy.get_session() + session = cls.MetaSQLAlchemy.get_session() # type: ignore session.add(instance) session.commit() @classmethod def create(cls, **kwargs): - session = cls.MetaSQLAlchemy.get_session() + session = cls.MetaSQLAlchemy.get_session() # type: ignore - model = cls.Meta.model - unique_fields = cls._meta.get("get_or_create", ["id"]) + model = cls.Meta.model # type: ignore + unique_fields = cls._meta.get("get_or_create", ["id"]) # type: ignore # Check for existing instance if unique_fields: @@ -2105,8 +2158,13 @@ def create(cls, **kwargs): # Create a new instance instance = model(**model_data) + + # Apply traits cls._apply_traits(instance, **kwargs) + # Apply LazyAttribute values + cls._apply_lazy_attributes(instance, model_data) + # Handle nested attributes for attr, value in nested_attrs.items(): field_name, nested_attr = attr.split("__", 1) @@ -2144,7 +2202,7 @@ class ClassProperty(property): def __get__(self, cls, owner): """Get.""" - return classmethod(self.fget).__get__(None, owner)() + return classmethod(self.fget).__get__(None, owner)() # type: ignore classproperty = ClassProperty @@ -2246,7 +2304,7 @@ def test_email(self) -> None: for domain, expected_domain in domains: with self.subTest(domain=domain, expected_domain=expected_domain): kwargs = {"domain": domain} - email: str = self.faker.email(**kwargs) # type: ignore + email: str = self.faker.email(**kwargs) self.assertIsInstance(email, str) self.assertTrue(email.endswith(f"@{expected_domain}")) @@ -2506,7 +2564,7 @@ def test_text_pdf(self) -> None: with self.subTest("All params None, should fail"): with self.assertRaises(ValueError): self.faker.pdf( - nb_pages=None, # type: ignore + nb_pages=None, texts=None, generator=TextPdfGenerator, ) @@ -2560,14 +2618,14 @@ def test_image(self): for image_format in {"png", "svg", "bmp", "gif"}: with self.subTest(image_format=image_format): image = self.faker.image( - image_format=image_format, # type: ignore + image_format=image_format, ) self.assertTrue(image) self.assertIsInstance(image, bytes) for image_format in {"bin"}: with self.subTest(image_format=image_format): with self.assertRaises(ValueError): - self.faker.image(image_format=image_format) # type: ignore + self.faker.image(image_format=image_format) def test_docx(self) -> None: with self.subTest("All params None, should fail"): @@ -2708,7 +2766,7 @@ def test_faker_init(self) -> None: self.assertNotEqual(faker.alias, "default") def test_get_by_uid(self) -> None: - faker = Faker.get_by_uid("fake.Faker") + faker = Faker.get_by_uid(f"{__name__}.{Faker.__name__}") self.assertIs(faker, self.faker) def test_get_by_alias(self) -> None: @@ -2792,7 +2850,7 @@ def save(self, *args, **kwargs): def objects(cls): """Mimicking Django's Manager behaviour.""" return DjangoManager( - instance=cls( # noqa + instance=cls( # type: ignore id=FAKER.pyint(), username=FAKER.username(), first_name=FAKER.first_name(), @@ -2809,6 +2867,8 @@ class Article: title: str slug: str content: str + headline: str + category: str author: User image: Optional[ str @@ -2827,11 +2887,13 @@ def save(self, *args, **kwargs): def objects(cls): """Mimicking Django's Manager behaviour.""" return DjangoManager( - instance=cls( # noqa + instance=cls( # type: ignore id=FAKER.pyint(), title=FAKER.word(), slug=FAKER.slug(), content=FAKER.text(), + headline=FAKER.sentence(), + category=random.choice(categories), author=User( id=FAKER.pyint(), username=FAKER.username(), @@ -2856,6 +2918,11 @@ def objects(cls): # **************************** # ******* ModelFactory ******* # **************************** + categories = ( + "art", + "technology", + "literature", + ) class UserFactory(ModelFactory): id = FACTORY.pyint() # type: ignore @@ -2891,48 +2958,60 @@ class ArticleFactory(ModelFactory): title = FACTORY.sentence() # type: ignore slug = FACTORY.slug() # type: ignore content = FACTORY.text() # type: ignore + headline = LazyAttribute(lambda o: o.content[:25]) + category = LazyFunction(partial(random.choice, categories)) image = FACTORY.png_file(storage=storage) # type: ignore pub_date = FACTORY.date() # type: ignore safe_for_work = FACTORY.pybool() # type: ignore minutes_to_read = FACTORY.pyint( # type: ignore min_value=1, max_value=10 ) - author = SubFactory(UserFactory) # type: ignore + author = SubFactory(UserFactory) class Meta: model = Article - article = ArticleFactory() + with self.subTest("ModelFactory"): + article = ArticleFactory() - # Testing SubFactory - self.assertIsInstance(article.author, User) - self.assertIsInstance(article.author.id, int) # type: ignore - self.assertIsInstance(article.author.is_staff, bool) # type: ignore - self.assertIsInstance( - article.author.date_joined, # type: ignore - datetime, - ) + # Testing SubFactory + self.assertIsInstance(article.author, User) + self.assertIsInstance(article.author.id, int) # type: ignore + self.assertIsInstance( + article.author.is_staff, # type: ignore + bool, + ) + self.assertIsInstance( + article.author.date_joined, # type: ignore + datetime, + ) - # Testing Factory - self.assertIsInstance(article.id, int) - self.assertIsInstance(article.slug, str) + # Testing LazyFunction + self.assertIn(article.category, categories) - # Testing hooks - user = article.author - self.assertTrue( - hasattr(user, "pre_save_called") and user.pre_save_called - ) - self.assertTrue( - hasattr(user, "post_save_called") and user.post_save_called - ) + # Testing LazyAttribute + self.assertIn(article.headline, article.content) - # Testing traits - admin_user = UserFactory(is_admin_user=True) - self.assertTrue( - admin_user.is_staff - and admin_user.is_superuser - and admin_user.is_active - ) + # Testing Factory + self.assertIsInstance(article.id, int) + self.assertIsInstance(article.slug, str) + + # Testing hooks + user = article.author + self.assertTrue( + hasattr(user, "pre_save_called") and user.pre_save_called + ) + self.assertTrue( + hasattr(user, "post_save_called") and user.post_save_called + ) + + # Testing traits + admin_user = UserFactory(is_admin_user=True) + self.assertTrue( + admin_user.is_staff + and admin_user.is_superuser + and admin_user.is_active + ) # ********************************** # ******* DjangoModelFactory ******* @@ -2973,6 +3052,8 @@ class DjangoArticleFactory(DjangoModelFactory): title = FACTORY.sentence() # type: ignore slug = FACTORY.slug() # type: ignore content = FACTORY.text() # type: ignore + headline = LazyAttribute(lambda o: o.content[:25]) + category = LazyFunction(partial(random.choice, categories)) image = FACTORY.png_file(storage=storage) # type: ignore pub_date = FACTORY.date() # type: ignore safe_for_work = FACTORY.pybool() # type: ignore @@ -2993,50 +3074,51 @@ def _pre_save_method(self, instance): def _post_save_method(self, instance): instance.post_save_called = True - django_article = DjangoArticleFactory(author__username="admin") - - # Testing SubFactory - self.assertIsInstance(django_article.author, User) - self.assertIsInstance(django_article.author.id, int) # type: ignore - self.assertIsInstance( - django_article.author.is_staff, # type: ignore - bool, - ) - self.assertIsInstance( - django_article.author.date_joined, # type: ignore - 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) - self.assertIsInstance(django_article.slug, str) + with self.subTest("DjangoModelFactory"): + django_article = DjangoArticleFactory(author__username="admin") - # Testing hooks - self.assertTrue( - hasattr(django_article, "pre_save_called") - and django_article.pre_save_called - ) - self.assertTrue( - hasattr(django_article, "post_save_called") - and django_article.post_save_called - ) - - # Testing batch creation - django_articles = DjangoArticleFactory.create_batch(5) - self.assertEqual(len(django_articles), 5) - self.assertIsInstance(django_articles[0], Article) + # Testing SubFactory + self.assertIsInstance(django_article.author, User) + self.assertIsInstance(django_article.author.id, int) # type: ignore + self.assertIsInstance( + django_article.author.is_staff, # type: ignore + bool, + ) + self.assertIsInstance( + django_article.author.date_joined, # type: ignore + 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) + self.assertIsInstance(django_article.slug, str) + + # Testing hooks + self.assertTrue( + hasattr(django_article, "pre_save_called") + and django_article.pre_save_called + ) + self.assertTrue( + hasattr(django_article, "post_save_called") + and django_article.post_save_called + ) - # Testing traits - django_admin_user = DjangoUserFactory(is_admin_user=True) - self.assertTrue( - django_admin_user.is_staff - and django_admin_user.is_superuser - and django_admin_user.is_active - ) + # Testing batch creation + django_articles = DjangoArticleFactory.create_batch(5) + self.assertEqual(len(django_articles), 5) + self.assertIsInstance(django_articles[0], Article) + + # Testing traits + django_admin_user = DjangoUserFactory(is_admin_user=True) + self.assertTrue( + django_admin_user.is_staff + and django_admin_user.is_superuser + and django_admin_user.is_active + ) # ********************************** # ****** TortoiseModelFactory ****** @@ -3101,6 +3183,8 @@ class TortoiseArticle: title: str slug: str content: str + headline: str + category: str author: TortoiseUser image: Optional[ str @@ -3117,6 +3201,8 @@ def filter(cls, *args, **kwargs) -> "TortoiseQuerySet": title=FAKER.word(), slug=FAKER.slug(), content=FAKER.text(), + headline=FAKER.sentence(), + category=random.choice(categories), author=TortoiseUser( id=FAKER.pyint(), username=FAKER.username(), @@ -3168,6 +3254,8 @@ class TortoiseArticleFactory(TortoiseModelFactory): title = FACTORY.sentence() # type: ignore slug = FACTORY.slug() # type: ignore content = FACTORY.text() # type: ignore + headline = LazyAttribute(lambda o: o.content[:25]) + category = LazyFunction(partial(random.choice, categories)) image = FACTORY.png_file(storage=storage) # type: ignore pub_date = FACTORY.date() # type: ignore safe_for_work = FACTORY.pybool() # type: ignore @@ -3188,102 +3276,109 @@ def _pre_save_method(self, instance): def _post_save_method(self, instance): instance.post_save_called = True - tortoise_article = TortoiseArticleFactory(author__username="admin") - - # Testing SubFactory - self.assertIsInstance(tortoise_article.author, TortoiseUser) - self.assertIsInstance(tortoise_article.author.id, int) # type: ignore - self.assertIsInstance( - tortoise_article.author.is_staff, # type: ignore - bool, - ) - self.assertIsInstance( - tortoise_article.author.date_joined, # type: ignore - 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") + with self.subTest("TortoiseModelFactory"): + tortoise_article = TortoiseArticleFactory(author__username="admin") - # Testing Factory - self.assertIsInstance(tortoise_article.id, int) - self.assertIsInstance(tortoise_article.slug, str) + # Testing SubFactory + self.assertIsInstance(tortoise_article.author, TortoiseUser) + self.assertIsInstance( + tortoise_article.author.id, # type: ignore + int, + ) + self.assertIsInstance( + tortoise_article.author.is_staff, # type: ignore + bool, + ) + self.assertIsInstance( + tortoise_article.author.date_joined, # type: ignore + 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 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], TortoiseArticle) + + # Testing traits + tortoise_admin_user = TortoiseUserFactory(is_admin_user=True) + self.assertTrue( + tortoise_admin_user.is_staff + and tortoise_admin_user.is_superuser + and tortoise_admin_user.is_active + ) - # Testing batch creation - tortoise_articles = TortoiseArticleFactory.create_batch(5) - self.assertEqual(len(tortoise_articles), 5) - self.assertIsInstance(tortoise_articles[0], TortoiseArticle) + # ********************************** + # ** Repeat for another condition ** + TortoiseQuerySet.return_instance_on_query_first = True - # Testing traits - tortoise_admin_user = TortoiseUserFactory(is_admin_user=True) - self.assertTrue( - tortoise_admin_user.is_staff - and tortoise_admin_user.is_superuser - and tortoise_admin_user.is_active - ) + tortoise_article = TortoiseArticleFactory(author__username="admin") + tortoise_user = TortoiseUserFactory(username="admin") - # ********************************** - # ** Repeat for another condition ** - TortoiseQuerySet.return_instance_on_query_first = True - - tortoise_article = TortoiseArticleFactory(author__username="admin") - tortoise_user = TortoiseUserFactory(username="admin") - - # Testing SubFactory - self.assertIsInstance(tortoise_article.author, TortoiseUser) - self.assertIsInstance(tortoise_article, TortoiseArticle) - self.assertIsInstance(tortoise_user, TortoiseUser) - self.assertIsInstance(tortoise_article.author.id, int) # type: ignore - self.assertIsInstance( - tortoise_article.author.is_staff, # type: ignore - bool, - ) - self.assertIsInstance( - tortoise_article.author.date_joined, # type: ignore - 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) - self.assertIsInstance(tortoise_user.id, int) - self.assertIsInstance(tortoise_user.username, str) - - # Testing hooks - # self.assertFalse( - # hasattr(tortoise_article, "pre_save_called") - # ) - # self.assertFalse( - # hasattr(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], TortoiseArticle) - - # Testing traits - tortoise_admin_user = TortoiseUserFactory(is_admin_user=True) - self.assertTrue( - tortoise_admin_user.is_staff - and tortoise_admin_user.is_superuser - and tortoise_admin_user.is_active - ) + # Testing SubFactory + self.assertIsInstance(tortoise_article.author, TortoiseUser) + self.assertIsInstance(tortoise_article, TortoiseArticle) + self.assertIsInstance(tortoise_user, TortoiseUser) + self.assertIsInstance( + tortoise_article.author.id, # type: ignore + int, + ) + self.assertIsInstance( + tortoise_article.author.is_staff, # type: ignore + bool, + ) + self.assertIsInstance( + tortoise_article.author.date_joined, # type: ignore + 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) + self.assertIsInstance(tortoise_user.id, int) + self.assertIsInstance(tortoise_user.username, str) + + # Testing hooks + # self.assertFalse( + # hasattr(tortoise_article, "pre_save_called") + # ) + # self.assertFalse( + # hasattr(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], TortoiseArticle) + + # Testing traits + tortoise_admin_user = TortoiseUserFactory(is_admin_user=True) + self.assertTrue( + tortoise_admin_user.is_staff + and tortoise_admin_user.is_superuser + and tortoise_admin_user.is_active + ) # ********************************** # ***** SQLAlchemyModelFactory ***** @@ -3314,7 +3409,7 @@ def first(self): return None if self.model == SQLAlchemyUser: - return self.model( # noqa + return self.model( # type: ignore id=FAKER.pyint(), username=FAKER.username(), first_name=FAKER.first_name(), @@ -3324,11 +3419,13 @@ def first(self): date_joined=FAKER.date_time(), ) elif self.model == SQLAlchemyArticle: - return self.model( # noqa + return self.model( # type: ignore id=FAKER.pyint(), title=FAKER.word(), slug=FAKER.slug(), content=FAKER.text(), + headline=FAKER.sentence(), + category=random.choice(categories), author=SQLAlchemyUser( id=FAKER.pyint(), username=FAKER.username(), @@ -3365,6 +3462,8 @@ class SQLAlchemyArticle: title: str slug: str content: str + headline: str + category: str author: SQLAlchemyUser image: Optional[ str @@ -3411,6 +3510,8 @@ class SQLAlchemyArticleFactory(SQLAlchemyModelFactory): title = FACTORY.sentence() # type: ignore slug = FACTORY.slug() # type: ignore content = FACTORY.text() # type: ignore + headline = LazyAttribute(lambda o: o.content[:25]) + category = LazyFunction(partial(random.choice, categories)) image = FACTORY.png_file(storage=storage) # type: ignore pub_date = FACTORY.date() # type: ignore safe_for_work = FACTORY.pybool() # type: ignore @@ -3434,95 +3535,100 @@ def _pre_save_method(self, instance): def _post_save_method(self, instance): instance.post_save_called = True - sqlalchemy_article = SQLAlchemyArticleFactory(author__username="admin") - - # Testing SubFactory - self.assertIsInstance(sqlalchemy_article.author, SQLAlchemyUser) - self.assertIsInstance( - sqlalchemy_article.author.id, # type: ignore - int, - ) - self.assertIsInstance( - sqlalchemy_article.author.is_staff, # type: ignore - bool, - ) - self.assertIsInstance( - sqlalchemy_article.author.date_joined, # type: ignore - datetime, - ) - # Since we're mimicking SQLAlchemy's behaviour, the following line - # would fail on test, however would pass when testing against real - # SQLAlchemy model (as done in the examples). - # self.assertEqual(sqlalchemy_article.author.username, "admin") - - # Testing Factory - self.assertIsInstance(sqlalchemy_article.id, int) - self.assertIsInstance(sqlalchemy_article.slug, str) - - # Testing hooks - self.assertTrue( - hasattr(sqlalchemy_article, "pre_save_called") - and sqlalchemy_article.pre_save_called - ) - self.assertTrue( - hasattr(sqlalchemy_article, "post_save_called") - and sqlalchemy_article.post_save_called - ) - - # Testing batch creation - sqlalchemy_articles = SQLAlchemyArticleFactory.create_batch(5) - self.assertEqual(len(sqlalchemy_articles), 5) - self.assertIsInstance(sqlalchemy_articles[0], SQLAlchemyArticle) + with self.subTest("SQLAlchemyModelFactory"): + sqlalchemy_article = SQLAlchemyArticleFactory( + author__username="admin" + ) - # Testing traits - sqlalchemy_admin_user = SQLAlchemyUserFactory(is_admin_user=True) - self.assertTrue( - sqlalchemy_admin_user.is_staff - and sqlalchemy_admin_user.is_superuser - and sqlalchemy_admin_user.is_active - ) + # Testing SubFactory + self.assertIsInstance(sqlalchemy_article.author, SQLAlchemyUser) + self.assertIsInstance( + sqlalchemy_article.author.id, # type: ignore + int, + ) + self.assertIsInstance( + sqlalchemy_article.author.is_staff, # type: ignore + bool, + ) + self.assertIsInstance( + sqlalchemy_article.author.date_joined, # type: ignore + datetime, + ) + # Since we're mimicking SQLAlchemy's behaviour, the following line + # would fail on test, however would pass when testing against real + # SQLAlchemy model (as done in the examples). + # self.assertEqual(sqlalchemy_article.author.username, "admin") + + # Testing Factory + self.assertIsInstance(sqlalchemy_article.id, int) + self.assertIsInstance(sqlalchemy_article.slug, str) + + # Testing hooks + self.assertTrue( + hasattr(sqlalchemy_article, "pre_save_called") + and sqlalchemy_article.pre_save_called + ) + self.assertTrue( + hasattr(sqlalchemy_article, "post_save_called") + and sqlalchemy_article.post_save_called + ) - # Repeat SQLAlchemy tests for another condition - SQLAlchemySession.return_instance_on_query_first = True + # Testing batch creation + sqlalchemy_articles = SQLAlchemyArticleFactory.create_batch(5) + self.assertEqual(len(sqlalchemy_articles), 5) + self.assertIsInstance(sqlalchemy_articles[0], SQLAlchemyArticle) + + # Testing traits + sqlalchemy_admin_user = SQLAlchemyUserFactory(is_admin_user=True) + self.assertTrue( + sqlalchemy_admin_user.is_staff + and sqlalchemy_admin_user.is_superuser + and sqlalchemy_admin_user.is_active + ) - sqlalchemy_article = SQLAlchemyArticleFactory(author__username="admin") - sqlalchemy_user = SQLAlchemyUserFactory(username="admin") + # Repeat SQLAlchemy tests for another condition + SQLAlchemySession.return_instance_on_query_first = True - # Testing SubFactory - self.assertIsInstance(sqlalchemy_article.author, SQLAlchemyUser) - self.assertIsInstance(sqlalchemy_article, SQLAlchemyArticle) - self.assertIsInstance(sqlalchemy_user, SQLAlchemyUser) - self.assertIsInstance( - sqlalchemy_article.author.id, # type: ignore - int, - ) - self.assertIsInstance( - sqlalchemy_article.author.is_staff, # type: ignore - bool, - ) - self.assertIsInstance( - sqlalchemy_article.author.date_joined, # type: ignore - datetime, - ) - # Since we're mimicking SQLAlchemy's behaviour, the following line - # would fail on test, however would pass when testing against real - # SQLAlchemy model (as done in the examples). - # self.assertEqual(sqlalchemy_article.author.username, "admin") - - # Testing Factory - self.assertIsInstance(sqlalchemy_article.id, int) - self.assertIsInstance(sqlalchemy_article.slug, str) - self.assertIsInstance(sqlalchemy_user.id, int) - self.assertIsInstance(sqlalchemy_user.username, str) - - # Testing hooks - self.assertFalse(hasattr(sqlalchemy_article, "pre_save_called")) - self.assertFalse(hasattr(sqlalchemy_article, "post_save_called")) - - # Testing batch creation - sqlalchemy_articles = SQLAlchemyArticleFactory.create_batch(5) - self.assertEqual(len(sqlalchemy_articles), 5) - self.assertIsInstance(sqlalchemy_articles[0], SQLAlchemyArticle) + sqlalchemy_article = SQLAlchemyArticleFactory( + author__username="admin" + ) + sqlalchemy_user = SQLAlchemyUserFactory(username="admin") + + # Testing SubFactory + self.assertIsInstance(sqlalchemy_article.author, SQLAlchemyUser) + self.assertIsInstance(sqlalchemy_article, SQLAlchemyArticle) + self.assertIsInstance(sqlalchemy_user, SQLAlchemyUser) + self.assertIsInstance( + sqlalchemy_article.author.id, # type: ignore + int, + ) + self.assertIsInstance( + sqlalchemy_article.author.is_staff, # type: ignore + bool, + ) + self.assertIsInstance( + sqlalchemy_article.author.date_joined, # type: ignore + datetime, + ) + # Since we're mimicking SQLAlchemy's behaviour, the following line + # would fail on test, however would pass when testing against real + # SQLAlchemy model (as done in the examples). + # self.assertEqual(sqlalchemy_article.author.username, "admin") + + # Testing Factory + self.assertIsInstance(sqlalchemy_article.id, int) + self.assertIsInstance(sqlalchemy_article.slug, str) + self.assertIsInstance(sqlalchemy_user.id, int) + self.assertIsInstance(sqlalchemy_user.username, str) + + # Testing hooks + self.assertFalse(hasattr(sqlalchemy_article, "pre_save_called")) + self.assertFalse(hasattr(sqlalchemy_article, "post_save_called")) + + # Testing batch creation + sqlalchemy_articles = SQLAlchemyArticleFactory.create_batch(5) + self.assertEqual(len(sqlalchemy_articles), 5) + self.assertIsInstance(sqlalchemy_articles[0], SQLAlchemyArticle) def test_registry_integration(self) -> None: """Test `add`.""" diff --git a/pyproject.toml b/pyproject.toml index 7e7a6f2..62a6480 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "fake.py" description = "Minimalistic, standalone alternative fake data generator with no dependencies." readme = "README.rst" -version = "0.6.2" +version = "0.6.3" dependencies = [] authors = [ {name = "Artur Barseghyan", email = "artur.barseghyan@gmail.com"}, @@ -141,7 +141,7 @@ ignore-path = [ ] [tool.pytest.ini_options] -minversion = "0.6.2" +minversion = "0.6.3" addopts = [ "-ra", "-vvv", @@ -159,6 +159,7 @@ addopts = [ ] testpaths = [ "fake.py", + "__copy_fake.py", ] [tool.coverage.run] @@ -175,3 +176,7 @@ exclude_lines = [ ] [tool.mypy] +check_untyped_defs = true +warn_unused_ignores = true +warn_redundant_casts = true +warn_unused_configs = true