Conditions and Pipes¶
Please, make sure you've covered Basics first.
Conditions¶
There are 2 conversions which let you define conditions:
c.if_(condition, if_true, if_false)for a single conditionc.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:
c.expect(condition, error_msg=None), whereerror_msgcan be a string or a conversion- 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:
pipemethod acceptslabel_input(applies to pipe's input data) andlabel_output(applies to the end result) keyword arguments, each of them is either:str- label namedict- label names to conversion map. Labels are put on results of conversions.
add_label(label_input)- shortcut topipe(c.this, label_input=label_input). Likelabel_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)
keydefines a conversion, which gets a keykey_to_convis a dict which maps keys to conversionsdefaultis 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]