Small library to dynamically create python functions.
—
Decorators for modifying function signatures while preserving metadata and introspection capabilities. These decorators provide enhanced alternatives to standard Python decorators with precise signature control and comprehensive metadata management.
from typing import Union, Optional, Callable, Any
from inspect import SignatureDecorator that changes function signatures, equivalent to create_function but applied as a decorator for cleaner syntax.
def with_signature(func_signature: Union[str, Signature, 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[[Callable], Callable]:
"""
Decorator to change function signature.
Parameters:
- func_signature: Union[str, Signature, None]
New signature specification. Can be:
- String without 'def': "foo(a, b: int, *args, **kwargs)"
- Signature object from inspect.signature()
- None for metadata-only changes (no signature modification)
- func_name: Optional[str], default None
Override for __name__ and __qualname__ attributes
- inject_as_first_arg: bool, default False
If True, inject created function as first positional argument
- add_source: bool, default True
Add __source__ attribute containing generated function source
- add_impl: bool, default True
Add __func_impl__ attribute pointing to original function
- doc: Optional[str], default None
Docstring for generated function. Defaults to original function's doc
- qualname: Optional[str], default None
Qualified name for generated function
- co_name: Optional[str], default None
Name for compiled code object
- module_name: Optional[str], default None
Module name for generated function
- **attrs: Any
Additional attributes to set on generated function
Returns:
Callable[[Callable], Callable]: Decorator function that takes a callable and returns a callable
Note:
When func_signature=None, only metadata changes are applied without
creating a wrapper. In this case, add_source, add_impl, and
inject_as_first_arg should not be used.
"""from makefun import with_signature
@with_signature("greet(name: str, age: int = 25)")
def hello(name, age):
return f"Hello {name}, you are {age} years old"
# Function now has the specified signature
print(hello("Alice")) # "Hello Alice, you are 25 years old"
print(hello("Bob", 30)) # "Hello Bob, you are 30 years old"
# Signature is properly reflected in introspection
from inspect import signature
print(signature(hello)) # (name: str, age: int = 25)from makefun import with_signature
from inspect import signature
def template_func(x: int, y: str, z: float = 1.0) -> str:
pass
@with_signature(signature(template_func))
def my_func(x, y, z):
return f"x={x}, y={y}, z={z}"
print(my_func(42, "hello")) # "x=42, y=hello, z=1.0"from makefun import with_signature
@with_signature(None,
func_name="enhanced_processor",
doc="Enhanced data processing function",
module_name="data_utils",
version="2.0")
def process_data(data):
"""Original docstring"""
return f"Processing {len(data)} items"
print(process_data.__name__) # "enhanced_processor"
print(process_data.__doc__) # "Enhanced data processing function"
print(process_data.__module__) # "data_utils"
print(process_data.version) # "2.0"from makefun import with_signature
# Keyword-only parameters
@with_signature("analyze(data: list, *, method: str, verbose: bool = False)")
def data_analyzer(data, method, verbose):
result = f"Analyzing {len(data)} items using {method}"
if verbose:
result += " (verbose mode)"
return result
print(data_analyzer([1, 2, 3], method="statistical"))
# Positional-only parameters (Python 3.8+)
@with_signature("compute(x: float, y: float, /, mode: str = 'fast') -> float")
def calculator(x, y, mode):
if mode == "fast":
return x + y
else:
return x * y + y * x
print(calculator(2.5, 3.5)) # 6.0
# Variable arguments
@with_signature("handler(*args, **kwargs) -> dict")
def request_handler(*args, **kwargs):
return {"args": args, "kwargs": kwargs}
print(request_handler(1, 2, 3, name="test", value=42))from makefun import with_signature
# Function name in signature overrides original name
@with_signature("custom_name(value: str) -> str")
def original_name(value):
return f"Processed: {value}"
print(original_name.__name__) # "custom_name"
print(original_name("test")) # "Processed: test"from makefun import with_signature
@with_signature("api_call(endpoint: str, data: dict = None, timeout: int = 30)",
func_name="make_api_call",
doc="Make an HTTP API call with timeout and error handling",
qualname="APIClient.make_api_call",
module_name="api_client",
add_source=True,
add_impl=True,
api_version="v1.2",
deprecated=False)
def http_request(endpoint, data, timeout):
return f"Calling {endpoint} with data={data}, timeout={timeout}"
# All metadata is properly set
print(http_request.__name__) # "make_api_call"
print(http_request.__qualname__) # "APIClient.make_api_call"
print(http_request.__module__) # "api_client"
print(http_request.api_version) # "v1.2"
print(http_request.deprecated) # False
# Source code is available
print(http_request.__source__) # Generated function source
print(http_request.__func_impl__) # Original function referencefrom makefun import with_signature
import asyncio
@with_signature("generate_numbers(start: int, end: int, step: int = 1)")
def number_generator(start, end, step):
for i in range(start, end, step):
yield f"Number: {i}"
# Generator signature is preserved
for num in number_generator(1, 5):
print(num)
@with_signature("fetch_data(url: str, headers: dict = None) -> dict")
async def async_fetcher(url, headers):
await asyncio.sleep(0.1) # Simulate network call
return {"url": url, "headers": headers or {}, "status": "success"}
# Async function signature is preserved
result = asyncio.run(async_fetcher("https://api.example.com"))
print(result)from makefun import with_signature
# Invalid usage: metadata-only mode with add_source=False conflicts
try:
@with_signature(None, add_source=False, add_impl=False, inject_as_first_arg=True)
def invalid_func():
pass
except ValueError as e:
print(f"Configuration error: {e}")
# Invalid signature format
try:
@with_signature("invalid syntax here")
def bad_signature():
pass
except SyntaxError as e:
print(f"Signature error: {e}")The @with_signature decorator is equivalent to create_function but provides cleaner syntax:
from makefun import with_signature, create_function
# These are equivalent:
# Using decorator
@with_signature("func(x: int, y: str) -> str")
def my_func1(x, y):
return f"{x}: {y}"
# Using create_function
def my_func2_impl(x, y):
return f"{x}: {y}"
my_func2 = create_function("func(x: int, y: str) -> str", my_func2_impl)The generated functions work properly with static type checkers like mypy:
from makefun import with_signature
from typing import List, Optional
@with_signature("process_items(items: List[str], filter_empty: bool = True) -> List[str]")
def item_processor(items, filter_empty):
if filter_empty:
return [item for item in items if item.strip()]
return items
# Type checker understands the signature
result: List[str] = item_processor(["hello", "", "world"]) # Type-safeInstall with Tessl CLI
npx tessl i tessl/pypi-makefun