├── pytest_lambda ├── py.typed ├── __init__.py ├── compat.py ├── types.py ├── exceptions.py ├── plugin.py ├── util.py ├── fixtures.py └── impl.py ├── _tox_install_command.sh ├── mypy.ini ├── pytest.ini ├── tests ├── test_async.py ├── test_module.py └── test_util.py ├── LICENSE ├── pyproject.toml ├── .gitignore ├── run_tox_tests.sh ├── tox.ini ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── README.md └── poetry.lock /pytest_lambda/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_tox_install_command.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | poetry install -v \ 3 | && poetry run pip install --no-warn-conflicts "$@" 4 | -------------------------------------------------------------------------------- /pytest_lambda/__init__.py: -------------------------------------------------------------------------------- 1 | """Define pytest fixtures using lambda functions""" 2 | 3 | __version__ = '2.2.1' 4 | 5 | from .fixtures import * 6 | from .util import * 7 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | # Show the error codes for each issue (e.g. "valid-type" or "override") 3 | # These error codes may be ignored per-line with: # type: ignore[, ...] 4 | show_error_codes = True 5 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | # NOTE: pytest.ini is used (instead of pyproject.toml) to support older pytest versions 2 | # when running tox tests. 3 | 4 | [pytest] 5 | addopts = -v --tb=short --doctest-modules 6 | 7 | asyncio_mode = auto 8 | 9 | python_classes = Test* Describe* Context* 10 | python_functions = test_* it_* its_* test 11 | python_files = tests.py test_*.py 12 | -------------------------------------------------------------------------------- /pytest_lambda/compat.py: -------------------------------------------------------------------------------- 1 | try: 2 | from typing import TypedDict 3 | except ImportError: # Python < 3.8 4 | from typing_extensions import TypedDict 5 | 6 | try: 7 | from _pytest.compat import _PytestWrapper 8 | except ImportError: # pytest<4 9 | # Old pytest versions set the wrapped value directly to __pytest_wrapped__ 10 | class _PytestWrapper: # type: ignore[no-redef] 11 | def __new__(cls, obj): 12 | return obj 13 | -------------------------------------------------------------------------------- /tests/test_async.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from pytest_lambda import lambda_fixture 4 | 5 | 6 | sync_value = lambda_fixture(lambda: 'apple') 7 | async_value = lambda_fixture(lambda: 'apple', async_=True) 8 | awaitable_value = lambda_fixture(lambda: asyncio.sleep(0, 'apple'), async_=True) 9 | 10 | 11 | def it_awaits_async_lambda_fixtures(sync_value, async_value, awaitable_value): 12 | assert sync_value == async_value == awaitable_value == 'apple' 13 | -------------------------------------------------------------------------------- /pytest_lambda/types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, Callable, Iterable, TYPE_CHECKING 4 | 5 | from .compat import TypedDict 6 | 7 | if TYPE_CHECKING: 8 | from _pytest.fixtures import _Scope 9 | 10 | 11 | class LambdaFixtureKwargs(TypedDict, total=False): 12 | scope: _Scope 13 | params: Iterable[object] | None 14 | autouse: bool 15 | ids: Iterable[None | str | float | int | bool] | Callable[[Any], object | None] 16 | name: str | None 17 | -------------------------------------------------------------------------------- /pytest_lambda/exceptions.py: -------------------------------------------------------------------------------- 1 | __all__ = ['DisabledFixtureError', 'NotImplementedFixtureError'] 2 | 3 | 4 | class DisabledFixtureError(Exception): 5 | """Thrown when a disabled fixture has been requested by a test or fixture 6 | 7 | See pytest_lambda.fixtures.disabled_fixture 8 | """ 9 | 10 | 11 | class NotImplementedFixtureError(NotImplementedError): 12 | """Thrown when an abstract fixture has been requested 13 | 14 | See pytest_lambda.fixtures.not_implemented_fixture 15 | """ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Zach Kanzler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = 'pytest-lambda' 3 | version = "2.2.1" 4 | description = 'Define pytest fixtures with lambda functions.' 5 | license = 'MIT' 6 | 7 | authors = [ 8 | 'Zach "theY4Kman" Kanzler ' 9 | ] 10 | 11 | readme = 'README.md' 12 | 13 | repository = 'https://github.com/theY4Kman/pytest-lambda' 14 | homepage = 'https://github.com/theY4Kman/pytest-lambda' 15 | 16 | keywords = ['pytest'] 17 | classifiers=[ 18 | 'Development Status :: 5 - Production/Stable', 19 | 'Programming Language :: Python', 20 | 'Framework :: Pytest', 21 | 'License :: OSI Approved :: MIT License', 22 | 'Topic :: Software Development :: Testing', 23 | ] 24 | 25 | 26 | [tool.poetry.dependencies] 27 | python = '^3.8.0' 28 | 29 | pytest = '>=3.6, <9' 30 | wrapt = '^1.11.0' 31 | 32 | 33 | [tool.poetry.dev-dependencies] 34 | mypy = "^0.971" 35 | pytest-asyncio = "*" 36 | pytest-markdown-docs = "*" 37 | tox = "^3.12" 38 | 39 | 40 | [tool.poetry.plugins."pytest11"] 41 | lambda = "pytest_lambda.plugin" 42 | 43 | 44 | [build-system] 45 | requires = ['poetry>=0.12'] 46 | build-backend = 'poetry.masonry.api' 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # pycharm 107 | .idea/ 108 | -------------------------------------------------------------------------------- /run_tox_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Run tests for each supported python/pytest version combo. 4 | # 5 | # When attempting to run the entire set of tox environments in parallel, some issues 6 | # arise due to the not-so-isolatedness of poetry dependency installation. To address 7 | # those issues, this script runs each version of pytest in separate batches, so that 8 | # each python version only runs a single suite at a time. 9 | # 10 | 11 | tox "$@" -p 5 -e py38-pytest82,py39-pytest82,py310-pytest82,py311-pytest82,py312-pytest82 12 | tox "$@" -p 5 -e py38-pytest81,py39-pytest81,py310-pytest81,py311-pytest81,py312-pytest81 13 | tox "$@" -p 5 -e py38-pytest80,py39-pytest80,py310-pytest80,py311-pytest80,py312-pytest80 14 | tox "$@" -p 5 -e py38-pytest74,py39-pytest74,py310-pytest74,py311-pytest74,py312-pytest74 15 | tox "$@" -p 5 -e py38-pytest73,py39-pytest73,py310-pytest73,py311-pytest73,py312-pytest73 16 | tox "$@" -p 4 -e py38-pytest72,py39-pytest72,py310-pytest72,py311-pytest72 17 | tox "$@" -p 4 -e py38-pytest71,py39-pytest71,py310-pytest71,py311-pytest71 18 | tox "$@" -p 4 -e py38-pytest70,py39-pytest70,py310-pytest70,py311-pytest70 19 | tox "$@" -p 4 -e py38-pytest62,py39-pytest62,py310-pytest62,py311-pytest62 20 | tox "$@" -p 2 -e py38-pytest61,py39-pytest61 21 | tox "$@" -p 2 -e py38-pytest60,py39-pytest60 22 | tox "$@" -p 2 -e py38-pytest54,py39-pytest54 23 | tox "$@" -p 2 -e py38-pytest53,py39-pytest53 24 | tox "$@" -p 2 -e py38-pytest52,py39-pytest52 25 | tox "$@" -p 2 -e py38-pytest51,py39-pytest51 26 | tox "$@" -p 2 -e py38-pytest50,py39-pytest50 27 | tox "$@" -p 2 -e py38-pytest46,py39-pytest46 28 | tox "$@" -p 2 -e py38-pytest45,py39-pytest45 29 | tox "$@" -p 2 -e py38-pytest44,py39-pytest44 30 | tox "$@" -p 2 -e py38-pytest40,py39-pytest40 31 | tox "$@" -p 2 -e py38-pytest39,py39-pytest39 32 | tox "$@" -p 2 -e py38-pytest36,py39-pytest36 33 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | isolated_build = true 3 | envlist = 4 | py{ 38, 39 }-pytest{36,39,40,44,45,46,50,51,52,53,54,60,61,62,70,71,72,73,74,80,81,82} 5 | py{ 38, 39,310,311 }-pytest{62,70,71,72} 6 | py{ 38, 39,310,311,312}-pytest{73,74,80,81,82} 7 | 8 | 9 | [testenv] 10 | whitelist_externals = poetry 11 | 12 | # We'll use `poetry install` to install the package; don't install the package, 13 | # which will likely override the pytest version we wish to use for the env. 14 | skip_install = true 15 | 16 | deps = 17 | pytest36: pytest~=3.6.0 18 | pytest39: pytest~=3.9.0 19 | pytest40: pytest~=4.0.0 20 | pytest44: pytest~=4.4.0 21 | pytest45: pytest~=4.5.0 22 | pytest46: pytest~=4.6.0 23 | pytest50: pytest~=5.0.0 24 | pytest51: pytest~=5.1.0 25 | pytest52: pytest~=5.2.0 26 | pytest53: pytest~=5.3.0 27 | pytest54: pytest~=5.4.0 28 | pytest60: pytest~=6.0.0 29 | pytest61: pytest~=6.1.0 30 | pytest62: pytest~=6.2.0 31 | pytest70: pytest~=7.0.0 32 | pytest71: pytest~=7.1.0 33 | pytest72: pytest~=7.2.0 34 | pytest73: pytest~=7.3.0 35 | pytest74: pytest~=7.4.0 36 | pytest80: pytest~=8.0.0 37 | pytest81: pytest~=8.1.0 38 | 39 | # Older versions of pytest require older versions of pytest-asyncio 40 | pytest{61,62}: pytest-asyncio<0.21 41 | pytest{54,60}: pytest-asyncio<0.17 42 | pytest{36,39,40,44,45,46,50,51,52,53}: pytest-asyncio<0.11 43 | 44 | # NOTE: the attrs dep resolves an issue with pytest 4.0 and attrs>19.2.0 45 | # see https://stackoverflow.com/a/58189684/148585 46 | pytest40: attrs==19.1.0 47 | 48 | 49 | install_command = 50 | {toxinidir}/_tox_install_command.sh {opts} {packages} 51 | 52 | commands = poetry run pytest --markdown-docs 53 | 54 | # pytest-markdown-docs is only compatible with pytest 7+. We skip markdown tests for older versions. 55 | [testenv:py{38,39,310,311,312}-pytest{36,39,40,44,45,46,50,51,52,53,54,60,61,62}] 56 | commands = 57 | poetry run pip uninstall -y pytest-markdown-docs 58 | poetry run pytest 59 | -------------------------------------------------------------------------------- /tests/test_module.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pytest_lambda import lambda_fixture, static_fixture 4 | 5 | 6 | unique = lambda_fixture(lambda: 'unique') 7 | 8 | 9 | def it_processes_toplevel_lambda_fixture(unique): 10 | expected = 'unique' 11 | actual = unique 12 | assert expected == actual 13 | 14 | 15 | unique_static = static_fixture('unique') 16 | 17 | 18 | def it_processes_toplevel_static_fixture(unique_static): 19 | expected = 'unique' 20 | actual = unique_static 21 | assert expected == actual 22 | 23 | 24 | unique_alias = lambda_fixture('unique_static') 25 | 26 | 27 | def it_processes_toplevel_aliased_lambda_fixture(unique_alias): 28 | expected = 'unique' 29 | actual = unique_alias 30 | assert expected == actual 31 | 32 | 33 | a = static_fixture('a') 34 | b = static_fixture('b') 35 | c = static_fixture('c') 36 | abc = lambda_fixture('a', 'b', 'c') 37 | 38 | 39 | def it_processes_toplevel_tuple_lambda_fixture(abc): 40 | expected = ('a', 'b', 'c') 41 | actual = abc 42 | assert expected == actual 43 | 44 | 45 | x, y, z = lambda_fixture('a', 'b', 'c') 46 | 47 | 48 | def it_processes_toplevel_destructured_tuple_lambda_fixture(x, y, z): 49 | expected = ('a', 'b', 'c') 50 | actual = (x, y, z) 51 | assert expected == actual 52 | 53 | 54 | pa, pb, pc, pd = lambda_fixture(params=[ 55 | pytest.param('alfalfa', 'better', 'dolt', 'gamer'), 56 | ]) 57 | 58 | 59 | def it_processes_toplevel_destructured_parametrized_lambda_fixture(pa, pb, pc, pd): 60 | expected = ('alfalfa', 'better', 'dolt', 'gamer') 61 | actual = (pa, pb, pc, pd) 62 | assert expected == actual 63 | 64 | 65 | destructured_id = lambda_fixture(params=[ 66 | pytest.param('muffin', id='muffin'), 67 | ]) 68 | 69 | 70 | def it_uses_ids_from_destructured_parametrized_lambda_fixture(destructured_id, request): 71 | assert destructured_id in request.node.callspec.id 72 | 73 | 74 | class TestClass: 75 | a = lambda_fixture() 76 | 77 | def it_processes_implicit_reference_fixture(self, a): 78 | expected = 'a' 79 | actual = a 80 | assert expected == actual 81 | -------------------------------------------------------------------------------- /pytest_lambda/plugin.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from typing import List, Sequence, Set, Tuple 3 | 4 | import pytest 5 | from _pytest.mark import Mark, ParameterSet 6 | from _pytest.python import Metafunc, Module 7 | 8 | from pytest_lambda.impl import LambdaFixture, _LambdaFixtureParametrizedIterator 9 | 10 | 11 | def pytest_collectstart(collector): 12 | if isinstance(collector, Module): 13 | process_lambda_fixtures(collector.module) 14 | 15 | 16 | def pytest_pycollect_makeitem(collector, name, obj): 17 | if inspect.isclass(obj): 18 | process_lambda_fixtures(obj) 19 | 20 | 21 | def process_lambda_fixtures(parent): 22 | """Turn all lambda_fixtures in a class/module into actual pytest fixtures 23 | """ 24 | lfix_attrs: List[Tuple[str, LambdaFixture]] = ( 25 | inspect.getmembers(parent, lambda o: isinstance(o, LambdaFixture))) 26 | 27 | for name, attr in lfix_attrs: 28 | attr.contribute_to_parent(parent, name) 29 | 30 | return parent 31 | 32 | 33 | @pytest.hookimpl(tryfirst=True) 34 | def pytest_generate_tests(metafunc: Metafunc) -> None: 35 | """Parametrize all tests using destructured parametrized lambda fixtures 36 | 37 | This is what powers things like: 38 | 39 | a, b, c = lambda_fixture(params=[ 40 | pytest.param(1, 2, 3) 41 | ]) 42 | 43 | def test_my_thing(a, b, c): 44 | assert a < b < c 45 | 46 | """ 47 | param_sources: Set[LambdaFixture] = set() 48 | 49 | for argname in metafunc.fixturenames: 50 | # Get the FixtureDefs for the argname. 51 | fixture_defs = metafunc._arg2fixturedefs.get(argname) 52 | if not fixture_defs: 53 | # Will raise FixtureLookupError at setup time if not parametrized somewhere 54 | # else (e.g @pytest.mark.parametrize) 55 | continue 56 | 57 | for fixturedef in reversed(fixture_defs): 58 | param_source = getattr(fixturedef.func, '_self_params_source', None) 59 | if param_source: 60 | param_sources.add(param_source) 61 | 62 | if param_sources: 63 | requested_fixturenames = set(metafunc.fixturenames) 64 | 65 | for param_source in param_sources: 66 | if param_source.fixture_kwargs['params'] is None: 67 | continue 68 | 69 | params_iter = param_source._self_iter 70 | assert isinstance(params_iter, _LambdaFixtureParametrizedIterator) 71 | 72 | # TODO(zk): skip parametrization for args already parametrized by @mark.parametrize 73 | # XXX(zk): is there a way around falsifying the requested fixturenames to avoid "uses no argument" error? 74 | for child_name in params_iter.child_names: 75 | if child_name not in requested_fixturenames: 76 | metafunc.fixturenames.append(child_name) 77 | requested_fixturenames.add(child_name) 78 | 79 | metafunc.parametrize( 80 | params_iter.child_names, 81 | param_source.fixture_kwargs['params'], 82 | scope=param_source.fixture_kwargs.get('scope'), 83 | ids=param_source.fixture_kwargs.get('ids'), 84 | ) 85 | -------------------------------------------------------------------------------- /tests/test_util.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from _pytest.compat import getfuncargnames 3 | 4 | from pytest_lambda import wrap_fixture 5 | 6 | 7 | class DescribeWrapFixture: 8 | 9 | def it_includes_extended_and_wrapped_args_in_spec(self): 10 | def fixture(fixture_unique): 11 | pass 12 | 13 | @wrap_fixture(fixture) 14 | def extended_fixture(extension_unique, wrapped): 15 | pass 16 | 17 | args = getfuncargnames(extended_fixture) 18 | 19 | expected = {'fixture_unique', 'extension_unique'} 20 | actual = set(args) - {'request'} 21 | assert expected == actual 22 | 23 | def it_orders_decorated_method_args_first(self): 24 | def fixture(fixture_unique): 25 | pass 26 | 27 | @wrap_fixture(fixture) 28 | def extended_fixture(extension_unique, wrapped): 29 | pass 30 | 31 | args = getfuncargnames(extended_fixture) 32 | 33 | expected = ('extension_unique', 'fixture_unique', 'request') 34 | actual = args 35 | assert expected == actual 36 | 37 | 38 | def it_passes_wrapped_fixture_to_extension(self, request): 39 | class Called(AssertionError): 40 | pass 41 | 42 | def fixture(): 43 | raise Called() 44 | 45 | @wrap_fixture(fixture) 46 | def extended_fixture(wrapped): 47 | wrapped() 48 | 49 | with pytest.raises(Called): 50 | extended_fixture(request=request) 51 | 52 | 53 | def it_allows_calling_wrapped_fixture_without_args(self, request): 54 | def fixture(message): 55 | return message 56 | 57 | @wrap_fixture(fixture) 58 | def extended_fixture(wrapped): 59 | return wrapped() 60 | 61 | expected = 'unique message' 62 | actual = extended_fixture(request=request, message=expected) 63 | assert expected == actual 64 | 65 | 66 | def it_allows_calling_wrapped_fixture_with_overridden_args(self, request): 67 | def fixture(message): 68 | return message 69 | 70 | @wrap_fixture(fixture) 71 | def extended_fixture(wrapped): 72 | return wrapped(message='overridden message') 73 | 74 | expected = 'overridden message' 75 | actual = extended_fixture(request=request, message='unique message') 76 | assert expected == actual 77 | 78 | 79 | def it_allows_calling_wrapped_fixture_with_ignored_args(self, request): 80 | def fixture(message): 81 | return message 82 | 83 | @wrap_fixture(fixture, ignore='message') 84 | def extended_fixture(wrapped): 85 | return wrapped(message='overridden message') 86 | 87 | expected = 'overridden message' 88 | actual = extended_fixture(request=request) 89 | assert expected == actual 90 | 91 | 92 | def it_passes_extension_args_to_extension(self, request): 93 | def fixture(*args, **kwargs): 94 | pass 95 | 96 | @wrap_fixture(fixture) 97 | def extended_fixture(wrapped, extension_arg): 98 | return extension_arg 99 | 100 | expected = 'extension' 101 | actual = extended_fixture(request=request, extension_arg=expected) 102 | assert expected == actual 103 | 104 | 105 | def it_doesnt_pass_extension_args_to_wrapped_fixture(self, request): 106 | def fixture(*args, **kwargs): 107 | return args, kwargs 108 | 109 | @wrap_fixture(fixture) 110 | def extended_fixture(wrapped, extension_arg): 111 | return wrapped() 112 | 113 | expected = (), {} 114 | actual = extended_fixture(request=request, extension_arg='stuff') 115 | assert expected == actual, \ 116 | 'Expected no args or kwargs to be passed to wrapped fixture' 117 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | 8 | ## [Unreleased] 9 | 10 | 11 | ## [2.2.1] — 2024-05-27 12 | ### Changed 13 | - Drop support for Python 3.7 (now EOL) 14 | - Add support for Python 3.12 15 | - Add support for pytest versions 8.0 and 8.1 16 | - Relax pytest version pin to allow all versions under 9.x 17 | 18 | 19 | ## [2.2.0] — 2022-08-20 20 | ### Added 21 | - Add `py.typed` file to package to enable mypy static type checking 22 | - Expose minimal generic typing on `LambdaFixture` 23 | 24 | ### Fixed 25 | - Avoid crash when running under PyCharm/pydev debugger due to `LambdaFixture.__class__` property 26 | 27 | 28 | ## [2.1.0] — 2022-07-17 29 | ### Changed 30 | - Preserve declared order of arguments with `wrap_fixture` (decorated method's first, then wrapped fixture's, then `request`) 31 | - DOC: add destructuring examples to README 32 | 33 | 34 | ## [2.0.0] — 2022-07-14 35 | ### BREAKING 36 | - Due to destructured parametrization now being powered by a custom `pytest_generate_tests` hook, incompatibilities may have been introduced. Out of caution, the major version has been bumped. 37 | 38 | ### Added 39 | - Add support for destructuring referential tuple lambda fixtures (e.g. `x, y, z = lambda_fixture('a', 'b', 'c')`) 40 | - Add support for destructuring parametrized lambda fixtures (e.g. `a, b, c = lambda_fixture(params=[pytest.param('ayy', 'bee', 'see')])`) 41 | 42 | 43 | ## [1.3.0] — 2022-05-17 44 | ### Added 45 | - Add support for async/awaitable fixtures 46 | 47 | 48 | ## [1.2.6] — 2022-05-15 49 | ### Changed 50 | - Add support for pytest versions 7.0 and 7.1 51 | - Relax pytest version pin to allow all versions under 8.x 52 | 53 | 54 | ## [1.2.5] — 2021-08-23 55 | ### Fixed 56 | - Avoid `ValueError: wrapper has not been initialized` when using implicit referential lambda fixtures (e.g. `name = lambda_fixture()`) in combination with `py.test --doctest-modules` 57 | 58 | 59 | ## [1.2.4] — 2020-12-28 60 | ### Changed 61 | - Add support for pytest version 6.2 62 | - Relax pytest version pin to allow all versions under 7.x 63 | 64 | 65 | ## [1.2.3] — 2020-11-02 66 | ### Fixed 67 | - Resolve error in `py.test --fixtures` due to `__module__` not properly being curried to fixture func 68 | 69 | 70 | ## [1.2.2] — 2020-11-02 71 | ### Fixed 72 | - Resolve error in `py.test --fixtures` when using `error_fixture`, `not_implemented_fixture`, or `disabled_fixture` 73 | 74 | 75 | ## [1.2.1] — 2020-11-02 76 | ### Changed 77 | - Add support for pytest version 6.1 78 | 79 | 80 | ## [1.2.0] — 2020-09-06 81 | ### Added 82 | - Allow certain arguments of wrapped fixture to be ignored w/ @wrap_fixture 83 | 84 | 85 | ## [1.1.1] — 2020-08-01 86 | ### Changed 87 | - Add support for pytest versions 4.6, 5.0, 5.1, 5.2, 5.3, 5.4, and 6.0 88 | 89 | 90 | ## [1.1.0] — 2019-08-14 91 | ### Added 92 | - Introduced `wrap_fixture` utility to extend fixtures while currying method signatures to feed pytest's dependency graph 93 | 94 | 95 | ## [1.0.0] — 2019-05-26 96 | ### Removed 97 | - Removed injection of pytest-lambda attrs into the `pytest` namespace 98 | 99 | 100 | ## [0.1.0] — 2019-02-02 101 | ### Fixed 102 | - Resolve error when executing `py.test --fixtures` 103 | 104 | 105 | ## [0.0.2] — 2018-07-29 106 | ### Added 107 | - Allow conditional raising of exceptions with `error_fixture` 108 | 109 | ### Changed 110 | - Updated README with more succinct examples, and titles for example sections 111 | 112 | 113 | ## [0.0.1] - 2018-07-28 114 | ### Added 115 | - `lambda_fixture`, `static_fixture`, `error_fixture`, `disabled_fixture`, and `not_implemented_fixture` 116 | - Totes rad README (that can actually be run with pytest! thanks to [pytest-markdown](https://github.com/Jc2k/pytest-markdown)) 117 | -------------------------------------------------------------------------------- /pytest_lambda/util.py: -------------------------------------------------------------------------------- 1 | import functools 2 | from contextlib import suppress 3 | from typing import Callable, Iterable, Union 4 | 5 | from _pytest.compat import getfuncargnames, get_real_func 6 | from _pytest.fixtures import call_fixture_func 7 | 8 | __all__ = ['wrap_fixture'] 9 | 10 | 11 | def wrap_fixture( 12 | fixturefunc: Callable, 13 | wrapped_param: str = 'wrapped', 14 | ignore: Union[str, Iterable[str]] = (), 15 | ) -> Callable[[Callable], Callable]: 16 | """Wrap a fixture function, extending its argspec w/ the decorated method 17 | 18 | pytest will prune the fixture dependency graph of any unneeded fixtures. It 19 | does this by reading the expected arg names of fixtures. When wrapping a 20 | fixture function, merely currying along **kwargs will cripple pytest's 21 | pruning. 22 | 23 | This method retains the arg names from the original fixture function, and 24 | returns a wrapper method that includes those original arg names, as well as 25 | any fixtures requested by the decorated function. 26 | 27 | The decorated method will be passed a wrapper of the passed fixturefunc 28 | that can be called with no arguments — the fixtures it requested will 29 | receive automagical defaults, though these may be overridden. The argument 30 | name of this wrapped fixturefunc may be customized with the `wrapped_param` 31 | arg, to avoid any collision with other fixture names. 32 | 33 | Example (contrived): 34 | 35 | bare_user = lambda_fixture(lambda user_factory: user_factory( 36 | username='bare-user', 37 | password='bare-password', 38 | )) 39 | 40 | @pytest.fixture 41 | @wrap_fixture(bare_user) 42 | def admin_user(team, wrapped): 43 | user = wrapped() 44 | team.add_member(user, role_id=TeamRole.Roles.ADMIN) 45 | return user 46 | 47 | :param fixturefunc: 48 | The fixture function to wrap 49 | 50 | :param wrapped_param: 51 | Name of parameter to pass the wrapped fixturefunc as 52 | 53 | :param ignore: 54 | Name of parameter(s) from fixturefunc to not include in wrapping 55 | fixture's args (and thus not request as fixtures from pytest) 56 | 57 | """ 58 | 59 | if isinstance(ignore, str): 60 | ignore = (ignore,) 61 | 62 | fixturefunc = get_real_func(fixturefunc) 63 | 64 | def decorator(fn: Callable): 65 | decorated_arg_names = list(getfuncargnames(fn)) 66 | if wrapped_param not in decorated_arg_names: 67 | raise TypeError( 68 | f'The decorated method must include an arg named {wrapped_param} ' 69 | f'as the wrapped fixture func.') 70 | 71 | # Don't include the wrapped param in the argspec we expose to pytest 72 | decorated_arg_names.remove(wrapped_param) 73 | 74 | fixture_arg_names = list(getfuncargnames(fixturefunc)) 75 | for ignored in ignore: 76 | with suppress(ValueError): 77 | fixture_arg_names.remove(ignored) 78 | 79 | # Remove duplicates while retaining order of args (decorated, then wrapped) 80 | all_arg_names = [*decorated_arg_names, *fixture_arg_names, 'request'] 81 | all_arg_names = list(sorted(set(all_arg_names), key=all_arg_names.index)) 82 | 83 | def extension_impl(**all_args): 84 | request = all_args['request'] 85 | 86 | ### 87 | # kwargs requested by the wrapped fixture 88 | # 89 | fixture_args = { 90 | name: value 91 | for name, value in all_args.items() 92 | if name in fixture_arg_names 93 | } 94 | 95 | ### 96 | # kwargs requested by the decorated method 97 | # 98 | decorated_args = { 99 | name: value 100 | for name, value in all_args.items() 101 | if name in decorated_arg_names 102 | } 103 | 104 | @functools.wraps(fixturefunc) 105 | def wrapped(**overridden_args): 106 | kwargs = { 107 | **fixture_args, 108 | **overridden_args, 109 | } 110 | return call_fixture_func(fixturefunc, request, kwargs) 111 | 112 | decorated_args[wrapped_param] = wrapped 113 | return call_fixture_func(fn, request, decorated_args) 114 | 115 | extension = build_wrapped_method(fn.__name__, all_arg_names, extension_impl) 116 | return extension 117 | 118 | return decorator 119 | 120 | 121 | _WRAPPED_FIXTURE_FORMAT = ''' 122 | def {name}({argnames}): 123 | return {impl_name}({kwargs}) 124 | ''' 125 | 126 | 127 | def build_wrapped_method(name: str, argnames: Iterable[str], impl: Callable) -> Callable: 128 | impl_name = '___extension_impl' 129 | argnames = tuple(argnames) 130 | 131 | source = _WRAPPED_FIXTURE_FORMAT.format( 132 | name=name, 133 | argnames=', '.join(argnames), 134 | kwargs=', '.join(f'{arg}={arg}' for arg in argnames), 135 | impl_name=impl_name 136 | ) 137 | context = {impl_name: impl} 138 | exec(source, context) 139 | 140 | return context[name] 141 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | they4kman@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /pytest_lambda/fixtures.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import inspect 4 | from typing import NoReturn, TYPE_CHECKING, Callable, Any, Iterable, Tuple, TypeVar 5 | 6 | from pytest_lambda.exceptions import DisabledFixtureError, NotImplementedFixtureError 7 | from pytest_lambda.impl import LambdaFixture 8 | 9 | if TYPE_CHECKING: 10 | from _pytest.fixtures import _Scope 11 | 12 | __all__ = ['lambda_fixture', 'static_fixture', 'error_fixture', 13 | 'disabled_fixture', 'not_implemented_fixture'] 14 | 15 | 16 | VT = TypeVar('VT') 17 | 18 | 19 | def lambda_fixture( 20 | fixture_name_or_lambda: str | Callable[..., VT] | None = None, 21 | *other_fixture_names: str, 22 | bind: bool = False, 23 | async_: bool = False, 24 | scope: _Scope = 'function', 25 | params: Iterable[object] | None = None, 26 | autouse: bool = False, 27 | ids: Iterable[None | str | float | int | bool] | Callable[[Any], object | None] | None = None, 28 | name: str | None = None, 29 | ) -> LambdaFixture[VT]: 30 | """Use a fixture name or lambda function to compactly declare a fixture 31 | 32 | Usage: 33 | 34 | class DescribeMyTests: 35 | url = lambda_fixture('list_url') 36 | updated_name = lambda_fixture(lambda vendor: vendor.name + ' updated') 37 | 38 | 39 | :param fixture_name_or_lambda: 40 | Either the name of another fixture, or a lambda function, which can request other fixtures 41 | with its params. If None, this defaults to the name of the attribute containing the 42 | lambda_fixture. 43 | 44 | :param bind: 45 | Set this to True to pass self to your fixture. It must be the first parameter in your 46 | fixture. This cannot be True if using a fixture name. 47 | 48 | :param async_: 49 | If True, the lambda will be wrapped in an async function; if the lambda evaluates to an 50 | awaitable value, it will be awaited. If False, the lambda's return value will be returned 51 | verbatim, regardless of whether it's awaitable. 52 | 53 | :param scope: 54 | :param params: 55 | :param autouse: 56 | :param ids: 57 | :param name: 58 | Options to pass to pytest.fixture() 59 | 60 | """ 61 | fixture_names_or_lambda: Tuple[str | Callable, ...] | str | Callable | None 62 | 63 | if other_fixture_names: 64 | if fixture_name_or_lambda is None: 65 | raise ValueError('If specified, all fixture names must be non-null.') 66 | fixture_names_or_lambda = (fixture_name_or_lambda,) + other_fixture_names 67 | else: 68 | fixture_names_or_lambda = fixture_name_or_lambda 69 | 70 | return LambdaFixture( 71 | fixture_names_or_lambda, 72 | bind=bind, 73 | async_=async_, 74 | scope=scope, params=params, autouse=autouse, ids=ids, name=name, 75 | ) 76 | 77 | 78 | def static_fixture(value: VT, **fixture_kwargs) -> LambdaFixture[VT]: 79 | """Compact method for defining a fixture that returns a static value 80 | """ 81 | return lambda_fixture(lambda: value, **fixture_kwargs) 82 | 83 | 84 | RAISE_EXCEPTION_FIXTURE_FUNCTION_FORMAT = ''' 85 | def raise_exception({args}): 86 | exc = error_fn({kwargs}) 87 | if exc is not None: 88 | raise exc 89 | ''' 90 | 91 | 92 | def error_fixture(error_fn: Callable, **fixture_kwargs) -> LambdaFixture[NoReturn]: 93 | """Fixture whose usage results in the raising of an exception 94 | 95 | Usage: 96 | 97 | class DescribeMyTests: 98 | url = error_fixture(lambda request: Exception( 99 | f'Please override the {request.fixturename} fixture!')) 100 | 101 | :param error_fn: 102 | Fixture method which returns an exception to raise. It may request pytest fixtures 103 | in its arguments. 104 | 105 | """ 106 | proto = tuple(inspect.signature(error_fn).parameters) 107 | args = ', '.join(proto) 108 | kwargs = ', '.join(f'{arg}={arg}' for arg in proto) 109 | 110 | source = RAISE_EXCEPTION_FIXTURE_FUNCTION_FORMAT.format( 111 | args=args, 112 | kwargs=kwargs, 113 | ) 114 | 115 | ctx = {'error_fn': error_fn} 116 | exec(source, ctx) 117 | 118 | raise_exception = ctx['raise_exception'] 119 | raise_exception.__module__ = getattr(error_fn, '__module__', raise_exception.__module__) 120 | return lambda_fixture(raise_exception, **fixture_kwargs) 121 | 122 | 123 | def disabled_fixture(**fixture_kwargs) -> LambdaFixture[NoReturn]: 124 | """Mark a fixture as disabled – using the fixture will raise an error 125 | 126 | This is useful when you know any usage of a fixture would be in error. When 127 | using disabled_fixture, pytest will raise an error if the fixture is 128 | requested, so errors can be detected early, and faulty assumptions may be 129 | avoided. 130 | 131 | Usage: 132 | 133 | class DescribeMyListOnlyViewSet(ViewSetTest): 134 | list_route = lambda_fixture(lambda: reverse('...')) 135 | detail_route = disabled_fixture() 136 | 137 | class DescribeRetrieve(UsesDetailRoute): 138 | def test_that_should_throw_error(): 139 | print('I should never be executed!') 140 | 141 | """ 142 | def build_disabled_fixture_error(request): 143 | msg = (f'Usage of the {request.fixturename} fixture has been disabled ' 144 | f'in the current context.') 145 | return DisabledFixtureError(msg) 146 | 147 | return error_fixture(build_disabled_fixture_error, **fixture_kwargs) 148 | 149 | 150 | def not_implemented_fixture(**fixture_kwargs) -> LambdaFixture[NoReturn]: 151 | """Mark a fixture as abstract – requiring definition/override by the user 152 | 153 | This is useful when defining abstract base classes requiring implementation 154 | to be used correctly. 155 | 156 | Usage: 157 | 158 | class MyBaseTest: 159 | list_route = not_implemented_fixture() 160 | 161 | class TestThings(MyBaseTest): 162 | list_route = lambda_fixture(lambda: reverse(...)) 163 | 164 | """ 165 | def build_not_implemented_fixture_error(request): 166 | msg = (f'Please define/override the {request.fixturename} fixture in ' 167 | f'the current context.') 168 | return NotImplementedFixtureError(msg) 169 | 170 | return error_fixture(build_not_implemented_fixture_error, **fixture_kwargs) 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pytest-lambda 2 | 3 | [![PyPI version](https://badge.fury.io/py/pytest-lambda.svg)](https://badge.fury.io/py/pytest-lambda) 4 | 5 | Define pytest fixtures with lambda functions. 6 | 7 | 8 | # Quickstart 9 | 10 | ```bash 11 | pip install pytest-lambda 12 | ``` 13 | 14 | ```python 15 | # test_the_namerator.py 16 | 17 | from pytest_lambda import lambda_fixture, static_fixture 18 | 19 | first = static_fixture('John') 20 | middle = static_fixture('Jacob') 21 | last = static_fixture('Jingleheimer-Schmidt') 22 | 23 | 24 | full_name = lambda_fixture(lambda first, middle, last: f'{first} {middle} {last}') 25 | 26 | 27 | def test_the_namerator(full_name): 28 | assert full_name == 'John Jacob Jingleheimer-Schmidt' 29 | ``` 30 | 31 | 32 | # Cheatsheet 33 | 34 | ```python 35 | import asyncio 36 | import pytest 37 | from pytest_lambda import ( 38 | disabled_fixture, 39 | error_fixture, 40 | lambda_fixture, 41 | not_implemented_fixture, 42 | static_fixture, 43 | ) 44 | 45 | # Basic usage 46 | fixture_name = lambda_fixture(lambda other_fixture: 'expression', scope='session', autouse=True) 47 | 48 | # Async fixtures (awaitables automatically awaited) — requires an async plugin, like pytest-asyncio 49 | fixture_name = lambda_fixture(lambda: asyncio.sleep(0, 'expression'), async_=True) 50 | 51 | # Request fixtures by name 52 | fixture_name = lambda_fixture('other_fixture') 53 | fixture_name = lambda_fixture('other_fixture', 'another_fixture', 'cant_believe_its_not_fixture') 54 | ren, ame, it = lambda_fixture('other_fixture', 'another_fixture', 'cant_believe_its_not_fixture') 55 | 56 | # Reference `self` inside a class 57 | class TestContext: 58 | fixture_name = lambda_fixture(lambda self: self.__class__.__name__, bind=True) 59 | 60 | # Parametrize 61 | fixture_name = lambda_fixture(params=['a', 'b']) 62 | fixture_name = lambda_fixture(params=['a', 'b'], ids=['A!', 'B!']) 63 | fixture_name = lambda_fixture(params=[pytest.param('a', id='A!'), 64 | pytest.param('b', id='B!')]) 65 | alpha, omega = lambda_fixture(params=[pytest.param('start', 'end', id='uno'), 66 | pytest.param('born', 'dead', id='dos')]) 67 | 68 | # Use literal value (not lazily evaluated) 69 | fixture_name = static_fixture(42) 70 | fixture_name = static_fixture('just six sevens', autouse=True, scope='module') 71 | 72 | # Raise an exception if fixture is requested 73 | fixture_name = error_fixture(lambda: ValueError('my life has no intrinsic value')) 74 | 75 | # Or maybe don't raise the exception 76 | fixture_name = error_fixture(lambda other_fixture: TypeError('nope') if other_fixture else None) 77 | 78 | # Create an abstract fixture (to be overridden by the user) 79 | fixture_name = not_implemented_fixture() 80 | fixture_name = not_implemented_fixture(autouse=True, scope='session') 81 | 82 | # Disable usage of a fixture (fail early to save future head scratching) 83 | fixture_name = disabled_fixture() 84 | ``` 85 | 86 | 87 | # What else is possible? 88 | 89 | Of course, you can use lambda fixtures inside test classes: 90 | ```python 91 | # test_staying_classy.py 92 | 93 | from pytest_lambda import lambda_fixture 94 | 95 | class TestClassiness: 96 | classiness = lambda_fixture(lambda: 9000 + 1) 97 | 98 | def test_how_classy_we_is(self, classiness): 99 | assert classiness == 9001 100 | ``` 101 | 102 | 103 | ### Aliasing other fixtures 104 | 105 | You can also pass the name of another fixture, instead of a lambda: 106 | ```python 107 | # test_the_bourne_identity.py 108 | 109 | from pytest_lambda import lambda_fixture, static_fixture 110 | 111 | agent = static_fixture('Bourne') 112 | who_i_am = lambda_fixture('agent') 113 | 114 | def test_my_identity(who_i_am): 115 | assert who_i_am == 'Bourne' 116 | ``` 117 | 118 | 119 | Even multiple fixture names can be used: 120 | ```python 121 | # test_the_bourne_identity.py 122 | 123 | from pytest_lambda import lambda_fixture, static_fixture 124 | 125 | agent_first = static_fixture('Jason') 126 | agent_last = static_fixture('Bourne') 127 | who_i_am = lambda_fixture('agent_first', 'agent_last') 128 | 129 | def test_my_identity(who_i_am): 130 | assert who_i_am == ('Jason', 'Bourne') 131 | ``` 132 | 133 | Destructuring assignment is also supported, allowing multiple fixtures to be renamed in one statement: 134 | ```python 135 | # test_the_bourne_identity.py 136 | 137 | from pytest_lambda import lambda_fixture, static_fixture 138 | 139 | agent_first = static_fixture('Jason') 140 | agent_last = static_fixture('Bourne') 141 | first, last = lambda_fixture('agent_first', 'agent_last') 142 | 143 | def test_my_identity(first, last): 144 | assert first == 'Jason' 145 | assert last == 'Bourne' 146 | ``` 147 | 148 | 149 | #### Annotating aliased fixtures 150 | 151 | You can force the loading of fixtures without trying to remember the name of `pytest.mark.usefixtures` 152 | ```python 153 | # test_garage.py 154 | 155 | from pytest_lambda import lambda_fixture, static_fixture 156 | 157 | car = static_fixture({ 158 | 'type': 'Sweet-ass Cadillac', 159 | 'is_started': False, 160 | }) 161 | turn_the_key = lambda_fixture(lambda car: car.update(is_started=True)) 162 | 163 | preconditions = lambda_fixture('turn_the_key', autouse=True) 164 | 165 | def test_my_caddy(car): 166 | assert car['is_started'] 167 | ``` 168 | 169 | 170 | ### Parametrizing 171 | 172 | Tests can be parametrized with `lambda_fixture`'s `params` kwarg 173 | ```python 174 | # test_number_5.py 175 | 176 | from pytest_lambda import lambda_fixture 177 | 178 | lady = lambda_fixture(params=[ 179 | 'Monica', 'Erica', 'Rita', 'Tina', 'Sandra', 'Mary', 'Jessica' 180 | ]) 181 | 182 | def test_your_man(lady): 183 | assert lady[:0] in 'my life' 184 | ``` 185 | 186 | Destructuring assignment of a parametrized lambda fixture is also supported 187 | ```python 188 | # test_number_5.py 189 | 190 | import pytest 191 | from pytest_lambda import lambda_fixture 192 | 193 | lady, where = lambda_fixture(params=[ 194 | pytest.param('Monica', 'in my life'), 195 | pytest.param('Erica', 'by my side'), 196 | pytest.param('Rita', 'is all I need'), 197 | pytest.param('Tina', 'is what I see'), 198 | pytest.param('Sandra', 'in the sun'), 199 | pytest.param('Mary', 'all night long'), 200 | pytest.param('Jessica', 'here I am'), 201 | ]) 202 | 203 | def test_your_man(lady, where): 204 | assert lady[:0] in where 205 | ``` 206 | 207 | 208 | ### Declaring abstract things 209 | 210 | `not_implemented_fixture` is perfect for labeling abstract parameter fixtures of test mixins 211 | ```python 212 | # test_mixinalot.py 213 | 214 | import pytest 215 | from pytest_lambda import static_fixture, not_implemented_fixture 216 | 217 | class Dials1900MixinALot: 218 | butt_shape = not_implemented_fixture() 219 | desires = not_implemented_fixture() 220 | 221 | def it_kicks_them_nasty_thoughts(self, butt_shape, desires): 222 | assert butt_shape == 'round' and 'triple X throw down' in desires 223 | 224 | 225 | @pytest.mark.xfail 226 | class DescribeMissThing(Dials1900MixinALot): 227 | butt_shape = static_fixture('flat') 228 | desires = static_fixture(['playin workout tapes by Fonda']) 229 | 230 | 231 | class DescribeSistaICantResista(Dials1900MixinALot): 232 | butt_shape = static_fixture('round') 233 | desires = static_fixture(['gettin in yo Benz', 'triple X throw down']) 234 | ``` 235 | 236 | 237 | Use `disabled_fixture` to mark a fixture as disabled. Go figure. 238 | ```python 239 | # test_ada.py 240 | 241 | import pytest 242 | from pytest_lambda import disabled_fixture 243 | 244 | wheelchair = disabled_fixture() 245 | 246 | @pytest.mark.xfail(strict=True) 247 | def test_stairs(wheelchair): 248 | assert wheelchair + 'floats' 249 | ``` 250 | 251 | 252 | ### Raising exceptions 253 | 254 | You can also raise an arbitrary exception when a fixture is requested, using `error_fixture` 255 | ```python 256 | # test_bikeshed.py 257 | 258 | import pytest 259 | from pytest_lambda import error_fixture, not_implemented_fixture, static_fixture 260 | 261 | bicycle = static_fixture('a sledgehammer') 262 | 263 | def it_does_sweet_jumps(bicycle): 264 | assert bicycle + 'jump' >= '3 feet' 265 | 266 | 267 | class ContextOcean: 268 | depth = not_implemented_fixture() 269 | bicycle = error_fixture(lambda bicycle, depth: ( 270 | RuntimeError(f'Now is not the time to use that! ({bicycle})') if depth > '1 league' else None)) 271 | 272 | 273 | class ContextDeep: 274 | depth = static_fixture('20,000 leagues') 275 | 276 | @pytest.mark.xfail(strict=True, raises=RuntimeError) 277 | def it_doesnt_flip_and_shit(self, bicycle): 278 | assert bicycle + 'floats' 279 | 280 | 281 | class ContextBeach: 282 | depth = static_fixture('1 inch') 283 | 284 | def it_gets_you_all_wet_but_otherwise_rides_like_a_champ(self, bicycle): 285 | assert 'im wet' 286 | ``` 287 | 288 | 289 | ### Async fixtures 290 | 291 | By passing `async_=True` to `lambda_fixture`, the fixture will be defined as an async function, and if the returned value is awaitable, it will be automatically awaited before exposing it to pytest. This allows the usage of async things while only being slightly salty that Python, TO THIS DAY, still does not support `await` expressions within lambdas! Yes, only slightly salty! 292 | 293 | NOTE: an asyncio pytest plugin is required to use async fixtures, such as [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) 294 | 295 | ```python 296 | # test_a_sink.py 297 | 298 | import asyncio 299 | import pytest 300 | from pytest_lambda import lambda_fixture 301 | 302 | async def hows_the_sink(): 303 | await asyncio.sleep(1) 304 | return 'leaky' 305 | 306 | a_sink = lambda_fixture(lambda: hows_the_sink(), async_=True) 307 | 308 | class DescribeASink: 309 | @pytest.mark.asyncio 310 | async def it_is_leaky(self, a_sink): 311 | assert a_sink is 'leaky' 312 | ``` 313 | 314 | 315 | # Development 316 | 317 | How can I build and test the thing locally? 318 | 319 | 1. Create a virtualenv, however you prefer. Or don't, if you prefer. 320 | 2. `pip install poetry` 321 | 3. `poetry install` to install setuptools entrypoint, so pytest automatically loads the plugin (otherwise, you'll have to run `py.test -p pytest_lambda.plugin`) 322 | 4. Run `py.test --markdown-docs`. The tests will be collected from the README.md (thanks to [pytest-markdown-docs](https://github.com/modal-labs/pytest-markdown-docs)). 323 | -------------------------------------------------------------------------------- /pytest_lambda/impl.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import functools 4 | import inspect 5 | from types import ModuleType 6 | from typing import Any, Generic, List, Optional, Tuple, TypeVar, Union, cast 7 | 8 | import pytest 9 | import wrapt # type: ignore[import] 10 | from _pytest.mark import ParameterSet 11 | 12 | from .compat import _PytestWrapper 13 | from .types import LambdaFixtureKwargs 14 | 15 | try: 16 | from collections.abc import Iterable, Sized 17 | except ImportError: 18 | from collections import Iterable, Sized 19 | 20 | _IDENTITY_LAMBDA_FORMAT = ''' 21 | {name} = lambda {argnames}: ({argnames}) 22 | ''' 23 | 24 | _DESTRUCTURED_PARAMETRIZED_LAMBDA_FORMAT = ''' 25 | {name} = lambda {source_name}: {source_name}[{index}] 26 | ''' 27 | 28 | 29 | def create_identity_lambda(name, *argnames): 30 | source = _IDENTITY_LAMBDA_FORMAT.format(name=name, argnames=', '.join(argnames)) 31 | context: dict[str, Any] = {} 32 | exec(source, context) 33 | 34 | fixture_func = context[name] 35 | return fixture_func 36 | 37 | 38 | def create_destructured_parametrized_lambda(name: str, source_name: str, index: int): 39 | source = _DESTRUCTURED_PARAMETRIZED_LAMBDA_FORMAT.format( 40 | name=name, source_name=source_name, index=index 41 | ) 42 | context: dict[str, Any] = {} 43 | exec(source, context) 44 | 45 | fixture_func = context[name] 46 | return fixture_func 47 | 48 | 49 | VT = TypeVar('VT') 50 | 51 | 52 | class LambdaFixture(Generic[VT], wrapt.ObjectProxy): 53 | # NOTE: pytest won't apply marks unless the markee has a __call__ and a 54 | # __name__ defined. 55 | __name__ = '' 56 | 57 | _self_iter: Iterable | None 58 | _self_params_source: LambdaFixture | None 59 | 60 | def __init__( 61 | self, 62 | fixture_names_or_lambda, 63 | *, 64 | bind: bool = False, 65 | async_: bool = False, 66 | _params_source: Optional['LambdaFixture'] = None, 67 | **fixture_kwargs, 68 | ): 69 | self.bind = bind 70 | self.is_async = async_ 71 | self.fixture_kwargs = cast(LambdaFixtureKwargs, fixture_kwargs) 72 | self.fixture_func = self._not_implemented 73 | self.has_fixture_func = False 74 | self.parent = None 75 | self._self_iter = None 76 | self._self_params_source = _params_source 77 | 78 | #: pytest fixture info definition 79 | self._pytestfixturefunction = pytest.fixture(**fixture_kwargs) 80 | 81 | # Instruct pytest not to unwrap our fixture down to its original lambda, but 82 | # instead treat the LambdaFixture as the fixture function. 83 | self.__pytest_wrapped__ = _PytestWrapper(self) 84 | 85 | if fixture_names_or_lambda is not None: 86 | supports_iter = ( 87 | not callable(fixture_names_or_lambda) 88 | and not isinstance(fixture_names_or_lambda, str) 89 | and isinstance(fixture_names_or_lambda, Iterable) 90 | ) 91 | if supports_iter: 92 | fixture_names_or_lambda = tuple(fixture_names_or_lambda) 93 | 94 | self.set_fixture_func(fixture_names_or_lambda) 95 | 96 | if supports_iter: 97 | self._self_iter = map( 98 | lambda name: LambdaFixture(name), 99 | fixture_names_or_lambda, 100 | ) 101 | 102 | elif fixture_kwargs.get('params'): 103 | # Shortcut to allow `lambda_fixture(params=[1,2,3])` 104 | self.set_fixture_func(lambda request: request.param) 105 | 106 | params = fixture_kwargs['params'] = tuple(fixture_kwargs['params']) 107 | self._self_iter = _LambdaFixtureParametrizedIterator(self, params) 108 | 109 | def __call__(self, *args, **kwargs) -> VT: 110 | if self.bind: 111 | args = (self.parent,) + args 112 | return self.fixture_func(*args, **kwargs) 113 | 114 | def __iter__(self): 115 | if self._self_iter: 116 | return iter(self._self_iter) 117 | 118 | def _not_implemented(self): 119 | raise NotImplementedError( 120 | 'The fixture_func for this LambdaFixture has not been defined. ' 121 | 'This is a catastrophic error!') 122 | 123 | def set_fixture_func(self, fixture_names_or_lambda): 124 | self.fixture_func = self.build_fixture_func(fixture_names_or_lambda) 125 | self.has_fixture_func = True 126 | 127 | # NOTE: this initializes the ObjectProxy 128 | super().__init__(self.fixture_func) 129 | 130 | def build_fixture_func(self, fixture_names_or_lambda): 131 | if callable(fixture_names_or_lambda): 132 | real_fixture_func = fixture_names_or_lambda 133 | 134 | # We create a new method with the same signature as the passed 135 | # method, which simply calls the passed method – this is so we can 136 | # modify __name__ and other properties of the function without fear 137 | # of overwriting functions unrelated to the fixture. (A lambda need 138 | # not be used – a method imported from another module can be used.) 139 | 140 | if self.is_async: 141 | @functools.wraps(real_fixture_func) 142 | async def insulator(*args, **kwargs): 143 | val = real_fixture_func(*args, **kwargs) 144 | if inspect.isawaitable(val): 145 | val = await val 146 | return val 147 | 148 | else: 149 | @functools.wraps(real_fixture_func) 150 | def insulator(*args, **kwargs): 151 | return real_fixture_func(*args, **kwargs) 152 | 153 | return insulator 154 | 155 | else: 156 | if self.bind: 157 | raise ValueError( 158 | 'bind must be False if requesting a fixture by name') 159 | 160 | fixture_names = fixture_names_or_lambda 161 | if isinstance(fixture_names, str): 162 | fixture_names = (fixture_names,) 163 | 164 | # Create a new method with the requested parameter, so pytest can 165 | # determine its dependencies at parse time. If we instead use 166 | # request.getfixturevalue, pytest won't know to include the fixture 167 | # in its dependency graph, and will vomit with "The requested 168 | # fixture has no parameter defined for the current test." 169 | name = 'fixture__' + '__'.join(fixture_names) # XXX: will this conflict in certain circumstances? 170 | return create_identity_lambda(name, *fixture_names) 171 | 172 | def contribute_to_parent(self, parent: Union[type, ModuleType], name: str, **kwargs): 173 | """Set up the LambdaFixture for the given class/module 174 | 175 | This method is called during collection, when a LambdaFixture is 176 | encountered in a module or class. This method is responsible for saving 177 | any names and setting any attributes on parent as necessary. 178 | """ 179 | is_in_class = isinstance(parent, type) 180 | is_in_module = isinstance(parent, ModuleType) 181 | assert is_in_class or is_in_module 182 | 183 | if is_in_module and self.bind: 184 | source_location = getattr(parent, '__file__', 'the parent') 185 | raise ValueError(f'bind=True cannot be used at the module level. ' 186 | f'Please remove this arg in the {name} fixture in {source_location}') 187 | 188 | if self._self_params_source: 189 | self.set_fixture_func(self._not_implemented) 190 | 191 | elif not self.has_fixture_func: 192 | # If no fixture definition was passed to lambda_fixture, it's our 193 | # responsibility to define it as the name of the attribute. This is 194 | # handy if ya just wanna force a fixture to be used, e.g.: 195 | # do_the_thing = lambda_fixture(autouse=True) 196 | self.set_fixture_func(name) 197 | 198 | self.__name__ = self.fixture_func.__name__ = name 199 | self.__module__ = self.fixture_func.__module__ = ( 200 | parent.__module__ if is_in_class else parent.__name__) 201 | self.parent = parent 202 | 203 | # With --doctest-modules enabled, the doctest finder will enumerate all objects 204 | # in all relevant modules, and use `isinstance(obj, ...)` to determine whether 205 | # the object has doctests to collect. Under the hood, isinstance retrieves the 206 | # value of the `obj.__class__` attribute. 207 | # 208 | # When using implicit referential lambda fixtures (e.g. `name = lambda_fixture()`), 209 | # the LambdaFixture object doesn't initialize its underlying object proxy until the 210 | # pytest collection phase. Unfortunately, doctest's scanning occurs before this. 211 | # When doctest attempts `isinstance(lfix, ...)` on an implicit referential 212 | # lambda fixture and accesses `__class__`, the object proxy tries to curry 213 | # the access to its wrapped object — but there isn't one, so it raises an error. 214 | # 215 | # To address this, we override __class__ to return LambdaFixture when the 216 | # object proxy has not yet been initialized. 217 | 218 | def _get__class__(self): 219 | try: 220 | self.__wrapped__ 221 | except ValueError: 222 | return LambdaFixture 223 | else: 224 | return self.__wrapped__.__class__ 225 | 226 | def _set__class__(self, val): 227 | self.__wrapped__.__class__ = val 228 | 229 | # NOTE: @property is avoided on __class__, as it interfered with the PyCharm/pydev debugger 230 | __class__ = property(_get__class__, _set__class__) # type: ignore[assignment] 231 | del _get__class__ 232 | del _set__class__ 233 | 234 | # These properties are required in order to expose attributes stored on the 235 | # LambdaFixture proxying instance without prefixing them with _self_ 236 | 237 | @property 238 | def bind(self) -> bool: return self._self_bind 239 | @bind.setter 240 | def bind(self, value: bool) -> None: self._self_bind = value 241 | 242 | @property 243 | def is_async(self) -> bool: return self._self_is_async 244 | @is_async.setter 245 | def is_async(self, value: bool) -> None: self._self_is_async = value 246 | 247 | @property 248 | def fixture_kwargs(self) -> LambdaFixtureKwargs: return self._self_fixture_kwargs 249 | @fixture_kwargs.setter 250 | def fixture_kwargs(self, value) -> None: self._self_fixture_kwargs = value 251 | 252 | @property 253 | def fixture_func(self): return self._self_fixture_func 254 | @fixture_func.setter 255 | def fixture_func(self, value) -> None: self._self_fixture_func = value 256 | 257 | @property 258 | def has_fixture_func(self) -> bool: return self._self_has_fixture_func 259 | @has_fixture_func.setter 260 | def has_fixture_func(self, value: bool) -> None: self._self_has_fixture_func = value 261 | 262 | @property 263 | def parent(self) -> type | ModuleType | None: return self._self_parent 264 | @parent.setter 265 | def parent(self, value: type | ModuleType): self._self_parent = value 266 | 267 | @property 268 | def _pytestfixturefunction(self) -> bool: return self._self__pytestfixturefunction 269 | @_pytestfixturefunction.setter 270 | def _pytestfixturefunction(self, value: bool) -> None: self._self__pytestfixturefunction = value 271 | 272 | @property 273 | def __pytest_wrapped__(self) -> _PytestWrapper: return self._self___pytest_wrapped__ 274 | @__pytest_wrapped__.setter 275 | def __pytest_wrapped__(self, value: _PytestWrapper) -> None: self._self___pytest_wrapped__ = value 276 | 277 | 278 | class _LambdaFixtureParametrizedIterator: 279 | def __init__(self, source: LambdaFixture, params: Iterable): 280 | self.source = source 281 | self.params = tuple(params) 282 | 283 | self.num_params = self._get_param_set_length(self.params[0]) if self.params else 0 284 | self.destructured: List[LambdaFixture] = [] 285 | 286 | def __iter__(self): 287 | if self.destructured: 288 | raise RuntimeError('Lambda fixtures may only be destructured once.') 289 | 290 | for i in range(self.num_params): 291 | child = LambdaFixture(None, _params_source=self.source) 292 | self.destructured.append(child) 293 | yield child 294 | 295 | @property 296 | def child_names(self) -> Tuple[str, ...]: 297 | return tuple(child.__name__ for child in self.destructured) 298 | 299 | @staticmethod 300 | def _get_param_set_length(param: Union[ParameterSet, Iterable, Any]) -> int: 301 | if isinstance(param, ParameterSet): 302 | return len(param.values) 303 | elif isinstance(param, Sized) and not isinstance(param, (str, bytes)): 304 | return len(param) 305 | else: 306 | return 1 307 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "colorama" 5 | version = "0.4.6" 6 | description = "Cross-platform colored terminal text." 7 | optional = false 8 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 9 | files = [ 10 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 11 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 12 | ] 13 | 14 | [[package]] 15 | name = "distlib" 16 | version = "0.3.8" 17 | description = "Distribution utilities" 18 | optional = false 19 | python-versions = "*" 20 | files = [ 21 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 22 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 23 | ] 24 | 25 | [[package]] 26 | name = "exceptiongroup" 27 | version = "1.2.1" 28 | description = "Backport of PEP 654 (exception groups)" 29 | optional = false 30 | python-versions = ">=3.7" 31 | files = [ 32 | {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, 33 | {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, 34 | ] 35 | 36 | [package.extras] 37 | test = ["pytest (>=6)"] 38 | 39 | [[package]] 40 | name = "filelock" 41 | version = "3.14.0" 42 | description = "A platform independent file lock." 43 | optional = false 44 | python-versions = ">=3.8" 45 | files = [ 46 | {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, 47 | {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, 48 | ] 49 | 50 | [package.extras] 51 | docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 52 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] 53 | typing = ["typing-extensions (>=4.8)"] 54 | 55 | [[package]] 56 | name = "iniconfig" 57 | version = "2.0.0" 58 | description = "brain-dead simple config-ini parsing" 59 | optional = false 60 | python-versions = ">=3.7" 61 | files = [ 62 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 63 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 64 | ] 65 | 66 | [[package]] 67 | name = "markdown-it-py" 68 | version = "3.0.0" 69 | description = "Python port of markdown-it. Markdown parsing, done right!" 70 | optional = false 71 | python-versions = ">=3.8" 72 | files = [ 73 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, 74 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, 75 | ] 76 | 77 | [package.dependencies] 78 | mdurl = ">=0.1,<1.0" 79 | 80 | [package.extras] 81 | benchmarking = ["psutil", "pytest", "pytest-benchmark"] 82 | code-style = ["pre-commit (>=3.0,<4.0)"] 83 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] 84 | linkify = ["linkify-it-py (>=1,<3)"] 85 | plugins = ["mdit-py-plugins"] 86 | profiling = ["gprof2dot"] 87 | rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] 88 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] 89 | 90 | [[package]] 91 | name = "mdurl" 92 | version = "0.1.2" 93 | description = "Markdown URL utilities" 94 | optional = false 95 | python-versions = ">=3.7" 96 | files = [ 97 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 98 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 99 | ] 100 | 101 | [[package]] 102 | name = "mypy" 103 | version = "0.971" 104 | description = "Optional static typing for Python" 105 | optional = false 106 | python-versions = ">=3.6" 107 | files = [ 108 | {file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"}, 109 | {file = "mypy-0.971-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5"}, 110 | {file = "mypy-0.971-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3"}, 111 | {file = "mypy-0.971-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655"}, 112 | {file = "mypy-0.971-cp310-cp310-win_amd64.whl", hash = "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103"}, 113 | {file = "mypy-0.971-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca"}, 114 | {file = "mypy-0.971-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417"}, 115 | {file = "mypy-0.971-cp36-cp36m-win_amd64.whl", hash = "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09"}, 116 | {file = "mypy-0.971-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8"}, 117 | {file = "mypy-0.971-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0"}, 118 | {file = "mypy-0.971-cp37-cp37m-win_amd64.whl", hash = "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2"}, 119 | {file = "mypy-0.971-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27"}, 120 | {file = "mypy-0.971-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856"}, 121 | {file = "mypy-0.971-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71"}, 122 | {file = "mypy-0.971-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27"}, 123 | {file = "mypy-0.971-cp38-cp38-win_amd64.whl", hash = "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58"}, 124 | {file = "mypy-0.971-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6"}, 125 | {file = "mypy-0.971-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe"}, 126 | {file = "mypy-0.971-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9"}, 127 | {file = "mypy-0.971-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf"}, 128 | {file = "mypy-0.971-cp39-cp39-win_amd64.whl", hash = "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0"}, 129 | {file = "mypy-0.971-py3-none-any.whl", hash = "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9"}, 130 | {file = "mypy-0.971.tar.gz", hash = "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56"}, 131 | ] 132 | 133 | [package.dependencies] 134 | mypy-extensions = ">=0.4.3" 135 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 136 | typing-extensions = ">=3.10" 137 | 138 | [package.extras] 139 | dmypy = ["psutil (>=4.0)"] 140 | python2 = ["typed-ast (>=1.4.0,<2)"] 141 | reports = ["lxml"] 142 | 143 | [[package]] 144 | name = "mypy-extensions" 145 | version = "1.0.0" 146 | description = "Type system extensions for programs checked with the mypy type checker." 147 | optional = false 148 | python-versions = ">=3.5" 149 | files = [ 150 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 151 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 152 | ] 153 | 154 | [[package]] 155 | name = "packaging" 156 | version = "24.0" 157 | description = "Core utilities for Python packages" 158 | optional = false 159 | python-versions = ">=3.7" 160 | files = [ 161 | {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, 162 | {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, 163 | ] 164 | 165 | [[package]] 166 | name = "platformdirs" 167 | version = "4.2.2" 168 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 169 | optional = false 170 | python-versions = ">=3.8" 171 | files = [ 172 | {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, 173 | {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, 174 | ] 175 | 176 | [package.extras] 177 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 178 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 179 | type = ["mypy (>=1.8)"] 180 | 181 | [[package]] 182 | name = "pluggy" 183 | version = "1.5.0" 184 | description = "plugin and hook calling mechanisms for python" 185 | optional = false 186 | python-versions = ">=3.8" 187 | files = [ 188 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 189 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 190 | ] 191 | 192 | [package.extras] 193 | dev = ["pre-commit", "tox"] 194 | testing = ["pytest", "pytest-benchmark"] 195 | 196 | [[package]] 197 | name = "py" 198 | version = "1.11.0" 199 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 200 | optional = false 201 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 202 | files = [ 203 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 204 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 205 | ] 206 | 207 | [[package]] 208 | name = "pytest" 209 | version = "8.2.1" 210 | description = "pytest: simple powerful testing with Python" 211 | optional = false 212 | python-versions = ">=3.8" 213 | files = [ 214 | {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, 215 | {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, 216 | ] 217 | 218 | [package.dependencies] 219 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 220 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 221 | iniconfig = "*" 222 | packaging = "*" 223 | pluggy = ">=1.5,<2.0" 224 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 225 | 226 | [package.extras] 227 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 228 | 229 | [[package]] 230 | name = "pytest-asyncio" 231 | version = "0.23.7" 232 | description = "Pytest support for asyncio" 233 | optional = false 234 | python-versions = ">=3.8" 235 | files = [ 236 | {file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"}, 237 | {file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"}, 238 | ] 239 | 240 | [package.dependencies] 241 | pytest = ">=7.0.0,<9" 242 | 243 | [package.extras] 244 | docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] 245 | testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] 246 | 247 | [[package]] 248 | name = "pytest-markdown-docs" 249 | version = "0.5.1" 250 | description = "Run markdown code fences through pytest" 251 | optional = false 252 | python-versions = ">=3.8,<4.0" 253 | files = [ 254 | {file = "pytest_markdown_docs-0.5.1-py3-none-any.whl", hash = "sha256:29849ae0fccb4dce27f86ee82d942a234095804d6f6ef6157473d38a6903aab3"}, 255 | {file = "pytest_markdown_docs-0.5.1.tar.gz", hash = "sha256:acf9b678b589779e817f740bb1c8c4984a1bdca23ddc1c68ea6377918f5a871c"}, 256 | ] 257 | 258 | [package.dependencies] 259 | markdown-it-py = ">=2.2.0,<4.0" 260 | pytest = ">=7.0.0" 261 | 262 | [[package]] 263 | name = "six" 264 | version = "1.16.0" 265 | description = "Python 2 and 3 compatibility utilities" 266 | optional = false 267 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 268 | files = [ 269 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 270 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 271 | ] 272 | 273 | [[package]] 274 | name = "tomli" 275 | version = "2.0.1" 276 | description = "A lil' TOML parser" 277 | optional = false 278 | python-versions = ">=3.7" 279 | files = [ 280 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 281 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 282 | ] 283 | 284 | [[package]] 285 | name = "tox" 286 | version = "3.28.0" 287 | description = "tox is a generic virtualenv management and test command line tool" 288 | optional = false 289 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 290 | files = [ 291 | {file = "tox-3.28.0-py2.py3-none-any.whl", hash = "sha256:57b5ab7e8bb3074edc3c0c0b4b192a4f3799d3723b2c5b76f1fa9f2d40316eea"}, 292 | {file = "tox-3.28.0.tar.gz", hash = "sha256:d0d28f3fe6d6d7195c27f8b054c3e99d5451952b54abdae673b71609a581f640"}, 293 | ] 294 | 295 | [package.dependencies] 296 | colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} 297 | filelock = ">=3.0.0" 298 | packaging = ">=14" 299 | pluggy = ">=0.12.0" 300 | py = ">=1.4.17" 301 | six = ">=1.14.0" 302 | tomli = {version = ">=2.0.1", markers = "python_version >= \"3.7\" and python_version < \"3.11\""} 303 | virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" 304 | 305 | [package.extras] 306 | docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] 307 | testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] 308 | 309 | [[package]] 310 | name = "typing-extensions" 311 | version = "4.12.0" 312 | description = "Backported and Experimental Type Hints for Python 3.8+" 313 | optional = false 314 | python-versions = ">=3.8" 315 | files = [ 316 | {file = "typing_extensions-4.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"}, 317 | {file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"}, 318 | ] 319 | 320 | [[package]] 321 | name = "virtualenv" 322 | version = "20.26.2" 323 | description = "Virtual Python Environment builder" 324 | optional = false 325 | python-versions = ">=3.7" 326 | files = [ 327 | {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, 328 | {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, 329 | ] 330 | 331 | [package.dependencies] 332 | distlib = ">=0.3.7,<1" 333 | filelock = ">=3.12.2,<4" 334 | platformdirs = ">=3.9.1,<5" 335 | 336 | [package.extras] 337 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 338 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 339 | 340 | [[package]] 341 | name = "wrapt" 342 | version = "1.16.0" 343 | description = "Module for decorators, wrappers and monkey patching." 344 | optional = false 345 | python-versions = ">=3.6" 346 | files = [ 347 | {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, 348 | {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, 349 | {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, 350 | {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, 351 | {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, 352 | {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, 353 | {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, 354 | {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, 355 | {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, 356 | {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, 357 | {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, 358 | {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, 359 | {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, 360 | {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, 361 | {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, 362 | {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, 363 | {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, 364 | {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, 365 | {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, 366 | {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, 367 | {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, 368 | {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, 369 | {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, 370 | {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, 371 | {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, 372 | {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, 373 | {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, 374 | {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, 375 | {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, 376 | {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, 377 | {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, 378 | {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, 379 | {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, 380 | {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, 381 | {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, 382 | {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, 383 | {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, 384 | {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, 385 | {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, 386 | {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, 387 | {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, 388 | {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, 389 | {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, 390 | {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, 391 | {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, 392 | {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, 393 | {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, 394 | {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, 395 | {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, 396 | {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, 397 | {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, 398 | {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, 399 | {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, 400 | {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, 401 | {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, 402 | {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, 403 | {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, 404 | {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, 405 | {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, 406 | {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, 407 | {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, 408 | {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, 409 | {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, 410 | {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, 411 | {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, 412 | {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, 413 | {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, 414 | {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, 415 | {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, 416 | {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, 417 | ] 418 | 419 | [metadata] 420 | lock-version = "2.0" 421 | python-versions = "^3.8.0" 422 | content-hash = "9c4ae4ffd67ee1cd1d0dcdb76501a8963053e06254d3ae89f206a55b17c8a224" 423 | --------------------------------------------------------------------------------