Select Git revision
tcpforward_commands.c
lookup.py 5.53 KiB
r"""
Jq(1) <https://jqlang.github.io/jq/> like expressions for python.
Something similar to Jq, but built on python objects.
All procedures eventually return the expecetd value, or a
user-supplied default value.
Example
-------
::
lookup(object) \
.ref('docstring') \
.ref('tags') \
.select(Ref('tag_name') == 'summary')) \
.idx(0) \
.ref('text') \
.exec()
.. todo::
``select``
Selects all values from a list which matches a given expression.
This would however require us to manage multiple values at once.
"""
from typing import Any, Union
class Expression:
"""
A test expression.
::
x.find(Ref("key") == "summary")
Would focus in on the first list element which has the key "key"
with a value of "summary".
This is the root-class, and doesn't make sense to initialize directly.
"""
def run(self, value: Any) -> bool:
"""Evaluate expression, returing true if the value matches the predicate."""
return False
class RefEqExpr(Expression):
"""
Equality expression.
Assumes that the left part is a RefExpr and the right part is a value.
Checks that the left reference exists in the given value, and that
it's value is equal to the right one.
"""
def __init__(self, left: 'RefExpr', right: Any):
self.left = left
self.right = right
def run(self, value: Any) -> bool:
"""
Evaluate the expression.
Returns true if the left hand side key is present in *value*,
and if the value under that key in *value* matches the right
hand side.
"""
if self.left.key not in value:
return False
else:
return bool(value[self.left.key] == self.right)
class RefExpr(Expression):
"""
A key reference expression.
By itself, checks if the given key exists in the given value.
Intended to be used for dictionaries, but will work on anything
implementing `in`.
"""
def __init__(self, key: str):
self.key = key
def __eq__(self, other: Any) -> 'RefEqExpr': # type: ignore
"""
Return a new expression checking equality between left and right.
Left side will be ourself, while the right side can in theory
be anything (see RefEqExpr for details).
Typing is removed here, since the base object specifies the type as
def __eq__(self, x: Any) -> bool:
...
Which we aren't allowed to deviate from according to the
Liskov substitution principle. However, at least sqlalchemy
uses the exact same "trick" for the exact same effect. So
there is a president.
"""
return RefEqExpr(self, other)
def run(self, value: Any) -> bool:
"""Return true if our key is present in *value*."""
return self.key in value
class _NullLookup:
"""
A failed lookup.
Shares the same interface as true "true" lookup class, but all
methods imidiattely propagate the failure state.
This saves us from null in the code.
"""
def get(self, _: str) -> '_NullLookup':
"""Propagate null."""
return self
def ref(self, _: str) -> '_NullLookup':
"""Propagate null."""
return self
def idx(self, _: int) -> '_NullLookup':
"""Propagate null."""
return self
def find(self, _: Expression) -> '_NullLookup':
"""Propagate null."""
return self
def value(self, dflt: Any = None) -> Any:
"""Return the default value."""
return dflt
class _TrueLookup:
"""Easily lookup values in nested data structures."""
def __init__(self, object: Any):
self.object = object
def get(self, key: str) -> Union['_TrueLookup', '_NullLookup']:
"""Select object field by name."""
try:
return _TrueLookup(getattr(self.object, key))
except Exception:
return _NullLookup()
def ref(self, key: str) -> Union['_TrueLookup', '_NullLookup']:
"""Select object by dictionary key."""
try:
return _TrueLookup(self.object[key])
except TypeError:
# Not a dictionary
return _NullLookup()
except KeyError:
# Key not in dictionary
return _NullLookup()
def idx(self, idx: int) -> Union['_TrueLookup', '_NullLookup']:
"""Select array index."""
try:
return _TrueLookup(self.object[idx])
except TypeError:
# Not a list
return _NullLookup()
except IndexError:
# Index out of range
return _NullLookup()
def find(self, expr: Expression) -> Union['_TrueLookup', '_NullLookup']:
"""Find the first element in list matching expression."""
for item in self.object:
if expr.run(item):
return _TrueLookup(item)
return _NullLookup()
def value(self, dflt: Any = None) -> Any:
"""
Return the found value.
If no value is found, either return None, or the second argument.
"""
return self.object
# Implemented as a union between our two different types, since the
# split is an implementation detail to easier handle null values.
Lookup = Union[_TrueLookup, _NullLookup]
"""Lookup type."""
def lookup(base: Any) -> Lookup:
"""
Create a new lookup base object.
All queries should start here.
:param base: Can be anything which has meaningful subfields.
"""
return _TrueLookup(base)
Ref = RefExpr