Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Coverage for CI's floor (pytest --cov --cov-fail-under=N in ci.yml).
[run]
source =
systemgate
omit =
*/tests/*

[report]
skip_empty = true
24 changes: 24 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Editor settings shared by every Conker repository; .gitattributes keeps line endings LF.
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4

[*.{js,jsx,mjs,cjs,ts,tsx,json,yml,yaml,css,html}]
indent_size = 2

[*.md]
# A trailing double space is a Markdown line break.
trim_trailing_whitespace = false

[*.{patch,diff}]
# Unified diffs need their single-space prefix on empty context lines.
trim_trailing_whitespace = false

[Makefile]
indent_style = tab
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Every change needs the owner's review.
* @alexeybe1kin
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
run: |
pip install --quiet ruff==0.16.6
python -m ruff check .
python -m ruff format --check .

- name: The module contract requires these files to exist
run: |
Expand Down Expand Up @@ -63,8 +64,15 @@ jobs:
# 600 lines per source file; files that were already larger may only shrink.
run: python scripts/check_file_sizes.py

- name: Types
# pyright basic mode (pyrightconfig.json). Errors that predate the check are counted
# per file in type-baseline.txt; a count may only go down.
run: python scripts/check_types.py

- name: Tests
run: python -m pytest tests/ -q
# The coverage floor is the coverage measured when it arrived, rounded down, so it
# only stops coverage from falling (.coveragerc names what is measured).
run: python -m pytest tests/ -q --cov --cov-report=term --cov-fail-under=93

contract:
# The health contract is an HTTP contract, so it is asserted against a
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ __pycache__/
.mypy_cache/
data/

.coverage
.coverage.*
32 changes: 32 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# The same checks CI runs, with the same pinned tools (requirements-dev.txt), so a commit
# that passes here passes the lint stage there. Install once: pip install pre-commit &&
# pre-commit install --install-hooks -t pre-commit -t pre-push
default_install_hook_types: [pre-commit, pre-push]
repos:
- repo: local
hooks:
- id: ruff-check
name: ruff check
entry: python -m ruff check .
language: system
pass_filenames: false
types: [python]
- id: ruff-format
name: ruff format --check
entry: python -m ruff format --check .
language: system
pass_filenames: false
types: [python]
- id: file-sizes
name: file sizes (600 lines)
entry: python scripts/check_file_sizes.py
language: system
pass_filenames: false
always_run: true
- id: types
name: pyright basic, baseline only shrinks
entry: python scripts/check_types.py
language: system
pass_filenames: false
types: [python]
stages: [pre-push]
7 changes: 7 additions & 0 deletions pyrightconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"typeCheckingMode": "basic",
"pythonVersion": "3.12",
"pythonPlatform": "Linux",
"include": ["systemgate"],
"exclude": ["**/tests", "**/__pycache__", "**/node_modules", "**/.venv"]
}
23 changes: 23 additions & 0 deletions renovate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"timezone": "UTC",
"schedule": ["before 6am on monday"],
"minimumReleaseAge": "7 days",
"automerge": false,
"dependencyDashboard": true,
"prConcurrentLimit": 5,
"packageRules": [
{
"description": "One weekly pull request for every minor, patch and digest update.",
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
"groupName": "weekly minor and patch updates",
"groupSlug": "weekly-minor-patch"
},
{
"description": "Major updates stay one pull request per package, reviewed one at a time.",
"matchUpdateTypes": ["major"],
"automerge": false
}
]
}
2 changes: 2 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
-r requirements.txt
pytest==9.1.1
ruff==0.16.6
pytest-cov==7.1.0
pyright==1.1.414
4 changes: 3 additions & 1 deletion ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ ignore = [
# that raised instead would take down the very endpoint the owner uses to
# find out what is wrong. See the truthful-status rule in CLAUDE.md.
"BLE001",
# Line length is left to review until this module adopts `ruff format`.
# `ruff format` enforces line length for code (CI runs `ruff format --check`).
# What it cannot wrap are long string literals, whose exact text matters more
# than a column limit.
"E501",
]

Expand Down
16 changes: 12 additions & 4 deletions scripts/check_file_sizes.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,20 @@ def main() -> int:
if ceiling is None and lines > LIMIT:
problems.append(f"{path}: {lines} lines, limit {LIMIT}. Split it by concern.")
elif ceiling is not None and lines > ceiling:
problems.append(f"{path}: grew to {lines} lines; it is allowlisted at {ceiling} and may only shrink.")
problems.append(
f"{path}: grew to {lines} lines; it is allowlisted at {ceiling} and may only shrink."
)
elif ceiling is not None and lines <= LIMIT:
problems.append(f"{path}: now {lines} lines, under the limit; delete its line from {ALLOWLIST}.")
problems.append(
f"{path}: now {lines} lines, under the limit; delete its line from {ALLOWLIST}."
)
elif ceiling is not None and lines < ceiling:
problems.append(f"{path}: shrank to {lines} lines; lower its entry in {ALLOWLIST} from {ceiling}.")
problems += [f"{path}: listed in {ALLOWLIST} but no longer tracked; delete the line." for path in allowed]
problems.append(
f"{path}: shrank to {lines} lines; lower its entry in {ALLOWLIST} from {ceiling}."
)
problems += [
f"{path}: listed in {ALLOWLIST} but no longer tracked; delete the line." for path in allowed
]
if problems:
print("\n".join(problems))
return 1
Expand Down
96 changes: 96 additions & 0 deletions scripts/check_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Fail when pyright (basic mode) finds a type error that is not in the baseline.

Run from the repository root: python scripts/check_types.py
pyrightconfig.json sets the mode and the checked folders. Errors that already existed when the
check arrived are counted per file in type-baseline.txt; a file's count may shrink but never
grow, and a line must be lowered or deleted once its file has fewer errors, so the list only
ever gets shorter. `--write` rewrites the baseline from the current errors: use it only to
lower counts, never to accept new errors.
"""

from __future__ import annotations

import json
import subprocess
import sys
from collections import Counter
from pathlib import Path

BASELINE = Path("type-baseline.txt")
HEADER = (
"# pyright basic-mode errors per file when the type check arrived; counts may only shrink.\n"
"# Lower or delete a line when its file improves (scripts/check_types.py says which).\n"
)


def current_errors() -> Counter[str]:
run = subprocess.run(
[sys.executable, "-m", "pyright", "--outputjson"],
capture_output=True,
text=True,
check=False,
)
try:
report = json.loads(run.stdout)
except json.JSONDecodeError:
print(run.stdout + run.stderr)
print("pyright did not produce a JSON report.")
sys.exit(2)
root = Path.cwd().resolve()
counts: Counter[str] = Counter()
for item in report.get("generalDiagnostics", []):
if item.get("severity") == "error":
path = Path(item["file"]).resolve().relative_to(root).as_posix()
counts[path] += 1
return counts


def baseline() -> dict[str, int]:
if not BASELINE.exists():
return {}
entries = {}
for line in BASELINE.read_text(encoding="utf-8").splitlines():
line = line.split("#", 1)[0].strip()
if not line:
continue
parts = line.rsplit(maxsplit=1)
if len(parts) != 2 or not parts[1].isdigit():
print(f"{BASELINE}: malformed entry: {line!r}")
sys.exit(1)
entries[parts[0]] = int(parts[1])
return entries


def write(counts: Counter[str]) -> None:
lines = [f"{path} {count}" for path, count in sorted(counts.items())]
text = HEADER + "".join(f"{line}\n" for line in lines)
BASELINE.write_text(text, encoding="utf-8", newline="\n")
print(f"wrote {BASELINE}: {sum(counts.values())} errors in {len(counts)} files")


def main() -> int:
counts = current_errors()
if "--write" in sys.argv[1:]:
write(counts)
return 0
allowed = baseline()
problems = []
for path in sorted(set(counts) | set(allowed)):
found, ceiling = counts.get(path, 0), allowed.get(path, 0)
if found > ceiling:
problems.append(
f"{path}: {found} type errors, baseline {ceiling}. Fix the new ones "
"(python -m pyright " + path + ")."
)
elif found < ceiling:
target = "delete its line" if found == 0 else f"lower it to {found}"
problems.append(f"{path}: down to {found} type errors; {target} in {BASELINE}.")
if problems:
print("\n".join(problems))
return 1
print(f"types ok ({sum(counts.values())} baselined errors in {len(counts)} files)")
return 0


if __name__ == "__main__":
sys.exit(main())
1 change: 0 additions & 1 deletion systemgate/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
"""SystemGate package."""

1 change: 0 additions & 1 deletion systemgate/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,3 @@ def verify_key(key: str | None, encoded: str) -> bool:
_matched.clear()
_matched[encoded] = fast
return True

Loading
Loading