aboutsummaryrefslogtreecommitdiff
path: root/muppet/lookup.py
blob: 0abb64479bd05c9e58f7d64dddd813e139f68f99 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
"""
[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