Skip to content

Instantly share code, notes, and snippets.

@mgaitan
Last active December 9, 2023 16:16
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mgaitan/dcbe08bf44a5af696f2af752624ac11b to your computer and use it in GitHub Desktop.
Save mgaitan/dcbe08bf44a5af696f2af752624ac11b to your computer and use it in GitHub Desktop.
Automatically define factory boy recipes for dataclasses by inspecting the type annotations
## 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='singhmichael@zuniga.biz', 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='singhmichael@zuniga.biz', 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']
"""
@SHxKM
Copy link

SHxKM commented Jun 25, 2022

This is perfect. Thanks for sharing. Any idea how to adjust this for Python 3.6? It seems get_args() and get_origins() aren’t available there..

@Lacrymology
Copy link

@SHxKM looks like https://pypi.org/project/typing-extensions/ backports those functions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment