From 148ea702ac52e3f09a336a02862e8686edae4863 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Mon, 17 Aug 2026 19:02:37 -0400 Subject: [PATCH] chore: faster and stricter mypy (#2968) * chore: faster mypy This makes mypy around 26% faster from a cold cache. Signed-off-by: Henry Schreiner * chore: enable more mypy error codes Turn on possibly-undefined, exhaustive-match, and other strict flags, and fix what they report. Two real bugs: pyodide never set `repaired_wheel` when it reused a compatible wheel, so the test step failed with a NameError; the OCI shell read loop spun forever if the shell exited. Assisted-by: ClaudeCode:claude-opus-5 * test: carry default OCI runtime into podman vfs test config Podman ignores its usual config files when CONTAINERS_CONF is set, so it falls back to the first runtime found on PATH. On current GitHub Ubuntu runner images that is crun 1.14.1, which cannot parse the OCI spec v1.2.x config that podman 5.8.4 generates ("crun: unknown version specified"). Query the default runtime path and set it in the generated config. Assisted-by: ClaudeCode:claude-fable-5 * fix: annotate vfs config dict so mypy passes on Linux mypy on macOS marks the rest of the function unreachable after the darwin pytest.skip, so the strict index error only appears on Linux. Assisted-by: ClaudeCode:claude-fable-5 * chore: minor cleanup of variable name Signed-off-by: Henry Schreiner * test: cover container shell exiting during call Assisted-by: ClaudeCode:claude-opus-5 --------- Signed-off-by: Henry Schreiner --- bin/generate_schema.py | 2 ++ bin/update_pythons.py | 6 +++--- cibuildwheel/oci_container.py | 8 ++++++-- cibuildwheel/platforms/ios.py | 6 ++---- cibuildwheel/platforms/pyodide.py | 2 +- cibuildwheel/projectfiles.py | 2 ++ docs/main.py | 2 +- pyproject.toml | 23 ++++++++++++++++++++++- unit_test/oci_container_test.py | 30 +++++++++++++++++++++++++++++- 9 files changed, 68 insertions(+), 13 deletions(-) diff --git a/bin/generate_schema.py b/bin/generate_schema.py index 4c127d18..7308b4ba 100755 --- a/bin/generate_schema.py +++ b/bin/generate_schema.py @@ -318,6 +318,8 @@ for value in schema["properties"].values(): case {"type": "string_table_array"}: del value["type"] value["oneOf"] = string_table_array + case _: + pass overrides = yaml.safe_load( """ diff --git a/bin/update_pythons.py b/bin/update_pythons.py index dd6bb38c..72cad1f4 100755 --- a/bin/update_pythons.py +++ b/bin/update_pythons.py @@ -76,9 +76,9 @@ class WindowsVersions: response.raise_for_status() api_info = response.json() - for resource in api_info["resources"]: - if resource["@type"] == "PackageBaseAddress/3.0.0": - endpoint = resource["@id"] + endpoint = next( + r["@id"] for r in api_info["resources"] if r["@type"] == "PackageBaseAddress/3.0.0" + ) ARCH_DICT = {"32": "win32", "64": "win_amd64", "ARM64": "win_arm64"} PACKAGE_DICT = {"32": "pythonx86", "64": "python", "ARM64": "pythonarm64"} diff --git a/cibuildwheel/oci_container.py b/cibuildwheel/oci_container.py index 49e14a93..e0844f97 100644 --- a/cibuildwheel/oci_container.py +++ b/cibuildwheel/oci_container.py @@ -546,9 +546,14 @@ class OCIContainer: else: output_io = sys.stdout.buffer - while True: + return_code = None + while return_code is None: line = self.bash_stdout.readline() + if not line: + msg = "Failed to read the return code, the container shell has exited" + raise RuntimeError(msg) + if line.endswith(bytes(end_of_message, encoding="utf8") + b"\n"): # fmt: off footer_offset = ( @@ -563,7 +568,6 @@ class OCIContainer: # add the last line to output, without the footer output_io.write(line[0:footer_offset]) output_io.flush() - break else: output_io.write(line) output_io.flush() diff --git a/cibuildwheel/platforms/ios.py b/cibuildwheel/platforms/ios.py index 72ab3595..2d1fefd6 100644 --- a/cibuildwheel/platforms/ios.py +++ b/cibuildwheel/platforms/ios.py @@ -540,7 +540,7 @@ def build(options: Options, tmp_path: Path) -> None: f"that is compatible with {config.identifier}. " "Skipping build step..." ) - test_wheel = compatible_wheel + repaired_wheel = compatible_wheel else: if build_options.before_build: log.step("Running before_build...") @@ -632,8 +632,6 @@ def build(options: Options, tmp_path: Path) -> None: run_audit(tmp_dir=tmp_path, build_options=build_options, wheel=repaired_wheel) - test_wheel = repaired_wheel - if build_options.test_command and build_options.test_selector(config.identifier): if not config.is_simulator: log.step("Skipping tests on non-simulator SDK") @@ -693,7 +691,7 @@ def build(options: Options, tmp_path: Path) -> None: platform_tag, "--target", testbed_path / "iOSTestbed" / "app_packages", - f"{test_wheel}{build_options.test_extras}", + f"{repaired_wheel}{build_options.test_extras}", *build_options.test_requires, env=test_env, ) diff --git a/cibuildwheel/platforms/pyodide.py b/cibuildwheel/platforms/pyodide.py index d4657c76..6de8a00d 100644 --- a/cibuildwheel/platforms/pyodide.py +++ b/cibuildwheel/platforms/pyodide.py @@ -473,7 +473,7 @@ def build(options: Options, tmp_path: Path) -> None: print( f"\nFound previously built wheel {compatible_wheel.name}, that's compatible with {config.identifier}. Skipping build step..." ) - built_wheel = compatible_wheel + repaired_wheel = compatible_wheel else: if build_options.before_build: log.step("Running before_build...") diff --git a/cibuildwheel/projectfiles.py b/cibuildwheel/projectfiles.py index c45d7fa4..185d2a74 100644 --- a/cibuildwheel/projectfiles.py +++ b/cibuildwheel/projectfiles.py @@ -60,6 +60,8 @@ class Analyzer(ast.NodeVisitor): case ast.keyword(arg="python_requires", value=ast.Constant(value=str() as version)): if unnested or name_main_unnested: self.requires_python = version + case _: + pass def setup_py_python_requires(content: str) -> str | None: diff --git a/docs/main.py b/docs/main.py index ce79df3a..cb0026ba 100644 --- a/docs/main.py +++ b/docs/main.py @@ -12,7 +12,7 @@ def define_env(env: Any) -> None: # noqa: ANN401 "Hook function for mkdocs-macros" @env.macro # type: ignore[untyped-decorator] - def subprocess_run(*args: str) -> str: + def subprocess_run(*args: str) -> str: # type: ignore[misc] "Run a subprocess and return the stdout" env = os.environ.copy() scripts = sysconfig.get_path("scripts") diff --git a/pyproject.toml b/pyproject.toml index 69e771db..8aba9f5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,10 +116,27 @@ files = [ "bin/*.py", "noxfile.py", ] +disallow_any_decorated = true +disallow_any_unimported = true +disallow_untyped_globals = true +disallow_redefinition = true warn_unused_configs = true strict = true -enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] +enable_error_code = [ + "deprecated", + "exhaustive-match", + "ignore-without-code", + "mutable-override", + "possibly-undefined", + "redundant-expr", + "redundant-self", + "truthy-bool", + "truthy-iterable", + "unimported-reveal", + "unused-awaitable", +] warn_unreachable = false +native_parser = true [[tool.mypy.overrides]] module = [ @@ -130,6 +147,10 @@ module = [ ] ignore_missing_imports = true +[[tool.mypy.overrides]] +module = ["cibuildwheel.bashlex_eval"] +disable_error_code = ["no-any-unimported"] + [tool.pylint] py-version = "3.11" diff --git a/unit_test/oci_container_test.py b/unit_test/oci_container_test.py index 8b94187c..5e931976 100644 --- a/unit_test/oci_container_test.py +++ b/unit_test/oci_container_test.py @@ -29,6 +29,7 @@ from cibuildwheel.oci_container import ( TYPE_CHECKING = False if TYPE_CHECKING: from collections.abc import Iterator + from typing import Any # Test utilities @@ -84,6 +85,18 @@ def test_no_lf(container_engine: OCIContainerEngineConfig) -> None: assert container.call(["printf", "hello"], capture_output=True) == "hello" +def test_abnormal_exit(container_engine: OCIContainerEngineConfig) -> None: + container = OCIContainer( + engine=container_engine, image=DEFAULT_IMAGE, oci_platform=DEFAULT_OCI_PLATFORM + ) + with container: + # kill the shell without a newline, so the write below still goes + # through the same buffer and the call fails on read, not on write + container.bash_stdin.write(b"exit") + with pytest.raises(RuntimeError): + container.call(["echo", "hello"]) + + def test_debug_info(container_engine: OCIContainerEngineConfig) -> None: container = OCIContainer( engine=container_engine, image=DEFAULT_IMAGE, oci_platform=DEFAULT_OCI_PLATFORM @@ -139,6 +152,7 @@ def test_container_removed(container_engine: OCIContainerEngineConfig) -> None: ) as container: assert container.name is not None container_name = container.name + docker_containers_listing = "" for _ in range(timeout): docker_containers_listing = subprocess.run( f"{container.engine.name} container ls", @@ -330,7 +344,7 @@ def test_podman_vfs( # This requires that we write configuration files and point to them # with environment variables before we run podman # https://github.com/containers/common/blob/main/docs/containers.conf.5.md - vfs_containers_conf_data = { + vfs_containers_conf_data: dict[str, dict[str, Any]] = { "containers": { "default_capabilities": [ "CHOWN", @@ -348,6 +362,20 @@ def test_podman_vfs( }, "engine": {"cgroup_manager": "cgroupfs", "events_logger": "file"}, } + + # Setting CONTAINERS_CONF makes podman ignore its usual config files, so + # carry over the default OCI runtime; the fallback found on PATH can be + # too old for the OCI spec version podman generates (e.g. Ubuntu 24.04's + # crun 1.14.1 with podman 5.x). + oci_runtime = subprocess.run( + ["podman", "info", "--format", "{{.Host.OCIRuntime.Path}}"], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + runtime_name = Path(oci_runtime).name + vfs_containers_conf_data["engine"]["runtime"] = runtime_name + vfs_containers_conf_data["engine"]["runtimes"] = {runtime_name: [oci_runtime]} # https://github.com/containers/storage/blob/main/docs/containers-storage.conf.5.md storage_root = vfs_path / ".local/share/containers/vfs-storage" run_root = vfs_path / ".local/share/containers/vfs-runroot"