Give Python functions your unsolicited input. cold-call
is implemented in pure
Python, fully type-annotated, and has zero runtime dependencies.
pip install cold-call
cold-call
is distributed under the terms of the
MIT license.
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 theb
argument, as thea
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
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))
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)