Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support type hinting? #468

Open
pgcd opened this issue Apr 17, 2018 · 27 comments
Open

Support type hinting? #468

pgcd opened this issue Apr 17, 2018 · 27 comments
Labels

Comments

@pgcd
Copy link

pgcd commented Apr 17, 2018

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)

@rbarrois
Copy link
Member

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.

@ktosiek
Copy link

ktosiek commented May 2, 2019

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: python/mypy#1020

@therefromhere
Copy link

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 )

@ProProgrammer
Copy link

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?

@michaeloliverx
Copy link

Typing support would be really nice!

@kamilglod
Copy link

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.

@nyk510
Copy link

nyk510 commented Jun 29, 2021

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

@Ragura
Copy link

Ragura commented Sep 20, 2021

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

@kamilglod
Copy link

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

@yunuscanemre
Copy link

yunuscanemre commented Jun 9, 2022

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

@tgross35
Copy link

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.

@n1ngu
Copy link

n1ngu commented Sep 25, 2022

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.

@sshishov
Copy link

sshishov commented Feb 1, 2023

Maintainers are not interested in adding type hints or what?

@Kludex
Copy link

Kludex commented Mar 3, 2023

Hi 👋

I'm interested in helping here. I did this on Uvicorn: encode/uvicorn#998.

Can I help here?

@rbarrois
Copy link
Member

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

@n1ngu
Copy link

n1ngu commented Mar 14, 2023

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

@Kludex
Copy link

Kludex commented Mar 14, 2023

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.

@anton-fomin
Copy link

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

@blalor
Copy link

blalor commented Jul 24, 2023

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

@sshishov
Copy link

sshishov commented Aug 6, 2023

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

@anton-fomin
Copy link

@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

@erdnaxeli
Copy link

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: python/mypy#11672

@brendanmaguire
Copy link

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

@caarmen
Copy link

caarmen commented Mar 3, 2024

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 added a commit to caarmen/slack-health-bot that referenced this issue Mar 3, 2024
See FactoryBoy/factory_boy#468

PIDD: Pycharm inspection driven development 🤷
caarmen added a commit to caarmen/slack-health-bot that referenced this issue Mar 3, 2024
See FactoryBoy/factory_boy#468

PIDD: Pycharm inspection driven development 🤷
@pgcd
Copy link
Author

pgcd commented Mar 4, 2024

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.

@pattersam
Copy link

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

@cjolowicz
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests