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.
Files changed (26) hide show
  1. {python_surveyor-0.2.0/python_surveyor.egg-info → python_surveyor-0.4.0}/PKG-INFO +3 -1
  2. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/pyproject.toml +3 -1
  3. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor/model.py +2 -7
  4. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor/report.py +16 -12
  5. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor/scanner.py +5 -0
  6. {python_surveyor-0.2.0 → python_surveyor-0.4.0/python_surveyor.egg-info}/PKG-INFO +3 -1
  7. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor.egg-info/requires.txt +2 -0
  8. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/tests/test_broad_except.py +41 -0
  9. python_surveyor-0.4.0/tests/test_optional_params.py +289 -0
  10. python_surveyor-0.2.0/tests/test_optional_params.py +0 -131
  11. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/LICENSE +0 -0
  12. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/README.md +0 -0
  13. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor/__init__.py +0 -0
  14. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor/__main__.py +0 -0
  15. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor/cli.py +0 -0
  16. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor.egg-info/SOURCES.txt +0 -0
  17. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor.egg-info/dependency_links.txt +0 -0
  18. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor.egg-info/entry_points.txt +0 -0
  19. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/python_surveyor.egg-info/top_level.txt +0 -0
  20. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/setup.cfg +0 -0
  21. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/tests/test_cli.py +0 -0
  22. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/tests/test_fixture_naming.py +0 -0
  23. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/tests/test_future_annotations.py +0 -0
  24. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/tests/test_nontoplevel_imports.py +0 -0
  25. {python_surveyor-0.2.0 → python_surveyor-0.4.0}/tests/test_scanner.py +0 -0
  26. {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.2.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.2.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
- path: Path, start: int, end: int, max_lines: int
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 ``path``, capping to ``max_lines``.
22
+ """Read ``[start, end]`` from ``source_lines``, capping to ``max_lines``.
20
23
 
21
24
  Returns ``(lines, truncation_note)``.
22
25
  """
23
- try:
24
- all_lines = path.read_text(encoding="utf-8").splitlines()
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
- prev_path: Path | None = None
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
- excerpt.path,
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
- prev_path == excerpt.path
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
- prev_path = excerpt.path
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.2.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"
@@ -1,6 +1,8 @@
1
1
  click>=8.1.0
2
2
 
3
3
  [dev]
4
+ fastapi>=0.115.0
5
+ flask>=3.0.0
4
6
  pyright==1.1.411
5
7
  pytest>=8.0.0
6
8
  pytest-cov
@@ -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