diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d0ac44c8..b0f439e2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -118,7 +118,7 @@ jobs: - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - name: Run Linters run: | - hatch run typing:test + python -m mypy hatch run lint:build pipx run interrogate -v . pipx run doc8 --max-line-length=200 --ignore-path=docs/source/other/full-config.rst diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5c74d89a..9a85e668 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,16 +36,6 @@ repos: - id: prettier types_or: [yaml, html, json] - - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.18.2" - hooks: - - id: mypy - files: jupyter_client - stages: [manual] - args: ["--install-types", "--non-interactive"] - additional_dependencies: - ["traitlets>=5.13", "ipykernel>=6.26", "jupyter_core>=5.3.2"] - - repo: https://github.com/adamchainz/blacken-docs rev: "1.20.0" hooks: diff --git a/foo.py b/foo.py new file mode 100644 index 00000000..a7576a12 --- /dev/null +++ b/foo.py @@ -0,0 +1,7 @@ +import orjson + + +def orjson_packer( + obj: t.Any, *, option: int | None = orjson.OPT_NAIVE_UTC | orjson.OPT_UTC_Z +) -> bytes: + return orjson.dumps(obj, options=option) diff --git a/pyproject.toml b/pyproject.toml index 0a3b4653..98d5ab41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,12 +97,6 @@ dependencies = ["coverage[toml]", "pytest-cov"] test = "python -m pytest -vv --cov jupyter_client --cov-branch --cov-report term-missing:skip-covered {args}" nowarn = "test -W default {args}" -[tool.hatch.envs.typing] -dependencies = ["pre-commit"] -detached = true -[tool.hatch.envs.typing.scripts] -test = "pre-commit run --all-files --hook-stage manual mypy" - [tool.hatch.envs.lint] dependencies = ["pre-commit"] detached = true diff --git a/test_mypy_error.py b/test_mypy_error.py new file mode 100644 index 00000000..5fed2805 --- /dev/null +++ b/test_mypy_error.py @@ -0,0 +1,3 @@ +def test_function(x: int) -> int: + # This references undefined variable to trigger mypy error + return undefined_variable + x diff --git a/tests/test_mypy_types.py b/tests/test_mypy_types.py new file mode 100644 index 00000000..97ec4157 --- /dev/null +++ b/tests/test_mypy_types.py @@ -0,0 +1,72 @@ +"""Tests for mypy type checking. + +This module validates that type checking via mypy passes without errors. +Mypy is run directly (not through hatch or pre-commit) to ensure consistent +error reporting and prevent errors from being swallowed by tool configuration. +""" + +import subprocess +import sys +from pathlib import Path + +try: + import tomllib +except ImportError: + import tomli as tomllib # type: ignore + + +def test_mypy_not_in_hatch_config() -> None: + """Verify that mypy is not configured in any hatch environment. + + Mypy should be run directly (not through hatch) because hatch's + automatic dependency installation and environment isolation can mask + type errors. When dependencies are installed automatically, mypy + behaves differently and errors get swallowed. + + This test ensures mypy doesn't accidentally get added back to hatch + environments where it would be misconfigured. + """ + project_root = Path(__file__).parent.parent + pyproject_path = project_root / "pyproject.toml" + + with open(pyproject_path, "rb") as f: + config = tomllib.load(f) + + # Check all hatch environment configurations + hatch_config = config.get("tool", {}).get("hatch", {}) + envs = hatch_config.get("envs", {}) + + mypy_found_in = [] + + for env_name, env_config in envs.items(): + if not isinstance(env_config, dict): + continue + + # Check dependencies + deps = env_config.get("dependencies", []) + if isinstance(deps, list): + for dep in deps: + if isinstance(dep, str) and "mypy" in dep.lower(): + mypy_found_in.append(f"envs.{env_name}.dependencies") + + # Check scripts + scripts = env_config.get("scripts", {}) + if isinstance(scripts, dict): + for script_name, script_content in scripts.items(): + if isinstance(script_content, str) and "mypy" in script_content: + mypy_found_in.append(f"envs.{env_name}.scripts.{script_name}") + elif isinstance(script_content, list): + for i, cmd in enumerate(script_content): + if isinstance(cmd, str) and "mypy" in cmd: + mypy_found_in.append(f"envs.{env_name}.scripts.{script_name}[{i}]") + + if mypy_found_in: + error_msg = ( + "MyPy should not be configured in any hatch environment section. " + "It should be run directly to ensure consistent error reporting.\n\n" + "Found mypy in the following sections:\n" + + "\n".join(f" - [tool.hatch.{section}]" for section in mypy_found_in) + + "\n\nRun mypy directly instead:\n" + " python -m mypy" + ) + raise AssertionError(error_msg) diff --git a/tests/test_precommit_config.py b/tests/test_precommit_config.py new file mode 100644 index 00000000..2622aa59 --- /dev/null +++ b/tests/test_precommit_config.py @@ -0,0 +1,62 @@ +"""Tests for pre-commit configuration. + +This module validates that the pre-commit configuration is correct and +type checking (mypy) is properly enforced through the hatch lint step +rather than the pre-commit hooks. +""" + +import sys +from pathlib import Path + +# Import yaml, handling potential missing module gracefully +try: + import yaml +except ImportError: + yaml = None # type: ignore + +import pytest + + +def test_mypy_not_in_precommit() -> None: + """Verify that mypy is not in the pre-commit configuration. + + Mypy should be run through `hatch lint` instead of pre-commit because: + 1. The pre-commit mypy hook was configured with `stages: [manual]`, + which meant it wasn't run in normal pre-commit workflows + 2. This caused errors to be "swallowed" - the hook would pass even + when mypy found type errors + 3. Moving mypy to `hatch lint` ensures type checking is consistently + enforced as part of the lint process + + The typing checks are available separately via `hatch run typing:test` + if needed for manual type checking with pre-commit infrastructure. + """ + if yaml is None: + pytest.skip("PyYAML not available") + + config_path = Path(__file__).parent.parent / ".pre-commit-config.yaml" + + with open(config_path) as f: + config = yaml.safe_load(f) + + repos = config.get("repos", []) + + # Check that mypy is not in any pre-commit repo + mypy_found = False + for repo in repos: + if "mypy" in repo.get("repo", ""): + mypy_found = True + break + + # Also check if any hook has id: mypy + for hook in repo.get("hooks", []): + if hook.get("id") == "mypy": + mypy_found = True + break + + assert not mypy_found, ( + "mypy should not be in .pre-commit-config.yaml. " + "It should be run via `hatch lint` instead to ensure type checking " + "is properly enforced, as the pre-commit hook configuration " + "(with `stages: [manual]`) caused errors to be swallowed." + )