{"id":15936872,"url":"https://github.com/tyleryep/pytest-idempotent","last_synced_at":"2025-03-25T06:31:17.184Z","repository":{"id":62584090,"uuid":"432023573","full_name":"TylerYep/pytest-idempotent","owner":"TylerYep","description":"Pytest plugin for testing the idempotency of a function.","archived":false,"fork":false,"pushed_at":"2025-03-17T18:45:53.000Z","size":142,"stargazers_count":3,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-19T09:40:58.093Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"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/TylerYep.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,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-11-26T01:45:22.000Z","updated_at":"2025-03-11T02:43:04.000Z","dependencies_parsed_at":"2023-12-28T10:46:32.378Z","dependency_job_id":"13d30054-9423-43a3-be16-fa930bab4df0","html_url":"https://github.com/TylerYep/pytest-idempotent","commit_stats":{"total_commits":86,"total_committers":3,"mean_commits":"28.666666666666668","dds":"0.43023255813953487","last_synced_commit":"102ebb8a34fd35009772941a897bc5f4a5b1edda"},"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TylerYep%2Fpytest-idempotent","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TylerYep%2Fpytest-idempotent/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TylerYep%2Fpytest-idempotent/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TylerYep%2Fpytest-idempotent/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TylerYep","download_url":"https://codeload.github.com/TylerYep/pytest-idempotent/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245413719,"owners_count":20611353,"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":[],"created_at":"2024-10-07T04:41:07.326Z","updated_at":"2025-03-25T06:31:16.916Z","avatar_url":"https://github.com/TylerYep.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pytest-idempotent\n\n[![Python 3.8+](https://img.shields.io/badge/python-3.7+-blue.svg)](https://www.python.org/downloads/release/python-380/)\n[![PyPI version](https://badge.fury.io/py/pytest-idempotent.svg)](https://badge.fury.io/py/pytest-idempotent)\n[![Build Status](https://github.com/TylerYep/pytest-idempotent/actions/workflows/test.yml/badge.svg)](https://github.com/TylerYep/pytest-idempotent/actions/workflows/test.yml)\n[![GitHub license](https://img.shields.io/github/license/TylerYep/pytest-idempotent)](https://github.com/TylerYep/pytest-idempotent/blob/main/LICENSE)\n[![codecov](https://codecov.io/gh/TylerYep/pytest-idempotent/branch/main/graph/badge.svg)](https://codecov.io/gh/TylerYep/pytest-idempotent)\n[![Downloads](https://pepy.tech/badge/pytest-idempotent)](https://pepy.tech/project/pytest-idempotent)\n\nPytest plugin for testing the idempotency of a function.\n\n## Usage\n\n```\npip install pytest-idempotent\n```\n\n## Documentation\n\nSuppose we had the following function, that we (incorrectly) assumed was idempotent. How would we write a test for this?\n\nFirst, we can label the function with a decorator:\n\n```python\n# abc.py\nfrom pytest_idempotent import idempotent  # or use your own decorator! See below.\n\n@idempotent\ndef func(x: list[int]) -\u003e None:\n    x += [9]\n```\n\nNote: this function is _not_ idempotent because calling it on the same list `x` grows the size of `x` by 1 each time. To be idempotent, we should be able to run `func` more than once without any adverse effects.\n\nWe can write an idempotency test for this function as follows:\n\n```python\n# tests/abc_test.py\nimport pytest\n\n@pytest.mark.idempotent\ndef test_func() -\u003e None:\n    x: list[int] = []\n\n    func(x)\n\n    assert x == [9]\n```\n\nAdding the `@pytest.mark.idempotent` mark automatically splits this test into two - one that tests the regular behavior, and one that tests that the function can be called twice without adverse effects.\n\n```\n❯❯❯ pytest\n\n================= test session starts ==================\nplatform darwin -- Python 3.9.2, pytest-6.2.5\ncollected 2 items\n\ntests/abc_test.py .F                     [100%]\n\n=====================  FAILURES ========================\n------------- test_func[idempotency-check] -------------\n\n    @pytest.mark.idempotent\n    def test_func() -\u003e None:\n        x: list[int] = []\n\n        func(x)\n\n\u003e       assert x == [9]\nE       assert [9, 9] == [9]\nE         Left contains one more item: 9\nE         Use -v to get the full diff\n\ntests/abc_test.py:19: AssertionError\n=============== short test summary info ================\nFAILED tests/abc_test.py::test_func[idempotency-check]\n  - assert [9, 9] == [9]\n============= 1 failed, 1 passed in 0.16s ==============\n```\n\n## How It Works\n\nIdempotency is a difficult pattern to enforce. To solve this issue, **pytest-idempotent** takes the following approach:\n\n- Introduce a decorator, `@idempotent`, to functions.\n\n  - This decorator serves as a visual aid. If this decorator is commonly used in the codebase, it is much easier to consider idempotency for new and existing functions.\n  - At runtime, this decorator is a no-op.\n  - At test-time, if the feature is enabled, we will run the decorated function twice with the same parameters in all test cases.\n  - We can also assert that the second run returns the same result using an additional parameter to the function's decorator: `@idempotent(equal_return=True)`.\n\n- For all tests marked using `@pytest.mark.idempotent`, we run each test twice: once normally, and once with the decorated function called twice.\n  - Both runs need to pass all assertions.\n  - We return the first result because the first run will complete the processing. The second will either return exact the same result or be a no-op.\n  - To disable idempotency testing for a test or group of tests, add the Pytest marker:\n    `@pytest.mark.idempotent(enabled=False)`\n\n## Enforcing Tests Use `@pytest.mark.idempotent`\n\nBy default, any test that calls an `@idempotent` function must also be decorated with the marker `@pytest.mark.idempotent`.\n\nTo disable idempotency testing for a test or group of tests, use:\n`@pytest.mark.idempotent(enabled=False)`, or add the following config to your project:\n\n```python\ndef pytest_idempotent_enforce_tests() -\u003e bool:\n    return False\n```\n\nTo disable enforced idempotency testing for a specific function, you can also pass the flag into the decorator:\n\n```python\n# abc.py\nfrom pytest_idempotent import idempotent\n\n@idempotent(enforce_tests=False)\ndef func() -\u003e None:\n    return\n```\n\n\u003c!-- To automatically enable this marker for all tests, you can use a custom autouse fixture. (Warning: this will run ALL tests twice, regardless of whether they contain an idempotent function or not.) --\u003e\n\nOr, you can automatically add the marker based on the test name by adding to `conftest.py`:\n\n```python\n# conftest.py\ndef pytest_collection_modifyitems(items):\n    for item in items:\n        if \"idempotent\" in item.nodeid:\n            item.add_marker(pytest.mark.idempotent)\n```\n\n## @idempotent decorator\n\nBy default, the `@idempotent` decorator does nothing during runtime. We do not want to add overhead to production code to run tests.\n\n```python\nfrom typing import Any, Callable, TypeVar\n\n_F = TypeVar(\"_F\", bound=Callable[..., Any])\n\n\ndef idempotent(func: _F) -\u003e _F:\n    \"\"\"\n    No-op during runtime.\n    This marker allows pytest-idempotent to override the decorated function\n    during test-time to verify the function is idempotent.\n    \"\"\"\n    return func\n```\n\nTo use your own `@idempotent` decorator, you can override the `pytest_idempotent_decorator` function in your `conftest.py` to return the module path to your implementation.\n\n```python\n# conftest.py\n# Optional: you can define this to ensure the plugin is correctly installed\npytest_plugins = [\"pytest_idempotent\"]\n\n\ndef pytest_idempotent_decorator() -\u003e str:\n    # This links to my custom implementation of @idempotent.\n    return \"src.utils.idempotent\"\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftyleryep%2Fpytest-idempotent","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftyleryep%2Fpytest-idempotent","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftyleryep%2Fpytest-idempotent/lists"}