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.
Files changed (33) hide show
  1. {python_surveyor-0.4.1/python_surveyor.egg-info → python_surveyor-0.5.0}/PKG-INFO +1 -1
  2. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/pyproject.toml +1 -1
  3. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/__init__.py +0 -2
  4. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/checks/__init__.py +1 -2
  5. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/checks/fixture_naming.py +1 -1
  6. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/checks/optional_params.py +14 -72
  7. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/cli.py +5 -5
  8. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/model.py +0 -16
  9. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/scanner.py +10 -45
  10. {python_surveyor-0.4.1 → python_surveyor-0.5.0/python_surveyor.egg-info}/PKG-INFO +1 -1
  11. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/tests/test_optional_params.py +0 -60
  12. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/tests/test_scanner.py +10 -60
  13. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/LICENSE +0 -0
  14. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/README.md +0 -0
  15. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/__main__.py +0 -0
  16. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/checks/_util.py +0 -0
  17. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/checks/broad_except.py +0 -0
  18. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/checks/future_annotations.py +0 -0
  19. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/checks/nontoplevel_imports.py +0 -0
  20. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/checks/suppressions.py +0 -0
  21. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor/report.py +0 -0
  22. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor.egg-info/SOURCES.txt +0 -0
  23. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor.egg-info/dependency_links.txt +0 -0
  24. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor.egg-info/entry_points.txt +0 -0
  25. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor.egg-info/requires.txt +0 -0
  26. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/python_surveyor.egg-info/top_level.txt +0 -0
  27. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/setup.cfg +0 -0
  28. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/tests/test_broad_except.py +0 -0
  29. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/tests/test_cli.py +0 -0
  30. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/tests/test_fixture_naming.py +0 -0
  31. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/tests/test_future_annotations.py +0 -0
  32. {python_surveyor-0.4.1 → python_surveyor-0.5.0}/tests/test_nontoplevel_imports.py +0 -0
  33. {python_surveyor-0.4.1 → python_surveyor-0.5.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.4.1
3
+ Version: 0.5.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
@@ -8,7 +8,7 @@ include = ["python_surveyor*"]
8
8
 
9
9
  [project]
10
10
  name = "python-surveyor"
11
- version = "0.4.1"
11
+ version = "0.5.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"
@@ -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 — the 'not called from "
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.max_call_sites
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 notes report the defaulted parameter names plus call sites that rely on
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
- "missing call sites" signal this check looks for doesn't apply. A handler is
16
- recognised only when its decorator (e.g. ``@app.get(...)``, ``@router.post(
17
- ...)``, ``@app.route(...)``) is a call on a name that is statically traced,
18
- via imports in the same file, to an actual ``fastapi.FastAPI`` /
19
- ``fastapi.APIRouter`` / ``flask.Flask`` / ``flask.Blueprint`` instance. Only
20
- direct ``name = Ctor()`` assignments are tracked — factory functions that
21
- build and return an app/router are not (this matches the heuristic, not
22
- exhaustive, philosophy of the other checks).
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 CallSite, Finding, SourceExcerpt
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", corpus: "Corpus") -> list[Finding]:
274
- """Find functions with defaulted params and attach signature + call-site context."""
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-call-sites",
49
- "max_call_sites",
48
+ "--max-samples",
49
+ "max_samples",
50
50
  type=int,
51
51
  default=10,
52
- help="Cap call-site samples in notes (default 10).",
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
- max_call_sites: int,
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
- max_call_sites=max_call_sites,
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
- call/use this identifier" without re-parsing.
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 CallSite, Finding, Location, ParseError
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
- ``max_call_sites`` is the cap the ``optional-param-default`` check applies
73
- when sampling call sites into a note (set by the scanner from the CLI
74
- flag so checks don't need optional parameters of their own).
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
- max_call_sites: int
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
- max_call_sites: int,
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
- max_call_sites=max_call_sites,
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
- max_call_sites: int,
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, max_call_sites, COVERED_PYLINT_IDS)
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-surveyor
3
- Version: 0.4.1
3
+ Version: 0.5.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
@@ -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
- max_call_sites=5,
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
- max_call_sites=5,
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
- max_call_sites=5,
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
- max_call_sites=5,
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
- max_call_sites=5,
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
- max_call_sites=5,
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
- max_call_sites=5,
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
- max_call_sites=5,
145
+ max_samples=5,
196
146
  covered_pylint_ids=COVERED_PYLINT_IDS,
197
147
  )
198
148
  with pytest.raises(dataclasses.FrozenInstanceError):
199
- corpus.max_call_sites = 10 # type: ignore[misc]
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
- max_call_sites=5,
158
+ max_samples=5,
209
159
  )
210
160
  assert isinstance(result, ScanResult)
211
161
  assert result.files_scanned == 1
File without changes