-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathpyproject.toml
More file actions
197 lines (188 loc) · 10.8 KB
/
Copy pathpyproject.toml
File metadata and controls
197 lines (188 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
[project]
name = "claude-supertool"
version = "0.45.0"
description = "Batched file operations for autonomous Claude Code runs. Collapses N reads/greps/globs into one Bash round-trip."
readme = "README.md"
requires-python = ">=3.9"
license = { file = "LICENSE" }
authors = [{ name = "Digital Process Tools" }]
keywords = ["claude-code", "batch", "file-ops", "tokens", "autonomous", "kevin", "round-trip"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Utilities",
]
[project.scripts]
supertool = "supertool:_cli"
[project.urls]
Homepage = "https://github.com/Digital-Process-Tools/claude-supertool"
Repository = "https://github.com/Digital-Process-Tools/claude-supertool"
Issues = "https://github.com/Digital-Process-Tools/claude-supertool/issues"
Changelog = "https://github.com/Digital-Process-Tools/claude-supertool/blob/main/CHANGELOG.md"
[tool.pytest.ini_options]
testpaths = ["tests"]
# `-n auto` — the suite is ~50% kernel time (269 subprocess call sites, ~4000
# tests), so it parallelises well: 262s serial to 68s on 11 cores, and it still
# wins on a 2-core runner because the workers overlap CPU with process spawning.
# pytest-cov combines the per-worker data files, so the term report below
# measures the whole suite. Use `-n0` to run serially when debugging — xdist
# interleaves output and disables --pdb.
#
# `--cov` here is a *view*, not the gate, and there is deliberately no
# `--cov-fail-under` (#861). Two reasons, both about honesty:
#
# 1. It used to read `--cov=supertool --cov-fail-under=86`, which measured one
# file. Everything under `presets/` — ~14k statements, the entire op surface
# — was outside it, so a preset could ship with zero tests against a green
# number. `presets` is in the scope now so the inner loop at least sees it.
# 2. This measurement *understates* what is tested, and a floor on it would
# fire wrongly. 122 test modules drive a preset by spawning it, and coverage
# does not follow a child process without `parallel = true` and
# `COVERAGE_PROCESS_START`. Measured here, `presets/git/diff.py` reads 9%;
# its 600-line dedicated test module actually covers 83.7% of it. Sending
# somebody to write tests that already exist is not a gate doing its job.
#
# The floor that reds the build is `.github/scripts/coverage_gate.py`, run by
# the `coverage` job in `tests.yml`, which sets that environment up and reports
# what it measured *and what it did not*. One number, one owner. Run it locally
# with `python3 .github/scripts/coverage_gate.py`.
addopts = "-n auto --cov=supertool --cov=_supertool --cov=presets --cov-report=term-missing -m 'not slow and not benchmark'"
markers = [
"slow: opt-in heavy tests (>5s). Run with `pytest -m slow` or `pytest -m ''` to include.",
# `benchmark` is not a synonym for `slow`. A slow test is heavy and still
# deterministic, so running it costs time and buys a real answer. A
# benchmark asserts on elapsed wall-clock, which on a parallel or shared
# runner measures the machine as much as the code — it can only fail for a
# reason its author cannot act on, so it is excluded from the default run
# AND from the pre-push hook, which `slow` is not (#485). This applies
# equally to a *ratio* of two elapsed measurements (#503) — dividing one
# noisy sample by another does not make either sample deterministic.
#
# NOT the same thing: a hang-guard also reads a clock, but its margin is
# generous enough (2-10x the slowest sane run, not a tight regression
# gate) that it only ever fires for the failure it exists to catch — a
# loop that never terminates, a call that never returns — not for
# scheduler noise. `test_edge_cases_vim.py`'s subprocess timeouts are
# this: "did it come back at all" has one bit of signal wall-clock can
# give that nothing else can, and moving them to `benchmark` would delete
# real infinite-loop coverage to fix a flakiness problem they don't have.
#
# The mechanical test, because "reads a clock" catches both and decides
# nothing (#505): multiply the threshold by ten. If the assertion still
# catches the bug it was written for, it is a hang-guard — keep it in the
# default run and, if it is flaky, widen it rather than move it. If a 10x
# threshold makes it vacuous, the number *is* the assertion and it is a
# benchmark. Equivalently: ask what a failure would tell you. "The machine
# was busy" is a benchmark; "something did not terminate" is a hang-guard.
#
# Whole families sit on one side. Every timing assertion in the security
# suites — `test_security_tree_sitter.py`, `test_security_config.py`,
# `test_edge_cases_batch_security.py`, `test_security_mcp_daemon.py` —
# bounds a *defect*: ReDoS, a symlink loop, unguarded recursion, a DoS cap
# that must reject before it executes. The threshold there is 100x the real
# cost and exists so the test fails instead of hanging the suite. The #505
# audit classified 16 hang-guards across six files and marked none of them.
# `test_xml.py`'s parse-scaling assertions are the other side: they name a
# budget the code is meant to meet, and all four are marked.
"benchmark: wall-clock timing assertions measuring performance, not liveness. Run deliberately and serially: `pytest -m benchmark -n0`.",
]
[project.optional-dependencies]
dev = [
"pytest",
"pytest-cov",
"pytest-xdist",
"pytest-timeout",
# Not a test dependency in the usual sense: nothing imports it. It is here
# so `pip install -e .[dev]` gives a contributor the same linter the
# post-edit validator uses, and so `tests/test_validators_ruff.py` runs its
# ruff-present half instead of skipping into a green that proves nothing.
"ruff",
# Not the same reason as ruff any more. This was the CommonMark oracle for
# `tests/test_changelog_fragment_whitelist_934.py`; since #936 it is also
# the fragment guard itself, imported by
# `.github/scripts/assemble_changelog.py`. Three hand-written scanners in a
# row disagreed with CommonMark and all three were bypassed, so the guard
# and the reader are one parser now. Without it that script can no longer
# claim anything: it reports `skipped`, names what is missing and writes
# nothing. Nothing a *user* installs imports it — the script is a
# repo-internal release tool, and `supertool.py` is still stdlib-only.
"markdown-it-py",
# Same reason as ruff, for the validator whose tool is a library rather than
# a binary: `tests/test_yaml_check.py` skips every assertion about parsing
# YAML when PyYAML is absent, so without this line that half of the file
# runs nowhere. It ran nowhere for a year already and nobody could see it —
# the adapter answered an absent PyYAML with `ok: true`, so the tests passed
# on a fabricated verdict instead of skipping (#1202/#1213). Pinned by
# `test_pyyaml_is_a_dev_dependency_so_ci_runs_the_parsing_half`.
"pyyaml",
]
[tool.ruff]
# The ruleset is small on purpose, and the number that decided it is this: on
# the tree as it stood at #666, `--select ALL` reports 40,514 findings and
# ruff 0.16's own defaults report 2,213. Either one lands a post-edit
# validator that prints a wall of pre-existing style on every edit, and the
# first thing anyone does about a wall is switch the validator off — at which
# point it protects nothing. What is selected below is the correctness half
# and it sits at zero, so anything it ever prints was caused by the edit that
# triggered it.
#
# Growing it is expected. The bar for adding a category is that the tree can
# be brought to zero for it in a diff a reviewer can read — not that the
# category is on by default somewhere.
target-version = "py39"
[tool.ruff.lint]
# E9 — syntax and IO errors. The file did not parse or could not be read.
# F — pyflakes: undefined names, redefinitions, unused imports/variables.
# This is the category that found the real defect in #666 (an f-string
# in `presets/mcp/daemon.py` interpolating a name that does not exist,
# on the error path, where a NameError would replace the message it was
# written to print).
# B — bugbear: mutable default arguments, `raise` inside `except` without
# chaining, loop-variable capture. Behavioural traps, not layout.
# PLE — pylint's *error* tier only. Its warning and refactor tiers are style
# and are deliberately not here.
select = ["E9", "F", "B", "PLE"]
# Every entry is a rule that is on in the categories above and is switched off
# with a reason. None of them is off because it was inconvenient to satisfy.
ignore = [
# F401/F841/F541 — unused imports, unused locals, f-strings with no
# placeholder. 263 pre-existing occurrences across ~120 files. All three
# are worth having and none of them is worth landing here: the fix is
# `ruff check --fix` across half the repo, which is a diff no one can read
# line by line and which would bury this validator inside it. They come
# back on in the PR that does that cleanup and nothing else.
"F401",
"F841",
"F541",
]
[tool.ruff.lint.per-file-ignores]
# B023 — "function definition does not bind loop variable". 34 occurrences,
# every one of them in the vim `:s` action loop, and every one a false
# positive: the closures there (`_run_sub`, `_resolve_d`) are called inside
# the same iteration that defines them, which is the case B023 cannot see.
# Suppressing it at 34 call sites would cost 34 lines of noqa to buy nothing,
# so it is scoped off for the one file instead.
#
# The honest cost, stated rather than hidden: a *real* loop-capture bug newly
# introduced in supertool.py will not be caught. That is the trade — 34
# standing false positives against prospective coverage in one file — and it
# is reversible the day those closures are lifted out of the loop.
"_supertool.py" = ["B023"]
[tool.setuptools]
# Two top-level modules, declared explicitly so setuptools doesn't try to
# flat-layout-autodiscover (hooks/, presets/, notifiers/, etc. are tool dirs,
# not Python packages).
#
# `_supertool` is not optional here and its absence would not be loud: the
# console script imports `supertool`, which imports `_supertool`, so a wheel
# missing it installs cleanly and dies on first use. Only the non-editable
# install test catches that, and it is marked `slow` — outside the default
# run. tests/test_entry_point_shim_931.py pins the list against the tree.
py-modules = ["supertool", "_supertool"]