symbex 0.2.1__py3-none-any.whl → 0.3__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
symbex/cli.py CHANGED
@@ -23,12 +23,18 @@ from .lib import code_for_node, find_symbol_nodes
23
23
  multiple=True,
24
24
  help="Directories to search",
25
25
  )
26
+ @click.option(
27
+ "-s",
28
+ "--signatures",
29
+ is_flag=True,
30
+ help="Show just function and class signatures",
31
+ )
26
32
  @click.option(
27
33
  "--silent",
28
34
  is_flag=True,
29
35
  help="Silently ignore Python files with parse errors",
30
36
  )
31
- def cli(symbols, files, directories, silent):
37
+ def cli(symbols, files, directories, signatures, silent):
32
38
  """
33
39
  Find symbols in Python code and print the code for them.
34
40
 
@@ -40,6 +46,15 @@ def cli(symbols, files, directories, silent):
40
46
  \b
41
47
  # Search using a wildcard
42
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
+
43
58
  \b
44
59
  # Search a specific file
45
60
  symbex MyClass -f my_file.py
@@ -47,7 +62,16 @@ def cli(symbols, files, directories, silent):
47
62
  # Search within a specific directory and its subdirectories
48
63
  symbex Database -d ~/projects/datasette
49
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
50
72
  """
73
+ if signatures and not symbols:
74
+ symbols = ["*"]
51
75
  if not files and not directories:
52
76
  directories = ["."]
53
77
  files = [pathlib.Path(f) for f in files]
@@ -62,14 +86,18 @@ def cli(symbols, files, directories, silent):
62
86
  if not silent:
63
87
  click.secho(f"# Syntax error in {file}: {ex}", err=True, fg="yellow")
64
88
  continue
65
- for node in nodes:
89
+ for node, class_name in nodes:
66
90
  # If file is within pwd, print relative path
67
91
  # else print absolute path
68
92
  if pwd in file.resolve().parents:
69
93
  path = file.resolve().relative_to(pwd)
70
94
  else:
71
95
  path = file.resolve()
72
- snippet, line_no = code_for_node(code, node)
73
- print("# File:", path, "Line:", line_no)
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)
74
102
  print(snippet)
75
103
  print()
symbex/lib.py CHANGED
@@ -1,41 +1,181 @@
1
1
  import fnmatch
2
- from ast import parse, AST
3
- from typing import Iterable, Tuple
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(code: str, symbols: Iterable[str]) -> Iterable[AST]:
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(code: str, node: AST) -> Tuple[str, int]:
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
- # If the node has decorator_list, include those too
21
- if getattr(node, "decorator_list", None):
22
- start = node.decorator_list[0].lineno - 1
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
- start = node.lineno - 1
25
- end = node.end_lineno
26
- return "\n".join(lines[start:end]), start + 1
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 symbol in symbols:
34
- if "*" not in symbol:
35
- if name == symbol:
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, symbol):
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.2.1
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
@@ -65,8 +80,7 @@ Pass `--silent` to suppress these warnings:
65
80
  ```bash
66
81
  symbex MyClass --silent
67
82
  ```
68
-
69
- ## Example output
83
+ ### Example output
70
84
 
71
85
  In a fresh checkout of [Datasette](https://github.com/simonw/datasette) I ran this command:
72
86
 
@@ -95,6 +109,50 @@ class PatternPortfolioView(View):
95
109
  )
96
110
  )
97
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.
98
156
 
99
157
  ## Using with LLM
100
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,,
@@ -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=LXrPvjSG4IvDeLbDNoZ6Dykb4zt4D6UK7wVwjClf8Dw,2142
4
- symbex/lib.py,sha256=1YViD5fJ_NP1Y32LTFM7o8UCTzUiz3yTldVvJb8owgs,1190
5
- symbex-0.2.1.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
6
- symbex-0.2.1.dist-info/METADATA,sha256=SaikT2bPWwSeRgCBlFzg13ONt-BiyV9dMdAtpdKDQJ8,4192
7
- symbex-0.2.1.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
8
- symbex-0.2.1.dist-info/entry_points.txt,sha256=YgMSEfEGqNMHM9RysFObH8lkQKVZKyymKLnXbVue_Uk,42
9
- symbex-0.2.1.dist-info/top_level.txt,sha256=qwle8HjAaYgpdMIHlJcTcN4gaG4wmDqUvkt54beTBTs,7
10
- symbex-0.2.1.dist-info/RECORD,,
File without changes
File without changes