ayon-core/pype/vendor/pather/core.py
2019-01-28 20:31:55 +01:00

168 lines
4.9 KiB
Python

__all__ = ['parse', 'ls', 'ls_iter', 'format']
import os
import re
import string
import glob
from .error import ParseError
# Regex pattern that matches valid file
# A filename may not contain \/:*?"<>|
RE_FILENAME = r"[^\\/:\"*?<>|]"
def format(pattern, data, allow_partial=True):
"""Format a pattern with a set of data
Examples:
Full formatting
>>> format("{a}/{b}/{c}", {"a": "foo", "b": "bar", "c": "nugget"})
'foo/bar/nugget'
Partial formatting
>>> format("{asset}/{character}", {"asset": "hero"})
'hero/{character}'
Disallow partial formatting
>>> format("{asset}/{character}", {"asset": "hero"},
... allow_partial=False)
Traceback (most recent call last):
...
KeyError: 'character'
Args:
pattern (str): The pattern to format.
data (dict): The key, value pairs used for formatting.
allow_partial (bool): Whether to raise error on partial format.
Returns:
str: The formatted result
"""
assert isinstance(data, dict)
if not all(isinstance(value, basestring) for value in data.values()):
raise TypeError("The values in the data "
"dictionary must be strings")
if allow_partial:
return _partial_format(pattern, data)
else:
return pattern.format(**data)
def parse(pattern, path):
"""Parse data from a path based on a pattern
Example:
>>> pattern = "root/{task}/{version}/data/"
>>> path = "root/modeling/v001/data/"
>>> parse(pattern, path)
{'task': 'modeling', 'version': 'v001'}
Returns:
dict: The data retrieved from path using pattern.
"""
pattern = os.path.normpath(pattern)
path = os.path.normpath(path)
# Force forward slashes
path = path.replace('\\', '/')
pattern = pattern.replace('\\', '/')
# Escape characters in path that are regex patterns so they are
# excluded by the regex searches. Exclude '{' and '}' in escaping.
pattern = re.escape(pattern)
pattern = pattern.replace('\{', '{').replace('\}', '}')
keys = re.findall(r'{(%s+)}' % RE_FILENAME,
pattern)
if not keys:
return []
# Find the corresponding values
value_pattern = re.sub(r'{(%s+)}' % RE_FILENAME,
r'(%s+)' % RE_FILENAME,
pattern)
match_values = re.match(value_pattern, path)
if not match_values:
raise ParseError("Path doesn't match with pattern. No values parsed")
values = match_values.groups()
return dict(zip(keys, values))
def ls_iter(pattern, include=None, with_matches=False):
"""Yield all matches for the given pattern.
If the pattern starts with a relative path (or a dynamic key) the search
will start from the current working directory, defined by os.path.realpath.
Arguments:
pattern (str): The pattern to match and search against.
include (dict): A dictionary used to target the search with the pattern
to include only those key-value pairs within the pattern. With this
you can reduce the filesystem query to a specified subset.
Example:
>>> data = {"root": "foobar", "content": "nugget"}
>>> for path in ls_iter("{root}/{project}/data/{content}/",
... include=data):
... print path
Returns:
(str, tuple): The matched paths (and data if `with_matches` is True)
The returned value changes whether `with_matches` parameter is True or
False. If True a 2-tuple is yielded for each match as (path, data) else
only the path is returned
"""
# format rule by data already provided to reduce query
if include is not None:
pattern = format(pattern, include, allow_partial=True)
pattern = os.path.expandvars(pattern)
pattern = os.path.realpath(pattern)
glob_pattern = re.sub(r'([/\\]{\w+}[/\\])', '/*/', pattern) # folder
glob_pattern = re.sub(r'({\w+})', '*', glob_pattern) # filename
for path in glob.iglob(glob_pattern):
path = os.path.realpath(path)
if with_matches:
data = parse(pattern, path)
yield path, data
else:
yield path
def ls(pattern, include=None, with_matches=False):
return list(ls_iter(pattern, include, with_matches=with_matches))
def _partial_format(s, data):
"""Return string `s` formatted by `data` allowing a partial format
Arguments:
s (str): The string that will be formatted
data (dict): The dictionary used to format with.
Example:
>>> _partial_format("{d} {a} {b} {c} {d}", {'b': "and", 'd': "left"})
'left {a} and {c} left'
"""
class FormatDict(dict):
def __missing__(self, key):
return "{" + key + "}"
formatter = string.Formatter()
mapping = FormatDict(**data)
return formatter.vformat(s, (), mapping)