Skip to content

Instantly share code, notes, and snippets.

@QNimbus
Last active December 18, 2023 19:19
Show Gist options
  • Save QNimbus/ff877ae2d0657d597fcd506fa41b73f2 to your computer and use it in GitHub Desktop.
Save QNimbus/ff877ae2d0657d597fcd506fa41b73f2 to your computer and use it in GitHub Desktop.
Python scripts
#!/usr/bin/env -S python -W ignore
# code_outliner_exporter.py
# Standard library imports
import os
import ast
import argparse
from pathlib import Path
# Third-party imports
# ...
# Local application/library imports
# ...
def extract_signatures(file_path: Path, exclude_docstrings: bool = False, escape_docstrings: bool = False) -> list:
"""
Extracts the function and class signatures along with their docstrings from a given file.
Args:
file_path (Path): The path to the file.
exclude_docstrings (bool): Whether to exclude docstrings or not.
escape_docstrings (bool): Whether to escape docstrings with ''' instead of triple double-quotes.
Returns:
list: A list of function and class signatures with docstrings.
"""
with open(file_path, "r", encoding="utf-8") as file:
def extract_from_node(node, class_name=None, parent_func_name=None) -> list:
signatures = []
for n in node.body:
if isinstance(n, ast.FunctionDef):
func_signature = extract_function_signature(n, class_name=class_name, parent_func_name=parent_func_name, exclude_docstrings=exclude_docstrings)
signatures.append(func_signature)
# Recursively search inside the function with the current function name as parent
signatures.extend(extract_from_node(n, class_name, parent_func_name=n.name))
elif isinstance(n, ast.ClassDef) and class_name is None: # Only top-level classes
class_header = f"class {n.name}:"
if not exclude_docstrings:
docstring = ast.get_docstring(n)
if docstring:
docstring = docstring.replace("\n", "\n ")
class_header += f'\n """\n {docstring}\n """'
signatures.append(class_header)
# Extract from class body
signatures.extend(extract_from_node(n, class_name=n.name))
if escape_docstrings:
signatures = [s.replace('"""', "'''") for s in signatures]
return signatures
# Use the recursive function
return extract_from_node(ast.parse(file.read()))
def extract_function_signature(func_def, class_name: str = None, parent_func_name: str = None, exclude_docstrings: bool = False) -> str:
"""
Extracts the signature of a function or method, including the return type if present.
Args:
func_def (ast.FunctionDef): The function definition node.
class_name (Optional[str]): The name of the class if it's a method.
parent_func_name (Optional[str]): The name of the parent function if it's a nested function.
exclude_docstrings (bool): Whether to exclude docstrings or not.
Returns:
str: The function or method signature with return type.
"""
# Process decorators
decorators = [f"@{ast.unparse(decorator)}" for decorator in func_def.decorator_list]
decorator_str = "\n".join(decorators) + "\n" if decorators else ""
# Function header construction
prefix = f"{class_name}." if class_name else ""
nested_prefix = f"{parent_func_name} > " if parent_func_name else ""
func_header = f"{nested_prefix}def {prefix}{func_def.name}("
# Parameters construction
params = [ast.unparse(arg) for arg in func_def.args.args]
func_header += ", ".join(params)
if func_def.args.vararg:
func_header += ", *" + func_def.args.vararg.arg
if func_def.args.kwonlyargs:
kwonlyargs = [ast.unparse(arg) for arg in func_def.args.kwonlyargs]
func_header += ", " + ", ".join(kwonlyargs) if params else ", ".join(kwonlyargs)
if func_def.args.kwarg:
func_header += ", **" + func_def.args.kwarg.arg
# Append return type if present
if func_def.returns:
return_type = ast.unparse(func_def.returns)
func_header += f") -> {return_type}:"
else:
func_header += "):"
# Append docstring if present
if not exclude_docstrings:
docstring = ast.get_docstring(func_def)
if docstring:
docstring = docstring.replace("\n", "\n ")
func_header += f'\n """\n {docstring}\n """'
return decorator_str + func_header
def write_to_markdown(functions, output_file: Path):
"""Write the function signatures to a Markdown file."""
with open(output_file, "w", encoding="utf-8") as file:
file.write("```python\n")
for func in functions:
file.write(f"\n{func}\n")
file.write("\n```\n\n")
def main():
"""
Extracts Python function signatures and class definitions from a given Python file and writes them to a Markdown file.
Usage:
python_file: str - Python file to extract code outline from.
output: str - Output Markdown file to write the function signatures to. If not provided, a default file name will be used.
Returns:
None
"""
parser = argparse.ArgumentParser(description="Extract Python function signatures.")
parser.add_argument("python_file", type=str, help="Python file to extract from")
parser.add_argument("-n", "--no-docstrings", action="store_true", help="Exclude docstrings")
parser.add_argument("-e", "--escape-docstrings", action="store_true", help="Escape docstrings with ''' instead of \"\"\"")
parser.add_argument("-o", "--output", type=str, help="Output Markdown file")
args = parser.parse_args()
python_file = args.python_file
output_file = args.output if args.output else os.path.splitext(python_file)[0] + "_sigs.md"
function_signatures = extract_signatures(python_file, exclude_docstrings=args.no_docstrings, escape_docstrings=args.escape_docstrings)
write_to_markdown(function_signatures, output_file)
print(f"Function signatures extracted to {output_file}")
if __name__ == "__main__":
main()
#!/usr/bin/env -S python -W ignore
# func_call_analyzer.py
# Standard library imports
import ast
import argparse
import importlib
from typing import Set, Tuple, Dict, List
# Third-party imports
# ...
# Local application/library imports
# ...
def map_imports(node) -> Dict[str, str]:
"""
Maps imported functions to their respective modules.
Args:
node: The AST node to analyze.
Returns:
Dict[str, str]: A dictionary mapping function names to module names.
"""
imports = {}
for child in ast.iter_child_nodes(node):
if isinstance(child, ast.Import):
for name in child.names:
imports[name.name] = name.name
elif isinstance(child, ast.ImportFrom):
module = child.module if child.module else ""
for name in child.names:
imports[name.name] = module
return imports
def is_builtin_function(name: str) -> bool:
"""
Check if a function name is a built-in function.
Args:
name: The name of the function.
Returns:
bool: True if it's a built-in function, False otherwise.
"""
return name in dir(__builtins__)
def find_function_calls_and_exceptions(node, internal_funcs: Set[str], imports: Dict[str, str], function_parameters: List[str]) -> Tuple[Set[str], Set[str], Set[str]]:
"""
Recursively analyzes an abstract syntax tree (AST) node to find function calls and raised exceptions.
Args:
node (ast.AST): The AST node to analyze.
internal_funcs (Set[str]): A set of internal function names.
imports (Dict[str, str]): A dictionary mapping imported function names to their respective modules.
function_parameters (List[str]): A list of function parameter names.
Returns:
Tuple[Set[str], Set[str], Set[str]]: A tuple containing three sets:
- internal_calls: Set of internal function calls found in the AST.
- external_calls: Set of external function calls found in the AST.
- raised_exceptions: Set of raised exceptions found in the AST.
"""
internal_calls = set()
external_calls = set()
raised_exceptions = set()
for child in ast.iter_child_nodes(node):
if isinstance(child, ast.FunctionDef):
# Update function parameters for this new function scope
new_function_parameters = [arg.arg for arg in child.args.args]
child_internal, child_external, child_exceptions = find_function_calls_and_exceptions(child, internal_funcs, imports, new_function_parameters)
internal_calls |= child_internal
external_calls |= child_external
raised_exceptions |= child_exceptions
elif isinstance(child, ast.Call) and isinstance(child.func, ast.Name):
if child.func.id in internal_funcs:
internal_calls.add(child.func.id)
elif child.func.id in function_parameters:
# This is a callable variable, skip it
continue
else:
if is_builtin_function(child.func.id):
external_calls.add(f"built-in.{child.func.id}")
else:
module = imports.get(child.func.id, "unknown_module")
external_calls.add(f"{module}.{child.func.id}")
elif isinstance(child, ast.Raise):
if child.exc:
if isinstance(child.exc, ast.Name):
raised_exceptions.add(child.exc.id)
elif isinstance(child.exc, ast.Call) and isinstance(child.exc.func, ast.Name):
raised_exceptions.add(child.exc.func.id)
else:
# Recursively process other nodes
child_internal, child_external, child_exceptions = find_function_calls_and_exceptions(child, internal_funcs, imports, function_parameters)
internal_calls |= child_internal
external_calls |= child_external
raised_exceptions |= child_exceptions
return internal_calls, external_calls, raised_exceptions
def analyze_function_calls(module_name: str, function_name: str) -> None:
"""
Analyzes the function calls within a module and prints the internal and external function calls of a target function.
Args:
module_name (str): The name of the module to analyze.
function_name (str): The name of the target function.
Returns:
None
"""
module = importlib.import_module(module_name)
with open(module.__file__, "r", encoding="utf-8") as file:
module_ast = ast.parse(file.read())
# Map imports
imports = map_imports(module_ast)
# Collect internal function names
internal_funcs = {node.name for node in ast.walk(module_ast) if isinstance(node, ast.FunctionDef)}
# Find the target function
target_func = next((node for node in ast.walk(module_ast) if isinstance(node, ast.FunctionDef) and node.name == function_name), None)
if not target_func:
print(f"Function '{function_name}' not found in the module.")
return
# Find function calls and raised exceptions
internal_calls, external_calls, raised_exceptions = find_function_calls_and_exceptions(target_func, internal_funcs, imports, [])
# Print internal function calls in bulleted list format
print(f"Internal function calls in '{function_name}':")
for call in internal_calls:
print(f"- {call}")
# Print external function calls in bulleted list format
print(f"External function calls in '{function_name}':")
for call in external_calls:
print(f"- {call}")
print(f"Exceptions raised in '{function_name}':")
for exception in raised_exceptions:
print(f"- {exception}")
def main():
parser = argparse.ArgumentParser(description="Analyze function calls within a specified function of a module.")
parser.add_argument("module_path", type=str, help="Path to the module")
parser.add_argument("function_name", type=str, help="Function name to analyze")
args = parser.parse_args()
analyze_function_calls(args.module_path, args.function_name)
if __name__ == "__main__":
main()
# python_patterns_settings.py.py
# Standard library imports
import os
from functools import lru_cache
from pydantic import BaseSettings
# Third-party imports
# ...
# Local application/library imports
# ...
class Settings(BaseSettings):
"""
Settings class for this application.
Utilizes the BaseSettings from pydantic for environment variables.
"""
openai_api_key: str
class Config:
env_file = ".env"
@lru_cache()
def get_settings():
"""Function to get and cache settings.
The settings are cached to avoid repeated disk I/O.
"""
return Settings()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment