You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

245 lines
7.6 KiB

# -*- coding: utf-8 -*-
from __future__ import print_function
import re
_MAXCACHE = 20
class Resolver(object):
_match_cache = {}
def __init__(self, pathattr='name'):
"""Resolve :any:`NodeMixin` paths using attribute `pathattr`."""
super(Resolver, self).__init__()
self.pathattr = pathattr
def get(self, node, path):
"""
Return instance at `path`.
An example module tree:
>>> from anytree import Node
>>> top = Node("top", parent=None)
>>> sub0 = Node("sub0", parent=top)
>>> sub0sub0 = Node("sub0sub0", parent=sub0)
>>> sub0sub1 = Node("sub0sub1", parent=sub0)
>>> sub1 = Node("sub1", parent=top)
A resolver using the `name` attribute:
>>> r = Resolver('name')
Relative paths:
>>> r.get(top, "sub0/sub0sub0")
Node('/top/sub0/sub0sub0')
>>> r.get(sub1, "..")
Node('/top')
>>> r.get(sub1, "../sub0/sub0sub1")
Node('/top/sub0/sub0sub1')
>>> r.get(sub1, ".")
Node('/top/sub1')
>>> r.get(sub1, "")
Node('/top/sub1')
>>> r.get(top, "sub2")
Traceback (most recent call last):
...
anytree.resolver.ChildResolverError: Node('/top') has no child sub2. Children are: 'sub0', 'sub1'.
Absolute paths:
>>> r.get(sub0sub0, "/top")
Node('/top')
>>> r.get(sub0sub0, "/top/sub0")
Node('/top/sub0')
>>> r.get(sub0sub0, "/")
Traceback (most recent call last):
...
anytree.resolver.ResolverError: root node missing. root is '/top'.
>>> r.get(sub0sub0, "/bar")
Traceback (most recent call last):
...
anytree.resolver.ResolverError: unknown root node '/bar'. root is '/top'.
"""
node, parts = self.__start(node, path)
for part in parts:
if part == "..":
node = node.parent
elif part in ("", "."):
pass
else:
node = self.__get(node, part)
return node
def __get(self, node, name):
for child in node.children:
if _getattr(child, self.pathattr) == name:
return child
raise ChildResolverError(node, name, self.pathattr)
def glob(self, node, path):
"""
Return instances at `path` supporting wildcards.
Behaves identical to :any:`get`, but accepts wildcards and returns
a list of found nodes.
* `*` matches any characters, except '/'.
* `?` matches a single character, except '/'.
An example module tree:
>>> from anytree import Node
>>> top = Node("top", parent=None)
>>> sub0 = Node("sub0", parent=top)
>>> sub0sub0 = Node("sub0", parent=sub0)
>>> sub0sub1 = Node("sub1", parent=sub0)
>>> sub1 = Node("sub1", parent=top)
>>> sub1sub0 = Node("sub0", parent=sub1)
A resolver using the `name` attribute:
>>> r = Resolver('name')
Relative paths:
>>> r.glob(top, "sub0/sub?")
[Node('/top/sub0/sub0'), Node('/top/sub0/sub1')]
>>> r.glob(sub1, ".././*")
[Node('/top/sub0'), Node('/top/sub1')]
>>> r.glob(top, "*/*")
[Node('/top/sub0/sub0'), Node('/top/sub0/sub1'), Node('/top/sub1/sub0')]
>>> r.glob(top, "*/sub0")
[Node('/top/sub0/sub0'), Node('/top/sub1/sub0')]
>>> r.glob(top, "sub1/sub1")
Traceback (most recent call last):
...
anytree.resolver.ChildResolverError: Node('/top/sub1') has no child sub1. Children are: 'sub0'.
Non-matching wildcards are no error:
>>> r.glob(top, "bar*")
[]
>>> r.glob(top, "sub2")
Traceback (most recent call last):
...
anytree.resolver.ChildResolverError: Node('/top') has no child sub2. Children are: 'sub0', 'sub1'.
Absolute paths:
>>> r.glob(sub0sub0, "/top/*")
[Node('/top/sub0'), Node('/top/sub1')]
>>> r.glob(sub0sub0, "/")
Traceback (most recent call last):
...
anytree.resolver.ResolverError: root node missing. root is '/top'.
>>> r.glob(sub0sub0, "/bar")
Traceback (most recent call last):
...
anytree.resolver.ResolverError: unknown root node '/bar'. root is '/top'.
"""
node, parts = self.__start(node, path)
return self.__glob(node, parts)
def __start(self, node, path):
sep = node.separator
parts = path.split(sep)
if path.startswith(sep):
node = node.root
rootpart = _getattr(node, self.pathattr)
parts.pop(0)
if not parts[0]:
msg = "root node missing. root is '%s%s'."
raise ResolverError(node, "", msg % (sep, str(rootpart)))
elif parts[0] != rootpart:
msg = "unknown root node '%s%s'. root is '%s%s'."
raise ResolverError(node, "", msg % (sep, parts[0], sep, str(rootpart)))
parts.pop(0)
return node, parts
def __glob(self, node, parts):
nodes = []
name = parts[0]
remainder = parts[1:]
# handle relative
if name == "..":
nodes += self.__glob(node.parent, remainder)
elif name in ("", "."):
nodes += self.__glob(node, remainder)
else:
matches = self.__find(node, name, remainder)
if not matches and not Resolver.is_wildcard(name):
raise ChildResolverError(node, name, self.pathattr)
nodes += matches
return nodes
def __find(self, node, pat, remainder):
matches = []
for child in node.children:
name = _getattr(child, self.pathattr)
try:
if Resolver.__match(name, pat):
if remainder:
matches += self.__glob(child, remainder)
else:
matches.append(child)
except ResolverError as exc:
if not Resolver.is_wildcard(pat):
raise exc
return matches
@staticmethod
def is_wildcard(path):
"""Return `True` is a wildcard."""
return "?" in path or "*" in path
@staticmethod
def __match(name, pat):
try:
re_pat = Resolver._match_cache[pat]
except KeyError:
res = Resolver.__translate(pat)
if len(Resolver._match_cache) >= _MAXCACHE:
Resolver._match_cache.clear()
Resolver._match_cache[pat] = re_pat = re.compile(res)
return re_pat.match(name) is not None
@staticmethod
def __translate(pat):
re_pat = ''
for char in pat:
if char == "*":
re_pat += ".*"
elif char == "?":
re_pat += "."
else:
re_pat += re.escape(char)
return re_pat + r'\Z(?ms)'
class ResolverError(RuntimeError):
def __init__(self, node, child, msg):
"""Resolve Error at `node` handling `child`."""
super(ResolverError, self).__init__(msg)
self.node = node
self.child = child
class ChildResolverError(ResolverError):
def __init__(self, node, child, pathattr):
"""Child Resolve Error at `node` handling `child`."""
names = [repr(_getattr(c, pathattr)) for c in node.children]
msg = "%r has no child %s. Children are: %s."
msg = msg % (node, child, ", ".join(names))
super(ChildResolverError, self).__init__(node, child, msg)
def _getattr(node, name):
return getattr(node, name, None)