factory_boy
factory_boy copied to clipboard
Support type hinting?
I can't seem to have PyCharm play nice with the objects generated by Factory Boy; it would be nice to either automatically support type hinting somehow (ideally, it would simply be the model defined as a base for the factory) or at least add some documentation to explain how to have type checkers understand that,
after p = PersonFactory()
, variuable p
is actually an instance of Person
rather than of PersonFactory
.
(Note: I'm not submitting a PR because I haven't figured out a solution)
Yep, that would be a great idea! But I'm not sure how this could be easily done.
A simple workaround could be to use p = PersonFactory.create()
— it does the same as PersonFactory()
, but shouldn't trigger the same paths in PyCharm.
for me PyCharm can't even find the PersonFactory.create()
(I guess because of the call to FactoryMetaClass?), so some hints would probably help.
As for the PersonFactory()
syntax, there might be no way to make it work at the moment: https://github.com/python/mypy/issues/1020
I haven't tried this with Mypy, but this will get the case of p = PersonFactory()
to type hint correctly in Pycharm:
class PersonFactory(factory.Factory):
class Meta:
model = Person
# Type hinting PersonFactory()
def __new__(cls, *args, **kwargs) -> "PersonFactory.Meta.model":
return super().__new__(*args, **kwargs)
Currently I'm doing this for each factory as a quick and dirty fix. Hopefully it's possible to move this up to the parent Factory class, maybe using Generics? (https://mypy.readthedocs.io/en/stable/generics.html )
Has anyone tried type hinting with a DjangoModelFactory
that derives from another factory whose Meta.abstract
value is set to True
E.g.
@factory.django.mute_signals(signals.pre_save, signals.post_save)
class AnimalFactory(factory.django.DjangoModelFactory):
class Meta:
model = Animal
abstract = True
# Some fields that might include foreign key in which case we use factory.SubFactory
creature_type = 'animal'
## As mentioned by @therefromhere
def __new__(cls, *args, **kwargs) -> "AnimalFactory.Meta.model":
return super().__new__(*args, **kwargs)
@factory.django.mute_signals(signals.pre_save, signals.post_save)
class DogFactory(AnimalFactory):
class Meta:
model = Dog
# Some fields as defined in Dog Class that inherits from Animal class.
# Since in reality, Dog class inherits from Animal class, the Dog class would have Animal classes attributes (methods, properties) too
creature_species = 'dog'
# As mentioned by @therefromhere
def __new__(cls, *args, **kwargs) -> "DogFactory.Meta.model":
return super().__new__(*args, **kwargs)
Now the problem that I face here is, in the __new__
for DogFactory
, PyCharm shows InspectionWarning for return super().__new__(*args, **kwargs)
because I have mentioned the return type as DogFactory.Meta.model
whereas super()
here references the AnimalFactory
from which the DogFactory
derives itself.
So how should we handle this?
Typing support would be really nice!
my workaround for missing General protocol is as follow:
T = TypeVar("T")
class BaseFactory(Generic[T], factory.Factory):
@classmethod
def create(cls, **kwargs) -> T:
return super().create(**kwargs)
class RoleFactory(BaseFactory[Role]):
class Meta:
model = Role
id = factory.Sequence(lambda n: n)
name = factory.Sequence(lambda n: f"Role {n}")
one drawback of this solution is that you always need to use Factory.create()
instead of Factory()
as it's not possible (at least I didn't find a proper solution) to override __new__()
method with TypeVar
.
@kamilglod I think it's a great workaround! Thank you!
Thanks @kamilglod for the excellent workaround. There's still the issue of the @factory.post_generation
decorated functions thinking the first parameter of the function is the factory instance and not an instance of the recently created object. Do you happen to have a solution for this as well (aside from manually annotating the first argument)?
@Ragura good question. All ideas that comes to my mind are equally complicated as manually typing first argument each time new method is added. It might be possible on python 3.10 which comes with better params typing support (https://docs.python.org/3.10/library/typing.html#typing.ParamSpec) but I do not have enough spare time to dig into it. I was trying to override post_generation class like this:
class post_generation(Generic[T]):
def __init__(self, func: Callable[[BaseFactory[T], Callable, bool], str]):
self.func = func
def __call__(self) -> Callable[[T, Callable, bool], str]:
return factory.post_generation(self.func)
but mypy still does not see obj
as with overwritten type:
class RoleFactory(BaseFactory[Role]):
class Meta:
model = Role
id = factory.Sequence(lambda n: n)
name = factory.Sequence(lambda n: f"Role {n}")
@post_generation
def mbox(obj, create, extracted, **kwargs):
reveal_type(obj) # Type of "obj" is "RoleFactory"
return "foo"
I'm using this trick currently which seems to play nice with the (Pylance) type checker in vscode.
from typing import Generic, TypeVar
import factory
T = TypeVar('T')
class BaseMetaFactory(Generic[T], factory.base.FactoryMetaClass):
def __call__(cls, *args, **kwargs) -> T:
return super().__call__(*args, **kwargs)
class MyModel:
pass
class MyModelFactory(factory.Factory, metaclass=BaseMetaFactory[MyModel]):
class Meta:
model = MyModel
test_model = MyModelFactory()
As far as what changes could actually be implemented in this library - FactoryBoy just needs to aim for mypy compliance. This really wouldn't be terrible, adding type hints is always relatively quick. I'd assume the maintainers would be open to a related PR.
The trickiest problem is what's discussed here, binding a "generic" type to the type of a meta class member. To my knowledge this unfortunately isn't directly possible. That means using the generics (per @kamilglod) is the most accurate way to accomplish this, to my knowledge. The downside is, of course, that the user must then specify the model in two places (in the base class generic binding, and in the metaclass) but at least having this option would be huge, and would mean that FactoryBoy could reach mypy compliance using generics until (if) a better binding method is discovered.
I am currently on a hunt to figure out if there's a better way to bind typevars to class members or metaclass members (for something unrelated), currently with 0 luck. My stackoverflow question is as-of now unsuccessful, I'm still hoping for a response on the python discord as a fallback.
I am currently on a hunt to figure out if there's a better way to bind typevars to class members or metaclass members
@tgross35 My actual factory declarations are most usually defined with string values in their meta model, either future references or keys for a class registry to avoid import-time troubles while an application / DB connection are not ready. The get_model_class
method from the factory options will lazily return the actual model at runtime, but statically it might be not accessible. The generics way feels like the correct solution to me. Pursuing that typevar-member binding is an interesting study field, but unsuitable for factory_boy IMHO.
I'm using this trick currently which seems to play nice with the (Pylance) type checker in vscode.
@yunuscanemre Just watch out for the tricky StubObject
that can potentially be returned from any factory declared with strategy = "stub"
. Not a common use case as far as I know, but I'd rather stick to the explicit .create
, .build
and .stub
methods and their _batch
counterparts than use the meta __call__
. Less meta magic and quite more semantic code along a simpler execution path.
Maintainers are not interested in adding type hints or what?
Hi :wave:
I'm interested in helping here. I did this on Uvicorn: https://github.com/encode/uvicorn/issues/998.
Can I help here?
Maintainers are not interested in adding type hints or what?
I'm interested in this, but lacking time to work on the project at the moment — and an accumulated backlog of code to review before a next release :(
Once I reduce said backlog and return to a smaller list of unreviewed changes, I would love to dig into this topic, along with others.
(As a side note, passive-agressive comments are unlikely to entice unpaid maintainers motivated into spending time on said projects 😉)
Some time ago I started experimenting with this https://github.com/n1ngu/factory_boy/tree/feature/typing
The branch evolved chaotically so I got stuck and abandoned the idea.
I am a bit clueless on how to address this incrementally so that it could be merged upstream with a sane review. But anyone feel free to pick any commit or idea from my experiment.
Also, if someone starts working on this, don't hesitate to post you work in progress here so I could watch and maybe learn something.
cc/ @Kludex
Some time ago I started experimenting with this n1ngu/factory_boy@
feature
/typingThe branch evolved chaotically so I got stuck and abandoned the idea.
I am a bit clueless on how to address this incrementally so that it could be merged upstream with a sane review. But anyone feel free to pick any commit or idea from my experiment.
Also, if someone starts working on this, don't hesitate to post you work in progress here so I could watch and maybe learn something.
cc/ @Kludex
I'll only help if a maintainer gives me a thumbs up to work on it, otherwise it will be a waste of time for the both of us.
It is also possible to override metaclass, so you don't need to specify model in meta
from typing import Generic, Type, TypeVar, get_args
import factory
from factory.base import FactoryMetaClass
T = TypeVar("T")
class BaseFactoryMeta(FactoryMetaClass):
def __new__(mcs, class_name, bases: list[Type], attrs):
orig_bases = attrs.get("__orig_bases__", [])
for t in orig_bases:
if t.__name__ == "BaseFactory" and t.__module__ == __name__:
type_args = get_args(t)
if len(type_args) == 1:
if "Meta" not in attrs:
attrs["Meta"] = type("Meta", (), {})
setattr(attrs["Meta"], "model", type_args[0])
return super().__new__(mcs, class_name, bases, attrs)
class BaseFactory(Generic[T], factory.Factory, metaclass=BaseFactoryMeta):
class Meta:
abstract = True
@classmethod
def create(cls, **kwargs) -> T:
return super().create(**kwargs)
@classmethod
def build(cls, **kwargs) -> T:
return super().build(**kwargs)
class UserFactory(BaseFactory[User]):
# no need to define Meta
email = factory.Faker("email")
first_name = factory.Faker("first_name")
last_name = factory.Faker("last_name")
verified = True
@anton-fomin that's fantastic. thank you!
@anton-fomin , is it possible to incorporate it into the main repo? Or do we see any possible issues?
@sshishov , I don't know how it will affect other parts, but it definitely will involve updates to the documentation and tests. I am not sure I will find time to do that
I'm using this trick currently which seems to play nice with the (Pylance) type checker in vscode.
from typing import Generic, TypeVar import factory T = TypeVar('T') class BaseMetaFactory(Generic[T], factory.base.FactoryMetaClass): def __call__(cls, *args, **kwargs) -> T: return super().__call__(*args, **kwargs) class MyModel: pass class MyModelFactory(factory.Factory, metaclass=BaseMetaFactory[MyModel]): class Meta: model = MyModel test_model = MyModelFactory()
Sadly mypy does not supporte generic metaclass yet: https://github.com/python/mypy/issues/11672
I think this could be closed as Factory#create
solves this issue.
I think this could be closed as
Factory#create
solves this issue.
It's certainly the simplest workaround! 🙏🏻
Is there any disadvantage to using user_factory.create()
instead of user_factory()
?
Is there any disadvantage to using
user_factory.create()
instead ofuser_factory()
?
Not "disadvantage" per se but, in some cases like mine, it means updating 10k+ tests, which is not very enjoyable.
I think this could be closed as
Factory#create
solves this issue.
thanks for sharing this :)
so far i've just used a work around similar to @erdnaxeli 's suggestion, so it'd be great to use the newest .create
behaviour with a new version of factory_boy 🤞 - any word on when the next release with these changes to type hinting might be @rbarrois ? 🙏
Am I missing something or does this still need a PEP 561 py.typed
marker file so clients can consume the types?