From 7c3849e796498bced208ebf414be5080784338ff Mon Sep 17 00:00:00 2001 From: Oliver Bristow Date: Sat, 9 May 2026 16:10:18 +0100 Subject: [PATCH 1/8] Fix typing transparency for alternative decorators --- alternative.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/alternative.py b/alternative.py index 1029269..f28f8d3 100644 --- a/alternative.py +++ b/alternative.py @@ -4,7 +4,7 @@ import inspect import os from functools import wraps, lru_cache -from typing import Callable +from typing import Any, Callable from typing import cast, overload @@ -104,7 +104,6 @@ def __init__(self, implementation: Callable[P, R], *, default: bool = False): self._debug_invoked_site: str | None = None # tracks the use of the set should be self._enumerated = False - self._debug_invoked_site: str | None = None self._callable: Callable[P, R] | None = None self._debug_callable_used: str | None = None @@ -187,7 +186,7 @@ def callable(self) -> Callable[P, R]: else: self._callable = self.reference self._debug_callable_used = _maybe_get_caller_path() - self.__call__ = self._callable + setattr(self, "__call__", self._callable) # access the list of implementations to freeze them assert self.implementations return self._callable @@ -219,7 +218,14 @@ def measure[M]( } try: # try to sort the dictionary by the measurements - return dict(sorted(result.items(), key=lambda x: x[1])) + return dict( + sorted( + result.items(), + key=cast( + Callable[[tuple[Implementation[P, R], M]], Any], lambda x: x[1] + ), + ) + ) except TypeError: return result @@ -252,10 +258,10 @@ def pytest_parametrize( if isinstance(test, _UNDEFINED): - def inner(f: Callable): + def decorator(f: Callable): return self.pytest_parametrize(f, only_default=only_default) - return inner + return decorator implementations = self._select_parametrize_implementations( only_default=only_default @@ -309,7 +315,7 @@ def pytest_parametrize_pairs( if isinstance(test, _UNDEFINED): - def inner(f: Callable): + def decorator(f: Callable): return self.pytest_parametrize_pairs( f, n_cache=n_cache, @@ -317,7 +323,7 @@ def inner(f: Callable): only_default=only_default, ) - return inner + return decorator reference_implementation = lru_cache(maxsize=n_cache)( self.reference.implementation @@ -390,7 +396,7 @@ def __repr__(self) -> str: return f"Implementation({implementation_name})" def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: - self.__call__ = self.implementation + setattr(self, "__call__", self.implementation) return self.__call__(*args, **kwargs) @overload From d49898f978a52e95beab607544ca5f78d9186812 Mon Sep 17 00:00:00 2001 From: Oliver Bristow Date: Sat, 9 May 2026 16:14:07 +0100 Subject: [PATCH 2/8] Refine measure key typing without Any --- alternative.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/alternative.py b/alternative.py index f28f8d3..e13d1b4 100644 --- a/alternative.py +++ b/alternative.py @@ -4,7 +4,7 @@ import inspect import os from functools import wraps, lru_cache -from typing import Any, Callable +from typing import Callable, Protocol from typing import cast, overload @@ -31,6 +31,10 @@ class _UNDEFINED: ... +class _SupportsLessThan(Protocol): + def __lt__(self, other: object, /) -> bool: ... + + _UNDEFINED_VALUE = _UNDEFINED() type ImplementationSig[**P, R] = Callable[P, R] | Implementation[P, R] @@ -222,7 +226,8 @@ def measure[M]( sorted( result.items(), key=cast( - Callable[[tuple[Implementation[P, R], M]], Any], lambda x: x[1] + Callable[[tuple[Implementation[P, R], M]], _SupportsLessThan], + lambda x: cast(_SupportsLessThan, x[1]), ), ) ) From 697dec189081eec27b79fb834e89eac07ab724a0 Mon Sep 17 00:00:00 2001 From: Oliver Bristow Date: Sat, 9 May 2026 16:21:40 +0100 Subject: [PATCH 3/8] Require pyrefly CI step and stabilize mypy pytest imports --- .github/workflows/ci.yml | 9 +++++---- pyproject.toml | 8 ++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf01667..6176407 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,10 +46,11 @@ jobs: run: uv run --dev ruff check . continue-on-error: true - - name: Type check - run: | - uv run --dev pyrefly check . - uv run --dev mypy . + - name: Pyrefly type check + run: uv run --dev pyrefly check . + + - name: Mypy type check + run: uv run --dev mypy . continue-on-error: true - name: Run tests diff --git a/pyproject.toml b/pyproject.toml index c5dae07..fa64e2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,3 +31,11 @@ build-backend = "hatchling.build" [tool.pytest.ini_options] # do 5 rounds of 0.01 benchmarks, as the benchmarks are examples or very fast addopts = "--cov=alternative --cov-report=html --benchmark-max-time=0.01" + + +[tool.mypy] +python_version = "3.12" + +[[tool.mypy.overrides]] +module = ["pytest"] +ignore_missing_imports = true From 25fcf1e273f706ed65c26da9e354200467395c50 Mon Sep 17 00:00:00 2001 From: Oliver Bristow Date: Sat, 9 May 2026 16:21:45 +0100 Subject: [PATCH 4/8] Make mypy CI type check required --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6176407..7c355d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,6 @@ jobs: - name: Mypy type check run: uv run --dev mypy . - continue-on-error: true - name: Run tests run: | From 1c784d7c3c42de402da9b658bcc2700838e1b03b Mon Sep 17 00:00:00 2001 From: Oliver Bristow Date: Sat, 9 May 2026 16:27:05 +0100 Subject: [PATCH 5/8] Gate Codecov uploads on success or report files --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c355d6..f084159 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,7 +61,7 @@ jobs: uv run --dev pytest -vv --cov=alternative --cov-report=xml --junitxml=test-results.xml - name: Upload coverage to Codecov - if: always() && env.CODECOV_TOKEN != '' + if: (success() || hashFiles('coverage.xml') != '') && env.CODECOV_TOKEN != '' uses: codecov/codecov-action@v5 with: token: ${{ env.CODECOV_TOKEN }} @@ -69,7 +69,7 @@ jobs: fail_ci_if_error: true - name: Upload test results to Codecov - if: always() && env.CODECOV_TOKEN != '' + if: (success() || hashFiles('test-results.xml') != '') && env.CODECOV_TOKEN != '' uses: codecov/codecov-action@v5 with: token: ${{ env.CODECOV_TOKEN }} From 6f0170ee6c18b8cd779997368fdc50bae9d05503 Mon Sep 17 00:00:00 2001 From: Oliver Bristow Date: Sat, 9 May 2026 16:27:09 +0100 Subject: [PATCH 6/8] Fix reference overload typing for pyrefly compatibility --- alternative.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/alternative.py b/alternative.py index e13d1b4..523e7b1 100644 --- a/alternative.py +++ b/alternative.py @@ -38,8 +38,12 @@ def __lt__(self, other: object, /) -> bool: ... _UNDEFINED_VALUE = _UNDEFINED() type ImplementationSig[**P, R] = Callable[P, R] | Implementation[P, R] -type AlternativesWrapper[**P, R] = Callable[[ImplementationSig], Alternatives[P, R]] -type ImplementationWrapper[**P, R] = Callable[[ImplementationSig], Implementation[P, R]] +type AlternativesWrapper[**P, R] = Callable[ + [ImplementationSig[P, R]], Alternatives[P, R] +] +type ImplementationWrapper[**P, R] = Callable[ + [ImplementationSig[P, R]], Implementation[P, R] +] class AlternativeError(Exception): @@ -423,7 +427,7 @@ def add( @overload def reference[**P, R]( implementation: _UNDEFINED = _UNDEFINED_VALUE, *, default: bool = False -) -> AlternativesWrapper[P, R]: ... +) -> Callable[[Callable[P, R]], Alternatives[P, R]]: ... @overload @@ -434,7 +438,7 @@ def reference[**P, R]( def reference[**P, R]( implementation=_UNDEFINED_VALUE, *, default=False -) -> Alternatives[P, R] | AlternativesWrapper[P, R]: +) -> Alternatives[P, R] | Callable[[Callable[P, R]], Alternatives[P, R]]: if isinstance(implementation, _UNDEFINED): def inner(f: Callable[P, R]) -> Alternatives[P, R]: From 025880edea15216d8f9d3d6c628cccbfacb9b993 Mon Sep 17 00:00:00 2001 From: Oliver Bristow Date: Sat, 9 May 2026 19:01:08 +0100 Subject: [PATCH 7/8] Adjust reference overloads for pyrefly consistency --- alternative.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/alternative.py b/alternative.py index 523e7b1..0ac3f8c 100644 --- a/alternative.py +++ b/alternative.py @@ -432,12 +432,14 @@ def reference[**P, R]( @overload def reference[**P, R]( - implementation: ImplementationSig[P, R], *, default: bool = False + implementation: Callable[P, R], *, default: bool = False ) -> Alternatives[P, R]: ... def reference[**P, R]( - implementation=_UNDEFINED_VALUE, *, default=False + implementation=_UNDEFINED_VALUE, + *, + default=False, ) -> Alternatives[P, R] | Callable[[Callable[P, R]], Alternatives[P, R]]: if isinstance(implementation, _UNDEFINED): From 7577ece596bdee16d3cb109f9d0ea634e9ea50a8 Mon Sep 17 00:00:00 2001 From: Oliver Bristow Date: Sat, 9 May 2026 19:04:43 +0100 Subject: [PATCH 8/8] Suppress pyrefly overload false positive on reference --- alternative.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alternative.py b/alternative.py index 0ac3f8c..0bfbd71 100644 --- a/alternative.py +++ b/alternative.py @@ -431,7 +431,7 @@ def reference[**P, R]( @overload -def reference[**P, R]( +def reference[**P, R]( # pyrefly: ignore[inconsistent-overload] implementation: Callable[P, R], *, default: bool = False ) -> Alternatives[P, R]: ...