auk is a micro-package for compiling s-expressions into predicate functions.
P, Q = True, False sexp = \ ['not', ['or', ['identifier', 'P'], ['identifier', 'Q'], ] ] func = compile_predicate(sexp) assert func(P, Q) == (not P and not Q)
auk's interface consists almost entirely of a single function -
compile_predicate, which accepts an s-expression and compiles it into
Python's function or lambda. Its signature is as follows:
def compile_predicate( sexp: List, funcname: str = None, force_func: bool = False, force_lambda: bool = False) -> Union[FunctionType, LambdaType]
Above s-expression, for example, is compiled down to the following AST
FunctionDef( name = '_a5c28596600346faa54c0b092500fc48', args = arguments( args = [ arg(arg = 'P', annotation = None), arg(arg = 'Q', annotation = None), ], vararg = None, kwonlyargs = , kw_defaults = , kwarg = None, defaults = , ), body = [ Return( value = UnaryOp( op = Not(), operand = BoolOp( op = Or(), values = [ Name(id = 'P', ctx = Load()), Name(id = 'Q', ctx = Load()), ], ), ), ), ], decorator_list = , returns = None, )
Compiled function returns value of the predicate s-expression (e.g.
as above), which, by the definition of a predicate, is always a truth value (I
explicitly avoid here saying "boolean", as it may happen that the target
function returns an object instead of
bool. This is desired, however, as
every object in Python is in fact a truth value. For example, a list in
is_not_empty = lambda array: array
By default, expressions with up to one argument are compiled to lambdas and
everything else to functions. This is in order to allow passing arguments via
kwargs. This method of passing arguments should be a preferred one when
constructing complex expressions, as keeping track of the argument order
quickly becomes cumbersome. Compilation to lambda and function can be forced
force_function options, respectively.
As you can see in the above example, when no name is specified for the target
function, random name is generated. Specifically, the name is generated using
'_%s' % uuid.uuid4().hex. In case of lambdas, the name
becomes a variable to which expression is assigned.
Because only a handful of built-in types (such as
corresponding AST nodes, non-primitive types (e.g. user-defined classes) have
to be compiled into an
ast.Name node (name binding) with random name (same
as above) and stored in target function's closure. Therefore, instances of
classes such as
class Foo: pass end up as free-variables. For more details
Allowable expressions are defined by the following grammar:
rules: predicate: - identifier - tautology - contradiction - not - and - or - eq - neq - lt - lte - gt - gte - in - literal - callable tautology: - [ ] contradiction: - [ ] identifier: - [ name ] not: - [ predicate ] and: - [ predicate+ ] or: - [ predicate+ ] eq: - [ term, term ] neq: - [ term, term ] lt: - [ term, term ] lte: - [ term, term ] gt: - [ term, term ] gte: - [ term, term ] in: - [ term, term ] callable: - [ '=FunctionType' ] term: - var_ref - literal var_ref: - identifier literal: - '~object' name: !regexpr '[a-zA-Z_][a-zA-Z0-9_]*'
For a detailed explanation of the grammar notation check out
sexpr library documentation.
In practice, almost every valid expression in Python can be translated to
valid s-expression. This includes constructs such as
in, callables and
sexp = ['in', ['identifier', 'num', [1, 2, 3]] func = compile_predicate(sexp) assert func(2) assert not func(4) dice_roll = lambda: randint(1, 6) sexp = ['eq', ['callable', dice_roll], 6] lucky_six = compile_predicate(sexp) assert lucky_six() # Luck required to pass obj = object() sexp = ['eq', ['identifier', 'obj'], obj] func = compile_predicate(sexp) assert func(obj) assert not func(object())
For more examples, check out unit tests.