sanest

sane nested dictionaries and lists

Sample JSON input:

{
  "data": {
    "users": [
      {"id": 12, "name": "alice"},
      {"id": 34, "name": "bob"}
    ]
  }
}

Without sanest:

d = json.loads(...)
for user in d['data']['users']:
    print(user['name'])

With sanest:

d = json.loads(...)
wrapped = sanest.dict.wrap(d)
for user in wrapped['data', 'users':[dict]]:
    print(user['name':str])

The code is now type-safe and will fail fast on unexpected input data.

Table of contents

Overview

sanest is a Python library that makes it easy to consume, produce, or modify nested JSON structures in a strict and type-safe way. It provides two container data structures, specifically designed for the JSON data model:

These are thin wrappers around the built-in dict and list, with minimal overhead and an almost identical API, but with a few new features that the built-in containers do not have:

  • nested operations
  • type checking
  • data model restrictions

These features are very easy to use: with minimal code additions, otherwise implicit assumptions about the nesting structure and the data types can be made explicit, adding type-safety and robustness.

sanest is not a validation library. It aims for the sweet spot between ‘let’s hope everything goes well’ (if not, unexpected crashes or undetected buggy behaviour ensues) and rigorous schema validation (lots of work, much more code).

In practice, sanest is especially useful when crafting requests for and processing responses from third-party JSON-based APIs, but is by no means limited to this use case.

Installation

Use pip to install sanest into a virtualenv:

pip install sanest

sanest requires Python 3.3+ and has no additional dependencies.

Why sanest?

Consider this JSON data structure, which is a stripped-down version of the example JSON response from the GitHub issues API documentation:

{
  "id": 1,
  "state": "open",
  "title": "Found a bug",
  "user": {
    "login": "octocat",
    "id": 1,
  },
  "labels": [
    {
      "id": 208045946,
      "name": "bug"
    }
  ],
  "milestone": {
    "id": 1002604,
    "state": "open",
    "title": "v1.0",
    "creator": {
      "login": "octocat",
      "id": 1,
    },
    "open_issues": 4,
    "closed_issues": 8,
  }
}

The following code prints all labels assigned to this issue, using only lowercase letters:

>>> issue = json.load(...)
>>> for label in issue['labels']:
...     print(label['name'].lower())
bug

Hidden asssumptions.       The above code is very straight-forward and will work fine for valid input documents. It can fail in many subtle ways though on input data does not have the exact same structure as the example document, since this code makes quite a few implicit assumptions about its input:

  • The result of json.load() is a dictionary.
  • The labels field exists.
  • The labels field points to a list.
  • This list contains zero or more dictionaries.
  • These dictionaries have a name field.
  • The name field points to a string.

When presented with input data fow which these assumptions do not hold, various things can happen. For instance:

  • Accessing d['labels'] raises KeyError when the field is missing.

  • Accessing d['labels'] raises TypeError if it is not a dict.

    The actual exception messages vary and can be confusing:

    • TypeError: string indices must be integers
    • TypeError: list indices must be integers or slices, not str
    • TypeError: 'NoneType' object is not subscriptable
  • If the labels field is not a list, the for loop may raise a TypeError, but not in all cases.

    If labels contained a string or a dictionary, the for loop will succeed, since strings and dictionaries are iterable, and loop over the individual characters of this string or over the keys of the dictionary. This was not intended, but will not raise an exception.

    In this example, the next line will crash, since the label['name'] lookup will fail with a TypeError telling that string indices must be integers, but depending on the code everything may seem fine even though it really is not.

The above is not an exhaustive list of things that can go wrong with this code, but it gives a pretty good overview.

Validation.       One approach of safe-guarding against the issues outlined above would be to write validation code. There are many validation libraries, such as jsonschema, Marshmallow, Colander, Django REST framework, and many others, that are perfectly suitable for this task.

The downside is that writing the required schema definitions is a lot of work. A strict validation step will also make the code much larger and hence more complex. Especially when dealing with data formats that are not ‘owned’ by the application, e.g. when interacting with a third-party REST API, this may be a prohibitive amount of effort.

In the end, rather than going through all this extra effort, it may be simpler to just use the code above as-is and hope for the best.

The sane approach.       However, there are more options than full schema validation and no validation at all. This is what sanest aims for: a sane safety net, without going overboard with upfront validation.

Here is the equivalent code using sanest:

>>> issue = sanest.dict.wrap(json.loads(...))   # 1
>>> for user in issue['labels':[dict]]:         # 2
...     print(label['name':str].lower())        # 3
bug

While the usage of slice syntax for dictionary lookups and using the built-in types directly (e.g. str and dict) may look a little surprising at first, the code is actually very readable and explicit.

Here is what it does:

  1. Create a thin dict wrapper.

    This ensures that the input is a dictionary, and enables the type checking lookups used in the following lines of code.

  2. Look up the labels field.

    This ensures that the field contains a list of dictionaries. ‘List of dictionaries’ is condensely expressed as [dict], and passed to the d[…] lookup using slice syntax (with a colon).

  3. Print the lowercase value of the name field.

    This checks that the value is a string before calling .lower() on it.

This code still raises KeyError for missing fields, but any failed check will immediately raise a very clear exception with a meaningful message detailing what went wrong.

Data model

The JSON data model is restricted, and sanest strictly adheres to it. sanest uses very strict type checks and will reject any values not conforming to this data model.

Containers.       There are two container types, which can have arbitrary nesting to build more complex structures:

In a dictionary, each item is a (key, value) pair, in which the key is a unique string (str). In a list, values have an associated index, which is an integer counting from zero.

Leaf values.       Leaf values are restricted to:

  • strings (str)
  • integer numbers (int)
  • floating point numbers (float)
  • booleans (bool)
  • None (no value, encoded as null in JSON)

Basic usage

sanest provides two classes, sanest.dict and sanest.list, that behave very much like the built-in dict and list, supporting all the regular operations such as getting, setting, and deleting items.

To get started, import the sanest module:

import sanest

Dictionary.       The sanest.dict constructor behaves like the built-in dict constructor:

d = sanest.dict(regular_dict_or_mapping)
d = sanest.dict(iterable_with_key_value_pairs)
d = sanest.dict(a=1, b=2)

Usage examples (see API docs for details):

d = sanest.dict(a=1, b=2)
d['a']
d['c'] = 3
d.update(d=4)
d.get('e', 5)
d.pop('f', 6)
del d['a']
for v in d.values():
    print(v)
d.clear()

List.       The sanest.list constructor behaves like the built-in list constructor:

l = sanest.list(regular_list_or_sequence)
l = sanest.list(iterable)

Usage examples (see API docs for details):

l = sanest.list([1, 2])
l[0]
l.append(3)
l.extend([4, 5])
del l[0]
for v in l():
    print(v)
l.pop()
l.count(2)
l.sort()
l.clear()

Container values.       Operations that return a nested dictionary or list will always be returned as a sanest.dict or sanest.list:

>>> issue['user']
sanest.dict({"login": "octocat", "id": 1})

Operations that accept a container value from the application, will accept regular dict and list instances, as well as sanest.dict and sanest.list instances:

>>> d = sanest.dict()
>>> d['x'] = {'a': 1, 'b': 2}
>>> d['y'] = sanest.dict({'a': 1, 'b': 2})

Nested operations

In addition to normal dictionary keys (str) and list indices (int), sanest.dict and sanest.list can operate directly on values in a nested structure. Nested operations work like normal container operations, but instead of a single key or index, they use a path that points into nested dictionaries and lists.

Path syntax.       A path is simply a sequence of strings (dictionary keys) and integers (list indices). Here are some examples for the Github issue JSON example from a previous section:

'user', 'login'
'labels', 0, 'name'
'milestone', 'creator', 'login'

A string-only syntax for paths (such as a.b.c or a/b/c) is not supported, since all conceivable syntaxes have drawbacks, and it is not up to sanest to make choices here.

Getting, setting, deleting.       For getting, setting, and deleting items, paths can be used directly inside square brackets:

>>> d = sanest.dict(...)
>>> d['a', 'b', 'c'] = 123
>>> d['a', 'b', 'c']
123
>>> del d['a', 'b', 'c']

Alternatively, paths can be specified as a list or tuple instead of the inline syntax:

>>> path = ['a', 'b', 'c']
>>> d[path] = 123
>>> path = ('a', 'b', 'c')
>>> d[path]
123

Other operations.       For the method based container operations taking a key or index, such as sanest.dict.get() or sanest.dict.pop(), paths must always be passed as a list or tuple:

>>> d.get(['a', 'b', 'c'], "default value")

Containment checks.       The in operator that checks whether a dictionary key exists, also works with paths:

>>> ['milestone', 'creator', 'login'] in issue
True
>>> ['milestone', 'creator', 'xyz'] in issue
False
>>> ['labels', 0] in issue
True
>>> ['labels', 123] in issue
False

Automatic creation of nested structures.       When setting a nested dictionary key that does not yet exist, the structure is automatically created by instantiating a fresh dictionary at each level of the path. This is sometimes known as autovivification:

>>> d = sanest.dict()
>>> d['a', 'b', 'c'] = 123
>>> d
sanest.dict({'a': {'b': {'c': 123}}})
>>> d.setdefault(['a', 'e', 'f'], 456)
456
>>> d
sanest.dict({'a': {'b': {'c': 123}, 'e': {'f': 456}}})

This only works for paths pointing to a dictionary key, not for lists (since padding with None values is seldom useful), but of course it will traverse existing lists just fine:

>>> d = sanest.dict({'items': [{'name': "a"}, {'name': "b"}]})
>>> d['items', 1, 'x', 'y', 'z'] = 123
>>> d['items', 1]
sanest.dict({'x': {'y': {'z': 123}}, 'name': 'b'})

Type checking

In addition to the basic validation to ensure that all values adhere to the JSON data model, almost all sanest.dict and sanest.list operations support explicit type checks.

Getting, setting, deleting.       For getting, setting, and deleting items, type checking uses slice syntax to indicate the expected data type:

>>> issue['id':int]
1
>>> issue['state':str]
'open'

Path lookups can be combined with type checking:

>>> issue['user', 'login':str]
'octocat'
>>> path = ['milestone', 'creator', 'id']
>>> issue[path:int]
1

Other operations.       Other methods use a more conventional approach by accepting a type argument:

>>> issue.get('id', type=int)
1
>>> issue.get(['user', 'login'], type=str)
'octocat'

Containment checks.       The in operator does not allow for slice syntax, so instead it uses a normal list with the type as the last item:

>>> ['id', int] in issue
True
>>> ['id', str] in issue
False

This also works with paths:

>>> ['user', 'login', str] in issue
True
>>> path = ['milestone', 'creator', 'id']
>>> [path, int] in issue
True
>>> [path, bool] in issue
False

Extended types.       In its simplest form, the type argument is just the built-in type: bool, float, int, str, dict, list. This works well for simple types, but for containers, only stating that ‘the application expects a list’ is often not good enough.

Typically lists are homogeneous, meaning that all values have the same type, and sanest can check this in one go. The syntax for checking the types of list values is a list containing a type, such as [dict] or [str]. For example, to ensure that a field contains a list of dictionaries:

>>> issue['labels':[dict]]
sanest.list([{"id": 208045946, "name": "bug"}])

To keep it sane, this approach cannot be used recursively, but then, nested lists are not that common anyway.

For dictionaries, sanest offers similar functionality. Its usefulness is limited, since it is not very common for dictionary values to all have the same type. (Note that dictionary keys are always strings.) The syntax is a literal dictionary with one key/value pair, in which the key is always the literal str, such as {str: int} or {str: bool}. For example, to ensure that all values in the dictionary pointed to by the path 'a', 'b', 'c' are integers:

d['a', 'b', 'c':{str: int}]

Checking container values.       To explicitly check that all values in a container have the same type, use sanest.list.check_types() or sanest.dict.check_types(), which take a type argument:

l = sanest.list()
l.append(1)
l.append(2)
l.append(3)
l.check_types(type=int)

Such explicit type checks may also help increasing code clarity, since it decouples type checking from container operations. For example, this combined lookup and type check:

>>> labels = issue['labels':[dict]]

…can also be written as:

>>> labels = issue['labels':list]
>>> labels.check_types(type=dict)

Type-safe iteration.       It is very common to iterate over a list of values that all have the same type, e.g. a list of strings. One way to do this would be:

>>> l = sanest.list(...)
>>> l.check_types(type=str)
>>> for value in l:
...     pass

The sanest.list.iter() method offers a more concise way to do the same:

>>> l = sanest.list(...)
>>> for value in l.iter(type=str):
...     pass

If the list was obtained from a lookup in another container, the type check can be combined with the lookup:

>>> for value in parent['values':list].iter(type=str):
...     pass

…or even shorter:

>>> for value in parent['values':[str]]:
...     pass

For dictionaries with homogeneously typed values, sanest.dict.values() and sanest.dict.items() offer the same functionality. For example,

>>> d = sanest.dict(...)
>>> d.check_types(type=int)
>>> for value in d.values():
...     pass
>>> for key, value in d.items():
...     pass

…can be shortened to the equivalent:

>>> d = sanest.dict(...)
>>> for value in d.values(type=int):
...     pass
>>> for key, value in d.items(type=int):
...     pass

Wrapping

Both sanest.dict and sanest.list are thin wrappers around a regular dict or list. All container operations (getting, setting, and so on) accept both regular containers and sanest containers when those are passed in by the application, and transparently ‘wrap’ any lists or dictionaries returned to the application.

For nested structures, only the outermost dict or list is wrapped: the nested structure is not changed in any way. In practice this means that the overhead of using sanest is very small, since internally all nested structures are just as they would be in regular Python.

Wrapping existing containers.       The sanest.dict and sanest.list constructors create a new container, and make a shallow copy when an existing dict or list is passed to it, analogous to the behaviour of the built-in dict and list.

sanest can also wrap an existing dict or list without making a copy, using the classmethods sanest.dict.wrap() and sanest.list.wrap(), that can be used as alternate constructors:

d = sanest.dict.wrap(existing_dict)
l = sanest.list.wrap(existing_list)

By default, wrap() recursively validates that the data structure matches the JSON data model. In some cases, these checks are not necessary, and can be skipped for performance reasons. A typical example is freshly deserialised JSON data:

d = sanest.dict.wrap(json.loads(...), check=False)
l = sanest.list.wrap(json.loads(...), check=False)

Unwrapping.       The reverse process is unwrapping: to obtain a plain dict or list, use sanest.dict.unwrap() or sanest.list.unwrap(), which will return the original objects:

normal_dict = d.unwrap()
normal_list = l.unwrap()

Unwrapping is typically done at the end of a piece of code, when a regular dict or list is required, e.g. right before serialisation:

json.dumps(d.unwrap())

Unwrapping is a very cheap operation and does not make any copies.

Localised use.       Wrapping an existing dict or list is also a very useful way to use sanest only in selected places in an application, e.g. in a function that modifies a regular dict that is passed to it, without any other part of the application being aware of sanest at all:

def set_fields(some_dict, num, flag):
    """
    Set a few fields in `some_dict`. This modifies `some_dict` in-place.
    """
    wrapped = sanest.dict.wrap(some_dict)
    wrapped["foo", "bar":int] = num * 2
    wrapped.setdefault(["x", "y"], type=bool) = flag

Error handling

sanest has very strict error handling, and raises predictable exceptions with a clear error message whenever an operation cannot be completed successfully.

In general, an operation can fail because of three reasons:

  • Missing or incomplete data, e.g. a key does not exist.
  • Problematic data, e.g wrong structure or an unexpected data type.
  • Problematic code, e.g. a malformed path.

Exceptions for missing data.       It is normal for applications to deal with missing values, for instance by falling back to a default value. For missing data, sanest uses the same exceptions as the regular Python dictionaries and lists:

  • Dictionary lookups may raise KeyError.
  • List lookups may raise IndexError.

Python also provides the not so widely used LookupError, which is a parent class of both. The exception hierarchy is:

  • Exception (built-in exception)
    • LookupError (built-in exception)
      • KeyError (built-in exception)
      • IndexError (built-in exception)

Below are some examples for the Github issue JSON example. Note that the error messages contain the (partial) path where the error occurred.

>>> issue['labels', 0, 'name']
'bug'

>>> issue['xyz', 'a', 'b', 'c']
Traceback (most recent call last):
...
KeyError: ['xyz']
>>> issue['labels', 0, 'xyz']
Traceback (most recent call last):
...
KeyError: ['labels', 0, 'xyz']
>>> issue['labels', 123, 'name']
Traceback (most recent call last):
...
IndexError: ['labels', 123]

To catch either KeyError or IndexError, use LookupError. Example:

try:
    first_label_name = issue['labels', 0, 'name':str]
except LookupError:
    ...

This except clause handles the following cases:

  • The labels field is missing.
  • The labels field exists, but is empty.
  • The name field is missing from the first dictionary in the labels list.

Exceptions for problematic data.       sanest can be used for basic input validation. When data does not match what the code expects, this typically means input data is malformed, and applications could for instance return an error response from an exception handler.

Data errors indicate either an invalid structure, or an invalid value. sanest uses two exceptions here: sanest.InvalidStructureError and sanest.InvalidValueError. Both share a common ancestor, sanest.DataError, which in turns inherits from the standard Python ValueError. The exception hierarchy is:

Below are some examples for the Github issue JSON sample.

>>> issue['milestone', 'creator', 'login']
'octocat'

>>> issue['milestone', 'creator', 'login':int]
Traceback (most recent call last):
...
InvalidValueError: expected int, got str at path ['milestone', 'creator', 'login']: 'octocat'
>>> issue['title':str] = ["This", "is", "a", {"malformed": "title"}]
Traceback (most recent call last):
  ...
InvalidValueError: expected str, got list: ['This', 'is', 'a', {'malformed': 'title'}]
>>> issue['labels']
sanest.list([{'name': 'bug', 'id': 208045946}])

>>> issue['labels', 'xyz']
Traceback (most recent call last):
...
InvalidStructureError: expected dict, got list at subpath ['labels'] of ['labels', 'xyz']

The generic sanest.DataError is never raised directly, but can be caught if the application does not care whether the source of the problem was an invalid structure or an invalid value:

try:
    first_label_name = issue['labels', 0, 'name':str]
except sanest.DataError:  # or just ValueError
    ...

Since sanest.DataError inherits from the built-in ValueError, applications can also catch ValueError instead of exceptions specific to sanest, which, depending on how the application code is organised, means that some modules may not require any sanest imports at all.

Exceptions for problematic code.       The following exceptions are typically the result of incorrect code, and hence should generally not be caught. The hierarchy is:

Examples:

>>> path = [True, True, True]
>>> d[path]
Traceback (most recent call last):
...
InvalidPathError: path must contain only str or int: [True, True, True]
>>> d.get('title', 'This is the default.', type="oops")
Traceback (most recent call last):
...
InvalidTypeError: expected dict, list, bool, float, int, str, [...] (for lists) or {str: ...} (for dicts), got 'oops'

API

Dictionary

class sanest.dict(*args, **kwargs)

dict-like container supporting nested lookups and type checking.

wrap(d, *, check=True)

Wrap an existing dictionary without making a copy.

Parameters:
  • d – existing dictionary
  • bool (check) – whether to perform basic validation
unwrap()

Return a regular dict without making a copy.

This sanest.dict can be safely used afterwards as long as the returned dictionary is not modified in an incompatible way.

fromkeys(iterable, value=None)

Like dict.fromkeys().

Parameters:
  • iterable – iterable of keys
  • value – initial value
d[path_like]
__getitem__(path_like)

Look up the item that path_like (with optional type) points to.

get(path_like, default=None, *, type=None)

Get a value or a default value; like dict.get().

Parameters:
  • path_like – key or path to look up
  • default – default value to return for failed lookups
  • type – expected type
d[path_like] = value
__setitem__(path_like, value)

Set the item that path_like (with optional type) points to.

setdefault(path_like, default=None, *, type=None)

Get a value or set (and return) a default; like dict.setdefault().

Parameters:
  • path_like – key or path
  • default – default value to return for failed lookups
  • type – expected type
update(*args, **kwargs)

Update with new items; like dict.update().

del d[path_like]
__delitem__(path_like)

Delete the item that path_like (with optional type) points to.

pop(path_like, default=<missing>, *, type=None)

Remove an item and return its value; like dict.pop().

Parameters:
  • path_like – key or path
  • default – default value to return for failed lookups
  • type – expected type
popitem(*, type=None)

Remove and return a random item; like dict.popitem().

Parameters:type – expected type
clear()

Remove all items; like dict.clear().

path_like in d
__contains__(path_like)

Check whether path_like (with optional type) points to an existing value.

len(d)
__len__()

Return the number of items in this container.

iter(d)
__iter__()

Iterate over the keys of this dictionary.

keys()

Return a dictionary view over the keys; like dict.keys().

values(*, type=None)

Return a dictionary view over the values; like dict.values().

Parameters:type – expected type
items(*, type=None)

Return a dictionary view over the items; like dict.items().

Parameters:type – expected type
copy(*, deep=False)

Make a copy of this container.

By default this return a shallow copy. When deep is True, this returns a deep copy.

Parameters:bool (deep) – whether to make a deep copy
check_types(*, type)

Check the type of all values in this dictionary.

Parameters:type – expected type
d == other
__eq__(other)

Determine whether this container and other have the same values.

d != other
__ne__(other)

Determine whether this container and other have different values.

List

class sanest.list(*args)

list-like container supporting nested lookups and type checking.

wrap(l, *, check=True)

Wrap an existing list without making a copy.

Parameters:
  • l – existing list
  • bool (check) – whether to perform basic validation
unwrap()

Return a regular list without making a copy.

This sanest.list can be safely used afterwards as long as the returned list is not modified in an incompatible way.

l[path_like]
__getitem__(path_like)

Look up the item that path_like (with optional type) points to.

index(value, start=0, stop=None, *, type=None)

Get the index of value; like list.index().

Parameters:
  • value – value to look up
  • start – start index
  • stop – stop index
  • type – expected type
count(value, *, type=None)

Count how often value occurs; like list.count().

Parameters:
  • value – value to count
  • type – expected type
l[path_like] = value
__setitem__(path_like, value)

Set the item that path_like (with optional type) points to.

insert(index, value, *, type=None)

Insert a value; like list.insert().

Parameters:
  • index – position to insert at
  • value – value to insert
  • type – expected type
append(value, *, type=None)

Append a value; like list.append().

Parameters:
  • value – value to append
  • type – expected type
l + other
__add__(other)

Return a new list with the concatenation of this list and other.

l += other
__iadd__(other)
extend(iterable, *, type=None)

Extend with values from iterable; like list.extend().

Parameters:
  • iterable – iterable of values to append
  • type – expected type
l * n
__mul__(n)

Return a new list containing n copies of this list.

del l[path_like]
__delitem__(path_like)

Delete the item that path_like (with optional type) points to.

pop(path_like=-1, *, type=None)

Remove and return an item; like list.pop().

Parameters:
  • path_like – position to look up
  • type – expected type
remove(value, *, type=None)

Remove an item; like list.remove().

Parameters:
  • value – value to remove
  • type – expected type
clear()

Remove all items; like list.clear().

path_like in l
__contains__(value)

Check whether value is contained in this list.

contains(value, *, type=None)

Check whether value is contained in this list.

This is the same as value in l but allows for a type check.

Parameters:type – expected type
len(l)
__len__()

Return the number of items in this container.

iter(l)
__iter__()

Iterate over the values in this list.

reversed(l)
__reversed__()

Return an iterator in reversed order.

sort(key=None, reverse=False)

Sort in-place; like list.sort().

Parameters:
  • key – callable to make a sort key
  • reverse – whether to sort in reverse order
copy(*, deep=False)

Make a copy of this container.

By default this return a shallow copy. When deep is True, this returns a deep copy.

Parameters:bool (deep) – whether to make a deep copy
check_types(*, type)

Check the type of all values in this list.

Parameters:type – expected type
l == other
__eq__(other)

Determine whether this container and other have the same values.

l != other
__ne__(other)

Determine whether this container and other have different values.

l1 < l2
l1 > l2
l1 <= l2
l1 >= l2

Compare lists.

Exceptions

exception sanest.DataError

Bases: ValueError

Exception raised for data errors, such as invalid values and unexpected nesting structures.

This is the base class for InvalidStructureError and InvalidValueError, and can be caught instead of the more specific exception types.

This is a subclass of the built-in ValueError.

exception sanest.InvalidStructureError

Bases: sanest.DataError

Exception raised when a nested structure does not match the request.

This is a subclass of DataError and the built-in ValueError, since this indicates malformed data.

exception sanest.InvalidValueError

Bases: sanest.DataError

Exception raised when requesting or providing an invalid value.

This is a subclass of DataError and the built-in ValueError.

exception sanest.InvalidPathError

Bases: Exception

Exception raised when a path is invalid.

This indicates problematic code that uses an incorrect API.

exception sanest.InvalidTypeError

Bases: Exception

Exception raised when a specified type is invalid.

This indicates problematic code that uses an incorrect API.

Version history

  • 0.1.0 (2017-07-02)
    • Initial release.

Contributing

The source code and issue tracker for this package can be found on Github:

sanest has an extensive test suite that covers the complete code base. Please provide minimal examples to demonstrate potential problems.

License

(This is the OSI approved 3-clause “New BSD License”.)

Copyright © 2017, wouter bolsterlee

All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  • Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  • Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  • Neither the name of the author nor the names of the contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.