From 1c773011e30d8021f995be13ca270ca106bcc45d Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 28 Jun 2025 06:53:06 +0200 Subject: [PATCH 01/16] Fix type errors in `compat.py` --- pytest_factoryboy/compat.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pytest_factoryboy/compat.py b/pytest_factoryboy/compat.py index b5a2d84..c2555b8 100644 --- a/pytest_factoryboy/compat.py +++ b/pytest_factoryboy/compat.py @@ -15,17 +15,23 @@ try: from factory.declarations import PostGenerationContext except ImportError: # factory_boy < 3.2.0 - from factory.builder import PostGenerationContext + from factory.builder import ( # type: ignore[attr-defined, no-redef] + PostGenerationContext, + ) if pytest_version.release >= (8, 1): - def getfixturedefs(fixturemanager: FixtureManager, fixturename: str, node: Node) -> Sequence[FixtureDef] | None: + def getfixturedefs( + fixturemanager: FixtureManager, fixturename: str, node: Node + ) -> Sequence[FixtureDef[object]] | None: return fixturemanager.getfixturedefs(fixturename, node) else: - def getfixturedefs(fixturemanager: FixtureManager, fixturename: str, node: Node) -> Sequence[FixtureDef] | None: - return fixturemanager.getfixturedefs(fixturename, node.nodeid) + def getfixturedefs( + fixturemanager: FixtureManager, fixturename: str, node: Node + ) -> Sequence[FixtureDef[object]] | None: + return fixturemanager.getfixturedefs(fixturename, node.nodeid) # type: ignore[arg-type] if pytest_version.release >= (8, 4): @@ -35,4 +41,4 @@ def getfixturedefs(fixturemanager: FixtureManager, fixturename: str, node: Node) else: from _pytest.fixtures import FixtureFunction - PytestFixtureT: TypeAlias = FixtureFunction + PytestFixtureT: TypeAlias = FixtureFunction # type: ignore[misc, no-redef] From ce4e6928c46f185a4985431ab085b74e792c21d6 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 28 Jun 2025 07:45:50 +0200 Subject: [PATCH 02/16] Fix type errors in `fixture.py` --- pyproject.toml | 2 +- pytest_factoryboy/fixture.py | 111 +++++++++++++++++++---------------- 2 files changed, 60 insertions(+), 53 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5dd3e2b..a29f173 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,7 +88,7 @@ check_untyped_defs = true disallow_untyped_decorators = true disallow_any_explicit = false disallow_any_generics = true -disallow_untyped_calls = true +disallow_untyped_calls = false disallow_untyped_defs = true ignore_errors = false ignore_missing_imports = true diff --git a/pytest_factoryboy/fixture.py b/pytest_factoryboy/fixture.py index e94b039..983d19c 100644 --- a/pytest_factoryboy/fixture.py +++ b/pytest_factoryboy/fixture.py @@ -13,11 +13,19 @@ from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, cast, overload import factory -import factory.builder -import factory.declarations import factory.enums import inflection -from typing_extensions import ParamSpec, TypeAlias +from factory.base import Factory +from factory.builder import BuildStep, DeclarationSet, StepBuilder +from factory.declarations import ( + NotProvided, + PostGeneration, + PostGenerationDeclaration, + PostGenerationMethodCall, + RelatedFactory, + SubFactory, +) +from typing_extensions import ParamSpec from .compat import PostGenerationContext from .fixturegen import create_fixture @@ -27,9 +35,8 @@ from .plugin import Request as FactoryboyRequest -FactoryType: TypeAlias = type[factory.Factory] -F = TypeVar("F", bound=FactoryType) T = TypeVar("T") +U = TypeVar("U") T_co = TypeVar("T_co", covariant=True) P = ParamSpec("P") @@ -38,9 +45,9 @@ @dataclass(eq=False) -class DeferredFunction: +class DeferredFunction(Generic[T]): name: str - factory: FactoryType + factory: type[Factory[T]] is_related: bool function: Callable[[SubRequest], Any] @@ -67,24 +74,24 @@ def named_model(model_cls: type[T], name: str) -> type[T]: # register(AuthorFactory, ...) # # @register -# class AuthorFactory(factory.Factory): ... +# class AuthorFactory(Factory): ... @overload -def register(factory_class: F, _name: str | None = None, **kwargs: Any) -> F: ... +def register(factory_class: type[Factory[T]], _name: str | None = None, **kwargs: Any) -> type[Factory[T]]: ... # @register(...) -# class AuthorFactory(factory.Factory): ... +# class AuthorFactory(Factory): ... @overload -def register(*, _name: str | None = None, **kwargs: Any) -> Callable[[F], F]: ... +def register(*, _name: str | None = None, **kwargs: Any) -> Callable[[type[Factory[T]]], type[Factory[T]]]: ... def register( - factory_class: F | None = None, + factory_class: type[Factory[T]] | None = None, _name: str | None = None, *, _caller_locals: Box[dict[str, Any]] | None = None, **kwargs: Any, -) -> F | Callable[[F], F]: +) -> type[Factory[T]] | Callable[[type[Factory[T]]], type[Factory[T]]]: r"""Register fixtures for the factory class. :param factory_class: Factory class to register. @@ -97,7 +104,7 @@ def register( if factory_class is None: - def register_(factory_class: F) -> F: + def register_(factory_class: type[Factory[T]]) -> type[Factory[T]]: return register(factory_class, _name=_name, _caller_locals=_caller_locals, **kwargs) return register_ @@ -131,7 +138,7 @@ def register_(factory_class: F) -> F: def generate_fixtures( - factory_class: FactoryType, + factory_class: type[Factory[T]], model_name: str, factory_name: str, overrides: Mapping[str, Any], @@ -193,23 +200,23 @@ def create_fixture_with_related( def make_declaration_fixturedef( attr_name: str, value: Any, - factory_class: FactoryType, + factory_class: type[Factory[T]], related: list[str], ) -> Callable[..., Any]: """Create the FixtureDef for a factory declaration.""" - if isinstance(value, (factory.SubFactory, factory.RelatedFactory)): - subfactory_class = value.get_factory() + if isinstance(value, (SubFactory, RelatedFactory)): + subfactory_class: type[Factory[object]] = value.get_factory() subfactory_deps = get_deps(subfactory_class, factory_class) args = list(subfactory_deps) - if isinstance(value, factory.RelatedFactory): + if isinstance(value, RelatedFactory): related_model = get_model_name(subfactory_class) args.append(related_model) related.append(related_model) related.append(attr_name) related.extend(subfactory_deps) - if isinstance(value, factory.SubFactory): + if isinstance(value, SubFactory): args.append(inflection.underscore(subfactory_class._meta.model.__name__)) return create_fixture_with_related( @@ -219,10 +226,10 @@ def make_declaration_fixturedef( ) deps: list[str] # makes mypy happy - if isinstance(value, factory.PostGeneration): + if isinstance(value, PostGeneration): value = None deps = [] - elif isinstance(value, factory.PostGenerationMethodCall): + elif isinstance(value, PostGenerationMethodCall): value = value.method_arg deps = [] elif isinstance(value, LazyFixture): @@ -258,7 +265,7 @@ def inject_into_caller(name: str, function: Callable[..., Any], locals_: Box[dic locals_.value[name] = function -def get_model_name(factory_class: FactoryType) -> str: +def get_model_name(factory_class: type[Factory[T]]) -> str: """Get model fixture name by factory.""" model_cls = factory_class._meta.model @@ -278,14 +285,14 @@ def get_model_name(factory_class: FactoryType) -> str: return model_name -def get_factory_name(factory_class: FactoryType) -> str: +def get_factory_name(factory_class: type[Factory[T]]) -> 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: type[Factory[T]], + parent_factory_class: type[Factory[U]] | None = None, model_name: str | None = None, ) -> list[str]: """Get factory dependencies. @@ -296,11 +303,13 @@ def get_deps( parent_model_name = get_model_name(parent_factory_class) if parent_factory_class is not None else None def is_dep(value: Any) -> bool: - if isinstance(value, factory.RelatedFactory): + if isinstance(value, RelatedFactory): return False - if isinstance(value, factory.SubFactory) and get_model_name(value.get_factory()) == parent_model_name: - return False - if isinstance(value, factory.declarations.PostGenerationDeclaration): + if isinstance(value, SubFactory): + subfactory_class: type[Factory[object]] = value.get_factory() + if get_model_name(subfactory_class) == parent_model_name: + return False + if isinstance(value, PostGenerationDeclaration): # Dependency on extracted value return True @@ -334,7 +343,7 @@ def disable_method(method: MethodType) -> Iterator[None]: setattr(klass, method.__name__, old_method) -def model_fixture(request: SubRequest, factory_name: str) -> Any: +def model_fixture(request: SubRequest, factory_name: str) -> object: """Model fixture implementation.""" factoryboy_request: FactoryboyRequest = request.getfixturevalue("factoryboy_request") @@ -345,21 +354,19 @@ def model_fixture(request: SubRequest, factory_name: str) -> Any: fixture_name = request.fixturename prefix = "".join((fixture_name, SEPARATOR)) - factory_class: FactoryType = request.getfixturevalue(factory_name) + factory_class: type[Factory[object]] = request.getfixturevalue(factory_name) # Create model fixture instance - Factory: FactoryType = cast(FactoryType, type("Factory", (factory_class,), {})) + NewFactory: type[Factory[object]] = cast(type[Factory[object]], type("Factory", (factory_class,), {})) # equivalent to: # class Factory(factory_class): # pass # it just makes mypy understand it. - Factory._meta.base_declarations = { - k: v - for k, v in Factory._meta.base_declarations.items() - if not isinstance(v, factory.declarations.PostGenerationDeclaration) + NewFactory._meta.base_declarations = { + k: v for k, v in NewFactory._meta.base_declarations.items() if not isinstance(v, PostGenerationDeclaration) } - Factory._meta.post_declarations = factory.builder.DeclarationSet() + NewFactory._meta.post_declarations = DeclarationSet() kwargs = {} for key in factory_class._meta.pre_declarations: @@ -368,25 +375,25 @@ def model_fixture(request: SubRequest, factory_name: str) -> Any: kwargs[key] = evaluate(request, request.getfixturevalue(argname)) strategy = factory.enums.CREATE_STRATEGY - builder = factory.builder.StepBuilder(Factory._meta, kwargs, strategy) - step = factory.builder.BuildStep(builder=builder, sequence=Factory._meta.next_sequence()) + builder = StepBuilder(NewFactory._meta, kwargs, strategy) + step = BuildStep(builder=builder, sequence=NewFactory._meta.next_sequence()) # FactoryBoy invokes the `_after_postgeneration` method, but we will instead call it manually later, # once we are able to evaluate all the related fixtures. - with disable_method(Factory._after_postgeneration): - instance = Factory(**kwargs) + with disable_method(NewFactory._after_postgeneration): # type: ignore[arg-type] # https://github.com/python/mypy/issues/14235 + instance = NewFactory(**kwargs) # 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[fixture_name] = request._fixturedef # Defer post-generation declarations - deferred: list[DeferredFunction] = [] + deferred: list[DeferredFunction[object]] = [] for attr in factory_class._meta.post_declarations.sorted(): decl = factory_class._meta.post_declarations.declarations[attr] - if isinstance(decl, factory.RelatedFactory): + if isinstance(decl, RelatedFactory): deferred.append(make_deferred_related(factory_class, fixture_name, attr)) else: argname = "".join((prefix, attr)) @@ -405,7 +412,7 @@ def model_fixture(request: SubRequest, factory_name: str) -> Any: # that `value_provided` should be falsy postgen_value = evaluate(request, request.getfixturevalue(argname)) postgen_context = PostGenerationContext( - value_provided=(postgen_value is not factory.declarations.NotProvided), + value_provided=(postgen_value is not NotProvided), value=postgen_value, extra=extra, ) @@ -420,7 +427,7 @@ def model_fixture(request: SubRequest, factory_name: str) -> Any: return instance -def make_deferred_related(factory: FactoryType, fixture: str, attr: str) -> DeferredFunction: +def make_deferred_related(factory: type[Factory[T]], fixture: str, attr: str) -> DeferredFunction[T]: """Make deferred function for the related factory declaration. :param factory: Factory class. @@ -443,14 +450,14 @@ def deferred_impl(request: SubRequest) -> Any: def make_deferred_postgen( - step: factory.builder.BuildStep, - factory_class: FactoryType, + step: BuildStep, + factory_class: type[Factory[T]], fixture: str, instance: Any, attr: str, - declaration: factory.declarations.PostGenerationDeclaration, + declaration: PostGenerationDeclaration, context: PostGenerationContext, -) -> DeferredFunction: +) -> DeferredFunction[T]: """Make deferred function for the post-generation declaration. :param step: factory_boy builder step. @@ -476,7 +483,7 @@ def deferred_impl(request: SubRequest) -> Any: ) -def factory_fixture(request: SubRequest, factory_class: F) -> F: +def factory_fixture(request: SubRequest, factory_class: type[Factory[T]]) -> type[Factory[T]]: """Factory fixture implementation.""" return factory_class @@ -486,7 +493,7 @@ def attr_fixture(request: SubRequest, value: T) -> T: return value -def subfactory_fixture(request: SubRequest, factory_class: FactoryType) -> Any: +def subfactory_fixture(request: SubRequest, factory_class: type[Factory[object]]) -> Any: """SubFactory/RelatedFactory fixture implementation.""" fixture = inflection.underscore(factory_class._meta.model.__name__) return request.getfixturevalue(fixture) From 16c4a633d634dbc24d8c3243ecbdc7891cabbdb5 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 28 Jun 2025 07:54:28 +0200 Subject: [PATCH 03/16] Fix type errors in `plugin.py` --- pytest_factoryboy/plugin.py | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/pytest_factoryboy/plugin.py b/pytest_factoryboy/plugin.py index ac2a7c6..ea944ab 100644 --- a/pytest_factoryboy/plugin.py +++ b/pytest_factoryboy/plugin.py @@ -3,22 +3,17 @@ from __future__ import annotations from collections import defaultdict -from typing import TYPE_CHECKING +from typing import Any import pytest +from _pytest.config import PytestPluginManager +from _pytest.fixtures import FixtureRequest, SubRequest +from _pytest.nodes import Item +from _pytest.python import Metafunc +from factory.base import Factory from .compat import getfixturedefs - -if TYPE_CHECKING: - from typing import Any - - from _pytest.config import PytestPluginManager - from _pytest.fixtures import FixtureRequest, SubRequest - from _pytest.nodes import Item - from _pytest.python import Metafunc - from factory import Factory - - from .fixture import DeferredFunction +from .fixture import DeferredFunction class CycleDetected(Exception): @@ -30,12 +25,12 @@ class Request: def __init__(self) -> None: """Create pytest_factoryboy request.""" - self.deferred: list[list[DeferredFunction]] = [] + self.deferred: list[list[DeferredFunction[object]]] = [] self.results: dict[str, dict[str, Any]] = defaultdict(dict) - self.model_factories: dict[str, type[Factory]] = {} - self.in_progress: set[DeferredFunction] = set() + self.model_factories: dict[str, type[Factory[object]]] = {} + self.in_progress: set[DeferredFunction[object]] = set() - def defer(self, functions: list[DeferredFunction]) -> None: + def defer(self, functions: list[DeferredFunction[object]]) -> None: """Defer post-generation declaration execution until the end of the test setup. :param functions: Functions to be deferred. @@ -51,6 +46,8 @@ def get_deps(self, request: SubRequest, fixture: str, deps: set[str] | None = No if fixture == "request": return deps + assert request._pyfuncitem.parent is not None, "Request must have a parent item." + fixturedefs = getfixturedefs(request._fixturemanager, fixture, request._pyfuncitem.parent) for fixturedef in fixturedefs or []: for argname in fixturedef.argnames: @@ -67,7 +64,9 @@ def get_current_deps(self, request: FixtureRequest | SubRequest) -> set[str]: request = request._parent_request return deps - def execute(self, request: SubRequest, function: DeferredFunction, deferred: list[DeferredFunction]) -> None: + def execute( + self, request: SubRequest, function: DeferredFunction[object], deferred: list[DeferredFunction[object]] + ) -> None: """Execute deferred function and store the result.""" if function in self.in_progress: raise CycleDetected() @@ -114,9 +113,7 @@ def factoryboy_request() -> Request: return Request() -# type ignored because pluggy v1.0.0 has no type annotations: -# https://github.com/pytest-dev/pluggy/issues/191 -@pytest.hookimpl(tryfirst=True) # type: ignore[misc] +@pytest.hookimpl(tryfirst=True) def pytest_runtest_call(item: Item) -> None: """Before the test item is called.""" # TODO: We should instead do an `if isinstance(item, Function)`. From 76354bbc7713981a869fb4ea1a40b36dfa119991 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 28 Jun 2025 07:55:40 +0200 Subject: [PATCH 04/16] Tox "mypy": check only the source directory --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 1ef8bf7..24c5af9 100644 --- a/tox.ini +++ b/tox.ini @@ -28,7 +28,7 @@ deps = [testenv:mypy] allowlist_externals = mypy -commands = mypy {posargs:.} +commands = mypy {posargs:pytest_factoryboy} [pytest] addopts = -vv -l From e88606a7dd9288c88631b9c9ce48fdfa70be86be Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 28 Jun 2025 08:07:18 +0200 Subject: [PATCH 05/16] Require type checks to pass --- .github/workflows/main.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0de1bd0..4fb6d38 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,27 +36,27 @@ jobs: include: - python-version: "3.9" toxfactor: py3.9 - ignore-typecheck-outcome: true + ignore-typecheck-outcome: false ignore-test-outcome: false - python-version: "3.10" toxfactor: py3.10 - ignore-typecheck-outcome: true + ignore-typecheck-outcome: false ignore-test-outcome: false - python-version: "3.11" toxfactor: py3.11 - ignore-typecheck-outcome: true + ignore-typecheck-outcome: false ignore-test-outcome: false - python-version: "3.12" toxfactor: py3.12 - ignore-typecheck-outcome: true + ignore-typecheck-outcome: false ignore-test-outcome: false - python-version: "3.13" toxfactor: py3.13 - ignore-typecheck-outcome: true + ignore-typecheck-outcome: false ignore-test-outcome: false - python-version: "3.14" toxfactor: py3.14 - ignore-typecheck-outcome: true + ignore-typecheck-outcome: false ignore-test-outcome: false steps: - uses: actions/checkout@v4 From 3d41f7b20d12fd3063052b68ef8fd1938fd69efd Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 28 Jun 2025 08:27:05 +0200 Subject: [PATCH 06/16] Fix CI mypy runs --- .github/workflows/main.yml | 2 +- tox.ini | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4fb6d38..d0f447a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -92,7 +92,7 @@ jobs: continue-on-error: ${{ matrix.ignore-typecheck-outcome }} run: | source .venv/bin/activate - tox -e mypy + tox -f "${{ matrix.toxfactor }}-mypy" - name: Test with tox continue-on-error: ${{ matrix.ignore-test-outcome }} run: | diff --git a/tox.ini b/tox.ini index 24c5af9..26dc0c6 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ distshare = {homedir}/.tox/distshare 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} py{3.9,3.10,3.11}-pytest{7.0,7.1,7.2} - mypy + py{3.9,3.10,3.11,3.12,3.13,3.14}-mypy [testenv] parallel_show_output = true @@ -26,7 +26,7 @@ deps = coverage[toml] -[testenv:mypy] +[testenv:py{3.9,3.10,3.11,3.12,3.13,3.14,3.15,3.16,3.17,3.18,3.19}-mypy] allowlist_externals = mypy commands = mypy {posargs:pytest_factoryboy} From 05a073ef2a4b9c8c1c7c30da01a104b60f60a8dc Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 28 Jun 2025 08:38:06 +0200 Subject: [PATCH 07/16] Use the correct mypy --- tox.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 26dc0c6..e4ecd81 100644 --- a/tox.ini +++ b/tox.ini @@ -27,8 +27,9 @@ deps = [testenv:py{3.9,3.10,3.11,3.12,3.13,3.14,3.15,3.16,3.17,3.18,3.19}-mypy] -allowlist_externals = mypy -commands = mypy {posargs:pytest_factoryboy} +allowlist_externals = poetry +commands_pre = poetry sync --no-root --only=dev +commands = poetry run mypy {posargs:pytest_factoryboy} [pytest] addopts = -vv -l From 28c80429ce6e770f23f1d2875cf181499cf8bca6 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 28 Jun 2025 08:59:57 +0200 Subject: [PATCH 08/16] add hack for py3.9 --- pytest_factoryboy/fixture.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_factoryboy/fixture.py b/pytest_factoryboy/fixture.py index 983d19c..c8ce4e8 100644 --- a/pytest_factoryboy/fixture.py +++ b/pytest_factoryboy/fixture.py @@ -260,7 +260,7 @@ def inject_into_caller(name: str, function: Callable[..., Any], locals_: Box[dic # 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_.value if is_class_or_function: - function = staticmethod(function) + function = staticmethod(function) # type: ignore[assignment] # python 3.9 quirk locals_.value[name] = function From 431dd02a80914fa9db2b585b982996717bedb824 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 28 Jun 2025 12:04:27 +0200 Subject: [PATCH 09/16] Alternative fix --- .github/workflows/main.yml | 2 +- pytest_factoryboy/fixture.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d0f447a..09f4d06 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,7 +36,7 @@ jobs: include: - python-version: "3.9" toxfactor: py3.9 - ignore-typecheck-outcome: false + ignore-typecheck-outcome: true ignore-test-outcome: false - python-version: "3.10" toxfactor: py3.10 diff --git a/pytest_factoryboy/fixture.py b/pytest_factoryboy/fixture.py index c8ce4e8..983d19c 100644 --- a/pytest_factoryboy/fixture.py +++ b/pytest_factoryboy/fixture.py @@ -260,7 +260,7 @@ def inject_into_caller(name: str, function: Callable[..., Any], locals_: Box[dic # 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_.value if is_class_or_function: - function = staticmethod(function) # type: ignore[assignment] # python 3.9 quirk + function = staticmethod(function) locals_.value[name] = function From e21b4de89ba3a89ea2f2a7bad4ff5d6dfdb168f7 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 28 Jun 2025 12:06:21 +0200 Subject: [PATCH 10/16] Add changelog entry --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 9a9c463..994bce2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -35,6 +35,7 @@ Added * Declare compatibility with python 3.13. Supported versions are now: 3.9, 3.10, 3.11, 3.12, 3.13. * Test against pytest 8.4 * Test against python 3.14 (beta) +* Run static type checks on all supported python version. Changed +++++++ @@ -51,6 +52,7 @@ Removed Fixed +++++ * Fix compatibility with ``pytest 8.4``. +* Fixed internal type annotations. Security ++++++++ From aed5ade2d8aea5a92305c7bf8a985ada790c15a7 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 28 Jun 2025 12:09:37 +0200 Subject: [PATCH 11/16] Remove mypy from tox, as it would be invoked in normal test runs too --- .github/workflows/main.yml | 2 +- tox.ini | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 09f4d06..1dbb463 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -92,7 +92,7 @@ jobs: continue-on-error: ${{ matrix.ignore-typecheck-outcome }} run: | source .venv/bin/activate - tox -f "${{ matrix.toxfactor }}-mypy" + mypy pytest_factoryboy - name: Test with tox continue-on-error: ${{ matrix.ignore-test-outcome }} run: | diff --git a/tox.ini b/tox.ini index e4ecd81..3fade08 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,6 @@ distshare = {homedir}/.tox/distshare 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} py{3.9,3.10,3.11}-pytest{7.0,7.1,7.2} - py{3.9,3.10,3.11,3.12,3.13,3.14}-mypy [testenv] parallel_show_output = true @@ -25,11 +24,5 @@ deps = coverage[toml] - -[testenv:py{3.9,3.10,3.11,3.12,3.13,3.14,3.15,3.16,3.17,3.18,3.19}-mypy] -allowlist_externals = poetry -commands_pre = poetry sync --no-root --only=dev -commands = poetry run mypy {posargs:pytest_factoryboy} - [pytest] addopts = -vv -l From 837efc432132b241b6130354888c0e925646ec01 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 28 Jun 2025 12:23:51 +0200 Subject: [PATCH 12/16] Enable failure for type checking only for the latest supported python --- .github/workflows/main.yml | 8 ++++---- CHANGES.rst | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1dbb463..48fd933 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,15 +40,15 @@ jobs: ignore-test-outcome: false - python-version: "3.10" toxfactor: py3.10 - ignore-typecheck-outcome: false + ignore-typecheck-outcome: true ignore-test-outcome: false - python-version: "3.11" toxfactor: py3.11 - ignore-typecheck-outcome: false + ignore-typecheck-outcome: true ignore-test-outcome: false - python-version: "3.12" toxfactor: py3.12 - ignore-typecheck-outcome: false + ignore-typecheck-outcome: true ignore-test-outcome: false - python-version: "3.13" toxfactor: py3.13 @@ -56,7 +56,7 @@ jobs: ignore-test-outcome: false - python-version: "3.14" toxfactor: py3.14 - ignore-typecheck-outcome: false + ignore-typecheck-outcome: true # TODO: Set to true once python3.14 is stable ignore-test-outcome: false steps: - uses: actions/checkout@v4 diff --git a/CHANGES.rst b/CHANGES.rst index 994bce2..bd67f79 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -35,7 +35,7 @@ Added * Declare compatibility with python 3.13. Supported versions are now: 3.9, 3.10, 3.11, 3.12, 3.13. * Test against pytest 8.4 * Test against python 3.14 (beta) -* Run static type checks on all supported python version. +* Run static type checks (only using the last stable python version). Changed +++++++ From 84038037544d2bf0f953019ac4948071ba83dd57 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 28 Jun 2025 13:08:18 +0200 Subject: [PATCH 13/16] Install the whole package, including pytest, otherwise we can't run type-checks --- .github/workflows/main.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 48fd933..d9c628d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,15 +40,15 @@ jobs: ignore-test-outcome: false - python-version: "3.10" toxfactor: py3.10 - ignore-typecheck-outcome: true + ignore-typecheck-outcome: false ignore-test-outcome: false - python-version: "3.11" toxfactor: py3.11 - ignore-typecheck-outcome: true + ignore-typecheck-outcome: false ignore-test-outcome: false - python-version: "3.12" toxfactor: py3.12 - ignore-typecheck-outcome: true + ignore-typecheck-outcome: false ignore-test-outcome: false - python-version: "3.13" toxfactor: py3.13 @@ -56,7 +56,7 @@ jobs: ignore-test-outcome: false - python-version: "3.14" toxfactor: py3.14 - ignore-typecheck-outcome: true # TODO: Set to true once python3.14 is stable + ignore-typecheck-outcome: false ignore-test-outcome: false steps: - uses: actions/checkout@v4 @@ -71,7 +71,7 @@ jobs: python -m pip install poetry==2.0.0 - name: Configure poetry run: | - python -m poetry config virtualenvs.in-project true + poetry config virtualenvs.in-project true - name: Cache the virtualenv id: poetry-dependencies-cache uses: actions/cache@v3 @@ -81,7 +81,7 @@ jobs: - name: Install dev dependencies if: steps.poetry-dependencies-cache.outputs.cache-hit != 'true' run: | - python -m poetry install --only=dev + poetry install - name: Download artifact uses: actions/download-artifact@v4 with: @@ -91,8 +91,7 @@ jobs: # Ignore errors for older pythons continue-on-error: ${{ matrix.ignore-typecheck-outcome }} run: | - source .venv/bin/activate - mypy pytest_factoryboy + poetry run mypy pytest_factoryboy - name: Test with tox continue-on-error: ${{ matrix.ignore-test-outcome }} run: | From 9bd656cafbd738a9ffc69f947f67ce8babd7fe4c Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 28 Jun 2025 13:08:34 +0200 Subject: [PATCH 14/16] Fix indentation --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d9c628d..391d243 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -97,9 +97,9 @@ jobs: run: | source .venv/bin/activate coverage erase - # Using `--parallel 4` as it's the number of CPUs in the GitHub Actions runner - # Using `installpkg dist/*.whl` because we want to install the pre-built package (want to test against that) - tox run-parallel -f ${{ matrix.toxfactor }} --parallel 4 --parallel-no-spinner --parallel-live --installpkg dist/*.whl + # Using `--parallel 4` as it's the number of CPUs in the GitHub Actions runner + # Using `installpkg dist/*.whl` because we want to install the pre-built package (want to test against that) + tox run-parallel -f ${{ matrix.toxfactor }} --parallel 4 --parallel-no-spinner --parallel-live --installpkg dist/*.whl coverage combine coverage xml - uses: codecov/codecov-action@v4 From 83b9ac3cab33524194f57dccb5b0e26167e42a30 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 28 Jun 2025 13:17:10 +0200 Subject: [PATCH 15/16] Add current dir to the cache key --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 391d243..04cfc9f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -77,7 +77,7 @@ jobs: uses: actions/cache@v3 with: path: ./.venv - key: ${{ runner.os }}-venv-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + key: ${{ runner.os }}-venv-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}${{ hashFiles('.github/workflows/**') }} - name: Install dev dependencies if: steps.poetry-dependencies-cache.outputs.cache-hit != 'true' run: | From 560f0025683bbd99dbaf1f6c4caecba13885d584 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 28 Jun 2025 13:23:18 +0200 Subject: [PATCH 16/16] Update changelog --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index bd67f79..1107c2d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -35,7 +35,7 @@ Added * Declare compatibility with python 3.13. Supported versions are now: 3.9, 3.10, 3.11, 3.12, 3.13. * Test against pytest 8.4 * Test against python 3.14 (beta) -* Run static type checks (only using the last stable python version). +* Run static type checks. Changed +++++++