python-surveyor 0.4.1__tar.gz → 0.5.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.
- {python_surveyor-0.4.1/python_surveyor.egg-info → python_surveyor-0.5.0}/PKG-INFO +1 -1
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/pyproject.toml +1 -1
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/__init__.py +0 -2
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/checks/__init__.py +1 -2
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/checks/fixture_naming.py +1 -1
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/checks/optional_params.py +14 -72
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/cli.py +5 -5
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/model.py +0 -16
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/scanner.py +10 -45
- {python_surveyor-0.4.1 → python_surveyor-0.5.0/python_surveyor.egg-info}/PKG-INFO +1 -1
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/tests/test_optional_params.py +0 -60
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/tests/test_scanner.py +10 -60
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/LICENSE +0 -0
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/README.md +0 -0
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/__main__.py +0 -0
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/checks/_util.py +0 -0
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/checks/broad_except.py +0 -0
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/checks/future_annotations.py +0 -0
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/checks/nontoplevel_imports.py +0 -0
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/checks/suppressions.py +0 -0
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/report.py +0 -0
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor.egg-info/SOURCES.txt +0 -0
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor.egg-info/dependency_links.txt +0 -0
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor.egg-info/entry_points.txt +0 -0
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor.egg-info/requires.txt +0 -0
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor.egg-info/top_level.txt +0 -0
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/setup.cfg +0 -0
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/tests/test_broad_except.py +0 -0
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/tests/test_cli.py +0 -0
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/tests/test_fixture_naming.py +0 -0
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/tests/test_future_annotations.py +0 -0
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/tests/test_nontoplevel_imports.py +0 -0
- {python_surveyor-0.4.1 → python_surveyor-0.5.0}/tests/test_suppressions.py +0 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""python-surveyor: AST-based scanner for AI-generated-code smells."""
|
|
2
2
|
|
|
3
3
|
from python_surveyor.model import (
|
|
4
|
-
CallSite,
|
|
5
4
|
Finding,
|
|
6
5
|
Location,
|
|
7
6
|
ParseError,
|
|
@@ -9,7 +8,6 @@ from python_surveyor.model import (
|
|
|
9
8
|
)
|
|
10
9
|
|
|
11
10
|
__all__ = [
|
|
12
|
-
"CallSite",
|
|
13
11
|
"Finding",
|
|
14
12
|
"Location",
|
|
15
13
|
"ParseError",
|
|
@@ -112,8 +112,7 @@ _ALL_CHECKS_DATA: tuple[
|
|
|
112
112
|
"missing or wrong arguments. FastAPI/Flask route handlers are "
|
|
113
113
|
"excluded because their defaults are a framework user interface "
|
|
114
114
|
"(query parameters, headers) and the handler is invoked by the "
|
|
115
|
-
"framework, not by application code
|
|
116
|
-
"anywhere' signal doesn't apply.",
|
|
115
|
+
"framework, not by application code.",
|
|
117
116
|
(),
|
|
118
117
|
run_optional_params,
|
|
119
118
|
),
|
|
@@ -51,7 +51,7 @@ def _collision_note(registered_name: str, corpus: "Corpus") -> str:
|
|
|
51
51
|
count = len(sites)
|
|
52
52
|
if count == 0:
|
|
53
53
|
return f"registered as `{registered_name}`; no parameter usages elsewhere"
|
|
54
|
-
cap = corpus.
|
|
54
|
+
cap = corpus.max_samples
|
|
55
55
|
samples = sorted(sites, key=lambda pair: (str(pair[0].path), pair[0].line))[:cap]
|
|
56
56
|
rendered = ", ".join(
|
|
57
57
|
f"{pair[0].path.name}:{pair[0].line} (in `{pair[1]}`)" for pair in samples
|
|
@@ -4,29 +4,26 @@ Finds any ``FunctionDef``/``AsyncFunctionDef`` with at least one defaulted
|
|
|
4
4
|
positional or keyword-only parameter (``*args``/``**kwargs`` names are not
|
|
5
5
|
"parameters" for this purpose). The signature (justifying comment + ``def``
|
|
6
6
|
line, with non-defaulted leading params elided) is attached as an excerpt,
|
|
7
|
-
and
|
|
8
|
-
at least one default value (capped at ``corpus.max_call_sites``, with
|
|
9
|
-
"and N other(s)" when truncated) — call sites that override every default
|
|
10
|
-
are not listed, since they don't exercise the default path.
|
|
7
|
+
and a note reports the defaulted parameter names.
|
|
11
8
|
|
|
12
9
|
**Framework route handlers are excluded.** FastAPI/Flask route handlers use
|
|
13
10
|
defaulted params as a user interface (query parameters, headers, etc.) and
|
|
14
|
-
are invoked by the framework rather than by application code, so the
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
...)
|
|
18
|
-
|
|
19
|
-
``
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
11
|
+
are invoked by the framework rather than by application code, so the smell
|
|
12
|
+
this check looks for doesn't apply. A handler is recognised only when its
|
|
13
|
+
decorator (e.g. ``@app.get(...)``, ``@router.post(...)``, ``@app.route(
|
|
14
|
+
...)``) is a call on a name that is statically traced, via imports in the
|
|
15
|
+
same file, to an actual ``fastapi.FastAPI`` / ``fastapi.APIRouter`` /
|
|
16
|
+
``flask.Flask`` / ``flask.Blueprint`` instance. Only direct ``name = Ctor()``
|
|
17
|
+
assignments are tracked — factory functions that build and return an
|
|
18
|
+
app/router are not (this matches the heuristic, not exhaustive, philosophy
|
|
19
|
+
of the other checks).
|
|
23
20
|
"""
|
|
24
21
|
|
|
25
22
|
import ast
|
|
26
23
|
from typing import TYPE_CHECKING
|
|
27
24
|
|
|
28
25
|
from python_surveyor.checks._util import justifying_comment
|
|
29
|
-
from python_surveyor.model import
|
|
26
|
+
from python_surveyor.model import Finding, SourceExcerpt
|
|
30
27
|
|
|
31
28
|
if TYPE_CHECKING:
|
|
32
29
|
from python_surveyor.scanner import Corpus, SourceFile
|
|
@@ -208,58 +205,6 @@ def _first_defaulted_line(
|
|
|
208
205
|
return func.lineno
|
|
209
206
|
|
|
210
207
|
|
|
211
|
-
def _param_uses_default(
|
|
212
|
-
param_name: str,
|
|
213
|
-
param_index: int,
|
|
214
|
-
call: CallSite,
|
|
215
|
-
) -> bool:
|
|
216
|
-
"""True if the call site does not pass this param (uses the default)."""
|
|
217
|
-
if param_name in call.keywords:
|
|
218
|
-
return False
|
|
219
|
-
if param_index < call.positional_count:
|
|
220
|
-
return False
|
|
221
|
-
return True
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def _call_site_notes(
|
|
225
|
-
name: str,
|
|
226
|
-
defaulted_params: list[str],
|
|
227
|
-
corpus: "Corpus",
|
|
228
|
-
) -> tuple[str, ...]:
|
|
229
|
-
"""Return notes describing call sites that use at least one default value.
|
|
230
|
-
|
|
231
|
-
Call sites that override every defaulted param are not listed — they
|
|
232
|
-
don't exercise the default path and only add noise. Call sites that
|
|
233
|
-
use at least one default are listed (capped at ``corpus.max_call_sites``)
|
|
234
|
-
with "and N other(s)" when truncated.
|
|
235
|
-
"""
|
|
236
|
-
sites = corpus.call_sites.get(name, ())
|
|
237
|
-
total = len(sites)
|
|
238
|
-
if total == 0:
|
|
239
|
-
return (f"`{name}` not called from anywhere scanned",)
|
|
240
|
-
using_default: list[CallSite] = []
|
|
241
|
-
for site in sites:
|
|
242
|
-
for idx, param_name in enumerate(defaulted_params):
|
|
243
|
-
if _param_uses_default(param_name, idx, site):
|
|
244
|
-
using_default.append(site)
|
|
245
|
-
break
|
|
246
|
-
using_default_sorted = sorted(
|
|
247
|
-
using_default, key=lambda s: (str(s.location.path), s.location.line)
|
|
248
|
-
)
|
|
249
|
-
cap = corpus.max_call_sites
|
|
250
|
-
shown = using_default_sorted[:cap]
|
|
251
|
-
if not shown:
|
|
252
|
-
return (f"All {total} call site(s) override every defaulted parameter",)
|
|
253
|
-
parts = [f"{s.location.path.name}:{s.location.line}" for s in shown]
|
|
254
|
-
remaining = len(using_default) - len(shown)
|
|
255
|
-
if remaining > 0:
|
|
256
|
-
parts.append(f"and {remaining} other(s)")
|
|
257
|
-
sites_str = ", ".join(parts)
|
|
258
|
-
return (
|
|
259
|
-
f"Call sites using defaults ({len(using_default)} of {total}): {sites_str}",
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
|
|
263
208
|
def _signature_end_line(
|
|
264
209
|
node: "ast.FunctionDef | ast.AsyncFunctionDef",
|
|
265
210
|
) -> int:
|
|
@@ -270,8 +215,8 @@ def _signature_end_line(
|
|
|
270
215
|
return max(node.lineno, first_body.lineno - 1)
|
|
271
216
|
|
|
272
217
|
|
|
273
|
-
def run(source: "SourceFile",
|
|
274
|
-
"""Find functions with defaulted params and attach signature
|
|
218
|
+
def run(source: "SourceFile", _corpus: "Corpus") -> list[Finding]:
|
|
219
|
+
"""Find functions with defaulted params and attach signature context."""
|
|
275
220
|
router_names = _framework_object_names(source.tree)
|
|
276
221
|
findings: list[Finding] = []
|
|
277
222
|
for node in ast.walk(source.tree):
|
|
@@ -302,10 +247,7 @@ def run(source: "SourceFile", corpus: "Corpus") -> list[Finding]:
|
|
|
302
247
|
end_line=end,
|
|
303
248
|
),
|
|
304
249
|
),
|
|
305
|
-
notes=(
|
|
306
|
-
f"Defaulted params: {', '.join(defaulted)}",
|
|
307
|
-
*_call_site_notes(node.name, defaulted, corpus),
|
|
308
|
-
),
|
|
250
|
+
notes=(f"Defaulted params: {', '.join(defaulted)}",),
|
|
309
251
|
)
|
|
310
252
|
)
|
|
311
253
|
return findings
|
|
@@ -45,11 +45,11 @@ def main() -> None:
|
|
|
45
45
|
help="Cap excerpt line count (default 20).",
|
|
46
46
|
)
|
|
47
47
|
@click.option(
|
|
48
|
-
"--max-
|
|
49
|
-
"
|
|
48
|
+
"--max-samples",
|
|
49
|
+
"max_samples",
|
|
50
50
|
type=int,
|
|
51
51
|
default=10,
|
|
52
|
-
help="Cap
|
|
52
|
+
help="Cap note-sample counts (default 10).",
|
|
53
53
|
)
|
|
54
54
|
def scan_cmd(
|
|
55
55
|
paths: tuple[str, ...],
|
|
@@ -57,7 +57,7 @@ def scan_cmd(
|
|
|
57
57
|
excludes: tuple[str, ...],
|
|
58
58
|
output: "Path | None",
|
|
59
59
|
max_excerpt_lines: int,
|
|
60
|
-
|
|
60
|
+
max_samples: int,
|
|
61
61
|
) -> None:
|
|
62
62
|
"""Scan PATHS for AI-generated-code smells."""
|
|
63
63
|
root_paths: tuple[str, ...] = paths if paths else (".",)
|
|
@@ -65,7 +65,7 @@ def scan_cmd(
|
|
|
65
65
|
root_paths=root_paths,
|
|
66
66
|
check_ids=check_ids if check_ids else None,
|
|
67
67
|
excludes=excludes,
|
|
68
|
-
|
|
68
|
+
max_samples=max_samples,
|
|
69
69
|
)
|
|
70
70
|
stream: TextIO
|
|
71
71
|
if output is not None:
|
|
@@ -12,22 +12,6 @@ class Location:
|
|
|
12
12
|
line: int
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
@dataclass(frozen=True)
|
|
16
|
-
class CallSite:
|
|
17
|
-
"""A call to a name, with enough info to tell if defaulted params are used.
|
|
18
|
-
|
|
19
|
-
``positional_count`` is the number of positional args (not counting
|
|
20
|
-
``*args`` spreads). ``keywords`` is the set of keyword arg names (not
|
|
21
|
-
counting ``**kwargs`` spreads). Together these let the
|
|
22
|
-
``optional-param-default`` check determine whether a call site uses the
|
|
23
|
-
default value or passes an explicit one.
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
location: Location
|
|
27
|
-
positional_count: int
|
|
28
|
-
keywords: frozenset[str]
|
|
29
|
-
|
|
30
|
-
|
|
31
15
|
@dataclass(frozen=True)
|
|
32
16
|
class SourceExcerpt:
|
|
33
17
|
"""A literal slice of source worth showing verbatim."""
|
|
@@ -9,7 +9,7 @@ The scanner runs in two passes:
|
|
|
9
9
|
dropped.
|
|
10
10
|
2. **Build a ``Corpus``** over the successfully parsed files, giving every
|
|
11
11
|
check a cheap, already-built answer to "does anything in what we scanned
|
|
12
|
-
|
|
12
|
+
use this identifier" without re-parsing.
|
|
13
13
|
3. **Run checks.** Every check has the same signature so the internal check
|
|
14
14
|
API itself has no optional parameters.
|
|
15
15
|
4. **Sort** findings by ``(check_id, path, line)`` and return a
|
|
@@ -25,7 +25,7 @@ from dataclasses import dataclass
|
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
|
|
27
27
|
from python_surveyor.checks import ALL_CHECKS, COVERED_PYLINT_IDS, CheckSpec
|
|
28
|
-
from python_surveyor.model import
|
|
28
|
+
from python_surveyor.model import Finding, Location, ParseError
|
|
29
29
|
|
|
30
30
|
DEFAULT_PRUNED_DIRS: tuple[str, ...] = (
|
|
31
31
|
".venv",
|
|
@@ -65,18 +65,16 @@ class SourceFile:
|
|
|
65
65
|
class Corpus:
|
|
66
66
|
"""Cross-file indices built once over every successfully parsed file.
|
|
67
67
|
|
|
68
|
-
``call_sites`` answers "is this name called anywhere we scanned?".
|
|
69
68
|
``param_locations`` answers "is this name used as a parameter anywhere?"
|
|
70
69
|
and pairs each hit with the enclosing function's name so the
|
|
71
70
|
fixture-naming check can spot redefined-outer-name collisions.
|
|
72
|
-
``
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
``max_samples`` is the cap checks apply when sampling note entries (set
|
|
72
|
+
by the scanner from the CLI flag so checks don't need optional
|
|
73
|
+
parameters of their own).
|
|
75
74
|
"""
|
|
76
75
|
|
|
77
|
-
call_sites: dict[str, tuple[CallSite, ...]]
|
|
78
76
|
param_locations: dict[str, tuple[tuple[Location, str], ...]]
|
|
79
|
-
|
|
77
|
+
max_samples: int
|
|
80
78
|
covered_pylint_ids: frozenset[str]
|
|
81
79
|
|
|
82
80
|
|
|
@@ -150,25 +148,6 @@ def _parse_file(path: Path) -> "tuple[SourceFile | None, ParseError | None]":
|
|
|
150
148
|
)
|
|
151
149
|
|
|
152
150
|
|
|
153
|
-
def _resolve_call_name(node: ast.Call) -> str | None:
|
|
154
|
-
func = node.func
|
|
155
|
-
if isinstance(func, ast.Name):
|
|
156
|
-
return func.id
|
|
157
|
-
if isinstance(func, ast.Attribute):
|
|
158
|
-
return func.attr
|
|
159
|
-
return None
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def _call_positional_count(node: ast.Call) -> int:
|
|
163
|
-
"""Count positional args (excluding ``*args`` spreads)."""
|
|
164
|
-
return sum(1 for arg in node.args if not isinstance(arg, ast.Starred))
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def _call_keywords(node: ast.Call) -> frozenset[str]:
|
|
168
|
-
"""Return the set of keyword arg names (excluding ``**kwargs`` spreads)."""
|
|
169
|
-
return frozenset(kw.arg for kw in node.keywords if kw.arg is not None)
|
|
170
|
-
|
|
171
|
-
|
|
172
151
|
def _collect_params(
|
|
173
152
|
func_node: "ast.FunctionDef | ast.AsyncFunctionDef",
|
|
174
153
|
path: Path,
|
|
@@ -193,31 +172,17 @@ def _collect_params(
|
|
|
193
172
|
|
|
194
173
|
def _build_corpus(
|
|
195
174
|
source_files: list[SourceFile],
|
|
196
|
-
|
|
175
|
+
max_samples: int,
|
|
197
176
|
covered_pylint_ids: frozenset[str],
|
|
198
177
|
) -> Corpus:
|
|
199
|
-
call_sites: dict[str, list[CallSite]] = {}
|
|
200
178
|
param_locations: dict[str, list[tuple[Location, str]]] = {}
|
|
201
179
|
for source_file in source_files:
|
|
202
180
|
for node in ast.walk(source_file.tree):
|
|
203
|
-
if isinstance(node, ast.Call):
|
|
204
|
-
resolved = _resolve_call_name(node)
|
|
205
|
-
if resolved is None:
|
|
206
|
-
continue
|
|
207
|
-
call_sites.setdefault(resolved, []).append(
|
|
208
|
-
CallSite(
|
|
209
|
-
location=Location(path=source_file.path, line=node.lineno),
|
|
210
|
-
positional_count=_call_positional_count(node),
|
|
211
|
-
keywords=_call_keywords(node),
|
|
212
|
-
)
|
|
213
|
-
)
|
|
214
|
-
continue
|
|
215
181
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
216
182
|
_collect_params(node, source_file.path, param_locations)
|
|
217
183
|
return Corpus(
|
|
218
|
-
call_sites={k: tuple(v) for k, v in call_sites.items()},
|
|
219
184
|
param_locations={k: tuple(v) for k, v in param_locations.items()},
|
|
220
|
-
|
|
185
|
+
max_samples=max_samples,
|
|
221
186
|
covered_pylint_ids=covered_pylint_ids,
|
|
222
187
|
)
|
|
223
188
|
|
|
@@ -226,7 +191,7 @@ def scan(
|
|
|
226
191
|
root_paths: tuple[str, ...],
|
|
227
192
|
check_ids: "tuple[str, ...] | None",
|
|
228
193
|
excludes: tuple[str, ...],
|
|
229
|
-
|
|
194
|
+
max_samples: int,
|
|
230
195
|
) -> ScanResult:
|
|
231
196
|
"""Run the full discover -> parse -> corpus -> checks pipeline."""
|
|
232
197
|
if check_ids is None:
|
|
@@ -248,7 +213,7 @@ def scan(
|
|
|
248
213
|
continue
|
|
249
214
|
assert parsed is not None
|
|
250
215
|
source_files.append(parsed)
|
|
251
|
-
corpus = _build_corpus(source_files,
|
|
216
|
+
corpus = _build_corpus(source_files, max_samples, COVERED_PYLINT_IDS)
|
|
252
217
|
findings: list[Finding] = []
|
|
253
218
|
for source_file in source_files:
|
|
254
219
|
for check_spec in enabled_checks:
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"""Tests for the ``optional-param-default`` check."""
|
|
2
2
|
|
|
3
|
-
from python_surveyor.checks import COVERED_PYLINT_IDS
|
|
4
3
|
from python_surveyor.checks.optional_params import run
|
|
5
|
-
from python_surveyor.scanner import _build_corpus
|
|
6
4
|
|
|
7
5
|
|
|
8
6
|
def test_defaulted_positional_param(make_source_file, empty_corpus):
|
|
@@ -11,8 +9,6 @@ def test_defaulted_positional_param(make_source_file, empty_corpus):
|
|
|
11
9
|
findings = run(source_file, empty_corpus)
|
|
12
10
|
assert len(findings) == 1
|
|
13
11
|
assert findings[0].check_id == "optional-param-default"
|
|
14
|
-
notes = "\n".join(findings[0].notes)
|
|
15
|
-
assert "`f`" in notes
|
|
16
12
|
|
|
17
13
|
|
|
18
14
|
def test_mixed_defaulted_positional(make_source_file, empty_corpus):
|
|
@@ -43,62 +39,6 @@ def test_args_kwargs_not_flagged(make_source_file, empty_corpus):
|
|
|
43
39
|
assert findings == []
|
|
44
40
|
|
|
45
41
|
|
|
46
|
-
def test_call_site_note_with_corpus(make_source_file):
|
|
47
|
-
source = "def helper(a=1):\n" " pass\n" "def caller():\n" " helper()\n"
|
|
48
|
-
source_file = make_source_file("a.py", source)
|
|
49
|
-
corpus = _build_corpus([source_file], 10, COVERED_PYLINT_IDS)
|
|
50
|
-
findings = run(source_file, corpus)
|
|
51
|
-
assert findings
|
|
52
|
-
notes = "\n".join(findings[0].notes)
|
|
53
|
-
assert "using defaults" in notes
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def test_call_site_note_not_called(make_source_file):
|
|
57
|
-
source = "def helper(a=1):\n" " pass\n"
|
|
58
|
-
source_file = make_source_file("a.py", source)
|
|
59
|
-
corpus = _build_corpus([source_file], 10, COVERED_PYLINT_IDS)
|
|
60
|
-
findings = run(source_file, corpus)
|
|
61
|
-
assert findings
|
|
62
|
-
assert "not called from anywhere" in findings[0].notes[1]
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def test_call_site_note_override(make_source_file):
|
|
66
|
-
source = "def helper(a=1):\n" " pass\n" "def caller():\n" " helper(2)\n"
|
|
67
|
-
source_file = make_source_file("a.py", source)
|
|
68
|
-
corpus = _build_corpus([source_file], 10, COVERED_PYLINT_IDS)
|
|
69
|
-
findings = run(source_file, corpus)
|
|
70
|
-
notes = "\n".join(findings[0].notes)
|
|
71
|
-
assert "override every defaulted parameter" in notes
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def test_call_site_note_mixed(make_source_file):
|
|
75
|
-
source = (
|
|
76
|
-
"def helper(a=1):\n"
|
|
77
|
-
" pass\n"
|
|
78
|
-
"def c1():\n"
|
|
79
|
-
" helper()\n"
|
|
80
|
-
"def c2():\n"
|
|
81
|
-
" helper(2)\n"
|
|
82
|
-
)
|
|
83
|
-
source_file = make_source_file("a.py", source)
|
|
84
|
-
corpus = _build_corpus([source_file], 10, COVERED_PYLINT_IDS)
|
|
85
|
-
findings = run(source_file, corpus)
|
|
86
|
-
notes = "\n".join(findings[0].notes)
|
|
87
|
-
assert "using defaults" in notes
|
|
88
|
-
assert "1 of 2" in notes
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def test_call_site_note_truncation(make_source_file):
|
|
92
|
-
source = "def helper(a=1):\n pass\n"
|
|
93
|
-
for i in range(15):
|
|
94
|
-
source += f"def c{i}():\n helper()\n"
|
|
95
|
-
source_file = make_source_file("a.py", source)
|
|
96
|
-
corpus = _build_corpus([source_file], 10, COVERED_PYLINT_IDS)
|
|
97
|
-
findings = run(source_file, corpus)
|
|
98
|
-
notes = "\n".join(findings[0].notes)
|
|
99
|
-
assert "and 5 other(s)" in notes
|
|
100
|
-
|
|
101
|
-
|
|
102
42
|
def test_defaulted_param_names_in_notes(make_source_file, empty_corpus):
|
|
103
43
|
source = "def f(a, b=2, *, c=3):\n pass\n"
|
|
104
44
|
source_file = make_source_file("a.py", source)
|
|
@@ -25,7 +25,7 @@ def test_scan_finds_smelly_file(tmp_path):
|
|
|
25
25
|
root_paths=(str(tmp_path),),
|
|
26
26
|
check_ids=None,
|
|
27
27
|
excludes=(),
|
|
28
|
-
|
|
28
|
+
max_samples=5,
|
|
29
29
|
)
|
|
30
30
|
check_ids_found = {f.check_id for f in result.findings}
|
|
31
31
|
assert "future-annotations-import" in check_ids_found
|
|
@@ -40,7 +40,7 @@ def test_scan_parse_error(tmp_path):
|
|
|
40
40
|
root_paths=(str(tmp_path),),
|
|
41
41
|
check_ids=None,
|
|
42
42
|
excludes=(),
|
|
43
|
-
|
|
43
|
+
max_samples=5,
|
|
44
44
|
)
|
|
45
45
|
assert result.parse_errors
|
|
46
46
|
assert result.parse_errors[0].path == broken
|
|
@@ -58,7 +58,7 @@ def test_scan_prunes_venv(tmp_path):
|
|
|
58
58
|
root_paths=(str(tmp_path),),
|
|
59
59
|
check_ids=None,
|
|
60
60
|
excludes=(),
|
|
61
|
-
|
|
61
|
+
max_samples=5,
|
|
62
62
|
)
|
|
63
63
|
assert result.files_scanned == 1
|
|
64
64
|
assert result.findings == ()
|
|
@@ -75,7 +75,7 @@ def test_scan_exclude_glob(tmp_path):
|
|
|
75
75
|
root_paths=(str(tmp_path),),
|
|
76
76
|
check_ids=None,
|
|
77
77
|
excludes=("vendor",),
|
|
78
|
-
|
|
78
|
+
max_samples=5,
|
|
79
79
|
)
|
|
80
80
|
assert result.files_scanned == 1
|
|
81
81
|
|
|
@@ -89,7 +89,7 @@ def test_scan_check_subset(tmp_path):
|
|
|
89
89
|
root_paths=(str(tmp_path),),
|
|
90
90
|
check_ids=("future-annotations-import",),
|
|
91
91
|
excludes=(),
|
|
92
|
-
|
|
92
|
+
max_samples=5,
|
|
93
93
|
)
|
|
94
94
|
assert all(f.check_id == "future-annotations-import" for f in result.findings)
|
|
95
95
|
assert any(f.check_id == "future-annotations-import" for f in result.findings)
|
|
@@ -101,59 +101,10 @@ def test_scan_unknown_check_id_raises():
|
|
|
101
101
|
root_paths=(".",),
|
|
102
102
|
check_ids=("nonexistent-check",),
|
|
103
103
|
excludes=(),
|
|
104
|
-
|
|
104
|
+
max_samples=5,
|
|
105
105
|
)
|
|
106
106
|
|
|
107
107
|
|
|
108
|
-
def test_build_corpus_call_sites(tmp_path):
|
|
109
|
-
source = (
|
|
110
|
-
"def helper():\n"
|
|
111
|
-
" pass\n"
|
|
112
|
-
"def caller():\n"
|
|
113
|
-
" helper()\n"
|
|
114
|
-
" other.method()\n"
|
|
115
|
-
)
|
|
116
|
-
path = tmp_path / "a.py"
|
|
117
|
-
path.write_text(source, encoding="utf-8")
|
|
118
|
-
tree = ast.parse(source)
|
|
119
|
-
source_file = SourceFile(
|
|
120
|
-
path=path,
|
|
121
|
-
source=source,
|
|
122
|
-
lines=tuple(source.splitlines()),
|
|
123
|
-
tree=tree,
|
|
124
|
-
tokens=(),
|
|
125
|
-
)
|
|
126
|
-
corpus = _build_corpus([source_file], 5, COVERED_PYLINT_IDS)
|
|
127
|
-
assert "helper" in corpus.call_sites
|
|
128
|
-
assert "method" in corpus.call_sites
|
|
129
|
-
assert corpus.call_sites["helper"][0].positional_count == 0
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def test_build_corpus_call_args(tmp_path):
|
|
133
|
-
source = (
|
|
134
|
-
"def f(a, b=1, *, c=2):\n" " pass\n" "f(1)\n" "f(1, 2, c=3)\n" "f(1, b=2)\n"
|
|
135
|
-
)
|
|
136
|
-
path = tmp_path / "a.py"
|
|
137
|
-
path.write_text(source, encoding="utf-8")
|
|
138
|
-
tree = ast.parse(source)
|
|
139
|
-
source_file = SourceFile(
|
|
140
|
-
path=path,
|
|
141
|
-
source=source,
|
|
142
|
-
lines=tuple(source.splitlines()),
|
|
143
|
-
tree=tree,
|
|
144
|
-
tokens=(),
|
|
145
|
-
)
|
|
146
|
-
corpus = _build_corpus([source_file], 5, COVERED_PYLINT_IDS)
|
|
147
|
-
sites = corpus.call_sites["f"]
|
|
148
|
-
assert len(sites) == 3
|
|
149
|
-
assert sites[0].positional_count == 1
|
|
150
|
-
assert sites[0].keywords == frozenset()
|
|
151
|
-
assert sites[1].positional_count == 2
|
|
152
|
-
assert sites[1].keywords == frozenset({"c"})
|
|
153
|
-
assert sites[2].positional_count == 1
|
|
154
|
-
assert sites[2].keywords == frozenset({"b"})
|
|
155
|
-
|
|
156
|
-
|
|
157
108
|
def test_build_corpus_param_locations(tmp_path):
|
|
158
109
|
source = "def f(db):\n" " pass\n" "def g(db):\n" " pass\n"
|
|
159
110
|
path = tmp_path / "a.py"
|
|
@@ -182,7 +133,7 @@ def test_scan_result_is_sorted(tmp_path):
|
|
|
182
133
|
root_paths=(str(tmp_path),),
|
|
183
134
|
check_ids=None,
|
|
184
135
|
excludes=(),
|
|
185
|
-
|
|
136
|
+
max_samples=5,
|
|
186
137
|
)
|
|
187
138
|
paths = [str(f.path) for f in result.findings]
|
|
188
139
|
assert paths == sorted(paths)
|
|
@@ -190,13 +141,12 @@ def test_scan_result_is_sorted(tmp_path):
|
|
|
190
141
|
|
|
191
142
|
def test_corpus_is_immutable():
|
|
192
143
|
corpus = Corpus(
|
|
193
|
-
call_sites={},
|
|
194
144
|
param_locations={},
|
|
195
|
-
|
|
145
|
+
max_samples=5,
|
|
196
146
|
covered_pylint_ids=COVERED_PYLINT_IDS,
|
|
197
147
|
)
|
|
198
148
|
with pytest.raises(dataclasses.FrozenInstanceError):
|
|
199
|
-
corpus.
|
|
149
|
+
corpus.max_samples = 10 # type: ignore[misc]
|
|
200
150
|
|
|
201
151
|
|
|
202
152
|
def test_scan_result_fields(tmp_path):
|
|
@@ -205,7 +155,7 @@ def test_scan_result_fields(tmp_path):
|
|
|
205
155
|
root_paths=(str(tmp_path),),
|
|
206
156
|
check_ids=None,
|
|
207
157
|
excludes=(),
|
|
208
|
-
|
|
158
|
+
max_samples=5,
|
|
209
159
|
)
|
|
210
160
|
assert isinstance(result, ScanResult)
|
|
211
161
|
assert result.files_scanned == 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/checks/future_annotations.py
RENAMED
|
File without changes
|
{python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/checks/nontoplevel_imports.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor.egg-info/dependency_links.txt
RENAMED
|
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
|