A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby
—
Comprehensive declaration types for generating dynamic attribute values in factories. These declarations enable sophisticated test data generation with sequences, lazy evaluation, faker integration, conditional logic, and complex object relationships.
Core declarations for computing attribute values using functions and external data.
class LazyFunction:
"""
Computed by calling function without arguments each time.
Args:
function: Callable that returns the attribute value
"""
def __init__(self, function): ...
class LazyAttribute:
"""
Computed using function that receives the current instance being built.
Args:
function: Callable that takes the instance and returns attribute value
"""
def __init__(self, function): ...
class SelfAttribute:
"""
Copy values from other fields, supports dot notation and parent access.
Args:
attribute_name (str): Name of attribute to copy (supports 'field', 'sub.field', '..parent.field')
default: Default value if attribute not found
"""
def __init__(self, attribute_name, default=UNSPECIFIED): ...class UserFactory(Factory):
class Meta:
model = User
# LazyFunction - called fresh each time
created_at = LazyFunction(lambda: timezone.now())
uuid = LazyFunction(lambda: str(uuid.uuid4()))
# LazyAttribute - receives the current instance
email = LazyAttribute(lambda obj: f'{obj.username}@example.com')
display_name = LazyAttribute(lambda obj: f'{obj.first_name} {obj.last_name}')
# SelfAttribute - copy from other fields
username = 'john_doe'
login_name = SelfAttribute('username')
backup_email = SelfAttribute('email', default='fallback@example.com')Declarations for generating unique, sequential, or cyclic values.
class Sequence:
"""
Generate increasing unique values using sequence counter.
Args:
function: Callable that takes sequence number (int) and returns value
"""
def __init__(self, function): ...
class LazyAttributeSequence:
"""
Combination of LazyAttribute and Sequence - receives instance and sequence number.
Args:
function: Callable that takes (instance, sequence_number) and returns value
"""
def __init__(self, function): ...
class Iterator:
"""
Fill value using iterator values, with optional cycling and getter function.
Args:
iterator: Iterable to get values from
cycle (bool): Whether to cycle through values when exhausted
getter: Optional function to extract values from iterator items
"""
def __init__(self, iterator, cycle=True, getter=None): ...
def reset(self):
"""Reset internal iterator to beginning."""class UserFactory(Factory):
class Meta:
model = User
# Simple sequence
email = Sequence(lambda n: f'user{n}@example.com')
# LazyAttributeSequence - instance + sequence
username = LazyAttributeSequence(lambda obj, n: f'{obj.first_name.lower()}_{n}')
# Iterator with cycling
department = Iterator(['Engineering', 'Sales', 'Marketing'])
# Iterator with getter function
priority = Iterator([
{'name': 'High', 'value': 1},
{'name': 'Medium', 'value': 2},
{'name': 'Low', 'value': 3}
], getter=lambda item: item['value'])
# Non-cycling iterator
one_time_code = Iterator(['ABC123', 'DEF456', 'GHI789'], cycle=False)Integration with the Faker library for generating realistic fake data.
class Faker:
"""
Wrapper for faker library values with locale support and custom providers.
Args:
provider (str): Faker provider name (e.g., 'name', 'email', 'address')
locale (str, optional): Locale for this faker instance
**kwargs: Additional arguments passed to the faker provider
"""
def __init__(self, provider, locale=None, **kwargs): ...
def generate(self, extra_kwargs=None):
"""Generate fake value with optional extra parameters."""
@classmethod
def override_default_locale(cls, locale):
"""Context manager for temporarily overriding default locale."""
@classmethod
def add_provider(cls, provider, locale=None):
"""Add custom faker provider."""class UserFactory(Factory):
class Meta:
model = User
# Basic faker usage
first_name = Faker('first_name')
last_name = Faker('last_name')
email = Faker('email')
# Faker with parameters
birthdate = Faker('date_of_birth', minimum_age=18, maximum_age=65)
bio = Faker('text', max_nb_chars=200)
# Locale-specific faker
phone = Faker('phone_number', locale='en_US')
# Using context manager for locale
with Faker.override_default_locale('fr_FR'):
address = Faker('address')
# Custom provider example
from faker.providers import BaseProvider
class CustomProvider(BaseProvider):
def department_code(self):
return self.random_element(['ENG', 'SAL', 'MKT', 'HR'])
Faker.add_provider(CustomProvider)
class EmployeeFactory(Factory):
dept_code = Faker('department_code')Declarations for working with containers and accessing factory context.
class ContainerAttribute:
"""
Receives current instance and container chain for complex attribute computation.
Args:
function: Callable that takes (instance, containers) and returns value
strict (bool): Whether to enforce strict container access
"""
def __init__(self, function, strict=True): ...class ProfileFactory(Factory):
class Meta:
model = Profile
# Access container chain (useful in SubFactory contexts)
user_id = ContainerAttribute(lambda obj, containers: containers[0].id if containers else None)
# Complex container logic
role = ContainerAttribute(
lambda obj, containers: 'admin' if containers and containers[0].is_staff else 'user'
)Declarations for creating related objects and complex data structures.
class SubFactory:
"""
Create related objects using another factory.
Args:
factory: Factory class or string path to factory
**defaults: Default values to pass to the sub-factory
"""
def __init__(self, factory, **defaults): ...
def get_factory(self):
"""Retrieve the wrapped factory class."""
class Dict:
"""
Fill dictionary with declarations.
Args:
params (dict): Dictionary of key-value pairs, values can be declarations
dict_factory (str): Factory to use for creating dictionary
"""
def __init__(self, params, dict_factory='factory.DictFactory'): ...
class List:
"""
Fill list with declarations.
Args:
params (list): List of values, can include declarations
list_factory (str): Factory to use for creating list
"""
def __init__(self, params, list_factory='factory.ListFactory'): ...class UserFactory(Factory):
class Meta:
model = User
name = Faker('name')
email = Faker('email')
class PostFactory(Factory):
class Meta:
model = Post
title = Faker('sentence')
# SubFactory creates related User
author = SubFactory(UserFactory, name='Post Author')
# Dict with mixed static and dynamic values
metadata = Dict({
'views': 0,
'created_at': LazyFunction(lambda: timezone.now().isoformat()),
'tags': List([Faker('word'), Faker('word'), 'default-tag'])
})
# List of related objects
comments = List([
SubFactory('CommentFactory', content='First comment'),
SubFactory('CommentFactory', content='Second comment')
])Declarations for conditional attribute generation based on other values.
class Maybe:
"""
Conditional declaration based on decider value.
Args:
decider (str or callable): Field name or function to evaluate condition
yes_declaration: Declaration to use when condition is truthy
no_declaration: Declaration to use when condition is falsy (defaults to SKIP)
"""
def __init__(self, decider, yes_declaration=SKIP, no_declaration=SKIP): ...
class Trait:
"""
Enable declarations based on boolean flag, used in Params section.
Args:
**overrides: Attribute overrides to apply when trait is enabled
"""
def __init__(self, **overrides): ...class UserFactory(Factory):
class Meta:
model = User
name = Faker('name')
is_premium = Iterator([True, False])
# Maybe based on field value
premium_features = Maybe(
'is_premium',
yes_declaration=Dict({'advanced_analytics': True, 'priority_support': True}),
no_declaration=Dict({'basic_features': True})
)
# Maybe with function decider
account_type = Maybe(
lambda obj: obj.is_premium,
yes_declaration='Premium',
no_declaration='Basic'
)
class PostFactory(Factory):
class Meta:
model = Post
title = Faker('sentence')
status = 'draft'
class Params:
# Trait for published posts
published = Trait(
status='published',
published_at=LazyFunction(lambda: timezone.now()),
slug=LazyAttribute(lambda obj: slugify(obj.title))
)
# Usage: PostFactory(published=True) enables the published trait
published_post = PostFactory(published=True)Declarations that execute after object creation for relationships and method calls.
class PostGeneration:
"""
Call function after object generation.
Args:
function: Callable that takes (instance, create, extracted, **kwargs)
"""
def __init__(self, function): ...
class PostGenerationMethodCall:
"""
Call method on generated object after creation.
Args:
method_name (str): Name of method to call on the instance
*args: Positional arguments to pass to method
**kwargs: Keyword arguments to pass to method
"""
def __init__(self, method_name, *args, **kwargs): ...
class RelatedFactory:
"""
Create related object after main object generation.
Args:
factory: Factory class to use for creating related object
factory_related_name (str): Attribute name on related object to set main object
**defaults: Default values for related object factory
"""
def __init__(self, factory, factory_related_name='', **defaults): ...
class RelatedFactoryList:
"""
Create multiple related objects after main object generation.
Args:
factory: Factory class to use for creating related objects
factory_related_name (str): Attribute name on related objects to set main object
size (int): Number of related objects to create
**defaults: Default values for related object factory
"""
def __init__(self, factory, factory_related_name='', size=2, **defaults): ...class UserFactory(Factory):
class Meta:
model = User
username = Faker('user_name')
email = Faker('email')
# PostGeneration for custom setup
setup_profile = PostGeneration(
lambda obj, create, extracted, **kwargs: obj.create_default_profile() if create else None
)
# PostGenerationMethodCall for method execution
set_password = PostGenerationMethodCall('set_password', 'default_password')
# RelatedFactory creates profile after user
profile = RelatedFactory('ProfileFactory', 'user')
class PostFactory(Factory):
class Meta:
model = Post
title = Faker('sentence')
author = SubFactory(UserFactory)
# RelatedFactoryList creates multiple comments
comments = RelatedFactoryList(
'CommentFactory',
'post',
size=3,
author=SubFactory(UserFactory)
)
# Usage with extracted values
user = UserFactory(set_password__password='custom_password')# Special marker for skipping fields
SKIP = object()
# Unspecified default marker
UNSPECIFIED = object()Functional-style wrappers for creating declarations:
def lazy_attribute(func):
"""Wrap function as LazyAttribute declaration."""
def sequence(func):
"""Wrap function as Sequence declaration."""
def lazy_attribute_sequence(func):
"""Wrap function as LazyAttributeSequence declaration."""
def container_attribute(func):
"""Wrap function as ContainerAttribute declaration (non-strict)."""
def post_generation(func):
"""Wrap function as PostGeneration declaration."""# Using wrapper functions
@lazy_attribute
def full_name(obj):
return f'{obj.first_name} {obj.last_name}'
@sequence
def email(n):
return f'user{n}@example.com'
@post_generation
def send_welcome_email(obj, create, extracted, **kwargs):
if create:
obj.send_welcome_email()
class UserFactory(Factory):
class Meta:
model = User
first_name = Faker('first_name')
last_name = Faker('last_name')
full_name = full_name
email = email
send_welcome_email = send_welcome_emailInstall with Tessl CLI
npx tessl i tessl/pypi-factory-boy