{"id":13468938,"url":"https://github.com/zmievsa/pytest-fixture-classes","last_synced_at":"2025-03-16T08:32:22.128Z","repository":{"id":63200580,"uuid":"565914775","full_name":"zmievsa/pytest-fixture-classes","owner":"zmievsa","description":"Fixtures as classes that work well with dependency injection, autocompletetion, type checkers, and language servers","archived":false,"fork":false,"pushed_at":"2023-10-28T15:28:41.000Z","size":46,"stargazers_count":39,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-08T10:46:37.440Z","etag":null,"topics":["annotations","fixtures","pytest","pytest-plugin","python","testing","type-hints","unit-testing"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zmievsa.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2022-11-14T15:37:42.000Z","updated_at":"2024-08-09T12:03:21.000Z","dependencies_parsed_at":"2023-10-28T14:30:20.942Z","dependency_job_id":"2a991fe2-fb2b-45bf-ade6-b6685d19178e","html_url":"https://github.com/zmievsa/pytest-fixture-classes","commit_stats":null,"previous_names":["zmievsa/pytest-fixture-classes","ovsyanka83/pytest-fixture-classes"],"tags_count":0,"template":false,"template_full_name":"zmievsa/python-template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zmievsa%2Fpytest-fixture-classes","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zmievsa%2Fpytest-fixture-classes/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zmievsa%2Fpytest-fixture-classes/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zmievsa%2Fpytest-fixture-classes/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zmievsa","download_url":"https://codeload.github.com/zmievsa/pytest-fixture-classes/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243806070,"owners_count":20350775,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["annotations","fixtures","pytest","pytest-plugin","python","testing","type-hints","unit-testing"],"created_at":"2024-07-31T15:01:22.293Z","updated_at":"2025-03-16T08:32:21.706Z","avatar_url":"https://github.com/zmievsa.png","language":"Python","funding_links":[],"categories":["Python"],"sub_categories":[],"readme":"# Pytest Fixture Classes\n\nTyped [factory fixtures](https://docs.pytest.org/en/6.2.x/fixture.html#factories-as-fixtures) that work well with dependency injection, autocompletetion, type checkers, and language servers.\n\nNo mypy plugins required!\n\n## Installation\n\n`pip install pytest-fixture-classes`\n\n## Usage\n\n### Quickstart\n\nHere's a factory fixture from [pytest's documentation](https://docs.pytest.org/en/6.2.x/fixture.html#factories-as-fixtures):\n\n```python\nfrom typing import Any\nimport pytest\n\n\n@pytest.fixture()\ndef orders() -\u003e list[str]:\n    return [\"order1\", \"order2\"]\n\n\n@pytest.fixture\ndef make_customer_record(orders: list[str]) -\u003e Callable[str, dict[str, Any]]:\n    def _make_customer_record(name):\n        return {\"name\": name, \"orders\": orders}\n\n    return _make_customer_record\n\n\ndef test_customer_records(make_customer_record: Callable[str, dict[str, Any]]):\n    customer_1 = make_customer_record(\"Lisa\")\n    customer_2 = make_customer_record(\"Mike\")\n    customer_3 = make_customer_record(\"Meredith\")\n```\n\nAnd here's the same factory implemented using fixture classes:\n\n```python\nfrom pytest_fixture_classes import fixture_class\nfrom typing import Any\nimport pytest\n\n\n@pytest.fixture()\ndef orders():\n    return [\"order1\", \"order2\"]\n\n\n@fixture_class(name=\"make_customer_record\")\nclass MakeCustomerRecord:\n    orders: list[str]\n\n    def __call__(self, name: str) -\u003e dict[str, Any]:\n        return return {\"name\": name, \"orders\": orders}\n\n\ndef test_customer_records(make_customer_record: MakeCustomerRecord):\n    customer_1 = make_customer_record(\"Lisa\")\n    customer_2 = make_customer_record(\"Mike\")\n    customer_3 = make_customer_record(\"Meredith\")\n```\n\nYou can, of course, add any methods you like into the class but I prefer to keep it a simple callable.\n\n### Rationale\n\nIf 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.\n\nSo 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.\n\nFixture classes solve all of the problems I mentioned:\n\n* Autocompletion out of the box\n* Return type of the fixture will automatically be inferred by pyright/mypy\n* When the interface of the fixture changes or when you use it incorrectly, your type checker will warn you\n* Search all references and show definition (ctrl + click) also works out of the box\n\n### Usage scenario\n\nLet's say that we have a few pre-existing fixtures: `db_connection`, `http_session`, and `current_user`. Now we would like to write a new fixture that can create arbitrary users based on `name`, `age`, and `height` arguments. 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:\n\n```python\nimport pytest\nimport requests\n\n@pytest.fixture\ndef db_connection() -\u003e dict[str, str]:\n    ...\n\n@pytest.fixture\ndef http_session() -\u003e requests.Session:\n    ...\n\n\n@pytest.fixture\ndef current_user() -\u003e requests.Session:\n    ...\n\n\n@pytest.fixture\nasync def create_user(\n    db_connection: dict[str, str],\n    http_session: requests.Session,\n    current_user: requests.Session,\n) -\u003e Callable[[str, int, int], dict[str, str | int | bool]]:\n    async def inner(name: str, age: int, height: int):\n        user = {...}\n        self.db_connection.execute(...)\n        if self.current_user[...] is not None:\n            self.http_session.post(...)\n        \n        return user\n\n    return inner\n\ndef test_my_code(create_user: Callable[[str, int str], dict[str, str | int | bool]]):\n    johny = create_user(\"Johny\", 27, 183)\n    michael = create_user(\"Michael\", 43, 165)\n    loretta = create_user(\"Loretta\", 31, 172)\n\n    # Some testing code below\n    ...\n\n```\n\nSee 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 if `test_my_code` with `create_user` were 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.\n\nNow let's rewrite this code to solve all of the problems I mentioned:\n\n```python\nfrom pytest_fixture_classes import fixture_class\nfrom collections.abc import Mapping\nimport requests\nimport pytest\n\n\n@pytest.fixture\ndef db_connection() -\u003e dict[str, str]:\n    ...\n\n\n@pytest.fixture\ndef http_session() -\u003e requests.Session:\n    ...\n\n\n@pytest.fixture\ndef current_user() -\u003e Mapping[str, str | int | bool]:\n    ...\n\n\n@fixture_class(name=\"create_user\")\nclass CreateUser:\n    db_connection: Mapping[str, str]\n    http_session: requests.Session\n    current_user: Mapping[str, str | int | bool]\n\n    def __call__(self, name: str, age: int, height: int) -\u003e dict[str, str | int | bool]:\n        user = {...}\n        self.db_connection.execute(...)\n        if self.current_user[...] is not None:\n            self.http_session.post(...)\n        \n        return user\n\n\ndef test_my_code(create_user: CreateUser):\n    johny = create_user(\"Johny\", 27, 183)\n    michael = create_user(\"Michael\", 43, 165)\n    loretta = create_user(\"Loretta\", 31, 172)\n\n    # Some testing code below\n    ...\n```\n\n## Implementation details\n\n* The 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__`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzmievsa%2Fpytest-fixture-classes","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzmievsa%2Fpytest-fixture-classes","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzmievsa%2Fpytest-fixture-classes/lists"}