How to add type hint to python factory_boy Factory classes?

Question:

So we are working with some existing code where there is an implementation of factories classes from the factory_boy project which create instances of other classes for example

class TableFactory(Factory):
    class Meta:
        model = Table

    id = LazyAttribute(lambda x: uuid4())
    color = SubFactory(ColorFactory)
    reference = 'WT9034'
    weight = 120
    height = 50
    length = 3
    width = 1 

where the Factory base meta-class has a logic in its __new__ method to dynamically instantiate a class using the model provided in the Meta class of the TableFactory. The Table model is a simple SQLAlchemy class/model.

Since the produced classes are made dynamically I guess we cannot have static type hinting for the produced class types? Or can we somehow add this?

Asked By: KZiovas

||

Answers:

I am assuming OP’s concrete problem is that he is getting errors from the type checker when trying to use instances of the factory in his test code.

Example:

obj = TableFactory()
print(obj.reference)

This will create a type error like this with pylance:

Can not access member "reference" for type TableFactory.

Solution 1

A quick solution is to type annotate the assignment with the correct type. However, this does not work with the class alone, you need to also use one of the instantiation methods like create() or build() to create the object:

obj: Table = TableFactory.create()
print(obj.reference)

This works fine, but requires you to annotate all factory assignments in that particular way, which can get repetitive and make the code harder to read.

Solution 2

A better solution is add the type annotation to the factory class itself. This can be done by creating a mixin which has the generic type annotation and use it to compose a new factory class.

Here is how this would work:

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 TableFactory(factory.Factory, metaclass=BaseMetaFactory[Table]):
    class Meta:
        model = Table
    
    ...

As added bonus this mixin can be used create annotated version of all our factory classes.

For a more in depth discussion on different workarounds for this problem please also see the related issue on the factory boy repo.

Answered By: Erik Kalkoken