willitbite 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ blank_issues_enabled: false
2
+
3
+ contact_links:
4
+ - name: Question, or a verdict you want to discuss
5
+ url: https://github.com/muhzuhaib/willitbite/discussions
6
+ about: Anything that is not yet a wrong-verdict report or a concrete request.
7
+
8
+ - name: Security vulnerability
9
+ url: https://github.com/muhzuhaib/willitbite/security/advisories/new
10
+ about: Report privately. Please do not open a public issue for these.
@@ -0,0 +1,26 @@
1
+ name: Feature request
2
+ description: A warning, rule or decision the tool should handle and does not
3
+ labels: ["enhancement"]
4
+ body:
5
+ - type: textarea
6
+ id: problem
7
+ attributes:
8
+ label: What decision would this help you make
9
+ description: >
10
+ This tool exists to answer one question: which of these warnings can
11
+ reach me at runtime. Describing the decision rather than the feature
12
+ makes it much easier to tell whether it belongs here.
13
+ validations:
14
+ required: true
15
+
16
+ - type: textarea
17
+ id: proposal
18
+ attributes:
19
+ label: What you have in mind
20
+ validations:
21
+ required: true
22
+
23
+ - type: textarea
24
+ id: alternatives
25
+ attributes:
26
+ label: What you do today instead
@@ -0,0 +1,49 @@
1
+ name: Wrong verdict
2
+ description: The tool called a warning SAFE (or BITES) and you can show it is wrong
3
+ labels: ["bug"]
4
+ body:
5
+ - type: textarea
6
+ id: verdict
7
+ attributes:
8
+ label: What the tool claimed
9
+ description: >
10
+ The verdict block for the warning in question, from the tool's output
11
+ or --json-out. A verdict that is wrong is a defect even when nothing
12
+ crashed: the tool exists so you can stop reading the warnings it clears.
13
+ render: shell
14
+ validations:
15
+ required: true
16
+
17
+ - type: textarea
18
+ id: code
19
+ attributes:
20
+ label: The function it is wrong about
21
+ description: >
22
+ The function (and, for B023, the call site) that shows the claim is
23
+ wrong. Trimmed to the smallest thing that reproduces is ideal.
24
+ render: python
25
+ validations:
26
+ required: true
27
+
28
+ - type: textarea
29
+ id: why
30
+ attributes:
31
+ label: What actually happens when this runs
32
+ validations:
33
+ required: true
34
+
35
+ - type: input
36
+ id: version
37
+ attributes:
38
+ label: Version
39
+ description: Output of `willitbite --version`
40
+ validations:
41
+ required: true
42
+
43
+ - type: input
44
+ id: python
45
+ attributes:
46
+ label: Python, ruff and operating system
47
+ placeholder: "3.13, ruff 0.13, Windows 11"
48
+ validations:
49
+ required: true
@@ -0,0 +1,27 @@
1
+ version: 2
2
+
3
+ updates:
4
+ # Actions pin to a major tag, which keeps moving underneath until a major is
5
+ # deprecated and CI starts warning. A monthly check catches that without a
6
+ # pull request every week.
7
+ - package-ecosystem: github-actions
8
+ directory: "/"
9
+ schedule:
10
+ interval: monthly
11
+ commit-message:
12
+ prefix: "Update"
13
+ groups:
14
+ actions:
15
+ patterns: ["*"]
16
+
17
+ # There are no runtime dependencies to update, so this only ever covers the
18
+ # test tooling, which is safe to group.
19
+ - package-ecosystem: pip
20
+ directory: "/"
21
+ schedule:
22
+ interval: monthly
23
+ commit-message:
24
+ prefix: "Update"
25
+ groups:
26
+ dev-dependencies:
27
+ dependency-type: development
@@ -0,0 +1,105 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ${{ matrix.os }}
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ # Oldest supported and newest released. The versions in between rarely
15
+ # break independently and cost minutes for no signal.
16
+ python-version: ["3.10", "3.13"]
17
+ os: [ubuntu-latest]
18
+ include:
19
+ # The analysis reads files off disk and reports paths, both of which
20
+ # differ on Windows. One job keeps that honest.
21
+ - python-version: "3.13"
22
+ os: windows-latest
23
+
24
+ steps:
25
+ - uses: actions/checkout@v7
26
+
27
+ - uses: actions/setup-python@v7
28
+ with:
29
+ python-version: ${{ matrix.python-version }}
30
+ cache: pip
31
+
32
+ - name: Install
33
+ run: pip install -e ".[dev]"
34
+
35
+ - name: Test
36
+ run: pytest -q
37
+
38
+ selfcheck:
39
+ # The tool is run against its own source. A tool that reports on Python
40
+ # code and has never been pointed at any is untested in the way that
41
+ # matters, and this also proves the ruff subprocess path works, which the
42
+ # unit tests deliberately avoid depending on.
43
+ runs-on: ubuntu-latest
44
+ steps:
45
+ - uses: actions/checkout@v7
46
+
47
+ - uses: actions/setup-python@v7
48
+ with:
49
+ python-version: "3.13"
50
+
51
+ - name: Install
52
+ run: pip install -e ".[dev]"
53
+
54
+ - name: Run it on itself
55
+ run: willitbite src --all
56
+
57
+ install:
58
+ # Verifies the way a stranger gets it: build the wheel, install it into a
59
+ # clean environment with nothing else present, and run it against a file
60
+ # with a known answer. A suite that runs from the source tree would not
61
+ # notice a packaging mistake that leaves a module out of the wheel.
62
+ runs-on: ubuntu-latest
63
+ steps:
64
+ - uses: actions/checkout@v7
65
+
66
+ - uses: actions/setup-python@v7
67
+ with:
68
+ python-version: "3.13"
69
+
70
+ - name: Build the wheel
71
+ run: |
72
+ pip install build
73
+ python -m build --wheel
74
+
75
+ - name: Install it somewhere clean and run it
76
+ run: |
77
+ python -m venv /tmp/clean
78
+ /tmp/clean/bin/pip install --quiet dist/*.whl ruff
79
+ /tmp/clean/bin/willitbite --version
80
+ /tmp/clean/bin/willitbite --help
81
+
82
+ - name: Decide a file whose answer is known
83
+ run: |
84
+ mkdir -p /tmp/subject
85
+ cat > /tmp/subject/known.py <<'PY'
86
+ handlers = []
87
+ for i in range(3):
88
+ def handler():
89
+ return i
90
+ handlers.append(handler)
91
+
92
+
93
+ def safe(items=[]):
94
+ return len(items)
95
+ PY
96
+ /tmp/clean/bin/willitbite /tmp/subject --json-out > out.json || true
97
+ python -c "
98
+ import json
99
+ data = json.load(open('out.json'))
100
+ assert data['bites'] == 1, data
101
+ biting = [r for r in data['results'] if r['verdict'] == 'BITES']
102
+ assert biting[0]['code'] == 'B023', biting
103
+ assert 'handler' in biting[0]['reason'], biting
104
+ print('decided', data['warnings'], 'warnings,', data['bites'], 'reachable')
105
+ "
@@ -0,0 +1,42 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+
11
+ # Trusted publishing. PyPI verifies this workflow's identity through
12
+ # OpenID Connect and issues a short lived token for the upload, so there
13
+ # is no API token stored anywhere: not in a repository secret, not on a
14
+ # laptop, and nothing to leak or rotate.
15
+ permissions:
16
+ id-token: write
17
+ contents: read
18
+
19
+ steps:
20
+ - uses: actions/checkout@v7
21
+
22
+ - uses: actions/setup-python@v7
23
+ with:
24
+ python-version: "3.13"
25
+
26
+ - name: Check the tag matches the version in the package
27
+ # A tag that disagrees with __version__ publishes a release whose
28
+ # metadata lies about itself, and it cannot be corrected afterwards
29
+ # because PyPI does not allow a version to be reuploaded.
30
+ run: |
31
+ tag="${GITHUB_REF_NAME#v}"
32
+ version=$(python -c "import re,pathlib; print(re.search(r'__version__ = \"(.*)\"', pathlib.Path('src/willitbite/__init__.py').read_text()).group(1))")
33
+ echo "tag=$tag package=$version"
34
+ test "$tag" = "$version"
35
+
36
+ - name: Build
37
+ run: |
38
+ pip install build
39
+ python -m build
40
+
41
+ - name: Publish to PyPI
42
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
+
6
+ ## [Unreleased]
7
+
8
+ ## [0.1.0] - 2026-09-08
9
+
10
+ ### Added
11
+
12
+ - First release.
13
+
14
+ - B023 escape analysis: decides whether a closure that captures a loop variable
15
+ can still be called after the loop has moved on.
16
+ - B006 mutable default analysis: decides whether a shared default is ever
17
+ actually mutated, accounting for the common `items = items or []` rebind and
18
+ for augmented assignment, which mutates a list in place rather than rebinding.
19
+ - A third verdict, `CALLEE`, for the cases where the answer depends on a
20
+ function the tool cannot see into. It names that function instead of guessing.
21
+ - Call-site analysis for B006. A mutable default can only be corrupted by a
22
+ caller that omits the argument, so warnings that survive the first pass are
23
+ checked against every call site under the path. When all of them pass the
24
+ argument explicitly the verdict becomes `LATENT`: the defect is real and is
25
+ still reported, but nothing in the tree can trigger it today. Anything the
26
+ scan cannot resolve stays at `BITES`, including a `*args` or `**kwargs` splat
27
+ at the call site and the case of no caller at all, which usually means a
28
+ public entry point called from outside the tree.
29
+ - `LATENT` does not set a failing exit code. The exit code answers whether
30
+ something can bite today.
31
+ - Command line with `--json` input, `--json-out`, `--all` and `--exit-zero`.
@@ -0,0 +1,66 @@
1
+ # Contributing
2
+
3
+ Thanks for looking. This is a small tool with one job: decide which of ruff's
4
+ B006 and B023 warnings can actually reach you at runtime. The bar for a change
5
+ is whether it makes that decision more trustworthy.
6
+
7
+ ## The most valuable report
8
+
9
+ **A verdict that is wrong.** This tool does not just count warnings, it claims
10
+ things about your code: that a warning is `SAFE`, that a defect is `LATENT`,
11
+ that a closure `DEPENDS ON A CALLEE`. A warning marked `SAFE` that can really
12
+ bite is the worst defect this tool can have, because the whole point is that
13
+ you stop reading the ones it clears. A `BITES` verdict on code that is fine
14
+ costs the trust the tool lives on too. There is an issue form for exactly this.
15
+ Please include the tool's output for the file and the code of the function in
16
+ question; both together make a report reproducible in minutes.
17
+
18
+ ## Known limitations, stated so they do not come back as bug reports
19
+
20
+ - **A warning with no caller found anywhere stays `BITES`.** Absence of
21
+ evidence never downgrades anything. If the call site is behind a decorator,
22
+ a framework, or a language we do not model, the tool refuses to guess.
23
+ - **`*args` and `**kwargs` splats keep a warning.** A splat can pass anything,
24
+ including the omitted argument, so the honest verdict is the loud one.
25
+ - **`CALLEE` is a verdict on purpose.** When safety depends on what some other
26
+ function does with your callable, the tool names that function instead of
27
+ pretending to know. It will not chase into other modules for you.
28
+ - **Call sites are found within the path you scanned.** Point it at `./src`
29
+ and that is the world it reasons about.
30
+
31
+ ## Running the tests
32
+
33
+ ```console
34
+ pip install -e ".[dev]"
35
+ pytest -q
36
+ ```
37
+
38
+ Nothing in the suite touches the network and it does not need ruff installed:
39
+ the tests build ruff's own JSON output shape inline and run the real code path
40
+ over files written to a temporary directory.
41
+
42
+ ## What a change needs
43
+
44
+ - **A test that fails without it.** For anything that changes a verdict, the
45
+ test should be a control case: it must fail if the behaviour is removed, not
46
+ merely pass while it is present.
47
+ - **A note in the README if it changes what a verdict means.** The Design
48
+ decisions section is the contract people read before trusting an exit code.
49
+ - **One concern per pull request.** Small is easy to read and easy to merge.
50
+
51
+ ## What is deliberately out of scope
52
+
53
+ - **Other ruff rules.** B006 and B023 produce most of the noise when a project
54
+ adopts ruff's bugbear set, and depth on two rules beats shallowness on twenty.
55
+ A third rule is welcome as a proposal, with the same four-verdict honesty.
56
+ - **Fixing the code.** This decides what is worth a person's time; it does not
57
+ rewrite your functions.
58
+ - **Runtime dependencies.** The analysis uses the standard library's `ast`
59
+ module, and ruff is invoked as a subprocess only when it is already on your
60
+ PATH. A tool that tells you about your linter should not force a version of
61
+ it on you.
62
+
63
+ ## Code style
64
+
65
+ Plain Python, type hints on anything that crosses a module boundary, and
66
+ comments that say why rather than what. Line length 100.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Muhammad Zuhaib Zahid
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.5
2
+ Name: willitbite
3
+ Version: 0.1.0
4
+ Summary: Tell which of ruff's loop-closure and mutable-default warnings can actually reach you at runtime
5
+ Project-URL: Homepage, https://github.com/muhzuhaib/willitbite
6
+ Project-URL: Issues, https://github.com/muhzuhaib/willitbite/issues
7
+ Project-URL: Changelog, https://github.com/muhzuhaib/willitbite/blob/main/CHANGELOG.md
8
+ Author: Muhammad Zuhaib Zahid
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Muhammad Zuhaib Zahid
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: cli,linting,python,ruff,static-analysis,triage
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Environment :: Console
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.13
39
+ Classifier: Topic :: Software Development :: Quality Assurance
40
+ Requires-Python: >=3.10
41
+ Provides-Extra: dev
42
+ Requires-Dist: pytest>=8.0; extra == 'dev'
43
+ Requires-Dist: ruff>=0.6; extra == 'dev'
44
+ Description-Content-Type: text/markdown
45
+
46
+ # willitbite
47
+
48
+ [![CI](https://github.com/muhzuhaib/willitbite/actions/workflows/ci.yml/badge.svg)](https://github.com/muhzuhaib/willitbite/actions/workflows/ci.yml)
49
+ [![PyPI](https://img.shields.io/pypi/v/willitbite.svg)](https://pypi.org/project/willitbite/)
50
+
51
+ Ruff will tell you that 99 closures in your codebase capture a loop variable. It will not tell you
52
+ that none of them can actually bite you.
53
+
54
+ `willitbite` takes the two rules that produce most of the noise when a project first adopts ruff and
55
+ asks the question the linter cannot: **can this one reach you at runtime?**
56
+
57
+ Here it is on [ragflow](https://github.com/infiniflow/ragflow), 1,259 files of Python, at commit
58
+ `0c28d59`:
59
+
60
+ ```
61
+ $ willitbite .
62
+ B006 mutable default arguments
63
+ 34 warning(s) 0 can bite 1 latent 13 depend on a callee 20 safe
64
+ [LATENT] agent/component/message.py:159
65
+ `kwargs` is mutated before it is rebound (kwargs[...] assigned on line
66
+ 181), but all 2 call sites of `get_kwargs()` pass it explicitly, so the
67
+ shared default is never the one being changed
68
+
69
+ B023 closures capturing a loop variable
70
+ 99 warning(s) 0 can bite 15 depend on a callee 84 safe
71
+
72
+ Nothing here can bite today. 133 warning(s), 0 reachable defects, 1 latent.
73
+ ```
74
+
75
+ 133 warnings, ten seconds, and 29 of them are worth a person's time: the one latent
76
+ defect and the 28 that turn on a function the tool cannot see into. The other 104 need nobody.
77
+
78
+ ## Why this exists
79
+
80
+ Both rules describe a shape, and the shape is not the bug.
81
+
82
+ **B023** flags every closure inside a loop that reads the loop variable. The bug it is looking for is
83
+ late binding: if the closure is still callable after the loop moves on, every copy sees the loop
84
+ variable's final value. A closure that is built and called inside the same iteration sees the value
85
+ it was written next to, which is what the author meant.
86
+
87
+ **B006** flags every mutable default argument. A mutable default is evaluated once and shared by
88
+ every call that omits it, but that only matters if the function changes it. A default that is only
89
+ read behaves exactly like the immutable one the author probably had in mind.
90
+
91
+ So a team switching these rules on faces a few hundred warnings, most of which are fine, with no way
92
+ to tell which is which except by reading all of them. That is the job this does.
93
+
94
+ ## Install and run
95
+
96
+ ```
97
+ pip install willitbite
98
+ willitbite ./src
99
+ ```
100
+
101
+ Python 3.10 or newer. To work from a checkout instead:
102
+
103
+ ```
104
+ git clone https://github.com/muhzuhaib/willitbite
105
+ pip install ./willitbite
106
+ ```
107
+
108
+ Ruff is called as a subprocess if it is on your PATH. If you would rather run your own:
109
+
110
+ ```
111
+ ruff check --select B006,B023 --output-format json ./src > warnings.json
112
+ willitbite --json warnings.json
113
+ ```
114
+
115
+ | Flag | Effect |
116
+ | --- | --- |
117
+ | `--all` | list the safe warnings too, not just the actionable ones |
118
+ | `--json-out` | print results as JSON |
119
+ | `--exit-zero` | always exit 0, for a first run that should not fail a build |
120
+
121
+ Exit code is 1 when something can bite, 0 when nothing can, 2 when the tool could not run.
122
+
123
+ ## Design decisions
124
+
125
+ **A linter has two states, warned and silent. This has four.** Two of the four exist because the
126
+ honest answer is sometimes neither of the other two.
127
+
128
+ `CALLEE` is for a closure handed to `run_with_retry(...)`, which is safe if that helper calls it and
129
+ dangerous if it stores it. Guessing safe would clear a real defect. Guessing unsafe would raise a
130
+ false alarm on every codebase this was built against. So it names the function you have to look at
131
+ and stops there.
132
+
133
+ `LATENT` is for a function that is genuinely wrong and that nothing currently calls in the way that
134
+ would hurt. It has its own section below.
135
+
136
+ **Ruff finds the candidates; this decides them.** Reimplementing the rules would be slower, less
137
+ correct, and would drift from ruff's behaviour. Ruff is invoked with `--isolated` on purpose, so the
138
+ answer does not depend on whether the project has enabled these rules: a team that has not adopted
139
+ them yet is exactly the team that wants this.
140
+
141
+ **Ruff is not a dependency.** A tool that reports on your linter should not pin a version of it.
142
+
143
+ **Every verdict carries a reason you can check without rerunning anything.** The reason names the
144
+ identifier and the line, never restates the rule. A verdict you have to take on trust is worth about
145
+ as much as the warning it replaced.
146
+
147
+ ## Latent defects: real, but nothing calls them that way
148
+
149
+ A function that mutates its own mutable default is wrong on its own terms. Whether the wrongness can
150
+ reach you is a separate question, and it is answered in the callers: the shared default is only ever
151
+ the object being mutated when somebody omits the argument.
152
+
153
+ So B006 warnings that survive the first pass get a second one, across every `.py` file under the
154
+ path you gave. If some caller omits the argument, the verdict stays `BITES`. If every caller passes
155
+ it explicitly, the verdict becomes `LATENT`: still a defect, still reported, but nothing in the tree
156
+ triggers it today.
157
+
158
+ ```
159
+ B006 mutable default arguments
160
+ 2 warning(s) 1 can bite 1 latent 0 depend on a callee 0 safe
161
+ [BITES] live.py:1
162
+ `cache` is mutated before it is rebound (cache[...] assigned on line 2),
163
+ so every call that omits it sees the previous call's changes
164
+ [LATENT] lib.py:1
165
+ `items` is mutated before it is rebound (items.append() on line 2), but all
166
+ 2 call sites of `collect()` pass it explicitly, so the shared default is
167
+ never the one being changed
168
+ ```
169
+
170
+ Those two functions have the same shape. Only their callers differ.
171
+
172
+ **A latent defect does not fail the build.** The exit code answers "can this bite today", and this
173
+ one cannot, so failing on it would fail every build until somebody rewrote code that currently
174
+ works. It is in the report because it will bite the first caller who leaves the argument out.
175
+
176
+ ## Known boundary: which callers get matched
177
+
178
+ Call sites are matched **by name**. Resolving `x.send()` to a definition properly needs type
179
+ inference, which this does not do, so a call counts whenever the called name matches, wherever it
180
+ appears. That over-matches, and the over-matching is deliberate: an unrelated `send` elsewhere can
181
+ only add an omission, and an omission is the answer that keeps the warning.
182
+
183
+ Three things count as no evidence at all, and each of them leaves a warning at `BITES`:
184
+
185
+ - a `**kwargs` splat at the call site, which might be carrying the argument
186
+ - a `*args` splat, which might be filling the position
187
+ - **no caller anywhere**, which usually means a public entry point called from outside the tree you
188
+ scanned, and is the case most likely to bite a stranger
189
+
190
+ The index is only built when something came back `BITES`, so a run that finds nothing reachable
191
+ never pays for the scan.
192
+
193
+ ## Where this came from
194
+
195
+ It was written while triaging 138 ruff warnings across two large open-source Python codebases by
196
+ hand. All 99 loop-closure warnings turned out to be false alarms, as did every mutable-default
197
+ warning that was actually reachable. Reading them one at a time to learn that took most of a day,
198
+ which seemed like a poor way to spend the next one.
199
+
200
+ ## License
201
+
202
+ MIT