python-surveyor 0.2.0__tar.gz → 0.3.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 (25) hide show
  1. {python_surveyor-0.2.0/python_surveyor.egg-info → python_surveyor-0.3.0}/PKG-INFO +1 -1
  2. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/pyproject.toml +1 -1
  3. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/python_surveyor/model.py +2 -7
  4. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/python_surveyor/report.py +16 -12
  5. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/python_surveyor/scanner.py +5 -0
  6. {python_surveyor-0.2.0 → python_surveyor-0.3.0/python_surveyor.egg-info}/PKG-INFO +1 -1
  7. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/tests/test_broad_except.py +41 -0
  8. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/tests/test_optional_params.py +15 -4
  9. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/LICENSE +0 -0
  10. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/README.md +0 -0
  11. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/python_surveyor/__init__.py +0 -0
  12. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/python_surveyor/__main__.py +0 -0
  13. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/python_surveyor/cli.py +0 -0
  14. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/python_surveyor.egg-info/SOURCES.txt +0 -0
  15. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/python_surveyor.egg-info/dependency_links.txt +0 -0
  16. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/python_surveyor.egg-info/entry_points.txt +0 -0
  17. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/python_surveyor.egg-info/requires.txt +0 -0
  18. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/python_surveyor.egg-info/top_level.txt +0 -0
  19. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/setup.cfg +0 -0
  20. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/tests/test_cli.py +0 -0
  21. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/tests/test_fixture_naming.py +0 -0
  22. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/tests/test_future_annotations.py +0 -0
  23. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/tests/test_nontoplevel_imports.py +0 -0
  24. {python_surveyor-0.2.0 → python_surveyor-0.3.0}/tests/test_scanner.py +0 -0
  25. {python_surveyor-0.2.0 → python_surveyor-0.3.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.3.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.2.0"
11
+ version = "0.3.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,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.3.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
@@ -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"
@@ -122,10 +122,21 @@ def test_signature_excerpt_not_full_body(make_source_file, empty_corpus):
122
122
  assert excerpt.end_line == 1
123
123
 
124
124
 
125
- def test_no_api_note(make_source_file, empty_corpus):
126
- source = "def _helper(a=1):\n pass\n"
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
127
  source_file = make_source_file("a.py", source)
128
128
  findings = run(source_file, empty_corpus)
129
129
  assert len(findings) == 1
130
- notes = "\n".join(findings[0].notes)
131
- assert "API" not in notes
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
File without changes