From 7907e4b47cd202ac77e2626f308d66f063eb0371 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Thu, 12 Jun 2025 16:30:24 -0700 Subject: [PATCH] Disallow untyped def in most files --- pyproject.toml | 16 +++++++- pytest_pyodide/_decorator_in_pyodide.py | 5 ++- pytest_pyodide/config.py | 14 ++++--- pytest_pyodide/copy_files_to_pyodide.py | 15 +++++--- pytest_pyodide/decorator.py | 43 ++++++++++++++-------- pytest_pyodide/doctest.py | 3 +- pytest_pyodide/hook.py | 7 ++-- pytest_pyodide/hypothesis.py | 11 +++--- pytest_pyodide/run_tests_inside_pyodide.py | 16 +++++--- pytest_pyodide/runner.py | 5 ++- pytest_pyodide/utils.py | 13 +++++-- utils/build_test_matrix.py | 6 +-- 12 files changed, 100 insertions(+), 54 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c4e24444..64e466a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,8 +59,8 @@ check_untyped_defs = true disallow_any_generics = true disallow_subclassing_any = true disallow_untyped_calls = false -disallow_untyped_defs = false -disallow_incomplete_defs = false +disallow_untyped_defs = true +disallow_incomplete_defs = true disallow_untyped_decorators = true no_implicit_optional = true warn_redundant_casts = true @@ -70,6 +70,18 @@ no_implicit_reexport = true strict_equality = true ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = [ + "pytest_pyodide.doctest", + "pytest_pyodide.fixture", + "pytest_pyodide.hook", + "pytest_pyodide.runner", + "pytest_pyodide.server", + "test_install_package" +] +disallow_untyped_defs = false + # this [tool.ruff] diff --git a/pytest_pyodide/_decorator_in_pyodide.py b/pytest_pyodide/_decorator_in_pyodide.py index 638c717b..302df0e2 100644 --- a/pytest_pyodide/_decorator_in_pyodide.py +++ b/pytest_pyodide/_decorator_in_pyodide.py @@ -20,6 +20,7 @@ from base64 import b64decode, b64encode from inspect import isclass from io import BytesIO +from types import FrameType from typing import Any import pyodide_js @@ -60,7 +61,7 @@ def persistent_id(self, obj: Any) -> Any: pyodide_js._module._Py_IncRef(obj.ptr) return ("PyodideHandle", obj.ptr) - def reducer_override(self, obj): + def reducer_override(self, obj: Any) -> Any: try: from _pytest.outcomes import OutcomeException @@ -125,7 +126,7 @@ async def run_in_pyodide_main( # If tblib is present, we can show much better tracebacks. from tblib import pickling_support - def get_locals(frame): + def get_locals(frame: FrameType) -> dict[str, Any]: result = {} tbhide = frame.f_locals.get("__tracebackhide__") if tbhide: diff --git a/pytest_pyodide/config.py b/pytest_pyodide/config.py index 0ed7c6b8..76ce6b12 100644 --- a/pytest_pyodide/config.py +++ b/pytest_pyodide/config.py @@ -13,7 +13,7 @@ class Config: - def __init__(self): + def __init__(self) -> None: # Flags to be passed to the browser or runtime. self.flags: dict[RUNTIMES, list[str]] = { "chrome": ["--js-flags=--expose-gc"], @@ -32,27 +32,29 @@ def __init__(self): # The script to be executed to initialize the runtime. self.initialize_script: str = "pyodide.runPython('');" - self.node_extra_globals = [] + self.node_extra_globals: list[str] = [] - def set_flags(self, runtime: RUNTIMES, flags: list[str]): + def set_flags(self, runtime: RUNTIMES, flags: list[str]) -> None: self.flags[runtime] = flags def get_flags(self, runtime: RUNTIMES) -> list[str]: return self.flags[runtime] - def set_load_pyodide_script(self, runtime: RUNTIMES, load_pyodide_script: str): + def set_load_pyodide_script( + self, runtime: RUNTIMES, load_pyodide_script: str + ) -> None: self.load_pyodide_script[runtime] = load_pyodide_script def get_load_pyodide_script(self, runtime: RUNTIMES) -> str: return self.load_pyodide_script[runtime] - def set_initialize_script(self, initialize_script: str): + def set_initialize_script(self, initialize_script: str) -> None: self.initialize_script = initialize_script def get_initialize_script(self) -> str: return self.initialize_script - def add_node_extra_globals(self, l: Iterable[str]): + def add_node_extra_globals(self, l: Iterable[str]) -> None: self.node_extra_globals.extend(l) def get_node_extra_globals(self) -> Sequence[str]: diff --git a/pytest_pyodide/copy_files_to_pyodide.py b/pytest_pyodide/copy_files_to_pyodide.py index 72ae2744..96e85910 100644 --- a/pytest_pyodide/copy_files_to_pyodide.py +++ b/pytest_pyodide/copy_files_to_pyodide.py @@ -1,18 +1,21 @@ from collections.abc import MutableSequence, Sequence from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .runner import _BrowserBaseRunner from .server import spawn_web_server -_copied_files: dict[Any, MutableSequence[tuple[Path, str]]] = {} +_copied_files: dict["_BrowserBaseRunner", MutableSequence[tuple[Path, str]]] = {} def copy_files_to_emscripten_fs( file_list: Sequence[Path | str | tuple[Path | str, Path | str]], - selenium: Any, - install_wheels=True, - recurse_directories=True, -): + selenium: "_BrowserBaseRunner", + install_wheels: bool = True, + recurse_directories: bool = True, +) -> None: """ Copies files in file_list to the emscripten file system. Files are passed as a list of source Path / install Path pairs. diff --git a/pytest_pyodide/decorator.py b/pytest_pyodide/decorator.py index dcbf365b..94f15ee3 100644 --- a/pytest_pyodide/decorator.py +++ b/pytest_pyodide/decorator.py @@ -3,20 +3,23 @@ import pickle import sys from base64 import b64decode, b64encode -from collections.abc import Callable, Collection +from collections.abc import Callable, Collection, Sequence from copy import deepcopy from io import BytesIO -from typing import Any, Protocol +from pathlib import Path +from typing import TYPE_CHECKING, Any, ParamSpec, Protocol, TypeVar from .copy_files_to_pyodide import copy_files_to_emscripten_fs from .hook import ORIGINAL_MODULE_ASTS, REWRITTEN_MODULE_ASTS, pytest_wrapper -from .runner import _BrowserBaseRunner from .utils import package_is_built as _package_is_built +if TYPE_CHECKING: + from .runner import _BrowserBaseRunner + MaybeAsyncFuncDef = ast.FunctionDef | ast.AsyncFunctionDef -def package_is_built(package_name: str): +def package_is_built(package_name: str) -> bool: return _package_is_built(package_name, pytest_wrapper.pyodide_dist_dir) @@ -24,13 +27,13 @@ class SeleniumType(Protocol): JavascriptException: type browser: str - def load_package(self, pkgs: str | list[str]): + def load_package(self, pkgs: str | list[str]) -> None: ... - def run_async(self, code: str): + def run_async(self, code: str) -> Any: ... - def run_js(self, code: str): + def run_js(self, code: str) -> Any: ... @@ -78,7 +81,7 @@ def __init__(self, selenium: SeleniumType, ptr: int): self.selenium = selenium self.ptr: int | None = ptr - def __del__(self): + def __del__(self) -> None: if self.ptr is None: return ptr = self.ptr @@ -261,7 +264,7 @@ def (, arg1, arg2, arg3): # This will show: # > run(selenium_arg_name, (arg1, arg2, ...)) # in the traceback. - def fake_body_for_traceback(arg1, arg2, selenium_arg_name): + def fake_body_for_traceback(arg1: Any, arg2: Any, selenium_arg_name: str) -> None: run(selenium_arg_name, (arg1, arg2, ...)) # Adjust line numbers to point into our fake function @@ -280,7 +283,7 @@ def fake_body_for_traceback(arg1, arg2, selenium_arg_name): return globs[funcdef.name] # type: ignore[no-any-return] -def initialize_decorator(selenium): +def initialize_decorator(selenium: "_BrowserBaseRunner") -> None: from pathlib import Path _decorator_in_pyodide = ( @@ -364,7 +367,9 @@ def _locate_funcdef( class run_in_pyodide: - def __new__(cls, function: Callable[..., Any] | None = None, /, **kwargs): + def __new__( + cls, function: Callable[..., Any] | None = None, /, **kwargs: Any + ) -> Any: if function: # Probably we were used like: # @@ -442,7 +447,7 @@ def __call__(self, f: Callable[..., Any]) -> Callable[..., Any]: self._async_func = isinstance(funcdef, ast.AsyncFunctionDef) return wrapper - def _run(self, selenium: SeleniumType, args: tuple[Any, ...]): + def _run(self, selenium: SeleniumType, args: tuple[Any, ...]) -> Any: """The main runner, called from the AST generated in _create_outer_func.""" __tracebackhide__ = True code = self._code_template(args) @@ -485,12 +490,20 @@ async def __tmp(): """ -def copy_files_to_pyodide(file_list, install_wheels=True, recurse_directories=True): +T = TypeVar("T") +P = ParamSpec("P") + + +def copy_files_to_pyodide( + file_list: Sequence[Path | str | tuple[Path | str, Path | str]], + install_wheels: bool = True, + recurse_directories: bool = True, +) -> Callable[[Callable[P, T]], Callable[P, T]]: """A decorator that copies files across to pyodide""" - def wrap(fn): + def wrap(fn: Callable[P, T]) -> Callable[P, T]: @functools.wraps(fn) - def wrapped_f(*args, **argv): + def wrapped_f(*args: P.args, **argv: P.kwargs) -> T: # get selenium from args selenium = None for a in args: diff --git a/pytest_pyodide/doctest.py b/pytest_pyodide/doctest.py index 02fa4650..ae396bc1 100644 --- a/pytest_pyodide/doctest.py +++ b/pytest_pyodide/doctest.py @@ -3,6 +3,7 @@ from copy import copy from doctest import DocTest, DocTestRunner, register_optionflag from pathlib import Path +from typing import Any from _pytest.doctest import ( DoctestModule, @@ -115,7 +116,7 @@ def run_doctest_in_pyodide_outer( compileflags: int | None = None, out: Callable[[str], object] | None = None, clear_globs: bool = True, -): +) -> Any: if not getattr(test, "pyodide_test", None): # Run host test as normal return host_DocTestRunner_run(self, test, compileflags, out, clear_globs) diff --git a/pytest_pyodide/hook.py b/pytest_pyodide/hook.py index c95f61b8..b214e5f5 100644 --- a/pytest_pyodide/hook.py +++ b/pytest_pyodide/hook.py @@ -186,7 +186,7 @@ def set_runtime_fixture_params(session): rt[0].params = pytest_wrapper.pyodide_runtimes -def pytest_collection(session: Session): +def pytest_collection(session: Session) -> None: from .doctest import patch_doctest_runner patch_doctest_runner() @@ -194,7 +194,7 @@ def pytest_collection(session: Session): set_runtime_fixture_params(session) -def pytest_collect_file(file_path: Path, parent: Collector): +def pytest_collect_file(file_path: Path, parent: Collector) -> Any: # Have to set doctestmodules to False to prevent original hook from # triggering parent.config.option.doctestmodules = False @@ -258,7 +258,7 @@ def _has_standalone_fixture(item): return False -def modifyitems_run_in_pyodide(items: list[Any]): +def modifyitems_run_in_pyodide(items: list[Any]) -> None: # TODO: get rid of this # if we are running tests in pyodide, then run all tests for each runtime new_items = [] @@ -273,7 +273,6 @@ def modifyitems_run_in_pyodide(items: list[Any]): x.pyodide_runtime = runtime new_items.append(x) items[:] = new_items - return def pytest_collection_modifyitems(items: list[Any]) -> None: diff --git a/pytest_pyodide/hypothesis.py b/pytest_pyodide/hypothesis.py index 61bf8c56..cb491aa2 100644 --- a/pytest_pyodide/hypothesis.py +++ b/pytest_pyodide/hypothesis.py @@ -1,11 +1,12 @@ import io import pickle +from typing import Any from zoneinfo import ZoneInfo from hypothesis import HealthCheck, settings, strategies -def is_picklable(x): +def is_picklable(x: Any) -> bool: try: pickle.dumps(x) return True @@ -13,9 +14,9 @@ def is_picklable(x): return False -def is_equal_to_self(x): +def is_equal_to_self(x: Any) -> bool: try: - return x == x + return bool(x == x) except Exception: return False @@ -29,14 +30,14 @@ class ExceptionGroup: # type: ignore[no-redef] class NoHypothesisUnpickler(pickle.Unpickler): - def find_class(self, module, name): + def find_class(self, module: str, name: str) -> Any: # Only allow safe classes from builtins. if module == "hypothesis": raise pickle.UnpicklingError() return super().find_class(module, name) -def no_hypothesis(x): +def no_hypothesis(x: Any) -> bool: try: NoHypothesisUnpickler(io.BytesIO(pickle.dumps(x))).load() return True diff --git a/pytest_pyodide/run_tests_inside_pyodide.py b/pytest_pyodide/run_tests_inside_pyodide.py index e488e966..3a960507 100644 --- a/pytest_pyodide/run_tests_inside_pyodide.py +++ b/pytest_pyodide/run_tests_inside_pyodide.py @@ -4,7 +4,7 @@ from contextlib import AbstractContextManager from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Generic, TypeVar if TYPE_CHECKING: from .runner import _BrowserBaseRunner @@ -32,10 +32,10 @@ def __init__(self, ctx_manager: AbstractContextManager[T]): def get_value(self) -> T: return self.value - def __del__(self): + def __del__(self) -> None: self.close() - def close(self): + def close(self) -> None: if self.ctx_manager is not None: self.ctx_manager.__exit__(None, None, None) del self.value @@ -52,7 +52,9 @@ class _SeleniumInstance: _playwright_browser_generator = None -def get_browser_pyodide(request: pytest.FixtureRequest, runtime: str): +def get_browser_pyodide( + request: pytest.FixtureRequest, runtime: str +) -> "_BrowserBaseRunner": """Start a browser running with pyodide, ready to run pytest calls. If the same runtime is already running, it will just return that. @@ -110,7 +112,9 @@ def _remove_pytest_capture_title( return None -def run_test_in_pyodide(node_tree_id, selenium, ignore_fail=False): +def run_test_in_pyodide( + node_tree_id: Any, selenium: "_BrowserBaseRunner", ignore_fail: bool = False +) -> bool: """This runs a single test (identified by node_tree_id) inside the pyodide runtime. How it does it is by calling pytest on the browser pyodide with the full node ID, which is the same @@ -162,7 +166,7 @@ def run_test_in_pyodide(node_tree_id, selenium, ignore_fail=False): return True -def close_pyodide_browsers(): +def close_pyodide_browsers() -> None: """Close the browsers that are currently open with pyodide runtime initialised. diff --git a/pytest_pyodide/runner.py b/pytest_pyodide/runner.py index 49aed483..e2ab8518 100644 --- a/pytest_pyodide/runner.py +++ b/pytest_pyodide/runner.py @@ -90,7 +90,10 @@ class JavascriptException(Exception): - def __init__(self, msg, stack): + msg: str + stack: str + + def __init__(self, msg: str, stack: str): self.msg = msg self.stack = stack # In chrome the stack contains the message diff --git a/pytest_pyodide/utils.py b/pytest_pyodide/utils.py index 3350e20d..bc0e96e6 100644 --- a/pytest_pyodide/utils.py +++ b/pytest_pyodide/utils.py @@ -1,11 +1,18 @@ import contextlib import functools import json +from collections.abc import Iterator from pathlib import Path +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from .runner import _BrowserBaseRunner @contextlib.contextmanager -def set_webdriver_script_timeout(selenium, script_timeout: float | None): +def set_webdriver_script_timeout( + selenium: "_BrowserBaseRunner", script_timeout: float | None +) -> Iterator[None]: """Set selenium script timeout Parameters @@ -23,7 +30,7 @@ def set_webdriver_script_timeout(selenium, script_timeout: float | None): selenium.set_script_timeout(selenium.script_timeout) -def parse_driver_timeout(node) -> float | None: +def parse_driver_timeout(node: Any) -> float | None: """Parse driver timeout value from pytest request object""" mark = node.get_closest_marker("driver_timeout") if mark is None: @@ -32,7 +39,7 @@ def parse_driver_timeout(node) -> float | None: return mark.args[0] # type: ignore[no-any-return] -def parse_xfail_browsers(node) -> dict[str, str]: +def parse_xfail_browsers(node: Any) -> dict[str, str]: mark = node.get_closest_marker("xfail_browsers") if mark is None: return {} diff --git a/utils/build_test_matrix.py b/utils/build_test_matrix.py index 062138a9..a4473383 100644 --- a/utils/build_test_matrix.py +++ b/utils/build_test_matrix.py @@ -77,7 +77,7 @@ def _inject_versions_inner( return configs_with_versions -def inject_versions(config: TestConfig, args: dict[str, list[str]]): +def inject_versions(config: TestConfig, args: dict[str, list[str]]) -> list[TestConfig]: """ Add corresponding versions to test-config """ @@ -141,7 +141,7 @@ def build_configs(args: dict[str, list[str]]) -> list[TestConfig]: return remove_duplicate_configs(matrix) -def validate_args(args: dict[str, list[str]]): +def validate_args(args: dict[str, list[str]]) -> None: runners = args["runner"] for runner in runners: if runner not in ("selenium", "playwright"): @@ -204,7 +204,7 @@ def parse_args() -> dict[str, list[str]]: return args_dict -def main(): +def main() -> None: args = parse_args() configs: list[TestConfig] = build_configs(args)