Skip to content

Conditions and Pipes

Please, make sure you've covered Basics first.

Conditions

There are 2 conversions which let you define conditions:

  1. c.if_(condition, if_true, if_false) for a single condition
  2. c.if_multiple(*condition_to_value_pairs, else_=...) for multiple conditions; else_ is passed as a keyword argument
from convtools import conversion as c

# Option 1
converter = (
    c.iter(c.if_(c.this < 0, c.this * 2, c.this / 2))
    .as_type(list)
    .gen_converter(debug=True)
)
assert converter([-1, 0, 1]) == [-2, 0.0, 0.5]

# Option 2
converter = (
    c.iter(
        c.if_multiple(
            (c.this < 0, c.this * 2),
            (c.this == 0, 100),
            (c.this < 10, c.this / 2),
            else_=c.this / 10,
        )
    )
    .as_type(list)
    .gen_converter(debug=True)
)
assert converter([-1, 0, 1, 20]) == [-2, 100, 0.5, 2]

Check and raise

There's a convenient helper, which checks whether a condition is met and returns input as is or raises c.ExpectException otherwise:

  1. c.expect(condition, error_msg=None), where error_msg can be a string or a conversion
  2. also as a method: c.attr("amount").expect(c.this < 10, "too big")

When error_msg is a string, it is passed to c.ExpectException directly. When it is a conversion, it is evaluated against the failing input and its result becomes the exception message. If error_msg is omitted or falsey, the default message is "condition is not met".

from convtools import conversion as c

# expect doesn't change input
converter = (
    c.iter(c.expect(c.this < 3, "too big") ** 10)
    .as_type(list)
    .gen_converter(debug=True)
)
assert converter(range(3)) == [0, 1, 1024]
try:
    converter(range(4))
except c.ExpectException as e:
    assert str(e) == "too big"
else:
    raise AssertionError("expected ExpectException")

# error_msg can be conversion itself
converter = (
    c.item("a")
    .expect(
        condition=c.this.len() > 3,
        error_msg=c.call_func("{} is too short".format, c.this),
    )
    .gen_converter(debug=True)
)
try:
    converter({"a": "val"})
except c.ExpectException as e:
    assert str(e) == "val is too short"

Pipes

Pipe is the most important conversion which allows to pass results of one conversion to another. The syntax is simple: first.pipe(second).

from convtools import conversion as c

converter = (
    c.iter(
        (c.item("value") + 1).pipe(
            c.if_(c.this < 0, c.this * c.this, c.this * 2)
        )
    )
    .as_type(list)
    .gen_converter(debug=True)
)
assert converter([{"value": -4}, {"value": 2}]) == [9, 6]

You can also pipe to callables, have a look at its signature: pipe(next_conversion, *args, label_input=None, label_output=None, **kwargs). It accepts *args and **kwargs, which are passed to a callable after pipe input:

from datetime import datetime
from convtools import conversion as c

assert c.this.pipe(int).execute("123", debug=True) == 123

converter = (
    c.item("dt").pipe(datetime.strptime, "%m/%d/%Y").gen_converter(debug=True)
)
assert converter({"dt": "12/25/2000"}) == datetime(2000, 12, 25)

and_then

and_then(conversion, condition=bool) method applies provided conversion if condition is true (by default condition is standard python's truth check) otherwise returns the original value unchanged. condition accepts both conversions and callables.

from convtools import conversion as c

# DEFAULT CONDITION
converter = (
    c.iter(c.this.and_then(c.this.as_type(int)))
    .as_type(list)
    .gen_converter(debug=True)
)
assert converter(["1", None, 2.0]) == [1, None, 2]

# CUSTOM CONDITION
converter = (
    c.iter(c.this.and_then(c.this + 10, condition=c.this != 1))
    .as_type(list)
    .gen_converter(debug=True)
)
assert converter(range(3)) == [10, 1, 12]

Labels

Warning

Despite the fact that convtools encourages a functional approach and working with immutable data, sometimes it's inevitable to use global variables. Avoid using labels if possible.

There are two ways to label data for further use:

  1. pipe method accepts label_input (applies to pipe's input data) and label_output (applies to the end result) keyword arguments, each of them is either:
    • str - label name
    • dict - label names to conversion map. Labels are put on results of conversions.
  2. add_label(label_input) - shortcut to pipe(c.this, label_input=label_input). Like label_input, it accepts either a string label name or a mapping of label names to conversions.

To reference previously labeled data use c.label("label_name"). Labels are one of several context-specific references covered in Placeholders & Special References.

from datetime import datetime
from convtools import conversion as c

converter = (
    c.this.add_label({"a": c.item("a")})
    .item("b")
    .iter({"a": c.label("a"), "b": c.this})
    .as_type(list)
    .gen_converter(debug=True)
)
# SAME
converter_2 = (
    c.this.pipe(c.item("b"), label_input={"a": c.item("a")})
    .iter({"a": c.label("a"), "b": c.this})
    .as_type(list)
    .gen_converter(debug=True)
)
input_data = {
    "a": 1,
    "b": [2, 3, 4],
}
expected_output = [{"a": 1, "b": 2}, {"a": 1, "b": 3}, {"a": 1, "b": 4}]
assert (
    converter(input_data) == expected_output
    and converter_2(input_data) == expected_output
)


# BETTER WITHOUT LABELS (HERE IT'S POSSIBLE)
converter_3 = (
    c.zip(a=c.repeat(c.item("a")), b=c.item("b"))
    .as_type(list)
    .gen_converter(debug=True)
)
assert converter_3(input_data) == expected_output

Dispatch

There are performance critical cases where it's desired to replace c.if_ and c.if_multiple with dict lookups. However it limits what can be used as keys as these need to be hashable.

Interface: c.this.dispatch(key, key_to_conv, default)

  1. key defines a conversion, which gets a key
  2. key_to_conv is a dict which maps keys to conversions
  3. default is an optional default conversion, when the dict doesn't contain the key
from convtools import conversion as c


input_data = [
    {"version": "v1", "field1": 10},
    {"version": "v2", "field2": 20},
    {"version": "v3", "field": 30},
]

converter = (
    c.iter(
        c.this.dispatch(
            c.item("version"),
            {
                "v1": c.item("field1"),
                "v2": c.item("field2"),
            },
            default=c.item("field"),
        )
    )
    .as_type(list)
    .gen_converter(debug=True)
)

assert converter(input_data) == [10, 20, 30]