Skip to content

Instantly share code, notes, and snippets.

@RyanJulyan
Last active June 16, 2024 19:28
Show Gist options
  • Save RyanJulyan/4fa1a4bc58f5537b0c0fe08f2abd2db2 to your computer and use it in GitHub Desktop.
Save RyanJulyan/4fa1a4bc58f5537b0c0fe08f2abd2db2 to your computer and use it in GitHub Desktop.
This Python script uses decorators to automatically extract and store metadata about functions and classes. It captures details like function names, docstrings, parameter types, and return types, making it easier to document and understand code.
from functools import wraps
from typing import Any, List, Dict, Union, Callable, Type, Optional, get_type_hints
from dataclasses import dataclass
import inspect
import re
import json
import attr
# Global dictionary to store function and class details grouped by namespace
DETAILS: Dict[str, List[Dict[str, Any]]] = {}
def strip_class_name(class_str):
# Remove module prefix
class_str = re.sub(r'(__main__\.|typing\.)', '', class_str)
# Handle common type representations
if class_str.startswith("<class '"):
# Extract the type name from the class string
class_str = class_str[8:-2]
# Replace specific internal type representations with more readable ones
replacements = {
"inspect._empty": "Any",
"NoneType": "None",
}
for old, new in replacements.items():
class_str = class_str.replace(old, new)
# Handle Optional type
match_optional = re.match(r"Optional\[(.*)\]", class_str)
if match_optional:
return f"Optional[{strip_class_name(match_optional.group(1))}]"
# Handle other generic types
match_generic = re.match(r"(\w+)\[(.*)\]", class_str)
if match_generic:
base_type = match_generic.group(1)
inner_types = match_generic.group(2).split(', ')
stripped_inner_types = [
strip_class_name(inner) for inner in inner_types
]
return f"{base_type}[{', '.join(stripped_inner_types)}]"
return class_str
def extract_info(namespace: str = "default") -> Union[Callable, Type]:
"""Decorator factory to extract information from functions or classes."""
def decorator(obj: Union[Callable, Type]) -> Union[Callable, Type]:
if inspect.isfunction(obj):
return extract_function_info(obj, namespace)
elif inspect.isclass(obj):
return extract_class_info(obj, namespace)
return decorator
def extract_function_info(func: Callable, namespace: str) -> Callable:
"""Decorator to extract information from a function."""
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
# Extract function name
function_name = func.__name__
# Extract docstring
docstring = func.__doc__
# Extract type hints
type_hints = get_type_hints(func)
# Extract parameter names from function signature
params = inspect.signature(func).parameters
# Initialize formatted_hints dictionary
formatted_hints = {}
for name, param in params.items():
if name in type_hints:
formatted_hints[name] = strip_class_name(str(type_hints[name]))
else:
formatted_hints[name] = "Any"
# Extract return type
return_type = strip_class_name(str(type_hints.get("return", "Any")))
return_type = return_type if return_type else "Any"
# Store function details in global variable
if namespace not in DETAILS:
DETAILS[namespace] = []
DETAILS[namespace].append({
"Type": "Function",
"Name": function_name,
"Docstring": docstring,
"Parameters": formatted_hints,
"Return_Type": return_type,
})
return wrapper
def extract_class_info(cls: Type, namespace: str) -> Type:
"""Decorator to extract information from a class."""
# Extract class name
class_name = cls.__name__
# Extract docstring
class_docstring = cls.__doc__
# Extract attributes
if hasattr(cls, "__attrs_attrs__"):
class_annotations = {
k: strip_class_name(str(v))
for k, v in cls.__annotations__.items()
}
class_attrs = {
a.name: strip_class_name(str(a.type))
for a in cls.__attrs_attrs__
}
class_attributes = {**class_annotations, **class_attrs}
else:
# Extract attributes from __init__ method
init_method = cls.__init__
init_params = inspect.signature(init_method).parameters
class_init_attributes = {
name: strip_class_name(str(param.annotation))
for name, param in init_params.items() if name != "self"
}
class_attributes = {**class_init_attributes}
# Extract methods
if hasattr(cls, "__dict__"):
methods = [name for name, obj in cls.__dict__.items() if callable(obj)]
else:
methods = [name for name in dir(cls) if callable(getattr(cls, name))]
# Initialize method details list
method_details = []
for method_name in methods:
method = getattr(cls, method_name)
# Extract function name
function_name = method.__name__
# Extract docstring
docstring = method.__doc__
# Try to extract type hints and parameter names from function signature
try:
# Extract type hints
type_hints = get_type_hints(method)
# Extract parameter names from function signature
params = inspect.signature(method).parameters
# Initialize formatted_hints dictionary
formatted_hints = {}
for name, param in params.items():
if name == "self" or name == "cls":
formatted_hints[name] = cls.__name__
elif name in type_hints:
formatted_hints[name] = strip_class_name(
str(type_hints[name]))
else:
formatted_hints[name] = "Any"
# Extract return type
return_type = strip_class_name(str(type_hints.get("return",
"Any")))
return_type = return_type if return_type else "Any"
# Append method details to list
method_details.append({
"Name": function_name,
"Docstring": docstring,
"Parameters": formatted_hints,
"Return_Type": return_type,
})
except (ValueError, TypeError):
# Skip methods that do not have a signature
continue
# Store class details in global variable
if namespace not in DETAILS:
DETAILS[namespace] = []
DETAILS[namespace].append({
"Type": "Class",
"Name": class_name,
"Docstring": class_docstring,
"Attributes": class_attributes,
"Methods": methods,
"Method_Details": method_details,
})
return cls
if __name__ == "__main__":
class CustomClass:
pass
@extract_info()
def my_mixed_function(a: int, b, c: Optional[CustomClass]):
"""This is a function with mixed type hints."""
pass
@extract_info()
class NormCustomClassPropsTest:
"""test Norm Class Docstring"""
def __init__(self, name: str) -> None:
self.name: str = name
def also_show_name(self) -> str:
"""Also show the name."""
print(self.Name)
return self.Name
@extract_info(namespace="dataclass_namespace")
@dataclass
class SomeDataClass:
values: List[int]
def something(self) -> str:
return "something"
def return_values(self) -> List[int]:
return self.values
@extract_info(namespace="custom_namespace")
@attr.s
class CustomClassPropsTest:
"""Test Class Docstring"""
Name: str
example = attr.ib(type=str)
def show_name(self) -> str:
"""Show the name."""
print(self.Name)
return self.Name
extract_info(namespace="built_in_data_types")(str)
extract_info(namespace="built_in_data_types")(int)
extract_info(namespace="built_in_data_types")(float)
extract_info(namespace="built_in_data_types")(list)
extract_info(namespace="built_in_data_types")(tuple)
extract_info(namespace="built_in_data_types")(range)
extract_info(namespace="built_in_data_types")(dict)
extract_info(namespace="built_in_data_types")(set)
extract_info(namespace="built_in_data_types")(bool)
# Access details without calling the function or instantiating the class
print(json.dumps(DETAILS, indent=4))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment