trafaret: monads and python

32
About me Mikhail Krivushin tech lead at AppCraft from Samara github: deepwalker [email protected] “Don’t fear” talk author Can tie bootlaces

Upload: -

Post on 13-Apr-2017

107 views

Category:

Software


0 download

TRANSCRIPT

About meMikhail Krivushin

tech lead at AppCraft from Samara

github: deepwalker [email protected]

• “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 beginningAndrey 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 beginningVictor 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 functiondef 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 evolutiondef 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?

PythonNo, 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 theoryMonoid 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 typemypy 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 Usagecheck_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