python-surveyor 0.1.0__tar.gz → 0.2.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.1.0/python_surveyor.egg-info → python_surveyor-0.2.0}/PKG-INFO +1 -1
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/pyproject.toml +4 -1
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/python_surveyor/cli.py +4 -15
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/python_surveyor/model.py +0 -2
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/python_surveyor/report.py +22 -62
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/python_surveyor/scanner.py +9 -3
- {python_surveyor-0.1.0 → python_surveyor-0.2.0/python_surveyor.egg-info}/PKG-INFO +1 -1
- python_surveyor-0.2.0/tests/test_broad_except.py +178 -0
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/tests/test_cli.py +0 -16
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/tests/test_fixture_naming.py +28 -5
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/tests/test_optional_params.py +50 -17
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/tests/test_scanner.py +10 -4
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/tests/test_suppressions.py +20 -5
- python_surveyor-0.1.0/tests/test_broad_except.py +0 -93
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/LICENSE +0 -0
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/README.md +0 -0
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/python_surveyor/__init__.py +0 -0
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/python_surveyor/__main__.py +0 -0
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/python_surveyor.egg-info/SOURCES.txt +0 -0
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/python_surveyor.egg-info/dependency_links.txt +0 -0
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/python_surveyor.egg-info/entry_points.txt +0 -0
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/python_surveyor.egg-info/requires.txt +0 -0
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/python_surveyor.egg-info/top_level.txt +0 -0
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/setup.cfg +0 -0
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/tests/test_future_annotations.py +0 -0
- {python_surveyor-0.1.0 → python_surveyor-0.2.0}/tests/test_nontoplevel_imports.py +0 -0
|
@@ -8,7 +8,7 @@ include = ["python_surveyor"]
|
|
|
8
8
|
|
|
9
9
|
[project]
|
|
10
10
|
name = "python-surveyor"
|
|
11
|
-
version = "0.
|
|
11
|
+
version = "0.2.0"
|
|
12
12
|
description = "Use python introspection to survey source code for final LLM judgement"
|
|
13
13
|
requires-python = ">=3.12"
|
|
14
14
|
license = "MIT"
|
|
@@ -82,3 +82,6 @@ disable = [
|
|
|
82
82
|
"duplicate-code",
|
|
83
83
|
"fixme",
|
|
84
84
|
]
|
|
85
|
+
enable = [
|
|
86
|
+
"useless-suppression",
|
|
87
|
+
]
|
|
@@ -7,7 +7,7 @@ from typing import TextIO
|
|
|
7
7
|
import click
|
|
8
8
|
|
|
9
9
|
from python_surveyor.checks import ALL_CHECKS
|
|
10
|
-
from python_surveyor.report import
|
|
10
|
+
from python_surveyor.report import render_text
|
|
11
11
|
from python_surveyor.scanner import scan
|
|
12
12
|
|
|
13
13
|
|
|
@@ -30,13 +30,6 @@ def main() -> None:
|
|
|
30
30
|
multiple=True,
|
|
31
31
|
help="fnmatch pattern to exclude (repeatable, additive to defaults).",
|
|
32
32
|
)
|
|
33
|
-
@click.option(
|
|
34
|
-
"--format",
|
|
35
|
-
"fmt",
|
|
36
|
-
type=click.Choice(["text", "json"]),
|
|
37
|
-
default="text",
|
|
38
|
-
help="Output format. Default: text.",
|
|
39
|
-
)
|
|
40
33
|
@click.option(
|
|
41
34
|
"--output",
|
|
42
35
|
"output",
|
|
@@ -55,14 +48,13 @@ def main() -> None:
|
|
|
55
48
|
"--max-call-sites",
|
|
56
49
|
"max_call_sites",
|
|
57
50
|
type=int,
|
|
58
|
-
default=
|
|
59
|
-
help="Cap call-site samples in notes (default
|
|
51
|
+
default=10,
|
|
52
|
+
help="Cap call-site samples in notes (default 10).",
|
|
60
53
|
)
|
|
61
54
|
def scan_cmd(
|
|
62
55
|
paths: tuple[str, ...],
|
|
63
56
|
check_ids: tuple[str, ...],
|
|
64
57
|
excludes: tuple[str, ...],
|
|
65
|
-
fmt: str,
|
|
66
58
|
output: "Path | None",
|
|
67
59
|
max_excerpt_lines: int,
|
|
68
60
|
max_call_sites: int,
|
|
@@ -81,10 +73,7 @@ def scan_cmd(
|
|
|
81
73
|
else:
|
|
82
74
|
stream = sys.stdout
|
|
83
75
|
try:
|
|
84
|
-
|
|
85
|
-
render_json(result, max_excerpt_lines, stream)
|
|
86
|
-
else:
|
|
87
|
-
render_text(result, max_excerpt_lines, stream)
|
|
76
|
+
render_text(result, max_excerpt_lines, stream)
|
|
88
77
|
finally:
|
|
89
78
|
if output is not None:
|
|
90
79
|
stream.close()
|
|
@@ -37,7 +37,6 @@ class CallSite:
|
|
|
37
37
|
class SourceExcerpt:
|
|
38
38
|
"""A literal slice of source worth showing verbatim."""
|
|
39
39
|
|
|
40
|
-
label: str
|
|
41
40
|
path: Path
|
|
42
41
|
start_line: int
|
|
43
42
|
end_line: int
|
|
@@ -51,7 +50,6 @@ class Finding:
|
|
|
51
50
|
path: Path
|
|
52
51
|
line: int
|
|
53
52
|
column: int
|
|
54
|
-
message: str
|
|
55
53
|
excerpts: tuple[SourceExcerpt, ...]
|
|
56
54
|
notes: tuple[str, ...]
|
|
57
55
|
|
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
"""Render a ``ScanResult`` as text
|
|
1
|
+
"""Render a ``ScanResult`` as text.
|
|
2
2
|
|
|
3
3
|
Text output groups findings by ``check_id``; each finding is shown as
|
|
4
|
-
``path:line:col
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
note.
|
|
8
|
-
|
|
9
|
-
JSON output is a flat list of the same fields for programmatic use (excerpt
|
|
10
|
-
text is not included — only the line range, so consumers can lazy-load).
|
|
4
|
+
``path:line:col`` followed by excerpts (with the line range in the header)
|
|
5
|
+
and note lines. Parse errors get their own leading section. Excerpts are
|
|
6
|
+
capped to ``--max-excerpt-lines`` with a truncation note.
|
|
11
7
|
"""
|
|
12
8
|
|
|
13
|
-
import json
|
|
14
9
|
from pathlib import Path
|
|
15
10
|
from typing import TextIO
|
|
16
11
|
|
|
@@ -65,6 +60,8 @@ def render_text(
|
|
|
65
60
|
if spec is not None:
|
|
66
61
|
stream.write(f"{spec.explanation}\n\n")
|
|
67
62
|
counter += 1
|
|
63
|
+
prev_path: Path | None = None
|
|
64
|
+
prev_end: int | None = None
|
|
68
65
|
for excerpt in finding.excerpts:
|
|
69
66
|
lines, truncation = _read_excerpt_lines(
|
|
70
67
|
excerpt.path,
|
|
@@ -72,66 +69,29 @@ def render_text(
|
|
|
72
69
|
excerpt.end_line,
|
|
73
70
|
max_excerpt_lines,
|
|
74
71
|
)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
is_continuation = (
|
|
73
|
+
prev_path == excerpt.path
|
|
74
|
+
and prev_end is not None
|
|
75
|
+
and excerpt.start_line > prev_end + 1
|
|
78
76
|
)
|
|
77
|
+
if is_continuation:
|
|
78
|
+
stream.write(
|
|
79
|
+
f"... (redacted until lines "
|
|
80
|
+
f"{excerpt.start_line}-{excerpt.end_line}) ...\n"
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
stream.write(
|
|
84
|
+
f"({counter}) {excerpt.path}:"
|
|
85
|
+
f"{excerpt.start_line}-{excerpt.end_line}\n"
|
|
86
|
+
)
|
|
79
87
|
for line in lines:
|
|
80
88
|
stream.write(f"{line}\n")
|
|
81
89
|
if truncation:
|
|
82
90
|
stream.write(f"... {truncation}\n")
|
|
91
|
+
prev_path = excerpt.path
|
|
92
|
+
prev_end = excerpt.end_line
|
|
83
93
|
for note in finding.notes:
|
|
84
94
|
stream.write(f" note: {note}\n")
|
|
85
95
|
stream.write("\n")
|
|
86
96
|
if not result.findings:
|
|
87
97
|
stream.write("no findings.\n")
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def render_json(
|
|
91
|
-
result: ScanResult,
|
|
92
|
-
max_excerpt_lines: int,
|
|
93
|
-
stream: TextIO,
|
|
94
|
-
) -> None:
|
|
95
|
-
"""Render ``result`` as JSON to ``stream``.
|
|
96
|
-
|
|
97
|
-
``max_excerpt_lines`` is accepted for API symmetry with
|
|
98
|
-
:func:`render_text`; JSON emits ranges only, not source text.
|
|
99
|
-
"""
|
|
100
|
-
del max_excerpt_lines
|
|
101
|
-
findings_payload = []
|
|
102
|
-
for finding in result.findings:
|
|
103
|
-
excerpts_payload = [
|
|
104
|
-
{
|
|
105
|
-
"label": excerpt.label,
|
|
106
|
-
"path": str(excerpt.path),
|
|
107
|
-
"start_line": excerpt.start_line,
|
|
108
|
-
"end_line": excerpt.end_line,
|
|
109
|
-
}
|
|
110
|
-
for excerpt in finding.excerpts
|
|
111
|
-
]
|
|
112
|
-
findings_payload.append(
|
|
113
|
-
{
|
|
114
|
-
"check_id": finding.check_id,
|
|
115
|
-
"path": str(finding.path),
|
|
116
|
-
"line": finding.line,
|
|
117
|
-
"column": finding.column,
|
|
118
|
-
"message": finding.message,
|
|
119
|
-
"excerpts": excerpts_payload,
|
|
120
|
-
"notes": list(finding.notes),
|
|
121
|
-
}
|
|
122
|
-
)
|
|
123
|
-
parse_errors_payload = [
|
|
124
|
-
{
|
|
125
|
-
"path": str(err.path),
|
|
126
|
-
"line": err.line,
|
|
127
|
-
"message": err.message,
|
|
128
|
-
}
|
|
129
|
-
for err in result.parse_errors
|
|
130
|
-
]
|
|
131
|
-
payload = {
|
|
132
|
-
"files_scanned": result.files_scanned,
|
|
133
|
-
"findings": findings_payload,
|
|
134
|
-
"parse_errors": parse_errors_payload,
|
|
135
|
-
}
|
|
136
|
-
json.dump(payload, stream, indent=2)
|
|
137
|
-
stream.write("\n")
|
|
@@ -24,7 +24,7 @@ from collections.abc import Iterator
|
|
|
24
24
|
from dataclasses import dataclass
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
|
|
27
|
-
from python_surveyor.checks import ALL_CHECKS, CheckSpec
|
|
27
|
+
from python_surveyor.checks import ALL_CHECKS, COVERED_PYLINT_IDS, CheckSpec
|
|
28
28
|
from python_surveyor.model import CallSite, Finding, Location, ParseError
|
|
29
29
|
|
|
30
30
|
DEFAULT_PRUNED_DIRS: tuple[str, ...] = (
|
|
@@ -77,6 +77,7 @@ class Corpus:
|
|
|
77
77
|
call_sites: dict[str, tuple[CallSite, ...]]
|
|
78
78
|
param_locations: dict[str, tuple[tuple[Location, str], ...]]
|
|
79
79
|
max_call_sites: int
|
|
80
|
+
covered_pylint_ids: frozenset[str]
|
|
80
81
|
|
|
81
82
|
|
|
82
83
|
@dataclass(frozen=True)
|
|
@@ -186,7 +187,11 @@ def _collect_params(
|
|
|
186
187
|
)
|
|
187
188
|
|
|
188
189
|
|
|
189
|
-
def _build_corpus(
|
|
190
|
+
def _build_corpus(
|
|
191
|
+
source_files: list[SourceFile],
|
|
192
|
+
max_call_sites: int,
|
|
193
|
+
covered_pylint_ids: frozenset[str],
|
|
194
|
+
) -> Corpus:
|
|
190
195
|
call_sites: dict[str, list[CallSite]] = {}
|
|
191
196
|
param_locations: dict[str, list[tuple[Location, str]]] = {}
|
|
192
197
|
for source_file in source_files:
|
|
@@ -209,6 +214,7 @@ def _build_corpus(source_files: list[SourceFile], max_call_sites: int) -> Corpus
|
|
|
209
214
|
call_sites={k: tuple(v) for k, v in call_sites.items()},
|
|
210
215
|
param_locations={k: tuple(v) for k, v in param_locations.items()},
|
|
211
216
|
max_call_sites=max_call_sites,
|
|
217
|
+
covered_pylint_ids=covered_pylint_ids,
|
|
212
218
|
)
|
|
213
219
|
|
|
214
220
|
|
|
@@ -238,7 +244,7 @@ def scan(
|
|
|
238
244
|
continue
|
|
239
245
|
assert parsed is not None
|
|
240
246
|
source_files.append(parsed)
|
|
241
|
-
corpus = _build_corpus(source_files, max_call_sites)
|
|
247
|
+
corpus = _build_corpus(source_files, max_call_sites, COVERED_PYLINT_IDS)
|
|
242
248
|
findings: list[Finding] = []
|
|
243
249
|
for source_file in source_files:
|
|
244
250
|
for check_spec in enabled_checks:
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Tests for the ``broad-except`` check."""
|
|
2
|
+
|
|
3
|
+
from python_surveyor.checks.broad_except import run
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_except_exception(make_source_file, empty_corpus):
|
|
7
|
+
source = (
|
|
8
|
+
"def f():\n"
|
|
9
|
+
" try:\n"
|
|
10
|
+
" pass\n"
|
|
11
|
+
" except Exception:\n"
|
|
12
|
+
" pass\n"
|
|
13
|
+
)
|
|
14
|
+
source_file = make_source_file("a.py", source)
|
|
15
|
+
findings = run(source_file, empty_corpus)
|
|
16
|
+
assert len(findings) == 1
|
|
17
|
+
assert findings[0].check_id == "broad-except"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_except_bare(make_source_file, empty_corpus):
|
|
21
|
+
source = "def f():\n" " try:\n" " pass\n" " except:\n" " pass\n"
|
|
22
|
+
source_file = make_source_file("a.py", source)
|
|
23
|
+
findings = run(source_file, empty_corpus)
|
|
24
|
+
assert len(findings) == 1
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_except_baseexception(make_source_file, empty_corpus):
|
|
28
|
+
source = (
|
|
29
|
+
"def f():\n"
|
|
30
|
+
" try:\n"
|
|
31
|
+
" pass\n"
|
|
32
|
+
" except BaseException:\n"
|
|
33
|
+
" pass\n"
|
|
34
|
+
)
|
|
35
|
+
source_file = make_source_file("a.py", source)
|
|
36
|
+
findings = run(source_file, empty_corpus)
|
|
37
|
+
assert len(findings) == 1
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_except_tuple_with_exception(make_source_file, empty_corpus):
|
|
41
|
+
source = (
|
|
42
|
+
"def f():\n"
|
|
43
|
+
" try:\n"
|
|
44
|
+
" pass\n"
|
|
45
|
+
" except (Exception, ValueError):\n"
|
|
46
|
+
" pass\n"
|
|
47
|
+
)
|
|
48
|
+
source_file = make_source_file("a.py", source)
|
|
49
|
+
findings = run(source_file, empty_corpus)
|
|
50
|
+
assert len(findings) == 1
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_except_specific_not_flagged(make_source_file, empty_corpus):
|
|
54
|
+
source = (
|
|
55
|
+
"def f():\n"
|
|
56
|
+
" try:\n"
|
|
57
|
+
" pass\n"
|
|
58
|
+
" except ValueError:\n"
|
|
59
|
+
" pass\n"
|
|
60
|
+
)
|
|
61
|
+
source_file = make_source_file("a.py", source)
|
|
62
|
+
findings = run(source_file, empty_corpus)
|
|
63
|
+
assert findings == []
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_except_star_exception(make_source_file, empty_corpus):
|
|
67
|
+
source = (
|
|
68
|
+
"def f():\n"
|
|
69
|
+
" try:\n"
|
|
70
|
+
" pass\n"
|
|
71
|
+
" except* Exception:\n"
|
|
72
|
+
" pass\n"
|
|
73
|
+
)
|
|
74
|
+
source_file = make_source_file("a.py", source)
|
|
75
|
+
findings = run(source_file, empty_corpus)
|
|
76
|
+
assert len(findings) == 1
|
|
77
|
+
assert findings[0].check_id == "broad-except"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_except_body_excerpt(make_source_file, empty_corpus):
|
|
81
|
+
source = (
|
|
82
|
+
"def f():\n"
|
|
83
|
+
" try:\n"
|
|
84
|
+
" pass\n"
|
|
85
|
+
" except Exception:\n"
|
|
86
|
+
" log_error()\n"
|
|
87
|
+
" cleanup()\n"
|
|
88
|
+
)
|
|
89
|
+
source_file = make_source_file("a.py", source)
|
|
90
|
+
findings = run(source_file, empty_corpus)
|
|
91
|
+
assert len(findings) == 1
|
|
92
|
+
excerpts = findings[0].excerpts
|
|
93
|
+
assert len(excerpts) == 1
|
|
94
|
+
assert excerpts[0].start_line == 4
|
|
95
|
+
assert excerpts[0].end_line == 6
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_except_long_try_body_split_excerpts(make_source_file, empty_corpus):
|
|
99
|
+
source = (
|
|
100
|
+
"def f():\n"
|
|
101
|
+
" try:\n"
|
|
102
|
+
+ " pass\n" * 20
|
|
103
|
+
+ " except Exception:\n"
|
|
104
|
+
+ " pass\n"
|
|
105
|
+
)
|
|
106
|
+
source_file = make_source_file("a.py", source)
|
|
107
|
+
findings = run(source_file, empty_corpus)
|
|
108
|
+
assert len(findings) == 1
|
|
109
|
+
excerpts = findings[0].excerpts
|
|
110
|
+
assert len(excerpts) == 1
|
|
111
|
+
handler_line = 2 + 20 + 1
|
|
112
|
+
assert excerpts[0].start_line == handler_line
|
|
113
|
+
assert excerpts[0].end_line == handler_line + 1
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_except_with_justifying_comment(make_source_file, empty_corpus):
|
|
117
|
+
source = (
|
|
118
|
+
"def f():\n"
|
|
119
|
+
"# We catch Exception here because the subprocess\n"
|
|
120
|
+
"# may already be dead during shutdown.\n"
|
|
121
|
+
" try:\n"
|
|
122
|
+
" pass\n"
|
|
123
|
+
" except Exception:\n"
|
|
124
|
+
" pass\n"
|
|
125
|
+
)
|
|
126
|
+
source_file = make_source_file("a.py", source)
|
|
127
|
+
findings = run(source_file, empty_corpus)
|
|
128
|
+
assert len(findings) == 1
|
|
129
|
+
excerpts = findings[0].excerpts
|
|
130
|
+
assert len(excerpts) == 2
|
|
131
|
+
assert excerpts[0].start_line == 2
|
|
132
|
+
assert excerpts[0].end_line == 4
|
|
133
|
+
assert excerpts[1].start_line == 6
|
|
134
|
+
assert excerpts[1].end_line == 7
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_try_with_comments_after(make_source_file, empty_corpus):
|
|
138
|
+
source = (
|
|
139
|
+
"def f():\n"
|
|
140
|
+
" try:\n"
|
|
141
|
+
" # This is a comment about the try\n"
|
|
142
|
+
" # And another line\n"
|
|
143
|
+
" actual_code()\n"
|
|
144
|
+
" more_code()\n"
|
|
145
|
+
" except Exception:\n"
|
|
146
|
+
" pass\n"
|
|
147
|
+
)
|
|
148
|
+
source_file = make_source_file("a.py", source)
|
|
149
|
+
findings = run(source_file, empty_corpus)
|
|
150
|
+
assert len(findings) == 1
|
|
151
|
+
excerpts = findings[0].excerpts
|
|
152
|
+
assert len(excerpts) == 2
|
|
153
|
+
first = excerpts[0]
|
|
154
|
+
second = excerpts[1]
|
|
155
|
+
assert first.start_line == 2
|
|
156
|
+
assert first.end_line == 4
|
|
157
|
+
assert second.start_line == 7
|
|
158
|
+
assert second.end_line == 8
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_try_with_comments_after_merged(make_source_file, empty_corpus):
|
|
162
|
+
source = (
|
|
163
|
+
"def f():\n"
|
|
164
|
+
" try:\n"
|
|
165
|
+
" # comment about try\n"
|
|
166
|
+
" pass\n"
|
|
167
|
+
" except Exception:\n"
|
|
168
|
+
" pass\n"
|
|
169
|
+
)
|
|
170
|
+
source_file = make_source_file("a.py", source)
|
|
171
|
+
findings = run(source_file, empty_corpus)
|
|
172
|
+
assert len(findings) == 1
|
|
173
|
+
excerpts = findings[0].excerpts
|
|
174
|
+
assert len(excerpts) == 2
|
|
175
|
+
assert excerpts[0].start_line == 2
|
|
176
|
+
assert excerpts[0].end_line == 3
|
|
177
|
+
assert excerpts[1].start_line == 5
|
|
178
|
+
assert excerpts[1].end_line == 6
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"""End-to-end CLI tests using click's ``CliRunner``."""
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
|
-
|
|
5
3
|
from click.testing import CliRunner
|
|
6
4
|
|
|
7
5
|
from python_surveyor.cli import main
|
|
@@ -27,20 +25,6 @@ def test_scan_text(tmp_path):
|
|
|
27
25
|
assert "file(s) scanned" in result.output
|
|
28
26
|
|
|
29
27
|
|
|
30
|
-
def test_scan_json(tmp_path):
|
|
31
|
-
(tmp_path / "a.py").write_text(
|
|
32
|
-
"from __future__ import annotations\n", encoding="utf-8"
|
|
33
|
-
)
|
|
34
|
-
runner = CliRunner()
|
|
35
|
-
result = runner.invoke(main, ["scan", str(tmp_path), "--format", "json"])
|
|
36
|
-
assert result.exit_code == 0
|
|
37
|
-
payload = json.loads(result.output)
|
|
38
|
-
assert payload["files_scanned"] == 1
|
|
39
|
-
assert any(
|
|
40
|
-
f["check_id"] == "future-annotations-import" for f in payload["findings"]
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
44
28
|
def test_scan_no_findings(tmp_path):
|
|
45
29
|
(tmp_path / "clean.py").write_text("CONST = 1\n", encoding="utf-8")
|
|
46
30
|
runner = CliRunner()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Tests for the ``fixture-naming`` check."""
|
|
2
2
|
|
|
3
|
+
from python_surveyor.checks import COVERED_PYLINT_IDS
|
|
3
4
|
from python_surveyor.checks.fixture_naming import run
|
|
4
5
|
from python_surveyor.scanner import _build_corpus
|
|
5
6
|
|
|
@@ -10,8 +11,9 @@ def test_plain_fixture_flagged(make_source_file, empty_corpus):
|
|
|
10
11
|
findings = run(source_file, empty_corpus)
|
|
11
12
|
assert len(findings) == 1
|
|
12
13
|
assert findings[0].check_id == "fixture-naming"
|
|
13
|
-
|
|
14
|
-
assert "
|
|
14
|
+
notes = "\n".join(findings[0].notes)
|
|
15
|
+
assert "no `name=` kwarg" in notes
|
|
16
|
+
assert "lacks `fixture_` prefix" in notes
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
def test_fixture_with_name_and_prefix_not_flagged(make_source_file, empty_corpus):
|
|
@@ -31,7 +33,8 @@ def test_fixture_prefix_without_name_flagged(make_source_file, empty_corpus):
|
|
|
31
33
|
source_file = make_source_file("a.py", source)
|
|
32
34
|
findings = run(source_file, empty_corpus)
|
|
33
35
|
assert len(findings) == 1
|
|
34
|
-
|
|
36
|
+
notes = "\n".join(findings[0].notes)
|
|
37
|
+
assert "no `name=` kwarg" in notes
|
|
35
38
|
|
|
36
39
|
|
|
37
40
|
def test_name_without_prefix_flagged(make_source_file, empty_corpus):
|
|
@@ -39,7 +42,8 @@ def test_name_without_prefix_flagged(make_source_file, empty_corpus):
|
|
|
39
42
|
source_file = make_source_file("a.py", source)
|
|
40
43
|
findings = run(source_file, empty_corpus)
|
|
41
44
|
assert len(findings) == 1
|
|
42
|
-
|
|
45
|
+
notes = "\n".join(findings[0].notes)
|
|
46
|
+
assert "lacks `fixture_` prefix" in notes
|
|
43
47
|
|
|
44
48
|
|
|
45
49
|
def test_bare_fixture_decorator(make_source_file, empty_corpus):
|
|
@@ -66,9 +70,28 @@ def test_collision_note(make_source_file):
|
|
|
66
70
|
" pass\n"
|
|
67
71
|
)
|
|
68
72
|
source_file = make_source_file("a.py", source)
|
|
69
|
-
corpus = _build_corpus([source_file],
|
|
73
|
+
corpus = _build_corpus([source_file], 10, COVERED_PYLINT_IDS)
|
|
70
74
|
findings = run(source_file, corpus)
|
|
71
75
|
assert findings
|
|
72
76
|
note = findings[0].notes[0]
|
|
73
77
|
assert "registered as `db`" in note
|
|
74
78
|
assert "1 other function" in note
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_autouse_fixture_not_flagged(make_source_file, empty_corpus):
|
|
82
|
+
source = (
|
|
83
|
+
"import pytest\n"
|
|
84
|
+
"@pytest.fixture(autouse=True)\n"
|
|
85
|
+
"def short_permission_ttl():\n"
|
|
86
|
+
" pass\n"
|
|
87
|
+
)
|
|
88
|
+
source_file = make_source_file("a.py", source)
|
|
89
|
+
findings = run(source_file, empty_corpus)
|
|
90
|
+
assert findings == []
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_conftest_not_flagged(make_source_file, empty_corpus):
|
|
94
|
+
source = "import pytest\n" "@pytest.fixture\n" "def db():\n" " pass\n"
|
|
95
|
+
source_file = make_source_file("conftest.py", source)
|
|
96
|
+
findings = run(source_file, empty_corpus)
|
|
97
|
+
assert findings == []
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Tests for the ``optional-param-default`` check."""
|
|
2
2
|
|
|
3
|
+
from python_surveyor.checks import COVERED_PYLINT_IDS
|
|
3
4
|
from python_surveyor.checks.optional_params import run
|
|
4
5
|
from python_surveyor.scanner import _build_corpus
|
|
5
6
|
|
|
@@ -10,7 +11,8 @@ def test_defaulted_positional_param(make_source_file, empty_corpus):
|
|
|
10
11
|
findings = run(source_file, empty_corpus)
|
|
11
12
|
assert len(findings) == 1
|
|
12
13
|
assert findings[0].check_id == "optional-param-default"
|
|
13
|
-
|
|
14
|
+
notes = "\n".join(findings[0].notes)
|
|
15
|
+
assert "`f`" in notes
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
def test_mixed_defaulted_positional(make_source_file, empty_corpus):
|
|
@@ -44,31 +46,29 @@ def test_args_kwargs_not_flagged(make_source_file, empty_corpus):
|
|
|
44
46
|
def test_call_site_note_with_corpus(make_source_file):
|
|
45
47
|
source = "def helper(a=1):\n" " pass\n" "def caller():\n" " helper()\n"
|
|
46
48
|
source_file = make_source_file("a.py", source)
|
|
47
|
-
corpus = _build_corpus([source_file],
|
|
49
|
+
corpus = _build_corpus([source_file], 10, COVERED_PYLINT_IDS)
|
|
48
50
|
findings = run(source_file, corpus)
|
|
49
51
|
assert findings
|
|
50
52
|
notes = "\n".join(findings[0].notes)
|
|
51
|
-
assert "using
|
|
52
|
-
assert "explicit value" in notes
|
|
53
|
+
assert "using defaults" in notes
|
|
53
54
|
|
|
54
55
|
|
|
55
56
|
def test_call_site_note_not_called(make_source_file):
|
|
56
|
-
source = "def helper(a=1):\n pass\n"
|
|
57
|
+
source = "def helper(a=1):\n" " pass\n"
|
|
57
58
|
source_file = make_source_file("a.py", source)
|
|
58
|
-
corpus = _build_corpus([source_file],
|
|
59
|
+
corpus = _build_corpus([source_file], 10, COVERED_PYLINT_IDS)
|
|
59
60
|
findings = run(source_file, corpus)
|
|
60
61
|
assert findings
|
|
61
|
-
assert "not called from anywhere" in findings[0].notes[
|
|
62
|
+
assert "not called from anywhere" in findings[0].notes[1]
|
|
62
63
|
|
|
63
64
|
|
|
64
65
|
def test_call_site_note_override(make_source_file):
|
|
65
66
|
source = "def helper(a=1):\n" " pass\n" "def caller():\n" " helper(2)\n"
|
|
66
67
|
source_file = make_source_file("a.py", source)
|
|
67
|
-
corpus = _build_corpus([source_file],
|
|
68
|
+
corpus = _build_corpus([source_file], 10, COVERED_PYLINT_IDS)
|
|
68
69
|
findings = run(source_file, corpus)
|
|
69
70
|
notes = "\n".join(findings[0].notes)
|
|
70
|
-
assert "
|
|
71
|
-
assert "using default" in notes
|
|
71
|
+
assert "override every defaulted parameter" in notes
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
def test_call_site_note_mixed(make_source_file):
|
|
@@ -81,18 +81,51 @@ def test_call_site_note_mixed(make_source_file):
|
|
|
81
81
|
" helper(2)\n"
|
|
82
82
|
)
|
|
83
83
|
source_file = make_source_file("a.py", source)
|
|
84
|
-
corpus = _build_corpus([source_file],
|
|
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)
|
|
85
97
|
findings = run(source_file, corpus)
|
|
86
98
|
notes = "\n".join(findings[0].notes)
|
|
87
|
-
assert "
|
|
88
|
-
assert "using default" in notes
|
|
99
|
+
assert "and 5 other(s)" in notes
|
|
89
100
|
|
|
90
101
|
|
|
91
|
-
def
|
|
102
|
+
def test_defaulted_param_names_in_notes(make_source_file, empty_corpus):
|
|
92
103
|
source = "def f(a, b=2, *, c=3):\n pass\n"
|
|
93
104
|
source_file = make_source_file("a.py", source)
|
|
94
105
|
findings = run(source_file, empty_corpus)
|
|
95
106
|
assert len(findings) == 1
|
|
96
|
-
|
|
97
|
-
assert "
|
|
98
|
-
assert "
|
|
107
|
+
notes = "\n".join(findings[0].notes)
|
|
108
|
+
assert "b" in notes
|
|
109
|
+
assert "c" in notes
|
|
110
|
+
# "a" should not appear in the defaulted-params note (it has no default)
|
|
111
|
+
defaulted_note = [n for n in findings[0].notes if "Defaulted params" in n][0]
|
|
112
|
+
assert "a" not in defaulted_note.split(":")[-1]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_signature_excerpt_not_full_body(make_source_file, empty_corpus):
|
|
116
|
+
source = "def f(a=1):\n" + " x = 1\n" * 30 + " return x\n"
|
|
117
|
+
source_file = make_source_file("a.py", source)
|
|
118
|
+
findings = run(source_file, empty_corpus)
|
|
119
|
+
assert len(findings) == 1
|
|
120
|
+
excerpt = findings[0].excerpts[0]
|
|
121
|
+
assert excerpt.start_line == 1
|
|
122
|
+
assert excerpt.end_line == 1
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_no_api_note(make_source_file, empty_corpus):
|
|
126
|
+
source = "def _helper(a=1):\n pass\n"
|
|
127
|
+
source_file = make_source_file("a.py", source)
|
|
128
|
+
findings = run(source_file, empty_corpus)
|
|
129
|
+
assert len(findings) == 1
|
|
130
|
+
notes = "\n".join(findings[0].notes)
|
|
131
|
+
assert "API" not in notes
|
|
@@ -5,6 +5,7 @@ import dataclasses
|
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
7
|
|
|
8
|
+
from python_surveyor.checks import COVERED_PYLINT_IDS
|
|
8
9
|
from python_surveyor.scanner import (
|
|
9
10
|
Corpus,
|
|
10
11
|
ScanResult,
|
|
@@ -122,7 +123,7 @@ def test_build_corpus_call_sites(tmp_path):
|
|
|
122
123
|
tree=tree,
|
|
123
124
|
tokens=(),
|
|
124
125
|
)
|
|
125
|
-
corpus = _build_corpus([source_file],
|
|
126
|
+
corpus = _build_corpus([source_file], 5, COVERED_PYLINT_IDS)
|
|
126
127
|
assert "helper" in corpus.call_sites
|
|
127
128
|
assert "method" in corpus.call_sites
|
|
128
129
|
assert corpus.call_sites["helper"][0].positional_count == 0
|
|
@@ -142,7 +143,7 @@ def test_build_corpus_call_args(tmp_path):
|
|
|
142
143
|
tree=tree,
|
|
143
144
|
tokens=(),
|
|
144
145
|
)
|
|
145
|
-
corpus = _build_corpus([source_file],
|
|
146
|
+
corpus = _build_corpus([source_file], 5, COVERED_PYLINT_IDS)
|
|
146
147
|
sites = corpus.call_sites["f"]
|
|
147
148
|
assert len(sites) == 3
|
|
148
149
|
assert sites[0].positional_count == 1
|
|
@@ -165,7 +166,7 @@ def test_build_corpus_param_locations(tmp_path):
|
|
|
165
166
|
tree=tree,
|
|
166
167
|
tokens=(),
|
|
167
168
|
)
|
|
168
|
-
corpus = _build_corpus([source_file],
|
|
169
|
+
corpus = _build_corpus([source_file], 5, COVERED_PYLINT_IDS)
|
|
169
170
|
assert "db" in corpus.param_locations
|
|
170
171
|
assert len(corpus.param_locations["db"]) == 2
|
|
171
172
|
|
|
@@ -188,7 +189,12 @@ def test_scan_result_is_sorted(tmp_path):
|
|
|
188
189
|
|
|
189
190
|
|
|
190
191
|
def test_corpus_is_immutable():
|
|
191
|
-
corpus = Corpus(
|
|
192
|
+
corpus = Corpus(
|
|
193
|
+
call_sites={},
|
|
194
|
+
param_locations={},
|
|
195
|
+
max_call_sites=5,
|
|
196
|
+
covered_pylint_ids=COVERED_PYLINT_IDS,
|
|
197
|
+
)
|
|
192
198
|
with pytest.raises(dataclasses.FrozenInstanceError):
|
|
193
199
|
corpus.max_call_sites = 10 # type: ignore[misc]
|
|
194
200
|
|
|
@@ -11,7 +11,8 @@ def test_pylint_disable_without_preceding_comment(make_source_file, empty_corpus
|
|
|
11
11
|
finding = findings[0]
|
|
12
12
|
assert finding.check_id == "suppression-comment"
|
|
13
13
|
assert finding.notes == ()
|
|
14
|
-
assert
|
|
14
|
+
assert len(finding.excerpts) == 1
|
|
15
|
+
assert finding.excerpts[0].start_line == 1
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def test_pyright_ignore_without_preceding_comment(make_source_file, empty_corpus):
|
|
@@ -20,7 +21,8 @@ def test_pyright_ignore_without_preceding_comment(make_source_file, empty_corpus
|
|
|
20
21
|
findings = run(source_file, empty_corpus)
|
|
21
22
|
assert len(findings) == 1
|
|
22
23
|
assert findings[0].notes == ()
|
|
23
|
-
assert
|
|
24
|
+
assert len(findings[0].excerpts) == 1
|
|
25
|
+
assert findings[0].excerpts[0].start_line == 1
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
def test_pylint_disable_next_line(make_source_file, empty_corpus):
|
|
@@ -42,7 +44,6 @@ def test_suppression_with_preceding_comment(make_source_file, empty_corpus):
|
|
|
42
44
|
assert len(findings) == 1
|
|
43
45
|
assert findings[0].notes == ()
|
|
44
46
|
excerpt = findings[0].excerpts[0]
|
|
45
|
-
assert excerpt.label == "source"
|
|
46
47
|
assert excerpt.start_line == 1
|
|
47
48
|
assert excerpt.end_line == 3
|
|
48
49
|
|
|
@@ -113,10 +114,17 @@ def test_broad_exception_caught_not_flagged(make_source_file, empty_corpus):
|
|
|
113
114
|
assert findings == []
|
|
114
115
|
|
|
115
116
|
|
|
116
|
-
def
|
|
117
|
+
def test_broad_exception_caught_mixed_list_flagged(make_source_file, empty_corpus):
|
|
117
118
|
source = "x = 1 # pylint: disable=broad-exception-caught,missing-docstring\n"
|
|
118
119
|
source_file = make_source_file("a.py", source)
|
|
119
120
|
findings = run(source_file, empty_corpus)
|
|
121
|
+
assert len(findings) == 1
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_all_covered_in_list_not_flagged(make_source_file, empty_corpus):
|
|
125
|
+
source = "x = 1 # pylint: disable=broad-exception-caught,import-outside-toplevel\n"
|
|
126
|
+
source_file = make_source_file("a.py", source)
|
|
127
|
+
findings = run(source_file, empty_corpus)
|
|
120
128
|
assert findings == []
|
|
121
129
|
|
|
122
130
|
|
|
@@ -127,8 +135,15 @@ def test_redefined_outer_name_not_flagged(make_source_file, empty_corpus):
|
|
|
127
135
|
assert findings == []
|
|
128
136
|
|
|
129
137
|
|
|
130
|
-
def
|
|
138
|
+
def test_redefined_outer_name_mixed_list_flagged(make_source_file, empty_corpus):
|
|
131
139
|
source = "x = 1 # pylint: disable=redefined-outer-name,missing-docstring\n"
|
|
132
140
|
source_file = make_source_file("a.py", source)
|
|
133
141
|
findings = run(source_file, empty_corpus)
|
|
142
|
+
assert len(findings) == 1
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_import_outside_toplevel_not_flagged(make_source_file, empty_corpus):
|
|
146
|
+
source = "x = 1 # pylint: disable=import-outside-toplevel\n"
|
|
147
|
+
source_file = make_source_file("a.py", source)
|
|
148
|
+
findings = run(source_file, empty_corpus)
|
|
134
149
|
assert findings == []
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
"""Tests for the ``broad-except`` check."""
|
|
2
|
-
|
|
3
|
-
from python_surveyor.checks.broad_except import run
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def test_except_exception(make_source_file, empty_corpus):
|
|
7
|
-
source = (
|
|
8
|
-
"def f():\n"
|
|
9
|
-
" try:\n"
|
|
10
|
-
" pass\n"
|
|
11
|
-
" except Exception:\n"
|
|
12
|
-
" pass\n"
|
|
13
|
-
)
|
|
14
|
-
source_file = make_source_file("a.py", source)
|
|
15
|
-
findings = run(source_file, empty_corpus)
|
|
16
|
-
assert len(findings) == 1
|
|
17
|
-
assert findings[0].check_id == "broad-except"
|
|
18
|
-
assert "Exception" in findings[0].message
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def test_except_bare(make_source_file, empty_corpus):
|
|
22
|
-
source = "def f():\n" " try:\n" " pass\n" " except:\n" " pass\n"
|
|
23
|
-
source_file = make_source_file("a.py", source)
|
|
24
|
-
findings = run(source_file, empty_corpus)
|
|
25
|
-
assert len(findings) == 1
|
|
26
|
-
assert "bare" in findings[0].message
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def test_except_baseexception(make_source_file, empty_corpus):
|
|
30
|
-
source = (
|
|
31
|
-
"def f():\n"
|
|
32
|
-
" try:\n"
|
|
33
|
-
" pass\n"
|
|
34
|
-
" except BaseException:\n"
|
|
35
|
-
" pass\n"
|
|
36
|
-
)
|
|
37
|
-
source_file = make_source_file("a.py", source)
|
|
38
|
-
findings = run(source_file, empty_corpus)
|
|
39
|
-
assert len(findings) == 1
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def test_except_tuple_with_exception(make_source_file, empty_corpus):
|
|
43
|
-
source = (
|
|
44
|
-
"def f():\n"
|
|
45
|
-
" try:\n"
|
|
46
|
-
" pass\n"
|
|
47
|
-
" except (Exception, ValueError):\n"
|
|
48
|
-
" pass\n"
|
|
49
|
-
)
|
|
50
|
-
source_file = make_source_file("a.py", source)
|
|
51
|
-
findings = run(source_file, empty_corpus)
|
|
52
|
-
assert len(findings) == 1
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_except_specific_not_flagged(make_source_file, empty_corpus):
|
|
56
|
-
source = (
|
|
57
|
-
"def f():\n"
|
|
58
|
-
" try:\n"
|
|
59
|
-
" pass\n"
|
|
60
|
-
" except ValueError:\n"
|
|
61
|
-
" pass\n"
|
|
62
|
-
)
|
|
63
|
-
source_file = make_source_file("a.py", source)
|
|
64
|
-
findings = run(source_file, empty_corpus)
|
|
65
|
-
assert findings == []
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def test_except_star_exception(make_source_file, empty_corpus):
|
|
69
|
-
source = (
|
|
70
|
-
"def f():\n"
|
|
71
|
-
" try:\n"
|
|
72
|
-
" pass\n"
|
|
73
|
-
" except* Exception:\n"
|
|
74
|
-
" pass\n"
|
|
75
|
-
)
|
|
76
|
-
source_file = make_source_file("a.py", source)
|
|
77
|
-
findings = run(source_file, empty_corpus)
|
|
78
|
-
assert len(findings) == 1
|
|
79
|
-
assert "except*" in findings[0].message
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def test_except_body_excerpt(make_source_file, empty_corpus):
|
|
83
|
-
source = (
|
|
84
|
-
"def f():\n"
|
|
85
|
-
" try:\n"
|
|
86
|
-
" pass\n"
|
|
87
|
-
" except Exception:\n"
|
|
88
|
-
" log_error()\n"
|
|
89
|
-
" cleanup()\n"
|
|
90
|
-
)
|
|
91
|
-
source_file = make_source_file("a.py", source)
|
|
92
|
-
findings = run(source_file, empty_corpus)
|
|
93
|
-
assert findings[0].excerpts[0].label == "source"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_surveyor-0.1.0 → python_surveyor-0.2.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
|