Skip to content
Snippets Groups Projects
Commit b2898df7 authored by Hugo Hörnquist's avatar Hugo Hörnquist
Browse files

Introduce lookup.

parent 44f37a85
No related branches found
No related tags found
No related merge requests found
"""
[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(i) \
.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:
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:
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 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.
Parameters
----------
base - Can be anything which has meaningful subfields.
"""
return _TrueLookup(base)
Ref = _RefExpr
"""Unit tests for lookup."""
from muppet.lookup import lookup, Ref
def test_simple_lookup():
assert lookup(str).get('split').value() == str.split
assert lookup({'a': 'b'}).ref('a').value() == 'b'
assert lookup("Hello").idx(1).value() == 'e'
def test_simple_failing_lookups():
assert lookup(str).get('missing').value() is None
assert lookup(str).get('missing').get('x').value() is None
assert lookup(str).get('missing').ref('x').value() is None
assert lookup(str).get('missing').idx(0).value() is None
assert lookup(str).get('missing').find('Anything can go here').value() is None
def test_expressions():
# Missing field
assert not Ref('field').run({})
# Present field
assert Ref('field').run({'field': 'anything'})
# Equality on missing field
assert not (Ref('field') == 'anything').run({'not': 'else'})
# Equality on present field with different value
assert not (Ref('field') == 'anything').run({'field': 'else'})
# Equality on present field with expected value
assert (Ref('field') == 'anything').run({'field': 'anything'})
def test_find():
assert lookup([{'something': 'else'}, {'key': 'value'}]) \
.find(Ref('key')) \
.value() == {'key': 'value'}
assert lookup([{'something': 'else'}, {'key': 'value'}, {'key': '2'}]) \
.find(Ref('key') == '2') \
.ref('key') \
.value() == '2'
assert lookup([{'something': 'else'}, {'key': 'value'}]) \
.find(Ref('key') == '2') \
.ref('key') \
.value() is None
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment