Package: pytest-fixture-classes
Pytest Fixture ClassesTypedfactory fixturesthat work well with dependency injection, autocompletetion, type checkers, and language servers.No mypy plugins required!Installationpip install pytest-fixture-classesUsageQuickstartHere's a factory fixture frompytest's documentation:fromtypingimportAnyimportpytest@pytest.fixture()deforders()->list[str]:return["order1","order2"]@pytest.fixturedefmake_customer_record(orders:list[str])->Callable[str,dict[str,Any]]:def_make_customer_record(name):return{"name":name,"orders":orders}return_make_customer_recorddeftest_customer_records(make_customer_record:Callable[str,dict[str,Any]]):customer_1=make_customer_record("Lisa")customer_2=make_customer_record("Mike")customer_3=make_customer_record("Meredith")And here's the same factory implemented using fixture classes:frompytest_fixture_classesimportfixture_classfromtypingimportAnyimportpytest@pytest.fixture()deforders():return["order1","order2"]@fixture_class(name="make_customer_record")classMakeCustomerRecord:orders:list[str]def__call__(self,name:str)->dict[str,Any]:returnreturn{"name":name,"orders":orders}deftest_customer_records(make_customer_record:MakeCustomerRecord):customer_1=make_customer_record("Lisa")customer_2=make_customer_record("Mike")customer_3=make_customer_record("Meredith")You can, of course, add any methods you like into the class but I prefer to keep it a simple callable.RationaleIf we want factory fixtures that automatically make use of pytest's dependency injection, we are essentially giving up any IDE/typechecker/language server support because such fixtures cannot be properly typehinted because they are returning a callable, not a value. And python is still pretty new to typehinting callables.So we can't use ctrl + click, we don't get any autocompletion, and mypy/pyright won't warn us when we are using the factory incorrectly. Additionally, any changes to the factory's interface will require us to search for its usages by hand and fix every single one.Fixture classes solve all of the problems I mentioned:Autocompletion out of the boxReturn type of the fixture will automatically be inferred by pyright/mypyWhen the interface of the fixture changes or when you use it incorrectly, your type checker will warn youSearch all references and show definition (ctrl + click) also works out of the boxUsage scenarioLet's say that we have a few pre-existing fixtures:db_connection,http_session, andcurrent_user. Now we would like to write a new fixture that can create arbitrary users based onname,age, andheightarguments. We want our new fixture,create_user, to automatically get our old fixtures using dependency injection. Let's see what such a fixture will look like:importpytestimportrequests@pytest.fixturedefdb_connection()->dict[str,str]:...@pytest.fixturedefhttp_session()->requests.Session:...@pytest.fixturedefcurrent_user()->requests.Session:...@pytest.fixtureasyncdefcreate_user(db_connection:dict[str,str],http_session:requests.Session,current_user:requests.Session,)->Callable[[str,int,int],dict[str,str|int|bool]]:asyncdefinner(name:str,age:int,height:int):user={...}self.db_connection.execute(...)ifself.current_user[...]isnotNone:self.http_session.post(...)returnuserreturninnerdeftest_my_code(create_user:Callable[[str,intstr],dict[str,str|int|bool]]):johny=create_user("Johny",27,183)michael=create_user("Michael",43,165)loretta=create_user("Loretta",31,172)# Some testing code below...See how ugly and vague the typehints for create_user are? Also, see how we duplicate the return type and argument information? Additionally, if you had thousands of tests and iftest_my_codewithcreate_userwere in different files, you would have to use plaintext search to find the definition of the fixture if you wanted to see how to use it. Not too nice.Now let's rewrite this code to solve all of the problems I mentioned:frompytest_fixture_classesimportfixture_classfromcollections.abcimportMappingimportrequestsimportpytest@pytest.fixturedefdb_connection()->dict[str,str]:...@pytest.fixturedefhttp_session()->requests.Session:...@pytest.fixturedefcurrent_user()->Mapping[str,str|int|bool]:...@fixture_class(name="create_user")classCreateUser:db_connection:Mapping[str,str]http_session:requests.Sessioncurrent_user:Mapping[str,str|int|bool]def__call__(self,name:str,age:int,height:int)->dict[str,str|int|bool]:user={...}self.db_connection.execute(...)ifself.current_user[...]isnotNone:self.http_session.post(...)returnuserdeftest_my_code(create_user:CreateUser):johny=create_user("Johny",27,183)michael=create_user("Michael",43,165)loretta=create_user("Loretta",31,172)# Some testing code below...Implementation detailsThe fixture_class decorator turns your class into a frozen dataclass with slots so you won't be able to add new attributes to it after definiton. You can, however, define any methods you like except__init__.