Usage#

This section contains a quick usage guide to both aspects of incant. incant methods fall broadly in two API layers, called the convenience layer and the power layer. The convenience layer functionality will let you get started quickly and elegantly, while the power layer will unlock the most customizability and performance.

Function Composition#

Function composition is the act of combining functions in various ways. Dependency injection is a very common example of function composition, and applying decorators to functions in Python is another.

In incant, you define rules which determine how incant chooses which functions to use for compositions. These rules are then applied to a function, producing another function which encapsulates the given function and all its dependent functions (called dependencies).

This state (in the form of dependency factories) is kept in an instance of Incanter.

from incant import Incanter

incanter = Incanter()

The incanter can now be used to compose and call functions (Incanter.compose_and_call()) and coroutines (Incanter.acompose_and_call()). Since there are no rules (dependency factories) registered yet, incanter.compose_and_call(fn, a, b, c) does nothing and is exactly equivalent to fn(a, b, c).

def my_function(my_argument):
    print(f"Called with {my_argument}")

incanter.compose_and_call(my_function, 1)  # Equivalent to my_function(1)
'Called with 1'

The simplest way to create a rule (or: register a dependency factory) is by name:

@incanter.register_by_name
def my_argument():
    return 2

This is a convenient way of making incant substitute any argument named my_argument with the result of the my_argument dependency factory. Now we don’t need to pass in the argument, incant will generate a new function under the hood doing the wiring for us.

incanter.compose_and_call(my_function)
'Called with 2'

This is equivalent to the following code:

def my_function_composed():
    return my_function(my_argument())

my_function_composed()

Another simple way to register a dependency factory is by its return type:

@incanter.register_by_type
def another_factory(my_argument) -> int:
    return my_argument + 1

def another_function(takes_int: int):
    print(f"Called with {takes_int}")

incanter.compose_and_call(another_function)
'Called with 3'

Note

Incanter.compose_and_call(), Incanter.acompose_and_call(), Incanter.register_by_name() and Incanter.register_by_type() are part of the convenience layer.

Dependency factories may themselves have dependencies provided to them, as shown in the above example. incant performs a depth-first pass of gathering nested dependencies.

Incanter.compose_and_call() uses Incanter.compose() internally. compose() does the actual heavy lifting of creating and caching a wrapper with the dependencies processed and composed. It’s useful for getting the wrappers for caching or inspection - the wrappers support ordinary Python introspection using the standard library inspect module.

compose() also allows customizing the wrapper without adding hooks to the actual Incanter.

from incant import Hook

@incanter.register_by_name
def my_argument():
    return 2

def my_function(my_argument):
    print(f"Called with {my_argument}")

>>> incanter.compose_and_call(my_function)
2

>>> incanter.compose(lambda: my_argument)()  # Equivalent.
2

>>> incanter.compose(lambda: my_argument, [Hook.for_name("my_argument", lambda: 1)])()
1

The hook argument is a sequence of hooks, which are a predicate function and dependency factory. Also be aware that since in Python lambdas don’t play well with caching, if you’re preparing functions with hook overrides often you will want to store the actual overrides somewhere and reuse them.

# Inefficient:
>>> incanter.compose(lambda: my_argument, [Hook.for_name("my_argument", lambda: 1)])()

# Efficient:
>>> additional_hooks = [Hook.for_name("my_argument", lambda: 1)] # Store this and reuse it.

>>> incanter.compose(lambda: my_argument, additional_hooks)()  # Now uses the cache.

Note

Incanter.compose() is part of the power layer.

Function Invocation#

Incanter instances also have helper methods, Incanter.incant() and Incanter.aincant(), that serve as a smart helper for calling functions. incant() filters out unnecessary arguments before calling the given function, and is a useful tool for building generic components. incant() also composes nicely with compose(), where you can prepare a function in advance (to inject dependencies) and incant it with proper parameters.

Note

Incanter.incant() and Incanter.aincant() are part of the convenience layer_.

Incanter.incant() uses Incanter.adapt() internally. adapt() takes a function to be called and a series of predicates describing future parameters, and produces a function accepting these parameters and smartly forwarding them to the original function.

register_by_name and register_by_type delegate to Incanter.register_hook(). register_hook() takes a predicate function and a dependency factory. When determining if a depency factory can be used for a parameter, incant will try predicate functions (from newest to oldest) until one matches and use that dependency. Predicate functions take an inspect.Parameter and return a bool, so they can match using anything present in Parameter.

register_hook() delegates to Incanter.register_hook_factory(), which takes a predicate function and a factory of depedendency factories. This outer factory takes an inspect.Parameter and returns a depedency factory, enabling generic depedendency factories.

Note

Incanter.adapt(), Incanter.register_hook() and Incanter.register_hook_factory() are part of the power layer.