assert-python-definition-is-used 20260829003639__tar.gz → 20260830020719__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.
- assert_python_definition_is_used-20260830020719/.claude/memories/pylint-refactor-messages-are-hard-failures.md +9 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/.github/workflows/documentation.yml +2 -2
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/.github/workflows/release.yml +32 -32
- {assert_python_definition_is_used-20260829003639/src/assert_python_definition_is_used.egg-info → assert_python_definition_is_used-20260830020719}/PKG-INFO +64 -2
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/README.md +63 -1
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/action.yml +6 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/src/assert_python_definition_is_used/cli.py +45 -3
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/src/assert_python_definition_is_used/scanner.py +45 -5
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719/src/assert_python_definition_is_used.egg-info}/PKG-INFO +64 -2
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/src/assert_python_definition_is_used.egg-info/SOURCES.txt +1 -1
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/src/assert_python_definition_is_used.egg-info/scm_file_list.json +1 -1
- assert_python_definition_is_used-20260830020719/src/assert_python_definition_is_used.egg-info/scm_version.json +8 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/test/integration/test_cli.py +44 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/test/integration/test_helpers.py +59 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/test/samples.py +52 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/test/unit/test_cli.py +87 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/test/unit/test_scanner.py +114 -0
- assert_python_definition_is_used-20260829003639/.claude/memories/disable-pylint-docstring-checks-on-the-command-line.md +0 -8
- assert_python_definition_is_used-20260829003639/src/assert_python_definition_is_used.egg-info/scm_version.json +0 -8
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/.claude/memories/ask-gh-before-saying-github-cannot.md +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/.claude/memories/commit-straight-to-main.md +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/.claude/memories/cover-new-source-in-both-tiers.md +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/.claude/memories/do-not-offer-a-pointless-choice.md +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/.claude/memories/do-what-the-issue-says-and-stop.md +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/.claude/memories/jscpd-reads-a-docstring-as-ordinary-tokens.md +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/.claude/memories/let-ci-run-the-checks.md +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/.claude/memories/name-a-conftest-fixture-apart-from-what-it-asks-for.md +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/.claude/memories/name-a-job-after-the-package-it-runs.md +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/.claude/memories/run-the-tool-before-writing-the-claim.md +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/.claude/memories/start-over-when-the-user-does-not-understand.md +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/.gitignore +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/CLAUDE.md +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/LICENSE.txt +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/pyproject.toml +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/setup.cfg +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/src/assert_python_definition_is_used/__init__.py +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/src/assert_python_definition_is_used/__main__.py +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/src/assert_python_definition_is_used/py.typed +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/src/assert_python_definition_is_used.egg-info/dependency_links.txt +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/src/assert_python_definition_is_used.egg-info/entry_points.txt +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/src/assert_python_definition_is_used.egg-info/top_level.txt +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/test/__init__.py +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/test/conftest.py +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/test/e2e/__init__.py +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/test/e2e/conftest.py +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/test/e2e/test_e2e.py +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/test/integration/__init__.py +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/test/runners.py +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/test/unit/__init__.py +0 -0
- {assert_python_definition_is_used-20260829003639 → assert_python_definition_is_used-20260830020719}/test/unit/test_main.py +0 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# pylint refactor messages are hard failures
|
|
2
|
+
|
|
3
|
+
`--fail-on=C,R,W` means pylint's R messages fail the job, so its default
|
|
4
|
+
limits are hard limits here: 5 arguments, 15 locals, 12 branches, 50
|
|
5
|
+
statements per function. Inline directives are banned, so the only route
|
|
6
|
+
is to split the function.
|
|
7
|
+
|
|
8
|
+
That is why `main` hands its work to `_findings_for` rather than growing
|
|
9
|
+
another local for each mode it grew.
|
|
@@ -7,9 +7,9 @@ jobs:
|
|
|
7
7
|
runs-on: ubuntu-latest
|
|
8
8
|
steps:
|
|
9
9
|
- name: Checkout code
|
|
10
|
-
uses: actions/checkout@
|
|
10
|
+
uses: actions/checkout@v7
|
|
11
11
|
- name: Set up Node
|
|
12
|
-
uses: actions/setup-node@
|
|
12
|
+
uses: actions/setup-node@v7
|
|
13
13
|
with:
|
|
14
14
|
node-version: "20"
|
|
15
15
|
- name: Install dependencies
|
|
@@ -7,9 +7,9 @@ jobs:
|
|
|
7
7
|
runs-on: ubuntu-latest
|
|
8
8
|
steps:
|
|
9
9
|
- name: Checkout code
|
|
10
|
-
uses: actions/checkout@
|
|
10
|
+
uses: actions/checkout@v7
|
|
11
11
|
- name: Set up Python
|
|
12
|
-
uses: actions/setup-python@
|
|
12
|
+
uses: actions/setup-python@v7
|
|
13
13
|
with:
|
|
14
14
|
python-version: "3.13"
|
|
15
15
|
- name: Install dependencies
|
|
@@ -23,9 +23,9 @@ jobs:
|
|
|
23
23
|
runs-on: ubuntu-latest
|
|
24
24
|
steps:
|
|
25
25
|
- name: Checkout code
|
|
26
|
-
uses: actions/checkout@
|
|
26
|
+
uses: actions/checkout@v7
|
|
27
27
|
- name: Set up Python
|
|
28
|
-
uses: actions/setup-python@
|
|
28
|
+
uses: actions/setup-python@v7
|
|
29
29
|
with:
|
|
30
30
|
python-version: "3.13"
|
|
31
31
|
- name: Install dependencies
|
|
@@ -40,9 +40,9 @@ jobs:
|
|
|
40
40
|
runs-on: ubuntu-latest
|
|
41
41
|
steps:
|
|
42
42
|
- name: Checkout code
|
|
43
|
-
uses: actions/checkout@
|
|
43
|
+
uses: actions/checkout@v7
|
|
44
44
|
- name: Set up Python
|
|
45
|
-
uses: actions/setup-python@
|
|
45
|
+
uses: actions/setup-python@v7
|
|
46
46
|
with:
|
|
47
47
|
python-version: "3.13"
|
|
48
48
|
- name: Install dependencies
|
|
@@ -57,9 +57,9 @@ jobs:
|
|
|
57
57
|
runs-on: ubuntu-latest
|
|
58
58
|
steps:
|
|
59
59
|
- name: Checkout code
|
|
60
|
-
uses: actions/checkout@
|
|
60
|
+
uses: actions/checkout@v7
|
|
61
61
|
- name: Set up Python
|
|
62
|
-
uses: actions/setup-python@
|
|
62
|
+
uses: actions/setup-python@v7
|
|
63
63
|
with:
|
|
64
64
|
python-version: "3.13"
|
|
65
65
|
- name: Install dependencies
|
|
@@ -73,9 +73,9 @@ jobs:
|
|
|
73
73
|
runs-on: ubuntu-latest
|
|
74
74
|
steps:
|
|
75
75
|
- name: Checkout code
|
|
76
|
-
uses: actions/checkout@
|
|
76
|
+
uses: actions/checkout@v7
|
|
77
77
|
- name: Set up Python
|
|
78
|
-
uses: actions/setup-python@
|
|
78
|
+
uses: actions/setup-python@v7
|
|
79
79
|
with:
|
|
80
80
|
python-version: "3.13"
|
|
81
81
|
- name: Install dependencies
|
|
@@ -91,9 +91,9 @@ jobs:
|
|
|
91
91
|
runs-on: ubuntu-latest
|
|
92
92
|
steps:
|
|
93
93
|
- name: Checkout code
|
|
94
|
-
uses: actions/checkout@
|
|
94
|
+
uses: actions/checkout@v7
|
|
95
95
|
- name: Set up Python
|
|
96
|
-
uses: actions/setup-python@
|
|
96
|
+
uses: actions/setup-python@v7
|
|
97
97
|
with:
|
|
98
98
|
python-version: "3.13"
|
|
99
99
|
- name: Install dependencies
|
|
@@ -104,9 +104,9 @@ jobs:
|
|
|
104
104
|
runs-on: ubuntu-latest
|
|
105
105
|
steps:
|
|
106
106
|
- name: Checkout code
|
|
107
|
-
uses: actions/checkout@
|
|
107
|
+
uses: actions/checkout@v7
|
|
108
108
|
- name: Set up Python
|
|
109
|
-
uses: actions/setup-python@
|
|
109
|
+
uses: actions/setup-python@v7
|
|
110
110
|
with:
|
|
111
111
|
python-version: "3.13"
|
|
112
112
|
- name: Install dependencies
|
|
@@ -123,9 +123,9 @@ jobs:
|
|
|
123
123
|
runs-on: ubuntu-latest
|
|
124
124
|
steps:
|
|
125
125
|
- name: Checkout code
|
|
126
|
-
uses: actions/checkout@
|
|
126
|
+
uses: actions/checkout@v7
|
|
127
127
|
- name: Set up Node
|
|
128
|
-
uses: actions/setup-node@
|
|
128
|
+
uses: actions/setup-node@v7
|
|
129
129
|
with:
|
|
130
130
|
node-version: "20"
|
|
131
131
|
- name: Install dependencies
|
|
@@ -136,9 +136,9 @@ jobs:
|
|
|
136
136
|
runs-on: ubuntu-latest
|
|
137
137
|
steps:
|
|
138
138
|
- name: Checkout code
|
|
139
|
-
uses: actions/checkout@
|
|
139
|
+
uses: actions/checkout@v7
|
|
140
140
|
- name: Set up Node
|
|
141
|
-
uses: actions/setup-node@
|
|
141
|
+
uses: actions/setup-node@v7
|
|
142
142
|
with:
|
|
143
143
|
node-version: "20"
|
|
144
144
|
- name: Install dependencies
|
|
@@ -149,9 +149,9 @@ jobs:
|
|
|
149
149
|
runs-on: ubuntu-latest
|
|
150
150
|
steps:
|
|
151
151
|
- name: Checkout code
|
|
152
|
-
uses: actions/checkout@
|
|
152
|
+
uses: actions/checkout@v7
|
|
153
153
|
- name: Set up Python
|
|
154
|
-
uses: actions/setup-python@
|
|
154
|
+
uses: actions/setup-python@v7
|
|
155
155
|
with:
|
|
156
156
|
python-version: "3.13"
|
|
157
157
|
- name: Install dependencies
|
|
@@ -162,9 +162,9 @@ jobs:
|
|
|
162
162
|
runs-on: ubuntu-latest
|
|
163
163
|
steps:
|
|
164
164
|
- name: Checkout code
|
|
165
|
-
uses: actions/checkout@
|
|
165
|
+
uses: actions/checkout@v7
|
|
166
166
|
- name: Set up Python
|
|
167
|
-
uses: actions/setup-python@
|
|
167
|
+
uses: actions/setup-python@v7
|
|
168
168
|
with:
|
|
169
169
|
python-version: "3.13"
|
|
170
170
|
- name: Install dependencies
|
|
@@ -175,9 +175,9 @@ jobs:
|
|
|
175
175
|
runs-on: ubuntu-latest
|
|
176
176
|
steps:
|
|
177
177
|
- name: Checkout code
|
|
178
|
-
uses: actions/checkout@
|
|
178
|
+
uses: actions/checkout@v7
|
|
179
179
|
- name: Set up Python
|
|
180
|
-
uses: actions/setup-python@
|
|
180
|
+
uses: actions/setup-python@v7
|
|
181
181
|
with:
|
|
182
182
|
python-version: "3.13"
|
|
183
183
|
- name: Install dependencies
|
|
@@ -194,9 +194,9 @@ jobs:
|
|
|
194
194
|
runs-on: ubuntu-latest
|
|
195
195
|
steps:
|
|
196
196
|
- name: Checkout code
|
|
197
|
-
uses: actions/checkout@
|
|
197
|
+
uses: actions/checkout@v7
|
|
198
198
|
- name: Set up Python
|
|
199
|
-
uses: actions/setup-python@
|
|
199
|
+
uses: actions/setup-python@v7
|
|
200
200
|
with:
|
|
201
201
|
python-version: "3.13"
|
|
202
202
|
- name: Install dependencies
|
|
@@ -233,11 +233,11 @@ jobs:
|
|
|
233
233
|
runs-on: ubuntu-latest
|
|
234
234
|
steps:
|
|
235
235
|
- name: Checkout code
|
|
236
|
-
uses: actions/checkout@
|
|
236
|
+
uses: actions/checkout@v7
|
|
237
237
|
with:
|
|
238
238
|
fetch-depth: 0
|
|
239
239
|
- name: Set up Python
|
|
240
|
-
uses: actions/setup-python@
|
|
240
|
+
uses: actions/setup-python@v7
|
|
241
241
|
with:
|
|
242
242
|
python-version: "3.13"
|
|
243
243
|
- id: version
|
|
@@ -267,9 +267,9 @@ jobs:
|
|
|
267
267
|
runs-on: ubuntu-latest
|
|
268
268
|
steps:
|
|
269
269
|
- name: Checkout code
|
|
270
|
-
uses: actions/checkout@
|
|
270
|
+
uses: actions/checkout@v7
|
|
271
271
|
- name: Set up Python
|
|
272
|
-
uses: actions/setup-python@
|
|
272
|
+
uses: actions/setup-python@v7
|
|
273
273
|
with:
|
|
274
274
|
python-version: "3.13"
|
|
275
275
|
- name: Install dependencies
|
|
@@ -286,9 +286,9 @@ jobs:
|
|
|
286
286
|
runs-on: ubuntu-latest
|
|
287
287
|
steps:
|
|
288
288
|
- name: Checkout code
|
|
289
|
-
uses: actions/checkout@
|
|
289
|
+
uses: actions/checkout@v7
|
|
290
290
|
- name: Set up Python
|
|
291
|
-
uses: actions/setup-python@
|
|
291
|
+
uses: actions/setup-python@v7
|
|
292
292
|
with:
|
|
293
293
|
python-version: "3.13"
|
|
294
294
|
- name: Install dependencies
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: assert-python-definition-is-used
|
|
3
|
-
Version:
|
|
3
|
+
Version: 20260830020719
|
|
4
4
|
Summary: CLI tool to assert that every public Python definition in a tree is used somewhere else
|
|
5
5
|
Author-email: 10U Labs <dev@10ulabs.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -76,6 +76,7 @@ tests written for it, which is what a coverage gate hides.
|
|
|
76
76
|
| `--exclude PATTERNS` | Comma-separated globs to leave out of both trees. |
|
|
77
77
|
| `--assume-used-matching PATTERNS` | Globs of names a runtime invokes. |
|
|
78
78
|
| `--assume-used-decorated-with PATHS` | Decorators a runtime invokes. |
|
|
79
|
+
| `--unimported-packages` | Report packages nothing imports, not definitions. |
|
|
79
80
|
| `--quiet` | Print nothing; report through the exit code. |
|
|
80
81
|
| `--count` | Print only how many findings there were. |
|
|
81
82
|
| `--verbose` | Print each file scanned, each one read as text, and a summary. |
|
|
@@ -120,7 +121,8 @@ file rather than failing, and `--verbose` names each file that fell back.
|
|
|
120
121
|
|
|
121
122
|
Reading a name is still not resolving it. A file that calls its own
|
|
122
123
|
unrelated function of the same name counts as a use, so the count is a
|
|
123
|
-
lower bound rather than an exact figure.
|
|
124
|
+
lower bound rather than an exact figure. `--unimported-packages` below
|
|
125
|
+
asks a question that survives that, one package at a time.
|
|
124
126
|
|
|
125
127
|
Two dead functions in one file that call each other still read as used,
|
|
126
128
|
because those calls are real code. Finding those needs reachability from
|
|
@@ -192,6 +194,53 @@ the run altogether, so its own definitions go unchecked too. A file left
|
|
|
192
194
|
out of the search is still read for the definitions it holds; it just
|
|
193
195
|
does not get a vote on whether anything else is used.
|
|
194
196
|
|
|
197
|
+
## Packages nothing imports
|
|
198
|
+
|
|
199
|
+
A function is reached by being named, so asking what names it is the
|
|
200
|
+
right question. A package is reached by being imported, which is a
|
|
201
|
+
different question with a different answer. A package can be entirely
|
|
202
|
+
dead while every one of its names still appears somewhere, because the
|
|
203
|
+
caller that reimplemented it picked the same ordinary words.
|
|
204
|
+
|
|
205
|
+
The name search is blind in proportion to how ordinary a package's names
|
|
206
|
+
are. A package of `get_client`, `set_client` and `reset_clients` is
|
|
207
|
+
nearly invisible to it, those words appearing everywhere; a package of
|
|
208
|
+
`resolve_terraform_interpolation` is fully visible, that word appearing
|
|
209
|
+
nowhere else. That is backwards, because the packages most likely to be
|
|
210
|
+
quietly reimplemented are exactly the ones whose names are ordinary
|
|
211
|
+
enough for a caller to pick the same ones.
|
|
212
|
+
|
|
213
|
+
`--unimported-packages` asks the other question. It reads every directory
|
|
214
|
+
directly below a definition tree that holds an `__init__.py`, collects
|
|
215
|
+
the top-level module of every `import` and `from ... import` in the
|
|
216
|
+
searched files, and reports each package nothing imports. The finding is
|
|
217
|
+
anchored on the package's `__init__.py` at line 1, in the same
|
|
218
|
+
`path:line:name` form as the rest:
|
|
219
|
+
|
|
220
|
+
```text
|
|
221
|
+
lib/python/aws_clients/__init__.py:1:aws_clients
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`--dont-search-in` applies here as it does elsewhere, so a package
|
|
225
|
+
imported only by its own tests is reported:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
assert-python-definition-is-used lib/python \
|
|
229
|
+
--search-in lib/python --search-in src --search-in test \
|
|
230
|
+
--dont-search-in 'test/lib/python/test_{package}' \
|
|
231
|
+
--unimported-packages
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
It is a mode rather than a check folded into every run, because the two
|
|
235
|
+
questions have different answers for a directory with no `__init__.py`,
|
|
236
|
+
and because a repository may want one without the other. Run it as a
|
|
237
|
+
second job beside the definition run.
|
|
238
|
+
|
|
239
|
+
A relative import names no top-level module, so `from . import helper`
|
|
240
|
+
inside a package does not make that package imported. A file that will
|
|
241
|
+
not parse credits nothing here either: a bare word in text is not an
|
|
242
|
+
import, and guessing one from text would be worse than staying quiet.
|
|
243
|
+
|
|
195
244
|
## GitHub Actions
|
|
196
245
|
|
|
197
246
|
```yaml
|
|
@@ -204,6 +253,19 @@ does not get a vote on whether anything else is used.
|
|
|
204
253
|
verbose: true
|
|
205
254
|
```
|
|
206
255
|
|
|
256
|
+
Beside it, ask whether the packages are imported at all:
|
|
257
|
+
|
|
258
|
+
```yaml
|
|
259
|
+
- name: Assert every package is imported outside its own tests
|
|
260
|
+
uses: 10U-Labs/assert-python-definition-is-used@latest
|
|
261
|
+
with:
|
|
262
|
+
dont-search-in: test/lib/python/test_{package}
|
|
263
|
+
search-in: lib/python scripts src test
|
|
264
|
+
trees: lib/python
|
|
265
|
+
unimported-packages: true
|
|
266
|
+
verbose: true
|
|
267
|
+
```
|
|
268
|
+
|
|
207
269
|
Over a test tree, name what pytest invokes:
|
|
208
270
|
|
|
209
271
|
```yaml
|
|
@@ -52,6 +52,7 @@ tests written for it, which is what a coverage gate hides.
|
|
|
52
52
|
| `--exclude PATTERNS` | Comma-separated globs to leave out of both trees. |
|
|
53
53
|
| `--assume-used-matching PATTERNS` | Globs of names a runtime invokes. |
|
|
54
54
|
| `--assume-used-decorated-with PATHS` | Decorators a runtime invokes. |
|
|
55
|
+
| `--unimported-packages` | Report packages nothing imports, not definitions. |
|
|
55
56
|
| `--quiet` | Print nothing; report through the exit code. |
|
|
56
57
|
| `--count` | Print only how many findings there were. |
|
|
57
58
|
| `--verbose` | Print each file scanned, each one read as text, and a summary. |
|
|
@@ -96,7 +97,8 @@ file rather than failing, and `--verbose` names each file that fell back.
|
|
|
96
97
|
|
|
97
98
|
Reading a name is still not resolving it. A file that calls its own
|
|
98
99
|
unrelated function of the same name counts as a use, so the count is a
|
|
99
|
-
lower bound rather than an exact figure.
|
|
100
|
+
lower bound rather than an exact figure. `--unimported-packages` below
|
|
101
|
+
asks a question that survives that, one package at a time.
|
|
100
102
|
|
|
101
103
|
Two dead functions in one file that call each other still read as used,
|
|
102
104
|
because those calls are real code. Finding those needs reachability from
|
|
@@ -168,6 +170,53 @@ the run altogether, so its own definitions go unchecked too. A file left
|
|
|
168
170
|
out of the search is still read for the definitions it holds; it just
|
|
169
171
|
does not get a vote on whether anything else is used.
|
|
170
172
|
|
|
173
|
+
## Packages nothing imports
|
|
174
|
+
|
|
175
|
+
A function is reached by being named, so asking what names it is the
|
|
176
|
+
right question. A package is reached by being imported, which is a
|
|
177
|
+
different question with a different answer. A package can be entirely
|
|
178
|
+
dead while every one of its names still appears somewhere, because the
|
|
179
|
+
caller that reimplemented it picked the same ordinary words.
|
|
180
|
+
|
|
181
|
+
The name search is blind in proportion to how ordinary a package's names
|
|
182
|
+
are. A package of `get_client`, `set_client` and `reset_clients` is
|
|
183
|
+
nearly invisible to it, those words appearing everywhere; a package of
|
|
184
|
+
`resolve_terraform_interpolation` is fully visible, that word appearing
|
|
185
|
+
nowhere else. That is backwards, because the packages most likely to be
|
|
186
|
+
quietly reimplemented are exactly the ones whose names are ordinary
|
|
187
|
+
enough for a caller to pick the same ones.
|
|
188
|
+
|
|
189
|
+
`--unimported-packages` asks the other question. It reads every directory
|
|
190
|
+
directly below a definition tree that holds an `__init__.py`, collects
|
|
191
|
+
the top-level module of every `import` and `from ... import` in the
|
|
192
|
+
searched files, and reports each package nothing imports. The finding is
|
|
193
|
+
anchored on the package's `__init__.py` at line 1, in the same
|
|
194
|
+
`path:line:name` form as the rest:
|
|
195
|
+
|
|
196
|
+
```text
|
|
197
|
+
lib/python/aws_clients/__init__.py:1:aws_clients
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`--dont-search-in` applies here as it does elsewhere, so a package
|
|
201
|
+
imported only by its own tests is reported:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
assert-python-definition-is-used lib/python \
|
|
205
|
+
--search-in lib/python --search-in src --search-in test \
|
|
206
|
+
--dont-search-in 'test/lib/python/test_{package}' \
|
|
207
|
+
--unimported-packages
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
It is a mode rather than a check folded into every run, because the two
|
|
211
|
+
questions have different answers for a directory with no `__init__.py`,
|
|
212
|
+
and because a repository may want one without the other. Run it as a
|
|
213
|
+
second job beside the definition run.
|
|
214
|
+
|
|
215
|
+
A relative import names no top-level module, so `from . import helper`
|
|
216
|
+
inside a package does not make that package imported. A file that will
|
|
217
|
+
not parse credits nothing here either: a bare word in text is not an
|
|
218
|
+
import, and guessing one from text would be worse than staying quiet.
|
|
219
|
+
|
|
171
220
|
## GitHub Actions
|
|
172
221
|
|
|
173
222
|
```yaml
|
|
@@ -180,6 +229,19 @@ does not get a vote on whether anything else is used.
|
|
|
180
229
|
verbose: true
|
|
181
230
|
```
|
|
182
231
|
|
|
232
|
+
Beside it, ask whether the packages are imported at all:
|
|
233
|
+
|
|
234
|
+
```yaml
|
|
235
|
+
- name: Assert every package is imported outside its own tests
|
|
236
|
+
uses: 10U-Labs/assert-python-definition-is-used@latest
|
|
237
|
+
with:
|
|
238
|
+
dont-search-in: test/lib/python/test_{package}
|
|
239
|
+
search-in: lib/python scripts src test
|
|
240
|
+
trees: lib/python
|
|
241
|
+
unimported-packages: true
|
|
242
|
+
verbose: true
|
|
243
|
+
```
|
|
244
|
+
|
|
183
245
|
Over a test tree, name what pytest invokes:
|
|
184
246
|
|
|
185
247
|
```yaml
|
|
@@ -27,6 +27,10 @@ inputs:
|
|
|
27
27
|
trees:
|
|
28
28
|
description: Trees holding the definitions to check (space-separated)
|
|
29
29
|
required: true
|
|
30
|
+
unimported-packages:
|
|
31
|
+
default: "false"
|
|
32
|
+
description: Report packages nothing imports, instead of definitions
|
|
33
|
+
required: false
|
|
30
34
|
verbose:
|
|
31
35
|
default: "false"
|
|
32
36
|
description: Show trees read, definitions found, findings and a summary
|
|
@@ -46,6 +50,8 @@ runs:
|
|
|
46
50
|
[ "${{ inputs.verbose }}" = "true" ] && ARGS="$ARGS --verbose"
|
|
47
51
|
[ "${{ inputs.fail-fast }}" = "true" ] && ARGS="$ARGS --fail-fast"
|
|
48
52
|
[ "${{ inputs.warn-only }}" = "true" ] && ARGS="$ARGS --warn-only"
|
|
53
|
+
[ "${{ inputs.unimported-packages }}" = "true" ] &&
|
|
54
|
+
ARGS="$ARGS --unimported-packages"
|
|
49
55
|
if [ -n "${{ inputs.exclude }}" ]; then
|
|
50
56
|
ARGS="$ARGS --exclude '${{ inputs.exclude }}'"
|
|
51
57
|
fi
|
|
@@ -15,6 +15,7 @@ from .scanner import (
|
|
|
15
15
|
assumed_used,
|
|
16
16
|
public_definitions,
|
|
17
17
|
read_searched,
|
|
18
|
+
unimported_packages,
|
|
18
19
|
unused_definitions,
|
|
19
20
|
)
|
|
20
21
|
|
|
@@ -106,6 +107,16 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
106
107
|
),
|
|
107
108
|
)
|
|
108
109
|
|
|
110
|
+
parser.add_argument(
|
|
111
|
+
"--unimported-packages",
|
|
112
|
+
action="store_true",
|
|
113
|
+
help=(
|
|
114
|
+
"Ask the package question instead: report every package under the trees "
|
|
115
|
+
"that nothing outside its own tests imports. A package is reached by being "
|
|
116
|
+
"imported, so a name match says nothing about whether it is still wanted."
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
|
|
109
120
|
output_group = parser.add_mutually_exclusive_group()
|
|
110
121
|
output_group.add_argument(
|
|
111
122
|
"--quiet",
|
|
@@ -183,6 +194,14 @@ def package_of(path: str, tree: str) -> str | None:
|
|
|
183
194
|
return parts[0]
|
|
184
195
|
|
|
185
196
|
|
|
197
|
+
def package_root(path: str, tree: str) -> str | None:
|
|
198
|
+
package = package_of(path, tree)
|
|
199
|
+
if package is None:
|
|
200
|
+
return None
|
|
201
|
+
anchor = os.path.join(os.path.normpath(tree), package, "__init__.py")
|
|
202
|
+
return package if os.path.normpath(path) == anchor else None
|
|
203
|
+
|
|
204
|
+
|
|
186
205
|
def _tree_root(path: str) -> str:
|
|
187
206
|
if os.path.isdir(path):
|
|
188
207
|
return path
|
|
@@ -261,6 +280,15 @@ def read_definitions(
|
|
|
261
280
|
return definitions
|
|
262
281
|
|
|
263
282
|
|
|
283
|
+
def read_packages(paths: dict[str, str]) -> list[Definition]:
|
|
284
|
+
packages: list[Definition] = []
|
|
285
|
+
for path in sorted(paths):
|
|
286
|
+
package = package_root(path, paths[path])
|
|
287
|
+
if package is not None:
|
|
288
|
+
packages.append(Definition(path=path, line_number=1, name=package, package=package))
|
|
289
|
+
return packages
|
|
290
|
+
|
|
291
|
+
|
|
264
292
|
def report_unparsed(searched: Searched) -> None:
|
|
265
293
|
for path in searched.unparsed:
|
|
266
294
|
print(f"Searching as text (will not parse): {path}")
|
|
@@ -316,6 +344,22 @@ def _report(result: ScanResult, args: argparse.Namespace) -> None:
|
|
|
316
344
|
output_findings(result.findings, args.count)
|
|
317
345
|
|
|
318
346
|
|
|
347
|
+
def _findings_for(
|
|
348
|
+
args: argparse.Namespace,
|
|
349
|
+
result: ScanResult,
|
|
350
|
+
definition_paths: dict[str, str],
|
|
351
|
+
sources: dict[str, str],
|
|
352
|
+
searched: Searched,
|
|
353
|
+
) -> list[Finding]:
|
|
354
|
+
if args.unimported_packages:
|
|
355
|
+
packages = read_packages(definition_paths)
|
|
356
|
+
result.files_scanned = result.definitions_read = len(packages)
|
|
357
|
+
return unimported_packages(packages, searched, args.dont_search_in)
|
|
358
|
+
definitions = read_definitions(definition_paths, sources, result, args.verbose)
|
|
359
|
+
checked = assume_used(definitions, args, result)
|
|
360
|
+
return unused_definitions(checked, searched, args.dont_search_in)
|
|
361
|
+
|
|
362
|
+
|
|
319
363
|
def main(argv: Sequence[str] | None = None) -> None:
|
|
320
364
|
args = create_parser().parse_args(argv)
|
|
321
365
|
exclude_patterns = parse_patterns(args.exclude)
|
|
@@ -340,14 +384,12 @@ def main(argv: Sequence[str] | None = None) -> None:
|
|
|
340
384
|
print()
|
|
341
385
|
|
|
342
386
|
sources = read_sources({**search_paths, **definition_paths}, result, args.verbose)
|
|
343
|
-
definitions = read_definitions(definition_paths, sources, result, args.verbose)
|
|
344
|
-
definitions = assume_used(definitions, args, result)
|
|
345
387
|
to_search = {path: content for path, content in sources.items() if path in search_paths}
|
|
346
388
|
searched = read_searched(to_search)
|
|
347
389
|
if args.verbose:
|
|
348
390
|
report_unparsed(searched)
|
|
349
391
|
|
|
350
|
-
findings =
|
|
392
|
+
findings = _findings_for(args, result, definition_paths, sources, searched)
|
|
351
393
|
result.findings = findings[:1] if (args.fail_fast and findings) else findings
|
|
352
394
|
|
|
353
395
|
_report(result, args)
|
|
@@ -8,7 +8,7 @@ from functools import lru_cache
|
|
|
8
8
|
from typing import TYPE_CHECKING
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
|
-
from collections.abc import Sequence
|
|
11
|
+
from collections.abc import Callable, Sequence
|
|
12
12
|
|
|
13
13
|
DEFINITION_NODES = (ast.AsyncFunctionDef, ast.ClassDef, ast.FunctionDef)
|
|
14
14
|
DOCSTRING_HOLDERS = (ast.AsyncFunctionDef, ast.ClassDef, ast.FunctionDef, ast.Module)
|
|
@@ -54,6 +54,7 @@ class Finding:
|
|
|
54
54
|
class Searched:
|
|
55
55
|
uses: dict[str, frozenset[str]]
|
|
56
56
|
unparsed: dict[str, str]
|
|
57
|
+
imports: dict[str, frozenset[str]]
|
|
57
58
|
|
|
58
59
|
|
|
59
60
|
def is_public(name: str) -> bool:
|
|
@@ -163,8 +164,19 @@ def names_used(tree: ast.Module) -> frozenset[str]:
|
|
|
163
164
|
return frozenset(names)
|
|
164
165
|
|
|
165
166
|
|
|
167
|
+
def imported_modules(tree: ast.Module) -> frozenset[str]:
|
|
168
|
+
modules: set[str] = set()
|
|
169
|
+
for node in ast.walk(tree):
|
|
170
|
+
if isinstance(node, ast.Import):
|
|
171
|
+
modules.update(alias.name.split(".")[0] for alias in node.names)
|
|
172
|
+
elif isinstance(node, ast.ImportFrom) and not node.level and node.module:
|
|
173
|
+
modules.add(node.module.split(".")[0])
|
|
174
|
+
return frozenset(modules)
|
|
175
|
+
|
|
176
|
+
|
|
166
177
|
def read_searched(sources: dict[str, str]) -> Searched:
|
|
167
178
|
uses: dict[str, frozenset[str]] = {}
|
|
179
|
+
imports: dict[str, frozenset[str]] = {}
|
|
168
180
|
unparsed: dict[str, str] = {}
|
|
169
181
|
for path in sorted(sources):
|
|
170
182
|
try:
|
|
@@ -173,7 +185,8 @@ def read_searched(sources: dict[str, str]) -> Searched:
|
|
|
173
185
|
unparsed[path] = sources[path]
|
|
174
186
|
continue
|
|
175
187
|
uses[path] = names_used(tree)
|
|
176
|
-
|
|
188
|
+
imports[path] = imported_modules(tree)
|
|
189
|
+
return Searched(uses=uses, unparsed=unparsed, imports=imports)
|
|
177
190
|
|
|
178
191
|
|
|
179
192
|
def is_assumed_used_by_name(name: str, patterns: Sequence[str]) -> bool:
|
|
@@ -228,14 +241,41 @@ def is_used(
|
|
|
228
241
|
return False
|
|
229
242
|
|
|
230
243
|
|
|
231
|
-
def
|
|
244
|
+
def is_imported(
|
|
245
|
+
definition: Definition, searched: Searched, unsearched: str | None = None
|
|
246
|
+
) -> bool:
|
|
247
|
+
return any(
|
|
248
|
+
definition.name in imported
|
|
249
|
+
for path, imported in searched.imports.items()
|
|
250
|
+
if not _is_unsearched(path, unsearched)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _findings_over(
|
|
232
255
|
definitions: list[Definition],
|
|
233
256
|
searched: Searched,
|
|
234
|
-
unsearched_template: str | None
|
|
257
|
+
unsearched_template: str | None,
|
|
258
|
+
reached: Callable[[Definition, Searched, str | None], bool],
|
|
235
259
|
) -> list[Finding]:
|
|
236
260
|
findings = []
|
|
237
261
|
for definition in definitions:
|
|
238
262
|
unsearched = unsearched_directory(unsearched_template, definition.package)
|
|
239
|
-
if not
|
|
263
|
+
if not reached(definition, searched, unsearched):
|
|
240
264
|
findings.append(Finding(definition=definition))
|
|
241
265
|
return findings
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def unused_definitions(
|
|
269
|
+
definitions: list[Definition],
|
|
270
|
+
searched: Searched,
|
|
271
|
+
unsearched_template: str | None = None,
|
|
272
|
+
) -> list[Finding]:
|
|
273
|
+
return _findings_over(definitions, searched, unsearched_template, is_used)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def unimported_packages(
|
|
277
|
+
packages: list[Definition],
|
|
278
|
+
searched: Searched,
|
|
279
|
+
unsearched_template: str | None = None,
|
|
280
|
+
) -> list[Finding]:
|
|
281
|
+
return _findings_over(packages, searched, unsearched_template, is_imported)
|