-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f6dd3a9
commit 76f8925
Showing
8 changed files
with
654 additions
and
187 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
from .tree import Tree | ||
from .node import Node | ||
from .tree import Tree | ||
from .interactive import TreeBasedObj | ||
|
||
__all__ = ["Tree", "Node"] | ||
__all__ = ["Tree", "Node", "TreeBasedObj"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
#!/usr/bin/env python | ||
# -*- coding: utf-8 -*- | ||
from __future__ import unicode_literals | ||
|
||
from builtins import str as text | ||
import re | ||
import unicodedata | ||
|
||
from future.utils import string_types, python_2_unicode_compatible | ||
from lighttree.tree import Tree | ||
|
||
|
||
def is_valid_attr_name(item): | ||
if not isinstance(item, string_types): | ||
return False | ||
if item.startswith("__"): | ||
return False | ||
if re.search(string=item, pattern=r"^[a-zA-Z_]+[a-zA-Z0-9_]*$") is None: | ||
return False | ||
if re.search(string=item, pattern=r"[^_]") is None: | ||
return False | ||
return True | ||
|
||
|
||
def _coerce_attr(attr): | ||
if not len(attr): | ||
return None | ||
new_attr = unicodedata.normalize("NFD", attr).encode("ASCII", "ignore").decode() | ||
new_attr = re.sub(string=new_attr, pattern=r"[^a-zA-Z_0-9]", repl="_") | ||
if new_attr[0].isdigit(): | ||
new_attr = "_" + new_attr | ||
if is_valid_attr_name(new_attr): | ||
return new_attr | ||
return None | ||
|
||
|
||
@python_2_unicode_compatible | ||
class Obj(object): | ||
"""Object class that allows to get items both by attribute `__getattribute__` access: `obj.attribute` or by dict | ||
`__getitem__` access: | ||
>>> obj = Obj(key='value') | ||
>>> obj.key | ||
'value' | ||
>>> obj['key'] | ||
'value' | ||
In Ipython interpreter, attributes will be available in autocomplete (except private ones): | ||
>>> obj = Obj(key='value', key2='value2') | ||
>>> obj.k # press tab for autocompletion | ||
key | ||
key2 | ||
Items names that are not compliant with python attributes (accepted characters are [a-zA-Z0-9\_] without beginning | ||
with a figure), will be only available through dict `__getitem__` access. | ||
""" | ||
|
||
_REPR_NAME = None | ||
_STRING_KEY_CONSTRAINT = True | ||
_COERCE_ATTR = False | ||
|
||
def __init__(self, **kwargs): | ||
# will store non-valid names | ||
self.__d = dict() | ||
for k, v in kwargs.items(): | ||
if not ( | ||
isinstance(k, string_types) | ||
and k not in ("_REPR_NAME", "_Obj__d") | ||
and not k.startswith("__") | ||
): | ||
raise ValueError( | ||
"Attribute <%s> of type <%s> is not valid." % (k, type(k)) | ||
) | ||
self[k] = v | ||
|
||
def __getitem__(self, item): | ||
# when calling d[key] | ||
if is_valid_attr_name(item): | ||
return self.__getattribute__(item) | ||
else: | ||
return self.__d[item] | ||
|
||
def __setitem__(self, key, value): | ||
# d[key] = value | ||
if not isinstance(key, string_types): | ||
if self._STRING_KEY_CONSTRAINT: | ||
raise ValueError( | ||
"Key <%s> of type <%s> cannot be set as attribute on <%s> instance." | ||
% (key, type(key), self.__class__.__name__) | ||
) | ||
self.__d[key] = value | ||
return | ||
if not is_valid_attr_name(key): | ||
self.__d[key] = value | ||
if self._COERCE_ATTR: | ||
# if coerc_attr is set to True, try to coerce | ||
n_key = _coerce_attr(key) | ||
if n_key is not None: | ||
super(Obj, self).__setattr__(n_key, value) | ||
else: | ||
super(Obj, self).__setattr__(key, value) | ||
|
||
def __keys(self): | ||
return list(self.__d.keys()) + [ | ||
k for k in self.__dict__.keys() if k not in ("_REPR_NAME", "_Obj__d") | ||
] | ||
|
||
def __contains__(self, item): | ||
return item in self.__keys() | ||
|
||
def __str__(self): | ||
return "<%s> %s" % ( | ||
text(self.__class__._REPR_NAME or self.__class__.__name__), | ||
text(sorted(map(text, self.__keys()))), | ||
) | ||
|
||
def __repr__(self): | ||
return self.__str__() | ||
|
||
|
||
@python_2_unicode_compatible | ||
class TreeBasedObj(Obj): | ||
""" | ||
Recursive Obj whose structure is defined by a lighttree.Tree object. | ||
The main purpose of this object is to iteratively expand the tree as attributes of this object. To avoid creating | ||
useless instances, only direct children of accessed nodes are expanded. | ||
""" | ||
|
||
_NODE_PATH_ATTR = "identifier" | ||
_COERCE_ATTR = False | ||
|
||
def __init__(self, tree, root_path=None, depth=1, initial_tree=None): | ||
super(TreeBasedObj, self).__init__() | ||
if not isinstance(tree, Tree): | ||
raise ValueError( | ||
'tree must be an instance of "lighttree.tree.Tree", got %s' % type(tree) | ||
) | ||
self._tree = tree | ||
self._root_path = root_path | ||
self._initial_tree = initial_tree if initial_tree is not None else tree | ||
self._expand_attrs(depth) | ||
|
||
def _clone(self, nid, root_path, depth): | ||
return self.__class__( | ||
tree=self._tree.subtree(nid), | ||
root_path=root_path, | ||
depth=depth, | ||
initial_tree=self._initial_tree, | ||
) | ||
|
||
def _expand_attrs(self, depth): | ||
if depth: | ||
for child in self._tree.children(nid=self._tree.root, id_only=False): | ||
try: | ||
child_path = getattr(child, self._NODE_PATH_ATTR) | ||
except AttributeError: | ||
raise AttributeError( | ||
'<%s> instance should have the "%s" attribute that serves as autocomplete ' | ||
"expanding attribute in <%s>." | ||
% ( | ||
self.tree.node_class.__name__, | ||
self._NODE_PATH_ATTR, | ||
self.__name__, | ||
) | ||
) | ||
if self._COERCE_ATTR: | ||
# if invalid coercion, coerce returns None, in this case we keep inital naming | ||
child_path = _coerce_attr(child_path) or child_path | ||
if child_path in self: | ||
continue | ||
if self._root_path is not None: | ||
child_root = "%s.%s" % (self._root_path, child_path) | ||
else: | ||
child_root = child_path | ||
self[child_path] = self._clone( | ||
child.identifier, root_path=child_root, depth=depth - 1 | ||
) | ||
|
||
def __getattribute__(self, item): | ||
# called by __getattribute__ will always refer to valid attribute item | ||
r = super(TreeBasedObj, self).__getattribute__(item) | ||
if isinstance(r, TreeBasedObj): | ||
r._expand_attrs(depth=1) | ||
return r | ||
|
||
def __getitem__(self, item): | ||
if is_valid_attr_name(item): | ||
r = super(TreeBasedObj, self).__getattribute__(item) | ||
else: | ||
r = super(TreeBasedObj, self).__getitem__(item) | ||
if isinstance(r, TreeBasedObj): | ||
r._expand_attrs(depth=1) | ||
return r | ||
|
||
def __str__(self): | ||
tree_repr = self._tree.show() | ||
if self._root_path is None: | ||
return "<%s>\n%s" % ( | ||
text(self.__class__._REPR_NAME or self.__class__.__name__), | ||
text(tree_repr), | ||
) | ||
current_path = self._root_path | ||
return "<%s subpart: %s>\n%s" % ( | ||
text(self.__class__._REPR_NAME or self.__class__.__name__), | ||
text(current_path), | ||
text(tree_repr), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.