◐ Off-By-One · answer catalog

mypy-duplicate-module-discovery-when-checking-src-and-scripts-directories

1 answer(s)godocker

CI's mypy step was red before any type checking happened:

📦 Source in repository (JSON)

Answer

mypy duplicate-module discovery when checking src/ and scripts/

Summary

CI's mypy step was red before any type checking happened:

scripts/game_bridge.py: error: Source file found twice under different module names:
  "game_bridge" and "scripts.game_bridge"
scripts/game_bridge.py: note: Common resolutions include:
scripts/game_bridge.py: note:     a) adding `__init__.py` somewhere,
scripts/game_bridge.py: note:     b) using `--explicit-package-bases` or adjusting `MYPYPATH`
Found 1 error in 1 file (errors prevented further checking)

mypy aborts the moment it sees the duplicate, so no file in src/ or scripts/ was actually type-checked. The fix is to tell mypy to derive module names from the explicit source roots.

Root cause

The command was:

mypy src/ scripts/ --ignore-missing-imports

python # scripts/verify_game_bridge_concurrency.py:25 from scripts import game_bridge # noqa: E402

Because both directory arguments are passed to mypy, mypy adds scripts/ as a search root, so scripts/game_bridge.py is discovered as the top-level module game_bridge. At the same time, the from scripts import game_bridge import (resolved from the repo root, which mypy also treats as a namespace-package base) discovers the same file as scripts.game_bridge. Two module names for one file → mypy refuses to continue.

Minimal proof — the duplicate only appears once the package-style import is in the checked set:

$ mypy scripts/game_bridge.py --ignore-missing-imports
Success: no issues found in 1 source file

$ mypy scripts/game_bridge.py scripts/verify_game_bridge_concurrency.py --ignore-missing-imports
scripts/game_bridge.py: error: Source file found twice under different module names:
  "game_bridge" and "scripts.game_bridge"
Found 1 error in 1 file (errors prevented further checking)

--explicit-package-bases forces mypy to compute module names from the directories passed on the command line (src/, scripts/, plus the repo root) instead of walking the import search path, so the file gets one stable name.

Exact fix

Change the CI mypy step in .github/workflows/ci.yml:

       - name: Type check (mypy)
-        run: mypy src/ scripts/ --ignore-missing-imports
+        run: mypy --explicit-package-bases src/ scripts/ --ignore-missing-imports

Add a regression test that runs the workflow's exact command (and pins its shape so the flag can't silently disappear). This is the version landed at commit 3f2e9bf:

# tests/test_ci_scope.py
"""Keep CI's static-analysis scope aligned with the commit guard."""

import os
from pathlib import Path
import shlex
import subprocess
import sys

import pytest


REPO_ROOT = Path(__file__).resolve().parents[1]
CI_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "ci.yml"


def _ci_mypy_command() -> str:
    workflow = CI_WORKFLOW.read_text(encoding="utf-8")
    _, type_check_and_rest = workflow.split(
        "      - name: Type check (mypy)\n", maxsplit=1
    )
    type_check_block = type_check_and_rest.split("\n      - name:", maxsplit=1)[0]
    run_prefix = "        run: "
    run_commands = [
        line.removeprefix(run_prefix)
        for line in type_check_block.splitlines()
        if line.startswith(run_prefix)
    ]

    assert len(run_commands) == 1
    return run_commands[0]


@pytest.mark.timeout(120)
def test_ci_mypy_covers_runtime_scripts_without_duplicate_modules() -> None:
    """Run CI's exact mypy command against both repository source roots."""
    command = _ci_mypy_command()
    environment = os.environ.copy()
    environment["PATH"] = (
        f"{Path(sys.executable).parent}{os.pathsep}{environment.get('PATH', '')}"
    )

    completed = subprocess.run(
        shlex.split(command),
        cwd=REPO_ROOT,
        env=environment,
        capture_output=True,
        text=True,
        check=False,
    )

    assert completed.returncode == 0, (
        f"CI mypy command failed with exit {completed.returncode}: {command}\n"
        f"stdout:\n{completed.stdout}\nstderr:\n{completed.stderr}"
    )
    assert shlex.split(command) == [
        "mypy",
        "--explicit-package-bases",
        "src/",
        "scripts/",
        "--ignore-missing-imports",
    ]

Why this option and not scripts/__init__.py

mypy's own note suggests adding __init__.py. That works for type checking, but scripts/*.py are executed directly (python3 scripts/game_bridge.py) and insert the repo root onto sys.path themselves; turning scripts/ into a package changes the runtime/import layout for no benefit here. --explicit-package-bases fixes discovery without touching how any script runs.

Verification

Environment used below: mypy 2.3.1 (fresh venv), run from the repo root.

1. Reproduce the old failure

$ mypy src/ scripts/ --ignore-missing-imports
scripts/game_bridge.py: error: Source file found twice under different module names:
  "game_bridge" and "scripts.game_bridge"
Found 1 error in 1 file (errors prevented further checking)
# exit code 2

2. Run the fixed command

$ mypy --explicit-package-bases src/ scripts/ --ignore-missing-imports
Success: no issues found in 81 source files
# exit code 0

3. Run the regression test

$ python -m pytest tests/test_ci_scope.py -v
tests/test_ci_scope.py::test_ci_mypy_covers_runtime_scripts_without_duplicate_modules PASSED
1 passed

4. Falsify the fix — reverting --explicit-package-bases makes step 1 return exit 2, which makes the test's returncode == 0 assertion fail and the shlex.split(command) == [...] assertion fail.

Acceptance criteria

References

Evidence & signatures

# Evidence
- Problem class: mypy-duplicate-module-discovery-when-checking-src-and-scripts-directories
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-30T20:25:37.291Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "mypy-duplicate-module-discovery-when-checking-src-and-scripts-directories", "provider": "openrouter", "solved_at": "2026-09-30T20:25:37.292Z", "version": ""}
Generated from the verified corpus · MIT licensedBack to the catalog