symbex 0.2__py3-none-any.whl → 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.
- symbex/cli.py +50 -7
- symbex/lib.py +155 -15
- {symbex-0.2.dist-info → symbex-0.3.dist-info}/METADATA +69 -3
- symbex-0.3.dist-info/RECORD +10 -0
- symbex-0.2.dist-info/RECORD +0 -10
- {symbex-0.2.dist-info → symbex-0.3.dist-info}/LICENSE +0 -0
- {symbex-0.2.dist-info → symbex-0.3.dist-info}/WHEEL +0 -0
- {symbex-0.2.dist-info → symbex-0.3.dist-info}/entry_points.txt +0 -0
- {symbex-0.2.dist-info → symbex-0.3.dist-info}/top_level.txt +0 -0
symbex/cli.py
CHANGED
@@ -8,7 +8,12 @@ from .lib import code_for_node, find_symbol_nodes
|
|
8
8
|
@click.version_option()
|
9
9
|
@click.argument("symbols", nargs=-1)
|
10
10
|
@click.option(
|
11
|
-
"files",
|
11
|
+
"files",
|
12
|
+
"-f",
|
13
|
+
"--file",
|
14
|
+
type=click.Path(file_okay=True, dir_okay=False),
|
15
|
+
multiple=True,
|
16
|
+
help="Files to search",
|
12
17
|
)
|
13
18
|
@click.option(
|
14
19
|
"directories",
|
@@ -18,7 +23,18 @@ from .lib import code_for_node, find_symbol_nodes
|
|
18
23
|
multiple=True,
|
19
24
|
help="Directories to search",
|
20
25
|
)
|
21
|
-
|
26
|
+
@click.option(
|
27
|
+
"-s",
|
28
|
+
"--signatures",
|
29
|
+
is_flag=True,
|
30
|
+
help="Show just function and class signatures",
|
31
|
+
)
|
32
|
+
@click.option(
|
33
|
+
"--silent",
|
34
|
+
is_flag=True,
|
35
|
+
help="Silently ignore Python files with parse errors",
|
36
|
+
)
|
37
|
+
def cli(symbols, files, directories, signatures, silent):
|
22
38
|
"""
|
23
39
|
Find symbols in Python code and print the code for them.
|
24
40
|
|
@@ -30,6 +46,15 @@ def cli(symbols, files, directories):
|
|
30
46
|
\b
|
31
47
|
# Search using a wildcard
|
32
48
|
symbex 'test_*'
|
49
|
+
|
50
|
+
\b
|
51
|
+
# Find a specific class method
|
52
|
+
symbex 'MyClass.my_method'
|
53
|
+
|
54
|
+
\b
|
55
|
+
# Find class methods using wildcards
|
56
|
+
symbex '*View.handle_*'
|
57
|
+
|
33
58
|
\b
|
34
59
|
# Search a specific file
|
35
60
|
symbex MyClass -f my_file.py
|
@@ -37,24 +62,42 @@ def cli(symbols, files, directories):
|
|
37
62
|
# Search within a specific directory and its subdirectories
|
38
63
|
symbex Database -d ~/projects/datasette
|
39
64
|
|
65
|
+
\b
|
66
|
+
# View signatures for all symbols in current directory and subdirectories
|
67
|
+
symbex -s
|
68
|
+
|
69
|
+
\b
|
70
|
+
# View signatures for all test functions
|
71
|
+
symbex 'test_*' -s
|
40
72
|
"""
|
73
|
+
if signatures and not symbols:
|
74
|
+
symbols = ["*"]
|
41
75
|
if not files and not directories:
|
42
76
|
directories = ["."]
|
43
|
-
files =
|
77
|
+
files = [pathlib.Path(f) for f in files]
|
44
78
|
for directory in directories:
|
45
79
|
files.extend(pathlib.Path(directory).rglob("*.py"))
|
46
80
|
pwd = pathlib.Path(".").resolve()
|
47
81
|
for file in files:
|
48
82
|
code = file.read_text("utf-8") if hasattr(file, "read_text") else file.read()
|
49
|
-
|
50
|
-
|
83
|
+
try:
|
84
|
+
nodes = find_symbol_nodes(code, symbols)
|
85
|
+
except SyntaxError as ex:
|
86
|
+
if not silent:
|
87
|
+
click.secho(f"# Syntax error in {file}: {ex}", err=True, fg="yellow")
|
88
|
+
continue
|
89
|
+
for node, class_name in nodes:
|
51
90
|
# If file is within pwd, print relative path
|
52
91
|
# else print absolute path
|
53
92
|
if pwd in file.resolve().parents:
|
54
93
|
path = file.resolve().relative_to(pwd)
|
55
94
|
else:
|
56
95
|
path = file.resolve()
|
57
|
-
snippet, line_no = code_for_node(code, node)
|
58
|
-
|
96
|
+
snippet, line_no = code_for_node(code, node, class_name, signatures)
|
97
|
+
bits = ["# File:", path]
|
98
|
+
if class_name:
|
99
|
+
bits.extend(["Class:", class_name])
|
100
|
+
bits.extend(["Line:", line_no])
|
101
|
+
print(*bits)
|
59
102
|
print(snippet)
|
60
103
|
print()
|
symbex/lib.py
CHANGED
@@ -1,41 +1,181 @@
|
|
1
1
|
import fnmatch
|
2
|
-
from ast import parse, AST
|
3
|
-
from
|
2
|
+
from ast import literal_eval, parse, AST, FunctionDef, ClassDef
|
3
|
+
from itertools import zip_longest
|
4
|
+
import textwrap
|
5
|
+
from typing import Iterable, List, Optional, Tuple
|
4
6
|
|
5
7
|
|
6
|
-
def find_symbol_nodes(
|
8
|
+
def find_symbol_nodes(
|
9
|
+
code: str, symbols: Iterable[str]
|
10
|
+
) -> List[Tuple[AST, Optional[str]]]:
|
7
11
|
"Returns ast Nodes matching symbols"
|
12
|
+
# list of (AST, None-or-class-name)
|
8
13
|
matches = []
|
9
14
|
module = parse(code)
|
10
15
|
for node in module.body:
|
16
|
+
if not isinstance(node, (ClassDef, FunctionDef)):
|
17
|
+
continue
|
11
18
|
name = getattr(node, "name", None)
|
12
19
|
if match(name, symbols):
|
13
|
-
matches.append(node)
|
20
|
+
matches.append((node, None))
|
21
|
+
# If it's a class search its methods too
|
22
|
+
if isinstance(node, ClassDef):
|
23
|
+
for child in node.body:
|
24
|
+
if isinstance(child, FunctionDef):
|
25
|
+
qualified_name = f"{name}.{child.name}"
|
26
|
+
if match(qualified_name, symbols):
|
27
|
+
matches.append((child, name))
|
28
|
+
|
14
29
|
return matches
|
15
30
|
|
16
31
|
|
17
|
-
def code_for_node(
|
32
|
+
def code_for_node(
|
33
|
+
code: str, node: AST, class_name: str, signatures: bool
|
34
|
+
) -> Tuple[str, int]:
|
18
35
|
"Returns the code for a given node"
|
19
36
|
lines = code.split("\n")
|
20
|
-
|
21
|
-
|
22
|
-
|
37
|
+
start = None
|
38
|
+
end = None
|
39
|
+
if signatures:
|
40
|
+
if isinstance(node, FunctionDef):
|
41
|
+
definition, lineno = function_definition(node), node.lineno
|
42
|
+
if class_name:
|
43
|
+
definition = " " + definition
|
44
|
+
return definition, lineno
|
45
|
+
elif isinstance(node, ClassDef):
|
46
|
+
return class_definition(node), node.lineno
|
47
|
+
else:
|
48
|
+
# Not a function or class, fall back on just the line
|
49
|
+
start = node.lineno - 1
|
50
|
+
end = node.lineno
|
23
51
|
else:
|
24
|
-
|
25
|
-
|
26
|
-
|
52
|
+
# If the node has decorator_list, include those too
|
53
|
+
if getattr(node, "decorator_list", None):
|
54
|
+
start = node.decorator_list[0].lineno - 1
|
55
|
+
else:
|
56
|
+
start = node.lineno - 1
|
57
|
+
end = node.end_lineno
|
58
|
+
output = "\n".join(lines[start:end])
|
59
|
+
# If it's in a class, indent it 4 spaces
|
60
|
+
return output, start + 1
|
27
61
|
|
28
62
|
|
29
63
|
def match(name: str, symbols: Iterable[str]) -> bool:
|
30
64
|
"Returns True if name matches any of the symbols, resolving wildcards"
|
31
65
|
if name is None:
|
32
66
|
return False
|
33
|
-
for
|
34
|
-
if "*" not in
|
35
|
-
|
67
|
+
for search in symbols:
|
68
|
+
if "*" not in search:
|
69
|
+
# Exact matches only
|
70
|
+
if name == search:
|
36
71
|
return True
|
72
|
+
elif search.count(".") == 1:
|
73
|
+
# wildcards are supported either side of the dot
|
74
|
+
if "." in name:
|
75
|
+
class_match, method_match = search.split(".")
|
76
|
+
class_name, method_name = name.split(".")
|
77
|
+
if fnmatch.fnmatch(class_name, class_match) and fnmatch.fnmatch(
|
78
|
+
method_name, method_match
|
79
|
+
):
|
80
|
+
return True
|
37
81
|
else:
|
38
|
-
if fnmatch.fnmatch(name,
|
82
|
+
if fnmatch.fnmatch(name, search) and "." not in name:
|
39
83
|
return True
|
40
84
|
|
41
85
|
return False
|
86
|
+
|
87
|
+
|
88
|
+
def function_definition(function_node: AST):
|
89
|
+
function_name = function_node.name
|
90
|
+
|
91
|
+
all_args = [
|
92
|
+
*function_node.args.posonlyargs,
|
93
|
+
*function_node.args.args,
|
94
|
+
*function_node.args.kwonlyargs,
|
95
|
+
]
|
96
|
+
|
97
|
+
# For position only args like "def foo(a, /, b, c)"
|
98
|
+
# we can look at the length of args.posonlyargs to see
|
99
|
+
# if any are set and, if so, at what index the `/` should go
|
100
|
+
position_of_slash = len(function_node.args.posonlyargs)
|
101
|
+
|
102
|
+
# For func_keyword_only_args(a, *, b, c) the length of
|
103
|
+
# the kwonlyargs tells us how many spaces back from the
|
104
|
+
# end the star should be displayed
|
105
|
+
position_of_star = len(all_args) - len(function_node.args.kwonlyargs)
|
106
|
+
|
107
|
+
# function_node.args.defaults may have defaults
|
108
|
+
# corresponding to function_node.args.args - but
|
109
|
+
# if defaults has 2 and args has 3 then those
|
110
|
+
# defaults correspond to the last two args
|
111
|
+
defaults = [None] * (len(all_args) - len(function_node.args.defaults))
|
112
|
+
defaults.extend(literal_eval(default) for default in function_node.args.defaults)
|
113
|
+
|
114
|
+
arguments = []
|
115
|
+
|
116
|
+
for i, (arg, default) in enumerate(zip_longest(all_args, defaults)):
|
117
|
+
if position_of_slash and i == position_of_slash:
|
118
|
+
arguments.append("/")
|
119
|
+
if position_of_star and i == position_of_star:
|
120
|
+
arguments.append("*")
|
121
|
+
if getattr(arg.annotation, "id", None):
|
122
|
+
arg_str = f"{arg.arg}: {arg.annotation.id}"
|
123
|
+
else:
|
124
|
+
arg_str = arg.arg
|
125
|
+
|
126
|
+
if default:
|
127
|
+
arg_str = f"{arg_str}={default}"
|
128
|
+
|
129
|
+
arguments.append(arg_str)
|
130
|
+
|
131
|
+
if function_node.args.vararg:
|
132
|
+
arguments.append(f"*{function_node.args.vararg.arg}")
|
133
|
+
|
134
|
+
if function_node.args.kwarg:
|
135
|
+
arguments.append(f"**{function_node.args.kwarg.arg}")
|
136
|
+
|
137
|
+
arguments_str = ", ".join(arguments)
|
138
|
+
|
139
|
+
return_annotation = ""
|
140
|
+
if function_node.returns:
|
141
|
+
if hasattr(function_node.returns, "id"):
|
142
|
+
return_annotation = f" -> {function_node.returns.id}"
|
143
|
+
elif function_node.returns.value is None:
|
144
|
+
# None shows as returns.value is None
|
145
|
+
return_annotation = " -> None"
|
146
|
+
|
147
|
+
return f"def {function_name}({arguments_str}){return_annotation}"
|
148
|
+
|
149
|
+
|
150
|
+
def class_definition(class_def):
|
151
|
+
# Base classes
|
152
|
+
base_classes = []
|
153
|
+
for base in class_def.bases:
|
154
|
+
if getattr(base, "id", None):
|
155
|
+
base_classes.append(base.id)
|
156
|
+
base_classes_str = ", ".join(base_classes)
|
157
|
+
|
158
|
+
# Keywords (including metaclass)
|
159
|
+
keywords = {k.arg: getattr(k.value, "id", str(k.value)) for k in class_def.keywords}
|
160
|
+
metaclass = keywords.pop("metaclass", None)
|
161
|
+
keyword_str = ", ".join([f"{k}=..." for k in keywords])
|
162
|
+
|
163
|
+
if base_classes_str and keyword_str:
|
164
|
+
signature = f"{base_classes_str}, {keyword_str}"
|
165
|
+
elif base_classes_str:
|
166
|
+
signature = base_classes_str
|
167
|
+
elif keyword_str:
|
168
|
+
signature = keyword_str
|
169
|
+
else:
|
170
|
+
signature = ""
|
171
|
+
|
172
|
+
if metaclass:
|
173
|
+
sep = ", " if signature else ""
|
174
|
+
signature = f"{signature}{sep}metaclass={metaclass}"
|
175
|
+
|
176
|
+
if signature:
|
177
|
+
signature = f"({signature})"
|
178
|
+
|
179
|
+
class_definition = f"class {class_def.name}{signature}"
|
180
|
+
|
181
|
+
return class_definition
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: symbex
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3
|
4
4
|
Summary: Find the Python code for specified symbols
|
5
5
|
Home-page: https://github.com/simonw/symbex
|
6
6
|
Author: Simon Willison
|
@@ -15,6 +15,7 @@ Requires-Dist: click
|
|
15
15
|
Provides-Extra: test
|
16
16
|
Requires-Dist: pytest ; extra == 'test'
|
17
17
|
Requires-Dist: pytest-icdiff ; extra == 'test'
|
18
|
+
Requires-Dist: cogapp ; extra == 'test'
|
18
19
|
|
19
20
|
# symbex
|
20
21
|
|
@@ -48,6 +49,20 @@ Wildcards are supported - to search for every `test_` function run this (note th
|
|
48
49
|
```bash
|
49
50
|
symbex 'test_*'
|
50
51
|
```
|
52
|
+
To search for methods within classes, use `class.method` notation:
|
53
|
+
```bash
|
54
|
+
symbex Entry.get_absolute_url
|
55
|
+
```
|
56
|
+
Wildcards are supported here as well:
|
57
|
+
```bash
|
58
|
+
symbex 'Entry.*'
|
59
|
+
symbex '*.get_absolute_url'
|
60
|
+
symbex '*.get_*'
|
61
|
+
```
|
62
|
+
Or to view every method of every class:
|
63
|
+
```bash
|
64
|
+
symbex '*.*'
|
65
|
+
```
|
51
66
|
To search within a specific file, pass that file using the `-f` option. You can pass this more than once to search multiple files.
|
52
67
|
|
53
68
|
```bash
|
@@ -57,8 +72,15 @@ To search within a specific directory and all of its subdirectories, use the `-d
|
|
57
72
|
```bash
|
58
73
|
symbex Database -d ~/projects/datasette
|
59
74
|
```
|
60
|
-
|
61
|
-
|
75
|
+
If `symbex` encounters any Python code that it cannot parse, it will print a warning message and continue searching:
|
76
|
+
```
|
77
|
+
# Syntax error in path/badcode.py: expected ':' (<unknown>, line 1)
|
78
|
+
```
|
79
|
+
Pass `--silent` to suppress these warnings:
|
80
|
+
```bash
|
81
|
+
symbex MyClass --silent
|
82
|
+
```
|
83
|
+
### Example output
|
62
84
|
|
63
85
|
In a fresh checkout of [Datasette](https://github.com/simonw/datasette) I ran this command:
|
64
86
|
|
@@ -87,6 +109,50 @@ class PatternPortfolioView(View):
|
|
87
109
|
)
|
88
110
|
)
|
89
111
|
```
|
112
|
+
### Just the signatures
|
113
|
+
|
114
|
+
The `-s/--signatures` option will list just the signatures of the functions and classes, for example:
|
115
|
+
```bash
|
116
|
+
symbex -s -d symbex
|
117
|
+
```
|
118
|
+
|
119
|
+
<!-- [[[cog
|
120
|
+
import cog
|
121
|
+
from click.testing import CliRunner
|
122
|
+
import pathlib
|
123
|
+
from symbex.cli import cli
|
124
|
+
|
125
|
+
path = pathlib.Path("symbex").resolve()
|
126
|
+
runner = CliRunner()
|
127
|
+
result = runner.invoke(cli, ["-s", "-d", str(path)])
|
128
|
+
# Need a consistent sort order
|
129
|
+
chunks = result.stdout.strip().split("\n\n")
|
130
|
+
chunks.sort()
|
131
|
+
cog.out(
|
132
|
+
"```\n{}\n```\n".format("\n\n".join(chunks))
|
133
|
+
)
|
134
|
+
]]] -->
|
135
|
+
```
|
136
|
+
# File: symbex/cli.py Line: 37
|
137
|
+
def cli(symbols, files, directories, signatures, silent)
|
138
|
+
|
139
|
+
# File: symbex/lib.py Line: 150
|
140
|
+
def class_definition(class_def)
|
141
|
+
|
142
|
+
# File: symbex/lib.py Line: 32
|
143
|
+
def code_for_node(code: str, node: AST, class_name: str, signatures: bool)
|
144
|
+
|
145
|
+
# File: symbex/lib.py Line: 63
|
146
|
+
def match(name: str, symbols) -> bool
|
147
|
+
|
148
|
+
# File: symbex/lib.py Line: 8
|
149
|
+
def find_symbol_nodes(code: str, symbols)
|
150
|
+
|
151
|
+
# File: symbex/lib.py Line: 88
|
152
|
+
def function_definition(function_node: AST)
|
153
|
+
```
|
154
|
+
<!-- [[[end]]] -->
|
155
|
+
This can be combined with other options, or you can run `symbex -s` to see every symbol in the current directory and its subdirectories.
|
90
156
|
|
91
157
|
## Using with LLM
|
92
158
|
|
@@ -0,0 +1,10 @@
|
|
1
|
+
symbex/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
symbex/__main__.py,sha256=8hDtWlaFZK24KhfNq_ZKgtXqYHsDQDetukOCMlsbW0Q,59
|
3
|
+
symbex/cli.py,sha256=tWzT49q-fPN34HQH2sEFHBV3MXFKy9J5eh2EzOo5_CA,2857
|
4
|
+
symbex/lib.py,sha256=q8WhjbPLiXuGnbD3scwCvJrqVvs-RHAMbBEXIYyFqeI,6121
|
5
|
+
symbex-0.3.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
6
|
+
symbex-0.3.dist-info/METADATA,sha256=Yn6vp7j1nWfTn_GYWXmeWhD7NFXHw2OxI3YnpMRVvew,5693
|
7
|
+
symbex-0.3.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
8
|
+
symbex-0.3.dist-info/entry_points.txt,sha256=YgMSEfEGqNMHM9RysFObH8lkQKVZKyymKLnXbVue_Uk,42
|
9
|
+
symbex-0.3.dist-info/top_level.txt,sha256=qwle8HjAaYgpdMIHlJcTcN4gaG4wmDqUvkt54beTBTs,7
|
10
|
+
symbex-0.3.dist-info/RECORD,,
|
symbex-0.2.dist-info/RECORD
DELETED
@@ -1,10 +0,0 @@
|
|
1
|
-
symbex/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
symbex/__main__.py,sha256=8hDtWlaFZK24KhfNq_ZKgtXqYHsDQDetukOCMlsbW0Q,59
|
3
|
-
symbex/cli.py,sha256=tJhsnbWEiIN9LSZGGcqrmpY5U9TgISvknXsMCpijlSs,1770
|
4
|
-
symbex/lib.py,sha256=1YViD5fJ_NP1Y32LTFM7o8UCTzUiz3yTldVvJb8owgs,1190
|
5
|
-
symbex-0.2.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
6
|
-
symbex-0.2.dist-info/METADATA,sha256=QMsejWdFVPSvRODCyd09SRl_mcH0dLvXk3tOX2GNbts,3917
|
7
|
-
symbex-0.2.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
8
|
-
symbex-0.2.dist-info/entry_points.txt,sha256=YgMSEfEGqNMHM9RysFObH8lkQKVZKyymKLnXbVue_Uk,42
|
9
|
-
symbex-0.2.dist-info/top_level.txt,sha256=qwle8HjAaYgpdMIHlJcTcN4gaG4wmDqUvkt54beTBTs,7
|
10
|
-
symbex-0.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|