/traversify

Handy python classes for manipulating json data, providing syntactic sugar for less verbose, easier to write code.

Primary LanguagePythonGNU General Public License v3.0GPL-3.0

traversify

Handy python classes for manipulating json data, providing syntactic sugar for less verbose, easier to write code.

Traverser class allows one to:

  • traverse complex trees of data with dotted syntax rather than the verbose dictionary dereferencing.
  • treat nodes on the tree as lists even if they are singleton dictionaries, eliminating a lot of type-checking code.
  • add or delete branches of the tree with simple dotted syntax.
  • treat missing keys on the tree as None rather than throwing a key exception, much as JavaScript returns undefined.
  • linkage to Filter class (defined next) for powerful tree comparisons or tree pruning.

Filter class allows one to:

  • define a set of criteria for comparing two partially incongruous trees by limiting the sets of fields compared.
  • apply said criteria to prune a tree of any unwanted fields.

Traverser

Pass tree data to Traverser, either as a list, dictionary, json string or any class offering a json method, and the resultant object will provide the syntactic sugar for traversing with dotted syntax, treating singleston nodes as lists:

>>> from traversify import Traverser
>>> obj = Traverser({'id': 1, 'username': 'jdoe'})
>>> obj.id
1
>>> obj.username
'jdoe'
>>> obj.bad_key is None
True
>>> [node.id for node in obj]
[1]
>>> obj[0].id
1
>>> {'id': 1, 'username': 'jdoe'} in obj
True

Not only can singletons be addressed as lists, but append and extend methods are available to turn singletons into lists on the fly:

>>> obj = Traverser({'id': 1})
>>> obj.append({'id': 2})
>>> obj.extend([{'id': 3, 'id': 4}])
>>> [node.id for node in obj]
[1, 2, 3, 4]

At any time, a Traverser instance will return the underlying value when called:

>>> obj = Traverser({'id': 1})
>>> obj()
{'id': 1}

The tree can be updated using dotted syntax. Note that by default, a Traverser instance makes a deepcopy of the json data so that there are no unintended side effects:

>>> data = {'id': 1, 'username': 'jdoe'}
>>> obj = Traverser(data)
>>> obj.id = 2
>>> del obj.username
>>> obj()
{'id': 2}
>>> data
{'id': 1, 'username': 'jdoe'}

However, if the side-effect of updating the data passed is desired (perhaps due to memory constaints), then pass deepcopy=False:

>>> data = {'id': 1}
>>> obj = Traverser(data, deepcopy=False)
>>> obj.id = 2
>>> obj()
{'id': 2}
>>> data
{'id': 2}

In case there are keys that are not identifiers, then dictionary dereferencing can still be used:

>>> obj = Traverser({'@xsi.type': 'textarea'})
>>> obj['@xsi.type']
'textarea'

The get method allows traversing multiple levels in one call, using dots to set off the levels:

>>> obj = Traverser({'root': {'username': 'any'}})
>>> obj.get('root.username')
'any'

Also, the get method supports dot-escaping so that keys containing dots can still be traversed:

>>> obj = Traverser({'@xsi.type': 'textarea'})
>>> obj.get('@xsi..type')
'textarea'

There's a set method that will update a node multiple levels down and even build out branches that aren't already there:

>>> obj = Traverser({'stats': {'id': 1}})
>>> obj.set('stats.id', 2)
>>> obj()
{'stats': {'id': 2}}
>>> obj.set('users.0.username', 'any')
>>> obj()
{'stats': {'id': 2}, 'users': [{'username': 'any'}]}

To save the trouble of importing json and using dumps, there's a handy to_json method:

>>> obj = Traverser({'id': 1})
>>> obj.to_json()
'{"id": 1}'

Filter

Often one needs to compare two trees without taking into account irrelavant fields, like when records in the tree have ids, but a new record doesn't have it yet. Filter provides a way to make this less verbose by providing blacklist and whitelist attributes for controlled comparison:

>>> from traversify import Traverser, Filter
>>> id_exclude_filter = Filter(blacklist='id')
>>> record = Traverser({'id': 1, 'username': 'jdoe'})
>>> id_exclude_filter.are_equal(record, {'username': 'jdoe'})
True

The same filter can be used to prune a tree of its unwanted fields:

>>> id_exclude_filter.prune(record)
>>> record()
{'username': 'jdoe'}

If a filter is passed while creating a Traverser instance, then ==, in and the prune() method will use it to do the comparison or pruning:

>>> record = Traverser({'id': 1, 'username': 'jdoe'}, filter=Filter(blacklist='id'))
>>> record == {'username': 'jdoe'}
True
>>> {'username': 'jdoe'} in record
True
>>> record.prune()
>>> record()
{'username': 'jdoe'}

Traverser's prune method will accept a filter to override the default (or supply one not already supplied):

>>> record = Traverser({'id': 1, 'username': 'jdoe'})
>>> record.prune(filter=Filter(blacklist='id'))
>>> record()
{'username': 'jdoe'}