d8s-python 0.10.0__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.
- d8s_python/__init__.py +6 -0
- d8s_python/ast_data.py +284 -0
- d8s_python/python_data.py +406 -0
- d8s_python-0.10.0.dist-info/METADATA +305 -0
- d8s_python-0.10.0.dist-info/RECORD +8 -0
- d8s_python-0.10.0.dist-info/WHEEL +4 -0
- d8s_python-0.10.0.dist-info/licenses/COPYING +674 -0
- d8s_python-0.10.0.dist-info/licenses/COPYING.LESSER +165 -0
d8s_python/__init__.py
ADDED
d8s_python/ast_data.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from typing import Iterable, List, Optional, Tuple, Union
|
|
3
|
+
|
|
4
|
+
import more_itertools
|
|
5
|
+
from d8s_lists import iterable_replace, truthy_items
|
|
6
|
+
|
|
7
|
+
# TODO: all of these functions where code_text is given should also be able to read a file at a given path (?)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _python_ast_exception_name(node: Union[ast.Raise, ast.ExceptHandler]) -> Optional[str]:
|
|
11
|
+
"""."""
|
|
12
|
+
if hasattr(node, "exc") and node.exc: # this handles ast.Raise nodes
|
|
13
|
+
if hasattr(
|
|
14
|
+
node.exc, "id"
|
|
15
|
+
): # this handles ast.Raise nodes where the exception being raised is an ast.Name (e.g. "e" or "ValueError")
|
|
16
|
+
return node.exc.id
|
|
17
|
+
elif hasattr(
|
|
18
|
+
node.exc.func, # type: ignore[union-attr]
|
|
19
|
+
"id",
|
|
20
|
+
): # handles ast.Raise nodes where the exception being raised is an ast.Call (e.g. "ValueError('Foo Bar')")
|
|
21
|
+
return node.exc.func.id # type: ignore[union-attr]
|
|
22
|
+
elif hasattr(
|
|
23
|
+
node.exc.func, # type: ignore[union-attr]
|
|
24
|
+
"attr",
|
|
25
|
+
): # this handles ast.Raise nodes raising a non-built-in error (e.g. "pint.UndefinedUnitError")
|
|
26
|
+
return f"{node.exc.func.value.id}.{node.exc.func.attr}" # type: ignore[union-attr]
|
|
27
|
+
elif hasattr(node, "type") and node.type: # this handles ast.ExceptHandler nodes
|
|
28
|
+
if hasattr(
|
|
29
|
+
node.type, "id"
|
|
30
|
+
): # this handles ast.ExceptHandler nodes raising a built-in error (e.g. "RuntimeError")
|
|
31
|
+
return node.type.id
|
|
32
|
+
elif hasattr(
|
|
33
|
+
node.type, "attr"
|
|
34
|
+
): # this handles ast.ExceptHandler nodes raising a non-built-in error (e.g. "pint.UndefinedUnitError")
|
|
35
|
+
return f"{node.type.value.id}.{node.type.attr}" # type: ignore[union-attr]
|
|
36
|
+
elif hasattr(
|
|
37
|
+
node, "attr"
|
|
38
|
+
): # this handles situations where the exception being raised is an ast.Attribute (e.g. "pint.UndefinedUnitError")
|
|
39
|
+
return f"{node.value.id}.{node.attr}" # type: ignore[union-attr]
|
|
40
|
+
elif hasattr(node, "id"): # this handles situations where the exception being raised is an ast.Name (e.g. "e")
|
|
41
|
+
return node.id
|
|
42
|
+
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def python_ast_raise_name(node: ast.Raise) -> Optional[str]:
|
|
47
|
+
"""Get the name of the exception raise by the given ast.Raise object."""
|
|
48
|
+
return _python_ast_exception_name(node)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def python_ast_exception_handler_exceptions_handled(handler: ast.ExceptHandler) -> Optional[Iterable[str]]: # type: ignore[return]
|
|
52
|
+
"""Return all of the exceptions handled by the given exception handler."""
|
|
53
|
+
handler_has_multiple_exceptions = handler.type and hasattr(handler.type, "elts")
|
|
54
|
+
if handler_has_multiple_exceptions:
|
|
55
|
+
yield from (_python_ast_exception_name(i) for i in handler.type.elts) # type: ignore[union-attr]
|
|
56
|
+
else:
|
|
57
|
+
exception_name = _python_ast_exception_name(handler)
|
|
58
|
+
if exception_name:
|
|
59
|
+
yield exception_name
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def python_ast_exception_handler_exceptions_raised(handler: ast.ExceptHandler) -> Optional[Iterable[str]]: # type: ignore[return]
|
|
63
|
+
"""Return the exception raised by the given exception handler."""
|
|
64
|
+
raise_nodes = python_ast_objects_of_type(handler, ast.Raise)
|
|
65
|
+
exceptions_names = list(map(python_ast_raise_name, raise_nodes)) # type: ignore[arg-type]
|
|
66
|
+
for name in exceptions_names:
|
|
67
|
+
if name and name == handler.name:
|
|
68
|
+
exceptions_names = iterable_replace(
|
|
69
|
+
exceptions_names, name, python_ast_exception_handler_exceptions_handled(handler)
|
|
70
|
+
)
|
|
71
|
+
elif name is None:
|
|
72
|
+
exceptions_names = iterable_replace(
|
|
73
|
+
exceptions_names, name, python_ast_exception_handler_exceptions_handled(handler)
|
|
74
|
+
)
|
|
75
|
+
yield from more_itertools.collapse(exceptions_names, base_type=str)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def python_exceptions_handled(code_text: str) -> Iterable[str]:
|
|
79
|
+
"""Return a list of all exceptions handled in the given code."""
|
|
80
|
+
ast_except_handlers = python_ast_objects_of_type(code_text, ast.ExceptHandler)
|
|
81
|
+
yield from more_itertools.collapse(
|
|
82
|
+
list(map(python_ast_exception_handler_exceptions_handled, ast_except_handlers)), # type: ignore[arg-type]
|
|
83
|
+
base_type=str,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def python_exceptions_raised(code_text: str) -> Iterable[str]:
|
|
88
|
+
"""Return a list of all exceptions raised in the given code."""
|
|
89
|
+
parsed_code = python_ast_parse(code_text)
|
|
90
|
+
|
|
91
|
+
ast_except_handlers = python_ast_objects_of_type(parsed_code, ast.ExceptHandler)
|
|
92
|
+
exceptions = list(map(python_ast_exception_handler_exceptions_raised, ast_except_handlers)) # type: ignore[arg-type]
|
|
93
|
+
|
|
94
|
+
# remove all of the ast.ExceptHandlers so exceptions are not parsed twice...
|
|
95
|
+
# (once from the code above and once in the code below)
|
|
96
|
+
nodes = python_ast_objects_not_of_type(parsed_code, ast.ExceptHandler)
|
|
97
|
+
exceptions.extend(list(map(python_ast_raise_name, (node for node in nodes if isinstance(node, ast.Raise)))))
|
|
98
|
+
|
|
99
|
+
yield from more_itertools.collapse(exceptions, base_type=str)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def python_functions_as_import_string(code_text: str, module_name: str) -> str:
|
|
103
|
+
"""."""
|
|
104
|
+
import jinja2
|
|
105
|
+
|
|
106
|
+
function_names = python_function_names(code_text)
|
|
107
|
+
template = """from {{ module_name }} import (
|
|
108
|
+
{%- for name in function_names %}
|
|
109
|
+
{{ name }},
|
|
110
|
+
{%- endfor %}
|
|
111
|
+
)"""
|
|
112
|
+
template = jinja2.Template(template) # type: ignore[assignment]
|
|
113
|
+
result = template.render(module_name=module_name, function_names=function_names) # type: ignore[attr-defined]
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def python_ast_object_line_number(ast_object: object) -> Optional[int]:
|
|
118
|
+
"""."""
|
|
119
|
+
if hasattr(ast_object, "lineno"):
|
|
120
|
+
return ast_object.lineno
|
|
121
|
+
else:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def python_ast_object_line_numbers(ast_object: object) -> Tuple[int, int]:
|
|
126
|
+
"""."""
|
|
127
|
+
from d8s_algorithms import depth_first_traverse
|
|
128
|
+
|
|
129
|
+
line_numbers = tuple(
|
|
130
|
+
truthy_items(
|
|
131
|
+
list(
|
|
132
|
+
depth_first_traverse(
|
|
133
|
+
ast_object, ast.iter_child_nodes, collect_items_function=python_ast_object_line_number
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
return min(line_numbers), max(line_numbers)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _python_ast_clean(code_text: str) -> str:
|
|
142
|
+
"""."""
|
|
143
|
+
import re
|
|
144
|
+
|
|
145
|
+
return re.sub("\n", "\\\\n", code_text)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# TODO: have a decorator to parse a first argument that is a string
|
|
149
|
+
def python_ast_objects_of_type( # noqa: CCR001
|
|
150
|
+
code_text_or_ast_object: Union[str, object], ast_type: type, *, recursive_search: bool = True
|
|
151
|
+
) -> Iterable[object]:
|
|
152
|
+
"""Return all of the ast objects of the given ast_type in the code_text_or_ast_object."""
|
|
153
|
+
if isinstance(code_text_or_ast_object, str):
|
|
154
|
+
parsed_code = python_ast_parse(code_text_or_ast_object)
|
|
155
|
+
else:
|
|
156
|
+
parsed_code = code_text_or_ast_object # type: ignore[assignment]
|
|
157
|
+
|
|
158
|
+
if recursive_search:
|
|
159
|
+
yield from (node for node in ast.walk(parsed_code) if isinstance(node, ast_type))
|
|
160
|
+
else:
|
|
161
|
+
if isinstance(parsed_code, ast_type):
|
|
162
|
+
yield parsed_code
|
|
163
|
+
|
|
164
|
+
if hasattr(parsed_code, "body"):
|
|
165
|
+
yield from (node for node in parsed_code.body if isinstance(node, ast_type))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def python_ast_objects_not_of_type(code_text_or_ast_object: Union[str, object], ast_type: type) -> Iterable[object]:
|
|
169
|
+
"""Return all of the ast objects which are not of the given ast_type in the code_text_or_ast_object."""
|
|
170
|
+
from d8s_algorithms import depth_first_traverse
|
|
171
|
+
|
|
172
|
+
if isinstance(code_text_or_ast_object, str):
|
|
173
|
+
parsed_code = python_ast_parse(code_text_or_ast_object)
|
|
174
|
+
else:
|
|
175
|
+
parsed_code = code_text_or_ast_object # type: ignore[assignment]
|
|
176
|
+
|
|
177
|
+
ast_objects_not_of_type = truthy_items(
|
|
178
|
+
list(
|
|
179
|
+
depth_first_traverse(
|
|
180
|
+
parsed_code,
|
|
181
|
+
lambda x: ast.iter_child_nodes(x) if not isinstance(x, ast_type) else [],
|
|
182
|
+
collect_items_function=lambda x: x,
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
return ast_objects_not_of_type
|
|
187
|
+
|
|
188
|
+
# ast_objects_not_of_type = (node for node in ast.walk(parsed_code) if not isinstance(node, ast_type))
|
|
189
|
+
# for node in ast.walk(parsed_code):
|
|
190
|
+
# if not isinstance(node, ast_type):
|
|
191
|
+
# yield node
|
|
192
|
+
# else:
|
|
193
|
+
# break
|
|
194
|
+
|
|
195
|
+
# return ast_objects_not_of_type
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def python_ast_parse(code_text: str) -> ast.Module:
|
|
199
|
+
"""."""
|
|
200
|
+
try:
|
|
201
|
+
parsed_code = ast.parse(code_text)
|
|
202
|
+
except Exception: # pylint: disable=W0703
|
|
203
|
+
code_text = _python_ast_clean(code_text)
|
|
204
|
+
parsed_code = ast.parse(code_text)
|
|
205
|
+
return parsed_code
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def python_ast_function_defs(code_text: str, recursive_search: bool = True) -> Iterable[ast.FunctionDef]:
|
|
209
|
+
"""."""
|
|
210
|
+
yield from python_ast_objects_of_type(code_text, ast.FunctionDef, recursive_search=recursive_search) # type: ignore[misc]
|
|
211
|
+
yield from python_ast_objects_of_type(code_text, ast.AsyncFunctionDef, recursive_search=recursive_search) # type: ignore[misc]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def python_function_arguments(function_text: str) -> List[ast.arg]:
|
|
215
|
+
"""."""
|
|
216
|
+
parsed_code = python_ast_parse(function_text)
|
|
217
|
+
args = parsed_code.body[0].args.args # type: ignore[attr-defined]
|
|
218
|
+
return args
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def python_function_argument_names(function_text: str) -> Iterable[str]:
|
|
222
|
+
"""."""
|
|
223
|
+
argument_names = (arg.arg for arg in python_function_arguments(function_text))
|
|
224
|
+
return argument_names
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def python_function_argument_defaults(function_text: str) -> List[str]:
|
|
228
|
+
"""."""
|
|
229
|
+
# TODO: this function does not return defaults for keyword args
|
|
230
|
+
parsed_code = python_ast_parse(function_text)
|
|
231
|
+
return parsed_code.body[0].args.defaults # type: ignore[attr-defined]
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def python_function_argument_annotations(function_text: str) -> List[str]:
|
|
235
|
+
"""."""
|
|
236
|
+
annotations = []
|
|
237
|
+
args = python_function_arguments(function_text)
|
|
238
|
+
for arg in args:
|
|
239
|
+
if arg.annotation:
|
|
240
|
+
annotations.append(arg.annotation.id) # type: ignore[attr-defined]
|
|
241
|
+
else:
|
|
242
|
+
annotations.append(None)
|
|
243
|
+
return annotations
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def python_function_names(
|
|
247
|
+
code_text: str, *, ignore_private_functions: bool = False, ignore_nested_functions: bool = False
|
|
248
|
+
) -> List[str]:
|
|
249
|
+
"""."""
|
|
250
|
+
function_objects = python_ast_function_defs(code_text, recursive_search=not ignore_nested_functions)
|
|
251
|
+
function_names = [f.name for f in function_objects]
|
|
252
|
+
if ignore_private_functions:
|
|
253
|
+
function_names = [name for name in function_names if not name.startswith("_")]
|
|
254
|
+
return function_names
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def python_function_docstrings(
|
|
258
|
+
code_text: str, *, ignore_private_functions: bool = False, ignore_nested_functions: bool = False
|
|
259
|
+
) -> List[str]:
|
|
260
|
+
"""Get docstrings for all of the functions in the given text."""
|
|
261
|
+
function_objects = python_ast_function_defs(code_text, recursive_search=not ignore_nested_functions)
|
|
262
|
+
docstrings = [
|
|
263
|
+
ast.get_docstring(f) for f in function_objects if not (ignore_private_functions and f.name.startswith("_"))
|
|
264
|
+
]
|
|
265
|
+
return docstrings # type: ignore[return-value]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def python_variable_names(code_text: str) -> List[str]:
|
|
269
|
+
"""Get all of the variables names in the code_text."""
|
|
270
|
+
# TODO: add a caveat that this function will only find *stored* variables and not those which are referenced or...
|
|
271
|
+
# loaded. E.g., given "x = y + 1", this function will return ["x"]; note that "y" is not included
|
|
272
|
+
parsed_code = python_ast_parse(code_text)
|
|
273
|
+
variable_names = [
|
|
274
|
+
node.id for node in ast.walk(parsed_code) if isinstance(node, ast.Name) and (isinstance(node.ctx, ast.Store))
|
|
275
|
+
]
|
|
276
|
+
return variable_names
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def python_constants(code_text: str) -> List[str]:
|
|
280
|
+
"""Get all constants in the code_text."""
|
|
281
|
+
# TODO: add a caveat that this function will only find *stored* variables which are uppercased
|
|
282
|
+
variables = python_variable_names(code_text)
|
|
283
|
+
constants = [var for var in variables if var.isupper()]
|
|
284
|
+
return constants
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
from ast import Import, ImportFrom
|
|
5
|
+
from typing import Any, Dict, Iterator, List, Union
|
|
6
|
+
|
|
7
|
+
from .ast_data import python_ast_objects_of_type
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# @decorators.map_firstp_arg
|
|
11
|
+
def python_functions_signatures(
|
|
12
|
+
code_text: str,
|
|
13
|
+
*,
|
|
14
|
+
ignore_private_functions: bool = False,
|
|
15
|
+
ignore_nested_functions: bool = False,
|
|
16
|
+
keep_function_name: bool = False,
|
|
17
|
+
) -> List[str]:
|
|
18
|
+
"""Return the function signatures for all of the functions in the given code_text."""
|
|
19
|
+
from d8s_strings import string_remove_from_start
|
|
20
|
+
|
|
21
|
+
from .ast_data import python_function_names
|
|
22
|
+
|
|
23
|
+
signatures = []
|
|
24
|
+
|
|
25
|
+
function_names = python_function_names(
|
|
26
|
+
code_text, ignore_private_functions=ignore_private_functions, ignore_nested_functions=ignore_nested_functions
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
for name in function_names:
|
|
30
|
+
regex_for_signature = rf"(def {name}\((?:.|\s)*?\).*?):"
|
|
31
|
+
sig = re.findall(regex_for_signature, code_text)
|
|
32
|
+
if any(sig):
|
|
33
|
+
new_sig = string_remove_from_start(sig[0], "def ")
|
|
34
|
+
if not keep_function_name:
|
|
35
|
+
new_sig = string_remove_from_start(new_sig, name)
|
|
36
|
+
signatures.append(new_sig)
|
|
37
|
+
else:
|
|
38
|
+
message = f"Unable to find signature for the {name} function"
|
|
39
|
+
print(message)
|
|
40
|
+
signatures.append(None)
|
|
41
|
+
|
|
42
|
+
return signatures
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def python_todos(code_text: str, todo_regex: str = "TODO:.*") -> List[str]:
|
|
46
|
+
"""Return all todos in the given code_text that match the given todo_regex."""
|
|
47
|
+
todos = re.findall(todo_regex, code_text)
|
|
48
|
+
return todos
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# @decorators.map_first_arg
|
|
52
|
+
def python_make_pythonic(name: str) -> str:
|
|
53
|
+
"""Make the name pythonic.
|
|
54
|
+
|
|
55
|
+
(e.g. 'fooBar' => 'foo_bar', 'foo-bar' => 'foo_bar', 'foo bar' => 'foo_bar', 'Foo Bar' => 'foo_bar').
|
|
56
|
+
"""
|
|
57
|
+
from d8s_strings import lowercase, snake_case, string_split_on_uppercase
|
|
58
|
+
|
|
59
|
+
split_string = "_".join(
|
|
60
|
+
[
|
|
61
|
+
string.strip()
|
|
62
|
+
for string in string_split_on_uppercase(name, include_uppercase_characters=True, split_acronyms=False)
|
|
63
|
+
]
|
|
64
|
+
)
|
|
65
|
+
split_string = lowercase(split_string)
|
|
66
|
+
result = snake_case(split_string)
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# @decorators.map_first_arg
|
|
71
|
+
def python_namespace_has_argument(namespace: argparse.Namespace, argument_name: str) -> bool:
|
|
72
|
+
"""."""
|
|
73
|
+
result = argument_name in namespace
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# @decorators.map_first_arg
|
|
78
|
+
def python_traceback_prettify(traceback: str) -> str:
|
|
79
|
+
"""Return a string with the given traceback pretty-printed."""
|
|
80
|
+
pretty_traceback = re.sub(" File ", "\nFile ", traceback)
|
|
81
|
+
|
|
82
|
+
return pretty_traceback
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# @decorators.map_first_arg
|
|
86
|
+
def python_traceback_pretty_print(traceback: str) -> None:
|
|
87
|
+
"""Return a string with the given traceback pretty-printed."""
|
|
88
|
+
prettified_string = python_traceback_prettify(traceback)
|
|
89
|
+
print(prettified_string)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# @decorators.map_first_arg
|
|
93
|
+
def python_clean(code_text: str) -> str:
|
|
94
|
+
"""Clean python code as it is often found in documentation and snippets."""
|
|
95
|
+
code_text = code_text.replace(">>> ", "")
|
|
96
|
+
code_text = code_text.replace("... ", "")
|
|
97
|
+
return code_text
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def python_function_blocks( # noqa: CCR001
|
|
101
|
+
code_text: str, *, ignore_private_functions: bool = False, ignore_nested_functions: bool = False
|
|
102
|
+
) -> List[str]:
|
|
103
|
+
"""Find the code (as a string) for every function in the given code_text."""
|
|
104
|
+
from d8s_lists import has_index
|
|
105
|
+
from d8s_strings import string_chars_at_start_len
|
|
106
|
+
|
|
107
|
+
from .ast_data import python_ast_function_defs, python_ast_object_line_numbers
|
|
108
|
+
|
|
109
|
+
function_block_strings = []
|
|
110
|
+
code_text_as_lines = code_text.splitlines()
|
|
111
|
+
ast_function_defs = python_ast_function_defs(code_text, recursive_search=not ignore_nested_functions)
|
|
112
|
+
function_block_line_numbers = [(f.name, python_ast_object_line_numbers(f)) for f in ast_function_defs]
|
|
113
|
+
|
|
114
|
+
for function_name, (start, end) in function_block_line_numbers:
|
|
115
|
+
function_block_lines = code_text_as_lines[start - 1 : end] # noqa=E203
|
|
116
|
+
function_block_string = "\n".join(function_block_lines)
|
|
117
|
+
|
|
118
|
+
if ignore_private_functions:
|
|
119
|
+
if function_name.startswith("_"):
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
# the code below checks to see if the line after what was determined to be the last line of the function...
|
|
123
|
+
# should also be included in the function block (which is the case when the closing parenthesis of a...
|
|
124
|
+
# function call in another function is on a newline (see the...
|
|
125
|
+
# python_data_tests.py::test_python_function_blocks_edge_cases_1 for an example))
|
|
126
|
+
if has_index(code_text_as_lines, end):
|
|
127
|
+
# find the indentation level of the function definition (the first line of the function)
|
|
128
|
+
function_indentation = string_chars_at_start_len(function_block_lines[0], " ")
|
|
129
|
+
# TODO: the check below assumes that spaces are used instead of tabs
|
|
130
|
+
next_line_is_indented = (
|
|
131
|
+
code_text_as_lines[end].startswith(" ")
|
|
132
|
+
and string_chars_at_start_len(code_text_as_lines[end], " ") > function_indentation
|
|
133
|
+
)
|
|
134
|
+
next_line_has_only_parenthesis = code_text_as_lines[end].strip(" ") == ")"
|
|
135
|
+
if next_line_is_indented and next_line_has_only_parenthesis:
|
|
136
|
+
function_block_string += f"\n{code_text_as_lines[end]}"
|
|
137
|
+
function_block_strings.append(function_block_string)
|
|
138
|
+
return function_block_strings
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def python_line_count(python_code: str, *, ignore_empty_lines: bool = True) -> int:
|
|
142
|
+
"""Return the number of lines in the given function_text."""
|
|
143
|
+
from d8s_lists import truthy_items
|
|
144
|
+
|
|
145
|
+
lines = python_code.splitlines()
|
|
146
|
+
if ignore_empty_lines:
|
|
147
|
+
return len(tuple(truthy_items(lines)))
|
|
148
|
+
else:
|
|
149
|
+
return len(lines)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def python_function_lengths(code_text: str) -> List[int]:
|
|
153
|
+
"""Find the lengths of each function in the given code_text."""
|
|
154
|
+
function_blocks = python_function_blocks(code_text)
|
|
155
|
+
return [python_line_count(function_block) for function_block in function_blocks]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def python_version() -> str:
|
|
159
|
+
"""Return the python version of the current environment."""
|
|
160
|
+
return "{}.{}.{}".format(sys.version_info.major, sys.version_info.minor, sys.version_info.micro)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def python_is_version_2() -> bool:
|
|
164
|
+
"""Return whether or not the python version of the current environment is v2.x."""
|
|
165
|
+
return python_version().startswith("2.")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def python_is_version_3() -> bool:
|
|
169
|
+
"""Return whether or not the python version of the current environment is v3.x."""
|
|
170
|
+
return not python_is_version_2()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# TODO: need to standardize the order of arguments between functions like this an the directorySearch function
|
|
174
|
+
# @decorators.map_first_arg
|
|
175
|
+
def python_files_using_function(function_name: str, search_path: str) -> List[str]:
|
|
176
|
+
"""Find where the given function is used in the given search path."""
|
|
177
|
+
from d8s_file_system import directory_read_files_with_path_matching
|
|
178
|
+
|
|
179
|
+
files_using_function = []
|
|
180
|
+
|
|
181
|
+
function_pattern = f"{function_name}("
|
|
182
|
+
python_files = directory_read_files_with_path_matching(search_path, "*.py")
|
|
183
|
+
for file_path, file_contents in python_files:
|
|
184
|
+
if function_pattern in file_contents:
|
|
185
|
+
files_using_function.append(file_path)
|
|
186
|
+
|
|
187
|
+
return files_using_function
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def python_keywords() -> List[str]:
|
|
191
|
+
"""Get a list of the python keywords."""
|
|
192
|
+
import keyword
|
|
193
|
+
|
|
194
|
+
return keyword.kwlist # type: ignore[return-value]
|
|
195
|
+
|
|
196
|
+
# if code_text is None:
|
|
197
|
+
# return python_keywords
|
|
198
|
+
# else:
|
|
199
|
+
# from d8s_strings import string_words
|
|
200
|
+
|
|
201
|
+
# words_in_code_text = string_words(code_text)
|
|
202
|
+
# keywords_used = []
|
|
203
|
+
|
|
204
|
+
# for palabra in words_in_code_text:
|
|
205
|
+
# if palabra in python_keywords:
|
|
206
|
+
# keywords_used.append(palabra)
|
|
207
|
+
|
|
208
|
+
# return keywords_used
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# @decorators.map_first_arg
|
|
212
|
+
def python_object_properties_enumerate( # noqa: CCR001
|
|
213
|
+
python_object: Any, *, run_methods: bool = True, internal_properties: bool = True
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Enumerate and print out the properties of the given object."""
|
|
216
|
+
for i in python_object.__dir__():
|
|
217
|
+
if not internal_properties:
|
|
218
|
+
if i.startswith("_"):
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
string_to_eval_as_property = "python_object.{}".format(i)
|
|
222
|
+
try:
|
|
223
|
+
if run_methods:
|
|
224
|
+
eval_result = eval(string_to_eval_as_property) # pylint: disable=W0123
|
|
225
|
+
if callable(eval_result):
|
|
226
|
+
string_to_eval_as_function = "python_object.{}()".format(i)
|
|
227
|
+
try:
|
|
228
|
+
print(f"{i}: {eval(string_to_eval_as_function)}") # pylint: disable=W0123
|
|
229
|
+
except TypeError:
|
|
230
|
+
print(f"{i}: {eval(string_to_eval_as_property)}") # pylint: disable=W0123
|
|
231
|
+
else:
|
|
232
|
+
print(f"{i}: {eval_result}")
|
|
233
|
+
else:
|
|
234
|
+
print(f"{i}: {eval(string_to_eval_as_property)}") # pylint: disable=W0123
|
|
235
|
+
except AttributeError:
|
|
236
|
+
print(f"! Unable to get the {i} attribute for the item.")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def python_copy_deep(python_object: Any) -> Any:
|
|
240
|
+
"""Return a deep (complete, recursive) copy of the given python object."""
|
|
241
|
+
import copy
|
|
242
|
+
|
|
243
|
+
return copy.deepcopy(python_object)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def python_copy_shallow(python_object: Any) -> Any:
|
|
247
|
+
"""Return shallow copy of the given python object."""
|
|
248
|
+
import copy
|
|
249
|
+
|
|
250
|
+
return copy.copy(python_object)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# @decorators.map_first_arg
|
|
254
|
+
def python_file_names(path: str, *, exclude_tests: bool = False) -> List[str]: # noqa: CCR001
|
|
255
|
+
"""Find all python files in the given directory."""
|
|
256
|
+
from d8s_file_system import directory_file_names_matching
|
|
257
|
+
|
|
258
|
+
files = directory_file_names_matching(path, "*.py")
|
|
259
|
+
|
|
260
|
+
if not exclude_tests:
|
|
261
|
+
return files
|
|
262
|
+
else:
|
|
263
|
+
non_test_files = []
|
|
264
|
+
|
|
265
|
+
for file in files:
|
|
266
|
+
if "_test" not in file and "test_" not in file:
|
|
267
|
+
non_test_files.append(file)
|
|
268
|
+
|
|
269
|
+
return non_test_files
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# @decorators.map_first_arg
|
|
273
|
+
def python_fstrings(code_text: str, *, include_braces: bool = False) -> Iterator[str]:
|
|
274
|
+
"""Find all of the python formatted string literals in the given text.
|
|
275
|
+
|
|
276
|
+
See https://realpython.com/python-f-strings/ for more details about f-strings.
|
|
277
|
+
"""
|
|
278
|
+
from d8s_grammars import python_formatted_string_literal
|
|
279
|
+
from d8s_lists import flatten
|
|
280
|
+
|
|
281
|
+
python_f_strings = flatten(python_formatted_string_literal.searchString(code_text).asList())
|
|
282
|
+
|
|
283
|
+
if not include_braces:
|
|
284
|
+
python_f_strings = (f_string.strip("{").strip("}") for f_string in python_f_strings)
|
|
285
|
+
|
|
286
|
+
return python_f_strings
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# @decorators.map_first_arg
|
|
290
|
+
def python_code_details(code_text: str):
|
|
291
|
+
"""Get details about the given code_text. This is a wrapper for `dis.code_info`"""
|
|
292
|
+
import dis
|
|
293
|
+
|
|
294
|
+
return dis.code_info(code_text)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# @decorators.map_first_arg
|
|
298
|
+
def python_disassemble(code_text: str):
|
|
299
|
+
"""Disassemble the python code_text. This is a wrapper for `dis.dis`"""
|
|
300
|
+
import dis
|
|
301
|
+
|
|
302
|
+
return dis.Bytecode(code_text).dis()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def python_stack_local_data():
|
|
306
|
+
"""Get local data in the current python environment."""
|
|
307
|
+
import inspect
|
|
308
|
+
|
|
309
|
+
# in Python 3.13+ frame.f_locals returns a write-through proxy (PEP 667) rather than a dict, so coerce to a dict
|
|
310
|
+
return dict(inspect.currentframe().f_locals)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# @decorators.map_first_arg
|
|
314
|
+
def python_object_doc_string(python_object: Any) -> Union[str, None]:
|
|
315
|
+
"""Get the doc string for the given python object (e.g. module, function, or class)."""
|
|
316
|
+
import inspect
|
|
317
|
+
|
|
318
|
+
return inspect.getdoc(python_object)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# @decorators.map_first_arg
|
|
322
|
+
def python_object_source_file(python_object: Any) -> str:
|
|
323
|
+
"""Get the source file for the given python object (e.g. module, function, or class)."""
|
|
324
|
+
import inspect
|
|
325
|
+
|
|
326
|
+
return inspect.getsourcefile(python_object) # type: ignore[return-value]
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# @decorators.map_first_arg
|
|
330
|
+
def python_object_module(python_object: Any) -> str:
|
|
331
|
+
"""Get the module for the given python object (e.g. function or class)."""
|
|
332
|
+
return python_object.__module__
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# @decorators.map_first_arg
|
|
336
|
+
def python_object_source_code(python_object: Any) -> str:
|
|
337
|
+
"""Get the source code for the given python object (e.g. module, function, or class)."""
|
|
338
|
+
import inspect
|
|
339
|
+
|
|
340
|
+
return inspect.getsource(python_object)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# @decorators.map_first_arg
|
|
344
|
+
def python_object_signature(python_object: Any) -> str:
|
|
345
|
+
"""Get the argument signature for the given python object (e.g. module, function, or class)."""
|
|
346
|
+
import inspect
|
|
347
|
+
|
|
348
|
+
return inspect.signature(python_object) # type: ignore[return-value]
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# TODO: improve the type annotations to be lists of types
|
|
352
|
+
def python_sort_type_list_by_name(python_type_list: List[type], **kwargs) -> List[type]:
|
|
353
|
+
"""."""
|
|
354
|
+
return sorted(python_type_list, key=lambda x: python_type_name(x), **kwargs) # pylint: disable=W0108
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# @decorators.map_first_arg
|
|
358
|
+
def python_type_name(python_type: type) -> str:
|
|
359
|
+
"""Return the common name of the given type."""
|
|
360
|
+
return python_type.__name__
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def python_object_type_to_word(python_object: Any) -> str:
|
|
364
|
+
"""Convert the given python type to a string."""
|
|
365
|
+
return python_type_name(type(python_object))
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _get_importfrom_module_name(node: ImportFrom) -> str:
|
|
369
|
+
"""Extract the module name from an ast.ImportFrom node.
|
|
370
|
+
|
|
371
|
+
The module name on the ast.ImportFrom node can be None for relative imports
|
|
372
|
+
In this case, this function will return the name as the dots from the import statement.
|
|
373
|
+
A few examples:
|
|
374
|
+
"from requests import get" -> "requests"
|
|
375
|
+
"from . import *" -> "."
|
|
376
|
+
"from .. import *" -> ".."
|
|
377
|
+
"from .foo import bar" -> "foo"
|
|
378
|
+
"""
|
|
379
|
+
if node.module is None:
|
|
380
|
+
module_name = "." * node.level
|
|
381
|
+
else:
|
|
382
|
+
module_name = node.module
|
|
383
|
+
|
|
384
|
+
return module_name
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def python_package_imports(code: str) -> Dict[str, List[str]]:
|
|
388
|
+
"""Return a dictionary containing the names of all imported modules."""
|
|
389
|
+
# Start with the Import nodes.
|
|
390
|
+
# These will always have an empty list of submodules
|
|
391
|
+
# so we can just overwrite them without losing any data
|
|
392
|
+
modules = dict() # type: ignore[var-annotated]
|
|
393
|
+
nodes = python_ast_objects_of_type(code, Import)
|
|
394
|
+
for node in nodes:
|
|
395
|
+
for alias in node.names: # type: ignore[attr-defined]
|
|
396
|
+
modules[alias.name] = []
|
|
397
|
+
|
|
398
|
+
# Now for the ImportFrom nodes
|
|
399
|
+
importfrom_nodes = python_ast_objects_of_type(code, ImportFrom)
|
|
400
|
+
for node in importfrom_nodes:
|
|
401
|
+
module_name = _get_importfrom_module_name(node) # type: ignore[arg-type]
|
|
402
|
+
|
|
403
|
+
for alias in node.names: # type: ignore[attr-defined]
|
|
404
|
+
modules.setdefault(module_name, []).append(alias.name)
|
|
405
|
+
|
|
406
|
+
return modules
|