Last active
January 8, 2025 08:00
-
-
Save mgaitan/dcbe08bf44a5af696f2af752624ac11b to your computer and use it in GitHub Desktop.
Automatically define factory boy recipes for dataclasses by inspecting the type annotations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## See https://github.com/FactoryBoy/factory_boy/issues/836 | |
import inspect | |
from typing import List, get_args, get_origin | |
import factory | |
import factory.fuzzy | |
from dataclasses import dataclass, Field, MISSING, is_dataclass | |
from enum import Enum | |
from datetime import date, datetime | |
from decimal import Decimal | |
def get_auto_field(field: Field): | |
if field.default is not MISSING: | |
return field.default | |
elif field.type is str and 'email' in field.name: | |
return factory.Faker("ascii_email") | |
elif field.type is datetime: | |
return factory.Faker("date_time_between") | |
elif field.type is date: | |
return factory.Faker("date_object") | |
elif is_dataclass(field.type): | |
# fixme reflect the factory un a better way than eval | |
return factory.SubFactory(eval(f"{field.type.__name__}AutoFactory")) | |
elif inspect.isclass(field.type) and issubclass(field.type, Enum): | |
return factory.fuzzy.FuzzyChoice(field.type.__members__.values()) | |
elif get_origin(field.type) in [list, tuple, set]: | |
args = get_args(field.type) | |
return factory.Faker(f"py{get_origin(field.type).__name__}", value_types=args) | |
# str, int, float, decimal | |
return factory.Faker(f"py{field.type.__name__.lower()}") | |
def auto_factory(target_model, field_overrides=None): | |
factory_name = f'{target_model.__name__}AutoFactory' | |
class Meta: | |
model = target_model | |
attrs = {name: get_auto_field(field) for name, field in target_model.__dataclass_fields__.items()} | |
attrs.update(field_overrides or {}) | |
attrs['Meta'] = Meta | |
factory_class = type(factory_name, (factory.Factory,), attrs) | |
return factory_class | |
@dataclass | |
class A: | |
a_text: str | |
integer: int | |
email: str | |
value: float | |
a_date: date | |
d: Decimal = Decimal("42") | |
class MyEnum(Enum): | |
option1 = 1 | |
option2 = 2 | |
@dataclass | |
class B: | |
related: A | |
an_enum: MyEnum | |
list_of_str: List[str] | |
AAutoFactory = auto_factory(A) | |
BAutoFactory = auto_factory(B) | |
""" | |
In [84]: b = BAutoFactory() | |
In [85]: b | |
Out[85]: B(related=A(a_text='sAywZloMAosFzdiYWTfd', integer=1118, email='[email protected]', value=-1487703326176.12, a_date=datetime.date(1979, 3, 8), d=Decimal('42')), an_enum=<MyEnum.option2: 2>, list_of_str=['JpDECtFLEmVTSzdHsIdV', 'HPAoyVmnStBoAtaSFvuE', 'AsTaUioIxLMxSqFaWPRt', 'uiRCFswCMwtbLTfjiaBK', 'sySmxSnkLhlxPJdyGWAa', 'ZwYWLZYswgXpSjNxLUOs', 'UjStmZkSMnVdgYBApUrx', 'ONryhrSHgrIVECmAOzuQ', 'LGmBZVOyXrKWnJbXkMNB', 'UGADWrTORVOKIqJGwPOG']) | |
In [86]: b.related | |
Out[86]: A(a_text='sAywZloMAosFzdiYWTfd', integer=1118, email='[email protected]', value=-1487703326176.12, a_date=datetime.date(1979, 3, 8), d=Decimal('42')) | |
In [87]: b.an_enum | |
Out[87]: <MyEnum.option2: 2> | |
In [88]: b2 = BAutoFactory() | |
In [89]: b2.an_enum | |
Out[89]: <MyEnum.option2: 2> | |
In [90]: b2 = BAutoFactory() | |
In [91]: b2.an_enum | |
Out[91]: <MyEnum.option1: 1> | |
In [92]: b.list_of_str | |
Out[92]: | |
['JpDECtFLEmVTSzdHsIdV', | |
'HPAoyVmnStBoAtaSFvuE', | |
'AsTaUioIxLMxSqFaWPRt', | |
'uiRCFswCMwtbLTfjiaBK', | |
'sySmxSnkLhlxPJdyGWAa', | |
'ZwYWLZYswgXpSjNxLUOs', | |
'UjStmZkSMnVdgYBApUrx', | |
'ONryhrSHgrIVECmAOzuQ', | |
'LGmBZVOyXrKWnJbXkMNB', | |
'UGADWrTORVOKIqJGwPOG'] | |
""" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@SHxKM looks like https://pypi.org/project/typing-extensions/ backports those functions