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..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 %} @@ -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,7 +44,8 @@ class AppService(win32serviceutil.ServiceFramework): if self._worker.is_alive(): servicemanager.LogErrorMsg(f"{APP_NAME}: worker did not stop within 30s; SCM will force-terminate") - def SvcRun(self) -> None: # noqa: N802 # pywin32 mandates this exact method name + @override + 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 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 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