skim-python 0.0.1__py3-none-any.whl → 0.0.2__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/block.py +4 -2
- skim_py/glance.py +4 -2
- skim_py/parser.py +246 -12
- skim_py/types.py +10 -0
- {skim_python-0.0.1.dist-info → skim_python-0.0.2.dist-info}/METADATA +33 -2
- skim_python-0.0.2.dist-info/RECORD +11 -0
- skim_python-0.0.1.dist-info/RECORD +0 -11
- {skim_python-0.0.1.dist-info → skim_python-0.0.2.dist-info}/WHEEL +0 -0
- {skim_python-0.0.1.dist-info → skim_python-0.0.2.dist-info}/entry_points.txt +0 -0
- {skim_python-0.0.1.dist-info → skim_python-0.0.2.dist-info}/licenses/LICENSE +0 -0
skim_py/block.py
CHANGED
|
@@ -7,7 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
from typing import List, Optional
|
|
8
8
|
|
|
9
9
|
from .types import OutlineItem, ParsedFile
|
|
10
|
-
from .parser import parse_source_file
|
|
10
|
+
from .parser import parse_source_file, split_source_lines
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def collect_all_items(items: List[OutlineItem]) -> List[OutlineItem]:
|
|
@@ -38,6 +38,8 @@ def run_block(
|
|
|
38
38
|
return 1
|
|
39
39
|
|
|
40
40
|
parsed = parse_source_file(file_path, content, include_tests=include_tests)
|
|
41
|
+
if parsed.masking_warning:
|
|
42
|
+
sys.stderr.write(f"{file_path}: {parsed.masking_warning}\n")
|
|
41
43
|
all_items = collect_all_items(parsed.items)
|
|
42
44
|
|
|
43
45
|
matches: List[OutlineItem] = []
|
|
@@ -123,7 +125,7 @@ def run_block(
|
|
|
123
125
|
return 3
|
|
124
126
|
|
|
125
127
|
chosen = matches[0]
|
|
126
|
-
src_lines = content
|
|
128
|
+
src_lines = split_source_lines(content, keepends=False)
|
|
127
129
|
block_lines = src_lines[chosen.start_line - 1 : chosen.end_line]
|
|
128
130
|
|
|
129
131
|
# Check for syntax errors within block range
|
skim_py/glance.py
CHANGED
|
@@ -8,7 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
from typing import List, Optional
|
|
9
9
|
|
|
10
10
|
from .types import OutlineItem, ParsedFile
|
|
11
|
-
from .parser import parse_source_file, is_test_file
|
|
11
|
+
from .parser import parse_source_file, is_test_file, split_source_lines
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
IGNORED_DIRS = {
|
|
@@ -121,7 +121,7 @@ def run_glance(
|
|
|
121
121
|
sys.stderr.write(f"Error reading {file_path}: {err}\n")
|
|
122
122
|
continue
|
|
123
123
|
|
|
124
|
-
src_lines = content
|
|
124
|
+
src_lines = split_source_lines(content, keepends=False)
|
|
125
125
|
parsed = parse_source_file(file_path, content, include_tests=include_tests)
|
|
126
126
|
|
|
127
127
|
if filters:
|
|
@@ -155,6 +155,8 @@ def run_glance(
|
|
|
155
155
|
rel_path = file_path
|
|
156
156
|
|
|
157
157
|
print(f"=== {rel_path} ({parsed.total_lines} lines){test_info}{error_tag} ===")
|
|
158
|
+
if parsed.masking_warning:
|
|
159
|
+
print(parsed.masking_warning)
|
|
158
160
|
|
|
159
161
|
for err in parsed.syntax_errors:
|
|
160
162
|
print(f" Syntax Notice: L{err.line}: {err.message}")
|
skim_py/parser.py
CHANGED
|
@@ -14,6 +14,43 @@ TEST_DIR_NAMES = {"tests", "test", "testing", "__tests__"}
|
|
|
14
14
|
TEST_FILE_PATTERN = re.compile(r"^(test_.*|.*_test|conftest)\.py$", re.IGNORECASE)
|
|
15
15
|
TEST_CLASS_PATTERN = re.compile(r"^Test[A-Z0-9_].*")
|
|
16
16
|
TEST_FUNC_PATTERN = re.compile(r"^test(_|[A-Z0-9_]).*")
|
|
17
|
+
HEADER_DEF_PATTERN = re.compile(r"^\s*(async\s+def|def|class)\s+([a-zA-Z_][a-zA-Z0-9_]*)")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_indent(line: str) -> int:
|
|
21
|
+
"""Return indentation level expanding tabs to 8 spaces (CPython rule)."""
|
|
22
|
+
expanded = line.expandtabs(8)
|
|
23
|
+
return len(expanded) - len(expanded.lstrip(" "))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_block_end(lines: List[str], start: int) -> int:
|
|
27
|
+
"""Find the last indented code line, ignoring blank lines and comments."""
|
|
28
|
+
indent = get_indent(lines[start])
|
|
29
|
+
end = start
|
|
30
|
+
for idx in range(start + 1, len(lines)):
|
|
31
|
+
stripped = lines[idx].strip()
|
|
32
|
+
if not stripped or stripped.startswith("#"):
|
|
33
|
+
continue
|
|
34
|
+
if get_indent(lines[idx]) <= indent:
|
|
35
|
+
break
|
|
36
|
+
end = idx
|
|
37
|
+
return end
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def split_source_lines(content: str, keepends: bool = False) -> List[str]:
|
|
41
|
+
"""Normalize newlines (\r\n, \r -> \n) and split into lines."""
|
|
42
|
+
norm = content.replace("\r\n", "\n").replace("\r", "\n")
|
|
43
|
+
if not norm:
|
|
44
|
+
return []
|
|
45
|
+
raw = norm.split("\n")
|
|
46
|
+
if norm.endswith("\n"):
|
|
47
|
+
raw = raw[:-1]
|
|
48
|
+
if keepends:
|
|
49
|
+
return [l + "\n" for l in raw]
|
|
50
|
+
return raw
|
|
51
|
+
if keepends:
|
|
52
|
+
return [l + "\n" for l in raw[:-1]] + [raw[-1]]
|
|
53
|
+
return raw
|
|
17
54
|
|
|
18
55
|
|
|
19
56
|
def is_test_file(file_path: str) -> bool:
|
|
@@ -79,7 +116,7 @@ def is_test_func(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
|
79
116
|
|
|
80
117
|
|
|
81
118
|
def parse_source_file(file_path: str, content: str, include_tests: bool = False) -> ParsedFile:
|
|
82
|
-
src_lines = content
|
|
119
|
+
src_lines = split_source_lines(content, keepends=False)
|
|
83
120
|
total_lines = len(src_lines)
|
|
84
121
|
is_test = is_test_file(file_path)
|
|
85
122
|
|
|
@@ -90,14 +127,21 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
|
|
|
90
127
|
)
|
|
91
128
|
|
|
92
129
|
tree: Optional[ast.AST] = None
|
|
130
|
+
salvaged_items: List[OutlineItem] = []
|
|
131
|
+
masked_block_ends = {}
|
|
132
|
+
stub_lines = set()
|
|
133
|
+
masked_lines = set()
|
|
134
|
+
|
|
93
135
|
try:
|
|
94
136
|
tree = ast.parse(content, filename=file_path, type_comments=True)
|
|
95
137
|
except SyntaxError as first_err:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
)
|
|
138
|
+
first_line = first_err.lineno or 1
|
|
139
|
+
first_msg = first_err.msg or "syntax error"
|
|
140
|
+
notice = SyntaxNotice(line=first_line, message=first_msg)
|
|
141
|
+
parsed.syntax_errors.append(notice)
|
|
142
|
+
|
|
99
143
|
# Fallback: Iterative line masking with up to 100 attempts
|
|
100
|
-
working_lines = content
|
|
144
|
+
working_lines = split_source_lines(content, keepends=True)
|
|
101
145
|
seen_states = set()
|
|
102
146
|
max_attempts = 100
|
|
103
147
|
current_err: Optional[SyntaxError] = first_err
|
|
@@ -111,7 +155,8 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
|
|
|
111
155
|
|
|
112
156
|
line_idx = lineno - 1
|
|
113
157
|
line = working_lines[line_idx]
|
|
114
|
-
indent =
|
|
158
|
+
indent = get_indent(line)
|
|
159
|
+
leading_ws = line[: len(line) - len(line.lstrip())]
|
|
115
160
|
|
|
116
161
|
state_key = (line_idx, line)
|
|
117
162
|
if state_key in seen_states:
|
|
@@ -119,10 +164,135 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
|
|
|
119
164
|
break
|
|
120
165
|
seen_states.add(state_key)
|
|
121
166
|
|
|
122
|
-
if
|
|
123
|
-
|
|
167
|
+
# Check if this line is a block header followed by an indented block.
|
|
168
|
+
# E.g. broken def/class/if header whose masking would cause IndentationError cascades.
|
|
169
|
+
end_idx = get_block_end(working_lines, line_idx)
|
|
170
|
+
|
|
171
|
+
# Check if header matches def/async def/class
|
|
172
|
+
m_hdr = HEADER_DEF_PATTERN.match(line)
|
|
173
|
+
|
|
174
|
+
if end_idx > line_idx or m_hdr:
|
|
175
|
+
# Update notice message with masked line range if not already suffixed
|
|
176
|
+
mask_suffix = f"(L{line_idx + 1}-L{end_idx + 1} masked)"
|
|
177
|
+
for n in parsed.syntax_errors:
|
|
178
|
+
if n.line == lineno and "masked)" not in n.message:
|
|
179
|
+
n.message = f"{n.message} {mask_suffix}"
|
|
180
|
+
|
|
181
|
+
# Salvage header definition if it matches def/async def/class
|
|
182
|
+
if m_hdr:
|
|
183
|
+
k_raw, name = m_hdr.group(1), m_hdr.group(2)
|
|
184
|
+
start_l = line_idx + 1
|
|
185
|
+
end_l = end_idx + 1
|
|
186
|
+
|
|
187
|
+
# 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
|
|
199
|
+
|
|
200
|
+
deco_prefix = (" ".join(decos) + " ") if decos else ""
|
|
201
|
+
|
|
202
|
+
if k_raw == "class":
|
|
203
|
+
cls_is_test = bool(TEST_CLASS_PATTERN.match(name))
|
|
204
|
+
class_item = OutlineItem(
|
|
205
|
+
name=name,
|
|
206
|
+
kind="test" if cls_is_test else "class",
|
|
207
|
+
start_line=start_l,
|
|
208
|
+
end_line=end_l,
|
|
209
|
+
raw_header=f"{deco_prefix}class {name} [syntax error]",
|
|
210
|
+
)
|
|
211
|
+
# Extract child methods inside the unparsable class
|
|
212
|
+
k = line_idx + 1
|
|
213
|
+
while k <= end_idx:
|
|
214
|
+
m_ch = HEADER_DEF_PATTERN.match(working_lines[k])
|
|
215
|
+
if m_ch and m_ch.group(1) in ("def", "async def"):
|
|
216
|
+
ch_k_raw, ch_name = m_ch.group(1), m_ch.group(2)
|
|
217
|
+
ch_start = k + 1
|
|
218
|
+
# 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
|
|
230
|
+
|
|
231
|
+
ch_deco_prefix = (" ".join(ch_decos) + " ") if ch_decos else ""
|
|
232
|
+
|
|
233
|
+
ch_end_idx = min(get_block_end(working_lines, k), end_idx)
|
|
234
|
+
ch_end = ch_end_idx + 1
|
|
235
|
+
ch_is_test = bool(TEST_FUNC_PATTERN.match(ch_name))
|
|
236
|
+
ch_kind = "test" if ch_is_test else "method"
|
|
237
|
+
class_item.children.append(
|
|
238
|
+
OutlineItem(
|
|
239
|
+
name=ch_name,
|
|
240
|
+
kind=ch_kind,
|
|
241
|
+
start_line=ch_start,
|
|
242
|
+
end_line=ch_end,
|
|
243
|
+
raw_header=f"{ch_deco_prefix}{ch_k_raw} {ch_name}()",
|
|
244
|
+
parent_name=name,
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
k = ch_end_idx + 1
|
|
248
|
+
else:
|
|
249
|
+
k += 1
|
|
250
|
+
|
|
251
|
+
salvaged_items.append(class_item)
|
|
252
|
+
else:
|
|
253
|
+
fn_is_test = bool(TEST_FUNC_PATTERN.match(name))
|
|
254
|
+
fn_kind = "test" if fn_is_test else ("async def" if "async" in k_raw else "function")
|
|
255
|
+
# Check if method inside an outer class, skipping comments and blank lines
|
|
256
|
+
parent_cls = None
|
|
257
|
+
for p_idx in range(line_idx - 1, -1, -1):
|
|
258
|
+
prev_l = working_lines[p_idx]
|
|
259
|
+
stripped_p = prev_l.strip()
|
|
260
|
+
if not stripped_p or stripped_p.startswith("#"):
|
|
261
|
+
continue
|
|
262
|
+
if get_indent(prev_l) < indent:
|
|
263
|
+
m_p = re.match(r"^\s*class\s+([a-zA-Z_][a-zA-Z0-9_]*)", prev_l)
|
|
264
|
+
if m_p:
|
|
265
|
+
parent_cls = m_p.group(1)
|
|
266
|
+
break
|
|
267
|
+
|
|
268
|
+
salvaged_items.append(
|
|
269
|
+
OutlineItem(
|
|
270
|
+
name=name,
|
|
271
|
+
kind="test" if fn_is_test else ("method" if parent_cls else fn_kind),
|
|
272
|
+
start_line=start_l,
|
|
273
|
+
end_line=end_l,
|
|
274
|
+
raw_header=f"{deco_prefix}{k_raw} {name} [syntax error]",
|
|
275
|
+
parent_name=parent_cls,
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Keep decorators attached to a harmless definition stub. Otherwise
|
|
280
|
+
# use pass so masking the only statement does not empty its suite.
|
|
281
|
+
if m_hdr:
|
|
282
|
+
stub = (
|
|
283
|
+
"class __skim_masked__: pass\n"
|
|
284
|
+
if m_hdr.group(1) == "class"
|
|
285
|
+
else "def __skim_masked__(): pass\n"
|
|
286
|
+
)
|
|
287
|
+
working_lines[line_idx] = leading_ws + stub
|
|
288
|
+
stub_lines.add(lineno)
|
|
124
289
|
else:
|
|
125
|
-
working_lines[line_idx] =
|
|
290
|
+
working_lines[line_idx] = leading_ws + "pass\n"
|
|
291
|
+
for k in range(line_idx + 1, end_idx + 1):
|
|
292
|
+
working_lines[k] = "# syntax error masked\n"
|
|
293
|
+
|
|
294
|
+
masked_lines.update(range(line_idx + 1, end_idx + 2))
|
|
295
|
+
masked_block_ends[lineno] = end_idx + 1
|
|
126
296
|
|
|
127
297
|
try:
|
|
128
298
|
tree = ast.parse("".join(working_lines), filename=file_path, type_comments=True)
|
|
@@ -142,7 +312,63 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
|
|
|
142
312
|
)
|
|
143
313
|
return parsed
|
|
144
314
|
|
|
315
|
+
parsed.masked_lines = sorted(masked_lines)
|
|
316
|
+
|
|
317
|
+
def recovered_end(node: ast.AST) -> int:
|
|
318
|
+
end = get_end_line(node)
|
|
319
|
+
return max([end] + [
|
|
320
|
+
masked_end for start, masked_end in masked_block_ends.items()
|
|
321
|
+
if node.lineno <= start <= end
|
|
322
|
+
])
|
|
323
|
+
|
|
324
|
+
def merge_salvaged() -> None:
|
|
325
|
+
if not salvaged_items:
|
|
326
|
+
return
|
|
327
|
+
for s_item in salvaged_items:
|
|
328
|
+
# The AST still contains excluded test classes, but parsed.items
|
|
329
|
+
# does not. Do not promote their recovered methods to top level.
|
|
330
|
+
if not include_tests and any(
|
|
331
|
+
start <= s_item.start_line <= end
|
|
332
|
+
for start, end in parsed.test_ranges
|
|
333
|
+
):
|
|
334
|
+
continue
|
|
335
|
+
# Check test filtering for salvaged items
|
|
336
|
+
if s_item.kind == "test":
|
|
337
|
+
parsed.test_ranges.append((s_item.start_line, s_item.end_line))
|
|
338
|
+
if not include_tests:
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
if s_item.parent_name:
|
|
342
|
+
parent = next(
|
|
343
|
+
(it for it in parsed.items
|
|
344
|
+
if it.name == s_item.parent_name and it.kind in ("class", "test")
|
|
345
|
+
and it.start_line <= s_item.start_line <= it.end_line),
|
|
346
|
+
None,
|
|
347
|
+
)
|
|
348
|
+
if parent:
|
|
349
|
+
parent.children.append(s_item)
|
|
350
|
+
parent.children.sort(key=lambda ch: ch.start_line)
|
|
351
|
+
parent.end_line = max(parent.end_line, s_item.end_line)
|
|
352
|
+
else:
|
|
353
|
+
parsed.items.append(s_item)
|
|
354
|
+
else:
|
|
355
|
+
# If s_item is a class, filter its child test methods if include_tests is False
|
|
356
|
+
if s_item.kind in ("class", "test"):
|
|
357
|
+
filtered_children = []
|
|
358
|
+
for ch in s_item.children:
|
|
359
|
+
if ch.kind == "test":
|
|
360
|
+
parsed.test_ranges.append((ch.start_line, ch.end_line))
|
|
361
|
+
if not include_tests:
|
|
362
|
+
continue
|
|
363
|
+
filtered_children.append(ch)
|
|
364
|
+
s_item.children = filtered_children
|
|
365
|
+
|
|
366
|
+
parsed.items.append(s_item)
|
|
367
|
+
|
|
368
|
+
parsed.items.sort(key=lambda it: it.start_line)
|
|
369
|
+
|
|
145
370
|
if tree is None:
|
|
371
|
+
merge_salvaged()
|
|
146
372
|
return parsed
|
|
147
373
|
|
|
148
374
|
# 1. Collect imports
|
|
@@ -155,11 +381,15 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
|
|
|
155
381
|
|
|
156
382
|
# 2. Extract outline items
|
|
157
383
|
for node in tree.body:
|
|
384
|
+
# Skip internal mask stubs
|
|
385
|
+
if node.lineno in stub_lines:
|
|
386
|
+
continue
|
|
387
|
+
|
|
158
388
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
159
389
|
is_async = isinstance(node, ast.AsyncFunctionDef)
|
|
160
390
|
is_test_fn = is_test_func(node)
|
|
161
391
|
start_l = get_start_line(node)
|
|
162
|
-
end_l =
|
|
392
|
+
end_l = recovered_end(node)
|
|
163
393
|
|
|
164
394
|
if is_test_fn:
|
|
165
395
|
parsed.test_ranges.append((start_l, end_l))
|
|
@@ -186,7 +416,7 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
|
|
|
186
416
|
elif isinstance(node, ast.ClassDef):
|
|
187
417
|
is_test_cls = is_test_class(node)
|
|
188
418
|
start_l = get_start_line(node)
|
|
189
|
-
end_l =
|
|
419
|
+
end_l = recovered_end(node)
|
|
190
420
|
|
|
191
421
|
if is_test_cls:
|
|
192
422
|
parsed.test_ranges.append((start_l, end_l))
|
|
@@ -208,11 +438,14 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
|
|
|
208
438
|
|
|
209
439
|
# Process class body
|
|
210
440
|
for child in node.body:
|
|
441
|
+
if child.lineno in stub_lines:
|
|
442
|
+
continue
|
|
443
|
+
|
|
211
444
|
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
212
445
|
child_async = isinstance(child, ast.AsyncFunctionDef)
|
|
213
446
|
child_test = is_test_func(child)
|
|
214
447
|
ch_start = get_start_line(child)
|
|
215
|
-
ch_end =
|
|
448
|
+
ch_end = recovered_end(child)
|
|
216
449
|
|
|
217
450
|
if child_test:
|
|
218
451
|
parsed.test_ranges.append((ch_start, ch_end))
|
|
@@ -297,4 +530,5 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
|
|
|
297
530
|
)
|
|
298
531
|
)
|
|
299
532
|
|
|
533
|
+
merge_salvaged()
|
|
300
534
|
return parsed
|
skim_py/types.py
CHANGED
|
@@ -38,3 +38,13 @@ class ParsedFile:
|
|
|
38
38
|
syntax_errors: List[SyntaxNotice] = field(default_factory=list)
|
|
39
39
|
is_test_file: bool = False
|
|
40
40
|
test_ranges: List[tuple[int, int]] = field(default_factory=list)
|
|
41
|
+
masked_lines: List[int] = field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def masking_warning(self) -> str:
|
|
45
|
+
if not self.masked_lines:
|
|
46
|
+
return ""
|
|
47
|
+
locations = ", ".join(f"L{line}" for line in self.masked_lines)
|
|
48
|
+
count = len(self.masked_lines)
|
|
49
|
+
noun = "line" if count == 1 else "lines"
|
|
50
|
+
return f"# parsed with {count} {noun} masked ({locations})"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: skim-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
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
|
|
@@ -335,6 +335,7 @@ skim-py glance tests/fixtures/syntax_error.py -s
|
|
|
335
335
|
```
|
|
336
336
|
```text
|
|
337
337
|
=== tests/fixtures/syntax_error.py (12 lines) [! 1 syntax error] ===
|
|
338
|
+
# parsed with 1 line masked (L7)
|
|
338
339
|
Syntax Notice: L7: invalid syntax
|
|
339
340
|
|
|
340
341
|
Outline:
|
|
@@ -343,12 +344,13 @@ Outline:
|
|
|
343
344
|
L11-L12 def valid_after()
|
|
344
345
|
```
|
|
345
346
|
|
|
346
|
-
Surrounding valid functions remain fully extractable via `block
|
|
347
|
+
Surrounding valid functions remain fully extractable via `block` (masking warnings are surfaced via stderr):
|
|
347
348
|
|
|
348
349
|
```bash
|
|
349
350
|
skim-py block tests/fixtures/syntax_error.py valid_after
|
|
350
351
|
```
|
|
351
352
|
```text
|
|
353
|
+
tests/fixtures/syntax_error.py: # parsed with 1 line masked (L7)
|
|
352
354
|
=== tests/fixtures/syntax_error.py (L11-L12) ===
|
|
353
355
|
11 | def valid_after():
|
|
354
356
|
12 | return 2
|
|
@@ -360,6 +362,7 @@ And if an extracted block contains a syntax error within its line span, the warn
|
|
|
360
362
|
skim-py block tests/fixtures/syntax_error.py broken_function
|
|
361
363
|
```
|
|
362
364
|
```text
|
|
365
|
+
tests/fixtures/syntax_error.py: # parsed with 1 line masked (L7)
|
|
363
366
|
=== tests/fixtures/syntax_error.py (L5-L8) [! 1 syntax error] ===
|
|
364
367
|
Syntax Notice: L7: invalid syntax
|
|
365
368
|
5 | def broken_function():
|
|
@@ -368,6 +371,34 @@ skim-py block tests/fixtures/syntax_error.py broken_function
|
|
|
368
371
|
8 | return x
|
|
369
372
|
```
|
|
370
373
|
|
|
374
|
+
For cascade-prone mid-edit states (such as an unclosed `def foo(x,` header or unclosed bracket structure), `skim-py` masks the entire broken block in one step, marks the masked line range in the notice, and salvages the broken declaration in the outline so it remains targetable by `block`:
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
skim-py glance tests/fixtures/syntax_error_cascade.py -s
|
|
378
|
+
```
|
|
379
|
+
```text
|
|
380
|
+
=== tests/fixtures/syntax_error_cascade.py (11 lines) [! 1 syntax error] ===
|
|
381
|
+
# parsed with 3 lines masked (L5, L6, L7)
|
|
382
|
+
Syntax Notice: L5: '(' was never closed (L5-L7 masked)
|
|
383
|
+
|
|
384
|
+
Outline:
|
|
385
|
+
L1-L2 def valid_before()
|
|
386
|
+
L5-L7 def broken_header [syntax error]
|
|
387
|
+
L10-L11 def valid_after()
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
skim-py block tests/fixtures/syntax_error_cascade.py broken_header
|
|
392
|
+
```
|
|
393
|
+
```text
|
|
394
|
+
tests/fixtures/syntax_error_cascade.py: # parsed with 3 lines masked (L5, L6, L7)
|
|
395
|
+
=== tests/fixtures/syntax_error_cascade.py (L5-L7) [! 1 syntax error] ===
|
|
396
|
+
Syntax Notice: L5: '(' was never closed (L5-L7 masked)
|
|
397
|
+
5 | def broken_header(x,
|
|
398
|
+
6 | y = 1
|
|
399
|
+
7 | return y
|
|
400
|
+
```
|
|
401
|
+
|
|
371
402
|
---
|
|
372
403
|
|
|
373
404
|
### 2. `block` Subcommand
|
|
@@ -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=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,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
skim_py/__init__.py,sha256=GCEQwh8hAZr6Vd8gbG-xpnqnulWXEAXhG-nA7sWtmoA,71
|
|
2
|
-
skim_py/block.py,sha256=R6KWLB8-WtsKxFMPKJces_H30vk7ht_E-ig5dEnDwns,4818
|
|
3
|
-
skim_py/cli.py,sha256=gcPPkHI9GCFjmhvWyGc4srC3xeA0I14e8MgAQYY643g,4977
|
|
4
|
-
skim_py/glance.py,sha256=4tsJT1cxlWUCJhi3fiLI1t_57UnP9cA6nrstzSOZrqM,6811
|
|
5
|
-
skim_py/parser.py,sha256=d9GiiG34iBl5WhdjUU1hFyQ1_3l8ynBsLeDsoz0Wufw,11255
|
|
6
|
-
skim_py/types.py,sha256=5Eeif5OKjqD9hWc5JFccMttSLrwIB1KnoC4cgzG4SBk,1083
|
|
7
|
-
skim_python-0.0.1.dist-info/METADATA,sha256=qMFLXCH1kZQot_guaA2soAS-w2WwK6QHOjgMN2gYUfI,21537
|
|
8
|
-
skim_python-0.0.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
9
|
-
skim_python-0.0.1.dist-info/entry_points.txt,sha256=nwzHJF7CgNobxT923pwxljnEuDdJe9a2k81Q91bPY8U,45
|
|
10
|
-
skim_python-0.0.1.dist-info/licenses/LICENSE,sha256=at6MgKqAJV-KpSzR2j1hUc2hTZfymTMkbvDwmypUA9I,1074
|
|
11
|
-
skim_python-0.0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|