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.
- willitbite-0.1.0/.github/ISSUE_TEMPLATE/config.yml +10 -0
- willitbite-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +26 -0
- willitbite-0.1.0/.github/ISSUE_TEMPLATE/wrong_verdict.yml +49 -0
- willitbite-0.1.0/.github/dependabot.yml +27 -0
- willitbite-0.1.0/.github/workflows/ci.yml +105 -0
- willitbite-0.1.0/.github/workflows/release.yml +42 -0
- willitbite-0.1.0/.gitignore +7 -0
- willitbite-0.1.0/CHANGELOG.md +31 -0
- willitbite-0.1.0/CONTRIBUTING.md +66 -0
- willitbite-0.1.0/LICENSE +21 -0
- willitbite-0.1.0/PKG-INFO +202 -0
- willitbite-0.1.0/README.md +157 -0
- willitbite-0.1.0/SECURITY.md +29 -0
- willitbite-0.1.0/pyproject.toml +51 -0
- willitbite-0.1.0/src/willitbite/__init__.py +3 -0
- willitbite-0.1.0/src/willitbite/callsites.py +187 -0
- willitbite-0.1.0/src/willitbite/cli.py +189 -0
- willitbite-0.1.0/src/willitbite/escape.py +215 -0
- willitbite-0.1.0/src/willitbite/mutation.py +268 -0
- willitbite-0.1.0/src/willitbite/ruffrun.py +102 -0
- willitbite-0.1.0/src/willitbite/verdict.py +50 -0
- willitbite-0.1.0/tests/test_callsites.py +256 -0
- willitbite-0.1.0/tests/test_cli.py +149 -0
- willitbite-0.1.0/tests/test_escape.py +150 -0
- willitbite-0.1.0/tests/test_mutation.py +160 -0
|
@@ -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,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.
|
willitbite-0.1.0/LICENSE
ADDED
|
@@ -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
|
+
[](https://github.com/muhzuhaib/willitbite/actions/workflows/ci.yml)
|
|
49
|
+
[](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
|