Source code for bibliopixel.control.address

"""
An address identifies how to get or set a piece of data within a Python object,
called the "root", using attributes and indexing.

An address description is a string looking like:

::

    .foo.bar[32][5][baz].bang()

which would mean

"given an object "root", the value ``root.foo.bar[32][5]['baz'].bang()``".

Addresses are divided into "segments".

A segment contained in brackets ``[]`` is an index (for a list) or a key (for
a dictionary) - otherwise, it's an attribute.

In the example above, the segments are ``foo``, ``bar``, ``[32]``, ``[5]``,
``[baz]`` and ``bang``; ``foo`` and ``bar`` are attributes.
``baz`` is a string index, and ``32`` and ``5`` are numeric indexes.

You can use an Address to either get or set values in the root object.

Any key that's entirely numeric is taken to be an integer index.  This is
convenient but prevents the creation of dictionaries like ``{1: 'x', '1': 'y'}``
which you probably didn't want to do anyway.
"""

from .. util import data_file, log


[docs]def number(s): try: return data_file.loads(s) except: return s
[docs]class Address:
[docs] class Segment: def __init__(self, name): self.name = name
[docs] def set(self, root, *value): self._set(root, (value[0] if len(value) == 1 else value))
[docs] class Attribute(Segment):
[docs] def get(self, root): return getattr(root, self.name)
def _set(self, root, value): setattr(root, self.name, value) def __str__(self): return '.%s' % self.name
[docs] class Index(Segment):
[docs] def get(self, root): return root[self.name]
def _set(self, root, value): root[self.name] = value def __str__(self): return '[%s]' % self.name
[docs] class Call(Segment): def __init__(self): pass
[docs] def get(self, root): return root()
[docs] def set(self, root, *value): root(*value)
def __str__(self): return '()'
def __init__(self, name=None): if not name: self.segments = self.assignment = () return self.name, *assignment = name.split('=', 1) assignment = assignment and assignment[0].strip() self.name = self.name.strip() try: self.segments = list(_generate(self.name)) except: raise ValueError('%s is not a legal address' % name) if not self.segments: raise ValueError('Empty Addresses are not allowed') if not assignment: self.assignment = () return if isinstance(self.segments[-1], Address.Call): raise ValueError('Cannot assign to a call operation') self.assignment = tuple(number(s) for s in assignment.split(',')) def __bool__(self): return bool(self.segments) def __str__(self): return self.name @staticmethod def _get(root, address): for a in address: root = a.get(root) return root
[docs] def get(self, root): return self._get(root, self.segments)
[docs] def set(self, root, *values): *first, last = self.segments parent = self._get(root, first) last.set(parent, *(self.assignment + values))
def _generate(s): def extract_calls(p): # Extract () pairs from start and finish of a string before, after = [], [] while p.startswith('()'): before.append(Address.Call()) p = p[2:] while p.endswith('()'): after.append(Address.Call()) p = p[:-2] return before, p, after # Split on dots, then use [ and ] to split out indices s = s.strip() if s.startswith('.'): s = s[1:] if s.endswith('.'): raise ValueError for i, part in enumerate(s.split('.')): if not part: raise ValueError head, *rest = part.split('[') # If we had e.g. 'xxx()()[yyy]()[zzz]()()' # Now we have first='xxx()' and rest = 'yyy]()', 'zzz]()()' # They might have written: ()xxx by mistake before, head, after = extract_calls(head) if before: # A call () is only allowed to start the first segment - # for example, an address like a.() is forbidden. if i or head: raise ValueError yield from before elif head: yield Address.Attribute(head) yield from after elif i: # An index [] is only allowed to start the first segment - # for example, an address like a.[2] is forbidden. raise ValueError for r in rest: before, r, after = extract_calls(r) if before: # A segment cannot contain a () - they must all be at top level. raise ValueError # A segment must have exactly one ']', exactly at the end. index, between = r.split(']', 1) if between: raise ValueError index = int(index) if index.isdigit() else index yield Address.Index(index) yield from after