About me
Mikhail Krivushin
tech lead at AppCraft
from Samara
github: deepwalker
krivushinme@gmail.com
• “Don’t fear” talk author
• Can tie bootlaces
Trafaret:
Monads and
Python
Trafaret
• one from tens of validation libs, but with
nuance
• trafaret not only validates but converts data
• loves simple functions and very easy to extend
Trafaret Base
• DataError, Trafaret base class, trafaret.Call
• Int, Float, String, Any, Null, Bool, Type,
Subclass
• StrBool, Atom, URL, Email
• List, Tuple, Enum
• Dict, Key
show me the code
import trafaret as t
name = t.RegexpRaw(‘name=(w+)’) & (lambda match: match.groups()[0])
name(‘name=Joe’) == ‘Joe’
APP_CONFIG = t.Dict({‘kill_them_all’: t.StrBool}) >> json.dumps
APP = t.Dict({
‘app_id’: t.Int,
‘name’: t.String,
‘config’: APP_CONFIG,
})
app = App(**APP(request.args))
db.session.add(app)
show me the code
def trafaret_error(meth):
@wraps(meth)
def wrapper(*a, **kw):
try:
return meth(*a, **kw)
except t.DataError as de:
return (
ujson.dumps({‘error': de.as_dict()}),
400,
{'Content-Type': ‘application/json’},
)
return wrapper
The beginning
Andrey Vlasovskikh: funcparserlib
https://github.com/vlasovskikh/
funcparserlib
null = n('null') >> const(None)
true = n('true') >> const(True)
false = n('false') >> const(False)
string = toktype('String') >> make_string
value = forward_decl()
member = string + op_(':') + value >> tuple
The beginning
Victor Kotseruba: Contract
https://github.com/barbuza/contract
from contract import (
IntC, ListC, DictC, StringC,
ContractValidationError,
)
list_of_ints = ListC[IntC]
foobar = DictC(foo=IntC, bar=StringC)
try:
foobar(data)
except ContractValidationError:
print “AAAAAAaaaaaAA”
Simple function
def check_int(value: Any) -> bool:
return (
isinstance(value, int)
or (isinstance(value, str) and value.isdigit())
)
def ensure_int(value: Any) -> Union[int, None]:
if (
isinstance(value, int)
or (isinstance(value, str) and value.isdigit())
):
return int(value)
return None
Function evolution
def try_int(value: Any) -> Union[int, DataError]:
if (
isinstance(value, int)
or (isinstance(value, str) and value.isdigit())
):
return int(value)
return DataError(‘Not an int’)
def less_then_500(value: int) -> Union[int, DataError]:
if value < 500:
return value
return DataError(‘value too big’)
Go Style go go go
try:
value = check_int(arg)
except t.DataError as data_error:
return str(data_error)
try:
value = less_then_500(value)
except t.DataError as data_error:
return str(data_error)
…
Legacy: The Dark Times
When Trafaret was young
class Trafaret:
def __init__(self):
self.converters = []
def append(self, converter):
self.converters.append(converter)
def __rshift__(self, converter):
self.append(converter)
int_less_500 = t.Int()
int_less_500.append(less_then_500)
int_less_500 = t.Int() >> less_then_500
Legacy: The Dark Times
When Trafaret was young
def int_less_500_producer():
return t.Int() >> less_then_500
int_less_500_producer() >> but_bigger_then_5
Monads?
Are you serious? Why do
you need monads in
python? Do you think this
is Haskell?
Python
No, this is not what we
like to do
@do(Maybe)
def with_maybe(first_divisor):
val1 = yield mdiv(2.0, 2.0)
val2 = yield mdiv(3.0, 0.0)
val3 = yield mdiv(val1, val2)
mreturn(val3)
Category theory
Monoid in the Endc category.
Just a whimsical
compose for a
weird functions
simple_func::simple_type -> simple_type
simple_func1 • simple_func2
weird1::simple_type -> complex_type
weird1 >>= weird2
Trafaret type
mypy notation:
TrafaretRes = Union[Any, DataError]
Trafaret = Callable[[Any], TrafaretRes]
in haskell you will name it Either
Whimsical Compose
Lets compose weird
trafaret functions
def t_and(t1, t2):
def composed(value):
res = t1(value)
if isinstance(res, DataError):
return res
return t2(res)
return composed
int_less_500 = t_and(try_int, less_then_500)
int_less_500 = t.Int & less_then_500
Go Style no no no
int_less_then_500 = t.Int & less_then_500
try:
value = int_less_then_500(arg)
except t.DataError as data_error:
return str(data_error)
…
Operations
check = t.Int | t.Bool
check(True) == True
import trafaret as t
check = t.Bool & int
check(True) == 1
check = (t.Int | t.Bool) & int
check(True) == 1
compose aka and
good old `or`
all together now
import arrow
def check_date(value):
try:
return arrow.get(value)
except arrow.parser.ParserError:
return t.DataError(‘Bad datetime format’)
import ujson
def check_json(value):
try:
return ujson.loads(value)
except ValueError:
return t.DataError(‘Bad JSON value’)
except TypeError:
return t.DataError(‘Bad JSON value’)
Enlarge your T
Dict and Key
• everyone needs to check dicts
• keys can be optional, can have default
• dict can ban extra values or ignore them
• do you want to check only dicts actually?
What about MultiDict?
Trafaret Dict Solution
• Key are the item getters and trafaret callers
• Dict collects keys output
• you can implement your own Key like a
function or subclass Key
• keys touches value instance, so be careful with
quantum dicts
What Key Type Signature Is?
KeyT = Callable[
[Mapping],
Sequence[
Tuple[
str,
Union[Any, t.DataError],
Sequence[str]
]
]
]
def some_key(name, trafaret):
trafaret = t.ensure_trafaret(trafaret)
def check(value):
if name in value:
yield name, t.catch_error(trafaret, value.get(name)), (name,)
else:
yield name, t.DataError(‘is required’), (name,)
return check
Dict Usage
check_request = check_json & t.Dict(some_key(‘count’, t.Int))
check_request(‘{“count”: 5}’) == {‘count’: 5}
check_comma_str = t.String() & (lambda s: s.split(‘,’))
t.Dict({
‘count’: t.Int(),
t.Key(‘UserName’) >> ‘username’: t.String(min_length=5),
‘groups’: check_comma_string & t.List(t.Enum(‘by_device’,
‘by_city’)),
})
MultiDict Key
class MultiDictKey(t.Key):
def get_data(self, value):
return value.get_all(self.name)
check_request = t.Dict(
t.Key(‘username’, t.String),
MultiDictKey(‘group’, t.List(check_comma_string)),
)
Keys Insanity
def xor_key(first, second, trafaret):
trafaret = t.Trafaret._trafaret(trafaret)
def check_(value):
if (first in value) ^ (second in value):
key = first if first in value else second
yield first, t.catch_error(trafaret, value[key]), (key,)
elif first in value and second in value:
yield first, t.DataError(error=f'correct only if {second} is not defined'), (first,)
yield second, t.DataError(error=f'correct only if {first} is not defined'), (second,)
else:
yield first, t.DataError(error=f'is required if {second} is not defined'), (first,)
yield second, t.DataError(error=f'is required if {first} is not defined'), (second,)
return check_
Sugar
from trafaret.constructor import construct, C
construct({
‘a’: int,
‘b’: [bool],
‘c’: (str, str),
})
C & int & bool
Form Helpers
fold
unfold
>>> unfold({'a': [1,2], 'b': {'c': 'bla'}})
{'a__0': 1, 'a__1': 2, 'b__c': 'bla'}
>>> fold({'a__0': 1, 'a__1': 2, 'b__c': 'bla'})
{'a': [1, 2], 'b': {'c': 'bla'}}
Why not competitors?
Weird DSLs with pain
Questions?
@deepwalker/
trafaret
trafaret.rb
pundle
backslant
aldjemy

Trafaret: monads and python

  • 1.
    About me Mikhail Krivushin techlead at AppCraft from Samara github: deepwalker krivushinme@gmail.com • “Don’t fear” talk author • Can tie bootlaces
  • 2.
  • 3.
    Trafaret • one fromtens of validation libs, but with nuance • trafaret not only validates but converts data • loves simple functions and very easy to extend
  • 4.
    Trafaret Base • DataError,Trafaret base class, trafaret.Call • Int, Float, String, Any, Null, Bool, Type, Subclass • StrBool, Atom, URL, Email • List, Tuple, Enum • Dict, Key
  • 5.
    show me thecode import trafaret as t name = t.RegexpRaw(‘name=(w+)’) & (lambda match: match.groups()[0]) name(‘name=Joe’) == ‘Joe’ APP_CONFIG = t.Dict({‘kill_them_all’: t.StrBool}) >> json.dumps APP = t.Dict({ ‘app_id’: t.Int, ‘name’: t.String, ‘config’: APP_CONFIG, }) app = App(**APP(request.args)) db.session.add(app)
  • 6.
    show me thecode def trafaret_error(meth): @wraps(meth) def wrapper(*a, **kw): try: return meth(*a, **kw) except t.DataError as de: return ( ujson.dumps({‘error': de.as_dict()}), 400, {'Content-Type': ‘application/json’}, ) return wrapper
  • 7.
    The beginning Andrey Vlasovskikh:funcparserlib https://github.com/vlasovskikh/ funcparserlib null = n('null') >> const(None) true = n('true') >> const(True) false = n('false') >> const(False) string = toktype('String') >> make_string value = forward_decl() member = string + op_(':') + value >> tuple
  • 8.
    The beginning Victor Kotseruba:Contract https://github.com/barbuza/contract from contract import ( IntC, ListC, DictC, StringC, ContractValidationError, ) list_of_ints = ListC[IntC] foobar = DictC(foo=IntC, bar=StringC) try: foobar(data) except ContractValidationError: print “AAAAAAaaaaaAA”
  • 9.
    Simple function def check_int(value:Any) -> bool: return ( isinstance(value, int) or (isinstance(value, str) and value.isdigit()) ) def ensure_int(value: Any) -> Union[int, None]: if ( isinstance(value, int) or (isinstance(value, str) and value.isdigit()) ): return int(value) return None
  • 10.
    Function evolution def try_int(value:Any) -> Union[int, DataError]: if ( isinstance(value, int) or (isinstance(value, str) and value.isdigit()) ): return int(value) return DataError(‘Not an int’) def less_then_500(value: int) -> Union[int, DataError]: if value < 500: return value return DataError(‘value too big’)
  • 11.
    Go Style gogo go try: value = check_int(arg) except t.DataError as data_error: return str(data_error) try: value = less_then_500(value) except t.DataError as data_error: return str(data_error) …
  • 12.
    Legacy: The DarkTimes When Trafaret was young class Trafaret: def __init__(self): self.converters = [] def append(self, converter): self.converters.append(converter) def __rshift__(self, converter): self.append(converter) int_less_500 = t.Int() int_less_500.append(less_then_500) int_less_500 = t.Int() >> less_then_500
  • 13.
    Legacy: The DarkTimes When Trafaret was young def int_less_500_producer(): return t.Int() >> less_then_500 int_less_500_producer() >> but_bigger_then_5
  • 14.
    Monads? Are you serious?Why do you need monads in python? Do you think this is Haskell?
  • 15.
    Python No, this isnot what we like to do @do(Maybe) def with_maybe(first_divisor): val1 = yield mdiv(2.0, 2.0) val2 = yield mdiv(3.0, 0.0) val3 = yield mdiv(val1, val2) mreturn(val3)
  • 16.
    Category theory Monoid inthe Endc category.
  • 17.
    Just a whimsical composefor a weird functions simple_func::simple_type -> simple_type simple_func1 • simple_func2 weird1::simple_type -> complex_type weird1 >>= weird2
  • 18.
    Trafaret type mypy notation: TrafaretRes= Union[Any, DataError] Trafaret = Callable[[Any], TrafaretRes] in haskell you will name it Either
  • 19.
    Whimsical Compose Lets composeweird trafaret functions def t_and(t1, t2): def composed(value): res = t1(value) if isinstance(res, DataError): return res return t2(res) return composed int_less_500 = t_and(try_int, less_then_500) int_less_500 = t.Int & less_then_500
  • 20.
    Go Style nono no int_less_then_500 = t.Int & less_then_500 try: value = int_less_then_500(arg) except t.DataError as data_error: return str(data_error) …
  • 21.
    Operations check = t.Int| t.Bool check(True) == True import trafaret as t check = t.Bool & int check(True) == 1 check = (t.Int | t.Bool) & int check(True) == 1 compose aka and good old `or` all together now
  • 22.
    import arrow def check_date(value): try: returnarrow.get(value) except arrow.parser.ParserError: return t.DataError(‘Bad datetime format’) import ujson def check_json(value): try: return ujson.loads(value) except ValueError: return t.DataError(‘Bad JSON value’) except TypeError: return t.DataError(‘Bad JSON value’) Enlarge your T
  • 23.
    Dict and Key •everyone needs to check dicts • keys can be optional, can have default • dict can ban extra values or ignore them • do you want to check only dicts actually? What about MultiDict?
  • 24.
    Trafaret Dict Solution •Key are the item getters and trafaret callers • Dict collects keys output • you can implement your own Key like a function or subclass Key • keys touches value instance, so be careful with quantum dicts
  • 25.
    What Key TypeSignature Is? KeyT = Callable[ [Mapping], Sequence[ Tuple[ str, Union[Any, t.DataError], Sequence[str] ] ] ] def some_key(name, trafaret): trafaret = t.ensure_trafaret(trafaret) def check(value): if name in value: yield name, t.catch_error(trafaret, value.get(name)), (name,) else: yield name, t.DataError(‘is required’), (name,) return check
  • 26.
    Dict Usage check_request =check_json & t.Dict(some_key(‘count’, t.Int)) check_request(‘{“count”: 5}’) == {‘count’: 5} check_comma_str = t.String() & (lambda s: s.split(‘,’)) t.Dict({ ‘count’: t.Int(), t.Key(‘UserName’) >> ‘username’: t.String(min_length=5), ‘groups’: check_comma_string & t.List(t.Enum(‘by_device’, ‘by_city’)), })
  • 27.
    MultiDict Key class MultiDictKey(t.Key): defget_data(self, value): return value.get_all(self.name) check_request = t.Dict( t.Key(‘username’, t.String), MultiDictKey(‘group’, t.List(check_comma_string)), )
  • 28.
    Keys Insanity def xor_key(first,second, trafaret): trafaret = t.Trafaret._trafaret(trafaret) def check_(value): if (first in value) ^ (second in value): key = first if first in value else second yield first, t.catch_error(trafaret, value[key]), (key,) elif first in value and second in value: yield first, t.DataError(error=f'correct only if {second} is not defined'), (first,) yield second, t.DataError(error=f'correct only if {first} is not defined'), (second,) else: yield first, t.DataError(error=f'is required if {second} is not defined'), (first,) yield second, t.DataError(error=f'is required if {first} is not defined'), (second,) return check_
  • 29.
    Sugar from trafaret.constructor importconstruct, C construct({ ‘a’: int, ‘b’: [bool], ‘c’: (str, str), }) C & int & bool
  • 30.
    Form Helpers fold unfold >>> unfold({'a':[1,2], 'b': {'c': 'bla'}}) {'a__0': 1, 'a__1': 2, 'b__c': 'bla'} >>> fold({'a__0': 1, 'a__1': 2, 'b__c': 'bla'}) {'a': [1, 2], 'b': {'c': 'bla'}}
  • 31.
  • 32.