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 ADDED
@@ -0,0 +1,6 @@
1
+ __version__ = "0.10.0"
2
+ __author__ = """Floyd Hightower"""
3
+ __email__ = "floyd.hightower27@gmail.com"
4
+
5
+ from .ast_data import *
6
+ from .python_data import *
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