Last active
June 16, 2024 19:28
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
attrs |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment