├── tests ├── __init__.py ├── conftest.py ├── test_fastapi.py ├── test_annotated.py └── test_graph.py ├── taskiq_dependencies ├── py.typed ├── __init__.py ├── utils.py ├── dependency.py ├── graph.py └── ctx.py ├── .python-version ├── tox.ini ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── LICENSE ├── .pre-commit-config.yaml ├── .gitignore ├── pyproject.toml ├── README.md └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /taskiq_dependencies/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.13.1 2 | 3.12.8 3 | 3.11.11 4 | 3.10.16 5 | 3.9.21 6 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope="session") 5 | def anyio_backend() -> str: 6 | """ 7 | Anyio backend. 8 | 9 | Backend for anyio pytest plugin. 10 | :return: backend name. 11 | """ 12 | return "asyncio" 13 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | isolated_build = true 3 | env_list = 4 | py313 5 | py312 6 | py311 7 | py310 8 | py39 9 | 10 | [testenv] 11 | skip_install = true 12 | allowlist_externals = poetry 13 | commands_pre = 14 | poetry install 15 | commands = 16 | pre-commit run --all-files 17 | poetry run pytest -vv 18 | -------------------------------------------------------------------------------- /taskiq_dependencies/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Taskiq dependencies package. 3 | 4 | This package is used to add dependency injection 5 | in your project easily. 6 | 7 | Github repo: https://github.com/taskiq-python/taskiq-dependencies 8 | """ 9 | 10 | from taskiq_dependencies.dependency import Depends 11 | from taskiq_dependencies.graph import DependencyGraph 12 | from taskiq_dependencies.utils import ParamInfo 13 | 14 | __all__ = ["DependencyGraph", "Depends", "ParamInfo"] 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release python package 2 | 3 | on: 4 | release: 5 | types: 6 | - released 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Install poetry 14 | run: pipx install poetry 15 | - name: Set up Python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: "3.11" 19 | - name: Install deps 20 | run: poetry install 21 | - name: Set version 22 | run: poetry version "${{ github.ref_name }}" 23 | - name: Release package 24 | env: 25 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} 26 | run: poetry publish --build 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Pavel Kirilin 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 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v2.4.0 6 | hooks: 7 | - id: check-ast 8 | - id: trailing-whitespace 9 | - id: check-toml 10 | - id: end-of-file-fixer 11 | 12 | - repo: https://github.com/asottile/add-trailing-comma 13 | rev: v2.1.0 14 | hooks: 15 | - id: add-trailing-comma 16 | 17 | - repo: local 18 | hooks: 19 | - id: black 20 | name: Format with Black 21 | entry: poetry run black 22 | language: system 23 | types: [python] 24 | 25 | - id: ruff 26 | name: Run ruff lints 27 | entry: poetry run ruff check 28 | language: system 29 | pass_filenames: false 30 | types: [python] 31 | args: 32 | - "--fix" 33 | - "--unsafe-fixes" 34 | - "taskiq_dependencies" 35 | - "tests" 36 | 37 | - id: mypy 38 | name: Validate types with MyPy 39 | entry: poetry run mypy 40 | language: system 41 | types: [ python ] 42 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | lint: 11 | strategy: 12 | matrix: 13 | cmd: 14 | - black 15 | - mypy 16 | - ruff 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Install poetry 21 | run: pipx install poetry 22 | - name: Set up Python 23 | uses: actions/setup-python@v4 24 | with: 25 | python-version: "3.11" 26 | cache: "poetry" 27 | - name: Install deps 28 | run: poetry install 29 | - name: Run lint check 30 | run: poetry run pre-commit run -a ${{ matrix.cmd }} 31 | pytest: 32 | permissions: 33 | checks: write 34 | pull-requests: write 35 | contents: write 36 | strategy: 37 | matrix: 38 | py_version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 39 | os: [ubuntu-latest, windows-latest] 40 | runs-on: "${{ matrix.os }}" 41 | steps: 42 | - uses: actions/checkout@v2 43 | - name: Install poetry 44 | run: pipx install poetry 45 | - name: Set up Python 46 | uses: actions/setup-python@v4 47 | with: 48 | python-version: "${{ matrix.py_version }}" 49 | cache: "poetry" 50 | - name: Install deps 51 | run: poetry install 52 | - name: Run pytest check 53 | run: poetry run pytest -vv -n auto --cov="taskiq_dependencies" . 54 | -------------------------------------------------------------------------------- /tests/test_fastapi.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import Any 3 | from unittest.mock import patch 4 | 5 | import pytest 6 | 7 | from taskiq_dependencies import DependencyGraph 8 | 9 | 10 | class MyFastapiDepends: 11 | def __init__(self, dependency: Any, use_cache: bool = False) -> None: 12 | self.dependency = dependency 13 | self.use_cache = use_cache 14 | 15 | 16 | def test_dependency_swap() -> None: 17 | """ 18 | Test that dependency classes are swapped. 19 | 20 | This test checks that if function depends on FastAPI depends, it will 21 | be swapped and resolved. 22 | """ 23 | with patch("taskiq_dependencies.graph.FastapiDepends", MyFastapiDepends): 24 | 25 | def func_a() -> int: 26 | return 1 27 | 28 | def func_b(dep_a: int = MyFastapiDepends(func_a)) -> int: # type: ignore 29 | return dep_a 30 | 31 | with DependencyGraph(func_b).sync_ctx() as ctx: 32 | kwargs = ctx.resolve_kwargs() 33 | 34 | assert kwargs == {"dep_a": 1} 35 | 36 | 37 | @pytest.mark.skipif(sys.version_info < (3, 10), reason="Only for python 3.10+") 38 | def test_dependency_swap_annotated() -> None: 39 | """ 40 | Test that dependency classes are swapped. 41 | 42 | This test checks that if function depends on FastAPI depends, it will 43 | be swapped and resolved. 44 | """ 45 | from typing import Annotated 46 | 47 | with patch("taskiq_dependencies.graph.FastapiDepends", MyFastapiDepends): 48 | 49 | def func_a() -> int: 50 | return 1 51 | 52 | def func_b(dep_a: Annotated[int, MyFastapiDepends(func_a)]) -> int: # type: ignore 53 | return dep_a 54 | 55 | with DependencyGraph(func_b).sync_ctx() as ctx: 56 | kwargs = ctx.resolve_kwargs() 57 | 58 | assert kwargs == {"dep_a": 1} 59 | -------------------------------------------------------------------------------- /taskiq_dependencies/utils.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import sys 3 | from contextlib import _AsyncGeneratorContextManager, _GeneratorContextManager 4 | from typing import TYPE_CHECKING, Any, AsyncContextManager, ContextManager, Optional 5 | 6 | if sys.version_info >= (3, 10): 7 | from typing import TypeGuard 8 | else: 9 | from typing_extensions import TypeGuard 10 | 11 | if TYPE_CHECKING: 12 | from taskiq_dependencies.graph import DependencyGraph 13 | 14 | 15 | class ParamInfo: 16 | """ 17 | Parameter information. 18 | 19 | This class helps you to get information, 20 | about how the current dependency was specified. 21 | 22 | If there's no dependant function, the name will be an empty string 23 | and the definition will be None. 24 | """ 25 | 26 | def __init__( 27 | self, 28 | name: str, 29 | graph: "DependencyGraph", 30 | signature: Optional[inspect.Parameter] = None, 31 | ) -> None: 32 | self.name = name 33 | self.graph = graph 34 | self.definition = signature 35 | 36 | def __repr__(self) -> str: 37 | return f"ParamInfo" 38 | 39 | 40 | def iscontextmanager(obj: Any) -> TypeGuard[ContextManager[Any]]: 41 | """ 42 | Return true if the object is a sync context manager. 43 | 44 | :param obj: object to check. 45 | :return: bool that indicates whether the object is a context manager or not. 46 | """ 47 | return issubclass(obj.__class__, _GeneratorContextManager) 48 | 49 | 50 | def isasynccontextmanager(obj: Any) -> TypeGuard[AsyncContextManager[Any]]: 51 | """ 52 | Return true if the object is a async context manager. 53 | 54 | :param obj: object to check. 55 | :return: bool that indicates whether the object is a async context manager or not. 56 | """ 57 | return issubclass(obj.__class__, _AsyncGeneratorContextManager) 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | coverage.* 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # poetry 99 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 100 | # This is especially recommended for binary packages to ensure reproducibility, and is more 101 | # commonly ignored for libraries. 102 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 103 | #poetry.lock 104 | 105 | # pdm 106 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 107 | #pdm.lock 108 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 109 | # in version control. 110 | # https://pdm.fming.dev/#use-with-ide 111 | .pdm.toml 112 | 113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 114 | __pypackages__/ 115 | 116 | # Celery stuff 117 | celerybeat-schedule 118 | celerybeat.pid 119 | 120 | # SageMath parsed files 121 | *.sage.py 122 | 123 | # Environments 124 | .env 125 | .venv 126 | env/ 127 | venv/ 128 | ENV/ 129 | env.bak/ 130 | venv.bak/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | 156 | # PyCharm 157 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 158 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 159 | # and can be added to the global gitignore or merged into this file. For a more nuclear 160 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 161 | .idea/ 162 | .vscode/ 163 | 164 | ## For docs 165 | node_modules 166 | .temp 167 | .cache 168 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "taskiq-dependencies" 3 | version = "0.0.0" 4 | description = "FastAPI like dependency injection implementation" 5 | authors = ["Pavel Kirilin "] 6 | readme = "README.md" 7 | packages = [{ include = "taskiq_dependencies" }] 8 | classifiers = [ 9 | "Typing :: Typed", 10 | "Programming Language :: Python", 11 | "Programming Language :: Python :: 3", 12 | "Programming Language :: Python :: 3 :: Only", 13 | "Programming Language :: Python :: 3.9", 14 | "Programming Language :: Python :: 3.10", 15 | "Programming Language :: Python :: 3.11", 16 | "Programming Language :: Python :: 3.12", 17 | "Programming Language :: Python :: 3.13", 18 | "Operating System :: OS Independent", 19 | "Intended Audience :: Developers", 20 | ] 21 | keywords = ["taskiq", "dependencies", "injection", "async", "DI"] 22 | 23 | [tool.poetry.dependencies] 24 | python = "^3.9" 25 | typing-extensions = { version = ">=4.6.3", python = "<3.10" } 26 | 27 | [tool.poetry.group.dev.dependencies] 28 | pytest = "^8" 29 | black = "^24" 30 | mypy = "^1" 31 | pre-commit = "^3" 32 | coverage = "^7" 33 | pytest-cov = "^5" 34 | anyio = "^4" 35 | pytest-xdist = { version = "^3", extras = ["psutil"] } 36 | ruff = "^0.6.8" 37 | 38 | 39 | [tool.mypy] 40 | strict = true 41 | ignore_missing_imports = true 42 | allow_subclassing_any = true 43 | allow_untyped_calls = true 44 | pretty = true 45 | show_error_codes = true 46 | implicit_reexport = true 47 | allow_untyped_decorators = true 48 | warn_return_any = false 49 | warn_unused_ignores = false 50 | 51 | [build-system] 52 | requires = ["poetry-core"] 53 | build-backend = "poetry.core.masonry.api" 54 | 55 | [tool.ruff] 56 | # List of enabled rulsets. 57 | # See https://docs.astral.sh/ruff/rules/ for more information. 58 | lint.select = [ 59 | "E", # Error 60 | "F", # Pyflakes 61 | "W", # Pycodestyle 62 | "C90", # McCabe complexity 63 | "I", # Isort 64 | "N", # pep8-naming 65 | "D", # Pydocstyle 66 | "ANN", # Pytype annotations 67 | "S", # Bandit 68 | "B", # Bugbear 69 | "COM", # Commas 70 | "C4", # Comprehensions 71 | "ISC", # Implicit string concat 72 | "PIE", # Unnecessary code 73 | "T20", # Catch prints 74 | "PYI", # validate pyi files 75 | "Q", # Checks for quotes 76 | "RSE", # Checks raise statements 77 | "RET", # Checks return statements 78 | "SLF", # Self checks 79 | "SIM", # Simplificator 80 | "PTH", # Pathlib checks 81 | "ERA", # Checks for commented out code 82 | "RUF", # Specific to Ruff checks 83 | ] 84 | lint.ignore = [ 85 | "D105", # Missing docstring in magic method 86 | "D107", # Missing docstring in __init__ 87 | "D212", # Multi-line docstring summary should start at the first line 88 | "D401", # First line should be in imperative mood 89 | "D104", # Missing docstring in public package 90 | "D100", # Missing docstring in public module 91 | "ANN102", # Missing type annotation for self in method 92 | "ANN101", # Missing type annotation for argument 93 | "ANN401", # typing.Any are disallowed in `**kwargs 94 | "D106", # Missing docstring in public nested class 95 | "N802", # Function name `Depends` should be lowercase 96 | "N806", # Variable in function should be lowercase 97 | ] 98 | line-length = 88 99 | exclude = [".venv/"] 100 | lint.mccabe = { max-complexity = 10 } 101 | 102 | [tool.ruff.lint.per-file-ignores] 103 | "tests/*" = [ 104 | "S101", # Use of assert detected 105 | "S301", # Use of pickle detected 106 | "D103", # Missing docstring in public function 107 | "SLF001", # Private member accessed 108 | "S311", # Standard pseudo-random generators are not suitable for security/cryptographic purposes 109 | "D101", # Missing docstring in public class 110 | ] 111 | 112 | [tool.ruff.lint.pydocstyle] 113 | convention = "pep257" 114 | ignore-decorators = ["typing.overload"] 115 | 116 | [tool.ruff.lint.pylint] 117 | allow-magic-value-types = ["int", "str", "float"] 118 | 119 | [tool.ruff.lint.flake8-bugbear] 120 | extend-immutable-calls = ["taskiq_dependencies.Depends", "taskiq.TaskiqDepends"] 121 | -------------------------------------------------------------------------------- /taskiq_dependencies/dependency.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import uuid 3 | from collections.abc import Coroutine 4 | from contextlib import _AsyncGeneratorContextManager, _GeneratorContextManager 5 | from types import CoroutineType 6 | from typing import ( 7 | Any, 8 | AsyncGenerator, 9 | Callable, 10 | Dict, 11 | Generator, 12 | Optional, 13 | Type, 14 | TypeVar, 15 | Union, 16 | overload, 17 | ) 18 | 19 | _T = TypeVar("_T") 20 | 21 | 22 | @overload 23 | def Depends( 24 | dependency: Optional[Callable[..., "_GeneratorContextManager[_T]"]] = None, 25 | *, 26 | use_cache: bool = True, 27 | kwargs: Optional[Dict[str, Any]] = None, 28 | ) -> _T: # pragma: no cover 29 | ... 30 | 31 | 32 | @overload 33 | def Depends( 34 | dependency: Optional[Callable[..., "_AsyncGeneratorContextManager[_T]"]] = None, 35 | *, 36 | use_cache: bool = True, 37 | kwargs: Optional[Dict[str, Any]] = None, 38 | ) -> _T: # pragma: no cover 39 | ... 40 | 41 | 42 | @overload 43 | def Depends( 44 | dependency: Optional[Callable[..., AsyncGenerator[_T, None]]] = None, 45 | *, 46 | use_cache: bool = True, 47 | kwargs: Optional[Dict[str, Any]] = None, 48 | ) -> _T: # pragma: no cover 49 | ... 50 | 51 | 52 | @overload 53 | def Depends( 54 | dependency: Optional[Callable[..., Generator[_T, None, None]]] = None, 55 | *, 56 | use_cache: bool = True, 57 | kwargs: Optional[Dict[str, Any]] = None, 58 | ) -> _T: # pragma: no cover 59 | ... 60 | 61 | 62 | @overload 63 | def Depends( 64 | dependency: Optional[Type[_T]] = None, 65 | *, 66 | use_cache: bool = True, 67 | kwargs: Optional[Dict[str, Any]] = None, 68 | ) -> _T: # pragma: no cover 69 | ... 70 | 71 | 72 | @overload 73 | def Depends( 74 | dependency: Optional[Callable[..., "CoroutineType[Any, Any, _T]"]] = None, 75 | *, 76 | use_cache: bool = True, 77 | kwargs: Optional[Dict[str, Any]] = None, 78 | ) -> _T: # pragma: no cover 79 | ... 80 | 81 | 82 | @overload 83 | def Depends( 84 | dependency: Optional[Callable[..., Coroutine[Any, Any, _T]]] = None, 85 | *, 86 | use_cache: bool = True, 87 | kwargs: Optional[Dict[str, Any]] = None, 88 | ) -> _T: # pragma: no cover 89 | ... 90 | 91 | 92 | @overload 93 | def Depends( 94 | dependency: Optional[Callable[..., _T]] = None, 95 | *, 96 | use_cache: bool = True, 97 | kwargs: Optional[Dict[str, Any]] = None, 98 | ) -> _T: # pragma: no cover 99 | ... 100 | 101 | 102 | def Depends( 103 | dependency: Optional[Any] = None, 104 | *, 105 | use_cache: bool = True, 106 | kwargs: Optional[Dict[str, Any]] = None, 107 | ) -> Any: 108 | """ 109 | Constructs a dependency. 110 | 111 | This function returns TaskiqDepends 112 | and needed for typehinting. 113 | 114 | :param dependency: function to run as a dependency. 115 | :param use_cache: whether the dependency 116 | can use previously calculated dependencies. 117 | :param kwargs: optional keyword arguments to the dependency. 118 | May be used to parametrize dependencies. 119 | :return: TaskiqDepends instance. 120 | """ 121 | return Dependency( 122 | dependency=dependency, 123 | use_cache=use_cache, 124 | kwargs=kwargs, 125 | ) 126 | 127 | 128 | class Dependency: 129 | """ 130 | Class to mark parameter as a dependency. 131 | 132 | This class is used to mark parameters of a function, 133 | or a class as injectables, so taskiq can resolve it 134 | and calculate before execution. 135 | """ 136 | 137 | def __init__( 138 | self, 139 | dependency: Optional[Union[Type[Any], Callable[..., Any]]] = None, 140 | *, 141 | use_cache: bool = True, 142 | kwargs: Optional[Dict[str, Any]] = None, 143 | signature: Optional[inspect.Parameter] = None, 144 | parent: "Optional[Dependency]" = None, 145 | ) -> None: 146 | self._id = uuid.uuid4() 147 | self.dependency = dependency 148 | self.use_cache = use_cache 149 | self.param_name = "" 150 | self.kwargs = kwargs or {} 151 | self.signature = signature 152 | self.parent = parent 153 | 154 | def __hash__(self) -> int: 155 | return hash(self._id) 156 | 157 | def __eq__(self, rhs: object) -> bool: 158 | """ 159 | Overriden eq operation. 160 | 161 | This is required to perform correct topological 162 | sort after building dependency graph. 163 | 164 | :param rhs: object to compare. 165 | :return: True if objects are equal. 166 | """ 167 | if not isinstance(rhs, Dependency): 168 | return False 169 | return self._id == rhs._id 170 | 171 | def __repr__(self) -> str: 172 | func_name = str(self.dependency) 173 | if self.dependency is not None and hasattr(self.dependency, "__name__"): 174 | func_name = self.dependency.__name__ 175 | return ( 176 | f"Dependency({func_name}, " 177 | f"use_cache={self.use_cache}, " 178 | f"kwargs={self.kwargs}, " 179 | f"parent={self.parent}" 180 | ")" 181 | ) 182 | -------------------------------------------------------------------------------- /tests/test_annotated.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from contextlib import asynccontextmanager, contextmanager 3 | 4 | import pytest 5 | 6 | if sys.version_info < (3, 10): 7 | pytest.skip("Annotated is available only for python 3.10+", allow_module_level=True) 8 | 9 | from typing import Annotated, AsyncGenerator, Generator, Generic, Tuple, TypeVar 10 | 11 | from taskiq_dependencies import DependencyGraph, Depends 12 | 13 | 14 | def test_annotated_func() -> None: 15 | def get_int() -> int: 16 | return 1 17 | 18 | def target_func(dep: Annotated[int, Depends(get_int)]) -> int: 19 | return dep 20 | 21 | with DependencyGraph(target_func).sync_ctx() as ctx: 22 | res = target_func(**ctx.resolve_kwargs()) 23 | assert res == 1 24 | 25 | 26 | def test_annotated_class() -> None: 27 | class TestClass: 28 | pass 29 | 30 | def target_func(dep: Annotated[TestClass, Depends()]) -> TestClass: 31 | return dep 32 | 33 | with DependencyGraph(target_func).sync_ctx() as ctx: 34 | res = target_func(**ctx.resolve_kwargs()) 35 | assert isinstance(res, TestClass) 36 | 37 | 38 | def test_annotated_generic() -> None: 39 | _T = TypeVar("_T") 40 | 41 | class MyClass: 42 | pass 43 | 44 | class MainClass(Generic[_T]): 45 | def __init__(self, val: _T = Depends()) -> None: 46 | self.val = val 47 | 48 | def test_func(a: Annotated[MainClass[MyClass], Depends()]) -> MyClass: 49 | return a.val 50 | 51 | with DependencyGraph(target=test_func).sync_ctx(exception_propagation=False) as g: 52 | value = test_func(**(g.resolve_kwargs())) 53 | 54 | assert isinstance(value, MyClass) 55 | 56 | 57 | @pytest.mark.anyio 58 | async def test_annotated_gen() -> None: 59 | opened = False 60 | closed = False 61 | 62 | def my_gen() -> Generator[int, None, None]: 63 | nonlocal opened, closed 64 | opened = True 65 | 66 | yield 1 67 | 68 | closed = True 69 | 70 | def test_func(dep: Annotated[int, Depends(my_gen)]) -> int: 71 | return dep 72 | 73 | with DependencyGraph(target=test_func).sync_ctx() as sctx: 74 | value = test_func(**sctx.resolve_kwargs()) 75 | assert value == 1 76 | 77 | assert opened and closed 78 | 79 | opened = False 80 | closed = False 81 | 82 | async with DependencyGraph(target=test_func).async_ctx() as actx: 83 | value = test_func(**(await actx.resolve_kwargs())) 84 | assert value == 1 85 | 86 | assert opened and closed 87 | 88 | 89 | @pytest.mark.anyio 90 | async def test_annotated_asyncgen() -> None: 91 | opened = False 92 | closed = False 93 | 94 | async def my_gen() -> AsyncGenerator[int, None]: 95 | nonlocal opened, closed 96 | opened = True 97 | 98 | yield 1 99 | 100 | closed = True 101 | 102 | def test_func(dep: Annotated[int, Depends(my_gen)]) -> int: 103 | return dep 104 | 105 | async with DependencyGraph(target=test_func).async_ctx() as g: 106 | value = test_func(**(await g.resolve_kwargs())) 107 | assert value == 1 108 | 109 | assert opened and closed 110 | 111 | 112 | @pytest.mark.anyio 113 | async def test_annotated_manager() -> None: 114 | opened = False 115 | closed = False 116 | 117 | @contextmanager 118 | def my_gen() -> Generator[int, None, None]: 119 | nonlocal opened, closed 120 | opened = True 121 | 122 | try: 123 | yield 1 124 | finally: 125 | closed = True 126 | 127 | def test_func(dep: Annotated[int, Depends(my_gen)]) -> int: 128 | return dep 129 | 130 | with DependencyGraph(target=test_func).sync_ctx() as sctx: 131 | value = test_func(**sctx.resolve_kwargs()) 132 | assert value == 1 133 | 134 | assert opened and closed 135 | 136 | opened = False 137 | closed = False 138 | 139 | async with DependencyGraph(target=test_func).async_ctx() as actx: 140 | value = test_func(**(await actx.resolve_kwargs())) 141 | assert value == 1 142 | 143 | assert opened and closed 144 | 145 | 146 | @pytest.mark.anyio 147 | async def test_annotated_asyncmanager() -> None: 148 | opened = False 149 | closed = False 150 | 151 | @asynccontextmanager 152 | async def my_gen() -> AsyncGenerator[int, None]: 153 | nonlocal opened, closed 154 | opened = True 155 | 156 | try: 157 | yield 1 158 | finally: 159 | closed = True 160 | 161 | def test_func(dep: Annotated[int, Depends(my_gen)]) -> int: 162 | return dep 163 | 164 | async with DependencyGraph(target=test_func).async_ctx() as g: 165 | value = test_func(**(await g.resolve_kwargs())) 166 | assert value == 1 167 | 168 | assert opened and closed 169 | 170 | 171 | def test_multiple() -> None: 172 | class TestClass: 173 | pass 174 | 175 | MyType = Annotated[TestClass, Depends(use_cache=False)] 176 | 177 | def test_func(dep: MyType, dep2: MyType) -> Tuple[MyType, MyType]: 178 | return dep, dep2 179 | 180 | with DependencyGraph(target=test_func).sync_ctx(exception_propagation=False) as g: 181 | value = test_func(**(g.resolve_kwargs())) 182 | assert value[0] != value[1] 183 | assert isinstance(value[0], TestClass) 184 | assert isinstance(value[1], TestClass) 185 | 186 | 187 | def test_multiple_with_cache() -> None: 188 | class TestClass: 189 | pass 190 | 191 | MyType = Annotated[TestClass, Depends()] 192 | 193 | def test_func(dep: MyType, dep2: MyType) -> Tuple[MyType, MyType]: 194 | return dep, dep2 195 | 196 | with DependencyGraph(target=test_func).sync_ctx(exception_propagation=False) as g: 197 | value = test_func(**(g.resolve_kwargs())) 198 | assert id(value[0]) == id(value[1]) 199 | assert isinstance(value[0], TestClass) 200 | 201 | 202 | def test_override() -> None: 203 | class TestClass: 204 | pass 205 | 206 | MyType = Annotated[TestClass, Depends()] 207 | 208 | def test_func( 209 | dep: MyType, 210 | dep2: Annotated[MyType, Depends(use_cache=False)], 211 | ) -> Tuple[MyType, MyType]: 212 | return dep, dep2 213 | 214 | with DependencyGraph(target=test_func).sync_ctx(exception_propagation=False) as g: 215 | value = test_func(**(g.resolve_kwargs())) 216 | assert id(value[0]) != id(value[1]) 217 | assert isinstance(value[0], TestClass) 218 | assert isinstance(value[1], TestClass) 219 | 220 | 221 | def test_skip_not_decorated_managers() -> None: 222 | """ 223 | Test that synct context skip context managers. 224 | 225 | Tests that even is class implements a context manager, 226 | it won't be called during the context resolution, 227 | because it's not annotated with contextmanager decorator. 228 | """ 229 | 230 | class TestCM: 231 | def __init__(self) -> None: 232 | self.opened = False 233 | 234 | def __enter__(self) -> None: 235 | self.opened = True 236 | 237 | def __exit__(self, *args: object) -> None: 238 | pass 239 | 240 | test_cm = TestCM() 241 | 242 | def get_test_cm() -> TestCM: 243 | nonlocal test_cm 244 | return test_cm 245 | 246 | def target(cm: Annotated[TestCM, Depends(get_test_cm)]) -> None: 247 | pass 248 | 249 | graph = DependencyGraph(target=target) 250 | with graph.sync_ctx() as ctx: 251 | kwargs = ctx.resolve_kwargs() 252 | assert kwargs["cm"] == test_cm 253 | assert not test_cm.opened 254 | 255 | 256 | @pytest.mark.anyio 257 | async def test_skip_not_decorated_async_managers() -> None: 258 | """ 259 | Test that synct context skip context managers. 260 | 261 | Tests that even is class implements a context manager, 262 | it won't be called during the context resolution, 263 | because it's not annotated with contextmanager decorator. 264 | """ 265 | 266 | class TestACM: 267 | def __init__(self) -> None: 268 | self.opened = False 269 | 270 | async def __aenter__(self) -> None: 271 | self.opened = True 272 | 273 | async def __aexit__(self, *args: object) -> None: 274 | pass 275 | 276 | test_acm = TestACM() 277 | 278 | def get_test_acm() -> TestACM: 279 | nonlocal test_acm 280 | return test_acm 281 | 282 | def target(acm: Annotated[TestACM, Depends(get_test_acm)]) -> None: 283 | pass 284 | 285 | graph = DependencyGraph(target=target) 286 | async with graph.async_ctx() as ctx: 287 | kwargs = await ctx.resolve_kwargs() 288 | assert kwargs["acm"] == test_acm 289 | assert not test_acm.opened 290 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Taskiq dependencies 2 | 3 | This project is used to add FastAPI-like dependency injection to projects. 4 | 5 | This project is a part of the taskiq, but it doesn't have any dependencies, 6 | and you can easily integrate it in any project. 7 | 8 | # Installation 9 | 10 | ```bash 11 | pip install taskiq-dependencies 12 | ``` 13 | 14 | # Usage 15 | 16 | Let's imagine you want to add DI in your project. What should you do? 17 | At first we need to create a dependency graph, check if there any cycles 18 | and compute the order of dependencies. This can be done with DependencyGraph. 19 | It does all of those actions on create. So we can remember all graphs at the start of 20 | our program for later use. Or we can do it when needed, but it's less optimal. 21 | 22 | ```python 23 | from taskiq_dependencies import Depends 24 | 25 | 26 | def dep1() -> int: 27 | return 1 28 | 29 | 30 | def target_func(some_int: int = Depends(dep1)): 31 | print(some_int) 32 | return some_int + 1 33 | 34 | ``` 35 | 36 | In this example we have a function called `target_func` and as you can see, it depends on `dep1` dependency. 37 | 38 | To create a dependnecy graph have to write this: 39 | ```python 40 | from taskiq_dependencies import DependencyGraph 41 | 42 | graph = DependencyGraph(target_func) 43 | ``` 44 | 45 | That's it. Now we want to resolve all dependencies and call a function. It's simple as this: 46 | 47 | ```python 48 | with graph.sync_ctx() as ctx: 49 | graph.target(**ctx.resolve_kwargs()) 50 | ``` 51 | 52 | Voila! We resolved all dependencies and called a function with no arguments. 53 | The `resolve_kwargs` function will return a dict, where keys are parameter names, and values are resolved dependencies. 54 | 55 | 56 | ### Async usage 57 | 58 | If your lib is asynchronous, you should use async context, it's similar to sync context, but instead of `with` you should use `async with`. But this way your users can use async dependencies and async generators. It's not possible in sync context. 59 | 60 | 61 | ```python 62 | async with graph.async_ctx() as ctx: 63 | kwargs = await ctx.resolve_kwargs() 64 | ``` 65 | 66 | ## Q&A 67 | 68 | > Why should I use `with` or `async with` statements? 69 | 70 | Becuase users can use generator functions as dependencies. 71 | Everything before `yield` happens before injecting the dependency, and everything after `yield` is executed after the `with` statement is over. 72 | 73 | > How to provide default dependencies? 74 | 75 | It maybe useful to have default dependencies for your project. 76 | For example, taskiq has `Context` and `State` classes that can be used as dependencies. `sync_context` and `async_context` methods have a parameter, where you can pass a dict with precalculated dependencies. 77 | 78 | 79 | ```python 80 | from taskiq_dependencies import Depends, DependencyGraph 81 | 82 | 83 | class DefaultDep: 84 | ... 85 | 86 | 87 | def target_func(dd: DefaultDep = Depends()): 88 | print(dd) 89 | return 1 90 | 91 | 92 | graph = DependencyGraph(target_func) 93 | 94 | with graph.sync_ctx({DefaultDep: DefaultDep()}) as ctx: 95 | print(ctx.resolve_kwargs()) 96 | 97 | ``` 98 | 99 | You can run this code. It will resolve dd dependency into a `DefaultDep` variable you provide. 100 | 101 | 102 | ## Getting parameters information 103 | 104 | If you want to get the information about how this dependency was specified, 105 | you can use special class `ParamInfo` for that. 106 | 107 | ```python 108 | from taskiq_dependencies import Depends, DependencyGraph, ParamInfo 109 | 110 | 111 | def dependency(info: ParamInfo = Depends()) -> str: 112 | assert info.name == "dd" 113 | return info.name 114 | 115 | def target_func(dd: str = Depends(dependency)): 116 | print(dd) 117 | return 1 118 | 119 | 120 | graph = DependencyGraph(target_func) 121 | 122 | with graph.sync_ctx() as ctx: 123 | print(ctx.resolve_kwargs()) 124 | 125 | ``` 126 | 127 | The ParamInfo has the information about name and parameters signature. It's useful if you want to create a dependency that changes based on parameter name, or signature. 128 | 129 | 130 | Also ParamInfo contains the initial graph that was used. 131 | 132 | ## Exception propagation 133 | 134 | By default if error happens within the context, we send this error to the dependency, 135 | so you can close it properly. You can disable this functionality by setting `exception_propagation` parameter to `False`. 136 | 137 | Let's imagine that you want to get a database session from pool and commit after the function is done. 138 | 139 | 140 | ```python 141 | async def get_session(): 142 | session = sessionmaker() 143 | 144 | yield session 145 | 146 | await session.commit() 147 | 148 | ``` 149 | 150 | But what if the error happened when the dependant function was called? In this case you want to rollback, instead of commit. 151 | To solve this problem, you can just wrap the `yield` statement in `try except` to handle the error. 152 | 153 | ```python 154 | async def get_session(): 155 | session = sessionmaker() 156 | 157 | try: 158 | yield session 159 | except Exception: 160 | await session.rollback() 161 | return 162 | 163 | await session.commit() 164 | 165 | ``` 166 | 167 | **Also, as a library developer, you can disable exception propagation**. If you do so, then no exception will ever be propagated to dependencies and no such `try except` expression will ever work. 168 | 169 | 170 | Example of disabled propogation. 171 | 172 | ```python 173 | 174 | graph = DependencyGraph(target_func) 175 | 176 | with graph.sync_ctx(exception_propagation=False) as ctx: 177 | print(ctx.resolve_kwargs()) 178 | 179 | 180 | ``` 181 | 182 | 183 | ## Generics support 184 | 185 | We support generics substitution for class-based dependencies. 186 | For example, let's define an interface and a class. This class can be 187 | parameterized with some type and we consider this type a dependency. 188 | 189 | ```python 190 | import abc 191 | from typing import Any, Generic, TypeVar 192 | 193 | class MyInterface(abc.ABC): 194 | @abc.abstractmethod 195 | def getval(self) -> Any: 196 | ... 197 | 198 | 199 | _T = TypeVar("_T", bound=MyInterface) 200 | 201 | 202 | class MyClass(Generic[_T]): 203 | # We don't know exact type, but we assume 204 | # that it can be used as a dependency. 205 | def __init__(self, resource: _T = Depends()): 206 | self.resource = resource 207 | 208 | @property 209 | def my_value(self) -> Any: 210 | return self.resource.getval() 211 | 212 | ``` 213 | 214 | Now let's create several implementation of defined interface: 215 | 216 | ```python 217 | 218 | def getstr() -> str: 219 | return "strstr" 220 | 221 | 222 | def getint() -> int: 223 | return 100 224 | 225 | 226 | class MyDep1(MyInterface): 227 | def __init__(self, s: str = Depends(getstr)) -> None: 228 | self.s = s 229 | 230 | def getval(self) -> str: 231 | return self.s 232 | 233 | 234 | class MyDep2(MyInterface): 235 | def __init__(self, i: int = Depends(getint)) -> None: 236 | self.i = i 237 | 238 | def getval(self) -> int: 239 | return self.i 240 | 241 | ``` 242 | 243 | Now you can use these dependencies by just setting proper type hints. 244 | 245 | ```python 246 | def my_target( 247 | d1: MyClass[MyDep1] = Depends(), 248 | d2: MyClass[MyDep2] = Depends(), 249 | ) -> None: 250 | print(d1.my_value) 251 | print(d2.my_value) 252 | 253 | 254 | with DependencyGraph(my_target).sync_ctx() as ctx: 255 | my_target(**ctx.resolve_kwargs()) 256 | 257 | ``` 258 | 259 | This code will is going to print: 260 | 261 | ``` 262 | strstr 263 | 100 264 | ``` 265 | 266 | ## Dependencies replacement 267 | 268 | You can replace dependencies in runtime, it will recalculate graph 269 | and will execute your function with updated dependencies. 270 | 271 | **!!! This functionality tremendously slows down dependency resolution.** 272 | 273 | Use this functionality only for tests. Otherwise, you will end up building dependency graphs on every resolution request. Which is very slow. 274 | 275 | But for tests it may be a game changer, since you don't want to change your code, but some dependencies instead. 276 | 277 | Here's an example. Imagine you have a built graph for a specific function, like this: 278 | 279 | ```python 280 | from taskiq_dependencies import DependencyGraph, Depends 281 | 282 | 283 | def dependency() -> int: 284 | return 1 285 | 286 | 287 | def target(dep_value: int = Depends(dependency)) -> None: 288 | assert dep_value == 1 289 | 290 | graph = DependencyGraph(target) 291 | ``` 292 | 293 | Normally, you would call the target, by writing something like this: 294 | 295 | ```python 296 | with graph.sync_ctx() as ctx: 297 | target(**ctx.resolve_kwargs()) 298 | ``` 299 | 300 | But what if you want to replace dependency in runtime, just 301 | before resolving kwargs? The solution is to add `replaced_deps` 302 | parameter to the context method. For example: 303 | 304 | ```python 305 | def replaced() -> int: 306 | return 2 307 | 308 | 309 | with graph.sync_ctx(replaced_deps={dependency: replaced}) as ctx: 310 | target(**ctx.resolve_kwargs()) 311 | ``` 312 | 313 | Furthermore, the new dependency can depend on other dependencies. Or you can change type of your dependency, like generator instead of plain return. Everything should work as you would expect it. 314 | 315 | ## Annotated types 316 | 317 | Taskiq dependenices also support dependency injection through Annotated types. 318 | 319 | ```python 320 | from typing import Annotated 321 | 322 | async def my_function(dependency: Annotated[int, Depends(my_func)]): 323 | pass 324 | ``` 325 | 326 | Or you can specify classes 327 | 328 | 329 | ```python 330 | from typing import Annotated 331 | 332 | class MyClass: 333 | pass 334 | 335 | async def my_function(dependency: Annotated[MyClass, Depends(my_func)]): 336 | pass 337 | ``` 338 | 339 | And, of course you can easily save such type aliases in variables. 340 | 341 | ```python 342 | from typing import Annotated 343 | 344 | DepType = Annotated[int, Depends(my_func)] 345 | 346 | def my_function(dependency: DepType): 347 | pass 348 | 349 | ``` 350 | 351 | Also we support overrides for annotated types. 352 | 353 | For example: 354 | 355 | ```python 356 | from typing import Annotated 357 | 358 | DepType = Annotated[int, Depends(my_func)] 359 | 360 | def my_function( 361 | dependency: DepType, 362 | no_cache_dep: Annotated[DepType, Depends(my_func, use_cache=False)], 363 | ) -> None: 364 | pass 365 | 366 | ``` 367 | 368 | Also, please note that if you're using `from __future__ import annotations` it won't work for python <= 3.9. Because the `inspect.signature` function doesn't support it. In all future versions it will work as expected. 369 | -------------------------------------------------------------------------------- /taskiq_dependencies/graph.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import sys 3 | import warnings 4 | from collections import defaultdict, deque 5 | from pathlib import Path 6 | from typing import Any, Callable, Dict, List, Optional, TypeVar, get_type_hints 7 | 8 | from graphlib import TopologicalSorter 9 | 10 | from taskiq_dependencies.ctx import AsyncResolveContext, SyncResolveContext 11 | from taskiq_dependencies.dependency import Dependency 12 | from taskiq_dependencies.utils import ParamInfo 13 | 14 | try: 15 | from fastapi.params import Depends as FastapiDepends 16 | except ImportError: 17 | FastapiDepends = None 18 | 19 | 20 | class DependencyGraph: 21 | """Class to build dependency graph from a function.""" 22 | 23 | dep_graph = True 24 | 25 | def __init__( 26 | self, 27 | target: Callable[..., Any], 28 | replaced_deps: Optional[Dict[Any, Any]] = None, 29 | ) -> None: 30 | self.target = target 31 | # Ordinary dependencies with cache. 32 | self.dependencies: Dict[Any, List[Dependency]] = defaultdict(list) 33 | # Dependencies without cache. 34 | # Can be considered as sub graphs. 35 | self.subgraphs: Dict[Any, DependencyGraph] = {} 36 | self.ordered_deps: List[Dependency] = [] 37 | self.replaced_deps = replaced_deps 38 | self._build_graph() 39 | 40 | def is_empty(self) -> bool: 41 | """ 42 | Checks that target function depends on at least something. 43 | 44 | :return: True if depends. 45 | """ 46 | return len(self.ordered_deps) <= 1 47 | 48 | def async_ctx( 49 | self, 50 | initial_cache: Optional[Dict[Any, Any]] = None, 51 | replaced_deps: Optional[Dict[Any, Any]] = None, 52 | exception_propagation: bool = True, 53 | ) -> AsyncResolveContext: 54 | """ 55 | Create dependency resolver context. 56 | 57 | This context is used to actually resolve dependencies. 58 | 59 | :param initial_cache: initial cache dict. 60 | :param exception_propagation: If true, all found errors within 61 | context will be propagated to dependencies. 62 | :param replaced_deps: Dependencies to replace during runtime. 63 | :return: new resolver context. 64 | """ 65 | graph = self 66 | if replaced_deps: 67 | graph = DependencyGraph(self.target, replaced_deps) 68 | return AsyncResolveContext( 69 | graph, 70 | graph, 71 | initial_cache, 72 | exception_propagation, 73 | ) 74 | 75 | def sync_ctx( 76 | self, 77 | initial_cache: Optional[Dict[Any, Any]] = None, 78 | replaced_deps: Optional[Dict[Any, Any]] = None, 79 | exception_propagation: bool = True, 80 | ) -> SyncResolveContext: 81 | """ 82 | Create dependency resolver context. 83 | 84 | This context is used to actually resolve dependencies. 85 | 86 | :param initial_cache: initial cache dict. 87 | :param exception_propagation: If true, all found errors within 88 | context will be propagated to dependencies. 89 | :param replaced_deps: Dependencies to replace during runtime. 90 | :return: new resolver context. 91 | """ 92 | graph = self 93 | if replaced_deps: 94 | graph = DependencyGraph(self.target, replaced_deps) 95 | return SyncResolveContext( 96 | graph, 97 | graph, 98 | initial_cache, 99 | exception_propagation, 100 | ) 101 | 102 | def _build_graph(self) -> None: # noqa: C901 103 | """ 104 | Builds actual graph. 105 | 106 | This function collects all dependencies 107 | and adds it the the _deps variable. 108 | 109 | After all dependencies are found, 110 | it runs topological sort, to get the 111 | dependency resolving order. 112 | 113 | :raises ValueError: if something happened. 114 | """ 115 | dep_deque = deque([Dependency(self.target, use_cache=True)]) 116 | # This is for `from __future__ import annotations` support. 117 | # We need to use `eval_str` argument, because 118 | # signature of the function is a string, not an object. 119 | signature_kwargs: Dict[str, Any] = {} 120 | if sys.version_info >= (3, 10): 121 | signature_kwargs["eval_str"] = True 122 | 123 | while dep_deque: 124 | dep = dep_deque.popleft() 125 | # Skip adding dependency if it's already present. 126 | if dep in self.dependencies: 127 | continue 128 | if dep.dependency is None: 129 | continue 130 | # If we have replaced dependencies, we need to replace 131 | # them in the current dependency. 132 | if self.replaced_deps and dep.dependency in self.replaced_deps: 133 | dep.dependency = self.replaced_deps[dep.dependency] 134 | # We can say for sure that ParamInfo doesn't have any dependencies, 135 | # so we skip it. 136 | if dep.dependency == ParamInfo: 137 | continue 138 | # Get signature and type hints. 139 | origin = getattr(dep.dependency, "__origin__", None) 140 | if origin is None: 141 | origin = dep.dependency 142 | 143 | # If we found the typevar. 144 | # It means, that somebody depend on generic type. 145 | if isinstance(origin, TypeVar): 146 | if dep.parent is None: 147 | raise ValueError(f"Cannot resolve generic {dep.dependency}") 148 | parent_cls = dep.parent.dependency 149 | parent_cls_origin = getattr(parent_cls, "__origin__", None) 150 | # If we cannot find origin, than means, that we cannot resolve 151 | # generic parameters. So exiting. 152 | if parent_cls_origin is None: 153 | raise ValueError( 154 | f"Unknown generic argument {origin}. " 155 | f"Please provide a type in param `{dep.parent.param_name}`" 156 | f" of `{dep.parent.dependency}`", 157 | ) 158 | # We zip together names of parameters and the substituted values 159 | # for generics. 160 | generics = zip( 161 | parent_cls_origin.__parameters__, 162 | parent_cls.__args__, # type: ignore 163 | ) 164 | for tvar, type_param in generics: 165 | # If we found the typevar we're currently try to resolve, 166 | # we need to find origin of the substituted class. 167 | if tvar == origin: 168 | dep.dependency = type_param 169 | origin = getattr(type_param, "__origin__", None) 170 | if origin is None: 171 | origin = type_param 172 | 173 | if inspect.isclass(origin): 174 | # If this is a class, we need to get signature of 175 | # an __init__ method. 176 | try: 177 | hints = get_type_hints(origin.__init__) 178 | except NameError: 179 | _, src_lineno = inspect.getsourcelines(origin) 180 | src_file = Path(inspect.getfile(origin)) 181 | cwd = Path.cwd() 182 | if src_file.is_relative_to(cwd): 183 | src_file = src_file.relative_to(cwd) 184 | warnings.warn( 185 | "Cannot resolve type hints for " 186 | f"a class {origin.__name__} defined " 187 | f"at {src_file}:{src_lineno}.", 188 | RuntimeWarning, 189 | stacklevel=2, 190 | ) 191 | continue 192 | sign = inspect.signature( 193 | origin.__init__, 194 | **signature_kwargs, 195 | ) 196 | elif inspect.isfunction(dep.dependency): 197 | # If this is function or an instance of a class, we get it's type hints. 198 | try: 199 | hints = get_type_hints(dep.dependency) 200 | except NameError: 201 | _, src_lineno = inspect.getsourcelines(dep.dependency) # type: ignore 202 | src_file = Path(inspect.getfile(dep.dependency)) 203 | cwd = Path.cwd() 204 | if src_file.is_relative_to(cwd): 205 | src_file = src_file.relative_to(cwd) 206 | warnings.warn( 207 | "Cannot resolve type hints for " 208 | f"a function {dep.dependency.__name__} defined " 209 | f"at {src_file}:{src_lineno}.", 210 | RuntimeWarning, 211 | stacklevel=2, 212 | ) 213 | continue 214 | sign = inspect.signature(origin, **signature_kwargs) # type: ignore 215 | else: 216 | try: 217 | hints = get_type_hints( 218 | dep.dependency.__call__, # type: ignore 219 | ) 220 | except NameError: 221 | _, src_lineno = inspect.getsourcelines(dep.dependency.__class__) 222 | src_file = Path(inspect.getfile(dep.dependency.__class__)) 223 | cwd = Path.cwd() 224 | if src_file.is_relative_to(cwd): 225 | src_file = src_file.relative_to(cwd) 226 | cls_name = dep.dependency.__class__.__name__ 227 | warnings.warn( 228 | "Cannot resolve type hints for " 229 | f"an object of class {cls_name} defined " 230 | f"at {src_file}:{src_lineno}.", 231 | RuntimeWarning, 232 | stacklevel=2, 233 | ) 234 | continue 235 | sign = inspect.signature(origin, **signature_kwargs) # type: ignore 236 | 237 | # Now we need to iterate over parameters, to 238 | # find all parameters, that have TaskiqDepends as it's 239 | # default vaule. 240 | for param_name, param in sign.parameters.items(): 241 | default_value = param.default 242 | if hasattr(param.annotation, "__metadata__"): 243 | # We go backwards, 244 | # because you may want to override your annotation 245 | # and the overriden value will appear to be after 246 | # the original `Depends` annotation. 247 | for meta in reversed(param.annotation.__metadata__): 248 | if isinstance(meta, Dependency): 249 | default_value = meta 250 | break 251 | if FastapiDepends is not None and isinstance( 252 | meta, 253 | FastapiDepends, 254 | ): 255 | default_value = meta 256 | break 257 | 258 | # This is for FastAPI integration. So you can 259 | # use Depends from taskiq mixed with fastapi's dependencies. 260 | if FastapiDepends is not None and isinstance( 261 | default_value, 262 | FastapiDepends, 263 | ): 264 | default_value = Dependency( 265 | dependency=default_value.dependency, 266 | use_cache=default_value.use_cache, 267 | signature=param, 268 | ) 269 | 270 | # We check, that default value is an instance of 271 | # TaskiqDepends. 272 | if not isinstance(default_value, Dependency): 273 | continue 274 | # If user haven't set the dependency, 275 | # using TaskiqDepends constructor, 276 | # we need to find variable's type hint. 277 | if default_value.dependency is None: 278 | if hints.get(param_name) is None: 279 | # In this case, we don't know anything 280 | # about this dependency. And it cannot be resolved. 281 | dep_mod = "unknown" 282 | dep_name = "unknown" 283 | if dep.dependency is not None: 284 | dep_mod = dep.dependency.__module__ 285 | if inspect.isclass(dep.dependency): 286 | dep_name = dep.dependency.__class__.__name__ 287 | else: 288 | dep_name = dep.dependency.__name__ 289 | raise ValueError( 290 | f"The dependency {param_name} of " 291 | f"{dep_mod}:{dep_name} cannot be resolved.", 292 | ) 293 | # We get dependency class from typehint. 294 | dependency_func = hints[param_name] 295 | else: 296 | # We can get dependency by simply using 297 | # user supplied function. 298 | dependency_func = default_value.dependency 299 | 300 | # Now we construct new TaskiqDepends instance 301 | # with correct dependency function and cache. 302 | dep_obj = Dependency( 303 | dependency_func, 304 | use_cache=default_value.use_cache, 305 | kwargs=default_value.kwargs, 306 | signature=param, 307 | parent=dep, 308 | ) 309 | # Also we set the parameter name, 310 | # it will help us in future when 311 | # we're going to resolve all dependencies. 312 | dep_obj.param_name = param_name 313 | 314 | # We append current dependency 315 | # to the list of dependencies of 316 | # the current function. 317 | self.dependencies[dep].append(dep_obj) 318 | if dep_obj.use_cache: 319 | # If this dependency uses cache, we need to resolve 320 | # it's dependencies further. 321 | dep_deque.append(dep_obj) 322 | else: 323 | # If this dependency doesn't use caches, 324 | # we build a subgraph for this dependency. 325 | self.subgraphs[dep_obj] = DependencyGraph( 326 | dependency_func, 327 | ) 328 | # Now we perform topological sort of all dependencies. 329 | # Now we know the order we'll be using to resolve dependencies. 330 | self.ordered_deps = list(TopologicalSorter(self.dependencies).static_order()) 331 | -------------------------------------------------------------------------------- /taskiq_dependencies/ctx.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import inspect 3 | from collections import defaultdict 4 | from copy import copy 5 | from logging import getLogger 6 | from typing import TYPE_CHECKING, Any, DefaultDict, Dict, Generator, List, Optional 7 | 8 | from taskiq_dependencies.utils import ParamInfo, isasynccontextmanager, iscontextmanager 9 | 10 | if TYPE_CHECKING: 11 | from taskiq_dependencies.graph import DependencyGraph # pragma: no cover 12 | 13 | 14 | logger = getLogger("taskiq.dependencies.ctx") 15 | 16 | 17 | class BaseResolveContext: 18 | """Base resolver context.""" 19 | 20 | def __init__( 21 | self, 22 | graph: "DependencyGraph", 23 | main_graph: "DependencyGraph", 24 | initial_cache: Optional[Dict[Any, Any]] = None, 25 | exception_propagation: bool = True, 26 | ) -> None: 27 | self.graph = graph 28 | # Main graph that contains all the subgraphs. 29 | self.main_graph = main_graph 30 | self.opened_dependencies: List[Any] = [] 31 | self.sub_contexts: "List[Any]" = [] 32 | self.initial_cache = initial_cache or {} 33 | self.propagate_excs = exception_propagation 34 | 35 | def traverse_deps( # noqa: C901 36 | self, 37 | ) -> "Generator[DependencyGraph | Any, None, Dict[str, Any]]": 38 | """ 39 | This function is used to traverse all dependencies and resolve them. 40 | 41 | It travels to all dependencies, everytime it need to resolve 42 | something it yields it and waits for the resolving result. 43 | 44 | :return: resolved kwargs. 45 | :yield: a function or a graph to resolve. 46 | """ 47 | # If we have nothing to calculate, we return 48 | # an empty dict. 49 | if self.graph.is_empty(): 50 | return {} 51 | kwargs: Dict[str, Any] = {} 52 | # We need to copy cache, in order 53 | # to separate dependencies that use cache, 54 | # from dependencies that aren't. 55 | cache = copy(self.initial_cache) 56 | # Cache for all dependencies with kwargs. 57 | kwargs_cache: "DefaultDict[Any, List[Any]]" = defaultdict(list) 58 | # We iterate over topologicaly sorted list of dependencies. 59 | for index, dep in enumerate(self.graph.ordered_deps): 60 | # If this dependency doesn't use cache, 61 | # we don't need to calculate it, since it may be met 62 | # later. 63 | if not dep.use_cache: 64 | continue 65 | # If somehow we have dependency with unknown function. 66 | if dep.dependency is None: 67 | continue 68 | # If dependency is already calculated. 69 | if dep.dependency in cache: 70 | continue 71 | # For dependencies with kwargs we check kwarged cache. 72 | elif dep.kwargs and dep.dependency in kwargs_cache: 73 | cache_hit = False 74 | # We have to iterate over all cached dependencies with 75 | # kwargs, because users may pass unhashable objects as kwargs. 76 | # That's why we cannot use them as dict keys. 77 | for cached_kwargs, _ in kwargs_cache[dep.dependency]: 78 | if cached_kwargs == dep.kwargs: 79 | cache_hit = True 80 | break 81 | if cache_hit: 82 | continue 83 | 84 | kwargs = {} 85 | # Now we get list of dependencies for current top-level dependency 86 | # and iterate over it. 87 | for subdep in self.graph.dependencies[dep]: 88 | # If we don't have known dependency function, 89 | # we skip it. 90 | if subdep.dependency is None: 91 | continue 92 | # If the user want to get ParamInfo, 93 | # we get declaration of the current dependency. 94 | if subdep.dependency == ParamInfo: 95 | kwargs[subdep.param_name] = ParamInfo( 96 | dep.param_name, 97 | self.main_graph, 98 | dep.signature, 99 | ) 100 | continue 101 | if subdep.use_cache: 102 | # If this dependency can be calculated, using cache, 103 | # we try to get it from cache. 104 | if subdep.kwargs and subdep.dependency in kwargs_cache: 105 | for cached_kwargs, kw_cache in kwargs_cache[subdep.dependency]: 106 | if cached_kwargs == subdep.kwargs: 107 | kwargs[subdep.param_name] = kw_cache 108 | break 109 | else: 110 | kwargs[subdep.param_name] = cache[subdep.dependency] 111 | else: 112 | # If this dependency doesn't use cache, 113 | # we resolve it's dependencies and 114 | # run it. 115 | resolved_kwargs = yield self.graph.subgraphs[subdep] 116 | # Subgraph wasn't resolved. 117 | if resolved_kwargs is None: 118 | continue 119 | if subdep.kwargs: 120 | resolved_kwargs.update(subdep.kwargs) 121 | kwargs[subdep.param_name] = yield subdep.dependency( 122 | **resolved_kwargs, 123 | ) 124 | 125 | # We don't want to calculate least function, 126 | # Because it's a target function. 127 | if ( 128 | index < len(self.graph.ordered_deps) - 1 129 | # We skip all ParamInfo dependencies, 130 | # because we calculate them when needed. 131 | and dep.dependency != ParamInfo 132 | ): 133 | user_kwargs = copy(dep.kwargs) 134 | user_kwargs.update(kwargs) 135 | resolved = yield dep.dependency(**user_kwargs) 136 | if dep.kwargs: 137 | kwargs_cache[dep.dependency].append((dep.kwargs, resolved)) 138 | else: 139 | cache[dep.dependency] = resolved 140 | return kwargs 141 | 142 | 143 | class SyncResolveContext(BaseResolveContext): 144 | """ 145 | Resolver context. 146 | 147 | This class is used to resolve dependencies 148 | with custom initial caches. 149 | 150 | The main idea is to separate resolving and graph building. 151 | It uses graph, but it doesn't modify it. 152 | """ 153 | 154 | def __enter__(self) -> "SyncResolveContext": 155 | return self 156 | 157 | def __exit__(self, *args: object) -> None: 158 | self.close(*args) 159 | 160 | def close(self, *args: Any) -> None: 161 | """ 162 | Close all opened dependencies. 163 | 164 | This function runs teardown of all dependencies. 165 | 166 | :param args: exception info if any. 167 | """ 168 | exception_found = False 169 | if self.propagate_excs and len(args) > 1 and args[1] is not None: 170 | exception_found = True 171 | for ctx in self.sub_contexts: 172 | ctx.close(*args) 173 | for dep in reversed(self.opened_dependencies): 174 | if inspect.isgenerator(dep): 175 | if exception_found: 176 | try: 177 | dep.throw(*args) 178 | except StopIteration: 179 | continue 180 | except BaseException as exc: 181 | logger.warning( 182 | "Exception found on dependency teardown %s", 183 | exc, 184 | exc_info=True, 185 | ) 186 | continue 187 | continue 188 | for _ in dep: 189 | pass 190 | elif iscontextmanager(dep): 191 | dep.__exit__(*args) 192 | 193 | def resolver(self, executed_func: Any, initial_cache: Dict[Any, Any]) -> Any: 194 | """ 195 | Sync resolver. 196 | 197 | This function is used to execute functions 198 | to resolve dependencies. 199 | 200 | :param executed_func: function to resolve. 201 | :param initial_cache: cache to build a context if graph was passed. 202 | :raises RuntimeError: if async function is passed as the dependency. 203 | 204 | :return: dict with resolved kwargs. 205 | """ 206 | if getattr(executed_func, "dep_graph", False): 207 | ctx = SyncResolveContext(executed_func, self.main_graph, initial_cache) 208 | self.sub_contexts.append(ctx) 209 | sub_result = ctx.resolve_kwargs() 210 | elif inspect.isgenerator(executed_func): 211 | sub_result = next(executed_func) 212 | self.opened_dependencies.append(executed_func) 213 | elif asyncio.iscoroutine(executed_func): 214 | raise RuntimeError( 215 | "Coroutines cannot be used in sync context. " 216 | "Please use async context instead.", 217 | ) 218 | elif iscontextmanager(executed_func): 219 | sub_result = executed_func.__enter__() 220 | self.opened_dependencies.append(executed_func) 221 | elif inspect.isasyncgen(executed_func) or isasynccontextmanager(executed_func): 222 | raise RuntimeError( 223 | "Coroutines cannot be used in sync context. " 224 | "Please use async context instead.", 225 | ) 226 | else: 227 | sub_result = executed_func 228 | return sub_result 229 | 230 | def resolve_kwargs( 231 | self, 232 | ) -> Dict[str, Any]: 233 | """ 234 | Resolve dependencies and return them as a dict. 235 | 236 | This function runs all dependencies 237 | and calculates key word arguments required to run target function. 238 | 239 | :return: Dict with keyword arguments. 240 | """ 241 | try: 242 | generator = self.traverse_deps() 243 | dependency = generator.send(None) 244 | while True: 245 | kwargs = self.resolver(dependency, self.initial_cache) 246 | dependency = generator.send(kwargs) 247 | except StopIteration as exc: 248 | return exc.value # type: ignore 249 | 250 | 251 | class AsyncResolveContext(BaseResolveContext): 252 | """ 253 | Resolver context. 254 | 255 | This class is used to resolve dependencies 256 | with custom initial caches. 257 | 258 | The main idea is to separate resolving and graph building. 259 | It uses graph, but it doesn't modify it. 260 | """ 261 | 262 | async def __aenter__(self) -> "AsyncResolveContext": 263 | return self 264 | 265 | async def __aexit__(self, *args: object) -> None: 266 | await self.close(*args) 267 | 268 | async def close(self, *args: Any) -> None: # noqa: C901 269 | """ 270 | Close all opened dependencies. 271 | 272 | This function runs teardown of all dependencies. 273 | 274 | :param args: exception info if any. 275 | """ 276 | exception_found = False 277 | if self.propagate_excs and len(args) > 1 and args[1] is not None: 278 | exception_found = True 279 | for ctx in self.sub_contexts: 280 | await ctx.close(*args) # type: ignore 281 | for dep in reversed(self.opened_dependencies): 282 | if inspect.isgenerator(dep): 283 | if exception_found: 284 | try: 285 | dep.throw(*args) 286 | except StopIteration: 287 | continue 288 | except BaseException as exc: 289 | logger.warning( 290 | "Exception found on dependency teardown %s", 291 | exc, 292 | exc_info=True, 293 | ) 294 | continue 295 | continue 296 | for _ in dep: 297 | pass 298 | elif inspect.isasyncgen(dep): 299 | if exception_found: 300 | try: 301 | await dep.athrow(*args) 302 | except StopAsyncIteration: 303 | continue 304 | except BaseException as exc: 305 | logger.warning( 306 | "Exception found on dependency teardown %s", 307 | exc, 308 | exc_info=True, 309 | ) 310 | continue 311 | continue 312 | async for _ in dep: 313 | pass 314 | elif iscontextmanager(dep): 315 | dep.__exit__(*args) 316 | elif isasynccontextmanager(dep): 317 | await dep.__aexit__(*args) 318 | 319 | async def resolver( 320 | self, 321 | executed_func: Any, 322 | initial_cache: Dict[Any, Any], 323 | ) -> Any: 324 | """ 325 | Async resolver. 326 | 327 | This function is used to execute functions 328 | to resolve dependencies. 329 | 330 | :param executed_func: function to resolve. 331 | :param initial_cache: cache to build a context if graph was passed. 332 | :return: dict with resolved kwargs. 333 | """ 334 | if getattr(executed_func, "dep_graph", False): 335 | ctx = AsyncResolveContext(executed_func, self.main_graph, initial_cache) # type: ignore 336 | self.sub_contexts.append(ctx) 337 | sub_result = await ctx.resolve_kwargs() 338 | elif inspect.isgenerator(executed_func): 339 | sub_result = next(executed_func) 340 | self.opened_dependencies.append(executed_func) 341 | elif asyncio.iscoroutine(executed_func): 342 | sub_result = await executed_func 343 | elif inspect.isasyncgen(executed_func): 344 | sub_result = await executed_func.__anext__() 345 | self.opened_dependencies.append(executed_func) 346 | elif iscontextmanager(executed_func): 347 | sub_result = executed_func.__enter__() 348 | self.opened_dependencies.append(executed_func) 349 | elif isasynccontextmanager(executed_func): 350 | sub_result = await executed_func.__aenter__() 351 | self.opened_dependencies.append(executed_func) 352 | else: 353 | sub_result = executed_func 354 | return sub_result 355 | 356 | async def resolve_kwargs( 357 | self, 358 | ) -> Dict[str, Any]: 359 | """ 360 | Resolve dependencies and return them as a dict. 361 | 362 | This function runs all dependencies 363 | and calculates key word arguments required to run target function. 364 | 365 | :return: Dict with keyword arguments. 366 | """ 367 | try: 368 | generator = self.traverse_deps() 369 | dependency = generator.send(None) 370 | while True: 371 | kwargs = await self.resolver(dependency, self.initial_cache) 372 | dependency = generator.send(kwargs) 373 | except StopIteration as exc: 374 | return exc.value # type: ignore 375 | -------------------------------------------------------------------------------- /tests/test_graph.py: -------------------------------------------------------------------------------- 1 | import re 2 | import uuid 3 | from contextlib import asynccontextmanager, contextmanager 4 | from typing import ( 5 | TYPE_CHECKING, 6 | Any, 7 | AsyncGenerator, 8 | Generator, 9 | Generic, 10 | Tuple, 11 | TypeVar, 12 | ) 13 | 14 | import pytest 15 | 16 | from taskiq_dependencies import DependencyGraph, Depends, ParamInfo 17 | 18 | 19 | @pytest.mark.anyio 20 | async def test_dependency_successful() -> None: 21 | """Test that a simlpe dependencies work.""" 22 | 23 | def dep1() -> int: 24 | return 1 25 | 26 | def testfunc(a: int = Depends(dep1)) -> int: 27 | return a 28 | 29 | with DependencyGraph(testfunc).sync_ctx({}) as sctx: 30 | assert sctx.resolve_kwargs() == {"a": 1} 31 | 32 | async with DependencyGraph(testfunc).async_ctx({}) as actx: 33 | assert await actx.resolve_kwargs() == {"a": 1} 34 | 35 | 36 | @pytest.mark.anyio 37 | async def test_dependency_async_successful() -> None: 38 | """Test that async dependencies work fine.""" 39 | 40 | async def dep1() -> int: 41 | return 1 42 | 43 | def testfunc(a: int = Depends(dep1)) -> int: 44 | return a 45 | 46 | with DependencyGraph(testfunc).sync_ctx({}) as sctx, pytest.warns( 47 | match=re.compile(".*was never awaited.*"), 48 | ), pytest.raises(RuntimeError): 49 | assert sctx.resolve_kwargs() == {"a": 1} 50 | 51 | async with DependencyGraph(testfunc).async_ctx({}) as actx: 52 | assert await actx.resolve_kwargs() == {"a": 1} 53 | 54 | 55 | @pytest.mark.anyio 56 | async def test_dependency_gen_successful() -> None: 57 | """Tests that generators work as expected.""" 58 | starts = 0 59 | closes = 0 60 | 61 | def dep1() -> Generator[int, None, None]: 62 | nonlocal starts 63 | nonlocal closes 64 | 65 | starts += 1 66 | 67 | yield 1 68 | 69 | closes += 1 70 | 71 | def testfunc(a: int = Depends(dep1)) -> int: 72 | return a 73 | 74 | with DependencyGraph(testfunc).sync_ctx({}) as sctx: 75 | assert sctx.resolve_kwargs() == {"a": 1} 76 | assert starts == 1 77 | assert closes == 0 78 | starts = 0 79 | assert closes == 1 80 | closes = 0 81 | 82 | async with DependencyGraph(testfunc).async_ctx({}) as actx: 83 | assert await actx.resolve_kwargs() == {"a": 1} 84 | assert starts == 1 85 | assert closes == 0 86 | assert closes == 1 87 | 88 | 89 | @pytest.mark.anyio 90 | async def test_dependency_async_gen_successful() -> None: 91 | """This test checks that async generators work.""" 92 | starts = 0 93 | closes = 0 94 | 95 | async def dep1() -> AsyncGenerator[int, None]: 96 | nonlocal starts 97 | nonlocal closes 98 | 99 | starts += 1 100 | 101 | yield 1 102 | 103 | closes += 1 104 | 105 | def testfunc(a: int = Depends(dep1)) -> int: 106 | return a 107 | 108 | with DependencyGraph(testfunc).sync_ctx({}) as sctx, pytest.raises(RuntimeError): 109 | assert sctx.resolve_kwargs() == {"a": 1} 110 | 111 | async with DependencyGraph(testfunc).async_ctx({}) as actx: 112 | assert await actx.resolve_kwargs() == {"a": 1} 113 | assert starts == 1 114 | assert closes == 0 115 | assert closes == 1 116 | 117 | 118 | @pytest.mark.anyio 119 | async def test_dependency_contextmanager_successful() -> None: 120 | """Tests that contextmanagers work as expected.""" 121 | starts = 0 122 | closes = 0 123 | 124 | @contextmanager 125 | def dep1() -> Generator[int, None, None]: 126 | nonlocal starts 127 | nonlocal closes 128 | 129 | starts += 1 130 | 131 | try: 132 | yield 1 133 | finally: 134 | closes += 1 135 | 136 | def testfunc(a: int = Depends(dep1)) -> int: 137 | return a 138 | 139 | with DependencyGraph(testfunc).sync_ctx({}) as sctx: 140 | assert sctx.resolve_kwargs() == {"a": 1} 141 | assert starts == 1 142 | assert closes == 0 143 | starts = 0 144 | assert closes == 1 145 | closes = 0 146 | 147 | async with DependencyGraph(testfunc).async_ctx({}) as actx: 148 | assert await actx.resolve_kwargs() == {"a": 1} 149 | assert starts == 1 150 | assert closes == 0 151 | assert closes == 1 152 | 153 | 154 | @pytest.mark.anyio 155 | async def test_dependency_async_manager_successful() -> None: 156 | """This test checks that async contextmanagers work.""" 157 | starts = 0 158 | closes = 0 159 | 160 | @asynccontextmanager 161 | async def dep1() -> AsyncGenerator[int, None]: 162 | nonlocal starts 163 | nonlocal closes 164 | 165 | starts += 1 166 | 167 | try: 168 | yield 1 169 | finally: 170 | closes += 1 171 | 172 | def testfunc(a: int = Depends(dep1)) -> int: 173 | return a 174 | 175 | with DependencyGraph(testfunc).sync_ctx({}) as sctx, pytest.raises(RuntimeError): 176 | assert sctx.resolve_kwargs() == {"a": 1} 177 | 178 | async with DependencyGraph(testfunc).async_ctx({}) as actx: 179 | assert await actx.resolve_kwargs() == {"a": 1} 180 | assert starts == 1 181 | assert closes == 0 182 | assert closes == 1 183 | 184 | 185 | @pytest.mark.anyio 186 | async def test_dependency_subdeps() -> None: 187 | """Tests how subdependencies work.""" 188 | 189 | def dep1() -> int: 190 | return 1 191 | 192 | def dep2(a: int = Depends(dep1)) -> int: 193 | return a + 1 194 | 195 | def testfunc(a: int = Depends(dep2)) -> int: 196 | return a 197 | 198 | with DependencyGraph(testfunc).sync_ctx({}) as sctx: 199 | assert sctx.resolve_kwargs() == {"a": 2} 200 | 201 | async with DependencyGraph(testfunc).async_ctx({}) as actx: 202 | assert await actx.resolve_kwargs() == {"a": 2} 203 | 204 | 205 | @pytest.mark.anyio 206 | async def test_dependency_caches() -> None: 207 | """ 208 | Tests how caches work. 209 | 210 | This test checks that 211 | if multiple functions depend on one function, 212 | This function must be calculated only once. 213 | """ 214 | dep_exec = 0 215 | 216 | def dep1() -> int: 217 | nonlocal dep_exec 218 | dep_exec += 1 219 | 220 | return 1 221 | 222 | def dep2(a: int = Depends(dep1)) -> int: 223 | return a + 1 224 | 225 | def dep3(a: int = Depends(dep1)) -> int: 226 | return a + 1 227 | 228 | def testfunc( 229 | a: int = Depends(dep2), 230 | b: int = Depends(dep3), 231 | ) -> int: 232 | return a + b 233 | 234 | with DependencyGraph(testfunc).sync_ctx({}) as sctx: 235 | assert sctx.resolve_kwargs() == {"a": 2, "b": 2} 236 | 237 | assert dep_exec == 1 238 | dep_exec = 0 239 | 240 | async with DependencyGraph(testfunc).async_ctx({}) as actx: 241 | assert await actx.resolve_kwargs() == {"a": 2, "b": 2} 242 | 243 | assert dep_exec == 1 244 | 245 | 246 | @pytest.mark.anyio 247 | async def test_dependency_subgraph() -> None: 248 | """ 249 | Tests how subgraphs work. 250 | 251 | If use_cache is False it must force 252 | dependency graph to reevaluate it's subdependencies. 253 | """ 254 | dep_exec = 0 255 | 256 | def dep1() -> int: 257 | nonlocal dep_exec 258 | dep_exec += 1 259 | 260 | return 1 261 | 262 | def dep2(a: int = Depends(dep1)) -> int: 263 | return a + 1 264 | 265 | def dep3(a: int = Depends(dep1, use_cache=False)) -> int: 266 | return a + 1 267 | 268 | def testfunc( 269 | a: int = Depends(dep2), 270 | b: int = Depends(dep3), 271 | ) -> int: 272 | return a + b 273 | 274 | with DependencyGraph(testfunc).sync_ctx({}) as sctx: 275 | assert sctx.resolve_kwargs() == {"a": 2, "b": 2} 276 | 277 | assert dep_exec == 2 278 | dep_exec = 0 279 | 280 | async with DependencyGraph(testfunc).async_ctx({}) as actx: 281 | assert await actx.resolve_kwargs() == {"a": 2, "b": 2} 282 | 283 | assert dep_exec == 2 284 | 285 | 286 | @pytest.mark.anyio 287 | async def test_initial_ctx() -> None: 288 | """ 289 | Tests that initial context is resolved. 290 | 291 | We pass a TeCtx instance as the default 292 | dependency. And now we want to know if 293 | it resolved in a value that we passed. 294 | """ 295 | 296 | class TeCtx: 297 | def __init__(self, val: Any) -> None: 298 | self.val = val 299 | 300 | val = uuid.uuid4() 301 | 302 | def dependency(t: TeCtx = Depends()) -> bool: 303 | return t.val == val 304 | 305 | def target(test: bool = Depends(dependency)) -> bool: 306 | return test 307 | 308 | with DependencyGraph(target).sync_ctx({TeCtx: TeCtx(val)}) as sctx: 309 | assert sctx.resolve_kwargs() == {"test": True} 310 | 311 | async with DependencyGraph(target).async_ctx({TeCtx: TeCtx(val)}) as actx: 312 | assert await actx.resolve_kwargs() == {"test": True} 313 | 314 | 315 | def test_unknown_dependency_func() -> None: 316 | """Tests that error is raised for unknown deps.""" 317 | 318 | def target(dep=Depends()) -> None: # type: ignore # noqa: ANN001 319 | pass 320 | 321 | with pytest.raises(ValueError): 322 | DependencyGraph(target) 323 | 324 | 325 | def test_unknown_dependency_class() -> None: 326 | """Tests that error is raised for unknown deps.""" 327 | 328 | class Target: 329 | def __init__(self, dep=Depends()) -> None: # type: ignore # noqa: ANN001 330 | pass 331 | 332 | with pytest.raises(ValueError): 333 | DependencyGraph(Target) 334 | 335 | 336 | def test_get_param_info() -> None: 337 | """Tests that param info resolved correctly.""" 338 | 339 | def dep(info: ParamInfo = Depends()) -> ParamInfo: 340 | return info 341 | 342 | def target(my_test_param: ParamInfo = Depends(dep)) -> None: 343 | return None 344 | 345 | graph = DependencyGraph(target=target) 346 | with graph.sync_ctx() as g: 347 | kwargs = g.resolve_kwargs() 348 | 349 | info: ParamInfo = kwargs["my_test_param"] 350 | assert info.name == "my_test_param" 351 | assert info.definition 352 | assert info.definition.annotation == ParamInfo 353 | assert info.graph == graph 354 | 355 | 356 | def test_param_info_no_dependant() -> None: 357 | """Tests that if ParamInfo is used on the target, no error is raised.""" 358 | 359 | def target(info: ParamInfo = Depends()) -> None: 360 | return None 361 | 362 | graph = DependencyGraph(target=target) 363 | with graph.sync_ctx() as g: 364 | kwargs = g.resolve_kwargs() 365 | 366 | info: ParamInfo = kwargs["info"] 367 | assert info.name == "" 368 | assert info.definition is None 369 | assert info.graph == graph 370 | 371 | 372 | def test_class_based_dependencies() -> None: 373 | """Tests that if ParamInfo is used on the target, no error is raised.""" 374 | 375 | class TeClass: 376 | def __init__(self, return_val: str) -> None: 377 | self.return_val = return_val 378 | 379 | def __call__(self) -> str: 380 | return self.return_val 381 | 382 | def target(class_val: str = Depends(TeClass("tval"))) -> None: 383 | return None 384 | 385 | with DependencyGraph(target=target).sync_ctx() as g: 386 | kwargs = g.resolve_kwargs() 387 | 388 | info: str = kwargs["class_val"] 389 | assert info == "tval" 390 | 391 | 392 | def test_exception_generators() -> None: 393 | errors_found = 0 394 | 395 | def my_generator() -> Generator[int, None, None]: 396 | nonlocal errors_found 397 | try: 398 | yield 1 399 | except ValueError: 400 | errors_found += 1 401 | 402 | def target(_: int = Depends(my_generator)) -> None: 403 | raise ValueError 404 | 405 | with pytest.raises(ValueError), DependencyGraph(target=target).sync_ctx() as g: 406 | target(**g.resolve_kwargs()) 407 | 408 | assert errors_found == 1 409 | 410 | 411 | @pytest.mark.anyio 412 | async def test_async_exception_generators() -> None: 413 | errors_found = 0 414 | 415 | async def my_generator() -> AsyncGenerator[int, None]: 416 | nonlocal errors_found 417 | try: 418 | yield 1 419 | except ValueError: 420 | errors_found += 1 421 | 422 | def target(_: int = Depends(my_generator)) -> None: 423 | raise ValueError 424 | 425 | with pytest.raises(ValueError): 426 | async with DependencyGraph(target=target).async_ctx() as g: 427 | target(**(await g.resolve_kwargs())) 428 | 429 | assert errors_found == 1 430 | 431 | 432 | @pytest.mark.anyio 433 | async def test_async_exception_generators_multiple() -> None: 434 | errors_found = 0 435 | 436 | async def my_generator() -> AsyncGenerator[int, None]: 437 | nonlocal errors_found 438 | try: 439 | yield 1 440 | except ValueError: 441 | errors_found += 1 442 | 443 | def target( 444 | _a: int = Depends(my_generator, use_cache=False), 445 | _b: int = Depends(my_generator, use_cache=False), 446 | _c: int = Depends(my_generator, use_cache=False), 447 | ) -> None: 448 | raise ValueError 449 | 450 | with pytest.raises(ValueError): 451 | async with DependencyGraph(target=target).async_ctx() as g: 452 | target(**(await g.resolve_kwargs())) 453 | 454 | assert errors_found == 3 455 | 456 | 457 | @pytest.mark.anyio 458 | async def test_async_exception_in_teardown() -> None: 459 | errors_found = 0 460 | 461 | async def my_generator() -> AsyncGenerator[int, None]: 462 | nonlocal errors_found 463 | try: 464 | yield 1 465 | except ValueError as verr: 466 | errors_found += 1 467 | raise Exception from verr 468 | 469 | def target(_: int = Depends(my_generator)) -> None: 470 | raise ValueError 471 | 472 | with pytest.raises(ValueError): 473 | async with DependencyGraph(target=target).async_ctx() as g: 474 | target(**(await g.resolve_kwargs())) 475 | 476 | 477 | @pytest.mark.anyio 478 | async def test_async_propagation_disabled() -> None: 479 | errors_found = 0 480 | 481 | async def my_generator() -> AsyncGenerator[int, None]: 482 | nonlocal errors_found 483 | try: 484 | yield 1 485 | except ValueError as verr: 486 | errors_found += 1 487 | raise Exception from verr 488 | 489 | def target(_: int = Depends(my_generator)) -> None: 490 | raise ValueError 491 | 492 | with pytest.raises(ValueError): 493 | async with DependencyGraph(target=target).async_ctx( 494 | exception_propagation=False, 495 | ) as g: 496 | target(**(await g.resolve_kwargs())) 497 | 498 | assert errors_found == 0 499 | 500 | 501 | def test_sync_propagation_disabled() -> None: 502 | errors_found = 0 503 | 504 | def my_generator() -> Generator[int, None, None]: 505 | nonlocal errors_found 506 | try: 507 | yield 1 508 | except ValueError as verr: 509 | errors_found += 1 510 | raise Exception from verr 511 | 512 | def target(_: int = Depends(my_generator)) -> None: 513 | raise ValueError 514 | 515 | with pytest.raises(ValueError), DependencyGraph(target=target).sync_ctx( 516 | exception_propagation=False, 517 | ) as g: 518 | target(**(g.resolve_kwargs())) 519 | 520 | assert errors_found == 0 521 | 522 | 523 | def test_generic_classes() -> None: 524 | errors_found = 0 525 | 526 | _T = TypeVar("_T") 527 | 528 | class MyClass: 529 | pass 530 | 531 | class MainClass(Generic[_T]): 532 | def __init__(self, val: _T = Depends()) -> None: 533 | self.val = val 534 | 535 | def test_func(a: MainClass[MyClass] = Depends()) -> MyClass: 536 | return a.val 537 | 538 | with DependencyGraph(target=test_func).sync_ctx(exception_propagation=False) as g: 539 | value = test_func(**(g.resolve_kwargs())) 540 | 541 | assert errors_found == 0 542 | assert isinstance(value, MyClass) 543 | 544 | 545 | def test_generic_multiple() -> None: 546 | errors_found = 0 547 | 548 | _T = TypeVar("_T") 549 | _V = TypeVar("_V") 550 | 551 | class MyClass1: 552 | pass 553 | 554 | class MyClass2: 555 | pass 556 | 557 | class MainClass(Generic[_T, _V]): 558 | def __init__(self, t_val: _T = Depends(), v_val: _V = Depends()) -> None: 559 | self.t_val = t_val 560 | self.v_val = v_val 561 | 562 | def test_func( 563 | a: MainClass[MyClass1, MyClass2] = Depends(), 564 | ) -> MainClass[MyClass1, MyClass2]: 565 | return a 566 | 567 | with DependencyGraph(target=test_func).sync_ctx(exception_propagation=False) as g: 568 | result = test_func(**(g.resolve_kwargs())) 569 | 570 | assert errors_found == 0 571 | assert isinstance(result.t_val, MyClass1) 572 | assert isinstance(result.v_val, MyClass2) 573 | 574 | 575 | def test_generic_unordered() -> None: 576 | errors_found = 0 577 | 578 | _T = TypeVar("_T") 579 | _V = TypeVar("_V") 580 | 581 | class MyClass1: 582 | pass 583 | 584 | class MyClass2: 585 | pass 586 | 587 | class MainClass(Generic[_T, _V]): 588 | def __init__(self, v_val: _V = Depends(), t_val: _T = Depends()) -> None: 589 | self.t_val = t_val 590 | self.v_val = v_val 591 | 592 | def test_func( 593 | a: MainClass[MyClass1, MyClass2] = Depends(), 594 | ) -> MainClass[MyClass1, MyClass2]: 595 | return a 596 | 597 | with DependencyGraph(target=test_func).sync_ctx(exception_propagation=False) as g: 598 | result = test_func(**(g.resolve_kwargs())) 599 | 600 | assert errors_found == 0 601 | assert isinstance(result.t_val, MyClass1) 602 | assert isinstance(result.v_val, MyClass2) 603 | 604 | 605 | def test_generic_classes_nesting() -> None: 606 | errors_found = 0 607 | 608 | _T = TypeVar("_T") 609 | _V = TypeVar("_V") 610 | 611 | class DummyClass: 612 | pass 613 | 614 | class DependantClass(Generic[_V]): 615 | def __init__(self, var: _V = Depends()) -> None: 616 | self.var = var 617 | 618 | class MainClass(Generic[_T]): 619 | def __init__(self, var: _T = Depends()) -> None: 620 | self.var = var 621 | 622 | def test_func(a: MainClass[DependantClass[DummyClass]] = Depends()) -> DummyClass: 623 | return a.var.var 624 | 625 | with DependencyGraph(target=test_func).sync_ctx(exception_propagation=False) as g: 626 | value = test_func(**(g.resolve_kwargs())) 627 | 628 | assert errors_found == 0 629 | assert isinstance(value, DummyClass) 630 | 631 | 632 | def test_generic_class_based_dependencies() -> None: 633 | """Tests that if ParamInfo is used on the target, no error is raised.""" 634 | _T = TypeVar("_T") 635 | 636 | class GenericClass(Generic[_T]): 637 | def __init__(self, class_val: _T = Depends()) -> None: 638 | self.return_val = class_val 639 | 640 | def func_dep() -> GenericClass[int]: 641 | return GenericClass(123) 642 | 643 | def target(my_dep: GenericClass[int] = Depends(func_dep)) -> int: 644 | return my_dep.return_val 645 | 646 | with DependencyGraph(target=target).sync_ctx() as g: 647 | result = target(**g.resolve_kwargs()) 648 | 649 | assert result == 123 650 | 651 | 652 | @pytest.mark.anyio 653 | async def test_graph_type_hints() -> None: 654 | def dep() -> int: 655 | return 123 656 | 657 | def target(class_val: int = Depends(dep, use_cache=False)) -> None: 658 | return None 659 | 660 | g = DependencyGraph(target=target) 661 | for dep_obj in g.subgraphs: 662 | assert dep_obj.param_name == "class_val" 663 | assert dep_obj.dependency == dep 664 | assert dep_obj.signature.name == "class_val" 665 | assert dep_obj.signature.annotation == int # noqa: E721 666 | 667 | 668 | @pytest.mark.anyio 669 | async def test_graph_generic_type_hints() -> None: 670 | _T = TypeVar("_T") 671 | 672 | def dep3() -> int: 673 | return 123 674 | 675 | class GenericClass(Generic[_T]): 676 | def __init__(self, class_val: int = Depends(dep3)) -> None: 677 | self.return_val = class_val 678 | 679 | def target( 680 | class_val: GenericClass[Tuple[str, int]] = Depends(use_cache=False), 681 | ) -> None: 682 | return None 683 | 684 | g = DependencyGraph(target=target) 685 | for dep_obj in g.subgraphs: 686 | assert dep_obj.param_name == "class_val" 687 | assert dep_obj.dependency == GenericClass[Tuple[str, int]] 688 | assert dep_obj.signature.name == "class_val" 689 | assert dep_obj.signature.annotation == GenericClass[Tuple[str, int]] 690 | 691 | 692 | @pytest.mark.anyio 693 | async def test_replaced_dep_simple() -> None: 694 | def replaced() -> int: 695 | return 321 696 | 697 | def dep() -> int: 698 | return 123 699 | 700 | def target(val: int = Depends(dep)) -> None: 701 | return None 702 | 703 | graph = DependencyGraph(target=target) 704 | async with graph.async_ctx(replaced_deps={dep: replaced}) as ctx: 705 | kwargs = await ctx.resolve_kwargs() 706 | assert kwargs["val"] == 321 707 | 708 | 709 | @pytest.mark.anyio 710 | async def test_replaced_dep_generators() -> None: 711 | call_count = 0 712 | 713 | def replaced() -> Generator[int, None, None]: 714 | nonlocal call_count 715 | yield 321 716 | call_count += 1 717 | 718 | def dep() -> int: 719 | return 123 720 | 721 | def target(val: int = Depends(dep)) -> None: 722 | return None 723 | 724 | graph = DependencyGraph(target=target) 725 | async with graph.async_ctx(replaced_deps={dep: replaced}) as ctx: 726 | kwargs = await ctx.resolve_kwargs() 727 | assert kwargs["val"] == 321 728 | assert call_count == 1 729 | 730 | 731 | @pytest.mark.anyio 732 | async def test_replaced_dep_exception_propogation() -> None: 733 | exc_count = 0 734 | 735 | def replaced() -> Generator[int, None, None]: 736 | nonlocal exc_count 737 | try: 738 | yield 321 739 | except ValueError: 740 | exc_count += 1 741 | 742 | def dep() -> int: 743 | return 123 744 | 745 | def target(val: int = Depends(dep)) -> None: 746 | raise ValueError("lol") 747 | 748 | graph = DependencyGraph(target=target) 749 | with pytest.raises(ValueError): 750 | async with graph.async_ctx( 751 | replaced_deps={dep: replaced}, 752 | exception_propagation=True, 753 | ) as ctx: 754 | kwargs = await ctx.resolve_kwargs() 755 | assert kwargs["val"] == 321 756 | target(**kwargs) 757 | assert exc_count == 1 758 | 759 | 760 | @pytest.mark.anyio 761 | async def test_replaced_dep_subdependencies() -> None: 762 | def subdep() -> int: 763 | return 321 764 | 765 | def replaced(ret_val: int = Depends(subdep)) -> int: 766 | return ret_val 767 | 768 | def dep() -> int: 769 | return 123 770 | 771 | def target(val: int = Depends(dep)) -> None: 772 | """Stub function.""" 773 | 774 | graph = DependencyGraph(target=target) 775 | async with graph.async_ctx( 776 | replaced_deps={dep: replaced}, 777 | exception_propagation=True, 778 | ) as ctx: 779 | kwargs = await ctx.resolve_kwargs() 780 | assert kwargs["val"] == 321 781 | 782 | 783 | def test_kwargs_caches() -> None: 784 | """ 785 | Test that kwarged caches work. 786 | 787 | If user wants to pass kwargs to the dependency 788 | multiple times, we must verify that it works. 789 | 790 | And dependency calculated multiple times, 791 | even with caches. 792 | """ 793 | 794 | def random_dep(a: int) -> int: 795 | return a 796 | 797 | A = Depends(random_dep, kwargs={"a": 1}) 798 | B = Depends(random_dep, kwargs={"a": 2}) 799 | 800 | def target(a: int = A, b: int = B) -> int: 801 | return a + b 802 | 803 | graph = DependencyGraph(target=target) 804 | with graph.sync_ctx() as ctx: 805 | kwargs = ctx.resolve_kwargs() 806 | assert target(**kwargs) == 3 807 | 808 | 809 | def test_skip_not_decorated_managers() -> None: 810 | """ 811 | Test that synct context skip context managers. 812 | 813 | Tests that even is class implements a context manager, 814 | it won't be called during the context resolution, 815 | because it's not annotated with contextmanager decorator. 816 | """ 817 | 818 | class TestCM: 819 | def __init__(self) -> None: 820 | self.opened = False 821 | 822 | def __enter__(self) -> None: 823 | self.opened = True 824 | 825 | def __exit__(self, *args: object) -> None: 826 | pass 827 | 828 | test_cm = TestCM() 829 | 830 | def get_test_cm() -> TestCM: 831 | nonlocal test_cm 832 | return test_cm 833 | 834 | def target(cm: TestCM = Depends(get_test_cm)) -> None: 835 | pass 836 | 837 | graph = DependencyGraph(target=target) 838 | with graph.sync_ctx() as ctx: 839 | kwargs = ctx.resolve_kwargs() 840 | assert kwargs["cm"] == test_cm 841 | assert not test_cm.opened 842 | 843 | 844 | @pytest.mark.anyio 845 | async def test_skip_not_decorated_async_managers() -> None: 846 | """ 847 | Test that synct context skip context managers. 848 | 849 | Tests that even is class implements a context manager, 850 | it won't be called during the context resolution, 851 | because it's not annotated with contextmanager decorator. 852 | """ 853 | 854 | class TestACM: 855 | def __init__(self) -> None: 856 | self.opened = False 857 | 858 | async def __aenter__(self) -> None: 859 | self.opened = True 860 | 861 | async def __aexit__(self, *args: object) -> None: 862 | pass 863 | 864 | test_acm = TestACM() 865 | 866 | def get_test_acm() -> TestACM: 867 | nonlocal test_acm 868 | return test_acm 869 | 870 | def target(acm: TestACM = Depends(get_test_acm)) -> None: 871 | pass 872 | 873 | graph = DependencyGraph(target=target) 874 | async with graph.async_ctx() as ctx: 875 | kwargs = await ctx.resolve_kwargs() 876 | assert kwargs["acm"] == test_acm 877 | assert not test_acm.opened 878 | 879 | 880 | def test_param_info_subgraph() -> None: 881 | """ 882 | Test subgraphs for ParamInfo. 883 | 884 | Test that correct graph is stored in ParamInfo 885 | even if evaluated from subgraphs. 886 | """ 887 | 888 | def inner_dep(info: ParamInfo = Depends()) -> ParamInfo: 889 | return info 890 | 891 | def target(info: ParamInfo = Depends(inner_dep, use_cache=False)) -> None: 892 | return None 893 | 894 | graph = DependencyGraph(target=target) 895 | with graph.sync_ctx() as g: 896 | kwargs = g.resolve_kwargs() 897 | 898 | info: ParamInfo = kwargs["info"] 899 | assert info.name == "" 900 | assert info.definition is None 901 | assert info.graph == graph 902 | 903 | 904 | def test_skip_type_checking_function() -> None: 905 | """Test if we can skip type only for type checking for the function.""" 906 | if TYPE_CHECKING: 907 | 908 | class A: 909 | pass 910 | 911 | def target(unknown: "A") -> None: 912 | pass 913 | 914 | with pytest.warns(RuntimeWarning, match=r"Cannot resolve.*function target.*"): 915 | graph = DependencyGraph(target=target) 916 | with graph.sync_ctx() as ctx: 917 | assert "unknown" not in ctx.resolve_kwargs() 918 | 919 | 920 | def test_skip_type_checking_class() -> None: 921 | """Test if we can skip type only for type checking for the function.""" 922 | if TYPE_CHECKING: 923 | 924 | class A: 925 | pass 926 | 927 | class Target: 928 | def __init__(self, unknown: "A") -> None: 929 | pass 930 | 931 | with pytest.warns(RuntimeWarning, match=r"Cannot resolve.*class Target.*"): 932 | graph = DependencyGraph(target=Target) 933 | with graph.sync_ctx() as ctx: 934 | assert "unknown" not in ctx.resolve_kwargs() 935 | 936 | 937 | def test_skip_type_checking_object() -> None: 938 | """Test if we can skip type only for type checking for the function.""" 939 | if TYPE_CHECKING: 940 | 941 | class A: 942 | pass 943 | 944 | class Target: 945 | def __call__(self, unknown: "A") -> None: 946 | pass 947 | 948 | with pytest.warns( 949 | RuntimeWarning, 950 | match=r"Cannot resolve.*object of class Target.*", 951 | ): 952 | graph = DependencyGraph(target=Target()) 953 | with graph.sync_ctx() as ctx: 954 | assert "unknown" not in ctx.resolve_kwargs() 955 | -------------------------------------------------------------------------------- /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 = "anyio" 5 | version = "4.7.0" 6 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 7 | optional = false 8 | python-versions = ">=3.9" 9 | files = [ 10 | {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, 11 | {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, 12 | ] 13 | 14 | [package.dependencies] 15 | exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} 16 | idna = ">=2.8" 17 | sniffio = ">=1.1" 18 | typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} 19 | 20 | [package.extras] 21 | doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] 22 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] 23 | trio = ["trio (>=0.26.1)"] 24 | 25 | [[package]] 26 | name = "black" 27 | version = "24.10.0" 28 | description = "The uncompromising code formatter." 29 | optional = false 30 | python-versions = ">=3.9" 31 | files = [ 32 | {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, 33 | {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, 34 | {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, 35 | {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, 36 | {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, 37 | {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, 38 | {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, 39 | {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, 40 | {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, 41 | {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, 42 | {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, 43 | {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, 44 | {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, 45 | {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, 46 | {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, 47 | {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, 48 | {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, 49 | {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, 50 | {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, 51 | {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, 52 | {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, 53 | {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, 54 | ] 55 | 56 | [package.dependencies] 57 | click = ">=8.0.0" 58 | mypy-extensions = ">=0.4.3" 59 | packaging = ">=22.0" 60 | pathspec = ">=0.9.0" 61 | platformdirs = ">=2" 62 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 63 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 64 | 65 | [package.extras] 66 | colorama = ["colorama (>=0.4.3)"] 67 | d = ["aiohttp (>=3.10)"] 68 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 69 | uvloop = ["uvloop (>=0.15.2)"] 70 | 71 | [[package]] 72 | name = "cfgv" 73 | version = "3.4.0" 74 | description = "Validate configuration and produce human readable error messages." 75 | optional = false 76 | python-versions = ">=3.8" 77 | files = [ 78 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 79 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 80 | ] 81 | 82 | [[package]] 83 | name = "click" 84 | version = "8.1.7" 85 | description = "Composable command line interface toolkit" 86 | optional = false 87 | python-versions = ">=3.7" 88 | files = [ 89 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 90 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 91 | ] 92 | 93 | [package.dependencies] 94 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 95 | 96 | [[package]] 97 | name = "colorama" 98 | version = "0.4.6" 99 | description = "Cross-platform colored terminal text." 100 | optional = false 101 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 102 | files = [ 103 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 104 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 105 | ] 106 | 107 | [[package]] 108 | name = "coverage" 109 | version = "7.6.9" 110 | description = "Code coverage measurement for Python" 111 | optional = false 112 | python-versions = ">=3.9" 113 | files = [ 114 | {file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"}, 115 | {file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"}, 116 | {file = "coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa"}, 117 | {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1"}, 118 | {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec"}, 119 | {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3"}, 120 | {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5"}, 121 | {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073"}, 122 | {file = "coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198"}, 123 | {file = "coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717"}, 124 | {file = "coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9"}, 125 | {file = "coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c"}, 126 | {file = "coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7"}, 127 | {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9"}, 128 | {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"}, 129 | {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1"}, 130 | {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b"}, 131 | {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3"}, 132 | {file = "coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0"}, 133 | {file = "coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b"}, 134 | {file = "coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8"}, 135 | {file = "coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a"}, 136 | {file = "coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015"}, 137 | {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3"}, 138 | {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae"}, 139 | {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4"}, 140 | {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6"}, 141 | {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f"}, 142 | {file = "coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692"}, 143 | {file = "coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97"}, 144 | {file = "coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664"}, 145 | {file = "coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c"}, 146 | {file = "coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014"}, 147 | {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00"}, 148 | {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d"}, 149 | {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a"}, 150 | {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077"}, 151 | {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb"}, 152 | {file = "coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba"}, 153 | {file = "coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1"}, 154 | {file = "coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419"}, 155 | {file = "coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a"}, 156 | {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4"}, 157 | {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae"}, 158 | {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030"}, 159 | {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be"}, 160 | {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e"}, 161 | {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9"}, 162 | {file = "coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b"}, 163 | {file = "coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611"}, 164 | {file = "coverage-7.6.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902"}, 165 | {file = "coverage-7.6.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be"}, 166 | {file = "coverage-7.6.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599"}, 167 | {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08"}, 168 | {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464"}, 169 | {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845"}, 170 | {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf"}, 171 | {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678"}, 172 | {file = "coverage-7.6.9-cp39-cp39-win32.whl", hash = "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6"}, 173 | {file = "coverage-7.6.9-cp39-cp39-win_amd64.whl", hash = "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4"}, 174 | {file = "coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b"}, 175 | {file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"}, 176 | ] 177 | 178 | [package.dependencies] 179 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 180 | 181 | [package.extras] 182 | toml = ["tomli"] 183 | 184 | [[package]] 185 | name = "distlib" 186 | version = "0.3.9" 187 | description = "Distribution utilities" 188 | optional = false 189 | python-versions = "*" 190 | files = [ 191 | {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, 192 | {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, 193 | ] 194 | 195 | [[package]] 196 | name = "exceptiongroup" 197 | version = "1.2.2" 198 | description = "Backport of PEP 654 (exception groups)" 199 | optional = false 200 | python-versions = ">=3.7" 201 | files = [ 202 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 203 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 204 | ] 205 | 206 | [package.extras] 207 | test = ["pytest (>=6)"] 208 | 209 | [[package]] 210 | name = "execnet" 211 | version = "2.1.1" 212 | description = "execnet: rapid multi-Python deployment" 213 | optional = false 214 | python-versions = ">=3.8" 215 | files = [ 216 | {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, 217 | {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, 218 | ] 219 | 220 | [package.extras] 221 | testing = ["hatch", "pre-commit", "pytest", "tox"] 222 | 223 | [[package]] 224 | name = "filelock" 225 | version = "3.16.1" 226 | description = "A platform independent file lock." 227 | optional = false 228 | python-versions = ">=3.8" 229 | files = [ 230 | {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, 231 | {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, 232 | ] 233 | 234 | [package.extras] 235 | docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] 236 | testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] 237 | typing = ["typing-extensions (>=4.12.2)"] 238 | 239 | [[package]] 240 | name = "identify" 241 | version = "2.6.3" 242 | description = "File identification library for Python" 243 | optional = false 244 | python-versions = ">=3.9" 245 | files = [ 246 | {file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"}, 247 | {file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"}, 248 | ] 249 | 250 | [package.extras] 251 | license = ["ukkonen"] 252 | 253 | [[package]] 254 | name = "idna" 255 | version = "3.10" 256 | description = "Internationalized Domain Names in Applications (IDNA)" 257 | optional = false 258 | python-versions = ">=3.6" 259 | files = [ 260 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 261 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 262 | ] 263 | 264 | [package.extras] 265 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 266 | 267 | [[package]] 268 | name = "iniconfig" 269 | version = "2.0.0" 270 | description = "brain-dead simple config-ini parsing" 271 | optional = false 272 | python-versions = ">=3.7" 273 | files = [ 274 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 275 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 276 | ] 277 | 278 | [[package]] 279 | name = "mypy" 280 | version = "1.13.0" 281 | description = "Optional static typing for Python" 282 | optional = false 283 | python-versions = ">=3.8" 284 | files = [ 285 | {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, 286 | {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, 287 | {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, 288 | {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, 289 | {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, 290 | {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, 291 | {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, 292 | {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, 293 | {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, 294 | {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, 295 | {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, 296 | {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, 297 | {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, 298 | {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, 299 | {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, 300 | {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, 301 | {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, 302 | {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, 303 | {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, 304 | {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, 305 | {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, 306 | {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, 307 | {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, 308 | {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, 309 | {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, 310 | {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, 311 | {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, 312 | {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, 313 | {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, 314 | {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, 315 | {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, 316 | {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, 317 | ] 318 | 319 | [package.dependencies] 320 | mypy-extensions = ">=1.0.0" 321 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 322 | typing-extensions = ">=4.6.0" 323 | 324 | [package.extras] 325 | dmypy = ["psutil (>=4.0)"] 326 | faster-cache = ["orjson"] 327 | install-types = ["pip"] 328 | mypyc = ["setuptools (>=50)"] 329 | reports = ["lxml"] 330 | 331 | [[package]] 332 | name = "mypy-extensions" 333 | version = "1.0.0" 334 | description = "Type system extensions for programs checked with the mypy type checker." 335 | optional = false 336 | python-versions = ">=3.5" 337 | files = [ 338 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 339 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 340 | ] 341 | 342 | [[package]] 343 | name = "nodeenv" 344 | version = "1.9.1" 345 | description = "Node.js virtual environment builder" 346 | optional = false 347 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 348 | files = [ 349 | {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, 350 | {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, 351 | ] 352 | 353 | [[package]] 354 | name = "packaging" 355 | version = "24.2" 356 | description = "Core utilities for Python packages" 357 | optional = false 358 | python-versions = ">=3.8" 359 | files = [ 360 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 361 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 362 | ] 363 | 364 | [[package]] 365 | name = "pathspec" 366 | version = "0.12.1" 367 | description = "Utility library for gitignore style pattern matching of file paths." 368 | optional = false 369 | python-versions = ">=3.8" 370 | files = [ 371 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 372 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 373 | ] 374 | 375 | [[package]] 376 | name = "platformdirs" 377 | version = "4.3.6" 378 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 379 | optional = false 380 | python-versions = ">=3.8" 381 | files = [ 382 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, 383 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, 384 | ] 385 | 386 | [package.extras] 387 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] 388 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] 389 | type = ["mypy (>=1.11.2)"] 390 | 391 | [[package]] 392 | name = "pluggy" 393 | version = "1.5.0" 394 | description = "plugin and hook calling mechanisms for python" 395 | optional = false 396 | python-versions = ">=3.8" 397 | files = [ 398 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 399 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 400 | ] 401 | 402 | [package.extras] 403 | dev = ["pre-commit", "tox"] 404 | testing = ["pytest", "pytest-benchmark"] 405 | 406 | [[package]] 407 | name = "pre-commit" 408 | version = "3.8.0" 409 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 410 | optional = false 411 | python-versions = ">=3.9" 412 | files = [ 413 | {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, 414 | {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, 415 | ] 416 | 417 | [package.dependencies] 418 | cfgv = ">=2.0.0" 419 | identify = ">=1.0.0" 420 | nodeenv = ">=0.11.1" 421 | pyyaml = ">=5.1" 422 | virtualenv = ">=20.10.0" 423 | 424 | [[package]] 425 | name = "psutil" 426 | version = "6.1.0" 427 | description = "Cross-platform lib for process and system monitoring in Python." 428 | optional = false 429 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" 430 | files = [ 431 | {file = "psutil-6.1.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff34df86226c0227c52f38b919213157588a678d049688eded74c76c8ba4a5d0"}, 432 | {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c0e0c00aa18ca2d3b2b991643b799a15fc8f0563d2ebb6040f64ce8dc027b942"}, 433 | {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:000d1d1ebd634b4efb383f4034437384e44a6d455260aaee2eca1e9c1b55f047"}, 434 | {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5cd2bcdc75b452ba2e10f0e8ecc0b57b827dd5d7aaffbc6821b2a9a242823a76"}, 435 | {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:045f00a43c737f960d273a83973b2511430d61f283a44c96bf13a6e829ba8fdc"}, 436 | {file = "psutil-6.1.0-cp27-none-win32.whl", hash = "sha256:9118f27452b70bb1d9ab3198c1f626c2499384935aaf55388211ad982611407e"}, 437 | {file = "psutil-6.1.0-cp27-none-win_amd64.whl", hash = "sha256:a8506f6119cff7015678e2bce904a4da21025cc70ad283a53b099e7620061d85"}, 438 | {file = "psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688"}, 439 | {file = "psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e"}, 440 | {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38"}, 441 | {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b"}, 442 | {file = "psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a"}, 443 | {file = "psutil-6.1.0-cp36-cp36m-win32.whl", hash = "sha256:6d3fbbc8d23fcdcb500d2c9f94e07b1342df8ed71b948a2649b5cb060a7c94ca"}, 444 | {file = "psutil-6.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1209036fbd0421afde505a4879dee3b2fd7b1e14fee81c0069807adcbbcca747"}, 445 | {file = "psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e"}, 446 | {file = "psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be"}, 447 | {file = "psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a"}, 448 | ] 449 | 450 | [package.extras] 451 | dev = ["black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "wheel"] 452 | test = ["pytest", "pytest-xdist", "setuptools"] 453 | 454 | [[package]] 455 | name = "pytest" 456 | version = "8.3.4" 457 | description = "pytest: simple powerful testing with Python" 458 | optional = false 459 | python-versions = ">=3.8" 460 | files = [ 461 | {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, 462 | {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, 463 | ] 464 | 465 | [package.dependencies] 466 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 467 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 468 | iniconfig = "*" 469 | packaging = "*" 470 | pluggy = ">=1.5,<2" 471 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 472 | 473 | [package.extras] 474 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 475 | 476 | [[package]] 477 | name = "pytest-cov" 478 | version = "5.0.0" 479 | description = "Pytest plugin for measuring coverage." 480 | optional = false 481 | python-versions = ">=3.8" 482 | files = [ 483 | {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, 484 | {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, 485 | ] 486 | 487 | [package.dependencies] 488 | coverage = {version = ">=5.2.1", extras = ["toml"]} 489 | pytest = ">=4.6" 490 | 491 | [package.extras] 492 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] 493 | 494 | [[package]] 495 | name = "pytest-xdist" 496 | version = "3.6.1" 497 | description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" 498 | optional = false 499 | python-versions = ">=3.8" 500 | files = [ 501 | {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, 502 | {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, 503 | ] 504 | 505 | [package.dependencies] 506 | execnet = ">=2.1" 507 | psutil = {version = ">=3.0", optional = true, markers = "extra == \"psutil\""} 508 | pytest = ">=7.0.0" 509 | 510 | [package.extras] 511 | psutil = ["psutil (>=3.0)"] 512 | setproctitle = ["setproctitle"] 513 | testing = ["filelock"] 514 | 515 | [[package]] 516 | name = "pyyaml" 517 | version = "6.0.2" 518 | description = "YAML parser and emitter for Python" 519 | optional = false 520 | python-versions = ">=3.8" 521 | files = [ 522 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, 523 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, 524 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, 525 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, 526 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, 527 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, 528 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, 529 | {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, 530 | {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, 531 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, 532 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, 533 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, 534 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, 535 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, 536 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, 537 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, 538 | {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, 539 | {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, 540 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, 541 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, 542 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, 543 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, 544 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, 545 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, 546 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, 547 | {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, 548 | {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, 549 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, 550 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, 551 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, 552 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, 553 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, 554 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, 555 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, 556 | {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, 557 | {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, 558 | {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, 559 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, 560 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, 561 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, 562 | {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, 563 | {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, 564 | {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, 565 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, 566 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, 567 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, 568 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, 569 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, 570 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, 571 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, 572 | {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, 573 | {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, 574 | {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, 575 | ] 576 | 577 | [[package]] 578 | name = "ruff" 579 | version = "0.6.9" 580 | description = "An extremely fast Python linter and code formatter, written in Rust." 581 | optional = false 582 | python-versions = ">=3.7" 583 | files = [ 584 | {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"}, 585 | {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"}, 586 | {file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"}, 587 | {file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"}, 588 | {file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"}, 589 | {file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"}, 590 | {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"}, 591 | {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"}, 592 | {file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"}, 593 | {file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"}, 594 | {file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"}, 595 | {file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"}, 596 | {file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"}, 597 | {file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"}, 598 | {file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"}, 599 | {file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"}, 600 | {file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"}, 601 | {file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"}, 602 | ] 603 | 604 | [[package]] 605 | name = "sniffio" 606 | version = "1.3.1" 607 | description = "Sniff out which async library your code is running under" 608 | optional = false 609 | python-versions = ">=3.7" 610 | files = [ 611 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 612 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 613 | ] 614 | 615 | [[package]] 616 | name = "tomli" 617 | version = "2.2.1" 618 | description = "A lil' TOML parser" 619 | optional = false 620 | python-versions = ">=3.8" 621 | files = [ 622 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 623 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 624 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 625 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 626 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 627 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 628 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 629 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 630 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 631 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 632 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 633 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 634 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 635 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 636 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 637 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 638 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 639 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 640 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 641 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 642 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 643 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 644 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 645 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 646 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 647 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 648 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 649 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 650 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 651 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 652 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 653 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 654 | ] 655 | 656 | [[package]] 657 | name = "typing-extensions" 658 | version = "4.12.2" 659 | description = "Backported and Experimental Type Hints for Python 3.8+" 660 | optional = false 661 | python-versions = ">=3.8" 662 | files = [ 663 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 664 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 665 | ] 666 | 667 | [[package]] 668 | name = "virtualenv" 669 | version = "20.28.0" 670 | description = "Virtual Python Environment builder" 671 | optional = false 672 | python-versions = ">=3.8" 673 | files = [ 674 | {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"}, 675 | {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"}, 676 | ] 677 | 678 | [package.dependencies] 679 | distlib = ">=0.3.7,<1" 680 | filelock = ">=3.12.2,<4" 681 | platformdirs = ">=3.9.1,<5" 682 | 683 | [package.extras] 684 | 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)"] 685 | 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)"] 686 | 687 | [metadata] 688 | lock-version = "2.0" 689 | python-versions = "^3.9" 690 | content-hash = "a597fda04d099d71cec3c64a9b6b63ca8b76237391528f0e3c65a3c2470bb355" 691 | --------------------------------------------------------------------------------