Skip to content
GitLab
Explore
Sign in
Register
Primary navigation
Search or go to…
Project
M
muppet-strings
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Releases
Package registry
Container registry
Model registry
Operate
Environments
Terraform modules
Monitor
Incidents
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
Hugo Hörnquist
muppet-strings
Commits
b2898df7
Commit
b2898df7
authored
1 year ago
by
Hugo Hörnquist
Browse files
Options
Downloads
Patches
Plain Diff
Introduce lookup.
parent
44f37a85
No related branches found
No related tags found
No related merge requests found
Changes
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
muppet/lookup.py
+200
-0
200 additions, 0 deletions
muppet/lookup.py
tests/test_lookup.py
+46
-0
46 additions, 0 deletions
tests/test_lookup.py
with
246 additions
and
0 deletions
muppet/lookup.py
0 → 100644
+
200
−
0
View file @
b2898df7
"""
[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
This diff is collapsed.
Click to expand it.
tests/test_lookup.py
0 → 100644
+
46
−
0
View file @
b2898df7
"""
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
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment