From e37b0f6b3bae0956e95d061f9bb781cee07c81d0 Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:30:35 +0000 Subject: [PATCH 1/4] Keep the type check green under pyrefly 1.4.0 pyrefly 1.4.0 (released today) adds method-assign and unknown-attribute-access checks, and the template pins pyrefly with no ceiling, so every project rendered by CI now fails. - app.openapi = partial(custom_openapi, app) is FastAPI's documented way of installing a custom schema generator; the app comes from FastAPIOffline, a factory that returns a plain FastAPI, so there is no class to subclass. Suppressed with a comment. - win_service.py subclasses pywin32's untyped ServiceFramework. The types-pywin32 stubs now type it on every platform: unknown-attribute-access goes away, the import suppressions gain missing-source (the stubs exist on Linux, the package does not), and SvcRun carries @override because the base class is now known. The stubs ship in the dev group only when install_as_windows_service is set, like pywin32 itself. Co-Authored-By: Claude Fable 5.1 --- .../pyproject.toml.jinja | 3 ++- ...tall_as_windows_service %}win_service.py{% endif %} | 10 ++++++---- .../src/backend_api/fast_api_exception_handlers.py | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/template/{% if has_backend %}backend{% endif %}/pyproject.toml.jinja b/template/{% if has_backend %}backend{% endif %}/pyproject.toml.jinja index cee2077bd..e68e0ffc6 100644 --- a/template/{% if has_backend %}backend{% endif %}/pyproject.toml.jinja +++ b/template/{% if has_backend %}backend{% endif %}/pyproject.toml.jinja @@ -52,7 +52,8 @@ dev = [ "pytest-asyncio{% endraw %}{{ pytest_asyncio_version }}{% raw %}",{% endraw %}{% if backend_makes_api_calls %}{% raw %} "pytest-recording{% endraw %}{{ pytest_recording_version }}{% raw %}", "vcrpy{% endraw %}{{ vcrpy_version }}{% raw %}",{% endraw %}{% endif %}{% raw %}{% endraw %}{% if deploy_as_executable %}{% raw %} - "pyinstaller{% endraw %}{{ pyinstaller_version }}{% raw %}",{% endraw %}{% endif %}{% raw %}{% endraw %}{% if is_circuit_python_driver %}{% raw %} + "pyinstaller{% endraw %}{{ pyinstaller_version }}{% raw %}",{% endraw %}{% endif %}{% raw %}{% endraw %}{% if install_as_windows_service %}{% raw %} + "types-pywin32>=312.0.0.20261006", # pywin32 itself only installs on Windows; the stubs type win_service.py on every platform{% endraw %}{% endif %}{% raw %}{% endraw %}{% if is_circuit_python_driver %}{% raw %} "pytest-reserial{% endraw %}{{ pytest_reserial_version }}{% raw %}", "circup>=3.0.1", "adafruit-circuitpython-busdevice==5.2.17", # no specific reason for this version, just pinning to ensure no unintended drift during firmware builds diff --git a/template/{% if has_backend %}backend{% endif %}/src/backend_api/entrypoint/{% if install_as_windows_service %}win_service.py{% endif %} b/template/{% if has_backend %}backend{% endif %}/src/backend_api/entrypoint/{% if install_as_windows_service %}win_service.py{% endif %} index c33786924..36d98164a 100644 --- a/template/{% if has_backend %}backend{% endif %}/src/backend_api/entrypoint/{% if install_as_windows_service %}win_service.py{% endif %} +++ b/template/{% if has_backend %}backend{% endif %}/src/backend_api/entrypoint/{% if install_as_windows_service %}win_service.py{% endif %} @@ -5,12 +5,13 @@ import threading import traceback from collections.abc import Sequence from pathlib import Path +from typing import override # pywin32 is win32-only, so the per-import missing-import ignores are required in CI/Linux but appear unnecessary on Windows dev machines. -import servicemanager # pyrefly: ignore[missing-import,unused-ignore] # pywin32 has no stubs; only importable on Windows -import win32service # pyrefly: ignore[missing-import,unused-ignore] # pywin32 has no stubs; only importable on Windows -import win32serviceutil # pyrefly: ignore[missing-import,unused-ignore] # pywin32 has no stubs; only importable on Windows -import winerror # pyrefly: ignore[missing-import,unused-ignore] # pywin32 has no stubs; only importable on Windows +import servicemanager # pyrefly: ignore[missing-import,missing-source,unused-ignore] # pywin32 only installs on Windows; its types-pywin32 stubs type it on every platform +import win32service # pyrefly: ignore[missing-import,missing-source,unused-ignore] # pywin32 only installs on Windows; its types-pywin32 stubs type it on every platform +import win32serviceutil # pyrefly: ignore[missing-import,missing-source,unused-ignore] # pywin32 only installs on Windows; its types-pywin32 stubs type it on every platform +import winerror # pyrefly: ignore[missing-import,missing-source,unused-ignore] # pywin32 only installs on Windows; its types-pywin32 stubs type it on every platform from .. import app_runner from ..jinja_constants import APP_NAME @@ -43,6 +44,7 @@ class AppService(win32serviceutil.ServiceFramework): if self._worker.is_alive(): servicemanager.LogErrorMsg(f"{APP_NAME}: worker did not stop within 30s; SCM will force-terminate") + @override def SvcRun(self) -> None: # noqa: N802 # pywin32 mandates this exact method name # Outermost guard pywin32 hands control to. On a slow SCM startup the status handle can already # be invalid by the time ReportServiceStatus runs, so a crash anywhere — startup, status diff --git a/template/{% if has_backend %}backend{% endif %}/src/backend_api/fast_api_exception_handlers.py b/template/{% if has_backend %}backend{% endif %}/src/backend_api/fast_api_exception_handlers.py index 43d89fde2..b7b8ee721 100644 --- a/template/{% if has_backend %}backend{% endif %}/src/backend_api/fast_api_exception_handlers.py +++ b/template/{% if has_backend %}backend{% endif %}/src/backend_api/fast_api_exception_handlers.py @@ -267,4 +267,4 @@ def problem_content() -> dict[str, JsonValue]: def register_exception_handlers(app: FastAPI) -> None: handler = ExceptionHandler(app) handler.register() - app.openapi = partial(custom_openapi, app) + app.openapi = partial(custom_openapi, app) # pyrefly: ignore[method-assign] # replacing app.openapi is how FastAPI documents installing a custom schema generator From c767e7a392f0ce9e69f028f7fd7105beef74d884 Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:35:36 +0000 Subject: [PATCH 2/4] Drop the noqa that @override makes unused ruff exempts @override methods from N802, so the directive became an RUF100 error. Co-Authored-By: Claude Fable 5.1 --- ...{% if install_as_windows_service %}win_service.py{% endif %} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/{% if has_backend %}backend{% endif %}/src/backend_api/entrypoint/{% if install_as_windows_service %}win_service.py{% endif %} b/template/{% if has_backend %}backend{% endif %}/src/backend_api/entrypoint/{% if install_as_windows_service %}win_service.py{% endif %} index 36d98164a..3e9eef816 100644 --- a/template/{% if has_backend %}backend{% endif %}/src/backend_api/entrypoint/{% if install_as_windows_service %}win_service.py{% endif %} +++ b/template/{% if has_backend %}backend{% endif %}/src/backend_api/entrypoint/{% if install_as_windows_service %}win_service.py{% endif %} @@ -45,7 +45,7 @@ class AppService(win32serviceutil.ServiceFramework): servicemanager.LogErrorMsg(f"{APP_NAME}: worker did not stop within 30s; SCM will force-terminate") @override - def SvcRun(self) -> None: # noqa: N802 # pywin32 mandates this exact method name + def SvcRun(self) -> None: # pywin32 mandates this exact method name # Outermost guard pywin32 hands control to. On a slow SCM startup the status handle can already # be invalid by the time ReportServiceStatus runs, so a crash anywhere — startup, status # reporting, or SvcDoRun itself — could escape before any dump was written. Write the dump as a From 25baa8aa69b7f8ff45bded9fe41cc3cc4c62d94d Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:36:38 +0000 Subject: [PATCH 3/4] Drop the comment that only explained the removed noqa Co-Authored-By: Claude Fable 5.1 --- ...{% if install_as_windows_service %}win_service.py{% endif %} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/{% if has_backend %}backend{% endif %}/src/backend_api/entrypoint/{% if install_as_windows_service %}win_service.py{% endif %} b/template/{% if has_backend %}backend{% endif %}/src/backend_api/entrypoint/{% if install_as_windows_service %}win_service.py{% endif %} index 3e9eef816..62fec994e 100644 --- a/template/{% if has_backend %}backend{% endif %}/src/backend_api/entrypoint/{% if install_as_windows_service %}win_service.py{% endif %} +++ b/template/{% if has_backend %}backend{% endif %}/src/backend_api/entrypoint/{% if install_as_windows_service %}win_service.py{% endif %} @@ -45,7 +45,7 @@ class AppService(win32serviceutil.ServiceFramework): servicemanager.LogErrorMsg(f"{APP_NAME}: worker did not stop within 30s; SCM will force-terminate") @override - def SvcRun(self) -> None: # pywin32 mandates this exact method name + def SvcRun(self) -> None: # Outermost guard pywin32 hands control to. On a slow SCM startup the status handle can already # be invalid by the time ReportServiceStatus runs, so a crash anywhere — startup, status # reporting, or SvcDoRun itself — could escape before any dump was written. Write the dump as a From 88dc53b4e3644c5b3a62f2f104a6fc44aaf6a7b6 Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:41:51 +0000 Subject: [PATCH 4/4] Hand the mocker to the strawberry test helper instead of assuming a subclass fixture set it pyrefly 1.4.0 reports the class-level `mocker: MockerFixture` declaration as never initialized, which is accurate: only the subclasses' autouse fixtures assign it. The helper now takes the fixture it needs. Co-Authored-By: Claude Fable 5.1 --- ...st_strawberry_exception_handler.py{% endif %} | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/template/{% if has_backend %}backend{% endif %}/tests/unit/{% if backend_uses_graphql %}test_strawberry_exception_handler.py{% endif %} b/template/{% if has_backend %}backend{% endif %}/tests/unit/{% if backend_uses_graphql %}test_strawberry_exception_handler.py{% endif %} index 571bfd1dd..8d9a96076 100644 --- a/template/{% if has_backend %}backend{% endif %}/tests/unit/{% if backend_uses_graphql %}test_strawberry_exception_handler.py{% endif %} +++ b/template/{% if has_backend %}backend{% endif %}/tests/unit/{% if backend_uses_graphql %}test_strawberry_exception_handler.py{% endif %} @@ -48,10 +48,8 @@ GQL_BOTH = """query ($value1: String!, $value2: Int!) { class Helper: - mocker: MockerFixture - - def _mock_should_show_error_details(self, *, should_show: bool): - _ = self.mocker.patch.object( + def _mock_should_show_error_details(self, *, mocker: MockerFixture, should_show: bool): + _ = mocker.patch.object( fast_api_exception_handlers, fast_api_exception_handlers.should_show_error_details.__name__, autospec=True, @@ -70,7 +68,7 @@ class TestSingleError(Helper): def test_Given_resolver_internal_mocked_to_error_and_error_details_should_be_displayed__Then_uuid_in_log_and_response__and_details_in_response_and_log( self, ): - self._mock_should_show_error_details(should_show=True) + self._mock_should_show_error_details(mocker=self.mocker, should_show=True) expected_error_message = str(uuid4()) error_cls = random.choice( (ValueError, RuntimeError, KeyError, TypeError) @@ -106,7 +104,7 @@ class TestSingleError(Helper): def test_Given_resolver_returns_wrong_type_for_second_query_and_error_details_should_not_be_displayed__Then_uuid_in_log_and_response__and_details_in_response_and_log_because_this_isnt_sensitive( self, ): - self._mock_should_show_error_details(should_show=False) + self._mock_should_show_error_details(mocker=self.mocker, should_show=False) wrong_value_1 = str(uuid4()) _ = self.mocker.patch(f"{__name__}.{_run_other_logic.__name__}", autospec=True, return_value=wrong_value_1) @@ -135,7 +133,7 @@ class TestSingleError(Helper): def test_Given_resolver_returns_null_value_for_second_query_not_matching_schema_and_error_details_should_not_be_displayed__Then_uuid_in_log_and_response__and_details_in_response_and_log_because_this_isnt_sensitive( self, ): - self._mock_should_show_error_details(should_show=False) + self._mock_should_show_error_details(mocker=self.mocker, should_show=False) wrong_value_1 = None _ = self.mocker.patch(f"{__name__}.{_run_other_logic.__name__}", autospec=True, return_value=wrong_value_1) @@ -164,7 +162,7 @@ class TestSingleError(Helper): def test_Given_invalid_input_for_second_query_and_error_details_should_not_be_displayed__Then_uuid_in_log_and_response__and_details_in_response_and_log_because_this_isnt_sensitive( self, ): - self._mock_should_show_error_details(should_show=False) + self._mock_should_show_error_details(mocker=self.mocker, should_show=False) wrong_value_2 = random.choice([True, False]) result = schema.execute_sync( @@ -202,7 +200,7 @@ class TestExceptionHandlersForMultipleErrors(Helper): def test_Given_invalid_input_for_both_queries_and_error_details_should_not_be_displayed__Then_uuid_in_log_and_response__and_details_in_response_and_log_because_this_isnt_sensitive( self, ): - self._mock_should_show_error_details(should_show=False) + self._mock_should_show_error_details(mocker=self.mocker, should_show=False) wrong_value_1 = random.randint(1, 100) wrong_value_2 = random.choice([True, False]) expected_num_errors = 2