Skip to content

A simple but powerful embedded expression language for Python

License

Notifications You must be signed in to change notification settings

tandemdude/pysel

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PySEL

The Python Simple Expression Language (PySEL) is a simple but powerful expression language that supports manipulation and creation of basic python literals (str, float, int) using a majority of the available operators, but also offers notable features such as attribute/property accessors and method invocation, as well as a global scope at runtime in which you can inject any python object.

This is somewhat influenced by the Spring Expression Language (SpEL), widely used by the Spring portfolio.

Installation

PySEL can be installed using pip:

$ pip install pysel-lang

Feature Overview

The expression language supports the following functionality:

  • Literal expressions
  • Logical, relational and mathematical operators
  • Attribute/property access
  • Method invocation
  • Getitem (list indexing, slicing, dict accessing, etc)
  • Ternary operator
  • Environment value substitution

Expression Evaluation

PySEL provides a utility class, Expression to allow you to run a valid expression in very few lines of code.

For example, the following expression, when evaluated, would return a string literal with value Hello World!:

>>> import pysel
>>> exp = pysel.Expression("'Hello World!'")

To evaluate any expression, call the evaluate() method of the Expression object:

>>> import pysel
>>> exp = pysel.Expression("'Hello World!")
>>> exp.evaluate()
'Hello World!'

You can also pass a mapping containing the execution environment for the expression. Any keys contained in the environment can be accessed from within an expression using an identifier - the name of the key:

>>> import pysel
>>> exp = pysel.Expression("foo")
>>> exp.evaluate({"foo": "bar"})
'bar'

If you do not wish to evaluate the expression immediately but would still like to know if it is syntactically valid, you can call the compile() method of the Expression class. This will parse and validate the expression and return the compiled code object.

Invalid Expression:

>>> import pysel
>>> exp = pysel.Expression("'foo")  # Invalid due to unclosed quotes
>>> exp.compile()
Traceback (most recent call last):
  ...
    raise errors.ExpressionSyntaxError(
pysel.errors.ExpressionSyntaxError: Unexpected EOF while parsing
    "'foo"
     ^   

Valid Expression:

>>> import pysel
>>> exp = pysel.Expression("'foo'")
>>> exp.compile()
<code object <...? at 0x..., file "pysel_expr", line 1>

Language Reference

Literals

PySEL currently supports four different types of literals - string literals, integer literals, float literals, and None.

A string literal is any set of characters surrounded by a matching pair of quotes. Quotes can be either single quotation marks (') or double quotation marks ("). You can also include quotation marks in the string by escaping them using a backslash:

>>> pysel.Expression("'foo'").evaluate()
'foo'
>>> pysel.Expression('"foo"').evaluate()
'foo'
>>> pysel.Expression("'foo\\'s'").evaluate()
"foo's"

An integer literal is any set of consecutive digits (0-9). Unlike Python, PySEL allows integer literals to begin with a 0 digit:

>>> pysel.Expression("1234").evaluate()
1234
>>> pysel.Expression("01234").evaluate()
1234

A float literal is any set of consecutive digits 0-9 followed by a dot .. Float literals can also optionally include digits after the decimal place - if none are specified, e.g. 2. then the number will still parse correctly:

>>> pysel.Expression("1.5").evaluate()
1.5
>>> pysel.Expression("1.").evaluate()
1.0

None is implemented identically to Python.

Logical, relational and mathematical operators

The logical operators that are supported are && (and), || (or) and ! (not). Their use is shown below:

# --- NOT ---
>>> pysel.Expression("!foo").evaluate({"foo": True})
False
>>> pysel.Expression("!foo").evaluate({"foo": False})
True
# --- AND ---
>>> pysel.Expression("foo && bar").evaluate({"foo": True, "bar": False})
False
>>> pysel.Expression("foo && bar").evaluate({"foo": True, "bar": True})
True
# --- OR ---
>>> pysel.Expression("foo || bar").evaluate({"foo": False, "bar": False})
False
>>> pysel.Expression("foo || bar").evaluate({"foo": False, "bar": True})
True

The relational operators that are supported are ==, !=, >, <, >=, and <= - all using standard operator notation:

>>> pysel.Expression("2 == 2").evaluate()
True
>>> pysel.Expression("2 < 5").evaluate()
True
>>> pysel.Expression("2 != 2").evaluate()
False
...

PySEL supports all the same mathematical operators supported by Python, excluding the bitwise operators (for now). Operator precedence follows the order specified in the "Operator Precedence" section.

>>> pysel.Expression("2 + 2").evaluate()
4
>>> pysel.Expression("2 * 3").evaluate()
6
>>> pysel.Expression("5 // 2").evaluate()
2
...

Attribute/property access

Attribute access in PySEL functions identically to that of Python - using the . operator:

>>> pysel.Expression("'foo'.__class__").evaluate()
<class 'str'>

You can also chain attribute accessors to an unlimited depth, as with python:

>>> pysel.Expression("'foo'.__class__.__name__").evaluate()
'str'

Method invocation

As with attribute access, methods are invoked identically to the way you would using Python:

>>> pysel.Expression("str()").evaluate()
''

PySEL also supports calling methods with an infinite number of arguments - however you should note that all arguments will be passed positionally. Keyword arguments are not implemented:

>>> pysel.Expression("str(10)").evaluate()
'10'

Getitem (indexing, slicing, dict accessing)

PySEL's syntax for this is completely identical to Python's. A pair of square brackets immediately following any expression are intepreted as a call to object.__getitem__, as with Python. This allows you to perform indexing, slicing, and dictionary accessing as you would normally:

>>> pysel.Expression("'foobar'[0]").evaluate()
'f'
>>> pysel.Expression("'foobar'[::-1]").evaluate()
'raboof'
>>> pysel.Expression("dict['foo']").evaluate({"dict": {"foo": "bar"}})
'bar'

Ternary operator

PySEL supports the standard ternary operator found in many other languages including but not limited to: C, JS, Java, etc.

The syntax is: condition ? when_true : when_false.

This is functionally equivalent to:

when_true if some_condition else when_false

Example:

>>> pysel.Expression("cond ? 'foo' : 'bar'").evaluate({"cond": True})
'foo'
>>> pysel.Expression("cond ? 'foo' : 'bar'").evaluate({"cond": False})
'bar'

Environment value substitution

As you may have seen in the previous sections, PySEL allows values to be substituted in place of identifiers in any given expression. When calling Expression.evaluate(), you can optionally pass a mapping of identifier name to value which will be accessible from the expression when it is run:

>>> pysel.Expression("foo").evaluate({"foo": "bar"})
'bar'

The default environment contains four identifiers - str, int, float and bool - which are intended to be used for casting values to different types within expressions, but of course you can use them for whatever you wish.

If you pass a mapping to the evaluate method, and some of the identifier names conflict with the ones mentioned above, then the default identifiers will be overridden with the value that you passed:

>>> pysel.Expression("str").evaluate()
<class 'str'>
>>> pysel.Expression("str").evaluate({"str": "foo"})
'foo'

Operator Precedence

  • Literals, parentheses
  • Accessor (., i.e. foo.bar)
  • Method call (i.e. foo()), getitem (i.e. 'foo'[0])
  • ** (exponent)
  • Unary -, +
  • *, /, //, %
  • Binary -, +
  • ==, !=, >, <, >=, <=
  • !
  • &&
  • ||
  • Ternary (i.e. expr ? expr : expr)

Grammar Specification

Note that this does not cover operator precedence, see the above section for that information.

The below is written using Extended Backus-Naur form:

all characters = ? All characters valid in a python string ?;

letter = ? All characters in set [a-zA-Z] ?;

digit = ? All characters in set [0-9] ?;

unop = "-" | "+" | "!";

binop = "==" | "!=" | ">" | "<" | ">=" | "<=" | "&&" | "||" |
        "+" | "-" | "*" | "/" | "%" | "//" | "**";

int = { digit };

float = int, "." [, int];

str = ("'" [, { all characters - "'" | "\'" }], "'") |
        ('"' [, { all characters - '"' | '\"' }], '"');

expr = int | str | float | expr binop expr | unop expr | ternary | identifier | accessor | methodcall | getitem;

ternary = expr, "?", expr, ":", expr;

identifier = (letter | "_") [, { letter | digit | "_" }];

accessor = expr, ".", identifier;

methodcall = expr, "(", expr [, { ",", expr }], ")";

slice = [expr, ] ":" [, expr] [, ":" [expr]]

getitem = expr "[", slice | expr, "]"

About

A simple but powerful embedded expression language for Python

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages