factory_boy icon indicating copy to clipboard operation
factory_boy copied to clipboard

Support type hinting?

Open pgcd opened this issue 6 years ago • 27 comments

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)

pgcd avatar Apr 17 '18 10:04 pgcd

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.

rbarrois avatar Apr 17 '18 11:04 rbarrois

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

ktosiek avatar May 02 '19 08:05 ktosiek

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 )

therefromhere avatar Jun 25 '19 22:06 therefromhere

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?

ProProgrammer avatar May 04 '20 13:05 ProProgrammer

Typing support would be really nice!

michaeloliverx avatar Aug 13 '20 16:08 michaeloliverx

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 avatar Jan 13 '21 13:01 kamilglod

@kamilglod I think it's a great workaround! Thank you!

nyk510 avatar Jun 29 '21 11:06 nyk510

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 avatar Sep 20 '21 14:09 Ragura

@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"

kamilglod avatar Sep 20 '21 16:09 kamilglod

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()

image

yunuscanemre avatar Jun 09 '22 21:06 yunuscanemre

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.

tgross35 avatar Jun 12 '22 02:06 tgross35

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.

n1ngu avatar Sep 25 '22 19:09 n1ngu

Maintainers are not interested in adding type hints or what?

sshishov avatar Feb 01 '23 13:02 sshishov

Hi :wave:

I'm interested in helping here. I did this on Uvicorn: https://github.com/encode/uvicorn/issues/998.

Can I help here?

Kludex avatar Mar 03 '23 17:03 Kludex

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 😉)

rbarrois avatar Mar 14 '23 09:03 rbarrois

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

n1ngu avatar Mar 14 '23 10:03 n1ngu

Some time ago I started experimenting with this n1ngu/factory_boy@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

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.

Kludex avatar Mar 14 '23 11:03 Kludex

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 avatar May 05 '23 14:05 anton-fomin

@anton-fomin that's fantastic. thank you!

blalor avatar Jul 24 '23 14:07 blalor

@anton-fomin , is it possible to incorporate it into the main repo? Or do we see any possible issues?

sshishov avatar Aug 06 '23 12:08 sshishov

@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

anton-fomin avatar Aug 07 '23 09:08 anton-fomin

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()

image

Sadly mypy does not supporte generic metaclass yet: https://github.com/python/mypy/issues/11672

erdnaxeli avatar Aug 21 '23 12:08 erdnaxeli

I think this could be closed as Factory#create solves this issue.

brendanmaguire avatar Feb 16 '24 19:02 brendanmaguire

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()?

caarmen avatar Mar 03 '24 22:03 caarmen

Is there any disadvantage to using user_factory.create() instead of user_factory()?

Not "disadvantage" per se but, in some cases like mine, it means updating 10k+ tests, which is not very enjoyable.

pgcd avatar Mar 04 '24 05:03 pgcd

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 ? 🙏

pattersam avatar Mar 22 '24 13:03 pattersam

Am I missing something or does this still need a PEP 561 py.typed marker file so clients can consume the types?

cjolowicz avatar Mar 23 '24 09:03 cjolowicz