screen-locker/pyproject.toml
Krzysztof kuhy Rudnicki 21a40ba9f5 Lint tests; sync pylint hook pins
ignore-paths made the tests hook lint zero files; drop it. Hook pins
had drifted (gatelock 0.10.0, earned-time 0.16.0) from pyproject; sync.
Add pylint design/naming/protected-access config for the mixin host
surface, and run the silent-failure hooks as modules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWYdwRAtMg7ki6TKxuU
2026-11-01 21:11:34 +01:00

299 lines
11 KiB
TOML

[project]
name = "screen-locker"
version = "1.0.0"
description = "Tkinter/systemd screen locker with workout tracking, shared sick days, and wake-alarm integration"
requires-python = ">=3.14"
# gatelock: shared lock-window/HMAC backend, also used by diet_guard and wake_alarm
# crdt-sync: shared CRDT merge scheme + sync transports, also used by
# diet_guard; here for phone-workout sync, Firebase RTDB only since 2026-10-09
dependencies = [
"gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.11.0#subdirectory=gatelock",
"crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync",
"freedays @ git+https://github.com/kuhyx/utils@freedays-v0.4.0#subdirectory=freedays",
"earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.18.0#subdirectory=earned_time",
"sickdays @ git+https://github.com/kuhyx/utils@sickdays-v0.3.0#subdirectory=sickdays",
]
[project.scripts]
screen-locker-status = "screen_locker.status_view:main"
screen-locker-web = "screen_locker._web_server:main"
# Optional extras. The `mcp` group is ONLY needed by the read-only MCP server
# (screen_locker._mcp), which Claude Code spawns via a dedicated venv (see
# scripts/setup_mcp.sh / .mcp.json). It is deliberately NOT a core dependency
# so the systemd/CLI system-python path stays dependency-light and never has
# to import the MCP SDK.
[project.optional-dependencies]
mcp = ["mcp==2.3.0"]
[tool.setuptools.packages.find]
# This repo also holds stronglift_replacement/ (an unrelated Flutter app, not
# a Python package); without this, setuptools' automatic flat-layout
# discovery sees two top-level directories and refuses to guess which is the
# installable package.
include = ["screen_locker*"]
[tool.ruff]
target-version = "py314"
include = ["*.py", "**/*.py"]
exclude = [".git", ".venv", "__pycache__", "build", "dist", ".eggs"]
[tool.ruff.lint]
select = ["ALL"]
ignore = [
"D203",
"D213",
"COM812",
# Personal repo, no copyright policy (ruff 0.17 made CPY001 stable under
# select=ALL).
"CPY001",
"ISC001",
"S603",
]
fixable = ["ALL"]
unfixable = []
[tool.ruff.lint.per-file-ignores]
"**/tests/**/*.py" = [
"ANN", "ARG", "D", "DTZ011", "E501", "FBT",
"PLC0415", "PLR0913", "PLR2004", "PTH", "RUF003", "S101", "SLF001",
]
"**/test_*.py" = [
"ANN", "ARG", "D", "DTZ011", "E501", "FBT",
"PLC0415", "PLR0913", "PLR2004", "PTH", "RUF003", "S101", "SLF001",
]
# The ET.parse call this covers moved to _runnerup_tcx.py when
# _runnerup_verification.py was split for the 250-line cap. Same waiver, new
# home -- the TCX files are pulled off the user's own phone by adb.
"screen_locker/_runnerup_tcx.py" = ["S314"]
# URL is always built from the hardcoded https:// _WTTR_URL constant with
# only the (quoted) city interpolated — never user/attacker controlled.
# ruff's S310 can't verify that via static analysis of a variable URL, so
# it always flags urlopen() here regardless; a runtime scheme check was
# considered and rejected (it can't ever trigger, so it's just an untested
# branch and a crash risk in the lock's startup path for zero real benefit).
"screen_locker/_temperature.py" = ["S310"]
# PERF203 (try/except inside a loop) describes a retry loop exactly, and lands
# where its cost is irrelevant: each iteration makes a network call and then
# sleeps for seconds, so handler setup is noise. Hoisting the handler out of
# the loop would mean swallowing the error into a return value, which the
# repo's no-silent-failures rule rightly discourages.
"screen_locker/_sync_retry.py" = ["PERF203"]
"screen_locker/_shutdown_base.py" = ["SLF001"]
# run_status walks the weekly-status pipeline (weekly logic, RunnerUp scan,
# bonus, heat-skip) as a sequence of stages — genuinely branchy by design, same
# rationale as the validation-chain functions below; not a config-object case.
"screen_locker/_status.py" = ["C901", "PLR0912", "PLR0915", "SLF001", "T201"]
# _heat_skip.py opens a tkinter fullscreen window; tk's .attributes()/other
# calls take positional booleans (its API), which FBT003 always flags.
"screen_locker/_heat_skip.py" = ["FBT003"]
# StatusWindow.__init__ takes one arg per independently-mocked dependency
# (verifier_factory, temperature_fetcher, log_file, ...) — same
# dependency-injection rationale as _status_data.py's gather_status below.
"screen_locker/status_view.py" = ["PLC0415", "PLR0913", "SLF001", "T201"]
# The verify-and-credit flow split out of status_view.py: it deliberately
# drives the bare verifier's own mixin methods (_verify_phone_workout,
# _verify_runnerup_workout, _apply_workout_credit) — the same single-source-of
# -truth reuse status_view.py above is exempted for — and _make_bare_verifier
# imports ScreenLocker inside the function to avoid an import cycle.
"screen_locker/_status_view_verify.py" = ["PLC0415", "SLF001"]
# explain_lock_decision takes one arg per real file path and input, and
# returns early per lock-decision-chain stage (see module docstring) — both
# are the deliberate design, not something to collapse into a config object.
"screen_locker/_compliance_state.py" = ["C901", "PLR0911", "PLR0913"]
# gather_status takes one arg per real file path, all independently
# overridable for tests — same rationale as _compliance_state.py above.
"screen_locker/_status_data.py" = ["PLR0913"]
# validate_manual_workout returns early per failed evidence-form check (same
# validation-chain shape as _compliance_state.py's explain_lock_decision) —
# deliberate design, not something to collapse into a config object.
"screen_locker/_manual_workout.py" = ["C901", "PLR0911"]
# _verify_phone_workout returns early per verification-chain stage (clock
# check, sync, ADB, HTTP, then terminal statuses) — same shape as the two
# ignores above, not something to collapse into a config object.
"screen_locker/_phone_verification.py" = ["C901", "PLR0911"]
"scripts/check_file_length.py" = ["PTH123"]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint.isort]
force-single-line = false
force-sort-within-sections = true
known-first-party = ["screen_locker"]
[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"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
docstring-code-format = true
[tool.mypy]
python_version = "3.14"
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
warn_unreachable = true
disallow_any_unimported = true
disallow_any_explicit = false
disallow_any_generics = true
disallow_subclassing_any = true
strict_equality = true
extra_checks = true
ignore_missing_imports = true
show_error_codes = true
color_output = true
exclude = [".venv/"]
[tool.pylint.main]
# The repo root on the path, so a file outside the package (scripts/) can be
# linted on its own. pylint otherwise infers the path from the files it is
# given, so `scripts/verify_screen_fits.py` resolved `screen_locker` only when
# a package file happened to be in the same batch -- passing or failing by
# what else was staged.
init-hook = "import sys; sys.path.insert(0, '.')"
analyse-fallback-blocks = true
persistent = true
# Serial on purpose. With jobs != 1 pylint ships the linter to its workers via
# dill.dumps(), and dill 0.4.1 (newest release) reads code.co_lnotab, which
# CPython 3.15 removed -- every run crashed once setup-python's "3.x" became
# 3.15.1. The fix sits on dill master, unreleased; return to 0 once a dill
# release past 0.4.1 ships. The pre-commit hooks take this value too.
jobs = 1
py-version = "3.14"
ignore = [".venv", "__pycache__"]
ignore-patterns = [".*\\.pyi$"]
[tool.pylint.messages_control]
enable = "all"
disable = []
[tool.pylint.design]
min-public-methods = 0
max-module-lines = 1000
# ScreenLocker composes ~30 mixins; collapsing them is not the goal.
max-parents = 40
# Keyword-only arguments count: the gather_*/explain_* entry points take one
# per real file path so tests can override each.
max-args = 8
# ManualWorkoutDraft is a flat 23-field record mirroring the evidence form and
# the wire payload; StatusWindow / ScreenLocker hold UI and injected state.
max-attributes = 25
[tool.pylint.similarities]
# The remaining overlaps are 6-11 line fragments of deliberately parallel code
# (Tk button rows, the two JSON ledgers' loaders, mixin host stubs); a real
# copy-paste block is longer than this.
min-similarity-lines = 12
[tool.pylint.format]
# BaseHTTPRequestHandler dispatches on the method names do_GET / do_POST.
good-names-rgxs = "^do_(GET|POST)$"
[tool.pylint.classes]
# The mixin-host surface: module-level helpers (shutdown_base, status, the
# status window's verify flow) drive the ScreenLocker's own mixin methods, the
# same single-source-of-truth reuse ruff exempts via per-file SLF001.
exclude-protected = [
"_asdict", "_fields", "_replace", "_source", "_make", "os._exit",
"_adjust_shutdown_time_by",
"_apply_sick_floor",
"_apply_workout_credit",
"_credit_ingested_workout",
"_read_shutdown_config",
"_scan_and_fill_week_runnerup",
"_try_fill_stronglifts_for_week",
"_verify_phone_workout",
"_verify_runnerup_workout",
"_write_shutdown_config",
]
[tool.pylint.typecheck]
generated-members = [
".*\\.assert_called_once_with",
".*\\.assert_called_once",
".*\\.assert_called",
".*\\.assert_not_called",
".*\\.assert_any_call",
".*\\.call_args",
".*\\.call_args_list",
".*\\.call_count",
]
[tool.bandit]
exclude_dirs = ["tests", ".venv"]
[tool.pytest.ini_options]
testpaths = ["screen_locker/tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"-v",
"--strict-markers",
"--strict-config",
"-ra",
"--cov=screen_locker",
"--cov-branch",
"--cov-report=term-missing",
"--cov-report=lcov",
# The commit gate runs plain `python -m pytest -q`; serial it took 26 s of
# its 60 s budget, 4 workers 9 s. Fixed, not `auto`: auto is 24 workers
# here, past capped.sh's 4 GiB. CI's own `-n auto` comes later and wins;
# `-n0` gets a serial run back.
"-n",
"4",
"--dist",
"worksteal",
# A fixture-only plugin module, not conftest.py (already at the
# repo's 400-line-per-file cap) -- see its own docstring.
"-p",
"screen_locker.tests.test_sync_fixtures",
]
filterwarnings = [
"error",
"ignore::DeprecationWarning",
"default::pytest.PytestUnraisableExceptionWarning",
]
[tool.coverage.run]
source = ["screen_locker"]
branch = true
# sys.monitoring (PEP 669) rather than the C tracer: test_shutdown_ceiling_
# invariant.py alone measured 13 s vs 22 s, for the same line+branch data.
core = "sysmon"
omit = [
"*/__pycache__/*",
"*/tests/*",
"*/.venv/*",
]
[tool.coverage.report]
fail_under = 100
show_missing = true
skip_covered = false
exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"raise AssertionError",
"if TYPE_CHECKING:",
'if __name__ == "__main__":',
]
partial_branches = ["pragma: no branch"]