willitbite 0.1.0__tar.gz → 0.1.1__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 → willitbite-0.1.1}/CHANGELOG.md +10 -0
- {willitbite-0.1.0 → willitbite-0.1.1}/PKG-INFO +9 -2
- {willitbite-0.1.0 → willitbite-0.1.1}/README.md +8 -1
- {willitbite-0.1.0 → willitbite-0.1.1}/src/willitbite/__init__.py +1 -1
- {willitbite-0.1.0 → willitbite-0.1.1}/src/willitbite/callsites.py +36 -2
- {willitbite-0.1.0 → willitbite-0.1.1}/tests/test_callsites.py +64 -0
- {willitbite-0.1.0 → willitbite-0.1.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {willitbite-0.1.0 → willitbite-0.1.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {willitbite-0.1.0 → willitbite-0.1.1}/.github/ISSUE_TEMPLATE/wrong_verdict.yml +0 -0
- {willitbite-0.1.0 → willitbite-0.1.1}/.github/dependabot.yml +0 -0
- {willitbite-0.1.0 → willitbite-0.1.1}/.github/workflows/ci.yml +0 -0
- {willitbite-0.1.0 → willitbite-0.1.1}/.github/workflows/release.yml +0 -0
- {willitbite-0.1.0 → willitbite-0.1.1}/.gitignore +0 -0
- {willitbite-0.1.0 → willitbite-0.1.1}/CONTRIBUTING.md +0 -0
- {willitbite-0.1.0 → willitbite-0.1.1}/LICENSE +0 -0
- {willitbite-0.1.0 → willitbite-0.1.1}/SECURITY.md +0 -0
- {willitbite-0.1.0 → willitbite-0.1.1}/pyproject.toml +0 -0
- {willitbite-0.1.0 → willitbite-0.1.1}/src/willitbite/cli.py +0 -0
- {willitbite-0.1.0 → willitbite-0.1.1}/src/willitbite/escape.py +0 -0
- {willitbite-0.1.0 → willitbite-0.1.1}/src/willitbite/mutation.py +0 -0
- {willitbite-0.1.0 → willitbite-0.1.1}/src/willitbite/ruffrun.py +0 -0
- {willitbite-0.1.0 → willitbite-0.1.1}/src/willitbite/verdict.py +0 -0
- {willitbite-0.1.0 → willitbite-0.1.1}/tests/test_cli.py +0 -0
- {willitbite-0.1.0 → willitbite-0.1.1}/tests/test_escape.py +0 -0
- {willitbite-0.1.0 → willitbite-0.1.1}/tests/test_mutation.py +0 -0
|
@@ -5,6 +5,16 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.1.1] - 2026-09-09
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Call sites written through an import alias (`from lib import collect as c`,
|
|
13
|
+
then `c(...)`) were invisible to the call-site pass. A warning whose only
|
|
14
|
+
visible caller supplied the argument could be cleared as `LATENT` while the
|
|
15
|
+
call that omits the argument sat in the same tree. Aliased calls are now
|
|
16
|
+
indexed under the imported name as well as the alias.
|
|
17
|
+
|
|
8
18
|
## [0.1.0] - 2026-09-08
|
|
9
19
|
|
|
10
20
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: willitbite
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Tell which of ruff's loop-closure and mutable-default warnings can actually reach you at runtime
|
|
5
5
|
Project-URL: Homepage, https://github.com/muhzuhaib/willitbite
|
|
6
6
|
Project-URL: Issues, https://github.com/muhzuhaib/willitbite/issues
|
|
@@ -180,12 +180,19 @@ inference, which this does not do, so a call counts whenever the called name mat
|
|
|
180
180
|
appears. That over-matches, and the over-matching is deliberate: an unrelated `send` elsewhere can
|
|
181
181
|
only add an omission, and an omission is the answer that keeps the warning.
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
Import aliases are resolved before the name match runs: `from lib import collect as c` followed by
|
|
184
|
+
`c(...)` is counted as a call to `collect`, including through relative imports. This matters because
|
|
185
|
+
an alias hides callers without adding any, and a caller the index cannot see counts as absent, which
|
|
186
|
+
is the direction a false `LATENT` comes from.
|
|
187
|
+
|
|
188
|
+
Four things count as no evidence at all, and each of them leaves a warning at `BITES`:
|
|
184
189
|
|
|
185
190
|
- a `**kwargs` splat at the call site, which might be carrying the argument
|
|
186
191
|
- a `*args` splat, which might be filling the position
|
|
187
192
|
- **no caller anywhere**, which usually means a public entry point called from outside the tree you
|
|
188
193
|
scanned, and is the case most likely to bite a stranger
|
|
194
|
+
- a name that reaches the function some other way: a star import, a rebinding like
|
|
195
|
+
`handler = collect`, or dispatch that only exists at runtime
|
|
189
196
|
|
|
190
197
|
The index is only built when something came back `BITES`, so a run that finds nothing reachable
|
|
191
198
|
never pays for the scan.
|
|
@@ -135,12 +135,19 @@ inference, which this does not do, so a call counts whenever the called name mat
|
|
|
135
135
|
appears. That over-matches, and the over-matching is deliberate: an unrelated `send` elsewhere can
|
|
136
136
|
only add an omission, and an omission is the answer that keeps the warning.
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
Import aliases are resolved before the name match runs: `from lib import collect as c` followed by
|
|
139
|
+
`c(...)` is counted as a call to `collect`, including through relative imports. This matters because
|
|
140
|
+
an alias hides callers without adding any, and a caller the index cannot see counts as absent, which
|
|
141
|
+
is the direction a false `LATENT` comes from.
|
|
142
|
+
|
|
143
|
+
Four things count as no evidence at all, and each of them leaves a warning at `BITES`:
|
|
139
144
|
|
|
140
145
|
- a `**kwargs` splat at the call site, which might be carrying the argument
|
|
141
146
|
- a `*args` splat, which might be filling the position
|
|
142
147
|
- **no caller anywhere**, which usually means a public entry point called from outside the tree you
|
|
143
148
|
scanned, and is the case most likely to bite a stranger
|
|
149
|
+
- a name that reaches the function some other way: a star import, a rebinding like
|
|
150
|
+
`handler = collect`, or dispatch that only exists at runtime
|
|
144
151
|
|
|
145
152
|
The index is only built when something came back `BITES`, so a run that finds nothing reachable
|
|
146
153
|
never pays for the scan.
|
|
@@ -15,7 +15,10 @@ to a definition properly needs type inference, which this does not have. So a
|
|
|
15
15
|
call is counted whenever the called name matches, wherever it appears. That
|
|
16
16
|
over-matches: an unrelated ``send`` in another module is counted too. The
|
|
17
17
|
over-matching is in the safe direction, because an extra caller can only ever
|
|
18
|
-
add an omission and an omission is the answer that keeps the warning.
|
|
18
|
+
add an omission and an omission is the answer that keeps the warning. Import
|
|
19
|
+
aliases are resolved (``from lib import collect as c`` files ``c(...)`` under
|
|
20
|
+
``collect`` too), because an alias hides callers without adding any, and
|
|
21
|
+
hidden callers are the one direction this pass must never err in.
|
|
19
22
|
|
|
20
23
|
**Everything unresolvable stays unresolved.** A ``**kwargs`` splat might be
|
|
21
24
|
carrying the argument, a ``*args`` splat might be filling the position, and a
|
|
@@ -78,6 +81,29 @@ def python_files(root):
|
|
|
78
81
|
yield os.path.join(folder, name)
|
|
79
82
|
|
|
80
83
|
|
|
84
|
+
def import_aliases(tree):
|
|
85
|
+
"""Map each local alias in ``tree`` to the imported name it stands for.
|
|
86
|
+
|
|
87
|
+
``from lib import collect as c`` makes ``c(...)`` a call to ``collect``, so
|
|
88
|
+
the index needs both spellings or the aliased callers vanish, and a caller
|
|
89
|
+
the index cannot see is a caller it counts as absent.
|
|
90
|
+
|
|
91
|
+
Only ``as`` bindings belong here. An import without one already matches by
|
|
92
|
+
its own name, and indexing it a second time would double-count its callers.
|
|
93
|
+
``import lib as l`` aliases a module, not a function. A star import names
|
|
94
|
+
nothing at all. Relative imports work because the module path is never
|
|
95
|
+
consulted, only the name being imported.
|
|
96
|
+
"""
|
|
97
|
+
aliases = {}
|
|
98
|
+
for node in ast.walk(tree):
|
|
99
|
+
if not isinstance(node, ast.ImportFrom) or node.module is None:
|
|
100
|
+
continue
|
|
101
|
+
for alias in node.names:
|
|
102
|
+
if alias.asname is not None:
|
|
103
|
+
aliases[alias.asname] = alias.name
|
|
104
|
+
return aliases
|
|
105
|
+
|
|
106
|
+
|
|
81
107
|
def index(root):
|
|
82
108
|
"""Map every called name under ``root`` to the calls that use it.
|
|
83
109
|
|
|
@@ -95,12 +121,20 @@ def index(root):
|
|
|
95
121
|
tree = ast.parse(handle.read())
|
|
96
122
|
except (OSError, SyntaxError, ValueError, UnicodeDecodeError):
|
|
97
123
|
continue
|
|
124
|
+
aliases = import_aliases(tree)
|
|
98
125
|
for node in ast.walk(tree):
|
|
99
126
|
if not isinstance(node, ast.Call):
|
|
100
127
|
continue
|
|
101
128
|
callee = node.func
|
|
102
129
|
if isinstance(callee, ast.Name):
|
|
103
|
-
|
|
130
|
+
call = Call(filename, node.lineno, node, False)
|
|
131
|
+
found[callee.id].append(call)
|
|
132
|
+
# Also under the imported name, so the definition sees its
|
|
133
|
+
# aliased callers. Purely additive: the alias spelling stays
|
|
134
|
+
# indexed for any same-named function of its own.
|
|
135
|
+
resolved = aliases.get(callee.id)
|
|
136
|
+
if resolved is not None:
|
|
137
|
+
found[resolved].append(call)
|
|
104
138
|
elif isinstance(callee, ast.Attribute):
|
|
105
139
|
found[callee.attr].append(Call(filename, node.lineno, node, True))
|
|
106
140
|
return found
|
|
@@ -179,6 +179,48 @@ def test_a_bound_method_call_that_omits_it_still_bites(tmp_path):
|
|
|
179
179
|
assert v.kind == BITES
|
|
180
180
|
|
|
181
181
|
|
|
182
|
+
# --------------------------------------------------------------------------
|
|
183
|
+
# Import aliases.
|
|
184
|
+
#
|
|
185
|
+
# ``from lib import collect as c`` spells every call ``c(...)``. The pass
|
|
186
|
+
# matches by name, so before aliases were resolved those callers were simply
|
|
187
|
+
# absent from the index, and absence reads as safety here: a warning whose
|
|
188
|
+
# only aliased caller omitted the argument came back LATENT with the call
|
|
189
|
+
# that triggers it sitting in the same tree. These are the tests that earn
|
|
190
|
+
# the feature.
|
|
191
|
+
# --------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_a_call_through_an_import_alias_is_still_found(tmp_path):
|
|
195
|
+
v = decide(
|
|
196
|
+
tmp_path,
|
|
197
|
+
{
|
|
198
|
+
"lib.py": MUTATES,
|
|
199
|
+
"app.py": "from lib import collect as c\ncollect(1, [])\nc(2)\n",
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
assert v.kind == BITES
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_an_alias_supplying_the_argument_is_evidence_for_latent(tmp_path):
|
|
206
|
+
v = decide(
|
|
207
|
+
tmp_path,
|
|
208
|
+
{"lib.py": MUTATES, "app.py": "from lib import collect as c\nc(1, [])\n"},
|
|
209
|
+
)
|
|
210
|
+
assert v.kind == LATENT
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_a_relative_import_alias_resolves_the_same_way(tmp_path):
|
|
214
|
+
v = decide(
|
|
215
|
+
tmp_path,
|
|
216
|
+
{
|
|
217
|
+
"lib.py": MUTATES,
|
|
218
|
+
"pkg/app.py": "from ..lib import collect as c\ncollect(1, [])\nc(2)\n",
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
assert v.kind == BITES
|
|
222
|
+
|
|
223
|
+
|
|
182
224
|
# --------------------------------------------------------------------------
|
|
183
225
|
# The index itself.
|
|
184
226
|
# --------------------------------------------------------------------------
|
|
@@ -210,6 +252,28 @@ def test_index_records_attribute_calls_under_the_attribute_name(tmp_path):
|
|
|
210
252
|
assert len(calls["send"]) == 2
|
|
211
253
|
|
|
212
254
|
|
|
255
|
+
def test_index_files_an_aliased_call_under_both_names(tmp_path):
|
|
256
|
+
calls = project(
|
|
257
|
+
tmp_path, {"app.py": "from lib import collect as c\nc(1)\nc(2)\n"}
|
|
258
|
+
)
|
|
259
|
+
assert len(calls["c"]) == 2
|
|
260
|
+
assert len(calls["collect"]) == 2
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_an_import_without_an_alias_is_indexed_exactly_once(tmp_path):
|
|
264
|
+
calls = project(
|
|
265
|
+
tmp_path, {"app.py": "from lib import collect\ncollect(1)\ncollect(2)\n"}
|
|
266
|
+
)
|
|
267
|
+
assert len(calls["collect"]) == 2
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_a_star_import_resolves_nothing(tmp_path):
|
|
271
|
+
"""A star import names no local alias, so there is nothing to resolve."""
|
|
272
|
+
calls = project(tmp_path, {"app.py": "from lib import *\nc(1)\n"})
|
|
273
|
+
assert len(calls["c"]) == 1
|
|
274
|
+
assert len(calls["collect"]) == 0
|
|
275
|
+
|
|
276
|
+
|
|
213
277
|
# --------------------------------------------------------------------------
|
|
214
278
|
# The pass must not disturb anything else.
|
|
215
279
|
# --------------------------------------------------------------------------
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|