1import itertools
2import json
3import pkgutil
4import re
5
6from jsonschema.compat import MutableMapping, str_types, urlsplit
7
8
9class URIDict(MutableMapping):
10    """
11    Dictionary which uses normalized URIs as keys.
12    """
13
14    def normalize(self, uri):
15        return urlsplit(uri).geturl()
16
17    def __init__(self, *args, **kwargs):
18        self.store = dict()
19        self.store.update(*args, **kwargs)
20
21    def __getitem__(self, uri):
22        return self.store[self.normalize(uri)]
23
24    def __setitem__(self, uri, value):
25        self.store[self.normalize(uri)] = value
26
27    def __delitem__(self, uri):
28        del self.store[self.normalize(uri)]
29
30    def __iter__(self):
31        return iter(self.store)
32
33    def __len__(self):
34        return len(self.store)
35
36    def __repr__(self):
37        return repr(self.store)
38
39
40class Unset(object):
41    """
42    An as-of-yet unset attribute or unprovided default parameter.
43    """
44
45    def __repr__(self):
46        return "<unset>"
47
48
49def load_schema(name):
50    """
51    Load a schema from ./schemas/``name``.json and return it.
52    """
53
54    data = pkgutil.get_data("jsonschema", "schemas/{0}.json".format(name))
55    return json.loads(data.decode("utf-8"))
56
57
58def indent(string, times=1):
59    """
60    A dumb version of `textwrap.indent` from Python 3.3.
61    """
62
63    return "\n".join(" " * (4 * times) + line for line in string.splitlines())
64
65
66def format_as_index(indices):
67    """
68    Construct a single string containing indexing operations for the indices.
69
70    For example, [1, 2, "foo"] -> [1][2]["foo"]
71
72    Arguments:
73
74        indices (sequence):
75
76            The indices to format.
77    """
78
79    if not indices:
80        return ""
81    return "[%s]" % "][".join(repr(index) for index in indices)
82
83
84def find_additional_properties(instance, schema):
85    """
86    Return the set of additional properties for the given ``instance``.
87
88    Weeds out properties that should have been validated by ``properties`` and
89    / or ``patternProperties``.
90
91    Assumes ``instance`` is dict-like already.
92    """
93
94    properties = schema.get("properties", {})
95    patterns = "|".join(schema.get("patternProperties", {}))
96    for property in instance:
97        if property not in properties:
98            if patterns and re.search(patterns, property):
99                continue
100            yield property
101
102
103def extras_msg(extras):
104    """
105    Create an error message for extra items or properties.
106    """
107
108    if len(extras) == 1:
109        verb = "was"
110    else:
111        verb = "were"
112    return ", ".join(repr(extra) for extra in extras), verb
113
114
115def types_msg(instance, types):
116    """
117    Create an error message for a failure to match the given types.
118
119    If the ``instance`` is an object and contains a ``name`` property, it will
120    be considered to be a description of that object and used as its type.
121
122    Otherwise the message is simply the reprs of the given ``types``.
123    """
124
125    reprs = []
126    for type in types:
127        try:
128            reprs.append(repr(type["name"]))
129        except Exception:
130            reprs.append(repr(type))
131    return "%r is not of type %s" % (instance, ", ".join(reprs))
132
133
134def flatten(suitable_for_isinstance):
135    """
136    isinstance() can accept a bunch of really annoying different types:
137        * a single type
138        * a tuple of types
139        * an arbitrary nested tree of tuples
140
141    Return a flattened tuple of the given argument.
142    """
143
144    types = set()
145
146    if not isinstance(suitable_for_isinstance, tuple):
147        suitable_for_isinstance = (suitable_for_isinstance,)
148    for thing in suitable_for_isinstance:
149        if isinstance(thing, tuple):
150            types.update(flatten(thing))
151        else:
152            types.add(thing)
153    return tuple(types)
154
155
156def ensure_list(thing):
157    """
158    Wrap ``thing`` in a list if it's a single str.
159
160    Otherwise, return it unchanged.
161    """
162
163    if isinstance(thing, str_types):
164        return [thing]
165    return thing
166
167
168def equal(one, two):
169    """
170    Check if two things are equal, but evade booleans and ints being equal.
171    """
172    return unbool(one) == unbool(two)
173
174
175def unbool(element, true=object(), false=object()):
176    """
177    A hack to make True and 1 and False and 0 unique for ``uniq``.
178    """
179
180    if element is True:
181        return true
182    elif element is False:
183        return false
184    return element
185
186
187def uniq(container):
188    """
189    Check if all of a container's elements are unique.
190
191    Successively tries first to rely that the elements are hashable, then
192    falls back on them being sortable, and finally falls back on brute
193    force.
194    """
195
196    try:
197        return len(set(unbool(i) for i in container)) == len(container)
198    except TypeError:
199        try:
200            sort = sorted(unbool(i) for i in container)
201            sliced = itertools.islice(sort, 1, None)
202            for i, j in zip(sort, sliced):
203                if i == j:
204                    return False
205        except (NotImplementedError, TypeError):
206            seen = []
207            for e in container:
208                e = unbool(e)
209                if e in seen:
210                    return False
211                seen.append(e)
212    return True
213