├── tests ├── __init__.py ├── conftest.py ├── test_validation.py ├── test_circular.py ├── compat.py ├── test_namespaces.py ├── test_lazy_fixture.py ├── test_registration.py ├── test_model_name.py ├── test_factory_fixtures.py └── test_postgen_dependencies.py ├── src └── pytest_factoryboy │ ├── py.typed │ ├── __init__.py │ ├── hooks.py │ ├── compat.py │ ├── fixturegen.py │ ├── plugin.py │ └── fixture.py ├── docs ├── index.rst ├── Makefile └── conf.py ├── .editorconfig ├── .readthedocs.yaml ├── AUTHORS.rst ├── .envrc ├── RELEASING.md ├── .gitignore ├── DEVELOPMENT.md ├── LICENSE.md ├── .pre-commit-config.yaml ├── tox.ini ├── pyproject.toml ├── .github └── workflows │ └── main.yml ├── CHANGES.rst ├── README.rst └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pytest_factoryboy/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | pytest_plugins = "pytester" 2 | -------------------------------------------------------------------------------- /src/pytest_factoryboy/__init__.py: -------------------------------------------------------------------------------- 1 | """pytest-factoryboy public API.""" 2 | 3 | from .fixture import LazyFixture, named_model, register 4 | 5 | __all__ = ("register", "named_model", "LazyFixture") 6 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to pytest-factoryboy's documentation! 2 | ============================================= 3 | 4 | .. include:: ../README.rst 5 | 6 | .. include:: ../AUTHORS.rst 7 | 8 | .. include:: ../CHANGES.rst 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | charset = utf-8 11 | 12 | [*.py] 13 | indent_style = space 14 | indent_size = 4 15 | 16 | [*.yml] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /src/pytest_factoryboy/hooks.py: -------------------------------------------------------------------------------- 1 | """pytest-factoryboy pytest hooks.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import TYPE_CHECKING 6 | 7 | if TYPE_CHECKING: 8 | from pytest import FixtureRequest 9 | 10 | 11 | def pytest_factoryboy_done(request: FixtureRequest) -> None: 12 | """Called after all factory based fixtures and their post-generation actions were evaluated.""" 13 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | version: 2 5 | build: 6 | os: ubuntu-22.04 7 | tools: 8 | python: "3" 9 | sphinx: 10 | configuration: docs/conf.py 11 | formats: 12 | - epub 13 | - pdf 14 | - htmlzip 15 | python: 16 | install: 17 | - method: pip 18 | path: . 19 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Authors 2 | ======= 3 | 4 | `Oleg Pidsadnyi `_ 5 | original idea and implementation 6 | 7 | These people have contributed to `pytest-factoryboy`, in alphabetical order: 8 | 9 | * `Anatoly Bubenkov `_ 10 | * `Daniel Duong `_ 11 | * `Daniel Hahler `_ 12 | * `Hugo van Kemenade `_ 13 | * `p13773 `_ 14 | * `Vasily Kuznetsov `_ 15 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | # config file for `direnv`: https://direnv.net 2 | # load the poetry virtual environment when entering the project directory 3 | 4 | strict_env 5 | 6 | if [[ ! -f "pyproject.toml" ]]; then 7 | log_error 'No pyproject.toml found. Use `poetry new` or `poetry init` to create one first.' 8 | exit 2 9 | fi 10 | 11 | local VENV="$(poetry env info --path)" 12 | if [[ -z $VENV || ! -d $VENV/bin ]]; then 13 | log_error 'No poetry virtual environment found. Use `poetry install` to create one first.' 14 | exit 2 15 | fi 16 | 17 | source_env "$VENV/bin/activate" 18 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # How to make a release 2 | 3 | 1. Bump the version: 4 | ```shell 5 | poetry version 6 | ``` 7 | 2. Update the [CHANGES.rst](CHANGES.rst) file with the release notes and the new version. 8 | 3. Commit the changes: 9 | ```shell 10 | git add pyproject.toml CHANGES.rst 11 | git commit -m "Bump version to " 12 | ``` 13 | 4. Create and push a tag: 14 | ```shell 15 | git tag 16 | git push origin 17 | ``` 18 | 19 | The GitHub Actions workflow will automatically build and publish the package to PyPI when a tag is pushed. 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | \.DS_Store 2 | *.rej 3 | *.py[cod] 4 | /.env 5 | /.env3 6 | *.orig 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Packages 12 | *.egg 13 | *.egg-info 14 | dist 15 | build 16 | _build 17 | eggs 18 | parts 19 | bin 20 | var 21 | sdist 22 | develop-eggs 23 | .installed.cfg 24 | lib 25 | lib64 26 | 27 | # Installer logs 28 | pip-log.txt 29 | 30 | # Unit test / coverage reports 31 | .coverage 32 | .tox 33 | nosetests.xml 34 | 35 | # Translations 36 | *.mo 37 | 38 | # Mr Developer 39 | .mr.developer.cfg 40 | .project 41 | .pydevproject 42 | .cache 43 | .pytest_cache 44 | .ropeproject 45 | 46 | # Sublime 47 | /*.sublime-* 48 | 49 | #PyCharm 50 | /.idea 51 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # How to prepare a development environment 2 | 3 | 1. [Install poetry](https://python-poetry.org/docs/#installation): 4 | 5 | ```shell 6 | # MacOS / Linux 7 | curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - 8 | 9 | # Windows 10 | (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python - 11 | ``` 12 | 13 | 2. Install dependencies & pre-commit hooks: 14 | 15 | ```shell 16 | poetry install 17 | 18 | pre-commit install 19 | ``` 20 | 21 | 3. (Optional) Activate the poetry virtual environment: 22 | 23 | ```shell 24 | poetry shell 25 | ``` 26 | 27 | 4. Run tests & mypy using tox: 28 | ```shell 29 | tox 30 | ``` 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015 Oleg Pidsadnyi 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 | -------------------------------------------------------------------------------- /tests/test_validation.py: -------------------------------------------------------------------------------- 1 | """Test factory registration validation.""" 2 | 3 | import factory 4 | import pytest 5 | 6 | from pytest_factoryboy import register 7 | 8 | 9 | def test_without_model(): 10 | """Test that factory without model can't be registered.""" 11 | 12 | class WithoutModelFactory(factory.Factory): 13 | """A factory without model.""" 14 | 15 | with pytest.raises(AssertionError, match="Can't register abstract factories"): 16 | register(WithoutModelFactory) 17 | 18 | 19 | def test_abstract(): 20 | class AbstractFactory(factory.Factory): 21 | """Abstract factory.""" 22 | 23 | class Meta: 24 | abstract = True 25 | model = dict 26 | 27 | with pytest.raises(AssertionError, match="Can't register abstract factories"): 28 | register(AbstractFactory) 29 | 30 | 31 | def test_fixture_name_conflict(): 32 | class Foo: 33 | pass 34 | 35 | class FooFactory(factory.Factory): 36 | class Meta: 37 | model = Foo 38 | 39 | with pytest.raises(AssertionError, match="Naming collision"): 40 | register(FooFactory, "foo_factory") 41 | -------------------------------------------------------------------------------- /tests/test_circular.py: -------------------------------------------------------------------------------- 1 | """Test circular definitions.""" 2 | 3 | from __future__ import annotations 4 | 5 | from dataclasses import dataclass, field 6 | 7 | import factory 8 | 9 | from pytest_factoryboy import register 10 | 11 | 12 | @dataclass 13 | class Book: 14 | """Book model.""" 15 | 16 | name: str 17 | price: float 18 | author: Author 19 | 20 | def __post_init__(self): 21 | self.author.books.append(self) 22 | 23 | 24 | @dataclass 25 | class Author: 26 | """Author model.""" 27 | 28 | name: str 29 | books: list[Book] = field(default_factory=list, init=False) 30 | 31 | 32 | class AuthorFactory(factory.Factory): 33 | class Meta: 34 | model = Author 35 | 36 | name = "Charles Dickens" 37 | 38 | book = factory.RelatedFactory("tests.test_circular.BookFactory", "author") 39 | 40 | 41 | class BookFactory(factory.Factory): 42 | class Meta: 43 | model = Book 44 | 45 | name = "Alice in Wonderland" 46 | price = factory.LazyAttribute(lambda f: 3.99) 47 | author = factory.SubFactory(AuthorFactory) 48 | 49 | 50 | register(AuthorFactory) 51 | register(BookFactory) 52 | 53 | 54 | def test_circular(author: Author, factoryboy_request, request): 55 | assert author.books 56 | -------------------------------------------------------------------------------- /.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/psf/black-pre-commit-mirror 5 | rev: fe95161893684893d68b1225356702ca71f8d388 # frozen: 25.9.0 6 | hooks: 7 | - id: black 8 | - repo: https://github.com/pycqa/isort 9 | rev: ec0efaee4acfce198521e43caa3029b06cedd64a # frozen: 6.1.0 10 | hooks: 11 | - id: isort 12 | - repo: https://github.com/pre-commit/pre-commit-hooks 13 | rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 14 | hooks: 15 | - id: trailing-whitespace 16 | - id: end-of-file-fixer 17 | - id: check-yaml 18 | - id: check-added-large-files 19 | - repo: https://github.com/google/yamlfmt 20 | rev: a69a26f0e2a6d5768f3496239ba1e41c6bb74b6e # frozen: v0.17.2 21 | hooks: 22 | - id: yamlfmt 23 | - repo: https://github.com/asottile/pyupgrade 24 | rev: 37bfa06d791fd38fb4e54910b36a2ff57641b074 # frozen: v3.20.0 25 | hooks: 26 | - id: pyupgrade 27 | args: [--py39-plus] 28 | - repo: https://github.com/python-poetry/poetry 29 | rev: "b9e5d79fc57de2f2e60973019d56662b7398440b" # frozen: 2.2.1 30 | hooks: 31 | - id: poetry-check 32 | args: ["--lock"] 33 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | distshare = {homedir}/.tox/distshare 3 | envlist = py{3.9,3.10,3.11,3.12,3.13,3.14}-pytest{7.3,7.4,8.0,8.1,8.2,8.3,8.4,latest,main} 4 | py{3.9,3.10,3.11}-pytest{7.0,7.1,7.2} 5 | py{3.9,3.10,3.11,3.12,3.13,3.14}-factoryboy{2.10,2.11,2.12,3.0,3.1,3.2,3.3,latest,main} 6 | 7 | [testenv] 8 | parallel_show_output = true 9 | commands = coverage run -m pytest {posargs} 10 | ignore_outcome = 11 | pytestmain: True 12 | deps = 13 | pytestlatest: pytest 14 | pytestmain: git+https://github.com/pytest-dev/pytest.git@main 15 | pytest8.4: pytest~=8.4.0 16 | pytest8.3: pytest~=8.3.0 17 | pytest8.2: pytest~=8.2.0 18 | pytest8.1: pytest~=8.1.0 19 | pytest8.0: pytest~=8.0.0 20 | pytest7.4: pytest~=7.4.0 21 | pytest7.3: pytest~=7.3.0 22 | pytest7.2: pytest~=7.2.0 23 | pytest7.1: pytest~=7.1.0 24 | pytest7.0: pytest~=7.0.0 25 | 26 | factoryboylatest: factory-boy 27 | factoryboymain: git+https://github.com/FactoryBoy/factory_boy.git@master 28 | factoryboy3.3: factory-boy~=3.3.0 29 | factoryboy3.2: factory-boy~=3.2.0 30 | factoryboy3.1: factory-boy~=3.1.0 31 | factoryboy3.0: factory-boy~=3.0.0 32 | factoryboy2.12: factory-boy~=2.12.0 33 | factoryboy2.11: factory-boy~=2.11.0 34 | factoryboy2.10: factory-boy~=2.10.0 35 | 36 | coverage[toml] 37 | 38 | [pytest] 39 | addopts = -vv -l 40 | -------------------------------------------------------------------------------- /tests/compat.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from importlib import metadata 4 | 5 | from _pytest.pytester import RunResult 6 | from packaging.version import Version 7 | 8 | PYTEST_VERSION = Version(metadata.version("pytest")) 9 | 10 | if PYTEST_VERSION >= Version("6.0.0"): 11 | 12 | def assert_outcomes( 13 | result: RunResult, 14 | passed: int = 0, 15 | skipped: int = 0, 16 | failed: int = 0, 17 | errors: int = 0, 18 | xpassed: int = 0, 19 | xfailed: int = 0, 20 | ) -> None: 21 | """Compatibility function for result.assert_outcomes""" 22 | result.assert_outcomes( 23 | errors=errors, 24 | passed=passed, 25 | skipped=skipped, 26 | failed=failed, 27 | xpassed=xpassed, 28 | xfailed=xfailed, 29 | ) 30 | 31 | else: 32 | 33 | def assert_outcomes( 34 | result: RunResult, 35 | passed: int = 0, 36 | skipped: int = 0, 37 | failed: int = 0, 38 | errors: int = 0, 39 | xpassed: int = 0, 40 | xfailed: int = 0, 41 | ) -> None: 42 | """Compatibility function for result.assert_outcomes""" 43 | result.assert_outcomes( 44 | error=errors, # Pytest < 6 uses the singular form 45 | passed=passed, 46 | skipped=skipped, 47 | failed=failed, 48 | xpassed=xpassed, 49 | xfailed=xfailed, 50 | ) # type: ignore[call-arg] 51 | -------------------------------------------------------------------------------- /src/pytest_factoryboy/compat.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Sequence 4 | from importlib.metadata import version 5 | 6 | from _pytest.fixtures import FixtureDef, FixtureManager 7 | from _pytest.nodes import Node 8 | from packaging.version import parse as parse_version 9 | from typing_extensions import TypeAlias 10 | 11 | pytest_version = parse_version(version("pytest")) 12 | 13 | __all__ = ("PostGenerationContext", "getfixturedefs", "PytestFixtureT") 14 | 15 | try: 16 | from factory.declarations import PostGenerationContext 17 | except ImportError: # factory_boy < 3.2.0 18 | from factory.builder import ( # type: ignore[attr-defined, no-redef] 19 | PostGenerationContext, 20 | ) 21 | 22 | if pytest_version.release >= (8, 1): 23 | 24 | def getfixturedefs( 25 | fixturemanager: FixtureManager, fixturename: str, node: Node 26 | ) -> Sequence[FixtureDef[object]] | None: 27 | return fixturemanager.getfixturedefs(fixturename, node) 28 | 29 | else: 30 | 31 | def getfixturedefs( 32 | fixturemanager: FixtureManager, fixturename: str, node: Node 33 | ) -> Sequence[FixtureDef[object]] | None: 34 | return fixturemanager.getfixturedefs(fixturename, node.nodeid) # type: ignore[arg-type] 35 | 36 | 37 | if pytest_version.release >= (8, 4): 38 | from _pytest.fixtures import FixtureFunctionDefinition 39 | 40 | PytestFixtureT: TypeAlias = FixtureFunctionDefinition 41 | else: 42 | from _pytest.fixtures import FixtureFunction 43 | 44 | PytestFixtureT: TypeAlias = FixtureFunction # type: ignore[misc, no-redef] 45 | -------------------------------------------------------------------------------- /tests/test_namespaces.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | import pytest 4 | from _pytest.fixtures import FixtureLookupError 5 | from factory import Factory 6 | 7 | from pytest_factoryboy import register 8 | 9 | 10 | @dataclass 11 | class Foo: 12 | value: str 13 | 14 | 15 | @register 16 | class FooFactory(Factory): 17 | class Meta: 18 | model = Foo 19 | 20 | value = "module_foo" 21 | 22 | 23 | def test_module_namespace(foo): 24 | assert foo.value == "module_foo" 25 | 26 | 27 | class TestClassNamespace: 28 | @register 29 | class FooFactory(Factory): 30 | class Meta: 31 | model = Foo 32 | 33 | value = "class_foo" 34 | 35 | register(FooFactory, "class_foo") 36 | 37 | def test_class_namespace(self, class_foo, foo): 38 | assert foo.value == class_foo.value == "class_foo" 39 | 40 | class TestNestedClassNamespace: 41 | @register 42 | class FooFactory(Factory): 43 | class Meta: 44 | model = Foo 45 | 46 | value = "nested_class_foo" 47 | 48 | register(FooFactory, "nested_class_foo") 49 | 50 | def test_nested_class_namespace(self, foo, nested_class_foo): 51 | assert foo.value == nested_class_foo.value == "nested_class_foo" 52 | 53 | def test_nested_class_factories_dont_pollute_the_class(self, request): 54 | with pytest.raises(FixtureLookupError): 55 | request.getfixturevalue("nested_class_foo") 56 | 57 | 58 | def test_class_factories_dont_pollute_the_module(request): 59 | with pytest.raises(FixtureLookupError): 60 | request.getfixturevalue("class_foo") 61 | with pytest.raises(FixtureLookupError): 62 | request.getfixturevalue("nested_class_foo") 63 | -------------------------------------------------------------------------------- /tests/test_lazy_fixture.py: -------------------------------------------------------------------------------- 1 | """Test LazyFixture related features.""" 2 | 3 | from __future__ import annotations 4 | 5 | from dataclasses import dataclass 6 | 7 | import factory 8 | import pytest 9 | 10 | from pytest_factoryboy import LazyFixture, register 11 | 12 | 13 | @dataclass 14 | class User: 15 | """User account.""" 16 | 17 | username: str 18 | password: str 19 | is_active: bool 20 | 21 | 22 | class UserFactory(factory.Factory): 23 | """User factory.""" 24 | 25 | class Meta: 26 | model = User 27 | 28 | username = factory.faker.Faker("user_name") 29 | password = factory.faker.Faker("password") 30 | is_active = factory.LazyAttribute(lambda f: f.password == "ok") 31 | 32 | 33 | register(UserFactory) 34 | 35 | 36 | register( 37 | UserFactory, 38 | "partial_user", 39 | password=LazyFixture("ok_password"), 40 | ) 41 | 42 | 43 | @pytest.fixture 44 | def ok_password() -> str: 45 | return "ok" 46 | 47 | 48 | @pytest.mark.parametrize("user__password", [LazyFixture("ok_password")]) 49 | def test_lazy_attribute(user: User): 50 | """Test LazyFixture value is extracted before the LazyAttribute is called.""" 51 | assert user.is_active 52 | 53 | 54 | def test_lazy_attribute_partial(partial_user: User): 55 | """Test LazyFixture value is extracted before the LazyAttribute is called. Partial.""" 56 | assert partial_user.is_active 57 | 58 | 59 | class TestLazyFixtureDeclaration: 60 | @pytest.fixture 61 | def name(self): 62 | return "from fixture name" 63 | 64 | @register 65 | class UserFactory(factory.Factory): 66 | class Meta: 67 | model = User 68 | 69 | username = LazyFixture[str]("name") 70 | password = "foo" 71 | is_active = False 72 | 73 | def test_lazy_fixture_declaration(self, user): 74 | """Test that we can use the LazyFixture declaration in the factory itself.""" 75 | assert user.username == "from fixture name" 76 | -------------------------------------------------------------------------------- /tests/test_registration.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | 5 | import factory 6 | import pytest 7 | from _pytest.fixtures import FixtureLookupError 8 | 9 | from pytest_factoryboy import register 10 | 11 | 12 | @dataclass(eq=False) 13 | class Foo: 14 | value: str 15 | 16 | 17 | class TestRegisterDirectDecorator: 18 | @register() 19 | class FooFactory(factory.Factory): 20 | class Meta: 21 | model = Foo 22 | 23 | value = "@register()" 24 | 25 | def test_register(self, foo: Foo): 26 | """Test that `register` can be used as a decorator with 0 arguments.""" 27 | assert foo.value == "@register()" 28 | 29 | 30 | class TestRegisterDecoratorNoArgs: 31 | @register 32 | class FooFactory(factory.Factory): 33 | class Meta: 34 | model = Foo 35 | 36 | value = "@register" 37 | 38 | def test_register(self, foo: Foo): 39 | """Test that `register` can be used as a direct decorator.""" 40 | assert foo.value == "@register" 41 | 42 | 43 | class TestRegisterDecoratorWithArgs: 44 | @register(value="bar") 45 | class FooFactory(factory.Factory): 46 | class Meta: 47 | model = Foo 48 | 49 | value = None 50 | 51 | def test_register(self, foo: Foo): 52 | """Test that `register` can be used as a decorator with arguments overriding the factory declarations.""" 53 | assert foo.value == "bar" 54 | 55 | 56 | class TestRegisterAlternativeName: 57 | @register(_name="second_foo") 58 | class FooFactory(factory.Factory): 59 | class Meta: 60 | model = Foo 61 | 62 | value = None 63 | 64 | def test_register(self, request, second_foo: Foo): 65 | """Test that `register` invoked with a specific `_name` registers the fixture under that `_name`.""" 66 | assert second_foo.value == None 67 | 68 | with pytest.raises(FixtureLookupError) as exc: 69 | request.getfixturevalue("foo") 70 | assert exc.value.argname == "foo" 71 | 72 | 73 | class TestRegisterAlternativeNameAndArgs: 74 | @register(_name="second_foo", value="second_bar") 75 | class FooFactory(factory.Factory): 76 | class Meta: 77 | model = Foo 78 | 79 | value = None 80 | 81 | def test_register(self, second_foo: Foo): 82 | """Test that `register` can be invoked with `_name` to specify an alternative 83 | fixture name and with any kwargs to override the factory declarations.""" 84 | assert second_foo.value == "second_bar" 85 | 86 | 87 | class TestRegisterCall: 88 | class FooFactory(factory.Factory): 89 | class Meta: 90 | model = Foo 91 | 92 | value = "register(FooFactory)" 93 | 94 | register(FooFactory) 95 | register(FooFactory, _name="second_foo", value="second_bar") 96 | 97 | def test_register(self, foo: Foo, second_foo: Foo): 98 | """Test that `register` can be invoked directly.""" 99 | assert foo.value == "register(FooFactory)" 100 | assert second_foo.value == "second_bar" 101 | -------------------------------------------------------------------------------- /src/pytest_factoryboy/fixturegen.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import functools 4 | import inspect 5 | from collections.abc import Collection 6 | from typing import Callable, TypeVar 7 | 8 | import pytest 9 | from typing_extensions import ParamSpec 10 | 11 | from .compat import PytestFixtureT 12 | 13 | T = TypeVar("T") 14 | P = ParamSpec("P") 15 | 16 | 17 | def create_fixture( 18 | name: str, 19 | function: Callable[P, T], 20 | dependencies: Collection[str] | None = None, 21 | ) -> tuple[PytestFixtureT, Callable[P, T]]: 22 | """Dynamically create a pytest fixture. 23 | 24 | :param name: Name of the fixture. 25 | :param function: Function to be called. 26 | :param dependencies: List of fixtures dependencies, but that will not be passed to ``function``. 27 | :return: The created fixture function and the actual function. 28 | 29 | Example: 30 | 31 | .. code-block:: python 32 | 33 | book = create_fixture("book", lambda name: Book(name=name), usefixtures=["db"])`` 34 | 35 | is equivalent to: 36 | 37 | .. code-block:: python 38 | 39 | @pytest.fixture 40 | def book(name, db): 41 | return Book(name=name) 42 | """ 43 | if dependencies is None: 44 | dependencies = [] 45 | 46 | @usefixtures(*dependencies) 47 | @functools.wraps(function) 48 | def fn(*args: P.args, **kwargs: P.kwargs) -> T: 49 | return function(*args, **kwargs) 50 | 51 | fixture = pytest.fixture(name=name, fixture_function=fn) 52 | 53 | return fixture, fn 54 | 55 | 56 | def usefixtures(*fixtures: str) -> Callable[[Callable[P, T]], Callable[P, T]]: 57 | """Like ``@pytest.mark.usefixtures(...)``, but for fixture functions.""" 58 | 59 | def inner(fixture_function: Callable[P, T]) -> Callable[P, T]: 60 | function_params = list(inspect.signature(fixture_function).parameters.values()) 61 | allowed_param_kinds = {inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY} 62 | # This is exactly what pytest does (at the moment) to discover which args to inject as fixtures. 63 | function_args = { 64 | param.name 65 | for param in function_params 66 | if param.kind in allowed_param_kinds 67 | # Ignoring parameters with a default allows us to use ``functools.partial``s. 68 | and param.default == inspect.Parameter.empty 69 | } 70 | 71 | use_fixtures_params = [ 72 | inspect.Parameter(name=name, kind=inspect.Parameter.KEYWORD_ONLY) 73 | for name in fixtures 74 | if name not in function_args # if the name is already in the function signature, don't add it again 75 | ] 76 | # If the function ends with **kwargs, we have to insert our params before that. 77 | if function_params and function_params[-1].kind == inspect.Parameter.VAR_KEYWORD: 78 | insert_pos = len(function_params) - 1 79 | else: 80 | insert_pos = len(function_params) 81 | 82 | params = function_params[0:insert_pos] + use_fixtures_params + function_params[insert_pos:] 83 | 84 | @functools.wraps(fixture_function) 85 | def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: 86 | for k in set(kwargs.keys()) - function_args: 87 | del kwargs[k] 88 | return fixture_function(*args, **kwargs) 89 | 90 | wrapper.__signature__ = inspect.signature(wrapper).replace(parameters=params) # type: ignore[attr-defined] 91 | return wrapper 92 | 93 | return inner 94 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pytest-factoryboy" 3 | version = "2.8.1" 4 | description = "Factory Boy support for pytest." 5 | authors = [ { name = "Oleg Pidsadnyi", email= "oleg.pidsadnyi@gmail.com" } ] 6 | maintainers = [ { name = "Alessio Bogon", email = "778703+youtux@users.noreply.github.com" } ] 7 | license = "MIT" 8 | license-files = [ "LICENSE.md" ] 9 | readme = "README.rst" 10 | 11 | classifiers = [ 12 | "Development Status :: 6 - Mature", 13 | "Intended Audience :: Developers", 14 | "License :: OSI Approved :: MIT License", 15 | "Operating System :: POSIX", 16 | "Operating System :: Microsoft :: Windows", 17 | "Operating System :: MacOS :: MacOS X", 18 | "Topic :: Software Development :: Testing", 19 | "Topic :: Software Development :: Libraries", 20 | "Topic :: Utilities", 21 | "Programming Language :: Python :: 3", 22 | "Programming Language :: Python :: 3.9", 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | "Programming Language :: Python :: 3.12", 26 | "Programming Language :: Python :: 3.13", 27 | ] 28 | requires-python = ">=3.9" 29 | dependencies = [ 30 | "inflection", 31 | "factory_boy>=2.10.0", 32 | "pytest>=7.0", 33 | "typing_extensions", 34 | "packaging", 35 | ] 36 | 37 | [project.urls] 38 | homepage = "https://pytest-factoryboy.readthedocs.io/" 39 | documentation = "https://pytest-factoryboy.readthedocs.io/" 40 | repository = "https://github.com/pytest-dev/pytest-factoryboy" 41 | 42 | [project.entry-points."pytest11"] 43 | "pytest-factoryboy" = "pytest_factoryboy.plugin" 44 | 45 | [tool.poetry] 46 | packages = [{include = "pytest_factoryboy", from = "src"}] 47 | 48 | [tool.poetry.group.dev] 49 | optional = true 50 | 51 | [tool.poetry.group.dev.dependencies] 52 | mypy = ">=1.4.1" 53 | tox = ">=4.0.8" 54 | coverage = {extras = ["toml"], version = ">=6.5.0"} 55 | 56 | [build-system] 57 | requires = ["poetry-core (>=2.0.0, <3.0.0)"] 58 | build-backend = "poetry.core.masonry.api" 59 | 60 | [tool.black] 61 | line-length = 120 62 | target-version = ["py39", "py310", "py311", "py312", "py313"] 63 | 64 | [tool.isort] 65 | profile = "black" 66 | 67 | [tool.coverage.report] 68 | exclude_lines = [ 69 | "if TYPE_CHECKING:", 70 | "if typing\\.TYPE_CHECKING:", 71 | ] 72 | 73 | [tool.coverage.html] 74 | show_contexts = true 75 | 76 | [tool.coverage.run] 77 | branch = true 78 | # `parallel` will cause each tox env to put data into a different file, so that we can combine them later 79 | parallel = true 80 | source = ["pytest_factoryboy", "tests"] 81 | dynamic_context = "test_function" 82 | 83 | [tool.coverage.paths] 84 | # treat these directories as the same when combining 85 | # the first item is going to be the canonical dir 86 | source = [ 87 | "pytest_factoryboy", 88 | ".tox/*/lib/python*/site-packages/pytest_factoryboy", 89 | ] 90 | 91 | [tool.mypy] 92 | exclude = ['docs/'] 93 | allow_redefinition = false 94 | check_untyped_defs = true 95 | disallow_untyped_decorators = true 96 | disallow_any_explicit = false 97 | disallow_any_generics = true 98 | disallow_untyped_calls = false 99 | disallow_untyped_defs = true 100 | ignore_errors = false 101 | ignore_missing_imports = true 102 | implicit_reexport = false 103 | strict_optional = true 104 | no_implicit_optional = true 105 | warn_unused_ignores = true 106 | warn_redundant_casts = true 107 | warn_unused_configs = true 108 | warn_unreachable = true 109 | warn_no_return = true 110 | warn_return_any = true 111 | pretty = true 112 | show_error_codes = true 113 | 114 | [[tool.mypy.overrides]] 115 | module = ["tests.*"] 116 | disallow_untyped_decorators = false 117 | disallow_untyped_defs = false 118 | warn_return_any = false 119 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main testing workflow 2 | on: 3 | push: 4 | pull_request: 5 | workflow_dispatch: 6 | jobs: 7 | build: 8 | name: Build package 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Set up Python 13 | uses: actions/setup-python@v5 14 | with: 15 | cache: "pip" 16 | - name: Install pypa/build 17 | # We need a recent version of `packaging`, otherwise we encounter this bug: 18 | # https://github.com/pypa/twine/issues/1216 19 | run: python3 -m pip install --user build twine "packaging>=25.0" 20 | - name: Build a binary wheel and a source tarball 21 | run: python3 -m build 22 | - name: Check the distribution files with `twine` 23 | run: twine check --strict dist/* 24 | - name: Upload artifact 25 | id: artifact-upload-step 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: dist-files 29 | path: dist/* 30 | if-no-files-found: error 31 | compression-level: 0 # They are already compressed 32 | test-run: 33 | runs-on: ubuntu-latest 34 | needs: build 35 | strategy: 36 | matrix: 37 | include: 38 | - python-version: "3.9" 39 | toxfactor: py3.9 40 | ignore-typecheck-outcome: true 41 | ignore-test-outcome: false 42 | - python-version: "3.10" 43 | toxfactor: py3.10 44 | ignore-typecheck-outcome: false 45 | ignore-test-outcome: false 46 | - python-version: "3.11" 47 | toxfactor: py3.11 48 | ignore-typecheck-outcome: false 49 | ignore-test-outcome: false 50 | - python-version: "3.12" 51 | toxfactor: py3.12 52 | ignore-typecheck-outcome: false 53 | ignore-test-outcome: false 54 | - python-version: "3.13" 55 | toxfactor: py3.13 56 | ignore-typecheck-outcome: false 57 | ignore-test-outcome: false 58 | - python-version: "3.14" 59 | toxfactor: py3.14 60 | ignore-typecheck-outcome: false 61 | ignore-test-outcome: false 62 | steps: 63 | - uses: actions/checkout@v4 64 | - name: Set up Python ${{ matrix.python-version }} 65 | uses: actions/setup-python@v5 66 | id: setup-python 67 | with: 68 | python-version: ${{ matrix.python-version }} 69 | allow-prereleases: true 70 | - name: Install poetry 71 | run: | 72 | python -m pip install poetry==2.2.0 73 | - name: Configure poetry 74 | run: | 75 | poetry config virtualenvs.in-project true 76 | - name: Cache the virtualenv 77 | id: poetry-dependencies-cache 78 | uses: actions/cache@v3 79 | with: 80 | path: ./.venv 81 | key: ${{ runner.os }}-venv-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}${{ hashFiles('.github/workflows/**') }} 82 | - name: Install dev dependencies 83 | if: steps.poetry-dependencies-cache.outputs.cache-hit != 'true' 84 | run: | 85 | poetry install --with=dev 86 | - name: Download artifact 87 | uses: actions/download-artifact@v4 88 | with: 89 | name: dist-files 90 | path: dist/ 91 | - name: Type checking 92 | # Ignore errors for older pythons 93 | continue-on-error: ${{ matrix.ignore-typecheck-outcome }} 94 | run: | 95 | poetry run mypy src/pytest_factoryboy 96 | - name: Test with tox 97 | continue-on-error: ${{ matrix.ignore-test-outcome }} 98 | run: | 99 | source .venv/bin/activate 100 | coverage erase 101 | # Using `--parallel 4` as it's the number of CPUs in the GitHub Actions runner 102 | # Using `installpkg dist/*.whl` because we want to install the pre-built package (want to test against that) 103 | tox run-parallel -f ${{ matrix.toxfactor }} --parallel 4 --parallel-no-spinner --parallel-live --installpkg dist/*.whl 104 | coverage combine 105 | coverage xml 106 | - uses: codecov/codecov-action@v4 107 | with: 108 | # Explicitly using the token to avoid Codecov rate limit errors 109 | # See https://community.codecov.com/t/upload-issues-unable-to-locate-build-via-github-actions-api/3954 110 | token: ${{ secrets.CODECOV_TOKEN }} 111 | fail_ci_if_error: false 112 | verbose: true # optional (default = false) 113 | pypi-publish: 114 | name: Upload release to PyPI 115 | runs-on: ubuntu-latest 116 | environment: 117 | name: pypi 118 | url: https://pypi.org/p/pytest-factoryboy 119 | permissions: 120 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 121 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 122 | needs: 123 | - "test-run" 124 | - "build" 125 | steps: 126 | - name: Download artifact 127 | uses: actions/download-artifact@v4 128 | with: 129 | name: dist-files 130 | path: dist/ 131 | - name: Publish package distributions to PyPI 132 | uses: pypa/gh-action-pypi-publish@release/v1 133 | -------------------------------------------------------------------------------- /tests/test_model_name.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | import factory 4 | import pytest 5 | 6 | from pytest_factoryboy.fixture import get_model_name, named_model 7 | from tests.compat import assert_outcomes 8 | 9 | 10 | def make_class(name: str): 11 | """Create a class with the given name.""" 12 | return type(name, (object,), {}) 13 | 14 | 15 | @pytest.mark.parametrize("model_cls", [dict, set, list, frozenset, tuple]) 16 | def test_get_model_name_warns_for_common_containers(model_cls): 17 | """Test that a warning is raised when common containers are used as models.""" 18 | 19 | class ModelFactory(factory.Factory): 20 | class Meta: 21 | model = model_cls 22 | 23 | with pytest.warns( 24 | UserWarning, 25 | match=rf"Using a .*{model_cls.__name__}.* as model type for .*ModelFactory.* is discouraged", 26 | ): 27 | assert get_model_name(ModelFactory) 28 | 29 | 30 | def test_get_model_name_does_not_warn_for_user_defined_models(): 31 | """Test that no warning is raised for when using user-defined models""" 32 | 33 | class Foo: 34 | pass 35 | 36 | class ModelFactory(factory.Factory): 37 | class Meta: 38 | model = Foo 39 | 40 | with warnings.catch_warnings(): 41 | warnings.simplefilter("error") 42 | assert get_model_name(ModelFactory) == "foo" 43 | 44 | 45 | @pytest.mark.parametrize( 46 | ["model_cls", "expected"], 47 | [ 48 | (make_class("Foo"), "foo"), 49 | (make_class("TwoWords"), "two_words"), 50 | (make_class("HTTPHeader"), "http_header"), 51 | (make_class("C3PO"), "c3_po"), 52 | ], 53 | ) 54 | def test_get_model_name(model_cls, expected): 55 | """Test normal cases for ``get_model_name``.""" 56 | 57 | class ModelFactory(factory.Factory): 58 | class Meta: 59 | model = model_cls 60 | 61 | assert get_model_name(ModelFactory) == expected 62 | 63 | 64 | def test_named_model(): 65 | """Assert behaviour of ``named_model``.""" 66 | cls = named_model(dict, "Foo") 67 | 68 | assert cls.__name__ == "Foo" 69 | assert issubclass(cls, dict) 70 | 71 | 72 | def test_generic_model_with_custom_name_no_warning(testdir): 73 | testdir.makepyfile( 74 | """ 75 | from factory import Factory 76 | from pytest_factoryboy import named_model, register 77 | 78 | @register 79 | class JSONPayloadFactory(Factory): 80 | class Meta: 81 | model = named_model(dict, "JSONPayload") 82 | foo = "bar" 83 | 84 | 85 | def test_payload(json_payload: dict): 86 | assert isinstance(json_payload, dict) 87 | assert json_payload["foo"] == "bar" 88 | """ 89 | ) 90 | result = testdir.runpytest("-Werror") # Warnings become errors 91 | assert_outcomes(result, passed=1) 92 | 93 | 94 | def test_generic_model_name_raises_warning(testdir): 95 | testdir.makepyfile( 96 | """ 97 | import builtins 98 | from factory import Factory 99 | from pytest_factoryboy import register 100 | 101 | @register 102 | class JSONPayloadFactory(Factory): 103 | class Meta: 104 | model = dict 105 | foo = "bar" 106 | 107 | 108 | def test_payload(dict): 109 | assert isinstance(dict, builtins.dict) 110 | assert dict["foo"] == "bar" 111 | """ 112 | ) 113 | result = testdir.runpytest() 114 | assert_outcomes(result, passed=1) 115 | result.stdout.fnmatch_lines( 116 | "*UserWarning: Using a *class*dict* as model type for *JSONPayloadFactory* is discouraged*" 117 | ) 118 | 119 | 120 | def test_generic_model_with_register_override_no_warning(testdir): 121 | testdir.makepyfile( 122 | """ 123 | from factory import Factory 124 | from pytest_factoryboy import named_model, register 125 | 126 | @register(_name="json_payload") 127 | class JSONPayloadFactory(Factory): 128 | class Meta: 129 | model = dict 130 | foo = "bar" 131 | 132 | 133 | def test_payload(json_payload: dict): 134 | assert isinstance(json_payload, dict) 135 | assert json_payload["foo"] == "bar" 136 | 137 | """ 138 | ) 139 | result = testdir.runpytest("-Werror") # Warnings become errors 140 | assert_outcomes(result, passed=1) 141 | 142 | 143 | def test_using_generic_model_name_for_subfactory_raises_warning(testdir): 144 | testdir.makepyfile( 145 | """ 146 | import builtins 147 | from factory import Factory, SubFactory 148 | from pytest_factoryboy import register 149 | 150 | @register(_name="JSONPayload") 151 | class JSONPayloadFactory(Factory): 152 | class Meta: 153 | model = dict # no warning raised here, since we override the name at the @register(...) 154 | foo = "bar" 155 | 156 | class HTTPRequest: 157 | def __init__(self, json: dict): 158 | self.json = json 159 | 160 | @register 161 | class HTTPRequestFactory(Factory): 162 | class Meta: 163 | model = HTTPRequest 164 | 165 | json = SubFactory(JSONPayloadFactory) # this will raise a warning 166 | 167 | def test_payload(http_request): 168 | assert http_request.json["foo"] == "bar" 169 | """ 170 | ) 171 | 172 | result = testdir.runpytest() 173 | assert_outcomes(result, errors=1) 174 | result.stdout.fnmatch_lines( 175 | "*UserWarning: Using *class*dict* as model type for *JSONPayloadFactory* is discouraged*" 176 | ) 177 | -------------------------------------------------------------------------------- /src/pytest_factoryboy/plugin.py: -------------------------------------------------------------------------------- 1 | """pytest-factoryboy plugin.""" 2 | 3 | from __future__ import annotations 4 | 5 | from collections import defaultdict 6 | 7 | import pytest 8 | from _pytest.config import PytestPluginManager 9 | from _pytest.fixtures import FixtureRequest, SubRequest 10 | from _pytest.nodes import Item 11 | from _pytest.python import Metafunc 12 | from factory.base import Factory 13 | 14 | from .compat import getfixturedefs 15 | from .fixture import DeferredFunction 16 | 17 | 18 | class CycleDetected(Exception): 19 | pass 20 | 21 | 22 | class Request: 23 | """PyTest FactoryBoy request.""" 24 | 25 | def __init__(self) -> None: 26 | """Create pytest_factoryboy request.""" 27 | self.deferred: list[list[DeferredFunction[object, object]]] = [] 28 | self.results: dict[str, dict[str, object]] = defaultdict(dict) 29 | self.model_factories: dict[str, type[Factory[object]]] = {} 30 | self.in_progress: set[DeferredFunction[object, object]] = set() 31 | 32 | def defer(self, functions: list[DeferredFunction[object, object]]) -> None: 33 | """Defer post-generation declaration execution until the end of the test setup. 34 | 35 | :param functions: Functions to be deferred. 36 | :note: Once already finalized all following defer calls will execute the function directly. 37 | """ 38 | self.deferred.append(functions) 39 | 40 | def get_deps(self, request: SubRequest, fixture: str, deps: set[str] | None = None) -> set[str]: 41 | request = request.getfixturevalue("request") 42 | 43 | if deps is None: 44 | deps = {fixture} 45 | if fixture == "request": 46 | return deps 47 | 48 | assert request._pyfuncitem.parent is not None, "Request must have a parent item." 49 | 50 | fixturedefs = getfixturedefs(request._fixturemanager, fixture, request._pyfuncitem.parent) 51 | for fixturedef in fixturedefs or []: 52 | for argname in fixturedef.argnames: 53 | if argname not in deps: 54 | deps.add(argname) 55 | deps.update(self.get_deps(request, argname, deps)) 56 | return deps 57 | 58 | def get_current_deps(self, request: FixtureRequest | SubRequest) -> set[str]: 59 | deps = set() 60 | while hasattr(request, "_parent_request"): 61 | if request.fixturename and request.fixturename not in getattr(request, "_fixturedefs", {}): 62 | deps.add(request.fixturename) 63 | request = request._parent_request 64 | return deps 65 | 66 | def execute( 67 | self, 68 | request: SubRequest, 69 | function: DeferredFunction[object, object], 70 | deferred: list[DeferredFunction[object, object]], 71 | ) -> None: 72 | """Execute deferred function and store the result.""" 73 | if function in self.in_progress: 74 | raise CycleDetected() 75 | fixture = function.name 76 | model, attr = fixture.split("__", 1) 77 | if function.is_related: 78 | deps = self.get_deps(request, fixture) 79 | if deps.intersection(self.get_current_deps(request)): 80 | raise CycleDetected() 81 | self.model_factories[model] = function.factory 82 | 83 | self.in_progress.add(function) 84 | self.results[model][attr] = function(request) 85 | deferred.remove(function) 86 | self.in_progress.remove(function) 87 | 88 | def after_postgeneration(self, request: SubRequest) -> None: 89 | """Call _after_postgeneration hooks.""" 90 | for model in list(self.results.keys()): 91 | results = self.results.pop(model) 92 | obj = request.getfixturevalue(model) 93 | factory = self.model_factories[model] 94 | factory._after_postgeneration(obj, create=True, results=results) 95 | 96 | def evaluate(self, request: SubRequest) -> None: 97 | """Finalize, run deferred post-generation actions, etc.""" 98 | while self.deferred: 99 | try: 100 | deferred = self.deferred[-1] 101 | for function in list(deferred): 102 | self.execute(request, function, deferred) 103 | if not deferred: 104 | self.deferred.remove(deferred) 105 | except CycleDetected: 106 | return 107 | 108 | if not self.deferred: 109 | self.after_postgeneration(request) 110 | 111 | 112 | @pytest.fixture 113 | def factoryboy_request() -> Request: 114 | """PyTest FactoryBoy request fixture.""" 115 | return Request() 116 | 117 | 118 | @pytest.hookimpl(tryfirst=True) 119 | def pytest_runtest_call(item: Item) -> None: 120 | """Before the test item is called.""" 121 | # TODO: We should instead do an `if isinstance(item, Function)`. 122 | try: 123 | request = item._request # type: ignore[attr-defined] 124 | except AttributeError: 125 | # pytest-pep8 plugin passes Pep8Item here during tests. 126 | return 127 | factoryboy_request = request.getfixturevalue("factoryboy_request") 128 | factoryboy_request.evaluate(request) 129 | assert not factoryboy_request.deferred 130 | request.config.hook.pytest_factoryboy_done(request=request) 131 | 132 | 133 | def pytest_addhooks(pluginmanager: PytestPluginManager) -> None: 134 | """Register plugin hooks.""" 135 | from pytest_factoryboy import hooks 136 | 137 | pluginmanager.add_hookspecs(hooks) 138 | 139 | 140 | def pytest_generate_tests(metafunc: Metafunc) -> None: 141 | related: list[str] = [] 142 | for arg2fixturedef in metafunc._arg2fixturedefs.values(): 143 | fixturedef = arg2fixturedef[-1] 144 | related_fixtures = getattr(fixturedef.func, "_factoryboy_related", []) 145 | related.extend(related_fixtures) 146 | 147 | metafunc.fixturenames.extend(related) 148 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Pytest-BDD.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Pytest-BDD.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Pytest-BDD" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Pytest-BDD" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /tests/test_factory_fixtures.py: -------------------------------------------------------------------------------- 1 | """Factory fixtures tests.""" 2 | 3 | from __future__ import annotations 4 | 5 | from dataclasses import dataclass, field 6 | from typing import TYPE_CHECKING 7 | 8 | import factory 9 | import pytest 10 | from factory import fuzzy 11 | 12 | from pytest_factoryboy import LazyFixture, register 13 | 14 | if TYPE_CHECKING: 15 | from factory.declarations import LazyAttribute 16 | 17 | 18 | @dataclass 19 | class User: 20 | """User account.""" 21 | 22 | username: str 23 | password: str 24 | is_active: bool 25 | 26 | 27 | @dataclass 28 | class Book: 29 | """Book model.""" 30 | 31 | name: str 32 | price: float 33 | author: Author 34 | editions: list[Edition] = field(default_factory=list, init=False) 35 | 36 | 37 | @dataclass 38 | class Author: 39 | """Author model.""" 40 | 41 | name: str 42 | user: User | None = field(init=False, default=None) 43 | 44 | 45 | @dataclass 46 | class Edition: 47 | """Book edition.""" 48 | 49 | book: Book 50 | year: int 51 | 52 | def __post_init__(self) -> None: 53 | self.book.editions.append(self) 54 | 55 | 56 | class UserFactory(factory.Factory): 57 | """User factory.""" 58 | 59 | class Meta: 60 | model = User 61 | 62 | password = fuzzy.FuzzyText(length=7) 63 | 64 | 65 | @register 66 | class AuthorFactory(factory.Factory): 67 | """Author factory.""" 68 | 69 | class Meta: 70 | model = Author 71 | 72 | name = "Charles Dickens" 73 | 74 | register_user__is_active = True # Make sure fixture is generated 75 | register_user__password = "qwerty" # Make sure fixture is generated 76 | 77 | @factory.post_generation 78 | def register_user(author: Author, create: bool, username: str | None, **kwargs: object) -> None: 79 | """Register author as a user in the system.""" 80 | if username is not None: 81 | author.user = UserFactory(username=username, **kwargs) 82 | 83 | 84 | class BookFactory(factory.Factory): 85 | """Test factory with all the features.""" 86 | 87 | class Meta: 88 | model = Book 89 | 90 | name = "Alice in Wonderland" 91 | price = factory.LazyAttribute(lambda f: 3.99) 92 | author = factory.SubFactory(AuthorFactory) 93 | book_edition = factory.RelatedFactory("tests.test_factory_fixtures.EditionFactory", "book") 94 | 95 | 96 | class EditionFactory(factory.Factory): 97 | """Book edition factory.""" 98 | 99 | class Meta: 100 | model = Edition 101 | 102 | book = factory.SubFactory(BookFactory) 103 | year = 1999 104 | 105 | 106 | register(BookFactory) 107 | register(EditionFactory) 108 | 109 | 110 | def test_factory(book_factory) -> None: 111 | """Test model factory fixture.""" 112 | assert book_factory == BookFactory 113 | 114 | 115 | def test_model(book: Book): 116 | """Test model fixture.""" 117 | assert book.name == "Alice in Wonderland" 118 | assert book.price == 3.99 119 | assert book.author.name == "Charles Dickens" 120 | assert book.author.user is None 121 | assert book.editions[0].year == 1999 122 | assert book.editions[0].book == book 123 | 124 | 125 | def test_attr(book__name, book__price, author__name, edition__year): 126 | """Test attribute fixtures. 127 | 128 | :note: Most of the attributes are lazy definitions. Use attribute fixtures in 129 | order to override the initial values. 130 | """ 131 | assert book__name == "Alice in Wonderland" 132 | assert book__price == BookFactory.price 133 | assert author__name == "Charles Dickens" 134 | assert edition__year == 1999 135 | 136 | 137 | @pytest.mark.parametrize("book__name", ["PyTest for Dummies"]) 138 | @pytest.mark.parametrize("book__price", [1.0]) 139 | @pytest.mark.parametrize("author__name", ["Bill Gates"]) 140 | @pytest.mark.parametrize("edition__year", [2000]) 141 | def test_parametrized(book: Book): 142 | """Test model factory fixture.""" 143 | assert book.name == "PyTest for Dummies" 144 | assert book.price == 1.0 145 | assert book.author.name == "Bill Gates" 146 | assert len(book.editions) == 1 147 | assert book.editions[0].year == 2000 148 | 149 | 150 | @pytest.mark.parametrize("author__register_user", ["admin"]) 151 | def test_post_generation(author: Author): 152 | """Test post generation declaration.""" 153 | assert author.user 154 | assert author.user.username == "admin" 155 | assert author.user.is_active is True 156 | 157 | 158 | class TestParametrizeAlternativeNameFixture: 159 | register(AuthorFactory, "second_author") 160 | 161 | @pytest.mark.parametrize("second_author__name", ["Mr. Hyde"]) 162 | def test_second_author(self, author: Author, second_author: Author): 163 | """Test parametrization of attributes for fixture registered under a different name 164 | ("second_author").""" 165 | assert author != second_author 166 | assert second_author.name == "Mr. Hyde" 167 | 168 | 169 | class TestPartialSpecialization: 170 | register(AuthorFactory, "partial_author", name="John Doe", register_user=LazyFixture(lambda: "jd@jd.com")) 171 | 172 | def test_partial(self, partial_author: Author): 173 | """Test fixture partial specialization.""" 174 | assert partial_author.name == "John Doe" 175 | assert partial_author.user # Makes mypy happy 176 | assert partial_author.user.username == "jd@jd.com" 177 | 178 | 179 | class TestLazyFixture: 180 | register(AuthorFactory, "another_author", name=LazyFixture(lambda: "Another Author")) 181 | register(BookFactory, "another_book", author=LazyFixture("another_author")) 182 | 183 | @pytest.mark.parametrize("book__author", [LazyFixture("another_author")]) 184 | def test_lazy_fixture_name(self, book: Book, another_author: Author): 185 | """Test that book author is replaced with another author by fixture name.""" 186 | assert book.author == another_author 187 | assert book.author.name == "Another Author" 188 | 189 | @pytest.mark.parametrize("book__author", [LazyFixture(lambda another_author: another_author)]) 190 | def test_lazy_fixture_callable(self, book: Book, another_author: Author) -> None: 191 | """Test that book author is replaced with another author by callable.""" 192 | assert book.author == another_author 193 | assert book.author.name == "Another Author" 194 | 195 | @pytest.mark.parametrize( 196 | ("author__register_user", "author__register_user__password"), 197 | [ 198 | (LazyFixture(lambda: "lazyfixture"), LazyFixture(lambda: "asdasd")), 199 | ], 200 | ) 201 | def test_lazy_fixture_post_generation(self, author: Author): 202 | """Test that post-generation values are replaced with lazy fixtures.""" 203 | assert author.user 204 | assert author.user.username == "lazyfixture" 205 | assert author.user.password == "asdasd" 206 | 207 | def test_override_subfactory_with_lazy_fixture(self, another_book: Book): 208 | """Ensure subfactory fixture can be overriden with ``LazyFixture``. 209 | 210 | Issue: https://github.com/pytest-dev/pytest-factoryboy/issues/158 211 | 212 | """ 213 | assert another_book.author.name == "Another Author" 214 | -------------------------------------------------------------------------------- /tests/test_postgen_dependencies.py: -------------------------------------------------------------------------------- 1 | """Test post-generation dependencies.""" 2 | 3 | from __future__ import annotations 4 | 5 | from collections.abc import Mapping 6 | from dataclasses import dataclass, field 7 | from typing import TYPE_CHECKING 8 | 9 | import factory 10 | import pytest 11 | from factory.declarations import NotProvided 12 | 13 | from pytest_factoryboy import register 14 | 15 | if TYPE_CHECKING: 16 | from pytest_factoryboy.plugin import Request 17 | 18 | 19 | @dataclass 20 | class Foo: 21 | value: int 22 | expected: int 23 | secret: str = "" 24 | number: int = 4 25 | 26 | def set_secret(self, new_secret: str) -> None: 27 | self.secret = new_secret 28 | 29 | def set_number(self, new_number: int = 987) -> None: 30 | self.number = new_number 31 | 32 | bar: Bar | None = None 33 | 34 | # NOTE: following attributes are used internally only for assertions 35 | _create: bool | None = None 36 | _postgeneration_results: dict[str, object] = field(default_factory=dict) 37 | 38 | 39 | @dataclass 40 | class Bar: 41 | foo: Foo 42 | 43 | 44 | @dataclass 45 | class Baz: 46 | foo: Foo 47 | 48 | 49 | @register 50 | class BazFactory(factory.Factory): 51 | class Meta: 52 | model = Baz 53 | 54 | foo = None 55 | 56 | 57 | @register 58 | class FooFactory(factory.Factory): 59 | """Foo factory.""" 60 | 61 | class Meta: 62 | model = Foo 63 | 64 | value = 0 65 | #: Value that is expected at the constructor 66 | expected = 0 67 | 68 | secret = factory.PostGenerationMethodCall("set_secret", "super secret") 69 | number = factory.PostGenerationMethodCall("set_number") 70 | 71 | @factory.post_generation 72 | def set1(foo: Foo, create: bool, value: object, **kwargs: object) -> str: 73 | foo.value = 1 74 | return "set to 1" 75 | 76 | baz = factory.RelatedFactory(BazFactory, factory_related_name="foo") 77 | 78 | @factory.post_generation 79 | def set2(foo, create, value, **kwargs): 80 | if create and value: 81 | foo.value = value 82 | 83 | @classmethod 84 | def _after_postgeneration(cls, obj: Foo, create: bool, results: Mapping[str, object] | None = None) -> None: 85 | obj._postgeneration_results = results or {} 86 | obj._create = create 87 | 88 | 89 | class BarFactory(factory.Factory): 90 | """Bar factory.""" 91 | 92 | foo = factory.SubFactory(FooFactory) 93 | 94 | @classmethod 95 | def _create(cls, model_class: type[Bar], foo: Foo) -> Bar: 96 | assert foo.value == foo.expected 97 | bar: Bar = super()._create(model_class, foo=foo) 98 | foo.bar = bar 99 | return bar 100 | 101 | class Meta: 102 | model = Bar 103 | 104 | 105 | def test_postgen_invoked(foo: Foo): 106 | """Test that post-generation hooks are done and the value is 2.""" 107 | assert foo.value == 1 108 | 109 | 110 | register(BarFactory) 111 | 112 | 113 | @pytest.mark.parametrize("foo__value", [3]) 114 | @pytest.mark.parametrize("foo__expected", [1]) 115 | def test_depends_on(bar: Bar): 116 | """Test that post-generation hooks are done and the value is 1.""" 117 | assert bar.foo.value == 1 118 | 119 | 120 | def test_getfixturevalue(request, factoryboy_request: Request): 121 | """Test post-generation declarations via the getfixturevalue.""" 122 | foo = request.getfixturevalue("foo") 123 | assert not factoryboy_request.deferred 124 | assert foo.value == 1 125 | assert foo.secret == "super secret" 126 | assert foo.number == 987 127 | 128 | 129 | def test_postgenerationmethodcall_getfixturevalue(request, factoryboy_request): 130 | """Test default fixture value generated for ``PostGenerationMethodCall``.""" 131 | secret = request.getfixturevalue("foo__secret") 132 | number = request.getfixturevalue("foo__number") 133 | assert not factoryboy_request.deferred 134 | assert secret == "super secret" 135 | assert number is NotProvided 136 | 137 | 138 | def test_postgeneration_getfixturevalue(request, factoryboy_request): 139 | """Ensure default fixture value generated for ``PostGeneration`` is `None`.""" 140 | set1 = request.getfixturevalue("foo__set1") 141 | set2 = request.getfixturevalue("foo__set2") 142 | assert not factoryboy_request.deferred 143 | assert set1 is None 144 | assert set2 is None 145 | 146 | 147 | def test_after_postgeneration(foo: Foo): 148 | """Test _after_postgeneration is called.""" 149 | assert foo._create is True 150 | 151 | assert foo._postgeneration_results["set1"] == "set to 1" 152 | assert foo._postgeneration_results["set2"] is None 153 | assert foo._postgeneration_results["secret"] is None 154 | assert foo._postgeneration_results["number"] is None 155 | 156 | 157 | @pytest.mark.xfail(reason="This test has been broken for a long time, we only discovered it recently") 158 | def test_postgen_related(foo: Foo): 159 | """Test that the initiating object `foo` is passed to the RelatedFactory `BazFactory`.""" 160 | baz = foo._postgeneration_results["baz"] 161 | assert baz.foo is foo 162 | 163 | 164 | @pytest.mark.parametrize("foo__set2", [123]) 165 | def test_postgeneration_fixture(foo: Foo): 166 | """Test fixture for ``PostGeneration`` declaration.""" 167 | assert foo.value == 123 168 | 169 | 170 | @pytest.mark.parametrize( 171 | ("foo__secret", "foo__number"), 172 | [ 173 | ("test secret", 456), 174 | ], 175 | ) 176 | def test_postgenerationmethodcall_fixture(foo: Foo): 177 | """Test fixture for ``PostGenerationMethodCall`` declaration.""" 178 | assert foo.secret == "test secret" 179 | assert foo.number == 456 180 | 181 | 182 | class TestPostgenerationCalledOnce: 183 | @register(_name="collector") 184 | class CollectorFactory(factory.Factory): 185 | class Meta: 186 | model = dict 187 | 188 | foo = factory.PostGeneration(lambda *args, **kwargs: 42) 189 | 190 | @classmethod 191 | def _after_postgeneration( 192 | cls, obj: Mapping[str, object], create: bool, results: Mapping[str, object] | None = None 193 | ) -> None: 194 | obj.setdefault("_after_postgeneration_calls", []).append((obj, create, results)) 195 | 196 | def test_postgeneration_called_once(self, request): 197 | """Test that ``_after_postgeneration`` is called only once.""" 198 | foo = request.getfixturevalue("collector") 199 | calls = foo["_after_postgeneration_calls"] 200 | assert len(calls) == 1 201 | [[obj, create, results]] = calls 202 | 203 | assert obj is foo 204 | assert create is True 205 | assert isinstance(results, dict) 206 | assert results["foo"] == 42 207 | 208 | 209 | @dataclass 210 | class Ordered: 211 | value: str | None = None 212 | 213 | 214 | @register 215 | class OrderedFactory(factory.Factory): 216 | class Meta: 217 | model = Ordered 218 | 219 | @factory.post_generation 220 | def zzz(obj: Ordered, create: bool, val: object, **kwargs: object) -> None: 221 | obj.value = "zzz" 222 | 223 | @factory.post_generation 224 | def aaa(obj: Ordered, create: bool, val: object, **kwargs: object) -> None: 225 | obj.value = "aaa" 226 | 227 | 228 | def test_ordered(ordered: Ordered): 229 | """Test post generation are ordered by creation counter.""" 230 | assert ordered.value == "aaa" 231 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | """Sphinx config.""" 2 | 3 | # 4 | # pytest-factoryboy documentation build configuration file, created by 5 | # sphinx-quickstart on Sun Apr 7 21:07:56 2013. 6 | # 7 | # This file is execfile()d with the current directory set to its containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | 19 | import os 20 | import sys 21 | from importlib import metadata 22 | 23 | sys.path.insert(0, os.path.abspath("..")) 24 | 25 | 26 | # -- General configuration ----------------------------------------------------- 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be extensions 32 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 33 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ["_templates"] 37 | 38 | # The suffix of source filenames. 39 | source_suffix = ".rst" 40 | 41 | # The encoding of source files. 42 | # source_encoding = 'utf-8-sig' 43 | 44 | # The master toctree document. 45 | master_doc = "index" 46 | 47 | # General information about the project. 48 | project = "pytest-factoryboy" 49 | AUTHOR = "Oleg Pidsadnyi, Anatoly Bubenkov and others" 50 | copyright = "2015, " + AUTHOR 51 | 52 | # The version info for the project you're documenting, acts as replacement for 53 | # |version| and |release|, also used in various other places throughout the 54 | # built documents. 55 | # 56 | # The short X.Y version. 57 | 58 | version = metadata.version("pytest-factoryboy") 59 | # The full version, including alpha/beta/rc tags. 60 | release = version 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # language = None 65 | 66 | # There are two options for replacing |today|: either, you set today to some 67 | # non-false value, then it is used: 68 | # today = '' 69 | # Else, today_fmt is used as the format for a strftime call. 70 | # today_fmt = '%B %d, %Y' 71 | 72 | # List of patterns, relative to source directory, that match files and 73 | # directories to ignore when looking for source files. 74 | exclude_patterns = ["_build"] 75 | 76 | # The reST default role (used for this markup: `text`) to use for all documents. 77 | # default_role = None 78 | 79 | # If true, '()' will be appended to :func: etc. cross-reference text. 80 | # add_function_parentheses = True 81 | 82 | # If true, the current module name will be prepended to all description 83 | # unit titles (such as .. function::). 84 | # add_module_names = True 85 | 86 | # If true, sectionauthor and moduleauthor directives will be shown in the 87 | # output. They are ignored by default. 88 | # show_authors = False 89 | 90 | # The name of the Pygments (syntax highlighting) style to use. 91 | pygments_style = "sphinx" 92 | 93 | # A list of ignored prefixes for module index sorting. 94 | # modindex_common_prefix = [] 95 | 96 | 97 | # -- Options for HTML output --------------------------------------------------- 98 | 99 | # The theme to use for HTML and HTML Help pages. See the documentation for 100 | # a list of builtin themes. 101 | html_theme = "sphinx_rtd_theme" 102 | 103 | # Theme options are theme-specific and customize the look and feel of a theme 104 | # further. For a list of options available for each theme, see the 105 | # documentation. 106 | # html_theme_options = {} 107 | 108 | # Add any paths that contain custom themes here, relative to this directory. 109 | # html_theme_path = [] 110 | 111 | # The name for this set of Sphinx documents. If None, it defaults to 112 | # " v documentation". 113 | # html_title = None 114 | 115 | # A shorter title for the navigation bar. Default is the same as html_title. 116 | # html_short_title = None 117 | 118 | # The name of an image file (relative to this directory) to place at the top 119 | # of the sidebar. 120 | # html_logo = None 121 | 122 | # The name of an image file (within the static path) to use as favicon of the 123 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 124 | # pixels large. 125 | # html_favicon = None 126 | 127 | # Add any paths that contain custom static files (such as style sheets) here, 128 | # relative to this directory. They are copied after the builtin static files, 129 | # so a file named "default.css" will overwrite the builtin "default.css". 130 | html_static_path = ["_static"] 131 | 132 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 133 | # using the given strftime format. 134 | # html_last_updated_fmt = '%b %d, %Y' 135 | 136 | # If true, SmartyPants will be used to convert quotes and dashes to 137 | # typographically correct entities. 138 | # html_use_smartypants = True 139 | 140 | # Custom sidebar templates, maps document names to template names. 141 | # html_sidebars = {} 142 | 143 | # Additional templates that should be rendered to pages, maps page names to 144 | # template names. 145 | # html_additional_pages = {} 146 | 147 | # If false, no module index is generated. 148 | # html_domain_indices = True 149 | 150 | # If false, no index is generated. 151 | # html_use_index = True 152 | 153 | # If true, the index is split into individual pages for each letter. 154 | # html_split_index = False 155 | 156 | # If true, links to the reST sources are added to the pages. 157 | # html_show_sourcelink = True 158 | 159 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 160 | # html_show_sphinx = True 161 | 162 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 163 | # html_show_copyright = True 164 | 165 | # If true, an OpenSearch description file will be output, and all pages will 166 | # contain a tag referring to it. The value of this option must be the 167 | # base URL from which the finished HTML is served. 168 | # html_use_opensearch = '' 169 | 170 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 171 | # html_file_suffix = None 172 | 173 | # Output file base name for HTML help builder. 174 | htmlhelp_basename = "pytest-factoryboy-doc" 175 | 176 | 177 | # -- Options for LaTeX output -------------------------------------------------- 178 | 179 | latex_elements = { 180 | # The paper size ('letterpaper' or 'a4paper'). 181 | #'papersize': 'letterpaper', 182 | # The font size ('10pt', '11pt' or '12pt'). 183 | #'pointsize': '10pt', 184 | # Additional stuff for the LaTeX preamble. 185 | #'preamble': '', 186 | } 187 | 188 | # Grouping the document tree into LaTeX files. List of tuples 189 | # (source start file, target name, title, author, documentclass [howto/manual]). 190 | latex_documents = [ 191 | ("index", "pytest-factoryboy.tex", "pytest-factoryboy Documentation", AUTHOR, "manual"), 192 | ] 193 | 194 | # The name of an image file (relative to this directory) to place at the top of 195 | # the title page. 196 | # latex_logo = None 197 | 198 | # For "manual" documents, if this is true, then toplevel headings are parts, 199 | # not chapters. 200 | # latex_use_parts = False 201 | 202 | # If true, show page references after internal links. 203 | # latex_show_pagerefs = False 204 | 205 | # If true, show URL addresses after external links. 206 | # latex_show_urls = False 207 | 208 | # Documents to append as an appendix to all manuals. 209 | # latex_appendices = [] 210 | 211 | # If false, no module index is generated. 212 | # latex_domain_indices = True 213 | 214 | 215 | # -- Options for manual page output -------------------------------------------- 216 | 217 | # One entry per manual page. List of tuples 218 | # (source start file, name, description, authors, manual section). 219 | man_pages = [("index", "pytest-factoryboy", "pytest-factoryboy Documentation", [AUTHOR], 1)] 220 | 221 | # If true, show URL addresses after external links. 222 | # man_show_urls = False 223 | 224 | 225 | # -- Options for Texinfo output ------------------------------------------------ 226 | 227 | # Grouping the document tree into Texinfo files. List of tuples 228 | # (source start file, target name, title, author, 229 | # dir menu entry, description, category) 230 | texinfo_documents = [ 231 | ( 232 | "index", 233 | "pytest-factoryboy", 234 | "pytest-factoryboy Documentation", 235 | AUTHOR, 236 | "pytest-factoryboy", 237 | "factory_boy integration the pytest runner.", 238 | "Miscellaneous", 239 | ), 240 | ] 241 | 242 | # Documents to append as an appendix to all manuals. 243 | # texinfo_appendices = [] 244 | 245 | # If false, no module index is generated. 246 | # texinfo_domain_indices = True 247 | 248 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 249 | # texinfo_show_urls = 'footnote' 250 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | All notable changes to this project will be documented in this file. 5 | 6 | The format is based on `Keep a Changelog `_, 7 | and this project adheres to `Semantic Versioning `_. 8 | 9 | Unreleased 10 | ---------- 11 | 12 | Added 13 | +++++ 14 | 15 | Changed 16 | +++++++ 17 | 18 | Deprecated 19 | ++++++++++ 20 | 21 | Removed 22 | +++++++ 23 | 24 | Fixed 25 | +++++ 26 | 27 | Security 28 | ++++++++ 29 | 30 | 2.8.1 31 | ---------- 32 | 33 | Added 34 | +++++ 35 | * Test against all supported ``factory-boy`` versions 36 | 37 | Changed 38 | +++++++ 39 | 40 | Deprecated 41 | ++++++++++ 42 | 43 | Removed 44 | +++++++ 45 | 46 | Fixed 47 | +++++ 48 | * Fix incompatibility when using the combination of python < 3.12 and factory-boy < 3.3. 49 | 50 | * The error was: ``TypeError: type 'Factory' is not subscriptable`` 51 | 52 | Security 53 | ++++++++ 54 | 55 | 2.8.0 56 | ---------- 57 | 58 | Added 59 | +++++ 60 | * Declare compatibility with python 3.13. Supported versions are now: 3.9, 3.10, 3.11, 3.12, 3.13. 61 | * Test against pytest 8.4 62 | * Test against python 3.14 (beta) 63 | * Run static type checks. 64 | 65 | Changed 66 | +++++++ 67 | * Changelog format updated to follow `Keep a Changelog `_. 68 | 69 | Deprecated 70 | ++++++++++ 71 | 72 | Removed 73 | +++++++ 74 | * Drop support for python 3.8. Supported versions are now: 3.9, 3.10, 3.11, 3.12, 3.13. 75 | * Drop support for pytest < 7.0.0. 76 | 77 | Fixed 78 | +++++ 79 | * Fix compatibility with ``pytest 8.4``. 80 | * Fixed internal type annotations. 81 | 82 | Security 83 | ++++++++ 84 | 85 | 2.7.0 86 | ---------- 87 | - Declare required python version >= 3.8. (python 3.7 support was already removed in 2.6.0, the declared supported version tag was not updated though). `#215 `_ 88 | 89 | 2.6.1 90 | ---------- 91 | - Address compatibility issue with pytest 8.1. `#213 `_ 92 | 93 | 2.6.0 94 | ---------- 95 | - Drop python 3.7 support and add support for python 3.12. Supported versions are now: 3.8, 3.9, 3.10, 3.11, 3.12. `#197 `_ 96 | - Drop support for pytest < 6.2. We now support only pytest >= 6.2 (tested against pytest 7.4 at the time of writing). `#197 `_ 97 | 98 | 2.5.1 99 | ---------- 100 | - Fix PytestDeprecationWarning. `#180 `_ `#179 `_ 101 | 102 | 2.5.0 103 | ---------- 104 | - Using a generic class container like ``dict``, ``list``, ``set``, etc. will raise a warning suggesting you to wrap your model using ``named_model(...)``. Doing this will make sure that the fixture name is correctly chosen, otherwise SubFactory and RelatedFactory aren't able to determine the name of the model. See `Generic Container Classes as models `_ `#167 `_ 105 | - Fix ``Factory._after_postgeneration`` being invoked twice. `#164 `_ `#156 `_ 106 | - Stack traces caused by pytest-factoryboy are now slimmer. `#169 `_ 107 | - Check for naming conflicts between factory and model fixture name, and raise a clear error immediately. `#86 `_ 108 | 109 | 2.4.0 110 | ---------- 111 | - ``LazyFixture`` is now a Generic[T] type. 112 | - Simplify fixture generation (internal change). 113 | - Use poetry (internal change). 114 | 115 | 2.3.1 116 | ---------- 117 | - Fix AttributeError when using LazyFixture in register(...) `#159 `_ `#158 `_ 118 | 119 | 120 | 2.3.0 121 | ---------- 122 | - Add support for ``factory.PostGenerationMethodCall`` `#103 `_ `#87 `_. 123 | 124 | 125 | 2.2.1 126 | ---------- 127 | - ``@register()`` decorator now refuses kwargs after the initial specialization. This behaviour was mistakenly introduced in version 2.2.0, and it complicates the usage of the ``register`` function unnecessarily. For example, the following is not allowed anymore: 128 | 129 | .. code-block:: python 130 | 131 | # INVALID 132 | register( 133 | _name="second_author", 134 | name="C.S. Lewis", 135 | )( 136 | AuthorFactory, 137 | register_user="cs_lewis", 138 | register_user__password="Aslan1", 139 | ) 140 | 141 | # VALID 142 | register( 143 | AuthorFactory, 144 | _name="second_author", 145 | name="C.S. Lewis", 146 | register_user="cs_lewis", 147 | register_user__password="Aslan1", 148 | ) 149 | 150 | 151 | 2.2.0 152 | ---------- 153 | - Drop support for Python 3.6. We now support only python >= 3.7. 154 | - Improve "debuggability". Internal pytest-factoryboy calls are now visible when using a debugger like PDB or PyCharm. 155 | - Add type annotations. Now ``register`` and ``LazyFixture`` are type annotated. 156 | - Fix `Factory._after_postgeneration `_ method not getting the evaluated ``post_generations`` and ``RelatedFactory`` results correctly in the ``result`` param. 157 | - Factories can now be registered inside classes (even nested classes) and they won't pollute the module namespace. 158 | - Allow the ``@register`` decorator to be called with parameters: 159 | 160 | .. code-block:: python 161 | 162 | @register 163 | @register("other_author") 164 | class AuthorFactory(Factory): 165 | ... 166 | 167 | 168 | 2.1.0 169 | ----- 170 | 171 | - Add support for factory_boy >= 3.2.0 172 | - Drop support for Python 2.7, 3.4, 3.5. We now support only python >= 3.6. 173 | - Drop support for pytest < 4.6. We now support only pytest >= 4.6. 174 | - Add missing versions of python (3.9 and 3.10) and pytest (6.x.x) to the CI test matrix. 175 | 176 | 177 | 2.0.3 178 | ----- 179 | 180 | - Fix compatibility with pytest 5. 181 | 182 | 183 | 2.0.2 184 | ----- 185 | 186 | - Fix warning `use of getfuncargvalue is deprecated, use getfixturevalue` (sliverc) 187 | 188 | 189 | 2.0.1 190 | ----- 191 | 192 | Breaking change due to the heavy refactor of both pytest and factory_boy. 193 | 194 | - Failing test for using a `attributes` field on the factory (blueyed) 195 | - Minimal pytest version is 3.3.2 (olegpidsadnyi) 196 | - Minimal factory_boy version is 2.10.0 (olegpidsadnyi) 197 | 198 | 199 | 1.3.2 200 | ----- 201 | 202 | - use {posargs} in pytest command (blueyed) 203 | - pin factory_boy<2.9 (blueyed) 204 | 205 | 206 | 1.3.1 207 | ----- 208 | 209 | - fix LazyFixture evaluation order (olegpidsadnyi) 210 | 211 | 212 | 1.3.0 213 | ----- 214 | 215 | - replace request._fixturedefs by request._fixture_defs (p13773) 216 | 217 | 218 | 1.2.2 219 | ----- 220 | 221 | - fix post-generation dependencies (olegpidsadnyi) 222 | 223 | 224 | 1.2.1 225 | ----- 226 | 227 | - automatic resolution of the post-generation dependencies (olegpidsadnyi, kvas-it) 228 | 229 | 230 | 1.1.6 231 | ----- 232 | 233 | - fixes fixture function module name attribute (olegpidsadnyi) 234 | - fixes _after_postgeneration hook invocation for deferred post-generation declarations (olegpidsadnyi) 235 | 236 | 237 | 1.1.5 238 | ----- 239 | 240 | - support factory models to be passed as strings (bubenkoff) 241 | 242 | 243 | 1.1.3 244 | ----- 245 | 246 | - circular dependency determination is fixed for the post-generation (olegpidsadnyi) 247 | 248 | 249 | 1.1.2 250 | ----- 251 | 252 | - circular dependency determination is fixed for the RelatedFactory attributes (olegpidsadnyi) 253 | 254 | 255 | 1.1.1 256 | ----- 257 | 258 | - fix installation issue when django environment is not set (bubenkoff, amakhnach) 259 | 260 | 261 | 1.1.0 262 | ----- 263 | 264 | - fixture dependencies on deferred post-generation declarations (olegpidsadnyi) 265 | 266 | 267 | 1.0.3 268 | ----- 269 | 270 | - post_generation extra parameters fixed (olegpidsadnyi) 271 | - fixture partial specialization (olegpidsadnyi) 272 | - fixes readme and example (dduong42) 273 | - lazy fixtures (olegpidsadnyi) 274 | - deferred post-generation evaluation (olegpidsadnyi) 275 | - hooks (olegpidsadnyi) 276 | 277 | 278 | 1.0.2 279 | ----- 280 | 281 | - refactoring of the fixture function compilation (olegpidsadnyi) 282 | - related factory fix (olegpidsadnyi) 283 | - post_generation fixture dependency fixed (olegpidsadnyi) 284 | - model fixture registration with specific name (olegpidsadnyi) 285 | - README updated (olegpidsadnyi) 286 | 287 | 1.0.1 288 | ----- 289 | 290 | - use ``inflection`` package to convert camel case to underscore (bubenkoff) 291 | 292 | 1.0.0 293 | ----- 294 | 295 | - initial release (olegpidsadnyi) 296 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | factory_boy_ integration with the pytest_ runner 2 | ================================================ 3 | 4 | .. image:: https://img.shields.io/pypi/v/pytest-factoryboy.svg 5 | :target: https://pypi.python.org/pypi/pytest-factoryboy 6 | .. image:: https://img.shields.io/pypi/pyversions/pytest-factoryboy.svg 7 | :target: https://pypi.python.org/pypi/pytest-factoryboy 8 | .. image:: https://github.com/pytest-dev/pytest-factoryboy/actions/workflows/main.yml/badge.svg 9 | :target: https://github.com/pytest-dev/pytest-factoryboy/actions?query=workflow%3Amain 10 | .. image:: https://readthedocs.org/projects/pytest-factoryboy/badge/?version=latest 11 | :target: https://readthedocs.org/projects/pytest-factoryboy/?badge=latest 12 | :alt: Documentation Status 13 | 14 | 15 | pytest-factoryboy makes it easy to combine ``factory`` approach to the test setup with the ``dependency`` injection, 16 | heart of the `pytest fixtures`_. 17 | 18 | .. _factory_boy: https://factoryboy.readthedocs.io 19 | .. _pytest: https://pytest.org 20 | .. _pytest fixtures: https://pytest.org/latest/fixture.html 21 | .. _overridden: https://docs.pytest.org/en/latest/how-to/fixtures.html#overriding-fixtures-on-various-levels 22 | 23 | 24 | Install pytest-factoryboy 25 | ------------------------- 26 | 27 | :: 28 | 29 | pip install pytest-factoryboy 30 | 31 | 32 | Concept 33 | ------- 34 | 35 | Library exports a function to register factories as fixtures. Fixtures are contributed 36 | to the same module where register function is called. 37 | 38 | 39 | Model Fixture 40 | ------------- 41 | 42 | Model fixture implements an instance of a model created by the factory. Name convention is model's lowercase-underscore 43 | class name. 44 | 45 | 46 | .. code-block:: python 47 | 48 | import factory 49 | from pytest_factoryboy import register 50 | 51 | @register 52 | class AuthorFactory(factory.Factory): 53 | class Meta: 54 | model = Author 55 | 56 | name = "Charles Dickens" 57 | 58 | 59 | def test_model_fixture(author): 60 | assert author.name == "Charles Dickens" 61 | 62 | 63 | Attributes are Fixtures 64 | ----------------------- 65 | 66 | There are fixtures created automatically for factory attributes. Attribute names are prefixed with the model fixture name and 67 | double underscore (similar to the convention used by factory_boy). 68 | 69 | 70 | .. code-block:: python 71 | 72 | @pytest.mark.parametrize("author__name", ["Bill Gates"]) 73 | def test_model_fixture(author): 74 | assert author.name == "Bill Gates" 75 | 76 | 77 | Multiple fixtures 78 | ----------------- 79 | 80 | Model fixtures can be registered with specific names. For example, if you address instances of some collection 81 | by the name like "first", "second" or of another parent as "other": 82 | 83 | 84 | .. code-block:: python 85 | 86 | register(AuthorFactory) # author 87 | register(AuthorFactory, "second_author") # second_author 88 | 89 | 90 | @register # book 91 | @register(_name="second_book") # second_book 92 | @register(_name="other_book") # other_book, book of another author 93 | class BookFactory(factory.Factory): 94 | class Meta: 95 | model = Book 96 | 97 | 98 | @pytest.fixture 99 | def other_book__author(second_author): 100 | """Make the relation of the `other_book.author` to `second_author`.""" 101 | return second_author 102 | 103 | 104 | def test_book_authors(book, second_book, other_book, author, second_author): 105 | assert book.author == second_book.author == author 106 | assert other_book.author == second_author 107 | 108 | 109 | SubFactory 110 | ---------- 111 | 112 | Sub-factory attribute points to the model fixture of the sub-factory. 113 | Attributes of sub-factories are injected as dependencies to the model fixture and can be overridden_ via 114 | the parametrization. 115 | 116 | Related Factory 117 | --------------- 118 | 119 | Related factory attribute points to the model fixture of the related factory. 120 | Attributes of related factories are injected as dependencies to the model fixture and can be overridden_ via 121 | the parametrization. 122 | 123 | 124 | post-generation 125 | --------------- 126 | 127 | Post-generation attribute fixture implements only the extracted value for the post generation function. 128 | 129 | Factory Fixture 130 | --------------- 131 | 132 | `pytest-factoryboy` also registers factory fixtures, to allow their use without importing them. The fixture name convention is to use the lowercase-underscore form of the class name. 133 | 134 | .. code-block:: python 135 | 136 | import factory 137 | from pytest_factoryboy import register 138 | 139 | class AuthorFactory(factory.Factory): 140 | class Meta: 141 | model = Author 142 | 143 | 144 | register(AuthorFactory) # => author_factory 145 | 146 | 147 | def test_factory_fixture(author_factory): 148 | author = author_factory(name="Charles Dickens") 149 | assert author.name == "Charles Dickens" 150 | 151 | 152 | Integration 153 | ----------- 154 | 155 | An example of factory_boy_ and pytest_ integration. 156 | 157 | .. code-block:: python 158 | 159 | # tests/factories.py 160 | 161 | import factory 162 | from app import models 163 | from faker import Factory as FakerFactory 164 | 165 | faker = FakerFactory.create() 166 | 167 | 168 | class AuthorFactory(factory.django.DjangoModelFactory): 169 | class Meta: 170 | model = models.Author 171 | 172 | name = factory.LazyFunction(lambda: faker.name()) 173 | 174 | 175 | class BookFactory(factory.django.DjangoModelFactory): 176 | class Meta: 177 | model = models.Book 178 | 179 | title = factory.LazyFunction(lambda: faker.sentence(nb_words=4)) 180 | author = factory.SubFactory(AuthorFactory) 181 | 182 | 183 | .. code-block:: python 184 | 185 | # tests/conftest.py 186 | 187 | from pytest_factoryboy import register 188 | 189 | from . import factories 190 | 191 | register(factories.AuthorFactory) 192 | register(factories.BookFactory) 193 | 194 | 195 | .. code-block:: python 196 | 197 | # tests/test_models.py 198 | 199 | from app.models import Book 200 | from .factories import BookFactory 201 | 202 | 203 | def test_book_factory(book_factory): 204 | """Factories become fixtures automatically.""" 205 | assert book_factory is BookFactory 206 | 207 | 208 | def test_book(book): 209 | """Instances become fixtures automatically.""" 210 | assert isinstance(book, Book) 211 | 212 | 213 | @pytest.mark.parametrize("book__title", ["PyTest for Dummies"]) 214 | @pytest.mark.parametrize("author__name", ["Bill Gates"]) 215 | def test_parametrized(book): 216 | """You can set any factory attribute as a fixture using naming convention.""" 217 | assert book.title == "PyTest for Dummies" 218 | assert book.author.name == "Bill Gates" 219 | 220 | 221 | Fixture partial specialization 222 | ------------------------------ 223 | 224 | There is a possibility to pass keyword parameters in order to override factory attribute values during fixture 225 | registration. This comes in handy when your test case is requesting a lot of fixture flavors. Too much for the 226 | regular pytest parametrization. 227 | In this case, you can register fixture flavors in the local test module and specify value deviations inside ``register`` 228 | function calls. 229 | 230 | 231 | .. code-block:: python 232 | 233 | register(AuthorFactory, "male_author", gender="M", name="John Doe") 234 | register(AuthorFactory, "female_author", gender="F") 235 | 236 | 237 | @pytest.fixture 238 | def female_author__name(): 239 | """Override female author name as a separate fixture.""" 240 | return "Jane Doe" 241 | 242 | 243 | @pytest.mark.parametrize("male_author__age", [42]) # Override even more 244 | def test_partial(male_author, female_author): 245 | """Test fixture partial specialization.""" 246 | assert male_author.gender == "M" 247 | assert male_author.name == "John Doe" 248 | assert male_author.age == 42 249 | 250 | assert female_author.gender == "F" 251 | assert female_author.name == "Jane Doe" 252 | 253 | 254 | Fixture attributes 255 | ------------------ 256 | 257 | Sometimes it is necessary to pass an instance of another fixture as an attribute value to the factory. 258 | It is possible to override the generated attribute fixture where desired values can be requested as 259 | fixture dependencies. There is also a lazy wrapper for the fixture that can be used in the parametrization 260 | without defining fixtures in a module. 261 | 262 | 263 | LazyFixture constructor accepts either existing fixture name or callable with dependencies: 264 | 265 | .. code-block:: python 266 | 267 | import pytest 268 | from pytest_factoryboy import register, LazyFixture 269 | 270 | 271 | @pytest.mark.parametrize("book__author", [LazyFixture("another_author")]) 272 | def test_lazy_fixture_name(book, another_author): 273 | """Test that book author is replaced with another author by fixture name.""" 274 | assert book.author == another_author 275 | 276 | 277 | @pytest.mark.parametrize("book__author", [LazyFixture(lambda another_author: another_author)]) 278 | def test_lazy_fixture_callable(book, another_author): 279 | """Test that book author is replaced with another author by callable.""" 280 | assert book.author == another_author 281 | 282 | 283 | # Can also be used in the partial specialization during the registration. 284 | register(BookFactory, "another_book", author=LazyFixture("another_author")) 285 | 286 | 287 | Generic container classes as models 288 | ----------------------------------- 289 | It's often useful to create factories for ``dict`` or other common generic container classes. 290 | In that case, you should wrap the container class around ``named_model(...)``, so that pytest-factoryboy can correctly determine the model name when using it in a SubFactory or RelatedFactory. 291 | 292 | Pytest-factoryboy will otherwise raise a warning. 293 | 294 | For example: 295 | 296 | .. code-block:: python 297 | 298 | import factory 299 | from pytest_factoryboy import named_model, register 300 | 301 | @register 302 | class JSONPayload(factory.Factory): 303 | class Meta: 304 | model = named_model("JSONPayload", dict) 305 | 306 | name = "foo" 307 | 308 | 309 | def test_foo(json_payload): 310 | assert json_payload.name == "foo" 311 | 312 | As a bonus, factory is automatically registering the ``json_payload`` fixture (rather than ``dict``), so there is no need to override ``@register(_name="json_payload"))``. 313 | 314 | Post-generation dependencies 315 | ============================ 316 | 317 | Unlike factory_boy which binds related objects using an internal container to store results of lazy evaluations, 318 | pytest-factoryboy relies on the PyTest request. 319 | 320 | Circular dependencies between objects can be resolved using post-generation hooks/related factories in combination with 321 | passing the SelfAttribute, but in the case of PyTest request fixture functions have to return values in order to be cached 322 | in the request and to become available to other fixtures. 323 | 324 | That's why evaluation of the post-generation declaration in pytest-factoryboy is deferred until calling 325 | the test function. 326 | This solves circular dependency resolution for situations like: 327 | 328 | :: 329 | 330 | o->[ A ]-->[ B ]<--[ C ]-o 331 | | | 332 | o----(C depends on A)----o 333 | 334 | 335 | On the other hand, deferring the evaluation of post-generation declarations evaluation makes their result unavailable during the generation 336 | of objects that are not in the circular dependency, but they rely on the post-generation action. 337 | 338 | pytest-factoryboy is trying to detect cycles and resolve post-generation dependencies automatically. 339 | 340 | 341 | .. code-block:: python 342 | 343 | from pytest_factoryboy import register 344 | 345 | 346 | class Foo(object): 347 | def __init__(self, value): 348 | self.value = value 349 | 350 | 351 | class Bar(object): 352 | def __init__(self, foo): 353 | self.foo = foo 354 | 355 | 356 | @register 357 | class FooFactory(factory.Factory): 358 | class Meta: 359 | model = Foo 360 | 361 | value = 0 362 | 363 | @factory.post_generation 364 | def set1(foo, create, value, **kwargs): 365 | foo.value = 1 366 | 367 | @register 368 | class BarFactory(factory.Factory): 369 | class Meta: 370 | model = Bar 371 | 372 | foo = factory.SubFactory(FooFactory) 373 | 374 | @classmethod 375 | def _create(cls, model_class, foo): 376 | assert foo.value == 1 # Assert that set1 is evaluated before object generation 377 | return super(BarFactory, cls)._create(model_class, foo=foo) 378 | 379 | 380 | # Forces 'set1' to be evaluated first. 381 | def test_depends_on_set1(bar): 382 | """Test that post-generation hooks are done and the value is 2.""" 383 | assert bar.foo.value == 1 384 | 385 | 386 | Hooks 387 | ----- 388 | 389 | pytest-factoryboy exposes several `pytest hooks `_ 390 | which might be helpful for e.g. controlling database transaction, for reporting etc: 391 | 392 | * pytest_factoryboy_done(request) - Called after all factory-based fixtures and their post-generation actions have been evaluated. 393 | 394 | 395 | License 396 | ------- 397 | 398 | This software is licensed under the `MIT license `_. 399 | 400 | © 2015 Oleg Pidsadnyi, Anatoly Bubenkov and others 401 | -------------------------------------------------------------------------------- /src/pytest_factoryboy/fixture.py: -------------------------------------------------------------------------------- 1 | """Factory boy fixture integration.""" 2 | 3 | from __future__ import annotations 4 | 5 | import contextlib 6 | import functools 7 | import sys 8 | import warnings 9 | from collections.abc import Collection, Iterable, Iterator, Mapping 10 | from dataclasses import dataclass 11 | from inspect import signature 12 | from types import MethodType 13 | from typing import TYPE_CHECKING, Callable, Generic, TypeVar, cast, overload 14 | 15 | import factory 16 | import factory.enums 17 | import inflection 18 | from factory.base import Factory 19 | from factory.builder import BuildStep, DeclarationSet, StepBuilder 20 | from factory.declarations import ( 21 | NotProvided, 22 | PostGeneration, 23 | PostGenerationDeclaration, 24 | PostGenerationMethodCall, 25 | RelatedFactory, 26 | SubFactory, 27 | ) 28 | from typing_extensions import ParamSpec 29 | 30 | from .compat import PostGenerationContext 31 | from .fixturegen import create_fixture 32 | 33 | if TYPE_CHECKING: 34 | from _pytest.fixtures import SubRequest 35 | 36 | from .plugin import Request as FactoryboyRequest 37 | 38 | T = TypeVar("T") 39 | U = TypeVar("U") 40 | T_co = TypeVar("T_co", covariant=True) 41 | P = ParamSpec("P") 42 | 43 | SEPARATOR = "__" 44 | WARN_FOR_MODEL_TYPES = frozenset({dict, list, set, tuple, frozenset}) 45 | 46 | 47 | @dataclass(eq=False) 48 | class DeferredFunction(Generic[T, U]): 49 | name: str 50 | factory: type[Factory[T]] 51 | is_related: bool 52 | function: Callable[[SubRequest], U] 53 | 54 | def __call__(self, request: SubRequest) -> U: 55 | return self.function(request) 56 | 57 | 58 | class Box(Generic[T_co]): 59 | """Simple box class, used to hold a value. 60 | 61 | The main purpose of this is to hold objects that we don't want to appear in stack traces. 62 | For example, the "caller_locals" dict holding a lot of items. 63 | """ 64 | 65 | def __init__(self, value: T_co) -> None: 66 | self.value = value 67 | 68 | 69 | def named_model(model_cls: type[T], name: str) -> type[T]: 70 | """Return a model class with a given name.""" 71 | return type(name, (model_cls,), {}) 72 | 73 | 74 | # register(AuthorFactory, ...) 75 | # 76 | # @register 77 | # class AuthorFactory(Factory): ... 78 | @overload 79 | def register( 80 | factory_class: type[Factory[T]], 81 | _name: str | None = ..., 82 | *, 83 | _caller_locals: Box[dict[str, object]] | None = ..., 84 | **kwargs: object, 85 | ) -> type[Factory[T]]: ... 86 | 87 | 88 | # @register(...) 89 | # class AuthorFactory(Factory): ... 90 | @overload 91 | def register( 92 | factory_class: None, 93 | _name: str | None = ..., 94 | *, 95 | _caller_locals: Box[dict[str, object]] | None = ..., 96 | **kwargs: object, 97 | ) -> Callable[[type[Factory[T]]], type[Factory[T]]]: ... 98 | 99 | 100 | def register( 101 | factory_class: type[Factory[T]] | None = None, 102 | _name: str | None = None, 103 | *, 104 | _caller_locals: Box[dict[str, object]] | None = None, 105 | **kwargs: object, 106 | ) -> type[Factory[T]] | Callable[[type[Factory[T]]], type[Factory[T]]]: 107 | r"""Register fixtures for the factory class. 108 | 109 | :param factory_class: Factory class to register. 110 | :param _name: Name of the model fixture. By default, is lowercase-underscored model name. 111 | :param _caller_locals: Dictionary where to inject the generated fixtures. Defaults to the caller's locals(). 112 | :param \**kwargs: Optional keyword arguments that override factory attributes. 113 | """ 114 | if _caller_locals is None: 115 | _caller_locals = Box(get_caller_locals()) 116 | 117 | if factory_class is None: 118 | 119 | def register_(factory_class: type[Factory[T]]) -> type[Factory[T]]: 120 | return register(factory_class, _name=_name, _caller_locals=_caller_locals, **kwargs) 121 | 122 | return register_ 123 | 124 | assert not factory_class._meta.abstract, "Can't register abstract factories." 125 | assert factory_class._meta.model is not None, "Factory model class is not specified." 126 | 127 | factory_name = get_factory_name(factory_class) 128 | model_name = get_model_name(factory_class) if _name is None else _name 129 | 130 | assert model_name != factory_name, ( 131 | f"Naming collision for {factory_class}:\n" 132 | f" * factory fixture name: {factory_name}\n" 133 | f" * model fixture name: {model_name}\n" 134 | f"Please provide different name for model fixture." 135 | ) 136 | 137 | fixture_defs = dict( 138 | generate_fixtures( 139 | factory_class=factory_class, 140 | model_name=model_name, 141 | factory_name=factory_name, 142 | overrides=kwargs, 143 | caller_locals=_caller_locals, 144 | ) 145 | ) 146 | for name, fixture in fixture_defs.items(): 147 | inject_into_caller(name, fixture, _caller_locals) 148 | 149 | return factory_class 150 | 151 | 152 | def generate_fixtures( 153 | factory_class: type[Factory[T]], 154 | model_name: str, 155 | factory_name: str, 156 | overrides: Mapping[str, object], 157 | caller_locals: Box[Mapping[str, object]], 158 | ) -> Iterable[tuple[str, Callable[..., object]]]: 159 | """Generate all the FixtureDefs for the given factory class.""" 160 | 161 | related: list[str] = [] 162 | for attr, value in factory_class._meta.declarations.items(): 163 | value = overrides.get(attr, value) 164 | attr_name = SEPARATOR.join((model_name, attr)) 165 | yield ( 166 | attr_name, 167 | make_declaration_fixturedef( 168 | attr_name=attr_name, 169 | value=value, 170 | factory_class=factory_class, 171 | related=related, 172 | ), 173 | ) 174 | 175 | if factory_name not in caller_locals.value: 176 | yield ( 177 | factory_name, 178 | create_fixture_with_related( 179 | name=factory_name, 180 | function=functools.partial(factory_fixture, factory_class=factory_class), 181 | ), 182 | ) 183 | 184 | deps = get_deps(factory_class, model_name=model_name) 185 | yield ( 186 | model_name, 187 | create_fixture_with_related( 188 | name=model_name, 189 | function=functools.partial(model_fixture, factory_name=factory_name), 190 | dependencies=deps, 191 | related=related, 192 | ), 193 | ) 194 | 195 | 196 | def create_fixture_with_related( 197 | name: str, 198 | function: Callable[P, T], 199 | dependencies: Collection[str] | None = None, 200 | related: Collection[str] | None = None, 201 | ) -> Callable[P, T]: 202 | if related is None: 203 | related = [] 204 | fixture, fn = create_fixture(name=name, function=function, dependencies=dependencies) 205 | 206 | # We have to set the `_factoryboy_related` attribute to the original function, since 207 | # FixtureDef.func will provide that one later when we discover the related fixtures. 208 | fn._factoryboy_related = related # type: ignore[attr-defined] 209 | return fixture 210 | 211 | 212 | def make_declaration_fixturedef( 213 | attr_name: str, 214 | value: object, 215 | factory_class: type[Factory[T]], 216 | related: list[str], 217 | ) -> Callable[[SubRequest], object]: 218 | """Create the FixtureDef for a factory declaration.""" 219 | if isinstance(value, (SubFactory, RelatedFactory)): 220 | subfactory_class: type[Factory[object]] = value.get_factory() 221 | subfactory_deps = get_deps(subfactory_class, factory_class) 222 | 223 | args = list(subfactory_deps) 224 | if isinstance(value, RelatedFactory): 225 | related_model = get_model_name(subfactory_class) 226 | args.append(related_model) 227 | related.append(related_model) 228 | related.append(attr_name) 229 | related.extend(subfactory_deps) 230 | 231 | if isinstance(value, SubFactory): 232 | args.append(inflection.underscore(subfactory_class._meta.model.__name__)) 233 | 234 | return create_fixture_with_related( 235 | name=attr_name, 236 | function=functools.partial(subfactory_fixture, factory_class=subfactory_class), 237 | dependencies=args, 238 | ) 239 | 240 | deps: list[str] # makes mypy happy 241 | if isinstance(value, PostGeneration): 242 | value = None 243 | deps = [] 244 | elif isinstance(value, PostGenerationMethodCall): 245 | value = value.method_arg 246 | deps = [] 247 | elif isinstance(value, LazyFixture): 248 | value = value 249 | deps = value.args 250 | else: 251 | value = value 252 | deps = [] 253 | 254 | return create_fixture_with_related( 255 | name=attr_name, 256 | function=functools.partial(attr_fixture, value=value), 257 | dependencies=deps, 258 | ) 259 | 260 | 261 | def inject_into_caller(name: str, function: Callable[..., object], locals_: Box[dict[str, object]]) -> None: 262 | """Inject a function into the caller's locals, making sure that the function will work also within classes.""" 263 | # We need to check if the caller frame is a class, since in that case the first argument is the class itself. 264 | # In that case, we can apply the staticmethod() decorator to the injected function, so that the first param 265 | # will be disregarded. 266 | # To figure out if the caller frame is a class, we can check if the __qualname__ attribute is present. 267 | 268 | # According to the python docs, __qualname__ is available for both **classes and functions**. 269 | # However, it seems that for functions it is not yet available in the function namespace before it's defined. 270 | # This could change in the future, but it shouldn't be too much of a problem since registering a factory 271 | # in a function namespace would not make it usable anyway. 272 | # Therefore, we can just check for __qualname__ to figure out if we are in a class, and apply the @staticmethod. 273 | is_class_or_function = "__qualname__" in locals_.value 274 | if is_class_or_function: 275 | function = staticmethod(function) 276 | 277 | locals_.value[name] = function 278 | 279 | 280 | def get_model_name(factory_class: type[Factory[T]]) -> str: 281 | """Get model fixture name by factory.""" 282 | model_cls = factory_class._meta.model 283 | 284 | if isinstance(model_cls, str): 285 | return model_cls 286 | 287 | model_name = inflection.underscore(model_cls.__name__) 288 | if model_cls in WARN_FOR_MODEL_TYPES: 289 | warnings.warn( 290 | f"Using a {model_cls} as model type for {factory_class} is discouraged by pytest-factoryboy, " 291 | f"as it assumes that the model name is {model_name!r} when using it as SubFactory or RelatedFactory, " 292 | "which is too generic and probably not what you want.\n" 293 | "You can giving an explicit name to the model by using:\n" 294 | f'model = named_model({model_cls.__name__}, "Foo")', 295 | ) 296 | 297 | return model_name 298 | 299 | 300 | def get_factory_name(factory_class: type[Factory[T]]) -> str: 301 | """Get factory fixture name by factory.""" 302 | return inflection.underscore(factory_class.__name__) 303 | 304 | 305 | def get_deps( 306 | factory_class: type[Factory[T]], 307 | parent_factory_class: type[Factory[U]] | None = None, 308 | model_name: str | None = None, 309 | ) -> list[str]: 310 | """Get factory dependencies. 311 | 312 | :return: List of the fixture argument names for dependency injection. 313 | """ 314 | model_name = get_model_name(factory_class) if model_name is None else model_name 315 | parent_model_name = get_model_name(parent_factory_class) if parent_factory_class is not None else None 316 | 317 | def is_dep(value: object) -> bool: 318 | if isinstance(value, RelatedFactory): 319 | return False 320 | if isinstance(value, SubFactory): 321 | subfactory_class: type[Factory[object]] = value.get_factory() 322 | if get_model_name(subfactory_class) == parent_model_name: 323 | return False 324 | if isinstance(value, PostGenerationDeclaration): 325 | # Dependency on extracted value 326 | return True 327 | 328 | return True 329 | 330 | return [ 331 | SEPARATOR.join((model_name, attr)) for attr, value in factory_class._meta.declarations.items() if is_dep(value) 332 | ] 333 | 334 | 335 | def evaluate(request: SubRequest, value: LazyFixture[T] | T) -> T: 336 | """Evaluate the declaration (lazy fixtures, etc).""" 337 | return value.evaluate(request) if isinstance(value, LazyFixture) else value 338 | 339 | 340 | def noop(*args: object, **kwargs: object) -> None: 341 | """No-op function.""" 342 | pass 343 | 344 | 345 | @contextlib.contextmanager 346 | def disable_method(method: MethodType) -> Iterator[None]: 347 | """Disable a method.""" 348 | klass = method.__self__ 349 | method_name = method.__name__ 350 | old_method = getattr(klass, method_name) 351 | setattr(klass, method_name, noop) 352 | try: 353 | yield 354 | finally: 355 | setattr(klass, method.__name__, old_method) 356 | 357 | 358 | def model_fixture(request: SubRequest, factory_name: str) -> object: 359 | """Model fixture implementation.""" 360 | factoryboy_request: FactoryboyRequest = request.getfixturevalue("factoryboy_request") 361 | 362 | # Try to evaluate as much post-generation dependencies as possible 363 | factoryboy_request.evaluate(request) 364 | 365 | assert request.fixturename # NOTE: satisfy mypy 366 | fixture_name = request.fixturename 367 | prefix = "".join((fixture_name, SEPARATOR)) 368 | 369 | factory_class: type[Factory[object]] = request.getfixturevalue(factory_name) 370 | 371 | # create Factory override for the model fixture 372 | NewFactory: type[Factory[object]] = type("Factory", (factory_class,), {}) 373 | # equivalent to: 374 | # class Factory(factory_class): 375 | # pass 376 | # NewFactory = Factory 377 | # del Factory 378 | 379 | # it just makes mypy understand it. 380 | 381 | NewFactory._meta.base_declarations = { 382 | k: v for k, v in NewFactory._meta.base_declarations.items() if not isinstance(v, PostGenerationDeclaration) 383 | } 384 | NewFactory._meta.post_declarations = DeclarationSet() 385 | 386 | kwargs = {} 387 | for key in factory_class._meta.pre_declarations: 388 | argname = "".join((prefix, key)) 389 | if argname in request._fixturedef.argnames: 390 | kwargs[key] = evaluate(request, request.getfixturevalue(argname)) 391 | 392 | strategy = factory.enums.CREATE_STRATEGY 393 | builder = StepBuilder(NewFactory._meta, kwargs, strategy) 394 | step = BuildStep(builder=builder, sequence=NewFactory._meta.next_sequence()) 395 | 396 | # FactoryBoy invokes the `_after_postgeneration` method, but we will instead call it manually later, 397 | # once we are able to evaluate all the related fixtures. 398 | with disable_method(NewFactory._after_postgeneration): # type: ignore[arg-type] # https://github.com/python/mypy/issues/14235 399 | instance = NewFactory(**kwargs) 400 | 401 | # Cache the instance value on pytest level so that the fixture can be resolved before the return 402 | request._fixturedef.cached_result = (instance, 0, None) 403 | request._fixture_defs[fixture_name] = request._fixturedef 404 | 405 | # Defer post-generation declarations 406 | deferred: list[DeferredFunction[object, object]] = [] 407 | 408 | for attr in factory_class._meta.post_declarations.sorted(): 409 | decl = factory_class._meta.post_declarations.declarations[attr] 410 | 411 | if isinstance(decl, RelatedFactory): 412 | deferred.append(make_deferred_related(factory_class, fixture_name, attr)) 413 | else: 414 | argname = "".join((prefix, attr)) 415 | extra = {} 416 | for k, v in factory_class._meta.post_declarations.contexts[attr].items(): 417 | if k == "": 418 | continue 419 | post_attr = SEPARATOR.join((argname, k)) 420 | 421 | if post_attr in request._fixturedef.argnames: 422 | extra[k] = evaluate(request, request.getfixturevalue(post_attr)) 423 | else: 424 | extra[k] = v 425 | # Handle special case for ``PostGenerationMethodCall`` where 426 | # `attr_fixture` value is equal to ``NotProvided``, which mean 427 | # that `value_provided` should be falsy 428 | postgen_value = evaluate(request, request.getfixturevalue(argname)) 429 | postgen_context = PostGenerationContext( 430 | value_provided=(postgen_value is not NotProvided), 431 | value=postgen_value, 432 | extra=extra, 433 | ) 434 | deferred.append( 435 | make_deferred_postgen(step, factory_class, fixture_name, instance, attr, decl, postgen_context) 436 | ) 437 | factoryboy_request.defer(deferred) 438 | 439 | # Try to evaluate as much post-generation dependencies as possible. 440 | # This will finally invoke Factory._after_postgeneration, which was previously disabled 441 | factoryboy_request.evaluate(request) 442 | return instance 443 | 444 | 445 | def make_deferred_related(factory: type[Factory[T]], fixture: str, attr: str) -> DeferredFunction[T, object]: 446 | """Make deferred function for the related factory declaration. 447 | 448 | :param factory: Factory class. 449 | :param fixture: Object fixture name e.g. "book". 450 | :param attr: Declaration attribute name e.g. "publications". 451 | 452 | :note: Deferred function name results in "book__publication". 453 | """ 454 | name = SEPARATOR.join((fixture, attr)) 455 | 456 | def deferred_impl(request: SubRequest) -> object: 457 | return request.getfixturevalue(name) 458 | 459 | return DeferredFunction( 460 | name=name, 461 | factory=factory, 462 | is_related=True, 463 | function=deferred_impl, 464 | ) 465 | 466 | 467 | def make_deferred_postgen( 468 | step: BuildStep, 469 | factory_class: type[Factory[T]], 470 | fixture: str, 471 | instance: T, 472 | attr: str, 473 | declaration: PostGenerationDeclaration, 474 | context: PostGenerationContext, 475 | ) -> DeferredFunction[T, object]: 476 | """Make deferred function for the post-generation declaration. 477 | 478 | :param step: factory_boy builder step. 479 | :param factory_class: Factory class. 480 | :param fixture: Object fixture name e.g. "author". 481 | :param instance: Parent object instance. 482 | :param attr: Declaration attribute name e.g. "register_user". 483 | :param declaration: Post-generation declaration. 484 | :param context: Post-generation declaration context. 485 | 486 | :note: Deferred function name results in "author__register_user". 487 | """ 488 | name = SEPARATOR.join((fixture, attr)) 489 | 490 | def deferred_impl(request: SubRequest) -> object: 491 | return declaration.call(instance, step, context) 492 | 493 | return DeferredFunction( 494 | name=name, 495 | factory=factory_class, 496 | is_related=False, 497 | function=deferred_impl, 498 | ) 499 | 500 | 501 | def factory_fixture(request: SubRequest, factory_class: type[Factory[T]]) -> type[Factory[T]]: 502 | """Factory fixture implementation.""" 503 | return factory_class 504 | 505 | 506 | def attr_fixture(request: SubRequest, value: T) -> T: 507 | """Attribute fixture implementation.""" 508 | return value 509 | 510 | 511 | def subfactory_fixture(request: SubRequest, factory_class: type[Factory[object]]) -> object: 512 | """SubFactory/RelatedFactory fixture implementation.""" 513 | fixture = inflection.underscore(factory_class._meta.model.__name__) 514 | return request.getfixturevalue(fixture) 515 | 516 | 517 | def get_caller_locals(depth: int = 0) -> dict[str, object]: 518 | """Get the local namespace of the caller frame.""" 519 | return sys._getframe(depth + 2).f_locals 520 | 521 | 522 | class LazyFixture(Generic[T]): 523 | """Lazy fixture.""" 524 | 525 | def __init__(self, fixture: Callable[..., T] | str) -> None: 526 | """Lazy pytest fixture wrapper. 527 | 528 | :param fixture: Fixture name or callable with dependencies. 529 | """ 530 | self.fixture = fixture 531 | if callable(self.fixture): 532 | params = signature(self.fixture).parameters.values() 533 | self.args = [param.name for param in params if param.kind == param.POSITIONAL_OR_KEYWORD] 534 | else: 535 | self.args = [self.fixture] 536 | 537 | def evaluate(self, request: SubRequest) -> T: 538 | """Evaluate the lazy fixture. 539 | 540 | :param request: pytest request object. 541 | :return: evaluated fixture. 542 | """ 543 | if callable(self.fixture): 544 | kwargs = {arg: request.getfixturevalue(arg) for arg in self.args} 545 | return self.fixture(**kwargs) 546 | else: 547 | return cast(T, request.getfixturevalue(self.fixture)) 548 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "cachetools" 5 | version = "6.1.0" 6 | description = "Extensible memoizing collections and decorators" 7 | optional = false 8 | python-versions = ">=3.9" 9 | groups = ["dev"] 10 | files = [ 11 | {file = "cachetools-6.1.0-py3-none-any.whl", hash = "sha256:1c7bb3cf9193deaf3508b7c5f2a79986c13ea38965c5adcff1f84519cf39163e"}, 12 | {file = "cachetools-6.1.0.tar.gz", hash = "sha256:b4c4f404392848db3ce7aac34950d17be4d864da4b8b66911008e430bc544587"}, 13 | ] 14 | 15 | [[package]] 16 | name = "chardet" 17 | version = "5.2.0" 18 | description = "Universal encoding detector for Python 3" 19 | optional = false 20 | python-versions = ">=3.7" 21 | groups = ["dev"] 22 | files = [ 23 | {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, 24 | {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, 25 | ] 26 | 27 | [[package]] 28 | name = "colorama" 29 | version = "0.4.6" 30 | description = "Cross-platform colored terminal text." 31 | optional = false 32 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 33 | groups = ["main", "dev"] 34 | files = [ 35 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 36 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 37 | ] 38 | markers = {main = "sys_platform == \"win32\""} 39 | 40 | [[package]] 41 | name = "coverage" 42 | version = "7.9.1" 43 | description = "Code coverage measurement for Python" 44 | optional = false 45 | python-versions = ">=3.9" 46 | groups = ["dev"] 47 | files = [ 48 | {file = "coverage-7.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc94d7c5e8423920787c33d811c0be67b7be83c705f001f7180c7b186dcf10ca"}, 49 | {file = "coverage-7.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16aa0830d0c08a2c40c264cef801db8bc4fc0e1892782e45bcacbd5889270509"}, 50 | {file = "coverage-7.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf95981b126f23db63e9dbe4cf65bd71f9a6305696fa5e2262693bc4e2183f5b"}, 51 | {file = "coverage-7.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f05031cf21699785cd47cb7485f67df619e7bcdae38e0fde40d23d3d0210d3c3"}, 52 | {file = "coverage-7.9.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb4fbcab8764dc072cb651a4bcda4d11fb5658a1d8d68842a862a6610bd8cfa3"}, 53 | {file = "coverage-7.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0f16649a7330ec307942ed27d06ee7e7a38417144620bb3d6e9a18ded8a2d3e5"}, 54 | {file = "coverage-7.9.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cea0a27a89e6432705fffc178064503508e3c0184b4f061700e771a09de58187"}, 55 | {file = "coverage-7.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e980b53a959fa53b6f05343afbd1e6f44a23ed6c23c4b4c56c6662bbb40c82ce"}, 56 | {file = "coverage-7.9.1-cp310-cp310-win32.whl", hash = "sha256:70760b4c5560be6ca70d11f8988ee6542b003f982b32f83d5ac0b72476607b70"}, 57 | {file = "coverage-7.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a66e8f628b71f78c0e0342003d53b53101ba4e00ea8dabb799d9dba0abbbcebe"}, 58 | {file = "coverage-7.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:95c765060e65c692da2d2f51a9499c5e9f5cf5453aeaf1420e3fc847cc060582"}, 59 | {file = "coverage-7.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba383dc6afd5ec5b7a0d0c23d38895db0e15bcba7fb0fa8901f245267ac30d86"}, 60 | {file = "coverage-7.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37ae0383f13cbdcf1e5e7014489b0d71cc0106458878ccde52e8a12ced4298ed"}, 61 | {file = "coverage-7.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69aa417a030bf11ec46149636314c24c8d60fadb12fc0ee8f10fda0d918c879d"}, 62 | {file = "coverage-7.9.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a4be2a28656afe279b34d4f91c3e26eccf2f85500d4a4ff0b1f8b54bf807338"}, 63 | {file = "coverage-7.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:382e7ddd5289f140259b610e5f5c58f713d025cb2f66d0eb17e68d0a94278875"}, 64 | {file = "coverage-7.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e5532482344186c543c37bfad0ee6069e8ae4fc38d073b8bc836fc8f03c9e250"}, 65 | {file = "coverage-7.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a39d18b3f50cc121d0ce3838d32d58bd1d15dab89c910358ebefc3665712256c"}, 66 | {file = "coverage-7.9.1-cp311-cp311-win32.whl", hash = "sha256:dd24bd8d77c98557880def750782df77ab2b6885a18483dc8588792247174b32"}, 67 | {file = "coverage-7.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:6b55ad10a35a21b8015eabddc9ba31eb590f54adc9cd39bcf09ff5349fd52125"}, 68 | {file = "coverage-7.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ad935f0016be24c0e97fc8c40c465f9c4b85cbbe6eac48934c0dc4d2568321e"}, 69 | {file = "coverage-7.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8de12b4b87c20de895f10567639c0797b621b22897b0af3ce4b4e204a743626"}, 70 | {file = "coverage-7.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5add197315a054e92cee1b5f686a2bcba60c4c3e66ee3de77ace6c867bdee7cb"}, 71 | {file = "coverage-7.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600a1d4106fe66f41e5d0136dfbc68fe7200a5cbe85610ddf094f8f22e1b0300"}, 72 | {file = "coverage-7.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a876e4c3e5a2a1715a6608906aa5a2e0475b9c0f68343c2ada98110512ab1d8"}, 73 | {file = "coverage-7.9.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81f34346dd63010453922c8e628a52ea2d2ccd73cb2487f7700ac531b247c8a5"}, 74 | {file = "coverage-7.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:888f8eee13f2377ce86d44f338968eedec3291876b0b8a7289247ba52cb984cd"}, 75 | {file = "coverage-7.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9969ef1e69b8c8e1e70d591f91bbc37fc9a3621e447525d1602801a24ceda898"}, 76 | {file = "coverage-7.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60c458224331ee3f1a5b472773e4a085cc27a86a0b48205409d364272d67140d"}, 77 | {file = "coverage-7.9.1-cp312-cp312-win32.whl", hash = "sha256:5f646a99a8c2b3ff4c6a6e081f78fad0dde275cd59f8f49dc4eab2e394332e74"}, 78 | {file = "coverage-7.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:30f445f85c353090b83e552dcbbdad3ec84c7967e108c3ae54556ca69955563e"}, 79 | {file = "coverage-7.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:af41da5dca398d3474129c58cb2b106a5d93bbb196be0d307ac82311ca234342"}, 80 | {file = "coverage-7.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:31324f18d5969feef7344a932c32428a2d1a3e50b15a6404e97cba1cc9b2c631"}, 81 | {file = "coverage-7.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c804506d624e8a20fb3108764c52e0eef664e29d21692afa375e0dd98dc384f"}, 82 | {file = "coverage-7.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef64c27bc40189f36fcc50c3fb8f16ccda73b6a0b80d9bd6e6ce4cffcd810bbd"}, 83 | {file = "coverage-7.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4fe2348cc6ec372e25adec0219ee2334a68d2f5222e0cba9c0d613394e12d86"}, 84 | {file = "coverage-7.9.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34ed2186fe52fcc24d4561041979a0dec69adae7bce2ae8d1c49eace13e55c43"}, 85 | {file = "coverage-7.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25308bd3d00d5eedd5ae7d4357161f4df743e3c0240fa773ee1b0f75e6c7c0f1"}, 86 | {file = "coverage-7.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73e9439310f65d55a5a1e0564b48e34f5369bee943d72c88378f2d576f5a5751"}, 87 | {file = "coverage-7.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37ab6be0859141b53aa89412a82454b482c81cf750de4f29223d52268a86de67"}, 88 | {file = "coverage-7.9.1-cp313-cp313-win32.whl", hash = "sha256:64bdd969456e2d02a8b08aa047a92d269c7ac1f47e0c977675d550c9a0863643"}, 89 | {file = "coverage-7.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:be9e3f68ca9edb897c2184ad0eee815c635565dbe7a0e7e814dc1f7cbab92c0a"}, 90 | {file = "coverage-7.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:1c503289ffef1d5105d91bbb4d62cbe4b14bec4d13ca225f9c73cde9bb46207d"}, 91 | {file = "coverage-7.9.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0b3496922cb5f4215bf5caaef4cf12364a26b0be82e9ed6d050f3352cf2d7ef0"}, 92 | {file = "coverage-7.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9565c3ab1c93310569ec0d86b017f128f027cab0b622b7af288696d7ed43a16d"}, 93 | {file = "coverage-7.9.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2241ad5dbf79ae1d9c08fe52b36d03ca122fb9ac6bca0f34439e99f8327ac89f"}, 94 | {file = "coverage-7.9.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bb5838701ca68b10ebc0937dbd0eb81974bac54447c55cd58dea5bca8451029"}, 95 | {file = "coverage-7.9.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a25f814591a8c0c5372c11ac8967f669b97444c47fd794926e175c4047ece"}, 96 | {file = "coverage-7.9.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2d04b16a6062516df97969f1ae7efd0de9c31eb6ebdceaa0d213b21c0ca1a683"}, 97 | {file = "coverage-7.9.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7931b9e249edefb07cd6ae10c702788546341d5fe44db5b6108a25da4dca513f"}, 98 | {file = "coverage-7.9.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52e92b01041151bf607ee858e5a56c62d4b70f4dac85b8c8cb7fb8a351ab2c10"}, 99 | {file = "coverage-7.9.1-cp313-cp313t-win32.whl", hash = "sha256:684e2110ed84fd1ca5f40e89aa44adf1729dc85444004111aa01866507adf363"}, 100 | {file = "coverage-7.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:437c576979e4db840539674e68c84b3cda82bc824dd138d56bead1435f1cb5d7"}, 101 | {file = "coverage-7.9.1-cp313-cp313t-win_arm64.whl", hash = "sha256:18a0912944d70aaf5f399e350445738a1a20b50fbea788f640751c2ed9208b6c"}, 102 | {file = "coverage-7.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f424507f57878e424d9a95dc4ead3fbdd72fd201e404e861e465f28ea469951"}, 103 | {file = "coverage-7.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:535fde4001b2783ac80865d90e7cc7798b6b126f4cd8a8c54acfe76804e54e58"}, 104 | {file = "coverage-7.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02532fd3290bb8fa6bec876520842428e2a6ed6c27014eca81b031c2d30e3f71"}, 105 | {file = "coverage-7.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56f5eb308b17bca3bbff810f55ee26d51926d9f89ba92707ee41d3c061257e55"}, 106 | {file = "coverage-7.9.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfa447506c1a52271f1b0de3f42ea0fa14676052549095e378d5bff1c505ff7b"}, 107 | {file = "coverage-7.9.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9ca8e220006966b4a7b68e8984a6aee645a0384b0769e829ba60281fe61ec4f7"}, 108 | {file = "coverage-7.9.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:49f1d0788ba5b7ba65933f3a18864117c6506619f5ca80326b478f72acf3f385"}, 109 | {file = "coverage-7.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:68cd53aec6f45b8e4724c0950ce86eacb775c6be01ce6e3669fe4f3a21e768ed"}, 110 | {file = "coverage-7.9.1-cp39-cp39-win32.whl", hash = "sha256:95335095b6c7b1cc14c3f3f17d5452ce677e8490d101698562b2ffcacc304c8d"}, 111 | {file = "coverage-7.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:e1b5191d1648acc439b24721caab2fd0c86679d8549ed2c84d5a7ec1bedcc244"}, 112 | {file = "coverage-7.9.1-pp39.pp310.pp311-none-any.whl", hash = "sha256:db0f04118d1db74db6c9e1cb1898532c7dcc220f1d2718f058601f7c3f499514"}, 113 | {file = "coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c"}, 114 | {file = "coverage-7.9.1.tar.gz", hash = "sha256:6cf43c78c4282708a28e466316935ec7489a9c487518a77fa68f716c67909cec"}, 115 | ] 116 | 117 | [package.dependencies] 118 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 119 | 120 | [package.extras] 121 | toml = ["tomli ; python_full_version <= \"3.11.0a6\""] 122 | 123 | [[package]] 124 | name = "distlib" 125 | version = "0.3.9" 126 | description = "Distribution utilities" 127 | optional = false 128 | python-versions = "*" 129 | groups = ["dev"] 130 | files = [ 131 | {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, 132 | {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, 133 | ] 134 | 135 | [[package]] 136 | name = "exceptiongroup" 137 | version = "1.3.0" 138 | description = "Backport of PEP 654 (exception groups)" 139 | optional = false 140 | python-versions = ">=3.7" 141 | groups = ["main"] 142 | markers = "python_version < \"3.11\"" 143 | files = [ 144 | {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, 145 | {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, 146 | ] 147 | 148 | [package.dependencies] 149 | typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} 150 | 151 | [package.extras] 152 | test = ["pytest (>=6)"] 153 | 154 | [[package]] 155 | name = "factory-boy" 156 | version = "3.3.3" 157 | description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." 158 | optional = false 159 | python-versions = ">=3.8" 160 | groups = ["main"] 161 | files = [ 162 | {file = "factory_boy-3.3.3-py2.py3-none-any.whl", hash = "sha256:1c39e3289f7e667c4285433f305f8d506efc2fe9c73aaea4151ebd5cdea394fc"}, 163 | {file = "factory_boy-3.3.3.tar.gz", hash = "sha256:866862d226128dfac7f2b4160287e899daf54f2612778327dd03d0e2cb1e3d03"}, 164 | ] 165 | 166 | [package.dependencies] 167 | Faker = ">=0.7.0" 168 | 169 | [package.extras] 170 | dev = ["Django", "Pillow", "SQLAlchemy", "coverage", "flake8", "isort", "mongoengine", "mongomock", "mypy", "tox", "wheel (>=0.32.0)", "zest.releaser[recommended]"] 171 | doc = ["Sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] 172 | 173 | [[package]] 174 | name = "faker" 175 | version = "37.4.0" 176 | description = "Faker is a Python package that generates fake data for you." 177 | optional = false 178 | python-versions = ">=3.9" 179 | groups = ["main"] 180 | files = [ 181 | {file = "faker-37.4.0-py3-none-any.whl", hash = "sha256:cb81c09ebe06c32a10971d1bbdb264bb0e22b59af59548f011ac4809556ce533"}, 182 | {file = "faker-37.4.0.tar.gz", hash = "sha256:7f69d579588c23d5ce671f3fa872654ede0e67047820255f43a4aa1925b89780"}, 183 | ] 184 | 185 | [package.dependencies] 186 | tzdata = "*" 187 | 188 | [[package]] 189 | name = "filelock" 190 | version = "3.18.0" 191 | description = "A platform independent file lock." 192 | optional = false 193 | python-versions = ">=3.9" 194 | groups = ["dev"] 195 | files = [ 196 | {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, 197 | {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, 198 | ] 199 | 200 | [package.extras] 201 | docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] 202 | testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] 203 | typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] 204 | 205 | [[package]] 206 | name = "inflection" 207 | version = "0.5.1" 208 | description = "A port of Ruby on Rails inflector to Python" 209 | optional = false 210 | python-versions = ">=3.5" 211 | groups = ["main"] 212 | files = [ 213 | {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, 214 | {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, 215 | ] 216 | 217 | [[package]] 218 | name = "iniconfig" 219 | version = "2.1.0" 220 | description = "brain-dead simple config-ini parsing" 221 | optional = false 222 | python-versions = ">=3.8" 223 | groups = ["main"] 224 | files = [ 225 | {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, 226 | {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, 227 | ] 228 | 229 | [[package]] 230 | name = "mypy" 231 | version = "1.16.1" 232 | description = "Optional static typing for Python" 233 | optional = false 234 | python-versions = ">=3.9" 235 | groups = ["dev"] 236 | files = [ 237 | {file = "mypy-1.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4f0fed1022a63c6fec38f28b7fc77fca47fd490445c69d0a66266c59dd0b88a"}, 238 | {file = "mypy-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86042bbf9f5a05ea000d3203cf87aa9d0ccf9a01f73f71c58979eb9249f46d72"}, 239 | {file = "mypy-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea7469ee5902c95542bea7ee545f7006508c65c8c54b06dc2c92676ce526f3ea"}, 240 | {file = "mypy-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352025753ef6a83cb9e7f2427319bb7875d1fdda8439d1e23de12ab164179574"}, 241 | {file = "mypy-1.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff9fa5b16e4c1364eb89a4d16bcda9987f05d39604e1e6c35378a2987c1aac2d"}, 242 | {file = "mypy-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:1256688e284632382f8f3b9e2123df7d279f603c561f099758e66dd6ed4e8bd6"}, 243 | {file = "mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc"}, 244 | {file = "mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782"}, 245 | {file = "mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507"}, 246 | {file = "mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca"}, 247 | {file = "mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4"}, 248 | {file = "mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6"}, 249 | {file = "mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d"}, 250 | {file = "mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9"}, 251 | {file = "mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79"}, 252 | {file = "mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15"}, 253 | {file = "mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd"}, 254 | {file = "mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b"}, 255 | {file = "mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438"}, 256 | {file = "mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536"}, 257 | {file = "mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f"}, 258 | {file = "mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359"}, 259 | {file = "mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be"}, 260 | {file = "mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee"}, 261 | {file = "mypy-1.16.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7fc688329af6a287567f45cc1cefb9db662defeb14625213a5b7da6e692e2069"}, 262 | {file = "mypy-1.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e198ab3f55924c03ead626ff424cad1732d0d391478dfbf7bb97b34602395da"}, 263 | {file = "mypy-1.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09aa4f91ada245f0a45dbc47e548fd94e0dd5a8433e0114917dc3b526912a30c"}, 264 | {file = "mypy-1.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13c7cd5b1cb2909aa318a90fd1b7e31f17c50b242953e7dd58345b2a814f6383"}, 265 | {file = "mypy-1.16.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:58e07fb958bc5d752a280da0e890c538f1515b79a65757bbdc54252ba82e0b40"}, 266 | {file = "mypy-1.16.1-cp39-cp39-win_amd64.whl", hash = "sha256:f895078594d918f93337a505f8add9bd654d1a24962b4c6ed9390e12531eb31b"}, 267 | {file = "mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37"}, 268 | {file = "mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab"}, 269 | ] 270 | 271 | [package.dependencies] 272 | mypy_extensions = ">=1.0.0" 273 | pathspec = ">=0.9.0" 274 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 275 | typing_extensions = ">=4.6.0" 276 | 277 | [package.extras] 278 | dmypy = ["psutil (>=4.0)"] 279 | faster-cache = ["orjson"] 280 | install-types = ["pip"] 281 | mypyc = ["setuptools (>=50)"] 282 | reports = ["lxml"] 283 | 284 | [[package]] 285 | name = "mypy-extensions" 286 | version = "1.1.0" 287 | description = "Type system extensions for programs checked with the mypy type checker." 288 | optional = false 289 | python-versions = ">=3.8" 290 | groups = ["dev"] 291 | files = [ 292 | {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, 293 | {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, 294 | ] 295 | 296 | [[package]] 297 | name = "packaging" 298 | version = "25.0" 299 | description = "Core utilities for Python packages" 300 | optional = false 301 | python-versions = ">=3.8" 302 | groups = ["main", "dev"] 303 | files = [ 304 | {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, 305 | {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, 306 | ] 307 | 308 | [[package]] 309 | name = "pathspec" 310 | version = "0.12.1" 311 | description = "Utility library for gitignore style pattern matching of file paths." 312 | optional = false 313 | python-versions = ">=3.8" 314 | groups = ["dev"] 315 | files = [ 316 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 317 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 318 | ] 319 | 320 | [[package]] 321 | name = "platformdirs" 322 | version = "4.3.8" 323 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 324 | optional = false 325 | python-versions = ">=3.9" 326 | groups = ["dev"] 327 | files = [ 328 | {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, 329 | {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, 330 | ] 331 | 332 | [package.extras] 333 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] 334 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] 335 | type = ["mypy (>=1.14.1)"] 336 | 337 | [[package]] 338 | name = "pluggy" 339 | version = "1.6.0" 340 | description = "plugin and hook calling mechanisms for python" 341 | optional = false 342 | python-versions = ">=3.9" 343 | groups = ["main", "dev"] 344 | files = [ 345 | {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, 346 | {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, 347 | ] 348 | 349 | [package.extras] 350 | dev = ["pre-commit", "tox"] 351 | testing = ["coverage", "pytest", "pytest-benchmark"] 352 | 353 | [[package]] 354 | name = "pygments" 355 | version = "2.19.2" 356 | description = "Pygments is a syntax highlighting package written in Python." 357 | optional = false 358 | python-versions = ">=3.8" 359 | groups = ["main"] 360 | files = [ 361 | {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, 362 | {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, 363 | ] 364 | 365 | [package.extras] 366 | windows-terminal = ["colorama (>=0.4.6)"] 367 | 368 | [[package]] 369 | name = "pyproject-api" 370 | version = "1.9.1" 371 | description = "API to interact with the python pyproject.toml based projects" 372 | optional = false 373 | python-versions = ">=3.9" 374 | groups = ["dev"] 375 | files = [ 376 | {file = "pyproject_api-1.9.1-py3-none-any.whl", hash = "sha256:7d6238d92f8962773dd75b5f0c4a6a27cce092a14b623b811dba656f3b628948"}, 377 | {file = "pyproject_api-1.9.1.tar.gz", hash = "sha256:43c9918f49daab37e302038fc1aed54a8c7a91a9fa935d00b9a485f37e0f5335"}, 378 | ] 379 | 380 | [package.dependencies] 381 | packaging = ">=25" 382 | tomli = {version = ">=2.2.1", markers = "python_version < \"3.11\""} 383 | 384 | [package.extras] 385 | docs = ["furo (>=2024.8.6)", "sphinx-autodoc-typehints (>=3.2)"] 386 | testing = ["covdefaults (>=2.3)", "pytest (>=8.3.5)", "pytest-cov (>=6.1.1)", "pytest-mock (>=3.14)", "setuptools (>=80.3.1)"] 387 | 388 | [[package]] 389 | name = "pytest" 390 | version = "8.4.1" 391 | description = "pytest: simple powerful testing with Python" 392 | optional = false 393 | python-versions = ">=3.9" 394 | groups = ["main"] 395 | files = [ 396 | {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, 397 | {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, 398 | ] 399 | 400 | [package.dependencies] 401 | colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} 402 | exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} 403 | iniconfig = ">=1" 404 | packaging = ">=20" 405 | pluggy = ">=1.5,<2" 406 | pygments = ">=2.7.2" 407 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 408 | 409 | [package.extras] 410 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] 411 | 412 | [[package]] 413 | name = "tomli" 414 | version = "2.2.1" 415 | description = "A lil' TOML parser" 416 | optional = false 417 | python-versions = ">=3.8" 418 | groups = ["main", "dev"] 419 | files = [ 420 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 421 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 422 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 423 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 424 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 425 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 426 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 427 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 428 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 429 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 430 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 431 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 432 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 433 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 434 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 435 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 436 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 437 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 438 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 439 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 440 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 441 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 442 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 443 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 444 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 445 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 446 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 447 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 448 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 449 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 450 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 451 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 452 | ] 453 | markers = {main = "python_version < \"3.11\"", dev = "python_full_version <= \"3.11.0a6\""} 454 | 455 | [[package]] 456 | name = "tox" 457 | version = "4.27.0" 458 | description = "tox is a generic virtualenv management and test command line tool" 459 | optional = false 460 | python-versions = ">=3.9" 461 | groups = ["dev"] 462 | files = [ 463 | {file = "tox-4.27.0-py3-none-any.whl", hash = "sha256:2b8a7fb986b82aa2c830c0615082a490d134e0626dbc9189986da46a313c4f20"}, 464 | {file = "tox-4.27.0.tar.gz", hash = "sha256:b97d5ecc0c0d5755bcc5348387fef793e1bfa68eb33746412f4c60881d7f5f57"}, 465 | ] 466 | 467 | [package.dependencies] 468 | cachetools = ">=5.5.1" 469 | chardet = ">=5.2" 470 | colorama = ">=0.4.6" 471 | filelock = ">=3.16.1" 472 | packaging = ">=24.2" 473 | platformdirs = ">=4.3.6" 474 | pluggy = ">=1.5" 475 | pyproject-api = ">=1.8" 476 | tomli = {version = ">=2.2.1", markers = "python_version < \"3.11\""} 477 | typing-extensions = {version = ">=4.12.2", markers = "python_version < \"3.11\""} 478 | virtualenv = ">=20.31" 479 | 480 | [package.extras] 481 | test = ["devpi-process (>=1.0.2)", "pytest (>=8.3.4)", "pytest-mock (>=3.14)"] 482 | 483 | [[package]] 484 | name = "typing-extensions" 485 | version = "4.14.0" 486 | description = "Backported and Experimental Type Hints for Python 3.9+" 487 | optional = false 488 | python-versions = ">=3.9" 489 | groups = ["main", "dev"] 490 | files = [ 491 | {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"}, 492 | {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, 493 | ] 494 | 495 | [[package]] 496 | name = "tzdata" 497 | version = "2025.2" 498 | description = "Provider of IANA time zone data" 499 | optional = false 500 | python-versions = ">=2" 501 | groups = ["main"] 502 | files = [ 503 | {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, 504 | {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, 505 | ] 506 | 507 | [[package]] 508 | name = "virtualenv" 509 | version = "20.31.2" 510 | description = "Virtual Python Environment builder" 511 | optional = false 512 | python-versions = ">=3.8" 513 | groups = ["dev"] 514 | files = [ 515 | {file = "virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11"}, 516 | {file = "virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af"}, 517 | ] 518 | 519 | [package.dependencies] 520 | distlib = ">=0.3.7,<1" 521 | filelock = ">=3.12.2,<4" 522 | platformdirs = ">=3.9.1,<5" 523 | 524 | [package.extras] 525 | 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)"] 526 | 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) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] 527 | 528 | [metadata] 529 | lock-version = "2.1" 530 | python-versions = ">=3.9" 531 | content-hash = "f834a2f271a22edfd03aef14972d8297b1027e0c6b38a374cbae287c719c5e09" 532 | --------------------------------------------------------------------------------