python-surveyor 0.2.0__tar.gz → 0.4.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.2.0/python_surveyor.egg-info → python_surveyor-0.4.0}/PKG-INFO +3 -1
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/pyproject.toml +3 -1
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor/model.py +2 -7
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor/report.py +16 -12
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor/scanner.py +5 -0
- {python_surveyor-0.2.0 → python_surveyor-0.4.0/python_surveyor.egg-info}/PKG-INFO +3 -1
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor.egg-info/requires.txt +2 -0
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/tests/test_broad_except.py +41 -0
- python_surveyor-0.4.0/tests/test_optional_params.py +289 -0
- python_surveyor-0.2.0/tests/test_optional_params.py +0 -131
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/LICENSE +0 -0
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/README.md +0 -0
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor/__init__.py +0 -0
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor/__main__.py +0 -0
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor/cli.py +0 -0
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor.egg-info/SOURCES.txt +0 -0
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor.egg-info/dependency_links.txt +0 -0
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor.egg-info/entry_points.txt +0 -0
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor.egg-info/top_level.txt +0 -0
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/setup.cfg +0 -0
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/tests/test_cli.py +0 -0
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/tests/test_fixture_naming.py +0 -0
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/tests/test_future_annotations.py +0 -0
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/tests/test_nontoplevel_imports.py +0 -0
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/tests/test_scanner.py +0 -0
- {python_surveyor-0.2.0 → python_surveyor-0.4.0}/tests/test_suppressions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-surveyor
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Use python introspection to survey source code for final LLM judgement
|
|
5
5
|
Author: Dave Cunningham
|
|
6
6
|
License-Expression: MIT
|
|
@@ -9,6 +9,8 @@ Requires-Python: >=3.12
|
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Dist: click>=8.1.0
|
|
11
11
|
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: fastapi>=0.115.0; extra == "dev"
|
|
13
|
+
Requires-Dist: flask>=3.0.0; extra == "dev"
|
|
12
14
|
Requires-Dist: pyright==1.1.411; extra == "dev"
|
|
13
15
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
14
16
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -8,7 +8,7 @@ include = ["python_surveyor"]
|
|
|
8
8
|
|
|
9
9
|
[project]
|
|
10
10
|
name = "python-surveyor"
|
|
11
|
-
version = "0.
|
|
11
|
+
version = "0.4.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"
|
|
@@ -24,6 +24,8 @@ dependencies = [
|
|
|
24
24
|
|
|
25
25
|
[project.optional-dependencies]
|
|
26
26
|
dev = [
|
|
27
|
+
"fastapi>=0.115.0",
|
|
28
|
+
"flask>=3.0.0",
|
|
27
29
|
"pyright==1.1.411",
|
|
28
30
|
"pytest>=8.0.0",
|
|
29
31
|
"pytest-cov",
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
"""Frozen dataclasses shared by scanner, checks, and report renderers.
|
|
2
|
-
|
|
3
|
-
All construction uses required positional fields; there are no
|
|
4
|
-
default-valued fields on ``Finding``/``SourceExcerpt``, consistent with the
|
|
5
|
-
"no optional params on internal APIs" rule this tool itself checks for.
|
|
6
|
-
"""
|
|
1
|
+
"""Frozen dataclasses shared by scanner, checks, and report renderers."""
|
|
7
2
|
|
|
8
3
|
from dataclasses import dataclass
|
|
9
4
|
from pathlib import Path
|
|
@@ -51,7 +46,7 @@ class Finding:
|
|
|
51
46
|
line: int
|
|
52
47
|
column: int
|
|
53
48
|
excerpts: tuple[SourceExcerpt, ...]
|
|
54
|
-
notes: tuple[str, ...]
|
|
49
|
+
notes: tuple[str, ...] = ()
|
|
55
50
|
|
|
56
51
|
|
|
57
52
|
@dataclass(frozen=True)
|
|
@@ -6,7 +6,6 @@ and note lines. Parse errors get their own leading section. Excerpts are
|
|
|
6
6
|
capped to ``--max-excerpt-lines`` with a truncation note.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from pathlib import Path
|
|
10
9
|
from typing import TextIO
|
|
11
10
|
|
|
12
11
|
from python_surveyor.checks import CHECKS_BY_ID
|
|
@@ -14,25 +13,28 @@ from python_surveyor.scanner import ScanResult
|
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
def _read_excerpt_lines(
|
|
17
|
-
|
|
16
|
+
source_lines: "dict[str, tuple[str, ...]]",
|
|
17
|
+
path_str: str,
|
|
18
|
+
start: int,
|
|
19
|
+
end: int,
|
|
20
|
+
max_lines: int,
|
|
18
21
|
) -> "tuple[list[str], str | None]":
|
|
19
|
-
"""Read ``[start, end]`` from ``
|
|
22
|
+
"""Read ``[start, end]`` from ``source_lines``, capping to ``max_lines``.
|
|
20
23
|
|
|
21
24
|
Returns ``(lines, truncation_note)``.
|
|
22
25
|
"""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
except OSError:
|
|
26
|
+
all_lines = source_lines.get(path_str)
|
|
27
|
+
if all_lines is None:
|
|
26
28
|
return ([], None)
|
|
27
29
|
available_end = min(end, len(all_lines))
|
|
28
30
|
if available_end < start:
|
|
29
31
|
return ([], None)
|
|
30
32
|
span = available_end - start + 1
|
|
31
33
|
if span <= max_lines:
|
|
32
|
-
return (all_lines[start - 1 : available_end], None)
|
|
34
|
+
return (list(all_lines[start - 1 : available_end]), None)
|
|
33
35
|
capped_end = start + max_lines - 1
|
|
34
36
|
return (
|
|
35
|
-
all_lines[start - 1 : capped_end],
|
|
37
|
+
list(all_lines[start - 1 : capped_end]),
|
|
36
38
|
f"excerpt truncated at {max_lines} lines",
|
|
37
39
|
)
|
|
38
40
|
|
|
@@ -60,17 +62,19 @@ def render_text(
|
|
|
60
62
|
if spec is not None:
|
|
61
63
|
stream.write(f"{spec.explanation}\n\n")
|
|
62
64
|
counter += 1
|
|
63
|
-
|
|
65
|
+
prev_path_str: str | None = None
|
|
64
66
|
prev_end: int | None = None
|
|
65
67
|
for excerpt in finding.excerpts:
|
|
68
|
+
path_str = str(excerpt.path)
|
|
66
69
|
lines, truncation = _read_excerpt_lines(
|
|
67
|
-
|
|
70
|
+
result.source_lines,
|
|
71
|
+
path_str,
|
|
68
72
|
excerpt.start_line,
|
|
69
73
|
excerpt.end_line,
|
|
70
74
|
max_excerpt_lines,
|
|
71
75
|
)
|
|
72
76
|
is_continuation = (
|
|
73
|
-
|
|
77
|
+
prev_path_str == path_str
|
|
74
78
|
and prev_end is not None
|
|
75
79
|
and excerpt.start_line > prev_end + 1
|
|
76
80
|
)
|
|
@@ -88,7 +92,7 @@ def render_text(
|
|
|
88
92
|
stream.write(f"{line}\n")
|
|
89
93
|
if truncation:
|
|
90
94
|
stream.write(f"... {truncation}\n")
|
|
91
|
-
|
|
95
|
+
prev_path_str = path_str
|
|
92
96
|
prev_end = excerpt.end_line
|
|
93
97
|
for note in finding.notes:
|
|
94
98
|
stream.write(f" note: {note}\n")
|
|
@@ -87,6 +87,7 @@ class ScanResult:
|
|
|
87
87
|
findings: tuple[Finding, ...]
|
|
88
88
|
parse_errors: tuple[ParseError, ...]
|
|
89
89
|
files_scanned: int
|
|
90
|
+
source_lines: "dict[str, tuple[str, ...]]"
|
|
90
91
|
|
|
91
92
|
|
|
92
93
|
def _discover(root_paths: tuple[str, ...], excludes: tuple[str, ...]) -> Iterator[Path]:
|
|
@@ -176,6 +177,9 @@ def _collect_params(
|
|
|
176
177
|
args = func_node.args
|
|
177
178
|
skip_names = {"self", "cls"}
|
|
178
179
|
names: list[str] = []
|
|
180
|
+
for arg in args.posonlyargs:
|
|
181
|
+
if arg.arg not in skip_names:
|
|
182
|
+
names.append(arg.arg)
|
|
179
183
|
for arg in args.args:
|
|
180
184
|
if arg.arg not in skip_names:
|
|
181
185
|
names.append(arg.arg)
|
|
@@ -254,4 +258,5 @@ def scan(
|
|
|
254
258
|
findings=tuple(findings),
|
|
255
259
|
parse_errors=tuple(parse_errors),
|
|
256
260
|
files_scanned=len(source_files),
|
|
261
|
+
source_lines={str(f.path): f.lines for f in source_files},
|
|
257
262
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-surveyor
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Use python introspection to survey source code for final LLM judgement
|
|
5
5
|
Author: Dave Cunningham
|
|
6
6
|
License-Expression: MIT
|
|
@@ -9,6 +9,8 @@ Requires-Python: >=3.12
|
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Dist: click>=8.1.0
|
|
11
11
|
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: fastapi>=0.115.0; extra == "dev"
|
|
13
|
+
Requires-Dist: flask>=3.0.0; extra == "dev"
|
|
12
14
|
Requires-Dist: pyright==1.1.411; extra == "dev"
|
|
13
15
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
14
16
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -63,6 +63,47 @@ def test_except_specific_not_flagged(make_source_file, empty_corpus):
|
|
|
63
63
|
assert findings == []
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
def test_bare_reraise_not_flagged(make_source_file, empty_corpus):
|
|
67
|
+
source = (
|
|
68
|
+
"def f():\n"
|
|
69
|
+
" try:\n"
|
|
70
|
+
" pass\n"
|
|
71
|
+
" except Exception:\n"
|
|
72
|
+
" raise\n"
|
|
73
|
+
)
|
|
74
|
+
source_file = make_source_file("a.py", source)
|
|
75
|
+
findings = run(source_file, empty_corpus)
|
|
76
|
+
assert findings == []
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_logger_exception_not_flagged(make_source_file, empty_corpus):
|
|
80
|
+
source = (
|
|
81
|
+
"import logging\n"
|
|
82
|
+
"logger = logging.getLogger(__name__)\n"
|
|
83
|
+
"def f():\n"
|
|
84
|
+
" try:\n"
|
|
85
|
+
" pass\n"
|
|
86
|
+
" except Exception:\n"
|
|
87
|
+
" logger.exception('failed')\n"
|
|
88
|
+
)
|
|
89
|
+
source_file = make_source_file("a.py", source)
|
|
90
|
+
findings = run(source_file, empty_corpus)
|
|
91
|
+
assert findings == []
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_reraise_with_value_still_flagged(make_source_file, empty_corpus):
|
|
95
|
+
source = (
|
|
96
|
+
"def f():\n"
|
|
97
|
+
" try:\n"
|
|
98
|
+
" pass\n"
|
|
99
|
+
" except Exception as e:\n"
|
|
100
|
+
" raise RuntimeError('bad') from e\n"
|
|
101
|
+
)
|
|
102
|
+
source_file = make_source_file("a.py", source)
|
|
103
|
+
findings = run(source_file, empty_corpus)
|
|
104
|
+
assert len(findings) == 1
|
|
105
|
+
|
|
106
|
+
|
|
66
107
|
def test_except_star_exception(make_source_file, empty_corpus):
|
|
67
108
|
source = (
|
|
68
109
|
"def f():\n"
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Tests for the ``optional-param-default`` check."""
|
|
2
|
+
|
|
3
|
+
from python_surveyor.checks import COVERED_PYLINT_IDS
|
|
4
|
+
from python_surveyor.checks.optional_params import run
|
|
5
|
+
from python_surveyor.scanner import _build_corpus
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_defaulted_positional_param(make_source_file, empty_corpus):
|
|
9
|
+
source = "def f(a=1):\n pass\n"
|
|
10
|
+
source_file = make_source_file("a.py", source)
|
|
11
|
+
findings = run(source_file, empty_corpus)
|
|
12
|
+
assert len(findings) == 1
|
|
13
|
+
assert findings[0].check_id == "optional-param-default"
|
|
14
|
+
notes = "\n".join(findings[0].notes)
|
|
15
|
+
assert "`f`" in notes
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_mixed_defaulted_positional(make_source_file, empty_corpus):
|
|
19
|
+
source = "def f(a, b=2):\n pass\n"
|
|
20
|
+
source_file = make_source_file("a.py", source)
|
|
21
|
+
findings = run(source_file, empty_corpus)
|
|
22
|
+
assert len(findings) == 1
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_defaulted_kwonly(make_source_file, empty_corpus):
|
|
26
|
+
source = "def f(*, a=1):\n pass\n"
|
|
27
|
+
source_file = make_source_file("a.py", source)
|
|
28
|
+
findings = run(source_file, empty_corpus)
|
|
29
|
+
assert len(findings) == 1
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_no_defaults_not_flagged(make_source_file, empty_corpus):
|
|
33
|
+
source = "def f(a, b):\n pass\n"
|
|
34
|
+
source_file = make_source_file("a.py", source)
|
|
35
|
+
findings = run(source_file, empty_corpus)
|
|
36
|
+
assert findings == []
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_args_kwargs_not_flagged(make_source_file, empty_corpus):
|
|
40
|
+
source = "def f(*args, **kwargs):\n pass\n"
|
|
41
|
+
source_file = make_source_file("a.py", source)
|
|
42
|
+
findings = run(source_file, empty_corpus)
|
|
43
|
+
assert findings == []
|
|
44
|
+
|
|
45
|
+
|
|
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
|
+
def test_defaulted_param_names_in_notes(make_source_file, empty_corpus):
|
|
103
|
+
source = "def f(a, b=2, *, c=3):\n pass\n"
|
|
104
|
+
source_file = make_source_file("a.py", source)
|
|
105
|
+
findings = run(source_file, empty_corpus)
|
|
106
|
+
assert len(findings) == 1
|
|
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_signature_elides_non_defaulted_params(make_source_file, empty_corpus):
|
|
126
|
+
source = "def f(a, b, c=3):\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
|
+
excerpt = findings[0].excerpts[0]
|
|
131
|
+
assert excerpt.start_line == 1
|
|
132
|
+
assert excerpt.end_line == 1
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_signature_elides_with_multiline_def(make_source_file, empty_corpus):
|
|
136
|
+
source = "def f(\n a,\n b,\n c=3,\n):\n pass\n"
|
|
137
|
+
source_file = make_source_file("a.py", source)
|
|
138
|
+
findings = run(source_file, empty_corpus)
|
|
139
|
+
assert len(findings) == 1
|
|
140
|
+
excerpt = findings[0].excerpts[0]
|
|
141
|
+
assert excerpt.start_line == 4
|
|
142
|
+
assert excerpt.end_line == 5
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# --- FastAPI / Flask route-handler exclusion ------------------------------
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_fastapi_app_get_handler_excluded(make_source_file, empty_corpus):
|
|
149
|
+
source = (
|
|
150
|
+
"from fastapi import FastAPI\n"
|
|
151
|
+
"app = FastAPI()\n"
|
|
152
|
+
"@app.get('/x')\n"
|
|
153
|
+
"def handler(q: str = 'a'):\n"
|
|
154
|
+
" return q\n"
|
|
155
|
+
)
|
|
156
|
+
source_file = make_source_file("a.py", source)
|
|
157
|
+
findings = run(source_file, empty_corpus)
|
|
158
|
+
assert findings == []
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_fastapi_router_post_handler_excluded(make_source_file, empty_corpus):
|
|
162
|
+
source = (
|
|
163
|
+
"from fastapi import APIRouter as AR\n"
|
|
164
|
+
"router = AR()\n"
|
|
165
|
+
"@router.post('/x')\n"
|
|
166
|
+
"async def create(q: int = 0):\n"
|
|
167
|
+
" return q\n"
|
|
168
|
+
)
|
|
169
|
+
source_file = make_source_file("a.py", source)
|
|
170
|
+
findings = run(source_file, empty_corpus)
|
|
171
|
+
assert findings == []
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_fastapi_module_import_form_excluded(make_source_file, empty_corpus):
|
|
175
|
+
source = (
|
|
176
|
+
"import fastapi\n"
|
|
177
|
+
"app = fastapi.FastAPI()\n"
|
|
178
|
+
"@app.get('/x')\n"
|
|
179
|
+
"def handler(q: str = 'a'):\n"
|
|
180
|
+
" return q\n"
|
|
181
|
+
)
|
|
182
|
+
source_file = make_source_file("a.py", source)
|
|
183
|
+
findings = run(source_file, empty_corpus)
|
|
184
|
+
assert findings == []
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_fastapi_api_route_handler_excluded(make_source_file, empty_corpus):
|
|
188
|
+
source = (
|
|
189
|
+
"from fastapi import FastAPI\n"
|
|
190
|
+
"app = FastAPI()\n"
|
|
191
|
+
"@app.api_route('/x', methods=['GET'])\n"
|
|
192
|
+
"def handler(q: str = 'a'):\n"
|
|
193
|
+
" return q\n"
|
|
194
|
+
)
|
|
195
|
+
source_file = make_source_file("a.py", source)
|
|
196
|
+
findings = run(source_file, empty_corpus)
|
|
197
|
+
assert findings == []
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_fastapi_websocket_handler_excluded(make_source_file, empty_corpus):
|
|
201
|
+
source = (
|
|
202
|
+
"from fastapi import FastAPI\n"
|
|
203
|
+
"app = FastAPI()\n"
|
|
204
|
+
"@app.websocket('/ws')\n"
|
|
205
|
+
"async def ws_handler(q: str = 'a'):\n"
|
|
206
|
+
" pass\n"
|
|
207
|
+
)
|
|
208
|
+
source_file = make_source_file("a.py", source)
|
|
209
|
+
findings = run(source_file, empty_corpus)
|
|
210
|
+
assert findings == []
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_flask_app_route_handler_excluded(make_source_file, empty_corpus):
|
|
214
|
+
source = (
|
|
215
|
+
"from flask import Flask\n"
|
|
216
|
+
"app = Flask(__name__)\n"
|
|
217
|
+
"@app.route('/x')\n"
|
|
218
|
+
"def handler(q='a'):\n"
|
|
219
|
+
" return q\n"
|
|
220
|
+
)
|
|
221
|
+
source_file = make_source_file("a.py", source)
|
|
222
|
+
findings = run(source_file, empty_corpus)
|
|
223
|
+
assert findings == []
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_flask_blueprint_get_handler_excluded(make_source_file, empty_corpus):
|
|
227
|
+
source = (
|
|
228
|
+
"from flask import Blueprint\n"
|
|
229
|
+
"bp = Blueprint('bp', __name__)\n"
|
|
230
|
+
"@bp.get('/x')\n"
|
|
231
|
+
"def handler(q='a'):\n"
|
|
232
|
+
" return q\n"
|
|
233
|
+
)
|
|
234
|
+
source_file = make_source_file("a.py", source)
|
|
235
|
+
findings = run(source_file, empty_corpus)
|
|
236
|
+
assert findings == []
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_fastapi_handler_and_plain_function_mixed(make_source_file, empty_corpus):
|
|
240
|
+
"""A route handler is excluded but a plain defaulted function still flagged."""
|
|
241
|
+
source = (
|
|
242
|
+
"from fastapi import FastAPI\n"
|
|
243
|
+
"app = FastAPI()\n"
|
|
244
|
+
"@app.get('/x')\n"
|
|
245
|
+
"def handler(q: str = 'a'):\n"
|
|
246
|
+
" return q\n"
|
|
247
|
+
"def plain(x=1):\n"
|
|
248
|
+
" return x\n"
|
|
249
|
+
)
|
|
250
|
+
source_file = make_source_file("a.py", source)
|
|
251
|
+
findings = run(source_file, empty_corpus)
|
|
252
|
+
assert len(findings) == 1
|
|
253
|
+
assert findings[0].line == 6
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_non_framework_decorator_still_flagged(make_source_file, empty_corpus):
|
|
257
|
+
"""A custom .get() decorator with no fastapi/flask import is still flagged."""
|
|
258
|
+
source = (
|
|
259
|
+
"class MyAPI:\n"
|
|
260
|
+
" def get(self, *a, **k):\n"
|
|
261
|
+
" def deco(f):\n"
|
|
262
|
+
" return f\n"
|
|
263
|
+
" return deco\n"
|
|
264
|
+
"api = MyAPI()\n"
|
|
265
|
+
"@api.get()\n"
|
|
266
|
+
"def handler(q=1):\n"
|
|
267
|
+
" return q\n"
|
|
268
|
+
)
|
|
269
|
+
source_file = make_source_file("a.py", source)
|
|
270
|
+
findings = run(source_file, empty_corpus)
|
|
271
|
+
assert len(findings) == 1
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def test_no_framework_import_still_flagged(make_source_file, empty_corpus):
|
|
275
|
+
"""Decorator shaped like a route but with no fastapi/flask import is flagged."""
|
|
276
|
+
source = (
|
|
277
|
+
"app = object()\n" "@app.get('/x')\n" "def handler(q=1):\n" " return q\n"
|
|
278
|
+
)
|
|
279
|
+
source_file = make_source_file("a.py", source)
|
|
280
|
+
findings = run(source_file, empty_corpus)
|
|
281
|
+
assert len(findings) == 1
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def test_staticmethod_decorator_still_flagged(make_source_file, empty_corpus):
|
|
285
|
+
"""Unrelated decorators (staticmethod) don't cause exclusion."""
|
|
286
|
+
source = "class C:\n" " @staticmethod\n" " def f(a=1):\n" " return a\n"
|
|
287
|
+
source_file = make_source_file("a.py", source)
|
|
288
|
+
findings = run(source_file, empty_corpus)
|
|
289
|
+
assert len(findings) == 1
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
"""Tests for the ``optional-param-default`` check."""
|
|
2
|
-
|
|
3
|
-
from python_surveyor.checks import COVERED_PYLINT_IDS
|
|
4
|
-
from python_surveyor.checks.optional_params import run
|
|
5
|
-
from python_surveyor.scanner import _build_corpus
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def test_defaulted_positional_param(make_source_file, empty_corpus):
|
|
9
|
-
source = "def f(a=1):\n pass\n"
|
|
10
|
-
source_file = make_source_file("a.py", source)
|
|
11
|
-
findings = run(source_file, empty_corpus)
|
|
12
|
-
assert len(findings) == 1
|
|
13
|
-
assert findings[0].check_id == "optional-param-default"
|
|
14
|
-
notes = "\n".join(findings[0].notes)
|
|
15
|
-
assert "`f`" in notes
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_mixed_defaulted_positional(make_source_file, empty_corpus):
|
|
19
|
-
source = "def f(a, b=2):\n pass\n"
|
|
20
|
-
source_file = make_source_file("a.py", source)
|
|
21
|
-
findings = run(source_file, empty_corpus)
|
|
22
|
-
assert len(findings) == 1
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def test_defaulted_kwonly(make_source_file, empty_corpus):
|
|
26
|
-
source = "def f(*, a=1):\n pass\n"
|
|
27
|
-
source_file = make_source_file("a.py", source)
|
|
28
|
-
findings = run(source_file, empty_corpus)
|
|
29
|
-
assert len(findings) == 1
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def test_no_defaults_not_flagged(make_source_file, empty_corpus):
|
|
33
|
-
source = "def f(a, b):\n pass\n"
|
|
34
|
-
source_file = make_source_file("a.py", source)
|
|
35
|
-
findings = run(source_file, empty_corpus)
|
|
36
|
-
assert findings == []
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def test_args_kwargs_not_flagged(make_source_file, empty_corpus):
|
|
40
|
-
source = "def f(*args, **kwargs):\n pass\n"
|
|
41
|
-
source_file = make_source_file("a.py", source)
|
|
42
|
-
findings = run(source_file, empty_corpus)
|
|
43
|
-
assert findings == []
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
def test_defaulted_param_names_in_notes(make_source_file, empty_corpus):
|
|
103
|
-
source = "def f(a, b=2, *, c=3):\n pass\n"
|
|
104
|
-
source_file = make_source_file("a.py", source)
|
|
105
|
-
findings = run(source_file, empty_corpus)
|
|
106
|
-
assert len(findings) == 1
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_surveyor-0.2.0 → python_surveyor-0.4.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
|