skim-python 0.0.1__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/__init__.py +5 -0
- skim_py/block.py +150 -0
- skim_py/cli.py +147 -0
- skim_py/glance.py +203 -0
- skim_py/parser.py +300 -0
- skim_py/types.py +40 -0
- skim_python-0.0.1.dist-info/METADATA +523 -0
- skim_python-0.0.1.dist-info/RECORD +11 -0
- skim_python-0.0.1.dist-info/WHEEL +4 -0
- skim_python-0.0.1.dist-info/entry_points.txt +2 -0
- skim_python-0.0.1.dist-info/licenses/LICENSE +21 -0
skim_py/__init__.py
ADDED
skim_py/block.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""block subcommand implementation for skim-py."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
from .types import OutlineItem, ParsedFile
|
|
10
|
+
from .parser import parse_source_file
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def collect_all_items(items: List[OutlineItem]) -> List[OutlineItem]:
|
|
14
|
+
res: List[OutlineItem] = []
|
|
15
|
+
for it in items:
|
|
16
|
+
res.append(it)
|
|
17
|
+
if it.children:
|
|
18
|
+
res.extend(collect_all_items(it.children))
|
|
19
|
+
return res
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run_block(
|
|
23
|
+
file_path: str,
|
|
24
|
+
target: str,
|
|
25
|
+
raw: bool = False,
|
|
26
|
+
include_tests: bool = False,
|
|
27
|
+
) -> int:
|
|
28
|
+
p = Path(file_path)
|
|
29
|
+
if not p.exists():
|
|
30
|
+
sys.stderr.write(f"Error: file not found: {file_path}\n")
|
|
31
|
+
return 1
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
35
|
+
content = f.read()
|
|
36
|
+
except Exception as err:
|
|
37
|
+
sys.stderr.write(f"Error reading {file_path}: {err}\n")
|
|
38
|
+
return 1
|
|
39
|
+
|
|
40
|
+
parsed = parse_source_file(file_path, content, include_tests=include_tests)
|
|
41
|
+
all_items = collect_all_items(parsed.items)
|
|
42
|
+
|
|
43
|
+
matches: List[OutlineItem] = []
|
|
44
|
+
|
|
45
|
+
if "." in target:
|
|
46
|
+
dot_idx = target.index(".")
|
|
47
|
+
parent_target = target[:dot_idx]
|
|
48
|
+
child_target = target[dot_idx + 1 :]
|
|
49
|
+
|
|
50
|
+
parent_candidates = [it for it in all_items if it.parent_name is not None]
|
|
51
|
+
|
|
52
|
+
# Tier 1: Exact case match on both parent and child
|
|
53
|
+
exact_matches = [
|
|
54
|
+
it
|
|
55
|
+
for it in parent_candidates
|
|
56
|
+
if it.parent_name == parent_target and it.name == child_target
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
if exact_matches:
|
|
60
|
+
matches = exact_matches
|
|
61
|
+
else:
|
|
62
|
+
# Tier 2: Case-insensitive match on both parent and child
|
|
63
|
+
p_lower = parent_target.lower()
|
|
64
|
+
c_lower = child_target.lower()
|
|
65
|
+
case_matches = [
|
|
66
|
+
it
|
|
67
|
+
for it in parent_candidates
|
|
68
|
+
if it.parent_name.lower() == p_lower and it.name.lower() == c_lower
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
if case_matches:
|
|
72
|
+
matches = case_matches
|
|
73
|
+
else:
|
|
74
|
+
# Tier 3: Substring search
|
|
75
|
+
matches = [
|
|
76
|
+
it
|
|
77
|
+
for it in parent_candidates
|
|
78
|
+
if p_lower in it.parent_name.lower()
|
|
79
|
+
and (c_lower in it.name.lower() or c_lower in it.raw_header.lower())
|
|
80
|
+
]
|
|
81
|
+
else:
|
|
82
|
+
# Standalone identifier
|
|
83
|
+
# Tier 1: Exact match on name
|
|
84
|
+
exact_matches = [it for it in all_items if it.name == target]
|
|
85
|
+
|
|
86
|
+
if exact_matches:
|
|
87
|
+
matches = exact_matches
|
|
88
|
+
else:
|
|
89
|
+
# Tier 2: Case-insensitive match on name
|
|
90
|
+
t_lower = target.lower()
|
|
91
|
+
case_matches = [it for it in all_items if it.name.lower() == t_lower]
|
|
92
|
+
|
|
93
|
+
if case_matches:
|
|
94
|
+
matches = case_matches
|
|
95
|
+
else:
|
|
96
|
+
# Tier 3: Case-insensitive substring match
|
|
97
|
+
matches = [
|
|
98
|
+
it
|
|
99
|
+
for it in all_items
|
|
100
|
+
if t_lower in it.name.lower() or t_lower in it.raw_header.lower()
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
cwd = os.getcwd()
|
|
104
|
+
try:
|
|
105
|
+
rel_path = os.path.relpath(file_path, cwd)
|
|
106
|
+
except ValueError:
|
|
107
|
+
rel_path = file_path
|
|
108
|
+
|
|
109
|
+
if not matches:
|
|
110
|
+
sys.stderr.write(f"Error: block '{target}' not found in {rel_path}\n")
|
|
111
|
+
return 2
|
|
112
|
+
|
|
113
|
+
if len(matches) > 1:
|
|
114
|
+
sys.stderr.write(
|
|
115
|
+
f"Error: ambiguous target '{target}' matched {len(matches)} candidates in {rel_path}:\n"
|
|
116
|
+
)
|
|
117
|
+
for idx, m in enumerate(matches, 1):
|
|
118
|
+
span = f"L{m.start_line}-L{m.end_line}".ljust(10)
|
|
119
|
+
sys.stderr.write(f" {idx}) {span} {m.kind} {m.qualified_name}\n")
|
|
120
|
+
sys.stderr.write(
|
|
121
|
+
"\nTip: Disambiguate with Class.method (e.g. BaseModel.model_dump) or read line range directly.\n"
|
|
122
|
+
)
|
|
123
|
+
return 3
|
|
124
|
+
|
|
125
|
+
chosen = matches[0]
|
|
126
|
+
src_lines = content.splitlines()
|
|
127
|
+
block_lines = src_lines[chosen.start_line - 1 : chosen.end_line]
|
|
128
|
+
|
|
129
|
+
# Check for syntax errors within block range
|
|
130
|
+
block_errors = [
|
|
131
|
+
e for e in parsed.syntax_errors if chosen.start_line <= e.line <= chosen.end_line
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
error_tag = ""
|
|
135
|
+
if block_errors:
|
|
136
|
+
n = len(block_errors)
|
|
137
|
+
error_tag = " [! 1 syntax error]" if n == 1 else f" [! {n} syntax errors]"
|
|
138
|
+
|
|
139
|
+
if raw:
|
|
140
|
+
print("\n".join(block_lines))
|
|
141
|
+
else:
|
|
142
|
+
print(f"=== {rel_path} (L{chosen.start_line}-L{chosen.end_line}){error_tag} ===")
|
|
143
|
+
for err in block_errors:
|
|
144
|
+
print(f" Syntax Notice: L{err.line}: {err.message}")
|
|
145
|
+
for idx, line in enumerate(block_lines):
|
|
146
|
+
line_num = chosen.start_line + idx
|
|
147
|
+
line_padded = str(line_num).rjust(5)
|
|
148
|
+
print(f"{line_padded} | {line}")
|
|
149
|
+
|
|
150
|
+
return 0
|
skim_py/cli.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""CLI entrypoint for skim-py."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import sys
|
|
5
|
+
import argparse
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
from .glance import run_glance
|
|
9
|
+
from .block import run_block
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
USAGE_DOC = """\
|
|
13
|
+
skim-py: Fast Python outline & block extractor without test bloat.
|
|
14
|
+
Prefer this over reading entire .py files to preserve token context.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
skim-py glance [options] <path>...
|
|
18
|
+
skim-py block [options] <file> <target>
|
|
19
|
+
|
|
20
|
+
glance:
|
|
21
|
+
Outline structure, signatures, and line ranges across files/directories.
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
skim-py glance [options] <path>...
|
|
25
|
+
|
|
26
|
+
Arguments:
|
|
27
|
+
<path>... One or more source files, directories, or globs
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
-s, --signatures Show declarations & types without function bodies
|
|
31
|
+
-d, --details Show class attributes, annotated fields, and enum members
|
|
32
|
+
-u, --uses, --imports Display top-level import statements
|
|
33
|
+
-f, --filter <text> Filter items containing <text> (case-insensitive, repeatable)
|
|
34
|
+
-t, --include-tests Include test files (test_*.py, etc.) and test blocks
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
skim-py glance src/models.py
|
|
38
|
+
skim-py glance src/ -s -u
|
|
39
|
+
skim-py glance src/ -s -f "BaseModel"
|
|
40
|
+
|
|
41
|
+
block:
|
|
42
|
+
Extract verbatim code block for a class, method, function, or variable.
|
|
43
|
+
|
|
44
|
+
Usage:
|
|
45
|
+
skim-py block [options] <file> <target>
|
|
46
|
+
|
|
47
|
+
Arguments:
|
|
48
|
+
<file> Path to the source file
|
|
49
|
+
<target> Name of class, method, function, or Class.method
|
|
50
|
+
|
|
51
|
+
Options:
|
|
52
|
+
-t, --include-tests Search inside test files and test blocks
|
|
53
|
+
--raw Omit line number gutter
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
skim-py block src/models.py BaseModel
|
|
57
|
+
skim-py block src/models.py BaseModel.model_dump
|
|
58
|
+
skim-py block src/models.py BaseModel.model_dump --raw
|
|
59
|
+
|
|
60
|
+
Disambiguation:
|
|
61
|
+
If multiple items match, qualify Class.method (e.g. BaseModel.model_dump) or read line range directly.
|
|
62
|
+
|
|
63
|
+
Agent Operational Workflow:
|
|
64
|
+
1. Glance first: skim-py glance <path> -s
|
|
65
|
+
2. Locate logic: skim-py glance <path> -f <keyword>
|
|
66
|
+
3. Inspect code: Use `block <file> <Class.method>`, or read reported line spans (Lxx-Lyy)
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main(args: Optional[List[str]] = None) -> int:
|
|
71
|
+
if args is None:
|
|
72
|
+
args = sys.argv[1:]
|
|
73
|
+
|
|
74
|
+
if not args or args[0] in {"-h", "--help"}:
|
|
75
|
+
sys.stdout.write(USAGE_DOC)
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
subcommand = args[0]
|
|
79
|
+
|
|
80
|
+
if subcommand == "glance":
|
|
81
|
+
parser = argparse.ArgumentParser(
|
|
82
|
+
prog="skim-py glance",
|
|
83
|
+
description="Outline structure, signatures, and line spans.",
|
|
84
|
+
add_help=False,
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument("-s", "--signatures", action="store_true")
|
|
87
|
+
parser.add_argument("-d", "--details", action="store_true")
|
|
88
|
+
parser.add_argument("-u", "--uses", "--imports", action="store_true", dest="uses")
|
|
89
|
+
parser.add_argument("-f", "--filter", action="append", default=[], dest="filter")
|
|
90
|
+
parser.add_argument("-t", "--include-tests", action="store_true", dest="include_tests")
|
|
91
|
+
parser.add_argument("-h", "--help", action="store_true")
|
|
92
|
+
parser.add_argument("targets", nargs="*")
|
|
93
|
+
|
|
94
|
+
parsed_args, extra = parser.parse_known_args(args[1:])
|
|
95
|
+
if parsed_args.help:
|
|
96
|
+
sys.stdout.write(USAGE_DOC)
|
|
97
|
+
return 0
|
|
98
|
+
if not parsed_args.targets:
|
|
99
|
+
sys.stderr.write("Error: missing target path(s) for glance.\n")
|
|
100
|
+
return 1
|
|
101
|
+
|
|
102
|
+
return run_glance(
|
|
103
|
+
targets=parsed_args.targets,
|
|
104
|
+
signatures=parsed_args.signatures,
|
|
105
|
+
details=parsed_args.details,
|
|
106
|
+
uses=parsed_args.uses,
|
|
107
|
+
filters=parsed_args.filter,
|
|
108
|
+
include_tests=parsed_args.include_tests,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
elif subcommand == "block":
|
|
112
|
+
parser = argparse.ArgumentParser(
|
|
113
|
+
prog="skim-py block",
|
|
114
|
+
description="Extract verbatim code block.",
|
|
115
|
+
add_help=False,
|
|
116
|
+
)
|
|
117
|
+
parser.add_argument("-t", "--include-tests", action="store_true", dest="include_tests")
|
|
118
|
+
parser.add_argument("--raw", action="store_true")
|
|
119
|
+
parser.add_argument("-h", "--help", action="store_true")
|
|
120
|
+
parser.add_argument("positionals", nargs="*")
|
|
121
|
+
|
|
122
|
+
parsed_args, extra = parser.parse_known_args(args[1:])
|
|
123
|
+
if parsed_args.help:
|
|
124
|
+
sys.stdout.write(USAGE_DOC)
|
|
125
|
+
return 0
|
|
126
|
+
if len(parsed_args.positionals) < 2:
|
|
127
|
+
sys.stderr.write("Error: block requires <file> and <target> arguments.\n")
|
|
128
|
+
return 1
|
|
129
|
+
|
|
130
|
+
file_path = parsed_args.positionals[0]
|
|
131
|
+
target = parsed_args.positionals[1]
|
|
132
|
+
|
|
133
|
+
return run_block(
|
|
134
|
+
file_path=file_path,
|
|
135
|
+
target=target,
|
|
136
|
+
raw=parsed_args.raw,
|
|
137
|
+
include_tests=parsed_args.include_tests,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
else:
|
|
141
|
+
sys.stderr.write(f"Error: unknown subcommand '{subcommand}'. Use 'glance' or 'block'.\n\n")
|
|
142
|
+
sys.stdout.write(USAGE_DOC)
|
|
143
|
+
return 1
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
sys.exit(main())
|
skim_py/glance.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""glance subcommand implementation for skim-py."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import glob
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from .types import OutlineItem, ParsedFile
|
|
11
|
+
from .parser import parse_source_file, is_test_file
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
IGNORED_DIRS = {
|
|
15
|
+
".git",
|
|
16
|
+
"__pycache__",
|
|
17
|
+
".pytest_cache",
|
|
18
|
+
".mypy_cache",
|
|
19
|
+
".ruff_cache",
|
|
20
|
+
".venv",
|
|
21
|
+
"venv",
|
|
22
|
+
"env",
|
|
23
|
+
"node_modules",
|
|
24
|
+
"dist",
|
|
25
|
+
"build",
|
|
26
|
+
".tox",
|
|
27
|
+
".eggs",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
SUPPORTED_EXTS = {".py", ".pyi"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_supported_file(path_str: str) -> bool:
|
|
34
|
+
return Path(path_str).suffix.lower() in SUPPORTED_EXTS
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def find_files_recursively(dir_path: str, include_tests: bool) -> List[str]:
|
|
38
|
+
results: List[str] = []
|
|
39
|
+
try:
|
|
40
|
+
with os.scandir(dir_path) as it:
|
|
41
|
+
for entry in it:
|
|
42
|
+
if entry.is_dir(follow_symlinks=False):
|
|
43
|
+
if entry.name in IGNORED_DIRS:
|
|
44
|
+
continue
|
|
45
|
+
if not include_tests and entry.name.lower() in {"tests", "test", "testing", "__tests__"}:
|
|
46
|
+
continue
|
|
47
|
+
results.extend(find_files_recursively(entry.path, include_tests))
|
|
48
|
+
elif entry.is_file(follow_symlinks=False):
|
|
49
|
+
if is_supported_file(entry.name):
|
|
50
|
+
if not include_tests and is_test_file(entry.path):
|
|
51
|
+
continue
|
|
52
|
+
results.append(entry.path)
|
|
53
|
+
except (PermissionError, FileNotFoundError) as err:
|
|
54
|
+
sys.stderr.write(f"Warning: error reading directory {dir_path}: {err}\n")
|
|
55
|
+
return results
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def collect_files(targets: List[str], include_tests: bool) -> List[str]:
|
|
59
|
+
files: List[str] = []
|
|
60
|
+
for target in targets:
|
|
61
|
+
p = Path(target)
|
|
62
|
+
if p.exists():
|
|
63
|
+
if p.is_dir():
|
|
64
|
+
files.extend(find_files_recursively(str(p), include_tests))
|
|
65
|
+
elif p.is_file():
|
|
66
|
+
if is_supported_file(str(p)):
|
|
67
|
+
# When an explicit file path is directly supplied, always include it
|
|
68
|
+
files.append(str(p))
|
|
69
|
+
elif any(char in target for char in "*?[]"):
|
|
70
|
+
matches = glob.glob(target, recursive=True)
|
|
71
|
+
for m in matches:
|
|
72
|
+
if is_supported_file(m):
|
|
73
|
+
if include_tests or not is_test_file(m):
|
|
74
|
+
files.append(m)
|
|
75
|
+
else:
|
|
76
|
+
sys.stderr.write(f"Warning: path not found: {target}\n")
|
|
77
|
+
|
|
78
|
+
unique = sorted(set(os.path.normpath(f) for f in files))
|
|
79
|
+
return unique
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def item_matches_filters(item: OutlineItem, content: str, lines: List[str], filters: List[str]) -> bool:
|
|
83
|
+
if not filters:
|
|
84
|
+
return True
|
|
85
|
+
block_text = "\n".join(lines[item.start_line - 1 : item.end_line]).lower()
|
|
86
|
+
return all(f.lower() in block_text for f in filters)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def has_child_matching(item: OutlineItem, content: str, lines: List[str], filters: List[str]) -> bool:
|
|
90
|
+
if not filters:
|
|
91
|
+
return True
|
|
92
|
+
for ch in item.children:
|
|
93
|
+
if item_matches_filters(ch, content, lines, filters) or has_child_matching(ch, content, lines, filters):
|
|
94
|
+
return True
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def run_glance(
|
|
99
|
+
targets: List[str],
|
|
100
|
+
signatures: bool = False,
|
|
101
|
+
details: bool = False,
|
|
102
|
+
uses: bool = False,
|
|
103
|
+
filters: Optional[List[str]] = None,
|
|
104
|
+
include_tests: bool = False,
|
|
105
|
+
) -> int:
|
|
106
|
+
filters = filters or []
|
|
107
|
+
files = collect_files(targets, include_tests)
|
|
108
|
+
|
|
109
|
+
if not files:
|
|
110
|
+
sys.stderr.write("Error: no Python files found matching input.\n")
|
|
111
|
+
return 1
|
|
112
|
+
|
|
113
|
+
printed_count = 0
|
|
114
|
+
cwd = os.getcwd()
|
|
115
|
+
|
|
116
|
+
for file_path in files:
|
|
117
|
+
try:
|
|
118
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
119
|
+
content = f.read()
|
|
120
|
+
except Exception as err:
|
|
121
|
+
sys.stderr.write(f"Error reading {file_path}: {err}\n")
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
src_lines = content.splitlines()
|
|
125
|
+
parsed = parse_source_file(file_path, content, include_tests=include_tests)
|
|
126
|
+
|
|
127
|
+
if filters:
|
|
128
|
+
has_match = any(
|
|
129
|
+
item_matches_filters(it, content, src_lines, filters)
|
|
130
|
+
or has_child_matching(it, content, src_lines, filters)
|
|
131
|
+
for it in parsed.items
|
|
132
|
+
)
|
|
133
|
+
if not has_match and not parsed.syntax_errors:
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
if printed_count > 0:
|
|
137
|
+
print("\n" + "=" * 70 + "\n")
|
|
138
|
+
printed_count += 1
|
|
139
|
+
|
|
140
|
+
test_info = ""
|
|
141
|
+
if parsed.is_test_file and include_tests:
|
|
142
|
+
test_info = " [test]"
|
|
143
|
+
elif parsed.test_ranges and not include_tests:
|
|
144
|
+
tr_desc = ", ".join(f"{s}..{e}" for s, e in parsed.test_ranges)
|
|
145
|
+
test_info = f" [tests skipped: {tr_desc}]"
|
|
146
|
+
|
|
147
|
+
error_tag = ""
|
|
148
|
+
if parsed.syntax_errors:
|
|
149
|
+
n = len(parsed.syntax_errors)
|
|
150
|
+
error_tag = " [! 1 syntax error]" if n == 1 else f" [! {n} syntax errors]"
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
rel_path = os.path.relpath(file_path, cwd)
|
|
154
|
+
except ValueError:
|
|
155
|
+
rel_path = file_path
|
|
156
|
+
|
|
157
|
+
print(f"=== {rel_path} ({parsed.total_lines} lines){test_info}{error_tag} ===")
|
|
158
|
+
|
|
159
|
+
for err in parsed.syntax_errors:
|
|
160
|
+
print(f" Syntax Notice: L{err.line}: {err.message}")
|
|
161
|
+
|
|
162
|
+
if uses:
|
|
163
|
+
if parsed.imports:
|
|
164
|
+
print("\nImports:")
|
|
165
|
+
for imp in parsed.imports:
|
|
166
|
+
for line in imp.splitlines():
|
|
167
|
+
print(f" {line}")
|
|
168
|
+
else:
|
|
169
|
+
print("\nImports: (none)")
|
|
170
|
+
|
|
171
|
+
print("\nOutline:")
|
|
172
|
+
|
|
173
|
+
def render_tree(items: List[OutlineItem], depth: int = 0) -> None:
|
|
174
|
+
for it in items:
|
|
175
|
+
matches = item_matches_filters(it, content, src_lines, filters)
|
|
176
|
+
child_matches = has_child_matching(it, content, src_lines, filters)
|
|
177
|
+
if not matches and not child_matches:
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
span = f"L{it.start_line}-L{it.end_line}"
|
|
181
|
+
if it.kind == "test":
|
|
182
|
+
label = it.raw_header
|
|
183
|
+
elif signatures:
|
|
184
|
+
label = it.raw_header
|
|
185
|
+
else:
|
|
186
|
+
label = f"{it.kind} {it.name}"
|
|
187
|
+
|
|
188
|
+
child_indent = " " * depth
|
|
189
|
+
span_padded = span.ljust(12)
|
|
190
|
+
print(f" {span_padded} {child_indent}{label}")
|
|
191
|
+
|
|
192
|
+
if it.fields_or_variants and details:
|
|
193
|
+
f_indent = " " * (17 + depth * 2)
|
|
194
|
+
for fv in it.fields_or_variants:
|
|
195
|
+
print(f"{f_indent}| {fv}")
|
|
196
|
+
|
|
197
|
+
methods_or_subitems = [ch for ch in it.children if ch.kind in {"method", "test"}]
|
|
198
|
+
if methods_or_subitems:
|
|
199
|
+
render_tree(methods_or_subitems, depth + 1)
|
|
200
|
+
|
|
201
|
+
render_tree(parsed.items)
|
|
202
|
+
|
|
203
|
+
return 0
|