Skip to content

bernardcooke53/cold-call

Repository files navigation

cold-call

PyPI - Version PyPI - Python Version Black Pre-Commit Enabled Ruff

Give Python functions your unsolicited input. cold-call is implemented in pure Python, fully type-annotated, and has zero runtime dependencies.


Table of Contents

Installation

pip install cold-call

License

cold-call is distributed under the terms of the MIT license.

Usage

cold-call enables you to throw any arguments or keyword arguments that you like at an arbitrary function, and call that function using the keys which match the corresponding parameter names of the function to provide values. For example:

from cold_call import cold_call


def func(a: int, b: str) -> None:
    print(a, b)


data = {"a": 5, "b": "foo"}

# prints "5 foo"
cold_call(func, **data)

On its own, this isn't very interesting - the same can be achieved with the builtin unpack operator (**{"a": 1, ...}):

# prints "5 foo"
func(**data)

However, cold_call enables you to pass a additional keys, which aren't in the function's parameter spec:

data["c"] = 73
# prints "5 foo"
cold_call(func, **data)

# TypeError: func() got an unexpected keyword argument 'c'
func(**data)

This is similar to JavaScript's ability to destructure an object passed into the function using the function's parameter spec. The following two code examples are equivalent:

// JavaScript
const foo({name, age}) => {
    console.log(`${name}: ${age}`);
};

// prints "Joe: 30"
foo({
    name: "Joe",
    age: 30,
    birthday: "01/06/1990"
})
# Python
def foo(name: str, age: int) -> None:
    print(f"{name}: {age}")


# prints "Joe: 30"
cold_call(
    foo,
    name="Joe",
    age=30,
    birthday="01/06/1990",
)

The cold_call function can be called with positional and keyword arguments; the values of the keyword arguments are used in preference to those in the positional arguments, so if a keyword argument matches the name of a parameter that is declared in the function as positional-only, it will be used in preference to any positional arguments.

NOTE: if a parameter can be passed as either a positional or keyword argument, it will be passed to the called function positionally. This is to avoid certain edge cases where Python treats a call to a function as providing multiple values for the same parameter (see Calls).

For example:

def foo(name: str, age: int) -> None:
    print(f"{name}: {age}")


# prints "Joe: 42"
cold_call(foo, "Tim", 21, name="Joe", age=42)

Note that positional arguments to cold_call are always passed to the function positionally, so you should always prefer keyword arguments unless the function you want to call requires positional-only arguments.

Additional positional or keyword arguments to cold_call are ignored, unless the function specifies variadic positional or keyword arguments (*args or **kwargs); in this case, any "left over" positional arguments are used to fill *args, and any "left over" keyword arguments are used to fill **kwargs:

def foo(name: str, *meals: str, age: int, **attrs) -> None:
    print(f"{name}, age: {age}")
    print(f"likes: {', '.join(meals)}")
    print(attrs)


# prints:
# Joe, 42
# likes: pizza, burgers, ice-cream
# {"hobbies": ["tennis"], "city": "London"}
cold_call(
    foo,
    "Joe",
    "pizza",
    "burgers",
    "ice-cream",
    hobbies=["tennis"],
    city="London",
    age=42,
)

cold_call also works with functions that have more specific signatures:

NOTE: here 5 is used as the b argument, as the a argument is explicitly specified by keyword.

def picky(
    a: str,
    /,
    b: int,
    *,
    c: bool,
) -> int:
    print(f"{a=}, {b=}, {c=}")
    return b * 2


# prints "a=gotcha, b=5, c=False"
x = cold_call(
    picky,
    5,
    a="gotcha",
    c=False,
)
assert x == 10

ColdCaller

cold-call also provides a convenience class for use with the standard-library dataclasses. This class implements a single method, call, which allows you to run cold_call on a function with the data that the dataclass instance stores:

from dataclasses import dataclass

from cold_call import ColdCaller


def user_action(name: str) -> None:
    print(f"user {name} is doing things!")


def is_authorized(name: str, is_admin: bool) -> bool:
    if not is_admin:
        print(f"forbidden: user {name} is not an admin")
    return is_admin and name != "Steve"  # Steve is banned


@dataclass
class User(ColdCaller):
    name: str
    age: int
    is_admin: bool = False


joe = User(name="Joe", age=30)

# prints "user Joe is doing things!"
joe.call(user_action)

# prints "forbidden: user Joe is not an admin"
joe.call(is_authorized)

# prints "True"
print(joe.call(is_authorized, is_admin=True))

cold_callable

Lastly, for convenience cold-call exports a decorator, cold_callable, which can be used to wrap a function so that it can always accept arbitrary input without erroring:

from cold_call import cold_callable


@cold_callable
def foo(name: str, age: int) -> None:
    print(f"{name}: {age}")


# prints "Joe: 42"
foo("Tim", 21, name="Joe", age=42)

About

Give Python functions your unsolicited input

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages