skim-python 0.0.2__py3-none-any.whl → 0.0.3__py3-none-any.whl

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.
skim_py/cli.py CHANGED
@@ -68,6 +68,11 @@ Agent Operational Workflow:
68
68
 
69
69
 
70
70
  def main(args: Optional[List[str]] = None) -> int:
71
+ if sys.version_info < (3, 10):
72
+ current_ver = f"{sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]}"
73
+ sys.stderr.write(f"Error: skim-py requires Python 3.10+ (running on Python {current_ver}).\n")
74
+ return 1
75
+
71
76
  if args is None:
72
77
  args = sys.argv[1:]
73
78
 
skim_py/parser.py CHANGED
@@ -2,10 +2,9 @@
2
2
 
3
3
  from __future__ import annotations
4
4
  import ast
5
- import os
6
5
  import re
7
6
  from pathlib import Path
8
- from typing import Optional, List, Tuple
7
+ from typing import Optional, List
9
8
 
10
9
  from .types import OutlineItem, ParsedFile, SyntaxNotice
11
10
 
@@ -24,7 +23,11 @@ def get_indent(line: str) -> int:
24
23
 
25
24
 
26
25
  def get_block_end(lines: List[str], start: int) -> int:
27
- """Find the last indented code line, ignoring blank lines and comments."""
26
+ """Find the last indented code line, ignoring blank lines and comments.
27
+
28
+ Known limit: a multi-line string with content at column 0, or a closing
29
+ ``):`` at the header's indent, ends the block early.
30
+ """
28
31
  indent = get_indent(lines[start])
29
32
  end = start
30
33
  for idx in range(start + 1, len(lines)):
@@ -37,6 +40,49 @@ def get_block_end(lines: List[str], start: int) -> int:
37
40
  return end
38
41
 
39
42
 
43
+ def collect_decorators(lines: List[str], header_idx: int, floor: int):
44
+ """Scan upward from a header for decorators, including multi-line ones.
45
+
46
+ Returns (start_idx or None, decorator strings in source order). Multi-line
47
+ decorators are found by bracket balancing; brackets inside strings are not
48
+ handled specially, so an unbalanced one can hide the decorator.
49
+ """
50
+ start: Optional[int] = None
51
+ decos: List[str] = []
52
+ idx = header_idx - 1
53
+ while idx >= floor:
54
+ s = lines[idx].strip()
55
+ if not s or s.startswith("#"):
56
+ idx -= 1
57
+ continue
58
+ if s.startswith("@"):
59
+ decos.insert(0, s)
60
+ start = idx
61
+ idx -= 1
62
+ continue
63
+ if s[-1] in ")]}":
64
+ depth = 0
65
+ found = None
66
+ for j in range(idx, floor - 1, -1):
67
+ t = lines[j].strip()
68
+ depth += sum(t.count(c) for c in ")]}") - sum(t.count(c) for c in "([{")
69
+ if depth <= 0:
70
+ if t.startswith("@"):
71
+ found = j
72
+ break
73
+ if found is not None:
74
+ parts = [
75
+ l.strip() for l in lines[found : idx + 1]
76
+ if l.strip() and not l.strip().startswith("#")
77
+ ]
78
+ decos.insert(0, " ".join(parts))
79
+ start = found
80
+ idx = found - 1
81
+ continue
82
+ break
83
+ return start, decos
84
+
85
+
40
86
  def split_source_lines(content: str, keepends: bool = False) -> List[str]:
41
87
  """Normalize newlines (\r\n, \r -> \n) and split into lines."""
42
88
  norm = content.replace("\r\n", "\n").replace("\r", "\n")
@@ -142,6 +188,7 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
142
188
 
143
189
  # Fallback: Iterative line masking with up to 100 attempts
144
190
  working_lines = split_source_lines(content, keepends=True)
191
+ original_len = len(working_lines)
145
192
  seen_states = set()
146
193
  max_attempts = 100
147
194
  current_err: Optional[SyntaxError] = first_err
@@ -156,7 +203,9 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
156
203
  line_idx = lineno - 1
157
204
  line = working_lines[line_idx]
158
205
  indent = get_indent(line)
159
- leading_ws = line[: len(line) - len(line.lstrip())]
206
+ leading_ws = line[: len(line) - len(line.lstrip(" \t"))]
207
+ if not line.strip():
208
+ leading_ws = ""
160
209
 
161
210
  state_key = (line_idx, line)
162
211
  if state_key in seen_states:
@@ -173,7 +222,10 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
173
222
 
174
223
  if end_idx > line_idx or m_hdr:
175
224
  # Update notice message with masked line range if not already suffixed
176
- mask_suffix = f"(L{line_idx + 1}-L{end_idx + 1} masked)"
225
+ if end_idx > line_idx:
226
+ mask_suffix = f"(L{line_idx + 1}-L{end_idx + 1} masked)"
227
+ else:
228
+ mask_suffix = f"(L{line_idx + 1} masked)"
177
229
  for n in parsed.syntax_errors:
178
230
  if n.line == lineno and "masked)" not in n.message:
179
231
  n.message = f"{n.message} {mask_suffix}"
@@ -185,17 +237,9 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
185
237
  end_l = end_idx + 1
186
238
 
187
239
  # Scan upward for leading decorators
188
- decos = []
189
- for d_idx in range(line_idx - 1, -1, -1):
190
- prev_d = working_lines[d_idx]
191
- stripped_d = prev_d.strip()
192
- if stripped_d.startswith("@"):
193
- start_l = d_idx + 1
194
- decos.insert(0, stripped_d)
195
- elif not stripped_d or stripped_d.startswith("#"):
196
- continue
197
- else:
198
- break
240
+ deco_start, decos = collect_decorators(working_lines, line_idx, 0)
241
+ if deco_start is not None:
242
+ start_l = deco_start + 1
199
243
 
200
244
  deco_prefix = (" ".join(decos) + " ") if decos else ""
201
245
 
@@ -216,17 +260,11 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
216
260
  ch_k_raw, ch_name = m_ch.group(1), m_ch.group(2)
217
261
  ch_start = k + 1
218
262
  # Scan upward for method decorators inside the class
219
- ch_decos = []
220
- for cj in range(k - 1, line_idx, -1):
221
- prev_cj = working_lines[cj]
222
- str_cj = prev_cj.strip()
223
- if str_cj.startswith("@"):
224
- ch_start = cj + 1
225
- ch_decos.insert(0, str_cj)
226
- elif not str_cj or str_cj.startswith("#"):
227
- continue
228
- else:
229
- break
263
+ ch_deco_start, ch_decos = collect_decorators(
264
+ working_lines, k, line_idx + 1
265
+ )
266
+ if ch_deco_start is not None:
267
+ ch_start = ch_deco_start + 1
230
268
 
231
269
  ch_deco_prefix = (" ".join(ch_decos) + " ") if ch_decos else ""
232
270
 
@@ -287,15 +325,24 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
287
325
  working_lines[line_idx] = leading_ws + stub
288
326
  stub_lines.add(lineno)
289
327
  else:
290
- working_lines[line_idx] = leading_ws + "pass\n"
328
+ working_lines[line_idx] = leading_ws + "# syntax error masked\n"
291
329
  for k in range(line_idx + 1, end_idx + 1):
292
330
  working_lines[k] = "# syntax error masked\n"
331
+ assert len(working_lines) == original_len
293
332
 
294
333
  masked_lines.update(range(line_idx + 1, end_idx + 2))
295
334
  masked_block_ends[lineno] = end_idx + 1
296
335
 
297
336
  try:
298
- tree = ast.parse("".join(working_lines), filename=file_path, type_comments=True)
337
+ try:
338
+ tree = ast.parse("".join(working_lines), filename=file_path, type_comments=True)
339
+ except IndentationError as ind_err:
340
+ # Masking the only statement of a suite left it empty; use pass instead.
341
+ if m_hdr or "expected an indented block" not in (ind_err.msg or ""):
342
+ raise
343
+ working_lines[line_idx] = leading_ws + "pass\n"
344
+ assert len(working_lines) == original_len
345
+ tree = ast.parse("".join(working_lines), filename=file_path, type_comments=True)
299
346
  break # Successful recovery!
300
347
  except SyntaxError as next_err:
301
348
  current_err = next_err
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: skim-python
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: Language-aware, AST-resilient outline and verbatim code extractor for Python tailored for AI agents
5
5
  Project-URL: Homepage, https://github.com/benedictjohannes/programming-helper-utils
6
6
  Project-URL: Repository, https://github.com/benedictjohannes/programming-helper-utils
@@ -0,0 +1,11 @@
1
+ skim_py/__init__.py,sha256=GCEQwh8hAZr6Vd8gbG-xpnqnulWXEAXhG-nA7sWtmoA,71
2
+ skim_py/block.py,sha256=1up79BSBdFxPspp9CFcJNRGMiQmAz_frCbwp8LrqvG4,4961
3
+ skim_py/cli.py,sha256=OmyCCL716NvFzVRyqHQo3b3xOn-1mjcJixHqrcEKJL0,5224
4
+ skim_py/glance.py,sha256=zMP2_VJMfdF2ZTOWBRTuC-zcF6vNc7i5R5nHO63da0c,6931
5
+ skim_py/parser.py,sha256=KNIuEEDRn47iC-1csRrVauFcH4llX8Xs8_vjQPt2aFE,23538
6
+ skim_py/types.py,sha256=k_e9kEx-jhGtJIrh9eNqe8R87JlYvMFQvGGlbzwpXm8,1478
7
+ skim_python-0.0.3.dist-info/METADATA,sha256=S-ziYNSMtZcvLvtkfmnK4Z86G6pGzRCzLVbxjePvUYM,22791
8
+ skim_python-0.0.3.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
9
+ skim_python-0.0.3.dist-info/entry_points.txt,sha256=nwzHJF7CgNobxT923pwxljnEuDdJe9a2k81Q91bPY8U,45
10
+ skim_python-0.0.3.dist-info/licenses/LICENSE,sha256=at6MgKqAJV-KpSzR2j1hUc2hTZfymTMkbvDwmypUA9I,1074
11
+ skim_python-0.0.3.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- skim_py/__init__.py,sha256=GCEQwh8hAZr6Vd8gbG-xpnqnulWXEAXhG-nA7sWtmoA,71
2
- skim_py/block.py,sha256=1up79BSBdFxPspp9CFcJNRGMiQmAz_frCbwp8LrqvG4,4961
3
- skim_py/cli.py,sha256=gcPPkHI9GCFjmhvWyGc4srC3xeA0I14e8MgAQYY643g,4977
4
- skim_py/glance.py,sha256=zMP2_VJMfdF2ZTOWBRTuC-zcF6vNc7i5R5nHO63da0c,6931
5
- skim_py/parser.py,sha256=3-at3xgB5y4ICSekduAjUQpFmmAkO_tmjEZzdVXwlCY,21784
6
- skim_py/types.py,sha256=k_e9kEx-jhGtJIrh9eNqe8R87JlYvMFQvGGlbzwpXm8,1478
7
- skim_python-0.0.2.dist-info/METADATA,sha256=3SljznEc0UcoT21BUxfiR5-pi-XXZtw69wlBZ9MwSGY,22791
8
- skim_python-0.0.2.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
9
- skim_python-0.0.2.dist-info/entry_points.txt,sha256=nwzHJF7CgNobxT923pwxljnEuDdJe9a2k81Q91bPY8U,45
10
- skim_python-0.0.2.dist-info/licenses/LICENSE,sha256=at6MgKqAJV-KpSzR2j1hUc2hTZfymTMkbvDwmypUA9I,1074
11
- skim_python-0.0.2.dist-info/RECORD,,