blob: 8ff037f45c870f99b03b4ef5f66ff4bd513ce3ed [file] [log] [blame]
#
# -*- coding: utf-8 -*-
#
# This file is part of reclass (http://github.com/madduck/reclass)
#
# Copyright © 2007–14 martin f. krafft <madduck@madduck.net>
# Released under the terms of the Artistic Licence 2.0
#
import types, re
class DictPath(object):
'''
Represents a path into a nested dictionary.
Given a dictionary like
d['foo']['bar'] = 42
it can be desirable to obtain a reference to the value stored in the
sub-levels, allowing that value to be accessed and changed. Unfortunately,
Python provides no easy way to do this, since
ref = d['foo']['bar']
does become a reference to the integer 42, but that reference is
overwritten when one assigns to it. Hence, DictPath represents the path
into a nested dictionary, and can be "applied to" a dictionary to obtain
and set values, using a list of keys, or a string representation using
a delimiter (which can be escaped):
p = DictPath(':', 'foo:bar')
p.get_value(d)
p.set_value(d, 43)
This is a bit backwards, but the right way around would require support by
the dict() type.
The primary purpose of this class within reclass is to cater for parameter
interpolation, so that a reference such as ${foo:bar} in a parameter value
may be resolved in the context of the Parameter collections (a nested
dict).
If the value is a list, then the "key" is assumed to be and interpreted as
an integer index:
d = {'list': [{'one':1},{'two':2}]}
p = DictPath(':', 'list:1:two')
p.get_value(d) → 2
This heuristic is okay within reclass, because dictionary keys (parameter
names) will always be strings. Therefore it is okay to interpret each
component of the path as a string, unless one finds a list at the current
level down the nested dictionary.
'''
def __init__(self, delim, contents=None):
self._delim = delim
if contents is None:
self._parts = []
else:
if isinstance(contents, types.StringTypes):
self._parts = self._split_string(contents)
elif isinstance(contents, tuple):
self._parts = list(contents)
elif isinstance(contents, list):
self._parts = contents
else:
raise TypeError('DictPath() takes string or list, '\
'not %s' % type(contents))
def __repr__(self):
return "DictPath(%r, %r)" % (self._delim, str(self))
def __str__(self):
return self._delim.join(str(i) for i in self._parts)
def __eq__(self, other):
if isinstance(other, types.StringTypes):
other = DictPath(self._delim, other)
return self._parts == other._parts \
and self._delim == other._delim
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash(str(self))
def _get_path(self):
return self._parts
path = property(_get_path)
def _get_key(self):
if len(self._parts) == 0:
return None
return self._parts[-1]
def _get_innermost_container(self, base):
container = base
for i in self.path[:-1]:
if isinstance(container, (list, tuple)):
container = container[int(i)]
else:
container = container[i]
return container
def _split_string(self, string):
return re.split(r'(?<!\\)' + re.escape(self._delim), string)
def _escape_string(self, string):
return string.replace(self._delim, '\\' + self._delim)
def has_ancestors(self):
return len(self._parts) > 1
def key_parts(self):
if self.has_ancestors():
return self._parts[::len(self._parts)-1]
else:
return []
def new_subpath(self, key):
try:
return DictPath(self._delim, self._parts + [self._escape_string(key)])
except AttributeError as e:
return DictPath(self._delim, self._parts + [key])
def get_value(self, base):
return self._get_innermost_container(base)[self._get_key()]
def set_value(self, base, value):
self._get_innermost_container(base)[self._get_key()] = value
def drop_first(self):
del self._parts[0]
return self
def exists_in(self, container):
item = container
for i in self._parts:
if isinstance(item, (dict, list)):
if i in item:
if isinstance(item, dict):
item = item[i]
elif isinstance(container, list):
item = item[int(i)]
else:
return False
else:
if item == self._parts[-1]:
return True
else:
return False
return True