CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-makefun

Small library to dynamically create python functions.

Pending
Overview
Eval results
Files

wrapping.mddocs/

Enhanced Wrapping

Advanced function wrapping capabilities that extend functools.wraps with signature modification, parameter addition/removal, and better introspection support. These utilities provide precise control over wrapper function signatures while maintaining proper metadata inheritance.

from typing import Union, Optional, Callable, Iterable, Any
from inspect import Signature, Parameter

Capabilities

Enhanced Function Wrapping

Enhanced alternative to functools.wraps with signature modification capabilities and better introspection support.

def wraps(wrapped_fun: Callable, 
         new_sig: Optional[Union[str, Signature]] = None, 
         prepend_args: Optional[Union[str, Parameter, Iterable[Union[str, Parameter]]]] = None, 
         append_args: Optional[Union[str, Parameter, Iterable[Union[str, Parameter]]]] = None,
         remove_args: Optional[Union[str, Iterable[str]]] = None, 
         func_name: Optional[str] = None, 
         co_name: Optional[str] = None, 
         inject_as_first_arg: bool = False,
         add_source: bool = True, 
         add_impl: bool = True, 
         doc: Optional[str] = None, 
         qualname: Optional[str] = None,
         module_name: Optional[str] = None, 
         **attrs: Any) -> Callable[[Callable], Callable]:
    """
    Enhanced functools.wraps with signature modification capabilities.
    
    Parameters:
    - wrapped_fun: Callable
        Function to wrap, used as default source for metadata
    - new_sig: Optional[Union[str, Signature]], default None
        Complete replacement signature. Cannot be used with other signature modifications
    - prepend_args: Optional[Union[str, Parameter, Iterable[Union[str, Parameter]]]], default None
        Arguments to add at the beginning of wrapped_fun's signature
    - append_args: Optional[Union[str, Parameter, Iterable[Union[str, Parameter]]]], default None
        Arguments to add at the end of wrapped_fun's signature
    - remove_args: Optional[Union[str, Iterable[str]]], default None
        Argument names to remove from wrapped_fun's signature
    - func_name: Optional[str], default None
        Override function name (defaults to wrapped_fun.__name__)
    - co_name: Optional[str], default None
        Name for compiled code object
    - inject_as_first_arg: bool, default False
        Inject created function as first positional argument to wrapper
    - add_source: bool, default True
        Add __source__ attribute with generated function source
    - add_impl: bool, default True
        Add __func_impl__ attribute pointing to wrapper implementation
    - doc: Optional[str], default None
        Docstring override (defaults to wrapped_fun.__doc__)
    - qualname: Optional[str], default None
        Qualified name override (defaults to wrapped_fun.__qualname__)
    - module_name: Optional[str], default None
        Module name override (defaults to wrapped_fun.__module__)
    - **attrs: Any
        Additional attributes to set (wrapped_fun.__dict__ is automatically copied)
        
    Returns:
    Callable[[Callable], Callable]: Decorator function that takes a callable and returns a callable
    
    Note:
    Sets __wrapped__ attribute for PEP 362 compliance.
    If signature is modified, sets __signature__ attribute.
    """

Direct Wrapper Creation

Function that directly creates wrapper without requiring decorator syntax.

def create_wrapper(wrapped: Callable, 
                  wrapper: Callable, 
                  new_sig: Optional[Union[str, Signature]] = None, 
                  prepend_args: Optional[Union[str, Parameter, Iterable[Union[str, Parameter]]]] = None, 
                  append_args: Optional[Union[str, Parameter, Iterable[Union[str, Parameter]]]] = None,
                  remove_args: Optional[Union[str, Iterable[str]]] = None, 
                  func_name: Optional[str] = None, 
                  inject_as_first_arg: bool = False,
                  add_source: bool = True, 
                  add_impl: bool = True, 
                  doc: Optional[str] = None, 
                  qualname: Optional[str] = None,
                  co_name: Optional[str] = None, 
                  module_name: Optional[str] = None, 
                  **attrs: Any) -> Callable:
    """
    Creates signature-preserving wrapper function.
    Equivalent to wraps(wrapped, **kwargs)(wrapper).
    
    Parameters: Same as wraps() function
    
    Returns:
    Callable: Generated wrapper function with preserved/modified signature
    """

Usage Examples

Basic Function Wrapping

from makefun import wraps
import time

def target_function(x: int, y: str = "default") -> str:
    """Original function that does important work."""
    return f"Result: {x}, {y}"

@wraps(target_function)
def wrapper(x, y):
    print(f"Calling with x={x}, y={y}")
    result = target_function(x, y)
    print(f"Got result: {result}")
    return result

# Wrapper preserves original signature and metadata
print(wrapper.__name__)    # "target_function"
print(wrapper.__doc__)     # "Original function that does important work."
print(wrapper(42))         # Calls with proper signature

Signature Modification - Adding Parameters

from makefun import wraps
from inspect import signature

def original_func(data: list) -> int:
    """Process a list of data."""
    return len(data)

@wraps(original_func, prepend_args="verbose: bool = False", append_args="log_level: str = 'INFO'")
def enhanced_wrapper(verbose, data, log_level):
    if verbose:
        print(f"Processing {len(data)} items at {log_level} level")
    result = original_func(data)
    if verbose:
        print(f"Processed {result} items")
    return result

print(signature(enhanced_wrapper))  # (verbose: bool = False, data: list, log_level: str = 'INFO') -> int
print(enhanced_wrapper([1, 2, 3]))  # 3 (non-verbose)
print(enhanced_wrapper(True, [1, 2, 3, 4], "DEBUG"))  # Verbose output + 4

Signature Modification - Removing Parameters

from makefun import wraps

def complex_function(data: list, mode: str, debug: bool = False, timeout: int = 30) -> dict:
    """Complex function with many parameters."""
    return {
        "data_size": len(data),
        "mode": mode,
        "debug": debug,
        "timeout": timeout
    }

@wraps(complex_function, remove_args=["debug", "timeout"])
def simplified_wrapper(data, mode):
    # Always use specific values for removed parameters
    return complex_function(data, mode, debug=True, timeout=60)

# Simplified signature: (data: list, mode: str) -> dict
print(simplified_wrapper([1, 2, 3], "fast"))

Complete Signature Replacement

from makefun import wraps

def legacy_function(a, b, c=None):
    """Legacy function with old-style signature."""
    return f"Legacy: a={a}, b={b}, c={c}"

@wraps(legacy_function, new_sig="modern_api(data: dict, options: dict = None) -> str")
def modernized_wrapper(data, options):
    # Transform modern parameters to legacy format
    a = data.get("value1", 0)
    b = data.get("value2", "")
    c = options.get("extra") if options else None
    return legacy_function(a, b, c)

# New signature: (data: dict, options: dict = None) -> str
result = modernized_wrapper({"value1": 42, "value2": "hello"}, {"extra": "world"})
print(result)  # "Legacy: a=42, b=hello, c=world"

Timing Decorator Example

from makefun import wraps
import time
import functools

def timing_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} took {end_time - start_time:.4f} seconds")
        return result
    return wrapper

@timing_decorator
def slow_computation(n: int) -> int:
    """Compute sum of squares up to n."""
    return sum(i * i for i in range(n))

# Wrapper preserves original signature and metadata
print(slow_computation.__name__)  # "slow_computation"
print(slow_computation.__doc__)   # "Compute sum of squares up to n."
result = slow_computation(1000)   # Prints timing info

Caching with Parameter Modification

from makefun import wraps
import functools

def cached_function(x: int, y: int, use_cache: bool = True) -> int:
    """Expensive computation."""
    print(f"Computing {x} + {y}")
    return x + y

@wraps(cached_function, remove_args="use_cache")
@functools.lru_cache(maxsize=128)
def cached_wrapper(x, y):
    # Always use caching, remove cache control parameter
    return cached_function(x, y, use_cache=True)

# Simplified signature without use_cache parameter
print(cached_wrapper(5, 3))  # Computes and caches
print(cached_wrapper(5, 3))  # Uses cache

Validation Decorator with Parameter Addition

from makefun import wraps

def unsafe_divide(x: float, y: float) -> float:
    """Divide x by y."""
    return x / y

@wraps(unsafe_divide, append_args="validate: bool = True")
def safe_wrapper(x, y, validate):
    if validate and y == 0:
        raise ValueError("Division by zero not allowed")
    if validate and not isinstance(x, (int, float)) or not isinstance(y, (int, float)):
        raise TypeError("Arguments must be numeric")
    return unsafe_divide(x, y)

# Enhanced signature: (x: float, y: float, validate: bool = True) -> float
print(safe_wrapper(10.0, 2.0))         # 5.0
print(safe_wrapper(10.0, 2.0, False))  # 5.0 (no validation)

Multiple Parameter Modifications

from makefun import wraps
from typing import List, Optional

def basic_processor(items: List[str]) -> List[str]:
    """Basic item processing."""
    return [item.upper() for item in items]

@wraps(basic_processor, 
       prepend_args="log_prefix: str = '[PROC]'",
       append_args=["max_items: int = 100", "filter_empty: bool = True"])
def enhanced_processor(log_prefix, items, max_items, filter_empty):
    print(f"{log_prefix} Processing up to {max_items} items")
    
    if filter_empty:
        items = [item for item in items if item.strip()]
    
    items = items[:max_items]
    result = basic_processor(items)
    
    print(f"{log_prefix} Processed {len(result)} items")
    return result

# New signature: (log_prefix: str = '[PROC]', items: List[str], max_items: int = 100, filter_empty: bool = True) -> List[str]
result = enhanced_processor(items=["hello", "", "world", "test"])

Error Handling

from makefun import wraps

def target_func(x: int) -> int:
    return x * 2

# Cannot combine new_sig with other signature modifications
try:
    @wraps(target_func, new_sig="(y: int)", prepend_args="z: int")
    def invalid_wrapper(z, y):
        return target_func(y)
except ValueError as e:
    print(f"Configuration error: {e}")

# Invalid parameter name to remove
try:
    @wraps(target_func, remove_args="nonexistent_param")
    def another_wrapper(x):
        return target_func(x)
except KeyError as e:
    print(f"Parameter error: {e}")

Direct Wrapper Creation

Alternative to decorator syntax for programmatic wrapper creation:

from makefun import create_wrapper

def original(x: int, y: str) -> str:
    return f"{x}: {y}"

def my_wrapper(x, y):
    print(f"Wrapper called with {x}, {y}")
    return original(x, y)

# Create wrapper directly
wrapped_func = create_wrapper(original, my_wrapper)

# Equivalent to:
# @wraps(original)
# def wrapped_func(x, y):
#     print(f"Wrapper called with {x}, {y}")
#     return original(x, y)

print(wrapped_func(42, "hello"))

Comparison with functools.wraps

Key advantages over functools.wraps:

  1. Signature Validation: Arguments are validated against the signature before entering wrapper
  2. Signature Modification: Can add, remove, or completely replace function signatures
  3. Better Introspection: Maintains proper inspect.signature() support
  4. Keyword Arguments: Arguments passed as keywords when possible for better flexibility
  5. PEP 362 Compliance: Proper __wrapped__ and __signature__ attributes
import functools
from makefun import wraps

def original(x: int, y: str = "default") -> str:
    return f"{x}: {y}"

# functools.wraps: basic wrapping
@functools.wraps(original)
def basic_wrapper(*args, **kwargs):
    return original(*args, **kwargs)

# makefun.wraps: enhanced wrapping with signature preservation
@wraps(original)
def enhanced_wrapper(x, y):
    return original(x, y)

# enhanced_wrapper validates arguments before calling wrapper
# enhanced_wrapper receives arguments as keywords when possible
# enhanced_wrapper maintains exact signature introspection

Install with Tessl CLI

npx tessl i tessl/pypi-makefun

docs

compilation.md

decorators.md

function-creation.md

index.md

partial.md

signature-utils.md

wrapping.md

tile.json