`@delegates` to multiple functions and filtering what goes to each one

import inspect
from typing import Callable, List

Disclaimer: Lots of dumb things ahead (possibly).

I’m not a professional coder, so please don’t take my code as a reference. I’m just a beginner who is trying to learn and improve. I’m open to suggestions and corrections.

------------------

I was wondering if one function is going to delegate to multiple funcions …
Can we use multiple @delegates?


def my_function1(a, b=2, c=3, **kwargs):
    print(a, b, c)

def my_function2(x, y=5, z=6, c=7, **kwargs):
    print(x, y, z, c)

@delegates(my_function1)
@delegates(my_function2)
def caller_v1(**kwargs):
    my_function1(**kwargs)
    my_function2(**kwargs)

# print the caller_v1 signature
print(inspect.signature(caller_v1))
(*, y=5, z=6, c=7)

So the answer is NO. Only one (the last) @delegates is used.

------------------

# But what if we pass to delegates a lambda with all the parameters?

@delegates(lambda a, b=2,  y=5, z=6, c=7, **kwargs: None)
def caller_v2(**kwargs):
    pass

# print the caller_v2 signature
print(inspect.signature(caller_v2))
(*, b=2, y=5, z=6, c=7)

ok, it works

This function will return a lambda with the combination of parameters.
If there is a conflict it will say it, and use the value of the last one provided.

import inspect
from typing import Callable, Dict, Any

def create_lambda_with_defaults(*funcs: Callable) -> Callable:
    """
    Given one or more functions, find their arguments with default values
    and return a lambda that unites them all.
    If there is a conflict (same argument with different default values), print a warning.
    """
    defaults = {}
    conflicts = {}

    for func in funcs:
        sig = inspect.signature(func)
        for name, param in sig.parameters.items():
            if param.default is not param.empty:
                if name in defaults and defaults[name] != param.default:
                    conflicts[name] = (defaults[name], param.default)
                defaults[name] = param.default

    if conflicts:
        for arg, (val1, val2) in conflicts.items():
            print(f"Warning: Conflict for argument '{arg}' with default values {val1} and {val2}")

    # Create the lambda with explicit arguments
    arg_list = ', '.join(f'{k}={v!r}' for k, v in defaults.items())
    lambda_code = f'lambda {arg_list}: {defaults}'
    combined_lambda = eval(lambda_code)

    return combined_lambda


# Example usage:
def func1(a=1, b=2):
    pass

def func2(a=10, y=20):
    pass

combined_lambda = create_lambda_with_defaults(func1, func2)
print(combined_lambda())  # Output: {'a': 10, 'b': 2, 'y': 20}
print(combined_lambda(a=100, y=200))  # Output: {'a': 100, 'b': 2, 'y': 200}

# Print the signature of the lambda
print(inspect.signature(combined_lambda))
Warning: Conflict for argument 'a' with default values 1 and 10
{'a': 10, 'b': 2, 'y': 20}
{'a': 10, 'b': 2, 'y': 20}
(a=10, b=2, y=20)

Now, putting all together, we can use this function to create a lambda with the parameters we want to use in the function we are delegating to.

# You can do it separatedly

_to_call = create_lambda_with_defaults(my_function1, my_function2)

# print the signature of the lambda
print(inspect.signature(_to_call))
Warning: Conflict for argument 'c' with default values 3 and 7
(b=2, c=7, y=5, z=6)
# or all at once
@delegates(create_lambda_with_defaults(my_function1, my_function2))
def caller_v3(**kwargs):
    my_function1(**kwargs)
    my_function2(**kwargs)
    pass

# print the caller_v3 signature
print(inspect.signature(caller_v3))

Warning: Conflict for argument 'c' with default values 3 and 7
(*, b=2, c=7, y=5, z=6)

------------------

This call doesn’t fail because both my_function1 and my_function2 accept **kwargs

But that may not be the case.

caller_v3(a=88,x=99) 
88 2 3
99 5 6 7

def my_function1(a, b=2, c=3):  # removed  **kwargs
    print(a, b, c)


def my_function2(x, y=5, z=6, c=7):  # removed  **kwargs
    print(x, y, z, c)


@delegates(create_lambda_with_defaults(my_function1, my_function2))
def caller_v4(**kwargs):
    my_function1(**kwargs)
    my_function2(**kwargs)
    pass



# print the caller_v4 signature
print(inspect.signature(caller_v4))


caller_v4(a=88,x=99) 
Warning: Conflict for argument 'c' with default values 3 and 7
(*, b=2, c=7, y=5, z=6)



---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

Cell In[1365], line 21
     17 # print the caller_v4 signature
     18 print(inspect.signature(caller_v4))
---> 21 caller_v4(a=88,x=99) 


Cell In[1365], line 11, in caller_v4(**kwargs)
      9 @delegates(create_lambda_with_defaults(my_function1, my_function2))
     10 def caller_v4(**kwargs):
---> 11     my_function1(**kwargs)
     12     my_function2(**kwargs)
     13     pass


TypeError: my_function1() got an unexpected keyword argument 'x'

------------------

Maybe this is overengineering, but we can filter what goes to the delegated function depending on the parameters it accepts.

import inspect


def filtered_send(data: dict, recipient_function: callable) -> dict:
    """
    Filters the data dictionary to only include keys that are present in the parameter names of the recipient function,
    unless the recipient function accepts **kwargs.

    Args:
        data (dict): The dictionary containing the data to be filtered.
        recipient_function (function): The function that will receive the filtered data.

    Returns:
        dict: The filtered data dictionary.
    """
    # Get the signature of the recipient function
    signature = inspect.signature(recipient_function)

    # Check if the recipient function accepts **kwargs
    if any(param.kind == param.VAR_KEYWORD for param in signature.parameters.values()):
        return data

    # Extract the parameter names
    parameter_names = set(signature.parameters.keys())

    # Filter the data dictionary to only include keys that are in the parameter names
    filtered_data = {key: value for key,
                     value in data.items() if key in parameter_names}

    return filtered_data

def my_function1(a, b=2, c=3):  # removed  **kwargs
    print(a, b, c)


def my_function2(x, y=5, z=6, c=7):  # removed  **kwargs
    print(x, y, z, c)


@delegates(create_lambda_with_defaults(my_function1, my_function2))
def caller_v5(**kwargs):
    d1=filtered_send(kwargs,my_function1)
    my_function1(**d1)

    d2=filtered_send(kwargs,my_function2)
    my_function2(**d2)
    pass


# print the caller_v5 signature
print(inspect.signature(caller_v5))


caller_v5(a=88, x=99)
Warning: Conflict for argument 'c' with default values 3 and 7
(*, b=2, c=7, y=5, z=6)
88 2 3
99 5 6 7
# ==========================
1 Like