utils/gatelock/pyproject.toml
Krzysztof Rudnicki ba549f8f05 gatelock 0.4.0: the viewport moves for the user and nobody else
On 2026-08-03 the screen locker's lock screen scrolled to mid-content and
back with nobody touching the keyboard, on a 1366x768 panel. Measured under
Xvfb with the real app: one programmatic focus_set() produced three scrolls,
frac 0.000 -> 0.436 -> 0.145.

Two causes, both here:

* ScrollableSurface followed every <FocusIn>. The apps focus a widget on
  every repaint (and gatelock re-asserts focus from its recovery tick), and
  Tk delivers <FocusIn> to the focused widget's whole ancestor chain when the
  toplevel takes X focus -- so each repaint aimed a scroll at containers
  taller than the screen. Now a focus move scrolls only when it followed a
  real keypress, or a click on the widget that took focus, and only for a
  leaf. Tab onto a clipped field still reveals it; nothing else moves.
* finalize()'s reset-to-top cancelled the centring, because centring moved
  the canvas item down and the region's top edge with it. The region is now
  pinned to the viewport whenever the content fits, so the view cannot move
  at all, the scrollbar is taken down, and centring survives the repaint.

Content that does not fit is a layout defect, not a mode to live in, so
finalize() reports it at warning and gatelock.measure_fit() lets each app
gate on it before shipping (scripts/verify_screen_fits.py in all four).

Also: LockConfig's type/space scales now compact below 768px (_density),
which is where a screen has no room to be at design-system size and no
scrollbar to make up for it. 768 and up is verified to fit at full size, so
it is never compacted.

Tests: _scrollable had none; the regression is now stated as code
(programmatic focus must not move yview; Tab must). 323 pass, coverage
95.38% -> 99.44%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsXnTHyuuCUZsqetTaajcF
2026-08-03 15:50:51 +02:00

171 lines
4.2 KiB
TOML

[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"
[project]
name = "gatelock"
version = "0.4.0"
description = "Shared per-output lock-window + HMAC log-integrity backend for blocking-overlay apps"
requires-python = ">=3.10"
dependencies = [] # pure stdlib — tkinter is bundled with Python
[project.optional-dependencies]
# python-xlib buys instant RandR change events. Without it the lock still
# detects output changes via Tk <Configure> and xrandr polling, just less
# promptly — so this must stay OPTIONAL: a locker that refuses to start
# because a dependency is missing is itself a way to end up unlocked.
# Consumers' install scripts install it unconditionally all the same.
randr = ["python-xlib>=0.33"]
[tool.setuptools.packages.find]
include = ["gatelock*"]
[tool.ruff]
target-version = "py310"
include = ["*.py", "**/*.py"]
exclude = [".git", ".venv", "__pycache__", "build", "dist", ".eggs"]
[tool.ruff.lint]
select = ["ALL"]
ignore = [
"D203",
"D213",
"COM812",
"ISC001",
"S603",
# Optional dependencies (python-xlib) are imported inside functions on
# purpose. A module-scope try/except ImportError resolves at import time,
# which makes the except arm permanently unreachable for branch coverage --
# and that arm is the degradation path we must keep tested. Per-line
# suppressions are banned here, so the rule is disabled repo-wide.
"PLC0415",
]
fixable = ["ALL"]
unfixable = []
[tool.ruff.lint.per-file-ignores]
"**/tests/**/*.py" = ["ARG", "D", "PLC0415", "PLR2004", "S101", "SLF001"]
"**/test_*.py" = ["ARG", "D", "PLC0415", "PLR2004", "S101", "SLF001"]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint.isort]
force-single-line = false
force-sort-within-sections = true
known-first-party = ["gatelock"]
[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.10"
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]
analyse-fallback-blocks = true
persistent = true
jobs = 0
py-version = "3.10"
ignore = [".venv", "__pycache__"]
ignore-patterns = [".*\\.pyi$"]
[tool.pylint.messages_control]
enable = "all"
disable = []
[tool.pylint.design]
min-public-methods = 0
max-module-lines = 1000
max-attributes = 10
[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 = ["gatelock/tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"-v",
"--strict-markers",
"--strict-config",
"-ra",
"--cov=gatelock",
"--cov-branch",
"--cov-report=term-missing",
"--cov-report=lcov",
]
filterwarnings = [
"error",
"ignore::DeprecationWarning",
"default::pytest.PytestUnraisableExceptionWarning",
]
[tool.coverage.run]
source = ["gatelock"]
branch = true
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"]