├── test ├── ext │ ├── __init__.py │ ├── test_flake8.py │ ├── test_pylint.py │ └── base_lint.py └── test_plugin.py ├── pytest_only ├── ext │ ├── __init__.py │ ├── pylint.py │ └── flake8.py ├── __init__.py ├── version.py ├── compat.py └── plugin.py ├── .gitignore ├── pytest.ini ├── _tox_install_command.sh ├── pyproject.toml ├── CHANGELOG.md ├── tox.ini ├── README.rst └── poetry.lock /test/ext/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pytest_only/ext/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pytest_only/__init__.py: -------------------------------------------------------------------------------- 1 | from . import plugin 2 | from .version import __version__ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .cache 3 | *.egg-info/ 4 | dist/ 5 | 6 | .idea/ 7 | 8 | # testing 9 | .pytest_cache 10 | .tox 11 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -p no:only --tb=short -vv 3 | 4 | python_classes = Test* Describe* Context* Case* 5 | python_functions = test_* it_* its_* test 6 | python_files = tests.py test_*.py 7 | -------------------------------------------------------------------------------- /pytest_only/version.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | 3 | try: 4 | __version__ = pkg_resources.get_distribution('pytest-only').version 5 | except pkg_resources.DistributionNotFound: 6 | __version__ = 'dev' 7 | -------------------------------------------------------------------------------- /_tox_install_command.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Virtualenvs created in 3.12+ may malfunction with older setuptools 4 | pip install -U setuptools 5 | 6 | poetry install -v \ 7 | && poetry run pip install --no-warn-conflicts "$@" 8 | -------------------------------------------------------------------------------- /pytest_only/compat.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | try: 4 | from _pytest.nodes import Node 5 | except ImportError: 6 | from _pytest.main import Node 7 | 8 | 9 | if hasattr(Node, 'get_closest_marker'): 10 | def get_closest_marker(item: Node, *args, **kwargs): 11 | return item.get_closest_marker(*args, **kwargs) 12 | elif hasattr(Node, 'get_marker'): 13 | def get_closest_marker(item: Node, *args, **kwargs): 14 | return item.get_marker(*args, **kwargs) 15 | else: 16 | raise RuntimeError( 17 | 'Unable to determine get_closest_marker alternative ' 18 | 'for pytest version {}'.format(pytest.__version__) 19 | ) 20 | -------------------------------------------------------------------------------- /pytest_only/plugin.py: -------------------------------------------------------------------------------- 1 | from .compat import get_closest_marker 2 | 3 | 4 | def pytest_addoption(parser): 5 | parser.addoption('--only', dest='enable_only', 6 | default=True, action='store_true', 7 | help='Only run tests with the "only" marker') 8 | 9 | parser.addoption('--no-only', dest='enable_only', 10 | action='store_false', 11 | help='Disable --only filtering') 12 | 13 | 14 | def pytest_configure(config): 15 | config.addinivalue_line( 16 | "markers", "only: normal runs will execute only marked tests" 17 | ) 18 | 19 | 20 | def pytest_collection_modifyitems(config, items): 21 | if not config.getoption('--only'): 22 | return 23 | 24 | only, other = [], [] 25 | for item in items: 26 | l = only if get_closest_marker(item, 'only') else other 27 | l.append(item) 28 | 29 | if only: 30 | items[:] = only 31 | if other: 32 | config.hook.pytest_deselected(items=other) 33 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = 'pytest-only' 3 | version = "2.1.2" 4 | description = 'Use @pytest.mark.only to run a single test' 5 | authors = ['Zach Kanzler '] 6 | license = 'MIT' 7 | 8 | readme = 'README.rst' 9 | 10 | repository = 'https://github.com/theY4Kman/pytest-only' 11 | homepage = 'https://github.com/theY4Kman/pytest-only' 12 | 13 | keywords = ['pytest'] 14 | classifiers=[ 15 | 'Development Status :: 5 - Production/Stable', 16 | 'Programming Language :: Python', 17 | 'Framework :: Pytest', 18 | 'License :: OSI Approved :: MIT License', 19 | 'Topic :: Software Development :: Testing', 20 | ] 21 | 22 | [tool.poetry.dependencies] 23 | python = '^3.8' 24 | pytest = '>=3.6.0,<9' 25 | 26 | [tool.poetry.group.dev.dependencies] 27 | flake8 = [ 28 | {version = '5.0.4', python = '>=3.8,<3.8.1'}, 29 | {version = '^7.0.0', python = '>=3.8.1'}, 30 | ] 31 | pylint = "^2.13.9" 32 | pytest-common-subject = "^1.0.6" 33 | pytest-pylint = "^0.21.0" 34 | tox = '^3.25.0' 35 | 36 | [tool.poetry.plugins."pytest11"] 37 | only = "pytest_only.plugin" 38 | 39 | [tool.poetry.plugins.'flake8.extension'] 40 | PTO = 'pytest_only.ext.flake8:PytestOnlyMarkChecker' 41 | 42 | [build-system] 43 | requires = ['poetry-core>=1.0.0'] 44 | build-backend = 'poetry.core.masonry.api' 45 | -------------------------------------------------------------------------------- /test/ext/test_flake8.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from typing import Callable 3 | 4 | import pytest 5 | from pytest_lambda import static_fixture 6 | 7 | from pytest_only.ext.flake8 import PytestOnlyMarkVisitor 8 | from .base_lint import BaseLintTest 9 | 10 | 11 | class DescribeFlake8Plugin(BaseLintTest): 12 | focused_warning_text = static_fixture('PTO01') 13 | 14 | @pytest.fixture 15 | def execute_linter(self, testdir, source_file) -> Callable[[], str]: 16 | """Run the linter and return the captured stdout""" 17 | 18 | def run_linter_test() -> str: 19 | result = testdir.run('flake8', '.') 20 | return result.stdout.str() 21 | 22 | return run_linter_test 23 | 24 | 25 | class DescribePytestOnlyMarkVisitor(BaseLintTest): 26 | focused_warning_text = static_fixture('PTO01') 27 | 28 | @pytest.fixture 29 | def execute_linter(self, dedented_source) -> Callable[[], str]: 30 | """Run the linter and return the captured stdout""" 31 | 32 | def run_linter_test() -> str: 33 | tree = ast.parse(dedented_source) 34 | visitor = PytestOnlyMarkVisitor() 35 | visitor.visit(tree) 36 | return '\n'.join( 37 | f'{lineno}:{col_offset} {msg}' 38 | for lineno, col_offset, msg in visitor.errors 39 | ) 40 | 41 | return run_linter_test 42 | -------------------------------------------------------------------------------- /test/ext/test_pylint.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | import pytest 4 | from pytest_lambda import static_fixture 5 | 6 | from .base_lint import BaseLintTest 7 | 8 | 9 | class DescribePylint(BaseLintTest): 10 | focused_warning_text = static_fixture('unexpected-focused') 11 | 12 | @pytest.fixture(autouse=True) 13 | def pylint_rc(self, testdir): 14 | return testdir.makefile( 15 | ext='.rc', 16 | pylint=''' 17 | [MASTER] 18 | load-plugins=pytest_only.ext.pylint 19 | 20 | [MESSAGES CONTROL] 21 | enable=unexpected-focused 22 | ''' 23 | ) 24 | 25 | @pytest.fixture 26 | def execute_linter(self, testdir, pylint_rc, source_file) -> Callable[[], str]: 27 | """Run the linter and return the captured stdout""" 28 | 29 | def run_linter_test() -> str: 30 | result = testdir.runpytest( 31 | # Run only the generated pylint "tests" 32 | '--pylint', 33 | 34 | # And don't load pytest-only, or it will deselect the pylint tests! 35 | '-p', 'no:only', 36 | 37 | # Explicitly define the pylint config file to use 38 | '--pylint-rcfile', pylint_rc.strpath, 39 | ) 40 | return result.stdout.str() 41 | 42 | return run_linter_test 43 | -------------------------------------------------------------------------------- /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.1.2] — 2024-05-27 12 | ### Changed 13 | - Drop support for Python 3.7 (now EOL) 14 | - Add support for pytest versions 8.1 and 8.2 15 | - Alter pytest version pin to disallow 9.x+ versions (to prevent unforeseen incompatibilities) 16 | 17 | 18 | ## [2.1.1] — 2024-03-09 19 | ### Fixed 20 | - Resolve brittle handling of class- and module-level assignments in flake8 plugin 21 | 22 | 23 | ## [2.1.0] — 2024-03-08 24 | ### Added 25 | - Add [flake8](https://github.com/PyCQA/flake8) plugin to detect `only` marks before they get committed (see [GH#11](https://github.com/theY4Kman/pytest-only/issues/11)) 26 | 27 | 28 | ## [2.0.0] — 2022-06-14 29 | ### Added 30 | - Added [pylint](https://pylint.pycqa.org) plugin to detect `only` marks before they get committed (thank you, [@nikolaik](https://github.com/nikolaik) — [GH#10](https://github.com/theY4Kman/pytest-only/pull/10)) 31 | 32 | ### Changed 33 | - Remove support for Python 2.7 34 | 35 | 36 | ## [1.2.2] — 2020-01-18 37 | ### Fixed 38 | - Register the `only` mark to avoid warning messages emitted since 4.5.0 (thank you, [@nicoddemus](https://github.com/nicoddemus) – [GH#8](https://github.com/theY4Kman/pytest-only/pull/8)) 39 | 40 | 41 | ## [1.2.1] — 2019-01-09 42 | ### Fixed 43 | - Fix get_marker usage for compatibility with pytest 4.1.0 (see https://github.com/pytest-dev/pytest/issues/4546) 44 | 45 | 46 | ## [1.2.0] — 2018-12-23 47 | ### Added 48 | - Add `--no-only` and `--only` cmd-line options to disable and enable plugin functionality 49 | 50 | 51 | ## [1.1.0] — 2017-03-28 52 | ### Fixed 53 | - Call `pytest_deselected` with deselected tests 54 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{ 38, 39}-pytest{36,39,40,44,45,46,50,51,52,53,54,60,61,62,70,71} 4 | py{ 310,311}-pytest{ 62,70,71,72,73,74,80,81,82} 5 | py{ 312}-pytest{ 73,74,80,81,82} 6 | 7 | 8 | [testenv] 9 | whitelist_externals = poetry 10 | 11 | # We'll use `poetry install` to install the package; don't install the package, 12 | # which will likely override the pytest version we wish to use for the env. 13 | skip_install = true 14 | 15 | # Avoid keyring errors when installing packages in tox envs 16 | setenv = 17 | PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring 18 | 19 | commands = poetry run pytest 20 | 21 | install_command = 22 | {toxinidir}/_tox_install_command.sh {opts} {packages} 23 | 24 | deps = 25 | pytest36: pytest~=3.6.0 26 | pytest39: pytest~=3.9.0 27 | pytest40: pytest~=4.0.0 28 | pytest44: pytest~=4.4.0 29 | pytest45: pytest~=4.5.0 30 | pytest46: pytest~=4.6.0 31 | pytest50: pytest~=5.0.0 32 | pytest51: pytest~=5.1.0 33 | pytest52: pytest~=5.2.0 34 | pytest53: pytest~=5.3.0 35 | pytest54: pytest~=5.4.0 36 | pytest60: pytest~=6.0.0 37 | pytest61: pytest~=6.1.0 38 | pytest62: pytest~=6.2.0 39 | pytest70: pytest~=7.0.0 40 | pytest71: pytest~=7.1.0 41 | pytest72: pytest~=7.2.0 42 | pytest73: pytest~=7.3.0 43 | pytest74: pytest~=7.4.0 44 | pytest80: pytest~=8.0.0 45 | pytest81: pytest~=8.1.0 46 | pytest82: pytest~=8.2.0 47 | 48 | # NOTE: the attrs dep resolves an issue with pytest 4.0 and attrs>19.2.0 49 | # see https://stackoverflow.com/a/58189684/148585 50 | pytest40: attrs==19.1.0 51 | 52 | # Older versions of pytest require older versions of pytest-pylint 53 | pytest{36,39,40,44,45,46}: pytest-pylint~=0.14.1 54 | pytest{50,51,52,53}: pytest-pylint~=0.15.1 55 | pytest{54}: pytest-pylint~=0.16.1 56 | pytest{60,61,62}: pytest-pylint~=0.18.0 57 | pytest{80}: pytest-pylint>=0.21.0 58 | -------------------------------------------------------------------------------- /pytest_only/ext/pylint.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union 2 | 3 | from astroid import nodes 4 | from pylint.checkers.base_checker import BaseChecker 5 | from pylint.interfaces import IAstroidChecker 6 | from pylint.lint import PyLinter 7 | 8 | 9 | def is_only_mark(node: nodes.NodeNG) -> bool: 10 | return ( 11 | isinstance(node, nodes.Attribute) 12 | and node.attrname == 'only' 13 | and isinstance(node.expr, nodes.Attribute) 14 | and node.expr.attrname == 'mark' 15 | ) 16 | 17 | 18 | def get_only_mark_decorators(decorators: nodes.Decorators) -> List[nodes.NodeNG]: 19 | return [node for node in decorators.nodes if is_only_mark(node)] 20 | 21 | 22 | def get_only_mark_pytestmarks( 23 | pytestmark: Union[nodes.AssignName, List[nodes.NodeNG]] 24 | ) -> List[nodes.NodeNG]: 25 | if isinstance(pytestmark, list): 26 | if len(pytestmark) != 1: 27 | return [] 28 | 29 | pytestmark = pytestmark[0] 30 | if not isinstance(pytestmark, nodes.AssignName): 31 | return [] 32 | 33 | assigned_stmts = tuple(pytestmark.assigned_stmts()) 34 | if len(assigned_stmts) == 1: 35 | rhs = assigned_stmts[0] 36 | 37 | if isinstance(rhs, nodes.List): 38 | all_marks = rhs.elts 39 | else: 40 | all_marks = (rhs,) 41 | 42 | return [mark for mark in all_marks if is_only_mark(mark)] 43 | 44 | 45 | class PytestOnlyMarkChecker(BaseChecker): 46 | """Check for stray pytest.mark.only decorators""" 47 | 48 | __implements__ = IAstroidChecker 49 | 50 | name = 'pytest-only-mark' 51 | msgs = { 52 | 'W1650': ( 53 | 'Unexpected focused test(s) using pytest.mark.only: %s %s', 54 | 'unexpected-focused', 55 | 'Remove pytest.mark.only from test', 56 | ) 57 | } 58 | 59 | def visit_functiondef(self, node: nodes.FunctionDef) -> None: 60 | funcdef = node 61 | 62 | if not funcdef.decorators: 63 | return 64 | 65 | func_type = 'async def' if isinstance(funcdef, nodes.AsyncFunctionDef) else 'def' 66 | for node in get_only_mark_decorators(funcdef.decorators): 67 | self.add_message('unexpected-focused', args=(func_type, funcdef.name), node=node) 68 | 69 | visit_asyncfunctiondef = visit_functiondef 70 | 71 | def visit_classdef(self, node: nodes.ClassDef) -> None: 72 | classdef = node 73 | 74 | if classdef.decorators: 75 | for mark in get_only_mark_decorators(classdef.decorators): 76 | self.add_message('unexpected-focused', args=('class', classdef.name), node=mark) 77 | 78 | if 'pytestmark' in classdef.locals: 79 | for mark in get_only_mark_pytestmarks(classdef.locals['pytestmark']): 80 | self.add_message('unexpected-focused', args=('class', classdef.name), node=mark) 81 | 82 | def visit_module(self, node: nodes.Module) -> None: 83 | module = node 84 | 85 | if 'pytestmark' in module.locals: 86 | for mark in get_only_mark_pytestmarks(module.locals['pytestmark']): 87 | self.add_message('unexpected-focused', args=('module', module.name), node=mark) 88 | 89 | 90 | def register(linter: PyLinter) -> None: 91 | linter.register_checker(PytestOnlyMarkChecker(linter)) 92 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pytest-only 2 | =========== 3 | 4 | Only run tests marked with ``@pytest.mark.only``. If none are marked, all tests run as usual. 5 | 6 | Borrowed from `mocha `_. 7 | 8 | 9 | Installation 10 | ------------ 11 | 12 | .. code-block:: bash 13 | 14 | pip install pytest-only 15 | 16 | 17 | Usage 18 | ----- 19 | 20 | Use it on functions 21 | 22 | .. code-block:: python 23 | 24 | import pytest 25 | 26 | def test_that_will_not_run(): 27 | assert 0 28 | 29 | @pytest.mark.only 30 | def test_that_will_run(): 31 | assert 1 32 | 33 | 34 | .. code-block:: bash 35 | 36 | $ py.test -v test_example.py 37 | 38 | ============================= test session starts ============================== 39 | platform linux -- Python 3.6.1, pytest-3.0.7, py-1.4.33, pluggy-0.4.0 -- /tmp/example/bin/python3.6 40 | cachedir: .cache 41 | rootdir: /tmp/example, inifile: 42 | plugins: only-1.0.0 43 | collected 2 items 44 | 45 | test_example.py::test_that_will_run PASSED 46 | 47 | =========================== 1 passed in 0.00 seconds =========================== 48 | 49 | 50 | Or use it on classes 51 | 52 | .. code-block:: python 53 | 54 | import pytest 55 | 56 | class TestThatWillNotRun: 57 | def test_that_will_not_run(self): 58 | assert 0 59 | 60 | 61 | @pytest.mark.only 62 | class TestThatWillRun: 63 | def test_that_will_run(self): 64 | assert 1 65 | 66 | 67 | .. code-block:: bash 68 | 69 | $ py.test -v test_example.py 70 | 71 | ============================= test session starts ============================== 72 | platform linux -- Python 3.6.1, pytest-3.0.7, py-1.4.33, pluggy-0.4.0 -- /tmp/example/bin/python3.6 73 | cachedir: .cache 74 | rootdir: /tmp/example, inifile: 75 | plugins: only-1.0.0 76 | collected 2 items 77 | 78 | test_example.py::TestThatWillRun::test_that_will_run PASSED 79 | 80 | =========================== 1 passed in 0.00 seconds =========================== 81 | 82 | 83 | Or use it on modules 84 | 85 | .. code-block:: python 86 | 87 | # test_example.py 88 | import pytest 89 | 90 | pytestmark = pytest.mark.only 91 | 92 | def test_that_will_run(): 93 | assert 1 94 | 95 | 96 | .. code-block:: python 97 | 98 | # test_example2.py 99 | def test_that_will_not_run(): 100 | assert 0 101 | 102 | 103 | .. code-block:: bash 104 | 105 | $ py.test -v test_example.py test_example2.py 106 | 107 | ============================= test session starts ============================== 108 | platform linux -- Python 3.6.1, pytest-3.0.7, py-1.4.33, pluggy-0.4.0 -- /home/they4kman/.virtualenvs/tmp-53d5944c7c78d28/bin/python3.6 109 | cachedir: .cache 110 | rootdir: /home/they4kman/.virtualenvs/tmp-53d5944c7c78d28, inifile: 111 | plugins: only-1.0.0 112 | collected 2 items 113 | 114 | test_example.py::test_that_will_run PASSED 115 | 116 | =========================== 1 passed in 0.00 seconds =========================== 117 | 118 | 119 | 120 | Disable for single test run 121 | --------------------------- 122 | 123 | To run all the tests, regardless of whether ``@pytest.mark.only`` is used, pass 124 | the ``--no-only`` flag to pytest: 125 | 126 | .. code-block:: bash 127 | 128 | $ py.test --no-only 129 | 130 | 131 | If ``--no-only`` has already been passed (perhaps by way of ``addopts`` in 132 | *pytest.ini*), use the ``--only`` flag to re-enable it: 133 | 134 | .. code-block:: bash 135 | 136 | $ py.test --no-only --only 137 | 138 | 139 | Pylint checker 140 | -------------- 141 | 142 | If you use pylint, you can avoid committing stray `only` marks with the bundled plugin. Just enable the pylint checker in your plugins and enable the `unexpected-focused` rule. 143 | 144 | .. code-block:: ini 145 | 146 | [MASTER] 147 | load-plugins=pytest_only.ext.pylint 148 | 149 | [MESSAGES CONTROL] 150 | enable=unexpected-focused 151 | 152 | .. code-block:: console 153 | 154 | $ cat test_ninja.py 155 | import pytest 156 | 157 | @pytest.mark.only 158 | def test_ninja(): 159 | pass 160 | 161 | $ pylint test_ninja.py 162 | ************* Module mymain 163 | test_ninja.py:3:0: W1650: Unexpected focused test(s) using pytest.mark.only: def test_ninja (unexpected-focused) 164 | 165 | 166 | Development 167 | ----------- 168 | 169 | 1. Install the test/dev requirements using `Poetry `_ 170 | 171 | .. code-block:: bash 172 | 173 | poetry install 174 | 175 | 2. Run the tests 176 | 177 | .. code-block:: bash 178 | 179 | py.test 180 | 181 | 3. Run the tests on all currently-supported platforms 182 | 183 | .. code-block:: bash 184 | 185 | tox 186 | -------------------------------------------------------------------------------- /pytest_only/ext/flake8.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | from typing import Iterable, Tuple, List, Union, Optional 5 | 6 | 7 | class PytestOnlyMarkChecker: 8 | """Check for stray pytest.mark.only decorators""" 9 | 10 | def __init__(self, tree: ast.AST): 11 | self.tree = tree 12 | 13 | def run(self) -> Iterable[Tuple[int, int, str, PytestOnlyMarkVisitor]]: 14 | visitor = PytestOnlyMarkVisitor() 15 | visitor.visit(self.tree) 16 | for lineno, col_offset, msg in visitor.errors: 17 | yield lineno, col_offset, msg, self 18 | 19 | 20 | class PytestOnlyMarkVisitor(ast.NodeVisitor): 21 | def __init__(self): 22 | self.errors = [] 23 | 24 | def add_error(self, node: ast.AST, code: str, msg: str): 25 | self.errors.append((node.lineno, node.col_offset, f'{code}: {msg}')) 26 | 27 | def add_focused_error(self, node: ast.AST, node_type: str, name: str): 28 | self.add_error(node, 'PTO01', f'Unexpected focused test using pytest.mark.only: {node_type} {name}') 29 | 30 | def visit_FunctionDef(self, node: ast.FunctionDef): 31 | funcdef = node 32 | 33 | if not funcdef.decorator_list: 34 | return 35 | 36 | func_type = 'async def' if isinstance(funcdef, ast.AsyncFunctionDef) else 'def' 37 | 38 | for decorator in get_only_mark_decorators(funcdef.decorator_list): 39 | self.add_focused_error(decorator, func_type, funcdef.name) 40 | 41 | visit_AsyncFunctionDef = visit_FunctionDef 42 | 43 | def visit_ClassDef(self, node: ast.ClassDef) -> None: 44 | classdef = node 45 | 46 | if classdef.decorator_list: 47 | for mark in get_only_mark_decorators(classdef.decorator_list): 48 | self.add_focused_error(mark, 'class', classdef.name) 49 | 50 | for stmt in classdef.body: 51 | mark = get_pytestmark_assign_value(stmt) 52 | if not mark: 53 | self.visit(stmt) 54 | continue 55 | 56 | for mark in iter_only_mark_pytestmarks(mark): 57 | self.add_focused_error(mark, 'class', classdef.name) 58 | 59 | def visit_Module(self, node: ast.Module) -> None: 60 | module = node 61 | 62 | for stmt in module.body: 63 | mark = get_pytestmark_assign_value(stmt) 64 | if not mark: 65 | self.visit(stmt) 66 | continue 67 | 68 | for mark in iter_only_mark_pytestmarks(mark): 69 | self.add_focused_error(mark, 'module', '') 70 | 71 | 72 | def is_only_mark(node: ast.expr) -> bool: 73 | """Return whether an expression is a pytest.mark.only mark 74 | 75 | >>> parse_decorator = lambda s: ast.parse(s).body[0].decorator_list[0] 76 | >>> is_only_mark(parse_decorator('@pytest.mark.only\\ndef test_it(): pass')) 77 | True 78 | >>> is_only_mark(parse_decorator('@pytest.mark.only()\\ndef test_it(): pass')) 79 | True 80 | >>> is_only_mark(parse_decorator('@pytest.mark.muffin\\ndef test_it(): pass')) 81 | False 82 | """ 83 | if isinstance(node, ast.Call): 84 | mark = node.func 85 | elif isinstance(node, ast.Attribute): 86 | mark = node 87 | else: 88 | return False 89 | 90 | for attr in ('only', 'mark'): 91 | if not isinstance(mark, ast.Attribute) or mark.attr != attr: 92 | return False 93 | mark = mark.value 94 | 95 | return isinstance(mark, ast.Name) and mark.id == 'pytest' 96 | 97 | 98 | def get_only_mark_decorators(decorators: List[ast.expr]) -> List[ast.expr]: 99 | return [node for node in decorators if is_only_mark(node)] 100 | 101 | 102 | def iter_only_mark_pytestmarks( 103 | pytestmark: Union[ast.expr, ast.List] 104 | ) -> List[ast.expr]: 105 | elements = pytestmark.elts if isinstance(pytestmark, ast.List) else [pytestmark] 106 | for elt in elements: 107 | if is_only_mark(elt): 108 | yield elt 109 | 110 | 111 | def get_pytestmark_assign_value(stmt: ast.stmt) -> Optional[ast.expr]: 112 | if isinstance(stmt, ast.Assign): 113 | if len(stmt.targets) == 1: 114 | if hasattr(stmt.targets[0], 'elts'): 115 | targets = stmt.targets[0].elts 116 | values = getattr(stmt.value, 'elts', None) 117 | if values is None: 118 | values = (stmt.value,) * len(targets) 119 | else: 120 | targets = stmt.targets 121 | values = (stmt.value,) 122 | else: 123 | raise AssertionError('when does this happen?') 124 | 125 | for target, value in zip(targets, values): 126 | if isinstance(target, ast.Name) and target.id == 'pytestmark': 127 | return value 128 | -------------------------------------------------------------------------------- /test/test_plugin.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | 6 | pytest_plugins = 'pytester' 7 | 8 | 9 | @pytest.fixture(autouse=True) 10 | def setup_syspath(testdir): 11 | repo_dir = os.path.dirname(os.path.dirname(__file__)) 12 | testdir.syspathinsert(repo_dir) 13 | testdir.makeconftest('pytest_plugins = ["pytest_only.plugin"]') 14 | testdir.makeini('[pytest]\n' 15 | 'addopts = -p no:only') 16 | 17 | 18 | def assert_test_did_run(res, name): 19 | res.stdout.fnmatch_lines('*' + name + '*') 20 | 21 | 22 | def assert_test_did_not_run(res, name): 23 | with pytest.raises(pytest.fail.Exception): 24 | res.stdout.fnmatch_lines('*' + name + '*') 25 | 26 | 27 | def test_function(testdir): 28 | file = testdir.makepyfile(''' 29 | import pytest 30 | 31 | def test_should_not_run(): 32 | pass 33 | 34 | @pytest.mark.only 35 | def test_should_run(): 36 | pass 37 | 38 | def test_should_also_not_run(): 39 | pass 40 | ''') 41 | res = testdir.runpytest(file, '--verbose') 42 | outcomes = res.parseoutcomes() 43 | assert 'passed' in outcomes, 'Tests did not run successfully' 44 | assert outcomes['passed'] == 1, 'Incorrect number of tests passed' 45 | 46 | assert_test_did_run(res, 'test_should_run') 47 | assert_test_did_not_run(res, 'test_should_not_run') 48 | assert_test_did_not_run(res, 'test_should_also_not_run') 49 | 50 | 51 | def test_class(testdir): 52 | file = testdir.makepyfile(''' 53 | import pytest 54 | 55 | def test_should_not_run(): 56 | pass 57 | 58 | @pytest.mark.only 59 | class TestShouldRun: 60 | def test_should_run(self): 61 | pass 62 | 63 | def test_should_also_run(self): 64 | pass 65 | 66 | class TestShouldNotRun: 67 | def test_should_also_not_run(self): 68 | pass 69 | ''') 70 | res = testdir.runpytest(file, '--verbose') 71 | outcomes = res.parseoutcomes() 72 | assert 'passed' in outcomes, 'Tests did not run successfully' 73 | assert outcomes['passed'] == 2, 'Incorrect number of tests passed' 74 | 75 | assert_test_did_run(res, 'test_should_run') 76 | assert_test_did_run(res, 'test_should_also_run') 77 | assert_test_did_not_run(res, 'test_should_not_run') 78 | assert_test_did_not_run(res, 'test_should_also_not_run') 79 | 80 | 81 | def test_file(testdir): 82 | should_run = testdir.makepyfile(should_run=''' 83 | import pytest 84 | 85 | pytestmark = pytest.mark.only 86 | 87 | def test_should_run(): 88 | pass 89 | 90 | def test_should_also_run(): 91 | pass 92 | ''') 93 | 94 | should_not_run = testdir.makepyfile(should_not_run=''' 95 | def test_should_not_run(): 96 | pass 97 | ''') 98 | 99 | res = testdir.runpytest('--verbose', should_run, should_not_run) 100 | outcomes = res.parseoutcomes() 101 | assert 'passed' in outcomes, 'Tests did not run successfully' 102 | assert outcomes['passed'] == 2, 'Incorrect number of tests passed' 103 | 104 | assert_test_did_run(res, 'test_should_run') 105 | assert_test_did_run(res, 'test_should_also_run') 106 | assert_test_did_not_run(res, 'test_should_not_run') 107 | 108 | 109 | def test_no_only_cmdline_option(testdir): 110 | file = testdir.makepyfile(''' 111 | import pytest 112 | 113 | def test_should_run_as_well(): 114 | pass 115 | 116 | @pytest.mark.only 117 | def test_should_run(): 118 | pass 119 | 120 | def test_should_also_run(): 121 | pass 122 | ''') 123 | res = testdir.runpytest(file, '--verbose', '--no-only') 124 | outcomes = res.parseoutcomes() 125 | assert 'passed' in outcomes, 'Tests did not run successfully' 126 | 127 | assert_test_did_run(res, 'test_should_run') 128 | assert_test_did_run(res, 'test_should_run_as_well') 129 | assert_test_did_run(res, 'test_should_also_run') 130 | 131 | 132 | def test_negating_cmdline_options(testdir): 133 | file = testdir.makepyfile(''' 134 | import pytest 135 | 136 | def test_should_not_run(): 137 | pass 138 | 139 | @pytest.mark.only 140 | def test_should_run(): 141 | pass 142 | 143 | def test_should_also_not_run(): 144 | pass 145 | ''') 146 | res = testdir.runpytest(file, '--verbose', '--no-only', '--only') 147 | outcomes = res.parseoutcomes() 148 | assert 'passed' in outcomes, 'Tests did not run successfully' 149 | 150 | assert_test_did_run(res, 'test_should_run') 151 | assert_test_did_not_run(res, 'test_should_also_not_run') 152 | assert_test_did_not_run(res, 'test_should_not_run') 153 | -------------------------------------------------------------------------------- /test/ext/base_lint.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | from typing import Callable 3 | 4 | import pytest 5 | from pytest_common_subject import CommonSubjectTestMixin 6 | from pytest_lambda import lambda_fixture, not_implemented_fixture, static_fixture 7 | 8 | 9 | class IncludesUnexpectedFocused: 10 | def it_includes_unexpected_focused_warning(self, focused_warning_text, stdout: str): 11 | assert focused_warning_text in stdout 12 | 13 | 14 | class DoesNotIncludeUnexpectedFocused: 15 | def it_does_not_include_unexpected_focused_warning(self, focused_warning_text, stdout: str): 16 | assert focused_warning_text not in stdout 17 | 18 | 19 | class BaseLintTest(CommonSubjectTestMixin): 20 | #: Test file source code. Overridden by child test contexts 21 | source = not_implemented_fixture() 22 | 23 | #: Text to be expected in output if focused test is found 24 | focused_warning_text = not_implemented_fixture() 25 | 26 | @pytest.fixture 27 | def execute_linter(self, testdir, source_file) -> Callable[[], str]: 28 | """Run the linter and return the captured stdout""" 29 | 30 | def run_linter_test() -> str: 31 | result = testdir.run() 32 | return result.stdout.str() 33 | 34 | return run_linter_test 35 | 36 | @pytest.fixture 37 | def common_subject(self, execute_linter): 38 | return execute_linter 39 | 40 | dedented_source = lambda_fixture(lambda source: textwrap.dedent(source)) 41 | source_file = lambda_fixture(lambda dedented_source, testdir: testdir.makepyfile(dedented_source)) 42 | 43 | # Domain-specific alias 44 | stdout = lambda_fixture('common_subject_rval') 45 | 46 | # Used to verify pytestmark behaviour 47 | pytestmark_decl = lambda_fixture(params=[ 48 | pytest.param('pytest.mark.only', id='single'), 49 | pytest.param('[pytest.mark.only]', id='list'), 50 | pytest.param('[pytest.mark.other, pytest.mark.only, pytest.mark.stuff]', id='list-multi'), 51 | ]) 52 | 53 | class ContextFunction: 54 | class ContextWithOnlyMark(IncludesUnexpectedFocused): 55 | # language=py 56 | source = static_fixture(''' 57 | import pytest 58 | 59 | @pytest.mark.only 60 | def test_stuff(): 61 | pass 62 | ''') 63 | 64 | class ContextWithoutOnlyMark(DoesNotIncludeUnexpectedFocused): 65 | # language=py 66 | source = static_fixture(''' 67 | def test_stuff(): 68 | pass 69 | ''') 70 | 71 | class ContextAsyncFunction: 72 | class ContextWithOnlyMark(IncludesUnexpectedFocused): 73 | # language=py 74 | source = static_fixture(''' 75 | import pytest 76 | 77 | @pytest.mark.asyncio 78 | @pytest.mark.only 79 | async def test_stuff(): 80 | pass 81 | ''') 82 | 83 | class ContextWithoutOnlyMark(DoesNotIncludeUnexpectedFocused): 84 | # language=py 85 | source = static_fixture(''' 86 | import pytest 87 | 88 | @pytest.mark.asyncio 89 | async def test_stuff(): 90 | pass 91 | ''') 92 | 93 | class ContextClass: 94 | class ContextWithOnlyMark: 95 | class CaseDecorator(IncludesUnexpectedFocused): 96 | # language=py 97 | source = static_fixture(''' 98 | import pytest 99 | 100 | @pytest.mark.only 101 | class TestMyStuff: 102 | def test_stuff(self): 103 | pass 104 | ''') 105 | 106 | class CasePyTestMark(IncludesUnexpectedFocused): 107 | # language=py 108 | source = lambda_fixture(lambda pytestmark_decl: f''' 109 | import pytest 110 | 111 | class TestMyStuff: 112 | pytestmark = {pytestmark_decl} 113 | 114 | def test_stuff(self): 115 | pass 116 | ''') 117 | 118 | class CasePyTestMarkUnpackAssign(IncludesUnexpectedFocused): 119 | # language=py 120 | source = lambda_fixture(lambda pytestmark_decl: f''' 121 | import pytest 122 | 123 | class TestMyStuff: 124 | pytest._notreal, pytestmark, stuff = 'abc', {pytestmark_decl}, 123 125 | 126 | def test_stuff(self): 127 | pass 128 | ''') 129 | 130 | class ContextWithoutOnlyMark(DoesNotIncludeUnexpectedFocused): 131 | # language=py 132 | source = static_fixture(''' 133 | class TestMyStuff: 134 | apples = 'pears' 135 | 136 | def test_stuff(self): 137 | pass 138 | ''') 139 | 140 | class ContextModule: 141 | class ContextWithOnlyMark: 142 | class CaseSingleAssign(IncludesUnexpectedFocused): 143 | # language=py 144 | source = lambda_fixture(lambda pytestmark_decl: f''' 145 | import pytest 146 | 147 | pytestmark = {pytestmark_decl} 148 | 149 | class TestMyStuff: 150 | def test_stuff(self): 151 | pass 152 | 153 | def test_other_stuff(): 154 | pass 155 | ''') 156 | 157 | class CaseUnpackAssign(IncludesUnexpectedFocused): 158 | # language=py 159 | source = lambda_fixture(lambda pytestmark_decl: f''' 160 | import pytest 161 | 162 | pytest._notreal, pytestmark, stuff = 'abc', {pytestmark_decl}, 123 163 | 164 | class TestMyStuff: 165 | def test_stuff(self): 166 | pass 167 | 168 | def test_other_stuff(): 169 | pass 170 | ''') 171 | 172 | class ContextWithoutOnlyMark(DoesNotIncludeUnexpectedFocused): 173 | # language=py 174 | source = static_fixture(''' 175 | class TestMyStuff: 176 | def test_stuff(self): 177 | pass 178 | ''') 179 | 180 | class CaseUnrelated(DoesNotIncludeUnexpectedFocused): 181 | # language=py 182 | source = static_fixture(''' 183 | # Ensure proper handling of module-level assignments 184 | d = dict() 185 | d['a'] = 1 186 | d |= {} 187 | d.keys = lambda x: x 188 | 189 | # Unpacking assignments of all kinds should not cause errors 190 | a, b, c = 1, 2, 3 191 | a, *b, c = 1, 2, 3, 4, 5 192 | a, *b, c = range(3) 193 | a, b, c = range(3) 194 | [a, b, c] = range(3) 195 | 196 | # Decorators of all kinds should not cause errors 197 | @staticmethod 198 | @a.b.c 199 | @a.b.c() 200 | def test_stuff(): 201 | pass 202 | ''') 203 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "astroid" 5 | version = "2.15.8" 6 | description = "An abstract syntax tree for Python with inference support." 7 | optional = false 8 | python-versions = ">=3.7.2" 9 | files = [ 10 | {file = "astroid-2.15.8-py3-none-any.whl", hash = "sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c"}, 11 | {file = "astroid-2.15.8.tar.gz", hash = "sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a"}, 12 | ] 13 | 14 | [package.dependencies] 15 | lazy-object-proxy = ">=1.4.0" 16 | typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} 17 | wrapt = [ 18 | {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, 19 | {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, 20 | ] 21 | 22 | [[package]] 23 | name = "colorama" 24 | version = "0.4.6" 25 | description = "Cross-platform colored terminal text." 26 | optional = false 27 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 28 | files = [ 29 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 30 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 31 | ] 32 | 33 | [[package]] 34 | name = "dill" 35 | version = "0.3.8" 36 | description = "serialize all of Python" 37 | optional = false 38 | python-versions = ">=3.8" 39 | files = [ 40 | {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, 41 | {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, 42 | ] 43 | 44 | [package.extras] 45 | graph = ["objgraph (>=1.7.2)"] 46 | profile = ["gprof2dot (>=2022.7.29)"] 47 | 48 | [[package]] 49 | name = "distlib" 50 | version = "0.3.8" 51 | description = "Distribution utilities" 52 | optional = false 53 | python-versions = "*" 54 | files = [ 55 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 56 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 57 | ] 58 | 59 | [[package]] 60 | name = "exceptiongroup" 61 | version = "1.2.1" 62 | description = "Backport of PEP 654 (exception groups)" 63 | optional = false 64 | python-versions = ">=3.7" 65 | files = [ 66 | {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, 67 | {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, 68 | ] 69 | 70 | [package.extras] 71 | test = ["pytest (>=6)"] 72 | 73 | [[package]] 74 | name = "filelock" 75 | version = "3.14.0" 76 | description = "A platform independent file lock." 77 | optional = false 78 | python-versions = ">=3.8" 79 | files = [ 80 | {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, 81 | {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, 82 | ] 83 | 84 | [package.extras] 85 | docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 86 | 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)"] 87 | typing = ["typing-extensions (>=4.8)"] 88 | 89 | [[package]] 90 | name = "flake8" 91 | version = "5.0.4" 92 | description = "the modular source code checker: pep8 pyflakes and co" 93 | optional = false 94 | python-versions = ">=3.6.1" 95 | files = [ 96 | {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, 97 | {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, 98 | ] 99 | 100 | [package.dependencies] 101 | mccabe = ">=0.7.0,<0.8.0" 102 | pycodestyle = ">=2.9.0,<2.10.0" 103 | pyflakes = ">=2.5.0,<2.6.0" 104 | 105 | [[package]] 106 | name = "flake8" 107 | version = "7.0.0" 108 | description = "the modular source code checker: pep8 pyflakes and co" 109 | optional = false 110 | python-versions = ">=3.8.1" 111 | files = [ 112 | {file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"}, 113 | {file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"}, 114 | ] 115 | 116 | [package.dependencies] 117 | mccabe = ">=0.7.0,<0.8.0" 118 | pycodestyle = ">=2.11.0,<2.12.0" 119 | pyflakes = ">=3.2.0,<3.3.0" 120 | 121 | [[package]] 122 | name = "iniconfig" 123 | version = "2.0.0" 124 | description = "brain-dead simple config-ini parsing" 125 | optional = false 126 | python-versions = ">=3.7" 127 | files = [ 128 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 129 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 130 | ] 131 | 132 | [[package]] 133 | name = "isort" 134 | version = "5.13.2" 135 | description = "A Python utility / library to sort Python imports." 136 | optional = false 137 | python-versions = ">=3.8.0" 138 | files = [ 139 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 140 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 141 | ] 142 | 143 | [package.extras] 144 | colors = ["colorama (>=0.4.6)"] 145 | 146 | [[package]] 147 | name = "lazy-object-proxy" 148 | version = "1.10.0" 149 | description = "A fast and thorough lazy object proxy." 150 | optional = false 151 | python-versions = ">=3.8" 152 | files = [ 153 | {file = "lazy-object-proxy-1.10.0.tar.gz", hash = "sha256:78247b6d45f43a52ef35c25b5581459e85117225408a4128a3daf8bf9648ac69"}, 154 | {file = "lazy_object_proxy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:855e068b0358ab916454464a884779c7ffa312b8925c6f7401e952dcf3b89977"}, 155 | {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab7004cf2e59f7c2e4345604a3e6ea0d92ac44e1c2375527d56492014e690c3"}, 156 | {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc0d2fc424e54c70c4bc06787e4072c4f3b1aa2f897dfdc34ce1013cf3ceef05"}, 157 | {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e2adb09778797da09d2b5ebdbceebf7dd32e2c96f79da9052b2e87b6ea495895"}, 158 | {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1f711e2c6dcd4edd372cf5dec5c5a30d23bba06ee012093267b3376c079ec83"}, 159 | {file = "lazy_object_proxy-1.10.0-cp310-cp310-win32.whl", hash = "sha256:76a095cfe6045c7d0ca77db9934e8f7b71b14645f0094ffcd842349ada5c5fb9"}, 160 | {file = "lazy_object_proxy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:b4f87d4ed9064b2628da63830986c3d2dca7501e6018347798313fcf028e2fd4"}, 161 | {file = "lazy_object_proxy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fec03caabbc6b59ea4a638bee5fce7117be8e99a4103d9d5ad77f15d6f81020c"}, 162 | {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c83f957782cbbe8136bee26416686a6ae998c7b6191711a04da776dc9e47d4"}, 163 | {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009e6bb1f1935a62889ddc8541514b6a9e1fcf302667dcb049a0be5c8f613e56"}, 164 | {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75fc59fc450050b1b3c203c35020bc41bd2695ed692a392924c6ce180c6f1dc9"}, 165 | {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:782e2c9b2aab1708ffb07d4bf377d12901d7a1d99e5e410d648d892f8967ab1f"}, 166 | {file = "lazy_object_proxy-1.10.0-cp311-cp311-win32.whl", hash = "sha256:edb45bb8278574710e68a6b021599a10ce730d156e5b254941754a9cc0b17d03"}, 167 | {file = "lazy_object_proxy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:e271058822765ad5e3bca7f05f2ace0de58a3f4e62045a8c90a0dfd2f8ad8cc6"}, 168 | {file = "lazy_object_proxy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e98c8af98d5707dcdecc9ab0863c0ea6e88545d42ca7c3feffb6b4d1e370c7ba"}, 169 | {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:952c81d415b9b80ea261d2372d2a4a2332a3890c2b83e0535f263ddfe43f0d43"}, 170 | {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80b39d3a151309efc8cc48675918891b865bdf742a8616a337cb0090791a0de9"}, 171 | {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e221060b701e2aa2ea991542900dd13907a5c90fa80e199dbf5a03359019e7a3"}, 172 | {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:92f09ff65ecff3108e56526f9e2481b8116c0b9e1425325e13245abfd79bdb1b"}, 173 | {file = "lazy_object_proxy-1.10.0-cp312-cp312-win32.whl", hash = "sha256:3ad54b9ddbe20ae9f7c1b29e52f123120772b06dbb18ec6be9101369d63a4074"}, 174 | {file = "lazy_object_proxy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:127a789c75151db6af398b8972178afe6bda7d6f68730c057fbbc2e96b08d282"}, 175 | {file = "lazy_object_proxy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4ed0518a14dd26092614412936920ad081a424bdcb54cc13349a8e2c6d106a"}, 176 | {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ad9e6ed739285919aa9661a5bbed0aaf410aa60231373c5579c6b4801bd883c"}, 177 | {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc0a92c02fa1ca1e84fc60fa258458e5bf89d90a1ddaeb8ed9cc3147f417255"}, 178 | {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0aefc7591920bbd360d57ea03c995cebc204b424524a5bd78406f6e1b8b2a5d8"}, 179 | {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5faf03a7d8942bb4476e3b62fd0f4cf94eaf4618e304a19865abf89a35c0bbee"}, 180 | {file = "lazy_object_proxy-1.10.0-cp38-cp38-win32.whl", hash = "sha256:e333e2324307a7b5d86adfa835bb500ee70bfcd1447384a822e96495796b0ca4"}, 181 | {file = "lazy_object_proxy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:cb73507defd385b7705c599a94474b1d5222a508e502553ef94114a143ec6696"}, 182 | {file = "lazy_object_proxy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:366c32fe5355ef5fc8a232c5436f4cc66e9d3e8967c01fb2e6302fd6627e3d94"}, 183 | {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2297f08f08a2bb0d32a4265e98a006643cd7233fb7983032bd61ac7a02956b3b"}, 184 | {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18dd842b49456aaa9a7cf535b04ca4571a302ff72ed8740d06b5adcd41fe0757"}, 185 | {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:217138197c170a2a74ca0e05bddcd5f1796c735c37d0eee33e43259b192aa424"}, 186 | {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a3a87cf1e133e5b1994144c12ca4aa3d9698517fe1e2ca82977781b16955658"}, 187 | {file = "lazy_object_proxy-1.10.0-cp39-cp39-win32.whl", hash = "sha256:30b339b2a743c5288405aa79a69e706a06e02958eab31859f7f3c04980853b70"}, 188 | {file = "lazy_object_proxy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:a899b10e17743683b293a729d3a11f2f399e8a90c73b089e29f5d0fe3509f0dd"}, 189 | {file = "lazy_object_proxy-1.10.0-pp310.pp311.pp312.pp38.pp39-none-any.whl", hash = "sha256:80fa48bd89c8f2f456fc0765c11c23bf5af827febacd2f523ca5bc1893fcc09d"}, 190 | ] 191 | 192 | [[package]] 193 | name = "mccabe" 194 | version = "0.7.0" 195 | description = "McCabe checker, plugin for flake8" 196 | optional = false 197 | python-versions = ">=3.6" 198 | files = [ 199 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 200 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 201 | ] 202 | 203 | [[package]] 204 | name = "packaging" 205 | version = "24.0" 206 | description = "Core utilities for Python packages" 207 | optional = false 208 | python-versions = ">=3.7" 209 | files = [ 210 | {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, 211 | {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, 212 | ] 213 | 214 | [[package]] 215 | name = "platformdirs" 216 | version = "4.2.2" 217 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 218 | optional = false 219 | python-versions = ">=3.8" 220 | files = [ 221 | {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, 222 | {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, 223 | ] 224 | 225 | [package.extras] 226 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 227 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 228 | type = ["mypy (>=1.8)"] 229 | 230 | [[package]] 231 | name = "pluggy" 232 | version = "1.5.0" 233 | description = "plugin and hook calling mechanisms for python" 234 | optional = false 235 | python-versions = ">=3.8" 236 | files = [ 237 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 238 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 239 | ] 240 | 241 | [package.extras] 242 | dev = ["pre-commit", "tox"] 243 | testing = ["pytest", "pytest-benchmark"] 244 | 245 | [[package]] 246 | name = "py" 247 | version = "1.11.0" 248 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 249 | optional = false 250 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 251 | files = [ 252 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 253 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 254 | ] 255 | 256 | [[package]] 257 | name = "pycodestyle" 258 | version = "2.9.1" 259 | description = "Python style guide checker" 260 | optional = false 261 | python-versions = ">=3.6" 262 | files = [ 263 | {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, 264 | {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, 265 | ] 266 | 267 | [[package]] 268 | name = "pycodestyle" 269 | version = "2.11.1" 270 | description = "Python style guide checker" 271 | optional = false 272 | python-versions = ">=3.8" 273 | files = [ 274 | {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, 275 | {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, 276 | ] 277 | 278 | [[package]] 279 | name = "pyflakes" 280 | version = "2.5.0" 281 | description = "passive checker of Python programs" 282 | optional = false 283 | python-versions = ">=3.6" 284 | files = [ 285 | {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, 286 | {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, 287 | ] 288 | 289 | [[package]] 290 | name = "pyflakes" 291 | version = "3.2.0" 292 | description = "passive checker of Python programs" 293 | optional = false 294 | python-versions = ">=3.8" 295 | files = [ 296 | {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, 297 | {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, 298 | ] 299 | 300 | [[package]] 301 | name = "pylint" 302 | version = "2.17.7" 303 | description = "python code static checker" 304 | optional = false 305 | python-versions = ">=3.7.2" 306 | files = [ 307 | {file = "pylint-2.17.7-py3-none-any.whl", hash = "sha256:27a8d4c7ddc8c2f8c18aa0050148f89ffc09838142193fdbe98f172781a3ff87"}, 308 | {file = "pylint-2.17.7.tar.gz", hash = "sha256:f4fcac7ae74cfe36bc8451e931d8438e4a476c20314b1101c458ad0f05191fad"}, 309 | ] 310 | 311 | [package.dependencies] 312 | astroid = ">=2.15.8,<=2.17.0-dev0" 313 | colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} 314 | dill = [ 315 | {version = ">=0.2", markers = "python_version < \"3.11\""}, 316 | {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, 317 | ] 318 | isort = ">=4.2.5,<6" 319 | mccabe = ">=0.6,<0.8" 320 | platformdirs = ">=2.2.0" 321 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 322 | tomlkit = ">=0.10.1" 323 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 324 | 325 | [package.extras] 326 | spelling = ["pyenchant (>=3.2,<4.0)"] 327 | testutils = ["gitpython (>3)"] 328 | 329 | [[package]] 330 | name = "pytest" 331 | version = "7.4.4" 332 | description = "pytest: simple powerful testing with Python" 333 | optional = false 334 | python-versions = ">=3.7" 335 | files = [ 336 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 337 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 338 | ] 339 | 340 | [package.dependencies] 341 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 342 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 343 | iniconfig = "*" 344 | packaging = "*" 345 | pluggy = ">=0.12,<2.0" 346 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 347 | 348 | [package.extras] 349 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 350 | 351 | [[package]] 352 | name = "pytest-common-subject" 353 | version = "1.0.6" 354 | description = "pytest framework for testing different aspects of a common method" 355 | optional = false 356 | python-versions = ">=3.6,<4.0" 357 | files = [ 358 | {file = "pytest-common-subject-1.0.6.tar.gz", hash = "sha256:e3cd860c7e6e4481f29a47f80b41a0c186d146f529a2f346b8a9f3ed4e113f6c"}, 359 | {file = "pytest_common_subject-1.0.6-py3-none-any.whl", hash = "sha256:3717bb80359513dd679744805fc39bf34ed48c872be9cbe616c69291bff54f1c"}, 360 | ] 361 | 362 | [package.dependencies] 363 | lazy-object-proxy = ">=1.3.1,<2.0.0" 364 | pytest = ">=3.6,<8" 365 | pytest-fixture-order = ">=0.1.2,<0.2.0" 366 | pytest-lambda = ">=0.1.0" 367 | 368 | [[package]] 369 | name = "pytest-fixture-order" 370 | version = "0.1.4" 371 | description = "pytest plugin to control fixture evaluation order" 372 | optional = false 373 | python-versions = ">=3.6,<4.0" 374 | files = [ 375 | {file = "pytest-fixture-order-0.1.4.tar.gz", hash = "sha256:257693f1c9fcc687c46e2562ee380e4e94f47670effbbce24545c73b00f25366"}, 376 | {file = "pytest_fixture_order-0.1.4-py3-none-any.whl", hash = "sha256:6554329dfe1c6961b82c8ab7e3565ad4af279d917317a3d66b2c5ff077d27b97"}, 377 | ] 378 | 379 | [package.dependencies] 380 | pytest = ">=3.0" 381 | 382 | [[package]] 383 | name = "pytest-lambda" 384 | version = "2.2.1" 385 | description = "Define pytest fixtures with lambda functions." 386 | optional = false 387 | python-versions = "<4.0.0,>=3.8.0" 388 | files = [ 389 | {file = "pytest_lambda-2.2.1-py3-none-any.whl", hash = "sha256:872c30dc14316fe75ba365645dca56e24851892e69ec76af38b8185c16e036ba"}, 390 | {file = "pytest_lambda-2.2.1.tar.gz", hash = "sha256:92d4c19d5cc3df93744dbb8d2ccec06abc5dcbe2d41a7a518282450c17b4948d"}, 391 | ] 392 | 393 | [package.dependencies] 394 | pytest = ">=3.6,<9" 395 | wrapt = ">=1.11.0,<2.0.0" 396 | 397 | [[package]] 398 | name = "pytest-pylint" 399 | version = "0.21.0" 400 | description = "pytest plugin to check source code with pylint" 401 | optional = false 402 | python-versions = ">=3.7" 403 | files = [ 404 | {file = "pytest-pylint-0.21.0.tar.gz", hash = "sha256:88764b8e1d5cfa18809248e0ccc2fc05035f08c35f0b0222ddcfea1c3c4e553e"}, 405 | {file = "pytest_pylint-0.21.0-py3-none-any.whl", hash = "sha256:f10d9eaa72b9fbe624ee4b55da0481f56482eee0a467afc1ee3ae8b1fefbd0b4"}, 406 | ] 407 | 408 | [package.dependencies] 409 | pylint = ">=2.15.0" 410 | pytest = ">=7.0" 411 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 412 | 413 | [[package]] 414 | name = "six" 415 | version = "1.16.0" 416 | description = "Python 2 and 3 compatibility utilities" 417 | optional = false 418 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 419 | files = [ 420 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 421 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 422 | ] 423 | 424 | [[package]] 425 | name = "tomli" 426 | version = "2.0.1" 427 | description = "A lil' TOML parser" 428 | optional = false 429 | python-versions = ">=3.7" 430 | files = [ 431 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 432 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 433 | ] 434 | 435 | [[package]] 436 | name = "tomlkit" 437 | version = "0.12.5" 438 | description = "Style preserving TOML library" 439 | optional = false 440 | python-versions = ">=3.7" 441 | files = [ 442 | {file = "tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f"}, 443 | {file = "tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c"}, 444 | ] 445 | 446 | [[package]] 447 | name = "tox" 448 | version = "3.28.0" 449 | description = "tox is a generic virtualenv management and test command line tool" 450 | optional = false 451 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 452 | files = [ 453 | {file = "tox-3.28.0-py2.py3-none-any.whl", hash = "sha256:57b5ab7e8bb3074edc3c0c0b4b192a4f3799d3723b2c5b76f1fa9f2d40316eea"}, 454 | {file = "tox-3.28.0.tar.gz", hash = "sha256:d0d28f3fe6d6d7195c27f8b054c3e99d5451952b54abdae673b71609a581f640"}, 455 | ] 456 | 457 | [package.dependencies] 458 | colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} 459 | filelock = ">=3.0.0" 460 | packaging = ">=14" 461 | pluggy = ">=0.12.0" 462 | py = ">=1.4.17" 463 | six = ">=1.14.0" 464 | tomli = {version = ">=2.0.1", markers = "python_version >= \"3.7\" and python_version < \"3.11\""} 465 | 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" 466 | 467 | [package.extras] 468 | docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] 469 | 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)"] 470 | 471 | [[package]] 472 | name = "typing-extensions" 473 | version = "4.12.0" 474 | description = "Backported and Experimental Type Hints for Python 3.8+" 475 | optional = false 476 | python-versions = ">=3.8" 477 | files = [ 478 | {file = "typing_extensions-4.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"}, 479 | {file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"}, 480 | ] 481 | 482 | [[package]] 483 | name = "virtualenv" 484 | version = "20.26.2" 485 | description = "Virtual Python Environment builder" 486 | optional = false 487 | python-versions = ">=3.7" 488 | files = [ 489 | {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, 490 | {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, 491 | ] 492 | 493 | [package.dependencies] 494 | distlib = ">=0.3.7,<1" 495 | filelock = ">=3.12.2,<4" 496 | platformdirs = ">=3.9.1,<5" 497 | 498 | [package.extras] 499 | 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)"] 500 | 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)"] 501 | 502 | [[package]] 503 | name = "wrapt" 504 | version = "1.16.0" 505 | description = "Module for decorators, wrappers and monkey patching." 506 | optional = false 507 | python-versions = ">=3.6" 508 | files = [ 509 | {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, 510 | {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, 511 | {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, 512 | {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, 513 | {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"}, 514 | {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, 515 | {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, 516 | {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, 517 | {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, 518 | {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, 519 | {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, 520 | {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, 521 | {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, 522 | {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, 523 | {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"}, 524 | {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, 525 | {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, 526 | {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, 527 | {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, 528 | {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, 529 | {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, 530 | {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, 531 | {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, 532 | {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, 533 | {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"}, 534 | {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, 535 | {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, 536 | {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, 537 | {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, 538 | {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, 539 | {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, 540 | {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, 541 | {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, 542 | {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"}, 543 | {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, 544 | {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, 545 | {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, 546 | {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, 547 | {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, 548 | {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, 549 | {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, 550 | {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, 551 | {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"}, 552 | {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, 553 | {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, 554 | {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, 555 | {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, 556 | {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, 557 | {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, 558 | {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, 559 | {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, 560 | {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, 561 | {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"}, 562 | {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, 563 | {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, 564 | {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, 565 | {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, 566 | {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, 567 | {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, 568 | {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, 569 | {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, 570 | {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, 571 | {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"}, 572 | {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, 573 | {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, 574 | {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, 575 | {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, 576 | {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, 577 | {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, 578 | {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, 579 | ] 580 | 581 | [metadata] 582 | lock-version = "2.0" 583 | python-versions = "^3.8" 584 | content-hash = "01277b9d5d3958a1074d880e56905cec237a5edfb369bf3755f1081820f07970" 585 | --------------------------------------------------------------------------------