Skip to main content

EasyFactory is a wrapper above FactoryBoy to allow the generation of Factory class with mandatory parameter filed.

Project description

EasyFactory

pipeline coverage release

Description

EasyFactory is a wrapper above FactoryBoy to allow the generation of Factory class with mandatory parameter filed.

This way one can concentrate its test on only meaningful data without having to care about properties not impacting the tests. The library also try to guess appropriate Faker class to use based on parameter name

This library was inspired by tiangolo's fastapi usage of typing.

Installation

pip install EasyFactory

Usage

two usage scenarii are considered :

FactoryBoy style

using FactoryBoy style one just have to herit from EasyFactory.

from easyfactory import AutoFactory
from pydantic import BaseModel
from uuid import UUID
from datetime import datetime


class Model(BaseModel):
    id: UUID | None = None
    name: str
    level: int
    status: str | None
    created_at: datetime = datetime(2020, 12, 16)


class ModelFactory(AutoFactory):
    class Meta:
        model = Model


model = ModelFactory()
# Model(id=None, name='Randy Johnson', level=9837, status=None, created_at=datetime.datetime(2020, 12, 16, 0, 0))

Overwriting generated attributes

It is possible FactoryBoy attribute for property to override default value generated by EasyFactory. Also, as under the hood, a Factory model is generated one can specify value at Factory instantiation.

from datetime import datetime
from uuid import UUID

import factory
from pydantic import BaseModel

from easyfactory import AutoFactory


class Model(BaseModel):
    id: UUID | None = None
    name: str
    level: int
    status: str | None
    created_at: datetime = datetime(2020, 12, 16)


class ModelFactory(AutoFactory):
    class Meta:
        model = Model

    status = "DONE"
    created_at = factory.Faker("past_datetime")


model = ModelFactory(level=5)
# Model(id=None, name='Mark Mckenzie', level=5, status='DONE', created_at=datetime.datetime(2023, 12, 5, 11, 9, 13))

Type preserving style

while FactoryBoy style is great for large codebase it cannot be used to tell that a Factory class return a model instance and not an instance of itself, EasyFactory provide a factory generation function that provide this utility:

from easyfactory import make_factory_for
from pydantic import BaseModel
from uuid import UUID
from datetime import datetime


class Model(BaseModel):
    id: UUID | None = None
    name: str
    level: int
    status: str | None
    created_at: datetime = datetime(2020, 12, 16)


ModelFactory = make_factory_for(Model)  # the type of ModelFactory is `type[Model]`

it is also possible to provide override for the Factory and the Instance.

from datetime import datetime
from uuid import UUID

import factory
from pydantic import BaseModel

from easyfactory import make_factory_for


class Model(BaseModel):
    id: UUID | None = None
    name: str
    level: int
    status: str | None
    created_at: datetime = datetime(2020, 12, 16)


ModelFactory = make_factory_for(Model, status="DONE", created_at=factory.Faker("past_datetime"))  # Factory override
model = ModelFactory(level=5)  # instance override

Library support

Currently only pydantic's BaseModel, SqlAlchemy ORM (DeclarativeBase child), and Python dataclasses are supported.

Dataclass

Dataclass support works similarly to Pydantic support. It handles required fields, optional fields, default values, and nested dataclasses.

from dataclasses import dataclass
from easyfactory import make_factory_for


@dataclass
class Address:
    city: str
    country: str


@dataclass
class Person:
    name: str
    age: int
    address: Address


PersonFactory = make_factory_for(Person)  # the type of PersonFactory is `type[Person]`
person = PersonFactory()  # person is typed as `Person` for IDE autocompletion
assert isinstance(person.address, Address)

Dataclasses with default values and optional fields are also supported:

from dataclasses import dataclass, field
from typing import Optional
from easyfactory import make_factory_for


@dataclass
class Config:
    name: str
    timeout: int = 30
    retries: int = field(default=3)
    description: Optional[str] = None


ConfigFactory = make_factory_for(Config)
config = ConfigFactory()
# Config(name='xyz', timeout=30, retries=3, description=None)

Pydantic

In case of pydantic, it is assumed that NO CYCLING relation exist within models. if one exist the library will crash (possibly with cryptic message)

SqlAlchemy

EasyFactory handle SqlAlchemy model in the following way:

for properties, just generate a generator like it is done with pydantic.

Relationship

in the case of relation between models, EasyFactory will always generate a Sub Model for a relation even if it is optional as the library doesn't know how to determine if a relation is optional

One To One

EasyFactory will generate a factory for the child on the parent class and after instantiating the parent, set the child to parent property to the parent using the back_populates/backref of the relationship.

if this is not clear consier the following python code:

from typing import Annotated
from uuid import UUID, uuid4

from easyfactory import make_factory_for
from sqlalchemy import ForeignKey, UniqueConstraint
from sqlalchemy.orm import mapped_column, DeclarativeBase, Mapped, relationship

UUID_PK = Annotated[UUID, mapped_column(primary_key=True)]
PARENT_FK = Annotated[UUID, mapped_column(ForeignKey("parent.id"))]


class Base(DeclarativeBase):
    id: Mapped[UUID_PK] = mapped_column(default=uuid4)


class Child(Base):
    __tablename__ = "child"
    __table_args__ = (UniqueConstraint("parent_id"),)

    parent_id: Mapped[PARENT_FK]
    parent: Mapped["Parent"] = relationship(back_populates="child", single_parent=True)


class Parent(Base):
    __tablename__ = "parent"

    child: Mapped["Child"] = relationship(back_populates="parent", cascade="all, delete-orphan")


ParentFactory = make_factory_for(Parent)  # the type of ParentFactory is `type[Parent]`
parent = ParentFactory()  # parent is typed as `Parent` for the IDE so the following line have type helps from IDE.
assert parent.child.parent == parent
One To Many

EasyFactory will generate a factory for the childs on the parent class, generate one child and after instantiating the parent, set the child to parent property to the parent using the back_populates/backref of the relationship.

if this is not clear consider the following python code:

from typing import Annotated
from uuid import UUID, uuid4

from easyfactory import make_factory_for
from sqlalchemy import ForeignKey, UniqueConstraint
from sqlalchemy.orm import mapped_column, DeclarativeBase, Mapped, relationship

UUID_PK = Annotated[UUID, mapped_column(primary_key=True)]
PARENT_FK = Annotated[UUID, mapped_column(ForeignKey("parent.id"))]


class Base(DeclarativeBase):
    id: Mapped[UUID_PK] = mapped_column(default=uuid4)


class Child(Base):
    __tablename__ = "child"
    __table_args__ = (UniqueConstraint("parent_id"),)

    parent_id: Mapped[PARENT_FK]
    parent: Mapped["Parent"] = relationship(back_populates="childs")


class Parent(Base):
    __tablename__ = "parent"

    childs: Mapped[list["Child"]] = relationship(back_populates="parent", cascade="all, delete-orphan")


ParentFactory = make_factory_for(Parent)  # the type of ParentFactory is `type[Parent]`
parent = ParentFactory()  # parent is typed as `Parent` for the IDE so the following line have type helps from IDE.
assert len(parent.childs) == 1
assert parent.childs[0].parent == parent
Many To Many

In case of Many-to-Many relationship, EasyFactory don't generate ChildFactory, set the value to None and let the developper set what it wants.

Support

Any help is welcome. you can either:

  • create an issue
  • look for TODO in the code and provide a MR with changes
  • provide a MR for support of new class

Authors and acknowledgment

Currently, solely developed by Tagashy but any help is welcomed and will be credited here.

License

See the LICENSE file for licensing information as it pertains to files in this repository.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

easyfactory-1.5.0.tar.gz (9.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

easyfactory-1.5.0-py3-none-any.whl (11.9 kB view details)

Uploaded Python 3

File details

Details for the file easyfactory-1.5.0.tar.gz.

File metadata

  • Download URL: easyfactory-1.5.0.tar.gz
  • Upload date:
  • Size: 9.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for easyfactory-1.5.0.tar.gz
Algorithm Hash digest
SHA256 344d4ef468b1aab95af7b42c2130c9a26fd00a9334f9b8859067800dac64b734
MD5 ada06bf8743a93bb60c2b6c33dffd1a2
BLAKE2b-256 3a39fd149b95d37c1c071fdc54872419fadf6293d5bb1e85da1c66a60dd009b0

See more details on using hashes here.

File details

Details for the file easyfactory-1.5.0-py3-none-any.whl.

File metadata

  • Download URL: easyfactory-1.5.0-py3-none-any.whl
  • Upload date:
  • Size: 11.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for easyfactory-1.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bde8367e18816c33e4cca6e5cf4445df81031de58e9056521168e244acbfc259
MD5 fbed6fb6f2079c4b33ba461b9cf46b54
BLAKE2b-256 198109bc7c8e38a4faa3d17ccd5ec22232a27f5cf1f5d9599e250445e432e2b1

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page