From 9c1d7e0811119b33f814aa0badd69320ed62abf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Skar=C5=BCy=C5=84ski?= Date: Mon, 2 May 2022 14:08:26 +0200 Subject: [PATCH 1/9] ci: introduce isort to the project --- .pre-commit-config.yaml | 4 ++++ docs/conf.py | 2 +- pyproject.toml | 3 +++ pytest_factoryboy/__init__.py | 2 +- pytest_factoryboy/codegen.py | 2 +- pytest_factoryboy/compat.py | 3 ++- pytest_factoryboy/fixture.py | 10 +++++----- pytest_factoryboy/plugin.py | 9 +++++---- tests/test_circular.py | 2 +- tests/test_factory_fixtures.py | 7 ++++--- tests/test_lazy_fixture.py | 2 +- tests/test_postgen_dependencies.py | 3 ++- tests/test_validation.py | 1 + 13 files changed, 31 insertions(+), 19 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3bcac8d..6a9a0a9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,6 +12,10 @@ repos: - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files +- repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort - repo: https://github.com/asottile/pyupgrade rev: v2.32.0 hooks: diff --git a/docs/conf.py b/docs/conf.py index d3cb4d6..aeb8178 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,8 +15,8 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -import sys import os +import sys sys.path.insert(0, os.path.abspath("..")) diff --git a/pyproject.toml b/pyproject.toml index b09368e..0bdc783 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,6 @@ [tool.black] line-length = 120 target-version = ['py37', 'py38', 'py39', 'py310', 'py310'] + +[tool.isort] +profile = 'black' diff --git a/pytest_factoryboy/__init__.py b/pytest_factoryboy/__init__.py index 832401d..b47e5c4 100644 --- a/pytest_factoryboy/__init__.py +++ b/pytest_factoryboy/__init__.py @@ -1,5 +1,5 @@ """pytest-factoryboy public API.""" -from .fixture import register, LazyFixture +from .fixture import LazyFixture, register __version__ = "2.1.0" diff --git a/pytest_factoryboy/codegen.py b/pytest_factoryboy/codegen.py index c5fee73..6b5db05 100644 --- a/pytest_factoryboy/codegen.py +++ b/pytest_factoryboy/codegen.py @@ -8,7 +8,7 @@ import shutil import tempfile import typing -from dataclasses import field, dataclass +from dataclasses import dataclass, field from functools import lru_cache from types import ModuleType diff --git a/pytest_factoryboy/compat.py b/pytest_factoryboy/compat.py index 4014532..9389d5d 100644 --- a/pytest_factoryboy/compat.py +++ b/pytest_factoryboy/compat.py @@ -1,6 +1,7 @@ from __future__ import annotations -import sys + import pathlib +import sys try: from factory.declarations import PostGenerationContext diff --git a/pytest_factoryboy/fixture.py b/pytest_factoryboy/fixture.py index 0d23660..db39f02 100644 --- a/pytest_factoryboy/fixture.py +++ b/pytest_factoryboy/fixture.py @@ -4,6 +4,7 @@ import sys from dataclasses import dataclass from inspect import getmodule, signature +from typing import TYPE_CHECKING import factory import factory.builder @@ -11,17 +12,16 @@ import factory.enums import inflection -from .codegen import make_fixture_model_module, FixtureDef +from .codegen import FixtureDef, make_fixture_model_module from .compat import PostGenerationContext -from typing import TYPE_CHECKING if TYPE_CHECKING: + from types import ModuleType from typing import Any, Callable, TypeVar + from _pytest.fixtures import FixtureRequest from factory.builder import BuildStep - from factory.declarations import PostGeneration - from factory.declarations import PostGenerationContext - from types import ModuleType + from factory.declarations import PostGeneration, PostGenerationContext FactoryType = type[factory.Factory] T = TypeVar("T") diff --git a/pytest_factoryboy/plugin.py b/pytest_factoryboy/plugin.py index 40f6c14..562f8ff 100644 --- a/pytest_factoryboy/plugin.py +++ b/pytest_factoryboy/plugin.py @@ -2,17 +2,18 @@ from __future__ import annotations from collections import defaultdict -import pytest from typing import TYPE_CHECKING +import pytest if TYPE_CHECKING: from typing import Any - from factory import Factory - from _pytest.fixtures import FixtureRequest + from _pytest.config import PytestPluginManager - from _pytest.python import Metafunc + from _pytest.fixtures import FixtureRequest from _pytest.nodes import Item + from _pytest.python import Metafunc + from factory import Factory from .fixture import DeferredFunction diff --git a/tests/test_circular.py b/tests/test_circular.py index 4762735..540278f 100644 --- a/tests/test_circular.py +++ b/tests/test_circular.py @@ -1,7 +1,7 @@ """Test circular definitions.""" from __future__ import annotations -from dataclasses import field, dataclass +from dataclasses import dataclass, field import factory diff --git a/tests/test_factory_fixtures.py b/tests/test_factory_fixtures.py index f889e43..3f6fcda 100644 --- a/tests/test_factory_fixtures.py +++ b/tests/test_factory_fixtures.py @@ -2,16 +2,17 @@ from __future__ import annotations from dataclasses import dataclass, field +from typing import TYPE_CHECKING import factory -from factory import fuzzy import pytest +from factory import fuzzy -from pytest_factoryboy import register, LazyFixture -from typing import TYPE_CHECKING +from pytest_factoryboy import LazyFixture, register if TYPE_CHECKING: from typing import Any + from factory.declarations import LazyAttribute diff --git a/tests/test_lazy_fixture.py b/tests/test_lazy_fixture.py index a332e46..e92ff4f 100644 --- a/tests/test_lazy_fixture.py +++ b/tests/test_lazy_fixture.py @@ -6,7 +6,7 @@ import factory import pytest -from pytest_factoryboy import register, LazyFixture +from pytest_factoryboy import LazyFixture, register @dataclass diff --git a/tests/test_postgen_dependencies.py b/tests/test_postgen_dependencies.py index d2d4c5a..5f8dec7 100644 --- a/tests/test_postgen_dependencies.py +++ b/tests/test_postgen_dependencies.py @@ -2,15 +2,16 @@ from __future__ import annotations from dataclasses import dataclass +from typing import TYPE_CHECKING import factory import pytest from pytest_factoryboy import register -from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Any + from pytest_factoryboy.plugin import Request diff --git a/tests/test_validation.py b/tests/test_validation.py index 03dad47..7fa446b 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -2,6 +2,7 @@ import factory import pytest + from pytest_factoryboy import register From fb3b1593e2a95faef6786bce71129b535a48104b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Skar=C5=BCy=C5=84ski?= Date: Mon, 2 May 2022 14:29:59 +0200 Subject: [PATCH 2/9] ci: introduce mypy to the project --- pyproject.toml | 21 +++++++++++++++++++++ tox.ini | 10 +++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0bdc783..d38b490 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,5 +2,26 @@ line-length = 120 target-version = ['py37', 'py38', 'py39', 'py310', 'py310'] +[tool.mypy] +allow_redefinition = false +check_untyped_defs = true +disallow_untyped_decorators = true +disallow_any_explicit = false +disallow_any_generics = true +disallow_untyped_calls = true +ignore_errors = false +ignore_missing_imports = true +implicit_reexport = false +strict_optional = true +strict_equality = true +no_implicit_optional = true +warn_unused_ignores = true +warn_redundant_casts = true +warn_unused_configs = true +warn_unreachable = true +warn_no_return = true +pretty = true +show_error_codes = true + [tool.isort] profile = 'black' diff --git a/tox.ini b/tox.ini index b00c7a5..1b93f3c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,8 @@ [tox] distshare = {homedir}/.tox/distshare envlist = py38-pytest{46,50,51,52,53,54,60,61,62,70,71,latest,main}, - py{37,39,310,311}-pytestlatest + py{37,39,310,311}-pytestlatest, + py{37,38,39,310,311}-mypy [testenv] commands = pytest --junitxml={envlogdir}/junit-{envname}.xml {posargs:tests} @@ -30,6 +31,13 @@ ignore_outcome = true # allow failures of tests run with unstable python 3.10 ignore_outcome = true +[testenv:py{37,38,39,310,311}-mypy] +commands = mypy {posargs:.} +deps = + mypy~=0.950 + + -r{toxinidir}/requirements-testing.txt + [pytest] addopts = -vv -l From d0703c12ec82a439ccc9ada70817ef3c3431cbe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Skar=C5=BCy=C5=84ski?= Date: Tue, 3 May 2022 00:19:14 +0200 Subject: [PATCH 3/9] Revert "ci: introduce isort to the project" This reverts commit 9c1d7e0811119b33f814aa0badd69320ed62abf2. --- .pre-commit-config.yaml | 4 ---- docs/conf.py | 2 +- pyproject.toml | 3 --- pytest_factoryboy/__init__.py | 2 +- pytest_factoryboy/codegen.py | 2 +- pytest_factoryboy/compat.py | 3 +-- pytest_factoryboy/fixture.py | 10 +++++----- pytest_factoryboy/plugin.py | 9 ++++----- tests/test_circular.py | 2 +- tests/test_factory_fixtures.py | 7 +++---- tests/test_lazy_fixture.py | 2 +- tests/test_postgen_dependencies.py | 3 +-- tests/test_validation.py | 1 - 13 files changed, 19 insertions(+), 31 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6a9a0a9..3bcac8d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,10 +12,6 @@ repos: - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files -- repo: https://github.com/pycqa/isort - rev: 5.10.1 - hooks: - - id: isort - repo: https://github.com/asottile/pyupgrade rev: v2.32.0 hooks: diff --git a/docs/conf.py b/docs/conf.py index aeb8178..d3cb4d6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,8 +15,8 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -import os import sys +import os sys.path.insert(0, os.path.abspath("..")) diff --git a/pyproject.toml b/pyproject.toml index d38b490..ed1f8ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,3 @@ warn_unreachable = true warn_no_return = true pretty = true show_error_codes = true - -[tool.isort] -profile = 'black' diff --git a/pytest_factoryboy/__init__.py b/pytest_factoryboy/__init__.py index b47e5c4..832401d 100644 --- a/pytest_factoryboy/__init__.py +++ b/pytest_factoryboy/__init__.py @@ -1,5 +1,5 @@ """pytest-factoryboy public API.""" -from .fixture import LazyFixture, register +from .fixture import register, LazyFixture __version__ = "2.1.0" diff --git a/pytest_factoryboy/codegen.py b/pytest_factoryboy/codegen.py index 6b5db05..c5fee73 100644 --- a/pytest_factoryboy/codegen.py +++ b/pytest_factoryboy/codegen.py @@ -8,7 +8,7 @@ import shutil import tempfile import typing -from dataclasses import dataclass, field +from dataclasses import field, dataclass from functools import lru_cache from types import ModuleType diff --git a/pytest_factoryboy/compat.py b/pytest_factoryboy/compat.py index 9389d5d..4014532 100644 --- a/pytest_factoryboy/compat.py +++ b/pytest_factoryboy/compat.py @@ -1,7 +1,6 @@ from __future__ import annotations - -import pathlib import sys +import pathlib try: from factory.declarations import PostGenerationContext diff --git a/pytest_factoryboy/fixture.py b/pytest_factoryboy/fixture.py index db39f02..0d23660 100644 --- a/pytest_factoryboy/fixture.py +++ b/pytest_factoryboy/fixture.py @@ -4,7 +4,6 @@ import sys from dataclasses import dataclass from inspect import getmodule, signature -from typing import TYPE_CHECKING import factory import factory.builder @@ -12,16 +11,17 @@ import factory.enums import inflection -from .codegen import FixtureDef, make_fixture_model_module +from .codegen import make_fixture_model_module, FixtureDef from .compat import PostGenerationContext +from typing import TYPE_CHECKING if TYPE_CHECKING: - from types import ModuleType from typing import Any, Callable, TypeVar - from _pytest.fixtures import FixtureRequest from factory.builder import BuildStep - from factory.declarations import PostGeneration, PostGenerationContext + from factory.declarations import PostGeneration + from factory.declarations import PostGenerationContext + from types import ModuleType FactoryType = type[factory.Factory] T = TypeVar("T") diff --git a/pytest_factoryboy/plugin.py b/pytest_factoryboy/plugin.py index 562f8ff..40f6c14 100644 --- a/pytest_factoryboy/plugin.py +++ b/pytest_factoryboy/plugin.py @@ -2,18 +2,17 @@ from __future__ import annotations from collections import defaultdict +import pytest from typing import TYPE_CHECKING -import pytest if TYPE_CHECKING: from typing import Any - - from _pytest.config import PytestPluginManager + from factory import Factory from _pytest.fixtures import FixtureRequest - from _pytest.nodes import Item + from _pytest.config import PytestPluginManager from _pytest.python import Metafunc - from factory import Factory + from _pytest.nodes import Item from .fixture import DeferredFunction diff --git a/tests/test_circular.py b/tests/test_circular.py index 540278f..4762735 100644 --- a/tests/test_circular.py +++ b/tests/test_circular.py @@ -1,7 +1,7 @@ """Test circular definitions.""" from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import field, dataclass import factory diff --git a/tests/test_factory_fixtures.py b/tests/test_factory_fixtures.py index 3f6fcda..f889e43 100644 --- a/tests/test_factory_fixtures.py +++ b/tests/test_factory_fixtures.py @@ -2,17 +2,16 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import TYPE_CHECKING import factory -import pytest from factory import fuzzy +import pytest -from pytest_factoryboy import LazyFixture, register +from pytest_factoryboy import register, LazyFixture +from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Any - from factory.declarations import LazyAttribute diff --git a/tests/test_lazy_fixture.py b/tests/test_lazy_fixture.py index e92ff4f..a332e46 100644 --- a/tests/test_lazy_fixture.py +++ b/tests/test_lazy_fixture.py @@ -6,7 +6,7 @@ import factory import pytest -from pytest_factoryboy import LazyFixture, register +from pytest_factoryboy import register, LazyFixture @dataclass diff --git a/tests/test_postgen_dependencies.py b/tests/test_postgen_dependencies.py index 5f8dec7..d2d4c5a 100644 --- a/tests/test_postgen_dependencies.py +++ b/tests/test_postgen_dependencies.py @@ -2,16 +2,15 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING import factory import pytest from pytest_factoryboy import register +from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Any - from pytest_factoryboy.plugin import Request diff --git a/tests/test_validation.py b/tests/test_validation.py index 7fa446b..03dad47 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -2,7 +2,6 @@ import factory import pytest - from pytest_factoryboy import register From 5f92690ff8c1a074b063cb109e7ff7632dfca836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Skar=C5=BCy=C5=84ski?= Date: Wed, 4 May 2022 11:38:19 +0200 Subject: [PATCH 4/9] refactor: adjust type hintings to satisfy mypy --- pyproject.toml | 5 ++++ pytest_factoryboy/codegen.py | 9 ++++-- pytest_factoryboy/compat.py | 2 ++ pytest_factoryboy/fixture.py | 45 +++++++++++++++--------------- pytest_factoryboy/plugin.py | 18 ++++++------ tests/test_postgen_dependencies.py | 13 +++++++-- 6 files changed, 55 insertions(+), 37 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e778f00..4d78d83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ line-length = 120 target-version = ['py37', 'py38', 'py39', 'py310'] [tool.mypy] +exclude = ['docs/'] allow_redefinition = false check_untyped_defs = true disallow_untyped_decorators = true @@ -26,3 +27,7 @@ warn_unreachable = true warn_no_return = true pretty = true show_error_codes = true + +[[tool.mypy.overrides]] +module = ["tests.*"] +disallow_untyped_decorators = false diff --git a/pytest_factoryboy/codegen.py b/pytest_factoryboy/codegen.py index c5fee73..ff842ec 100644 --- a/pytest_factoryboy/codegen.py +++ b/pytest_factoryboy/codegen.py @@ -7,10 +7,11 @@ import pathlib import shutil import tempfile -import typing from dataclasses import field, dataclass from functools import lru_cache from types import ModuleType +from typing import Any +from typing_extensions import Literal import mako.template from appdirs import AppDirs @@ -25,8 +26,8 @@ @dataclass class FixtureDef: name: str - function_name: typing.Literal["model_fixture", "attr_fixture", "factory_fixture", "subfactory_fixture"] - function_kwargs: dict = field(default_factory=dict) + function_name: Literal["model_fixture", "attr_fixture", "factory_fixture", "subfactory_fixture"] + function_kwargs: dict[str, Any] = field(default_factory=dict) deps: list[str] = field(default_factory=list) related: list[str] = field(default_factory=list) @@ -122,7 +123,9 @@ def make_module(code: str, module_name: str, package_name: str) -> ModuleType: tmp_module_path.write_text(code) name = f"{package_name}.{module_name}" spec = importlib.util.spec_from_file_location(name, tmp_module_path) + assert spec # NOTE: satisfy `mypy` mod = importlib.util.module_from_spec(spec) + assert spec.loader # NOTE: satisfy `mypy` spec.loader.exec_module(mod) return mod diff --git a/pytest_factoryboy/compat.py b/pytest_factoryboy/compat.py index 4014532..229467a 100644 --- a/pytest_factoryboy/compat.py +++ b/pytest_factoryboy/compat.py @@ -2,6 +2,8 @@ import sys import pathlib +__all__ = ("PostGenerationContext", "path_with_stem") + try: from factory.declarations import PostGenerationContext except ImportError: # factory_boy < 3.2.0 diff --git a/pytest_factoryboy/fixture.py b/pytest_factoryboy/fixture.py index 095de67..f7c6025 100644 --- a/pytest_factoryboy/fixture.py +++ b/pytest_factoryboy/fixture.py @@ -14,17 +14,17 @@ from .codegen import make_fixture_model_module, FixtureDef from .compat import PostGenerationContext -from typing import TYPE_CHECKING, overload -from typing_extensions import Protocol +from typing import TYPE_CHECKING, overload, cast +from typing_extensions import Protocol, TypeAlias if TYPE_CHECKING: from typing import Any, Callable, TypeVar - from _pytest.fixtures import FixtureRequest + from _pytest.fixtures import SubRequest, FixtureFunction from factory.builder import BuildStep from factory.declarations import PostGeneration from factory.declarations import PostGenerationContext - FactoryType = type[factory.Factory] + FactoryType: TypeAlias = factory.Factory T = TypeVar("T") F = TypeVar("F", bound=FactoryType) @@ -37,9 +37,9 @@ class DeferredFunction: name: str factory: FactoryType is_related: bool - function: Callable[[FixtureRequest], Any] + function: Callable[[SubRequest], Any] - def __call__(self, request: FixtureRequest) -> Any: + def __call__(self, request: SubRequest) -> Any: return self.function(request) @@ -51,7 +51,7 @@ def __call__(self, factory_class: F, _name: str | None = None, **kwargs: Any) -> @overload -def register( +def register( # type: ignore[misc] factory_class: None = None, _name: str | None = None, **kwargs: Any, @@ -177,7 +177,7 @@ def register( return factory_class -def inject_into_caller(name: str, function: Callable, locals_: dict[str, Any]) -> None: +def inject_into_caller(name: str, function: Callable[..., Any], locals_: dict[str, Any]) -> None: """Inject a function into the caller's locals, making sure that the function will work also within classes.""" # We need to check if the caller frame is a class, since in that case the first argument is the class itself. # In that case, we can apply the staticmethod() decorator to the injected function, so that the first param @@ -191,7 +191,7 @@ def inject_into_caller(name: str, function: Callable, locals_: dict[str, Any]) - # Therefore, we can just check for __qualname__ to figure out if we are in a class, and apply the @staticmethod. is_class_or_function = "__qualname__" in locals_ if is_class_or_function: - function = staticmethod(function) + function = staticmethod(function) # type: ignore[assignment] locals_[name] = function @@ -238,20 +238,21 @@ def is_dep(value: Any) -> bool: ] -def evaluate(request: FixtureRequest, value: LazyFixture | Any) -> Any: +def evaluate(request: SubRequest, value: LazyFixture | Any) -> Any: """Evaluate the declaration (lazy fixtures, etc).""" return value.evaluate(request) if isinstance(value, LazyFixture) else value -def model_fixture(request: FixtureRequest, factory_name: str) -> Any: +def model_fixture(request: SubRequest, factory_name: str) -> Any: """Model fixture implementation.""" factoryboy_request = request.getfixturevalue("factoryboy_request") # Try to evaluate as much post-generation dependencies as possible factoryboy_request.evaluate(request) + fixture_name = str(request.fixturename) factory_class: FactoryType = request.getfixturevalue(factory_name) - prefix = "".join((request.fixturename, SEPARATOR)) + prefix = "".join((fixture_name, SEPARATOR)) # Create model fixture instance @@ -279,7 +280,7 @@ class Factory(factory_class): # Cache the instance value on pytest level so that the fixture can be resolved before the return request._fixturedef.cached_result = (instance, 0, None) - request._fixture_defs[request.fixturename] = request._fixturedef + request._fixture_defs[fixture_name] = request._fixturedef # Defer post-generation declarations deferred: list[DeferredFunction] = [] @@ -289,7 +290,7 @@ class Factory(factory_class): decl = factory_class._meta.post_declarations.declarations[attr] if isinstance(decl, factory.RelatedFactory): - deferred.append(make_deferred_related(factory_class, request.fixturename, attr)) + deferred.append(make_deferred_related(factory_class, fixture_name, attr)) else: argname = "".join((prefix, attr)) extra = {} @@ -309,7 +310,7 @@ class Factory(factory_class): extra=extra, ) deferred.append( - make_deferred_postgen(step, factory_class, request.fixturename, instance, attr, decl, postgen_context) + make_deferred_postgen(step, factory_class, fixture_name, instance, attr, decl, postgen_context) ) factoryboy_request.defer(deferred) @@ -329,7 +330,7 @@ def make_deferred_related(factory: FactoryType, fixture: str, attr: str) -> Defe """ name = SEPARATOR.join((fixture, attr)) - def deferred_impl(request: FixtureRequest) -> Any: + def deferred_impl(request: SubRequest) -> Any: return request.getfixturevalue(name) return DeferredFunction( @@ -362,7 +363,7 @@ def make_deferred_postgen( """ name = SEPARATOR.join((fixture, attr)) - def deferred_impl(request: FixtureRequest) -> Any: + def deferred_impl(request: SubRequest) -> Any: return declaration.call(instance, step, context) return DeferredFunction( @@ -373,17 +374,17 @@ def deferred_impl(request: FixtureRequest) -> Any: ) -def factory_fixture(request: FixtureRequest, factory_class: F) -> F: +def factory_fixture(request: SubRequest, factory_class: F) -> F: """Factory fixture implementation.""" return factory_class -def attr_fixture(request: FixtureRequest, value: T) -> T: +def attr_fixture(request: SubRequest, value: T) -> T: """Attribute fixture implementation.""" return value -def subfactory_fixture(request: FixtureRequest, factory_class: FactoryType) -> Any: +def subfactory_fixture(request: SubRequest, factory_class: FactoryType) -> Any: """SubFactory/RelatedFactory fixture implementation.""" fixture = inflection.underscore(factory_class._meta.model.__name__) return request.getfixturevalue(fixture) @@ -397,7 +398,7 @@ def get_caller_locals(depth: int = 2) -> dict[str, Any]: class LazyFixture: """Lazy fixture.""" - def __init__(self, fixture: Callable | str) -> None: + def __init__(self, fixture: FixtureFunction | str) -> None: """Lazy pytest fixture wrapper. :param fixture: Fixture name or callable with dependencies. @@ -409,7 +410,7 @@ def __init__(self, fixture: Callable | str) -> None: else: self.args = [self.fixture] - def evaluate(self, request: FixtureRequest) -> Any: + def evaluate(self, request: SubRequest) -> Any: """Evaluate the lazy fixture. :param request: pytest request object. diff --git a/pytest_factoryboy/plugin.py b/pytest_factoryboy/plugin.py index 40f6c14..0c813c0 100644 --- a/pytest_factoryboy/plugin.py +++ b/pytest_factoryboy/plugin.py @@ -9,7 +9,7 @@ if TYPE_CHECKING: from typing import Any from factory import Factory - from _pytest.fixtures import FixtureRequest + from _pytest.fixtures import FixtureRequest, SubRequest from _pytest.config import PytestPluginManager from _pytest.python import Metafunc from _pytest.nodes import Item @@ -29,7 +29,7 @@ def __init__(self) -> None: self.deferred: list[list[DeferredFunction]] = [] self.results: dict[str, dict[str, Any]] = defaultdict(dict) self.model_factories: dict[str, type[Factory]] = {} - self.in_progress: set = set() + self.in_progress: set[DeferredFunction] = set() def defer(self, functions: list[DeferredFunction]) -> None: """Defer post-generation declaration execution until the end of the test setup. @@ -39,7 +39,7 @@ def defer(self, functions: list[DeferredFunction]) -> None: """ self.deferred.append(functions) - def get_deps(self, request: FixtureRequest, fixture: str, deps: set[str] | None = None) -> set[str]: + def get_deps(self, request: SubRequest, fixture: str, deps: set[str] | None = None) -> set[str]: request = request.getfixturevalue("request") if deps is None: @@ -54,15 +54,15 @@ def get_deps(self, request: FixtureRequest, fixture: str, deps: set[str] | None deps.update(self.get_deps(request, argname, deps)) return deps - def get_current_deps(self, request: FixtureRequest) -> set[str]: + def get_current_deps(self, request: FixtureRequest | SubRequest) -> set[str]: deps = set() while hasattr(request, "_parent_request"): if request.fixturename and request.fixturename not in getattr(request, "_fixturedefs", {}): deps.add(request.fixturename) - request = request._parent_request + request = request._parent_request # type: ignore[union-attr] return deps - def execute(self, request: FixtureRequest, function: DeferredFunction, deferred: list[DeferredFunction]) -> None: + def execute(self, request: SubRequest, function: DeferredFunction, deferred: list[DeferredFunction]) -> None: """Execute deferred function and store the result.""" if function in self.in_progress: raise CycleDetected() @@ -79,7 +79,7 @@ def execute(self, request: FixtureRequest, function: DeferredFunction, deferred: deferred.remove(function) self.in_progress.remove(function) - def after_postgeneration(self, request: FixtureRequest) -> None: + def after_postgeneration(self, request: SubRequest) -> None: """Call _after_postgeneration hooks.""" for model in list(self.results.keys()): results = self.results.pop(model) @@ -87,7 +87,7 @@ def after_postgeneration(self, request: FixtureRequest) -> None: factory = self.model_factories[model] factory._after_postgeneration(obj, create=True, results=results) - def evaluate(self, request: FixtureRequest) -> None: + def evaluate(self, request: SubRequest) -> None: """Finalize, run deferred post-generation actions, etc.""" while self.deferred: try: @@ -114,7 +114,7 @@ def pytest_runtest_call(item: Item) -> None: """Before the test item is called.""" # TODO: We should instead do an `if isinstance(item, Function)`. try: - request = item._request + request = item._request # type: ignore[attr-defined] except AttributeError: # pytest-pep8 plugin passes Pep8Item here during tests. return diff --git a/tests/test_postgen_dependencies.py b/tests/test_postgen_dependencies.py index d2d4c5a..a05eedf 100644 --- a/tests/test_postgen_dependencies.py +++ b/tests/test_postgen_dependencies.py @@ -1,7 +1,7 @@ """Test post-generation dependencies.""" from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field import factory import pytest @@ -19,6 +19,12 @@ class Foo: value: int expected: int + bar: Bar | None = None + + # NOTE: following attributes are used internally only for assertions + _create: bool | None = None + _postgeneration_results: dict[str, Any] = field(default_factory=dict) + @dataclass class Bar: @@ -59,7 +65,7 @@ def set1(foo: Foo, create: bool, value: Any, **kwargs: Any) -> str: @classmethod def _after_postgeneration(cls, obj: Foo, create: bool, results: dict[str, Any] | None = None) -> None: - obj._postgeneration_results = results + obj._postgeneration_results = results or {} obj._create = create @@ -111,8 +117,9 @@ def test_after_postgeneration(foo: Foo): assert len(foo._postgeneration_results) == 2 +@dataclass class Ordered: - value = None + value: str | None = None @register From 9103cb094c1c978ffec71d41ca9e536d334147a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Skar=C5=BCy=C5=84ski?= Date: Wed, 4 May 2022 15:35:25 +0200 Subject: [PATCH 5/9] fix: improve type hintings --- pyproject.toml | 1 - pytest_factoryboy/fixture.py | 22 ++++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fc8cf0c..27f2d6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,6 @@ ignore_errors = false ignore_missing_imports = true implicit_reexport = false strict_optional = true -strict_equality = true no_implicit_optional = true warn_unused_ignores = true warn_redundant_casts = true diff --git a/pytest_factoryboy/fixture.py b/pytest_factoryboy/fixture.py index dde2d08..225b8ba 100644 --- a/pytest_factoryboy/fixture.py +++ b/pytest_factoryboy/fixture.py @@ -12,7 +12,7 @@ import factory.declarations import factory.enums import inflection -from typing_extensions import Protocol, TypeAlias +from typing_extensions import Protocol from .codegen import FixtureDef, make_fixture_model_module from .compat import PostGenerationContext @@ -24,7 +24,7 @@ from factory.builder import BuildStep from factory.declarations import PostGeneration, PostGenerationContext - FactoryType: TypeAlias = type[factory.Factory] + FactoryType = type[factory.Factory] T = TypeVar("T") F = TypeVar("F", bound=FactoryType) @@ -51,7 +51,7 @@ def __call__(self, factory_class: F, _name: str | None = None, **kwargs: Any) -> @overload -def register( # type: ignore[misc] +def register( factory_class: None = None, _name: str | None = None, **kwargs: Any, @@ -196,7 +196,7 @@ def inject_into_caller(name: str, function: Callable[..., Any], locals_: dict[st locals_[name] = function -def get_model_name(factory_class: FactoryType) -> str: +def get_model_name(factory_class: F) -> str: """Get model fixture name by factory.""" return ( inflection.underscore(factory_class._meta.model.__name__) @@ -205,14 +205,14 @@ def get_model_name(factory_class: FactoryType) -> str: ) -def get_factory_name(factory_class: FactoryType) -> str: +def get_factory_name(factory_class: F) -> str: """Get factory fixture name by factory.""" return inflection.underscore(factory_class.__name__) def get_deps( - factory_class: FactoryType, - parent_factory_class: FactoryType | None = None, + factory_class: F, + parent_factory_class: F | None = None, model_name: str | None = None, ) -> list[str]: """Get factory dependencies. @@ -250,12 +250,14 @@ def model_fixture(request: SubRequest, factory_name: str) -> Any: # Try to evaluate as much post-generation dependencies as possible factoryboy_request.evaluate(request) - fixture_name = str(request.fixturename) - factory_class: FactoryType = request.getfixturevalue(factory_name) + assert request.fixturename # NOTE: satisfy mypy + fixture_name = request.fixturename prefix = "".join((fixture_name, SEPARATOR)) + # NOTE: following type hinting is required, because of `mypy` bug. + # Reference: https://github.com/python/mypy/issues/2477 + factory_class: factory.FactoryMetaClass = request.getfixturevalue(factory_name) # Create model fixture instance - class Factory(factory_class): pass From c664123966666118fd989159e73ecf8e43635382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Skar=C5=BCy=C5=84ski?= Date: Thu, 5 May 2022 14:48:12 +0200 Subject: [PATCH 6/9] refactor: apply code review suggestions --- pytest_factoryboy/fixture.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pytest_factoryboy/fixture.py b/pytest_factoryboy/fixture.py index 225b8ba..98b3166 100644 --- a/pytest_factoryboy/fixture.py +++ b/pytest_factoryboy/fixture.py @@ -196,7 +196,7 @@ def inject_into_caller(name: str, function: Callable[..., Any], locals_: dict[st locals_[name] = function -def get_model_name(factory_class: F) -> str: +def get_model_name(factory_class: FactoryType) -> str: """Get model fixture name by factory.""" return ( inflection.underscore(factory_class._meta.model.__name__) @@ -205,14 +205,14 @@ def get_model_name(factory_class: F) -> str: ) -def get_factory_name(factory_class: F) -> str: +def get_factory_name(factory_class: FactoryType) -> str: """Get factory fixture name by factory.""" return inflection.underscore(factory_class.__name__) def get_deps( - factory_class: F, - parent_factory_class: F | None = None, + factory_class: FactoryType, + parent_factory_class: FactoryType | None = None, model_name: str | None = None, ) -> list[str]: """Get factory dependencies. @@ -255,7 +255,7 @@ def model_fixture(request: SubRequest, factory_name: str) -> Any: prefix = "".join((fixture_name, SEPARATOR)) # NOTE: following type hinting is required, because of `mypy` bug. # Reference: https://github.com/python/mypy/issues/2477 - factory_class: factory.FactoryMetaClass = request.getfixturevalue(factory_name) + factory_class: factory.base.FactoryMetaClass = request.getfixturevalue(factory_name) # Create model fixture instance class Factory(factory_class): From 132838005baede239d7886d612cbf82aa0692d1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Skar=C5=BCy=C5=84ski?= Date: Thu, 5 May 2022 14:58:12 +0200 Subject: [PATCH 7/9] fix: add missing type hintings for hooks --- pytest_factoryboy/hooks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pytest_factoryboy/hooks.py b/pytest_factoryboy/hooks.py index fc65ba9..c31a01a 100644 --- a/pytest_factoryboy/hooks.py +++ b/pytest_factoryboy/hooks.py @@ -1,5 +1,7 @@ """pytest-factoryboy pytest hooks.""" +from pytest import FixtureRequest -def pytest_factoryboy_done(request): + +def pytest_factoryboy_done(request: FixtureRequest) -> None: """Called after all factory based fixtures and their post-generation actions were evaluated.""" From c762cd977afc3233f09ef59f9a80f0a60100502c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Skar=C5=BCy=C5=84ski?= Date: Fri, 6 May 2022 11:10:34 +0200 Subject: [PATCH 8/9] ci(tox): ignore outcome of mypy envs --- pytest_factoryboy/fixture.py | 2 +- tox.ini | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pytest_factoryboy/fixture.py b/pytest_factoryboy/fixture.py index 98b3166..7333de6 100644 --- a/pytest_factoryboy/fixture.py +++ b/pytest_factoryboy/fixture.py @@ -191,7 +191,7 @@ def inject_into_caller(name: str, function: Callable[..., Any], locals_: dict[st # Therefore, we can just check for __qualname__ to figure out if we are in a class, and apply the @staticmethod. is_class_or_function = "__qualname__" in locals_ if is_class_or_function: - function = staticmethod(function) # type: ignore[assignment] + function = staticmethod(function) locals_[name] = function diff --git a/tox.ini b/tox.ini index 9434fcf..141a427 100644 --- a/tox.ini +++ b/tox.ini @@ -31,6 +31,9 @@ ignore_outcome = true ignore_outcome = true [testenv:py{37,38,39,310,311}-mypy] +# allow failures, because type hintings are not fully valid for some +# `python` versions +ignore_outcome = true commands = mypy {posargs:.} deps = mypy~=0.950 From 8fac35770f9f3bec12b98ae8bfd35a63b8e99579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Skar=C5=BCy=C5=84ski?= Date: Fri, 6 May 2022 11:16:17 +0200 Subject: [PATCH 9/9] fix: fix hooks type hintings --- pytest_factoryboy/hooks.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pytest_factoryboy/hooks.py b/pytest_factoryboy/hooks.py index c31a01a..1ef07ce 100644 --- a/pytest_factoryboy/hooks.py +++ b/pytest_factoryboy/hooks.py @@ -1,6 +1,10 @@ """pytest-factoryboy pytest hooks.""" +from __future__ import annotations -from pytest import FixtureRequest +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pytest import FixtureRequest def pytest_factoryboy_done(request: FixtureRequest) -> None: