mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 19:07:19 +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
155 lines
4.9 KiB
TOML
155 lines
4.9 KiB
TOML
[project]
|
|
name = "signal-bot"
|
|
version = "0.2.1"
|
|
description = "A Signal group bot: image commands and daily message tallies."
|
|
readme = "README.md"
|
|
license = "MIT"
|
|
license-files = ["LICENSE"]
|
|
requires-python = ">=3.14"
|
|
|
|
# Exact pins, newest stable, per the dependency-freshness rule. A caret range
|
|
# is "newest-compatible" by definition and gives the freshness gate nothing to
|
|
# compare against.
|
|
dependencies = [
|
|
"fastapi==0.141.1",
|
|
"httpx==0.28.1",
|
|
"python-dotenv==1.2.3",
|
|
"PyYAML==6.0.3",
|
|
"uvicorn==0.52.4",
|
|
"websockets==17.1",
|
|
]
|
|
|
|
[project.optional-dependencies]
|
|
dev = [
|
|
"mypy==2.3.1",
|
|
"pre-commit==4.6.2",
|
|
"pytest==9.1.1",
|
|
"pytest-asyncio==1.4.0",
|
|
"pytest-cov==7.1.0",
|
|
"ruff==0.16.5",
|
|
"types-PyYAML==6.0.12.20260815",
|
|
]
|
|
|
|
[project.scripts]
|
|
signal-bot = "signal_bot.__main__:main"
|
|
|
|
[build-system]
|
|
requires = ["setuptools==84.0.0"]
|
|
build-backend = "setuptools.build_meta"
|
|
|
|
[tool.setuptools]
|
|
packages = ["signal_bot", "fake_signal"]
|
|
|
|
# ============================================================================
|
|
# RUFF
|
|
# ============================================================================
|
|
[tool.ruff]
|
|
target-version = "py314"
|
|
line-length = 100
|
|
|
|
[tool.ruff.lint]
|
|
select = ["ALL"]
|
|
ignore = [
|
|
"D203", # conflicts with D211, we use D211
|
|
"D213", # conflicts with D212, we use D212
|
|
"COM812", # formatter owns trailing commas
|
|
"ISC001", # formatter may create implicit string concatenation
|
|
]
|
|
fixable = ["ALL"]
|
|
|
|
# Verified empirically, not copied: with this block removed, ruff reports
|
|
# exactly these four rules across tests/ and nothing else. SLF001 was in
|
|
# the original list and fired zero times, so it is gone.
|
|
[tool.ruff.lint.isort]
|
|
# venv_bootstrap sets up .venv as a side effect of being imported, so it has
|
|
# to be imported before anything that lives in .venv. Left to its own devices
|
|
# isort sorts it alphabetically -- after fastapi and httpx -- which silently
|
|
# breaks the thing it exists to do. Giving it its own section ahead of the
|
|
# standard library makes the formatter enforce the order instead of undoing
|
|
# it, which is better than a comment asking people not to reformat.
|
|
section-order = [
|
|
"future",
|
|
"bootstrap",
|
|
"standard-library",
|
|
"third-party",
|
|
"first-party",
|
|
"local-folder",
|
|
]
|
|
|
|
[tool.ruff.lint.isort.sections]
|
|
bootstrap = ["venv_bootstrap"]
|
|
|
|
[tool.ruff.lint.per-file-ignores]
|
|
# The single subprocess in this repository. S603 flags every subprocess call
|
|
# and has no code-level fix -- exiftool is a binary, and the pure-Python EXIF
|
|
# libraries handle neither HEIC nor MP4. signal_bot/external.py exists so this
|
|
# entry can name exactly one file: any other module that shells out fails the
|
|
# gate, which is the point. Do not widen this to a directory.
|
|
"signal_bot/external.py" = ["S603"]
|
|
# The bootstrap that installs .venv. It cannot route through external.py --
|
|
# that module lives inside the package it is installing -- so it is the one
|
|
# other place allowed to start a process. Kept to a single small file for
|
|
# exactly that reason. kuhy's call, 2026-08-31. Do not widen to a directory.
|
|
"tool/venv_bootstrap.py" = ["S603", "S606"]
|
|
"tests/**/*.py" = [
|
|
"ARG", # fixture and mock params exist for their side effect
|
|
"D", # test names are the documentation
|
|
"PLR2004", # `assert count == 2` beats a named constant
|
|
"S101", # assert IS what tests do
|
|
]
|
|
|
|
[tool.ruff.lint.pylint]
|
|
# The test envelope builder takes 7 keyword-only fields, one per envelope
|
|
# attribute. Splitting it into a dataclass would only move the count.
|
|
max-args = 7
|
|
|
|
[tool.ruff.lint.pydocstyle]
|
|
convention = "google"
|
|
|
|
[tool.ruff.lint.flake8-quotes]
|
|
docstring-quotes = "double"
|
|
inline-quotes = "double"
|
|
|
|
[tool.ruff.lint.flake8-tidy-imports]
|
|
ban-relative-imports = "all"
|
|
|
|
[tool.ruff.format]
|
|
quote-style = "double"
|
|
docstring-code-format = true
|
|
|
|
# ============================================================================
|
|
# MYPY
|
|
# ============================================================================
|
|
[tool.mypy]
|
|
python_version = "3.14"
|
|
strict = true
|
|
warn_unreachable = true
|
|
disallow_any_explicit = false
|
|
|
|
# ============================================================================
|
|
# PYTEST / COVERAGE
|
|
# ============================================================================
|
|
[tool.pytest.ini_options]
|
|
testpaths = ["tests"]
|
|
asyncio_mode = "auto"
|
|
asyncio_default_fixture_loop_scope = "function"
|
|
addopts = [
|
|
"--cov=signal_bot",
|
|
"--cov=fake_signal",
|
|
"--cov-branch",
|
|
"--cov-report=term-missing",
|
|
"--cov-fail-under=100",
|
|
"--strict-markers",
|
|
]
|
|
|
|
[tool.coverage.run]
|
|
branch = true
|
|
# No omits. Every line of both packages is covered by tests.
|
|
|
|
[tool.coverage.report]
|
|
# No "pragma: no cover" escape hatch: nothing in this repo uses one, and
|
|
# leaving the pattern configured is an invitation to hide code from the
|
|
# 100% gate later. TYPE_CHECKING bodies are genuinely never executed.
|
|
exclude_lines = ["if TYPE_CHECKING:"]
|
|
show_missing = true
|