summaryrefslogtreecommitdiffstats
path: root/sploit/types/lict.py
blob: ab6cb1f8766f7163c92803dbe881878fa2287948 (plain) (blame)
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
201
202
from collections.abc import MutableSequence, MutableMapping

class Lict(MutableSequence, MutableMapping):
    """
    List / dictionary hybrid container

    Lict attempts to provide an API for a list which supports optional element
    keys in addition to normal positional indices.  For Lict, index types are
    int and slice.  Keys may be any other type except for None, as None
    indicates an unkeyed value.

    Nearly all of the operations you'd except to perform on list or dict are
    implemented here.  However, in cases of conflict, the list behavior is
    usually preferred (for example: iter(Lict) iterates over values instead of
    keys).  In general, Licts are slightly more list-like, since keys are
    optional, but sequence is required.

    Licts can be constructed from any iterable, including other Licts.  When
    constructing from mappings, similar heuristics as dict's are used to parse
    key values.

    In addition to keys and indices, slices may be used to return, modify, or
    delete a portion of a Lict.  The start and end fields of a slice may be
    either keys or indices, however the step field must be an integer as usual.

    When assigning to a non-existent key, the new element is sequentially
    inserted at the end of the Lict.
    """

    def __init__(self, values=None):
        """Construct new Lict, optionally populated by the given iterable."""
        self.__keys = []
        self.__vals = []
        if values is not None:
            self.extend(values)

    def __repr__(self):
        """Return human-readable Lict."""
        s = ""
        for i in range(len(self)):
            if self.__keys[i] is not None:
                s += f"{repr(self.__keys[i])}: "
            s += f"{repr(self.__vals[i])}, "
        return f"{{[{s[:-2]}]}}"

    def __copy__(self):
        """Return shallow copy of object."""
        return self.copy()

    def copy(self):
        """Return shallow copy of object."""
        return Lict(self)

    def key2idx(self, arg):
        """
        Return value of index type for the given input.

        For keys, return the corresponding index, or raise KeyError.
        For slices, return a slice with components converted to index type.

        If arg is already an index (or None) it is returned as-is with no
        assertions made.
        """
        if isinstance(arg, slice):
            return slice(self.key2idx(arg.start), self.key2idx(arg.stop), arg.step)
        if isinstance(arg, int) or arg is None:
            return arg
        try:
            return self.__keys.index(arg)
        except ValueError as ex:
            raise KeyError(f"Lict: Key does not exist: {arg}") from ex

    def idx2key(self, arg):
        """
        Return value of key type for the given input.

        For indices, return the corresponding key, None, or raise IndexError.
        For slices, return a slice with components converted to key type.

        If arg is already a key type (or None) it is returned as-is with no
        assertions made.
        """
        if isinstance(arg, slice):
            return slice(self.idx2key(arg.start), self.idx2key(arg.stop), arg.step)
        if not isinstance(arg, int) or arg is None:
            return arg
        return self.__keys[arg]

    def haskey(self, arg):
        """
        Test existence of key in Lict object.

        `x in Lict` only tests for the _value_ x in Lict.  This is consistent
        with list behavior.  This method is provided to test keys as well.
        Raises TypeError on any index type.
        """
        if arg is None:
            return False
        if isinstance(arg, (int, slice)):
            raise TypeError(f"Lict: Unsupported key type: {type(arg)}")
        return arg in self.__keys

    def __assign_slice(self, i, value):
        """Update Lict values according to element slice."""
        value = Lict(value)
        tmp = self.copy()
        tmp.__keys[i] = value.__keys
        tmp.__vals[i] = value.__vals

        check_keys = [ x for x in tmp.__keys if x is not None ]
        if len(check_keys) != len(set(check_keys)):
            raise ValueError("Lict: Slice assignment results in duplicate keys")

        self.__keys = tmp.__keys
        self.__vals = tmp.__vals

    # collections.abc abstract methods

    def __len__(self):
        """Return number of elements in Lict."""
        assert len(self.__keys) == len(self.__vals)
        return len(self.__keys)

    def __getitem__(self, arg):
        """Return value for given index, key, or slice."""
        i = self.key2idx(arg)
        if isinstance(i, slice):
            return Lict(zip(self.__keys[i], self.__vals[i]))
        return self.__vals[i]

    def __setitem__(self, arg, value):
        """Set value for given index, key, or slice."""
        try:
            i = self.key2idx(arg)
        except KeyError:
            self.append(value, arg)
        else:
            if isinstance(i, slice):
                self.__assign_slice(i, value)
            else:
                self.__vals[i] = value

    def __delitem__(self, arg):
        """Delete value for given index, key, or slice."""
        i = self.key2idx(arg)
        del self.__keys[i]
        del self.__vals[i]

    def insert(self, where, value, key=None):
        """Insert value into Lict.  Optionally apply the given key."""
        if self.haskey(key):
            raise KeyError(f"Lict: Key is not unique: {key}")
        i = self.key2idx(where)
        self.__keys.insert(i, key)
        self.__vals.insert(i, value)

    # Sequence overrides

    def append(self, value, key=None):
        """Append value to Lict.  Optionally apply the given key."""
        self.insert(len(self), value, key)

    def extend(self, values):
        """Append all values in given iterable to the Lict."""
        try: values = [ [k, v] for k, v in values.items() ]
        except:
            try: values = [ [k, v] for k, v in iter(values) ]
            except: values = [ [None, v] for v in iter(values) ]
        for k, v in values:
            self.append(v, k)

    def reverse(self):
        """Reverse the sequence of Lict in-place.  Keys follow values."""
        self.__keys.reverse()
        self.__vals.reverse()

    # Mapping overrides

    def get(self, key, default=None):
        """Return value for given key, or default if unable."""
        try: return self[key]
        except: return default

    def popitem(self):
        """Pop a key-value pair from the Lict and return it."""
        return (self.__keys.pop(), self.__vals.pop())

    def items(self):
        """Return an iterable of key-value pairs."""
        return list(zip(self.__keys, self.__vals))

    def keys(self):
        """Return an iterable of the Lict's keys."""
        return [ x for x in self.__keys if x is not None ]

    def values(self):
        """Return an iterable of the Lict's values."""
        return list(self.__vals)

    def update(self, values):
        """Method is unsupported."""
        raise NotImplementedError