File: path.py

package info (click to toggle)
python-prance 25.4.8.0%2Bds1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 6,140 kB
  • sloc: python: 3,381; makefile: 205
file content (208 lines) | stat: -rw-r--r-- 6,909 bytes parent folder | download
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
203
204
205
206
207
208
"""This module contains code for accessing values in nested data structures."""

__author__ = "Jens Finkhaeuser"
__copyright__ = "Copyright (c) 2018 Jens Finkhaeuser"
__license__ = "MIT"
__all__ = ()


def _json_ref_escape(path):
    """JSON-reference escape object path."""
    path = str(path)  # Could be an int, etc.
    path = path.replace("~", "~0")
    path = path.replace("/", "~1")
    return path


def _str_path(path):
    """Stringify object path."""
    return "/" + "/".join([_json_ref_escape(p) for p in path])


def path_get(obj, path, defaultvalue=None, path_of_obj=()):
    """
    Retrieve the value from obj indicated by path.

    Like dict.get(), except:

      - Any Mapping or Sequence is supported.
      - Path is itself a Sequence; the first part is applied to the passed
        object, the second part to the value returned from this operation, and
        so forth recursively.

    :param mixed obj: The Sequence or Mapping from which to retrieve values.
    :param Sequence path: A Sequence of zero or more key/index elements.
    :param mixed defaultvalue: If the value at the path does not exist and this
      parameter is not None, it is returned. Otherwise an error is raised.
    """
    from collections.abc import Mapping, Sequence

    # For error reporting.
    path_of_obj_str = _str_path(path_of_obj)

    if path is not None and not isinstance(path, Sequence):
        raise TypeError(f"Path is a {type(path)}, but must be None or a Collection!")

    if isinstance(obj, Mapping):
        if path is None or len(path) < 1:
            return obj or defaultvalue

        if path[0] not in obj:
            raise KeyError(
                'Object at "{}" does not contain key: {}'.format(
                    path_of_obj_str, path[0]
                )
            )

        return path_get(
            obj[path[0]], path[1:], defaultvalue, path_of_obj=path_of_obj + (path[0],)
        )

    elif isinstance(obj, Sequence):
        if path is None or len(path) < 1:
            return obj or defaultvalue

        try:
            idx = int(path[0])
        except ValueError:
            raise KeyError(
                'Sequence at "%s" needs integer indices only, but got: '
                "%s"
                % (
                    path_of_obj_str,
                    path[0],
                )
            )

        if idx < 0 or idx >= len(obj):
            raise IndexError(
                'Index out of bounds for sequence at "%s": %d' % (path_of_obj_str, idx)
            )

        return path_get(
            obj[idx], path[1:], defaultvalue, path_of_obj=path_of_obj + (path[0],)
        )

    else:
        # Path must be empty.
        if path is not None and len(path) > 0:
            raise TypeError(f"Cannot get anything from type {type(obj)}!")
        return obj or defaultvalue


def path_set(obj, path, value, **options):
    """
    Set the value in obj indicated by path.

    Setter anologous to path_get() above.

    As setting values is a write operation, this function optionally creates
    intermediate objects to ensure all elements of path can be dereferenced.

    :param mixed obj: The Sequence or Mapping from which to retrieve values.
    :param Sequence path: A Sequence of zero or more key/index elements.
    :param mixed value: The value to set.
    :param bool create: [optional] Flag indicating whether to create
      intermediate values or not. Defaults to False.
    """
    # Retrieve options
    create = options.get("create", False)

    def fill_sequence(seq, index, value_index_type):
        """
        Fill the sequence seq with elements until index can be accessed.

        Fills with None except for the indexed element. That is either a dict or
        a list, depending on the value_index_type. If the latter is an int, a
        list is added. If the latter is None (unknown), None is added. Otherwise
        a dict is added.
        """
        if len(seq) > index:
            return

        while len(seq) < index:
            seq.append(None)

        if value_index_type == int:
            seq.append([])
        elif value_index_type is None:
            seq.append(None)
        else:
            seq.append({})

    def safe_idx(seq, index):
        """
        Safely index a sequence.

        Much like dict.get with default value, except returns None instead of
        raising IndexError.
        """
        try:
            return type(seq[index])
        except IndexError:
            return None

    # print('obj', obj, type(obj))
    # print('path', path)
    # print('value', value)

    from collections.abc import Sequence, MutableSequence, Mapping, MutableMapping

    if path is not None and not isinstance(path, Sequence):
        raise TypeError(f"Path is a {type(path)}, but must be None or a Collection!")

    if len(path) < 1:
        raise KeyError("Cannot set with an empty path!")

    if isinstance(obj, Mapping):
        # If we don't have a mutable mapping, we should raise a TypeError
        if not isinstance(obj, MutableMapping):  # pragma: nocover
            raise TypeError(f"Mapping is not mutable: {type(obj)}")

        # If the path has only one element, we just overwrite the element at the
        # given key. Otherwise we recurse.
        if len(path) == 1:
            if not create and path[0] not in obj:
                # dicts would normally silently create, but we have to make it
                # explicit to fulfil our contract.
                raise KeyError(f'Key "{path[0]}" not in Mapping!')
            obj[path[0]] = value
        else:
            if create and path[0] not in obj:
                if type(path[1]) == int:
                    obj[path[0]] = []
                else:
                    obj[path[0]] = {}
            path_set(obj[path[0]], path[1:], value, create=create)

        return obj

    elif isinstance(obj, Sequence):
        idx = path[0]

        # If we don't have a mutable sequence, we should raise a TypeError
        if not isinstance(obj, MutableSequence):
            raise TypeError(f"Sequence is not mutable: {type(obj)}")

        # Ensure integer indices
        try:
            idx = int(idx)
        except ValueError:
            raise KeyError("Sequences need integer indices only.")

        # If we're supposed to create and the index at path[0] doesn't exist,
        # then we need to push some dummy objects.
        if create:
            fill_sequence(obj, idx, safe_idx(path, 1))

        # If the path has only one element, we just overwrite the element at the
        # given index. Otherwise we recurse.
        # print('pl', len(path))
        if len(path) == 1:
            obj[idx] = value
        else:
            path_set(obj[idx], path[1:], value, create=create)

        return obj
    else:
        raise TypeError(f"Cannot set anything on type {type(obj)}!")