From be613219f285efc414bbb6b7a5020577822d5706 Mon Sep 17 00:00:00 2001 From: Artur Barseghyan Date: Mon, 18 Dec 2023 00:17:27 +0100 Subject: [PATCH] Fix/mypy issues (#52) * Fix mypy issues * Rework docs --- .../dataclasses/article/factories.py | 1 + .../factories/dataclasses/article/models.py | 1 + .../factories/django/article/factories.py | 1 + .../factories/django/article/models.py | 1 + .../examples/factories/django_factories_1.py | 46 -- .../examples/factories/django_models_1.py | 19 - .../factories/pydantic/article/factories.py | 1 + .../factories/pydantic/article/models.py | 1 + .../factories/pydantic_factories_1.py | 41 -- .../examples/factories/pydantic_models_1.py | 36 - .../factories/sqlalchemy/article/factories.py | 1 + .../factories/sqlalchemy/article/models.py | 1 + .../examples/factories/sqlalchemy/config.py | 1 + .../factories/tortoise/article/factories.py | 1 + .../factories/tortoise/article/models.py | 1 + .../factories/tortoise_factories_1.py | 41 -- .../examples/factories/tortoise_models_1.py | 36 - docs/conf.py | 1 + docs/factories.rst | 627 +++--------------- docs/recipes.rst | 2 +- examples/dataclasses/article/factories.py | 4 + examples/django/article/factories.py | 54 ++ ...er_article_pub_date.py => 0001_initial.py} | 18 +- examples/django/article/models.py | 2 + examples/pydantic/article/factories.py | 4 + examples/sqlalchemy/article/factories.py | 4 + examples/tortoise/article/factories.py | 3 + fake.py | 70 +- 28 files changed, 216 insertions(+), 803 deletions(-) create mode 120000 docs/_static/examples/factories/dataclasses/article/factories.py create mode 120000 docs/_static/examples/factories/dataclasses/article/models.py create mode 120000 docs/_static/examples/factories/django/article/factories.py create mode 120000 docs/_static/examples/factories/django/article/models.py delete mode 100644 docs/_static/examples/factories/django_factories_1.py delete mode 100644 docs/_static/examples/factories/django_models_1.py create mode 120000 docs/_static/examples/factories/pydantic/article/factories.py create mode 120000 docs/_static/examples/factories/pydantic/article/models.py delete mode 100644 docs/_static/examples/factories/pydantic_factories_1.py delete mode 100644 docs/_static/examples/factories/pydantic_models_1.py create mode 120000 docs/_static/examples/factories/sqlalchemy/article/factories.py create mode 120000 docs/_static/examples/factories/sqlalchemy/article/models.py create mode 120000 docs/_static/examples/factories/sqlalchemy/config.py create mode 120000 docs/_static/examples/factories/tortoise/article/factories.py create mode 120000 docs/_static/examples/factories/tortoise/article/models.py delete mode 100644 docs/_static/examples/factories/tortoise_factories_1.py delete mode 100644 docs/_static/examples/factories/tortoise_models_1.py rename examples/django/article/migrations/{0001_squashed_0003_alter_article_pub_date.py => 0001_initial.py} (88%) diff --git a/docs/_static/examples/factories/dataclasses/article/factories.py b/docs/_static/examples/factories/dataclasses/article/factories.py new file mode 120000 index 0000000..d5c1196 --- /dev/null +++ b/docs/_static/examples/factories/dataclasses/article/factories.py @@ -0,0 +1 @@ +../../../../../../examples/dataclasses/article/factories.py \ No newline at end of file diff --git a/docs/_static/examples/factories/dataclasses/article/models.py b/docs/_static/examples/factories/dataclasses/article/models.py new file mode 120000 index 0000000..59178c9 --- /dev/null +++ b/docs/_static/examples/factories/dataclasses/article/models.py @@ -0,0 +1 @@ +../../../../../../examples/dataclasses/article/models.py \ No newline at end of file diff --git a/docs/_static/examples/factories/django/article/factories.py b/docs/_static/examples/factories/django/article/factories.py new file mode 120000 index 0000000..45d436a --- /dev/null +++ b/docs/_static/examples/factories/django/article/factories.py @@ -0,0 +1 @@ +../../../../../../examples/django/article/factories.py \ No newline at end of file diff --git a/docs/_static/examples/factories/django/article/models.py b/docs/_static/examples/factories/django/article/models.py new file mode 120000 index 0000000..1c96f31 --- /dev/null +++ b/docs/_static/examples/factories/django/article/models.py @@ -0,0 +1 @@ +../../../../../../examples/django/article/models.py \ No newline at end of file diff --git a/docs/_static/examples/factories/django_factories_1.py b/docs/_static/examples/factories/django_factories_1.py deleted file mode 100644 index a941d9e..0000000 --- a/docs/_static/examples/factories/django_factories_1.py +++ /dev/null @@ -1,46 +0,0 @@ -from django.conf import settings -from django.contrib.auth.models import User -from fake import ( - DjangoModelFactory, - Factory, - FileSystemStorage, - SubFactory, - pre_save, -) - -from article.models import Article - -STORAGE = FileSystemStorage(root_path=settings.MEDIA_ROOT, rel_path="tmp") - - -class UserFactory(DjangoModelFactory): - 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 - - @pre_save - def __set_password(instance): - instance.set_password("test") - - -class ArticleFactory(DjangoModelFactory): - 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(UserFactory) - - class Meta: - model = Article diff --git a/docs/_static/examples/factories/django_models_1.py b/docs/_static/examples/factories/django_models_1.py deleted file mode 100644 index 2979637..0000000 --- a/docs/_static/examples/factories/django_models_1.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.conf import settings -from django.db import models -from django.utils import timezone - - -class Article(models.Model): - title = models.CharField(max_length=255) - slug = models.SlugField(unique=True) - content = models.TextField() - image = models.ImageField(null=True, blank=True) - pub_date = models.DateTimeField(default=timezone.now) - safe_for_work = models.BooleanField(default=False) - minutes_to_read = models.IntegerField(default=5) - author = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.CASCADE - ) - - def __str__(self): - return self.title diff --git a/docs/_static/examples/factories/pydantic/article/factories.py b/docs/_static/examples/factories/pydantic/article/factories.py new file mode 120000 index 0000000..02c385f --- /dev/null +++ b/docs/_static/examples/factories/pydantic/article/factories.py @@ -0,0 +1 @@ +../../../../../../examples/pydantic/article/factories.py \ No newline at end of file diff --git a/docs/_static/examples/factories/pydantic/article/models.py b/docs/_static/examples/factories/pydantic/article/models.py new file mode 120000 index 0000000..446e58f --- /dev/null +++ b/docs/_static/examples/factories/pydantic/article/models.py @@ -0,0 +1 @@ +../../../../../../examples/pydantic/article/models.py \ No newline at end of file diff --git a/docs/_static/examples/factories/pydantic_factories_1.py b/docs/_static/examples/factories/pydantic_factories_1.py deleted file mode 100644 index 86311de..0000000 --- a/docs/_static/examples/factories/pydantic_factories_1.py +++ /dev/null @@ -1,41 +0,0 @@ -from pathlib import Path - -from fake import Factory, FileSystemStorage, ModelFactory, SubFactory - -from article.models import Article, User - -BASE_DIR = Path(__file__).resolve().parent.parent -MEDIA_ROOT = BASE_DIR / "media" - -STORAGE = FileSystemStorage(root_path=MEDIA_ROOT, rel_path="tmp") - - -class UserFactory(ModelFactory): - 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 - - -class ArticleFactory(ModelFactory): - 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(UserFactory) - - class Meta: - model = Article diff --git a/docs/_static/examples/factories/pydantic_models_1.py b/docs/_static/examples/factories/pydantic_models_1.py deleted file mode 100644 index 3d40dbc..0000000 --- a/docs/_static/examples/factories/pydantic_models_1.py +++ /dev/null @@ -1,36 +0,0 @@ -from datetime import datetime -from typing import Optional - -from pydantic import BaseModel, Field - - -class User(BaseModel): - id: int - username: str = Field(..., max_length=255) - first_name: str = Field(..., max_length=255) - last_name: str = Field(..., max_length=255) - email: str = Field(..., max_length=255) - password: Optional[str] = Field("", max_length=255) - last_login: Optional[datetime] - is_superuser: bool = Field(default=False) - is_staff: bool = Field(default=False) - is_active: bool = Field(default=True) - date_joined: Optional[datetime] - - def __str__(self): - return self.username - - -class Article(BaseModel): - id: int - title: str = Field(..., max_length=255) - slug: str = Field(..., max_length=255, unique=True) - content: 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 - minutes_to_read: int = 5 - author: User - - def __str__(self): - return self.title diff --git a/docs/_static/examples/factories/sqlalchemy/article/factories.py b/docs/_static/examples/factories/sqlalchemy/article/factories.py new file mode 120000 index 0000000..8e0ad0e --- /dev/null +++ b/docs/_static/examples/factories/sqlalchemy/article/factories.py @@ -0,0 +1 @@ +../../../../../../examples/sqlalchemy/article/factories.py \ No newline at end of file diff --git a/docs/_static/examples/factories/sqlalchemy/article/models.py b/docs/_static/examples/factories/sqlalchemy/article/models.py new file mode 120000 index 0000000..fa76360 --- /dev/null +++ b/docs/_static/examples/factories/sqlalchemy/article/models.py @@ -0,0 +1 @@ +../../../../../../examples/sqlalchemy/article/models.py \ No newline at end of file diff --git a/docs/_static/examples/factories/sqlalchemy/config.py b/docs/_static/examples/factories/sqlalchemy/config.py new file mode 120000 index 0000000..6096fa6 --- /dev/null +++ b/docs/_static/examples/factories/sqlalchemy/config.py @@ -0,0 +1 @@ +../../../../../examples/sqlalchemy/config.py \ No newline at end of file diff --git a/docs/_static/examples/factories/tortoise/article/factories.py b/docs/_static/examples/factories/tortoise/article/factories.py new file mode 120000 index 0000000..a9d552f --- /dev/null +++ b/docs/_static/examples/factories/tortoise/article/factories.py @@ -0,0 +1 @@ +../../../../../../examples/tortoise/article/factories.py \ No newline at end of file diff --git a/docs/_static/examples/factories/tortoise/article/models.py b/docs/_static/examples/factories/tortoise/article/models.py new file mode 120000 index 0000000..1ca4022 --- /dev/null +++ b/docs/_static/examples/factories/tortoise/article/models.py @@ -0,0 +1 @@ +../../../../../../examples/tortoise/article/models.py \ No newline at end of file diff --git a/docs/_static/examples/factories/tortoise_factories_1.py b/docs/_static/examples/factories/tortoise_factories_1.py deleted file mode 100644 index c6e8d7e..0000000 --- a/docs/_static/examples/factories/tortoise_factories_1.py +++ /dev/null @@ -1,41 +0,0 @@ -from pathlib import Path - -from fake import Factory, FileSystemStorage, SubFactory, TortoiseModelFactory - -from article.models import Article, User - -BASE_DIR = Path(__file__).resolve().parent.parent -MEDIA_ROOT = BASE_DIR / "media" - -STORAGE = FileSystemStorage(root_path=MEDIA_ROOT, rel_path="tmp") - - -class UserFactory(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 - - -class ArticleFactory(TortoiseModelFactory): - id = Factory.pyint() - title = Factory.sentence() - slug = Factory.slug() - content = Factory.text() - image = Factory.png_file(storage=STORAGE) - pub_date = Factory.date_time() - safe_for_work = Factory.pybool() - minutes_to_read = Factory.pyint(min_value=1, max_value=10) - author = SubFactory(UserFactory) - - class Meta: - model = Article diff --git a/docs/_static/examples/factories/tortoise_models_1.py b/docs/_static/examples/factories/tortoise_models_1.py deleted file mode 100644 index b61ecaa..0000000 --- a/docs/_static/examples/factories/tortoise_models_1.py +++ /dev/null @@ -1,36 +0,0 @@ -from datetime import datetime - -from tortoise import fields -from tortoise.models import Model - - -class User(Model): - id = fields.IntField(pk=True) - username = fields.CharField(max_length=255) - first_name = fields.CharField(max_length=255) - last_name = fields.CharField(max_length=255) - email = fields.CharField(max_length=255) - password = fields.CharField(max_length=255, null=True, blank=True) - last_login = fields.DatetimeField(null=True, blank=True) - is_superuser = fields.BooleanField(default=False) - is_staff = fields.BooleanField(default=False) - is_active = fields.BooleanField(default=True) - date_joined = fields.DatetimeField(null=True, blank=True) - - def __str__(self): - return self.title - - -class Article(Model): - id = fields.IntField(pk=True) - title = fields.CharField(max_length=255) - slug = fields.CharField(max_length=255, unique=True) - content = fields.TextField() - image = fields.TextField(null=True, blank=True) - pub_date = fields.DatetimeField(default=datetime.now) - safe_for_work = fields.BooleanField(default=False) - minutes_to_read = fields.IntField(default=5) - author = fields.ForeignKeyField("models.User", on_delete=fields.CASCADE) - - def __str__(self): - return self.title diff --git a/docs/conf.py b/docs/conf.py index a95a2e6..a7afb45 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,6 +52,7 @@ html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] +# html_extra_path = ["examples"] prismjs_base = "//cdnjs.cloudflare.com/ajax/libs/prism/1.29.0" diff --git a/docs/factories.rst b/docs/factories.rst index 2bced58..75db51d 100644 --- a/docs/factories.rst +++ b/docs/factories.rst @@ -24,129 +24,25 @@ Django example -------------- **article/models.py** -.. code-block:: python - - from django.conf import settings - from django.db import models - from django.utils import timezone - +.. container:: jsphinx-download - class Article(models.Model): - 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) - minutes_to_read = models.IntegerField(default=5) - author = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.CASCADE - ) + .. literalinclude:: _static/examples/factories/django/article/models.py + :language: python + :lines: 1-3, 9-23 - def __str__(self): - return self.title + *See the full example* + :download:`here <_static/examples/factories/django/article/models.py>` **article/factories.py** -.. code-block:: python - - import random - from functools import partial - - from django.conf import settings - from django.contrib.auth.models import Group, User - from fake import ( - FACTORY, - DjangoModelFactory, - FileSystemStorage, - LazyAttribute, - LazyFunction, - SubFactory, - PreSave, - PostSave, - post_save, - trait, - ) - - from article.models import Article - - # For Django, all files shall be placed inside `MEDIA_ROOT` directory. - # That's why you need to apply this trick - define a - # 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 GroupFactory(ModelFactory): - - name = FACTORY.word() - - class Meta: - model = Group - get_or_create = ("name",) - - - def set_password(user: User, password: str) -> None: - user.set_password(password) - - - def add_to_group(user: User, name: str) -> None: - group = GroupFactory(name=name) - user.groups.add(group) - - - class UserFactory(DjangoModelFactory): +.. container:: jsphinx-download - 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() - password = PreSave(set_password, password="test1234") - group = PostSave(add_to_group, name="Test group") + .. literalinclude:: _static/examples/factories/django/article/factories.py + :language: python + :lines: 1-21, 31-120, 130-162 - class Meta: - model = User - get_or_create = ("username",) - - @post_save - def _send_registration_email(self, instance): - """Send an email with registration information.""" - # Your code here - - @trait - def is_admin_user(self, instance: User) -> None: - instance.is_superuser = True - instance.is_staff = True - instance.is_active = True - - - class ArticleFactory(DjangoModelFactory): - - 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 + *See the full example* + :download:`here <_static/examples/factories/django/article/factories.py>` **Usage example** @@ -173,342 +69,95 @@ Django example # Create a user with custom password user = UserFactory( - password = PreSave(set_password, password="another-pass"), + password=PreSave(set_password, password="another-pass"), ) # Add a user to another group user = UserFactory( - group = PostSave(add_to_group, name="Another group"), + group=PostSave(add_to_group, name="Another group"), + ) + + # Or even add user to multiple groups at once + user = UserFactory( + group_1=PostSave(add_to_group, name="Another group"), + group_2=PostSave(add_to_group, name="Yet another group"), ) Pydantic example ---------------- **article/models.py** -.. code-block:: python - - from datetime import datetime - from typing import Optional - - from pydantic import BaseModel, Field - - class User(BaseModel): - id: int - username: str = Field(..., max_length=255) - first_name: str = Field(..., max_length=255) - last_name: str = Field(..., max_length=255) - email: str = Field(..., max_length=255) - password: Optional[str] = Field("", max_length=255) - last_login: Optional[datetime] - is_superuser: bool = Field(default=False) - is_staff: bool = Field(default=False) - is_active: bool = Field(default=True) - date_joined: Optional[datetime] - - def __str__(self): - return self.username - - class Article(BaseModel): - id: int - 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 - minutes_to_read: int = 5 - author: User - - def __str__(self): - return self.title - -**article/factories.py** +.. container:: jsphinx-download -.. code-block:: python + .. literalinclude:: _static/examples/factories/pydantic/article/models.py + :language: python + :lines: 1-5, 15- - import random - from functools import partial - from pathlib import Path + *See the full example* + :download:`here <_static/examples/factories/pydantic/article/models.py>` - from fake import FACTORY, FileSystemStorage, ModelFactory, SubFactory +**article/factories.py** - from article.models import Article, User +.. container:: jsphinx-download - 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", - ) + .. literalinclude:: _static/examples/factories/pydantic/article/factories.py + :language: python + :lines: 1-15, 25-72, 83-98 - class UserFactory(ModelFactory): - 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 - - @trait - def is_admin_user(self, instance: User) -> None: - instance.is_superuser = True - instance.is_staff = True - instance.is_active = True - - @pre_save - def _set_password(self, instance): - instance.set_password("test") - - 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 + *See the full example* + :download:`here <_static/examples/factories/pydantic/article/factories.py>` *Used just like in previous example.* TortoiseORM example ------------------- - **article/models.py** -.. code-block:: python - - from datetime import datetime - - from tortoise import fields - from tortoise.models import Model - - class User(Model): - - id = fields.IntField(pk=True) - username = fields.CharField(max_length=255, unique=True) - first_name = fields.CharField(max_length=255) - last_name = fields.CharField(max_length=255) - email = fields.CharField(max_length=255) - password = fields.CharField(max_length=255, null=True, blank=True) - last_login = fields.DatetimeField(null=True, blank=True) - is_superuser = fields.BooleanField(default=False) - is_staff = fields.BooleanField(default=False) - is_active = fields.BooleanField(default=True) - date_joined = fields.DatetimeField(null=True, blank=True) - - def __str__(self): - return self.title - - class Article(Model): - - id = fields.IntField(pk=True) - 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) - minutes_to_read = fields.IntField(default=5) - author = fields.ForeignKeyField("models.User", on_delete=fields.CASCADE) - - def __str__(self): - return self.title +.. container:: jsphinx-download -**article/factories.py** - -.. code-block:: python + .. literalinclude:: _static/examples/factories/tortoise/article/models.py + :language: python + :lines: 1-5, 15-21, 25-41, 45-61 - import random - from functools import partial - from pathlib import Path + *See the full example* + :download:`here <_static/examples/factories/tortoise/article/models.py>` - from fake import FACTORY, FileSystemStorage, SubFactory, TortoiseModelFactory +**article/factories.py** - from article.models import Article, User +.. container:: jsphinx-download - 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", - ) + .. literalinclude:: _static/examples/factories/tortoise/article/factories.py + :language: python + :lines: 1-16, 26-81, 91-106 - class UserFactory(TortoiseModelFactory): - """User factory.""" - - 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",) - - @trait - def is_admin_user(self, instance: User) -> None: - instance.is_superuser = True - instance.is_staff = True - instance.is_active = True - - @pre_save - def _set_password(self, instance): - instance.set_password("test") - - class ArticleFactory(TortoiseModelFactory): - """Article factory.""" - - 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() - minutes_to_read = FACTORY.pyint(min_value=1, max_value=10) - author = SubFactory(UserFactory) - - class Meta: - model = Article + *See the full example* + :download:`here <_static/examples/factories/tortoise/article/factories.py>` *Used just like in previous example.* Dataclasses example ------------------- - **article/models.py** -.. code-block:: python +.. container:: jsphinx-download - import random - from dataclasses import dataclass - from datetime import datetime - from functools import partial - from typing import Optional - - @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 = datetime.now() - safe_for_work: bool = False - minutes_to_read: int = 5 - - def __str__(self): - return self.title + .. literalinclude:: _static/examples/factories/dataclasses/article/models.py + :language: python + :lines: 1-5, 15- -**article/factories.py** - -.. code-block:: python - - import random - from functools import partial - from pathlib import Path + *See the full example* + :download:`here <_static/examples/factories/dataclasses/article/models.py>` - from fake import FACTORY, FileSystemStorage, ModelFactory, SubFactory +**article/factories.py** - from article.models import Article, User +.. container:: jsphinx-download - 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", - ) + .. literalinclude:: _static/examples/factories/dataclasses/article/factories.py + :language: python + :lines: 1-15, 25-72, 83-97 - class UserFactory(ModelFactory): - 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 - - @trait - def is_admin_user(self, instance: User) -> None: - instance.is_superuser = True - instance.is_staff = True - instance.is_active = True - - @pre_save - def _set_password(self, instance): - instance.set_password("test") - - 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 + *See the full example* + :download:`here <_static/examples/factories/dataclasses/article/factories.py>` *Used just like in previous example.* @@ -517,157 +166,35 @@ SQLAlchemy example **config.py** -.. code-block:: python +.. container:: jsphinx-download - from sqlalchemy import create_engine - from sqlalchemy.orm import scoped_session, sessionmaker + .. literalinclude:: _static/examples/factories/sqlalchemy/config.py + :language: python + :lines: 1-2, 12- - DATABASE_URL = "sqlite:///test_database.db" - ENGINE = create_engine(DATABASE_URL) - SESSION = scoped_session(sessionmaker(bind=ENGINE)) + *See the full example* + :download:`here <_static/examples/factories/sqlalchemy/config.py>` **article/models.py** -.. code-block:: python +.. container:: jsphinx-download - from datetime import datetime + .. literalinclude:: _static/examples/factories/sqlalchemy/article/models.py + :language: python + :lines: 1-15, 25-45, 49-74, 78-98 - from sqlalchemy import ( - Boolean, - Column, - DateTime, - ForeignKey, - Integer, - String, - Text, - ) - from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy.orm import relationship - - Base = declarative_base() - - class User(Base): - """User model.""" - - __tablename__ = "users" - - id = Column(Integer, primary_key=True) - username = Column(String(255), unique=True) - first_name = Column(String(255)) - last_name = Column(String(255)) - email = Column(String(255)) - password = Column(String(255), nullable=True) - last_login = Column(DateTime, nullable=True) - is_superuser = Column(Boolean, default=False) - is_staff = Column(Boolean, default=False) - is_active = Column(Boolean, default=True) - date_joined = Column(DateTime, nullable=True) - - articles = relationship("Article", back_populates="author") - - class Article(Base): - """Article model.""" - - __tablename__ = "articles" - - id = Column(Integer, primary_key=True) - 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) - minutes_to_read = Column(Integer, default=5) - author_id = Column(Integer, ForeignKey("users.id")) - - author = relationship("User", back_populates="articles") + *See the full example* + :download:`here <_static/examples/factories/sqlalchemy/article/models.py>` **article/factories.py** -Pay attention to the ``MetaSQLAlchemy`` meta-class and the ``get_session`` -method. - -.. code-block:: python - - import random - from functools import partial - from pathlib import Path - - from fake import ( - FACTORY, - FileSystemStorage, - SQLAlchemyModelFactory, - SubFactory, - post_save, - pre_save, - trait, - ) - - from article.models import Article, User - from config import SESSION +.. container:: jsphinx-download - 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", - ) + .. literalinclude:: _static/examples/factories/sqlalchemy/article/factories.py + :language: python + :lines: 1-16, 25-96, 107-125 - def get_session(): - return SESSION() - - class UserFactory(SQLAlchemyModelFactory): - """User factory.""" - - 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",) - - class MetaSQLAlchemy: - get_session = get_session - - @trait - def is_admin_user(self, instance: User) -> None: - instance.is_superuser = True - instance.is_staff = True - instance.is_active = True - - @pre_save - def _set_password(self, instance): - instance.set_password("test") - - class ArticleFactory(SQLAlchemyModelFactory): - """Article factory.""" - - 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 - - class MetaSQLAlchemy: - get_session = get_session + *See the full example* + :download:`here <_static/examples/factories/sqlalchemy/article/factories.py>` *Used just like in previous example.* diff --git a/docs/recipes.rst b/docs/recipes.rst index 6699632..a11f38e 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -263,7 +263,7 @@ Returns a random IPv4 address. ---- -**date* +**date** Generates a random date between ``start_date`` and ``end_date``. diff --git a/examples/dataclasses/article/factories.py b/examples/dataclasses/article/factories.py index 056ac93..7b3fcef 100644 --- a/examples/dataclasses/article/factories.py +++ b/examples/dataclasses/article/factories.py @@ -73,10 +73,12 @@ def is_admin_user(self, instance: User) -> None: @pre_save def _pre_save_method(self, instance): + # For testing purposes only instance.pre_save_called = True @post_save def _post_save_method(self, instance): + # For testing purposes only instance.post_save_called = True @@ -96,8 +98,10 @@ class Meta: @pre_save def _pre_save_method(self, instance): + # For testing purposes only instance.pre_save_called = True @post_save def _post_save_method(self, instance): + # For testing purposes only instance.post_save_called = True diff --git a/examples/django/article/factories.py b/examples/django/article/factories.py index b443eb8..e46a7ee 100644 --- a/examples/django/article/factories.py +++ b/examples/django/article/factories.py @@ -1,3 +1,6 @@ +import random +from functools import partial + from django.conf import settings from django.contrib.auth.models import Group, User from django.utils import timezone @@ -5,6 +8,8 @@ FACTORY, DjangoModelFactory, FileSystemStorage, + LazyAttribute, + LazyFunction, PostSave, PreSave, SubFactory, @@ -20,10 +25,20 @@ __license__ = "MIT" __all__ = ( "ArticleFactory", + "GroupFactory", "UserFactory", ) +# For Django, all files shall be placed inside `MEDIA_ROOT` directory. +# That's why you need to apply this trick - define a +# 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 GroupFactory(DjangoModelFactory): @@ -52,8 +67,28 @@ class UserFactory(DjangoModelFactory): .. code-block:: python + # Create a user. Created user will automatically have his password + # set to "test1234" and will be added to the group "Test group". user = UserFactory() + + # Create 5 users. users = UserFactory.create_batch(5) + + # Create a user with custom password + user = UserFactory( + password=PreSave(set_password, password="another-pass"), + ) + + # Add a user to another group + user = UserFactory( + group=PostSave(add_to_group, name="Another group"), + ) + + # Or even add user to multiple groups at once + user = UserFactory( + group_1=PostSave(add_to_group, name="Another group"), + group_2=PostSave(add_to_group, name="Yet another group"), + ) """ username = FACTORY.username() @@ -72,6 +107,11 @@ class Meta: model = User get_or_create = ("username",) + @post_save + def _send_registration_email(self, instance): + """Send an email with registration information.""" + # Your code here + @trait def is_admin_user(self, instance: User) -> None: instance.is_superuser = True @@ -80,10 +120,12 @@ def is_admin_user(self, instance: User) -> None: @pre_save def _pre_save_method(self, instance): + # For testing purposes only instance.pre_save_called = True @post_save def _post_save_method(self, instance): + # For testing purposes only instance.post_save_called = True @@ -92,13 +134,23 @@ class ArticleFactory(DjangoModelFactory): Usage example: + .. code-block:: python + + # Create one article article = ArticleFactory() + + # Create 5 articles articles = ArticleFactory.create_batch(5) + + # Create one article with authors username set to admin. + article = ArticleFactory(author__username="admin") """ 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(tzinfo=timezone.get_current_timezone()) safe_for_work = FACTORY.pybool() @@ -110,8 +162,10 @@ class Meta: @pre_save def _pre_save_method(self, instance): + # For testing purposes only instance.pre_save_called = True @post_save def _post_save_method(self, instance): + # For testing purposes only instance.post_save_called = True diff --git a/examples/django/article/migrations/0001_squashed_0003_alter_article_pub_date.py b/examples/django/article/migrations/0001_initial.py similarity index 88% rename from examples/django/article/migrations/0001_squashed_0003_alter_article_pub_date.py rename to examples/django/article/migrations/0001_initial.py index 0d14e19..beaf0da 100644 --- a/examples/django/article/migrations/0001_squashed_0003_alter_article_pub_date.py +++ b/examples/django/article/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.7 on 2023-12-07 21:37 +# Generated by Django 4.2.7 on 2023-12-17 20:58 import django.db.models.deletion import django.utils.timezone @@ -7,12 +7,6 @@ class Migration(migrations.Migration): - replaces = [ - ("article", "0001_initial"), - ("article", "0002_article_image"), - ("article", "0003_alter_article_pub_date"), - ] - initial = True dependencies = [ @@ -35,6 +29,12 @@ class Migration(migrations.Migration): ("title", models.CharField(max_length=255)), ("slug", models.SlugField(unique=True)), ("content", models.TextField()), + ("headline", models.TextField()), + ("category", models.CharField(max_length=255)), + ( + "image", + models.ImageField(blank=True, null=True, upload_to=""), + ), ( "pub_date", models.DateField(default=django.utils.timezone.now), @@ -48,10 +48,6 @@ class Migration(migrations.Migration): to=settings.AUTH_USER_MODEL, ), ), - ( - "image", - models.ImageField(blank=True, null=True, upload_to=""), - ), ], ), ] diff --git a/examples/django/article/models.py b/examples/django/article/models.py index 8b8368e..53c24ad 100644 --- a/examples/django/article/models.py +++ b/examples/django/article/models.py @@ -12,6 +12,8 @@ class Article(models.Model): title = models.CharField(max_length=255) slug = models.SlugField(unique=True) content = models.TextField() + headline = models.TextField() + category = models.CharField(max_length=255) image = models.ImageField(null=True, blank=True) pub_date = models.DateField(default=timezone.now) safe_for_work = models.BooleanField(default=False) diff --git a/examples/pydantic/article/factories.py b/examples/pydantic/article/factories.py index 99783ca..7463752 100644 --- a/examples/pydantic/article/factories.py +++ b/examples/pydantic/article/factories.py @@ -73,10 +73,12 @@ def is_admin_user(self, instance: User) -> None: @pre_save def _pre_save_method(self, instance): + # For testing purposes only instance.pre_save_called = True @post_save def _post_save_method(self, instance): + # For testing purposes only instance.post_save_called = True @@ -96,8 +98,10 @@ class Meta: @pre_save def _pre_save_method(self, instance): + # For testing purposes only instance.pre_save_called = True @post_save def _post_save_method(self, instance): + # For testing purposes only instance.post_save_called = True diff --git a/examples/sqlalchemy/article/factories.py b/examples/sqlalchemy/article/factories.py index aba859c..92c5d0c 100644 --- a/examples/sqlalchemy/article/factories.py +++ b/examples/sqlalchemy/article/factories.py @@ -97,10 +97,12 @@ def is_admin_user(self, instance: User) -> None: @pre_save def _pre_save_method(self, instance): + # For testing purposes only instance.pre_save_called = True @post_save def _post_save_method(self, instance): + # For testing purposes only instance.post_save_called = True @@ -124,8 +126,10 @@ class MetaSQLAlchemy: @pre_save def _pre_save_method(self, instance): + # For testing purposes only instance.pre_save_called = True @post_save def _post_save_method(self, instance): + # For testing purposes only instance.post_save_called = True diff --git a/examples/tortoise/article/factories.py b/examples/tortoise/article/factories.py index ed961cf..1c236d2 100644 --- a/examples/tortoise/article/factories.py +++ b/examples/tortoise/article/factories.py @@ -82,6 +82,7 @@ def is_admin_user(self, instance: User) -> None: @pre_save def _pre_save_method(self, instance): + # For testing purposes only instance.pre_save_called = True @post_save @@ -106,8 +107,10 @@ class Meta: @pre_save def _pre_save_method(self, instance): + # For testing purposes only instance.pre_save_called = True @post_save def _post_save_method(self, instance): + # For testing purposes only instance.post_save_called = True diff --git a/fake.py b/fake.py index 55e0d81..085558e 100644 --- a/fake.py +++ b/fake.py @@ -2485,7 +2485,7 @@ def get_provider_for_type(cls, field_type) -> Optional[Callable]: return cls.TYPE_TO_PROVIDER.get(field_type) @classmethod - def fill(cls, dataclass_type: Type) -> object: + def fill(cls, dataclass_type: Type) -> Any: """Fill dataclass with data.""" if not is_dataclass(dataclass_type): raise ValueError("The provided type must be a dataclass") @@ -2536,7 +2536,7 @@ def is_class_type(cls, type_hint): ) @classmethod - def fill(cls, object_type: Type) -> object: + def fill(cls, object_type: Type) -> Any: if not ( hasattr(object_type, "__fields__") and hasattr(object_type, "Config") @@ -3262,7 +3262,9 @@ def set_password(self, password: str) -> None: @classproperty def objects(cls): """Mimicking Django's Manager behaviour.""" - return DjangoManager(instance=fill_dataclass(cls)) + return DjangoManager( + instance=fill_dataclass(cls), # type: ignore + ) @dataclass class Article: @@ -3289,7 +3291,9 @@ def save(self, *args, **kwargs): @classproperty def objects(cls): """Mimicking Django's Manager behaviour.""" - return DjangoManager(instance=fill_dataclass(cls)) + return DjangoManager( + instance=fill_dataclass(cls), # type: ignore + ) with self.subTest("fill_pydantic_model on dataclass"): with self.assertRaises(ValueError): @@ -3389,13 +3393,13 @@ class Meta: # Testing SubFactory self.assertIsInstance(_article.author, User) - self.assertIsInstance(_article.author.id, int) # type: ignore + self.assertIsInstance(_article.author.id, int) self.assertIsInstance( - _article.author.is_staff, # type: ignore + _article.author.is_staff, bool, ) self.assertIsInstance( - _article.author.date_joined, # type: ignore + _article.author.date_joined, datetime, ) @@ -3432,11 +3436,11 @@ class Meta: ) # Testing PreSave - self.assertEqual(xor_transform(_user.password), "test1234") + self.assertEqual(xor_transform(str(_user.password)), "test1234") _user = UserFactory( password=PreSave(set_password, password="1234test") ) - self.assertEqual(xor_transform(_user.password), "1234test") + self.assertEqual(xor_transform(str(_user.password)), "1234test") # Testing PostSave self.assertEqual(list(_user.groups)[0].name, "TestGroup1234") @@ -3563,18 +3567,30 @@ def _post_save_method(self, instance): # Testing PreSave _django_user = DjangoUserFactory() - self.assertEqual(xor_transform(_django_user.password), "jest1234") + self.assertEqual( + xor_transform(str(_django_user.password)), + "jest1234", + ) _django_user = DjangoUserFactory( password=PreSave(set_password, password="1234jest") ) - self.assertEqual(xor_transform(_django_user.password), "1234jest") + self.assertEqual( + xor_transform(str(_django_user.password)), + "1234jest", + ) # Testing PostSave - self.assertEqual(list(_django_user.groups)[0].name, "JestGroup1234") + self.assertEqual( + list(_django_user.groups)[0].name, # type: ignore + "JestGroup1234", + ) _django_user = DjangoUserFactory( group=PostSave(add_to_group, name="1234JestGroup") ) - self.assertEqual(list(_django_user.groups)[0].name, "1234JestGroup") + self.assertEqual( + list(_django_user.groups)[0].name, # type: ignore + "1234JestGroup", + ) # ********************************** # ****** TortoiseModelFactory ****** @@ -3789,21 +3805,29 @@ def _post_save_method(self, instance): # Testing PreSave _tortoise_user = TortoiseUserFactory() - self.assertEqual(xor_transform(_tortoise_user.password), "tost1234") + self.assertEqual( + xor_transform(str(_tortoise_user.password)), + "tost1234", + ) _tortoise_user = TortoiseUserFactory( password=PreSave(set_password, password="1234tost") ) - self.assertEqual(xor_transform(_tortoise_user.password), "1234tost") + self.assertEqual( + xor_transform(str(_tortoise_user.password)), + "1234tost", + ) # Testing PostSave self.assertEqual( - list(_tortoise_user.groups)[0].name, "TostGroup1234" + list(_tortoise_user.groups)[0].name, # type: ignore + "TostGroup1234", ) _tortoise_user = TortoiseUserFactory( group=PostSave(add_to_tortoise_group, name="1234TostGroup") ) self.assertEqual( - list(_tortoise_user.groups)[0].name, "1234TostGroup" + list(_tortoise_user.groups)[0].name, # type: ignore + "1234TostGroup", ) # ********************************** @@ -3889,7 +3913,7 @@ def first(self): if not self.return_instance_on_query_first: return None - return fill_dataclass(self.model) + return fill_dataclass(self.model) # type: ignore def get_sqlalchemy_session(): return SQLAlchemySession() @@ -4069,24 +4093,26 @@ def _post_save_method(self, instance): # Testing PreSave _sqlalchemy_user = SQLAlchemyUserFactory() self.assertEqual( - xor_transform(_sqlalchemy_user.password), "sest1234" + xor_transform(str(_sqlalchemy_user.password)), "sest1234" ) _sqlalchemy_user = SQLAlchemyUserFactory( password=PreSave(set_password, password="1234sest") ) self.assertEqual( - xor_transform(_sqlalchemy_user.password), "1234sest" + xor_transform(str(_sqlalchemy_user.password)), "1234sest" ) # Testing PostSave self.assertEqual( - list(_sqlalchemy_user.groups)[0].name, "SestGroup1234" + list(_sqlalchemy_user.groups)[0].name, # type: ignore + "SestGroup1234", ) _sqlalchemy_user = SQLAlchemyUserFactory( group=PostSave(add_to_sqlalchemy_group, name="1234SestGroup") ) self.assertEqual( - list(_sqlalchemy_user.groups)[0].name, "1234SestGroup" + list(_sqlalchemy_user.groups)[0].name, # type: ignore + "1234SestGroup", ) # Repeat SQLAlchemy tests for another condition