# i2.routing_forest

Tools to specify functions through trees and forests.

Whaaa?!?

Well, you see, often – especially when writing transformers – you have a series of
if/then conditions nested into eachother, in code, where it gets ugly and un-reusable.

This module explores ways to objectivy this: That is, to give us the means to create
such nested conditions in a way that we can define the parts as reusable operable
components.

Think of the relationship between the for loop (code) and the iterator (object), along
with iterator tools (itertools).
This is what we’re trying to explore, but for if/then conditions.

I said explore. Some more work is needed here to make it robust and easily usable.

Let’s look at an example involving the three main actors of our play.
Each of these are `Iterable` and `Callable` (`Generator` to be precise).

- `CondNode`: implements the if/then (no else) logic
- `FinalNode`: Final – yields (both with call and iter) it’s single `.val` attribute.
- `RoutingForest`: An Iterable of `CondNode`

You’ll note that instances of these classes are all both callables and iterables,
and that when called, they return iterables.
It’s this aspect that makes us be able to nest conditions within conditions,
and further, control the flow of the iteration from outside.
A routing node (or forest) called on an object will yield all values that match the
conditions that were specified for it.
For example, if you need all matches, you can wrap it with `list`, if you need the
first match only, you can wrap it with `next`, if you have a default value,
you can wrap it in `next` with a default value.

```pycon
>>> import inspect
>>>
>>> def could_be_int(obj):
...     if isinstance(obj, int):
...         b = True
...     else:
...         try:
...             int(obj)
...             b = True
...         except ValueError:
...             b = False
...     if b:
...         print(f'{inspect.currentframe().f_code.co_name}')
...     return b
...
>>> def could_be_float(obj):
...     if isinstance(obj, float):
...         b = True
...     else:
...         try:
...             float(obj)
...             b = True
...         except ValueError:
...             b = False
...     if b:
...         print(f'{inspect.currentframe().f_code.co_name}')
...     return b
...
>>> print(
...     could_be_int(30),
...     could_be_int(30.3),
...     could_be_int('30.2'),
...     could_be_int('nope'),
... )
could_be_int
could_be_int
True True False False
>>> print(
...     could_be_float(30),
...     could_be_float(30.3),
...     could_be_float('30.2'),
...     could_be_float('nope'),
... )
could_be_float
could_be_float
could_be_float
True True True False
>>> assert could_be_int('30.2') is False
>>> assert could_be_float('30.2') is True
could_be_float
>>>
>>> st = RoutingForest(
...     [
...         CondNode(
...             cond=could_be_int,
...             then=RoutingForest(
...                 [
...                     CondNode(
...                         cond=lambda x: int(x) >= 10,
...                         then=FinalNode('More than a digit'),
...                     ),
...                     CondNode(
...                         cond=lambda x: (int(x) % 2) == 1,
...                         then=FinalNode("That's odd!"),
...                     ),
...                 ]
...             ),
...         ),
...         CondNode(cond=could_be_float, then=FinalNode('could be seen as a float')),
...     ]
... )
>>> assert list(st('nothing I can do with that')) == []
>>> assert list(st(8)) == ['could be seen as a float']
could_be_int
could_be_float
>>> assert list(st(9)) == ["That's odd!", 'could be seen as a float']
could_be_int
could_be_float
>>> assert list(st(10)) == ['More than a digit', 'could be seen as a float']
could_be_int
could_be_float
>>> assert list(st(11)) == [
...     'More than a digit',
...     "That's odd!",
...     'could be seen as a float',
... ]
could_be_int
could_be_float
>>>
>>> print(
...     '### RoutingForest ########################################################################################'
... )
### RoutingForest ########################################################################################
>>> rf = RoutingForest(
...     [
...         SwitchCaseNode(
...             switch=lambda x: x % 5,
...             cases={0: FinalNode('zero_mod_5'), 1: FinalNode('one_mod_5')},
...             default=FinalNode('default_mod_5'),
...         ),
...         SwitchCaseNode(
...             switch=lambda x: x % 2,
...             cases={0: FinalNode('even'), 1: FinalNode('odd')},
...             default=FinalNode('that is not an int'),
...         ),
...     ]
... )
>>>
>>> assert list(rf(5)) == ['zero_mod_5', 'odd']
>>> assert list(rf(6)) == ['one_mod_5', 'even']
>>> assert list(rf(7)) == ['default_mod_5', 'odd']
>>> assert list(rf(8)) == ['default_mod_5', 'even']
>>> assert list(rf(10)) == ['zero_mod_5', 'even']
>>>
```

### Functions

| [`identity`](#i2.routing_forest.identity)(obj)                    | Return the input unchanged (the default leaf function).                           |
|-----------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
| [`return_sentinel`](#i2.routing_forest.return_sentinel)(obj[, sentinel]) | Return a constanc sentinel value when called.                                     |
| [`test_routing_forest`](#i2.routing_forest.test_routing_forest)()            | Exercise the routing nodes end to end (kept here as a runnable example).          |
| [`wrap_leafs_with_final_node`](#i2.routing_forest.wrap_leafs_with_final_node)(x)    | Yield the items of `x`, wrapping those that are not `RoutingNode` in `FinalNode`. |

### Classes

| [`CondNode`](#i2.routing_forest.CondNode)(cond, then)                            | A RoutingNode that implements the if/then (no else) logic                                                                                                                                                              |
|--------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`DelegateToMappingAttrMixin`](#i2.routing_forest.DelegateToMappingAttrMixin)()                    | A mixin to delegate `Mapping` methods to a mapping attribute called `mapping`                                                                                                                                          |
| [`FeatCondNode`](#i2.routing_forest.FeatCondNode)(feat, feat_cond_thens)             | A RoutingNode that yields multiple routes, one for each of several conditions met, where the condition is computed implements computes a feature of the obj and according to an iterable of conditions on the feature. |
| [`FinalNode`](#i2.routing_forest.FinalNode)(val)                                  | A RoutingNode that is final.                                                                                                                                                                                           |
| [`KeyFuncMapping`](#i2.routing_forest.KeyFuncMapping)([mapping, key, default_factory]) | Implements a switch-case-like mapping with a callable key function.                                                                                                                                                    |
| `NoDefault`()                                                                                    |                                                                                                                                                                                                                        |
| [`RoutingForest`](#i2.routing_forest.RoutingForest)(cond_nodes)                       |                                                                                                                                                                                                                        |
| [`RoutingNode`](#i2.routing_forest.RoutingNode)()                                   | A RoutingNode instance needs to be callable on a single object, yielding an iterable or a final value                                                                                                                  |
| [`SwitchCaseNode`](#i2.routing_forest.SwitchCaseNode)(switch, cases[, default])        | A RoutingNode that implements the switch/case/else logic.                                                                                                                                                              |

### *class* i2.routing_forest.CondNode(cond, then)

Bases: [`RoutingNode`](#i2.routing_forest.RoutingNode)

A RoutingNode that implements the if/then (no else) logic

### *class* i2.routing_forest.DelegateToMappingAttrMixin

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A mixin to delegate `Mapping` methods to a mapping attribute called `mapping`

### *class* i2.routing_forest.FeatCondNode(feat, feat_cond_thens)

Bases: [`RoutingNode`](#i2.routing_forest.RoutingNode)

A RoutingNode that yields multiple routes, one for each of several conditions
met, where the condition is computed implements computes a feature of the obj and
according to an iterable of conditions on the feature.

```pycon
>>> fcn = FeatCondNode(
...     feat=lambda x: x % 5,
...     feat_cond_thens=[
...         (lambda x: x == 0, lambda x: 'zero_mod_5'),
...         (lambda x: x == 1, lambda x: 'one_mod_5'),
...         (lambda x: x == 2, lambda x: 'two_mod_5'),
...         (lambda x: x == 3, lambda x: 'three_mod_5'),
...         (lambda x: x == 4, lambda x: 'four_mod_5'),
...     ]
... )
>>> assert list(fcn(0)) == ['zero_mod_5']
>>> assert list(fcn(1)) == ['one_mod_5']
>>> assert list(fcn(2)) == ['two_mod_5']
>>> assert list(fcn(3)) == ['three_mod_5']
>>> assert list(fcn(4)) == ['four_mod_5']
>>> assert list(fcn(5)) == ['zero_mod_5']
>>> assert list(fcn(6)) == ['one_mod_5']
```

#### *classmethod* from_feature_val_map(feat, feat_cond_thens)

A FeatCondNode where the conditions are equality checks on the feature value

# >>> fvn = FeatCondNode.from_feature_val_map(
# …     feat=lambda x: x % 3,
# …     feat_cond_thens={
# …         0: lambda x: ‘zero_mod_3’,
# …         1: lambda x: ‘one_mod_3’,
# …         2: lambda x: ‘two_mod_3’,
# …     }
# … )
# >>> list(fvn(0))
#
# >>> assert list(fvn(0)) == [‘zero_mod_3’]
# >>> assert list(fvn(1)) == [‘one_mod_3’]
# >>> assert list(fvn(2)) == [‘two_mod_3’]
#

### *class* i2.routing_forest.FinalNode(val)

Bases: [`RoutingNode`](#i2.routing_forest.RoutingNode)

A RoutingNode that is final.
It yields (both with call and iter) it’s single `.val` attribute.

### *class* i2.routing_forest.KeyFuncMapping(mapping=None, key=<function identity>, default_factory=<function return_sentinel>)

Bases: [`DelegateToMappingAttrMixin`](#i2.routing_forest.DelegateToMappingAttrMixin), [`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)

Implements a switch-case-like mapping with a callable key function.

The purpose of `KeyFuncMapping` is to  allow switch-case logic to be
given as a plugin specification.

```pycon
>>> from i2.routing_forest import KeyFuncMapping
>>>
>>> get_extension = lambda x: x.split('.')[-1]
>>>
>>> data_type = KeyFuncMapping(
...     {'csv': 'table', 'xls': 'table', 'wav': 'audio'}, key=get_extension
... )
```

Calling a `KeyFuncMapping` instance will call the `key` function on the input,
then look up the result in the `mapping`.

```pycon
>>> data_type('my_file.csv')
'table'
>>> data_type('another_file.xls')
'table'
>>> data_type('sound.wav')
'audio'
```

If the key is not found in the mapping, the `default_factory` is **called**
with the input and the result is returned. The default `default_factory` is
`return_sentinel`, which by default returns `None`

```pycon
>>> assert data_type('poem.txt') is None
```

Note that instances of `KeyFuncMapping` are also `Mapping``s, so all ``Mapping`
methods can be used.

```pycon
>>> list(data_type)
['csv', 'xls', 'wav']
>>> dict(data_type)
{'csv': 'table', 'xls': 'table', 'wav': 'audio'}
```

Including `update`, which constitutes a convenient way to extend the mapping.

```pycon
>>> data_type.update(txt='text')
>>> data_type('poem.txt')
'text'
```

The `default_factory` can be set to any callable, including a
`KeyFuncMapping` itself, which enables us to define an `else` for the
switch-case logic that a `KeyFuncMapping` implements.
Say, for example, if no handled extension is found, we want to check the protocol
of the input string instead. This is not only a new mapping, but also a new key
function. We can do it as such:

```pycon
>>> get_protocol = lambda x: x.split('://')[0]
>>> protocol = KeyFuncMapping({'https': 'url'}, get_protocol)
>>> new_data_type = KeyFuncMapping(
...     data_type.mapping, data_type.key, default_factory=protocol
... )
>>> new_data_type('notes.txt')
'text'
>>> new_data_type('https://www.python.org/')
'url'
```

Given how useful this pattern is, we made the `+` operator implement this.
Note that here, `+` is not associative or commutative (as with numbers).
It should be understood to function more like the `+` for iterables like `list`
and `tuple`.

```pycon
>>> nested = data_type + protocol
>>> nested('https://www.python.org/')
'url'
>>> nested('jazz.wav')
'audio'
```

#### default_factory(sentinel=None)

Return a constanc sentinel value when called. Use partial to set sentinel

#### key()

Return the input unchanged (the default leaf function).

### *class* i2.routing_forest.RoutingForest(cond_nodes)

Bases: [`RoutingNode`](#i2.routing_forest.RoutingNode)

```pycon
>>> rf = RoutingForest([
...     CondNode(cond=lambda x: isinstance(x, int),
...              then=RoutingForest([
...                  CondNode(cond=lambda x: int(x) >= 10, then=FinalNode('More than a digit')),
...                  CondNode(cond=lambda x: (int(x) % 2) == 1, then=FinalNode("That's odd!"))])
...             ),
...     CondNode(cond=lambda x: isinstance(x, (int, float)),
...              then=FinalNode('could be seen as a float')),
... ])
>>> assert list(rf('nothing I can do with that')) == []
>>> assert list(rf(8)) == ['could be seen as a float']
>>> assert list(rf(9)) == ["That's odd!", 'could be seen as a float']
>>> assert list(rf(10)) == ['More than a digit', 'could be seen as a float']
>>> assert list(rf(11)) == ['More than a digit', "That's odd!", 'could be seen as a float']
```

### *class* i2.routing_forest.RoutingNode

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A RoutingNode instance needs to be callable on a single object,
yielding an iterable or a final value

#### *static* from_object(x, mini_lang=<function \_default_mini_lang>)

Converts an object to a RoutingNode instance.
Enables mini-languages to be developed for defining routing trees.

### *class* i2.routing_forest.SwitchCaseNode(switch, cases, default=<i2.routing_forest.NoDefault object>)

Bases: [`RoutingNode`](#i2.routing_forest.RoutingNode)

A RoutingNode that implements the switch/case/else logic.
It’s just a specialization (enhanced with a “default” option) of the FeatCondNode
class to a situation where the cond function of feat_cond_thens is equality,
therefore the routing can be
implemented with a {value_to_compare_to_feature: then_node} map.

* **Parameters:**
  * **switch** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)) – A function returning the feature of an object we want to switch on
  * **cases** ([`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)) – The mapping from feature to RoutingNode that should be yield for that
    feature. It is often a dict, but only requirement is that it implements the
    `cases.get(val, default)` method.
  * **default** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any)) – Default RoutingNode to yield if no

```pycon
>>> rf = RoutingForest([
...     SwitchCaseNode(switch=lambda x: x % 5,
...                    cases={0: FinalNode('zero_mod_5'), 1: FinalNode('one_mod_5')},
...                    default=FinalNode('default_mod_5')),
...     SwitchCaseNode(switch=lambda x: x % 2,
...                    cases={0: FinalNode('even'), 1: FinalNode('odd')},
...                    default=FinalNode('that is not an int')),
... ])
>>>
>>> assert(list(rf(5)) == ['zero_mod_5', 'odd'])
>>> assert(list(rf(6)) == ['one_mod_5', 'even'])
>>> assert(list(rf(7)) == ['default_mod_5', 'odd'])
>>> assert(list(rf(8)) == ['default_mod_5', 'even'])
>>> assert(list(rf(10)) == ['zero_mod_5', 'even'])
```

### i2.routing_forest.identity(obj)

Return the input unchanged (the default leaf function).

### i2.routing_forest.return_sentinel(obj, sentinel=None)

Return a constanc sentinel value when called. Use partial to set sentinel

### i2.routing_forest.test_routing_forest()

Exercise the routing nodes end to end (kept here as a runnable example).

### i2.routing_forest.wrap_leafs_with_final_node(x)

Yield the items of `x`, wrapping those that are not `RoutingNode` in `FinalNode`.
