mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 19:17:02 +01:00
The shell entry points got this in 99aa194; running the .py directly still died with ModuleNotFoundError, which is the command the file's own docstring had been telling people to use for months. tool/venv_bootstrap.py creates .venv, installs the project and re-executes the same command line through it. It is stdlib-only by necessity: it runs on an interpreter where this package does not import yet, so it cannot route through signal_bot.external, the one module allowed to start a process. That is why it gets the second scoped per-file-ignore rather than the rule being bent -- kuhy's call. test_external.py now asserts a closed list of two, so a third file still fails the gate. Two things that are less obvious than they look: It runs on import rather than exposing a function to call. A call would have to happen before the caller's own third-party imports, and code before imports is E402 -- so a call-based design needs a lint suppression in every file it protects. Importing the module is the whole interface. And isort would sort "venv_bootstrap" after "fastapi", silently breaking the ordering the module depends on. It gets its own isort section ahead of the standard library, so the formatter enforces the order instead of undoing it. Caught while verifying: ensure() used one check to decide both whether to re-exec and whether to install. The interpreter you typed never grows a fastapi, so every launch reinstalled the project first. Splitting them (ready() probes the venv, missing() probes the caller) takes a warm run from a full pip install to 0.47s. Pinned by a test. Verified on a clone with no .venv: first run installs and prints one line, second and third print nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WBrE1b9HvFw3xvG9bJiDFt
112 lines
4.5 KiB
Python
112 lines
4.5 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
|
|
"""Every script under tool/ imports, and the tagger answers --help.
|
|
|
|
This exists because ``tool/tagger.py`` shipped an unconditional ImportError
|
|
and survived two releases with it: commit 48957a1 renamed
|
|
``pack.default_dir`` to ``default_dirs`` and missed the caller. Nothing could
|
|
have caught it. ``tool/`` is outside ``--cov`` in pyproject.toml, mypy ran
|
|
with ``args: [signal_bot, fake_signal]``, and ruff lints these files but
|
|
cannot resolve a name in another module.
|
|
|
|
So this is deliberately the cheapest possible test: import each file and, for
|
|
the one with a CLI, run it. It adds no coverage burden -- ``tests/`` is not
|
|
measured -- and no module-scope ImportError can survive it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from signal_bot.external import run
|
|
|
|
TOOL = Path(__file__).resolve().parent.parent / "tool"
|
|
|
|
|
|
def _modules() -> list[Path]:
|
|
"""Every Python file under tool/, so a new one is covered automatically."""
|
|
return sorted(path for path in TOOL.glob("*.py") if path.name != "__init__.py")
|
|
|
|
|
|
def test_there_is_something_to_check() -> None:
|
|
# Guards against the glob silently matching nothing and the whole file
|
|
# passing vacuously.
|
|
assert _modules()
|
|
|
|
|
|
def _load(stem: str) -> object:
|
|
"""Import one tool/ module by path, the way the parametrised test does."""
|
|
spec = importlib.util.spec_from_file_location(f"_probe_{stem}", TOOL / f"{stem}.py")
|
|
assert spec is not None
|
|
assert spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
@pytest.mark.parametrize("path", _modules(), ids=lambda path: path.name)
|
|
def test_the_module_imports(path: Path) -> None:
|
|
spec = importlib.util.spec_from_file_location(f"_tool_{path.stem}", path)
|
|
assert spec is not None
|
|
assert spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
# tool/ is a directory of scripts, not a package, and a couple of them
|
|
# import a sibling by bare name. That resolves in real use -- running
|
|
# "python tool/x.py" puts tool/ at sys.path[0] -- so the harness has to
|
|
# reproduce it rather than the scripts having to work around it.
|
|
sys.path.insert(0, str(TOOL))
|
|
try:
|
|
spec.loader.exec_module(module)
|
|
finally:
|
|
sys.path.remove(str(TOOL))
|
|
sys.modules.pop(spec.name, None)
|
|
|
|
|
|
def test_the_tagger_answers_help() -> None:
|
|
# Through signal_bot.external, like every other subprocess here: S603 is
|
|
# suppressed for exactly one file and widening it is what that entry
|
|
# exists to prevent.
|
|
result = run([sys.executable, str(TOOL / "tagger.py"), "--help"])
|
|
assert result.ok, result.stderr
|
|
for flag in ("--import", "--untagged", "--dry-run"):
|
|
assert flag in result.stdout
|
|
|
|
|
|
def test_the_bootstrap_reports_what_this_interpreter_cannot_import() -> None:
|
|
# The whole decision rests on this: an interpreter that can already
|
|
# import everything must be left alone, or every run pays for a venv it
|
|
# does not need.
|
|
bootstrap = _load("venv_bootstrap")
|
|
assert bootstrap.missing() == []
|
|
assert bootstrap.missing(("definitely_not_a_real_module",)) == ["definitely_not_a_real_module"]
|
|
# A name that is not importable *and* not a valid spec query has to be
|
|
# reported, not raised: find_spec raises ValueError for some of them.
|
|
assert bootstrap.missing((".relative",)) == [".relative"]
|
|
|
|
|
|
def test_the_bootstrap_module_is_stdlib_only() -> None:
|
|
# It runs before .venv exists, so a third-party import here would be the
|
|
# same bug one level down.
|
|
source = (TOOL / "venv_bootstrap.py").read_text(encoding="utf-8")
|
|
for banned in ("import fastapi", "import httpx", "import uvicorn", "from signal_bot"):
|
|
assert banned not in source
|
|
|
|
|
|
def test_the_bootstrap_only_installs_when_the_venv_cannot_already_do_it(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
# Measured bug: ensure() tested the *current* interpreter to decide both
|
|
# whether to re-exec and whether to install. The interpreter you typed
|
|
# never grows a fastapi, so every single launch reinstalled the project
|
|
# before re-execing. The two decisions are separate.
|
|
bootstrap = _load("venv_bootstrap")
|
|
bootstrap.VENV_PYTHON = tmp_path / "nonexistent" / "python"
|
|
assert bootstrap.ready() is False
|
|
bootstrap.VENV_PYTHON = Path(sys.executable)
|
|
assert bootstrap.ready(("sys",)) is True
|
|
assert bootstrap.ready(("definitely_not_a_real_module",)) is False
|