"""Module for the main class."""
import copy
import glob
import re
import lucidity # used only for errors and creating in one go
from . import file_utils
# pylint: disable=invalid-name
# pylint: disable=consider-using-f-string
[docs]
class TemplateFile(object):
"""Class that wraps lucidity template adding the ability for listing files and partial templates."""
MAX_FILES_COUNT = 999999
DEFAULT_VALUES_KEY = 'default_values'
ROOT_KEY = 'roots'
[docs]
@classmethod
def create(
cls,
name,
pattern,
default_placeholder_expression='[\\w_.\\-]+',
template_resolver=None,
default_values=None,
roots=None,
):
"""Creates a TemplateFile object initialized.
Args:
name (str): Name of the template.
pattern (str): File pattern to use with lucidity.
default_placeholder_expression (str, optional): Pseudo regex used in the pattern fields.
Defaults to '[\\w_.\\-]+'.
template_resolver (lucidity.Resolver, optional): A resolver for the inner templates. Defaults to None.
default_values (dict, optional): A mapping between values and their defaults. Defaults to None.
roots (dict, optional): A mapping between root names and root paths. Defaults to None.
Returns:
TemplateFile: The initialized template file.
"""
l_template = lucidity.Template(
name,
pattern,
anchor=lucidity.Template.ANCHOR_BOTH,
default_placeholder_expression=default_placeholder_expression,
duplicate_placeholder_mode=lucidity.Template.STRICT,
template_resolver=template_resolver,
)
return cls(l_template, default_values=default_values, roots=roots)
[docs]
def __init__(self, l_template, default_values=None, roots=None):
'''
Args:
l_template (lucidity.Template): the template to wrap
default_values (dict): the default values for some key in lucidity template. This overrides values added to
the template
roots (dict): the root values for the templates. This overrides values added to the template
'''
self.l_template = l_template
# without STRICT the whole system breaks, because you could have multiple values for same key
self.l_template.duplicate_placeholder_mode = self.l_template.STRICT
# without ANCHOR_BOTH the whole system breaks, because you could have partial matches
self.l_template._anchor = self.l_template.ANCHOR_BOTH
# Check that supplied pattern is valid and able to be compiled.
self.l_template._construct_regular_expression(self.l_template.pattern)
self.roots = self._resolveParam(self.ROOT_KEY, roots, {})
self.default_values = self._resolveParam(self.DEFAULT_VALUES_KEY, default_values, {})
def __str__(self):
msg = '<TemplateFile id:{} name:{} pattern:{}>'.format(id(self), self.lucidity_name, self.l_template.pattern)
return msg
[docs]
def _resolveParam(self, key, override, default):
'''Helper to resolve parameters.
Args:
key (str): key to resolve
override (obj): the overriding value, can be None in case it's not overridden.
default (obj): the value to use in case it's not defined on the template or the override.
Returns:
obj: the resolved value.
'''
# Resolution order of roots and default values
# 1) args in this init overrides the values in the template
if override is not None:
return override
# 2) if not defined in args, it uses templates ones
if hasattr(self.l_template, key):
return getattr(self.l_template, key)
# 3) if there is none on the template and non defined in args, it assumes there is no need for them and a
# functional default is returned (this should be passed as argument)
return default
# lucidity stuff ----
@property
def lucidity_name(self):
'''returns lucidity template name'''
return self.l_template.name
[docs]
def parse(self, path, return_roots=True):
'''Wraps lucidity parse function.
Args:
path (str): the path to parse
return_roots (bool): whether or not to return the roots in the result
Returns:
dict: the key values pairs
Raises
lucidity.ParseError if path cant be parsed.
'''
path = file_utils.toLucidityPath(path)
# replace root path so it matches
r_path, root_found = self._replaceKeysOnPath(path, self.roots)
data = self.l_template.parse(r_path)
# add root real value or remove the key (otherwise you'll get 'XTDXrootout': 'XTDXrootout')
if return_roots:
data.update(root_found)
else:
for root_key in root_found:
data.pop(root_key)
return data
[docs]
def _replaceKeysOnPath(self, path, key_dict):
'''Replaces keys in a template path with their corresponding values.
Args:
path (str): path to replace roots on.
key_dict (dict): the dict of keys to find and values to put in replacement.
Returns:
str: the path with the roots replaced.
Notes:
TODO: this dont depend on self, could be extracted
'''
found = {}
# search for each value and replace with the key
for k, v in key_dict.items():
# because of bars, we need to convert the values to something compatible
v_safe = file_utils.toLucidityPath(v)
if v_safe not in path:
continue
# replace on the path with the key
path = re.sub(re.escape(v_safe), k, path, re.IGNORECASE)
found[k] = v_safe
return path, found
# list files ----
[docs]
def _getGlobPath(self, fields, skip_keys=None):
'''Builds a glob path to use with python's glob function.
Args:
fields (dict): the fields to define. Undefined fields will become an * .
skip_keys (list): the keys to ignore from the fields dict.
Returns:
str: a path to use with glob.
'''
keys = self.l_template.keys()
all_fields = {k: '*' for k in keys}
# shouldn't we replace the roots?
if self.roots:
for root_key, root_value in self.roots.items():
if root_key in keys:
all_fields[root_key] = root_value
update_fields = {k: v for k, v in fields.items() if not skip_keys or k not in skip_keys}
all_fields.update(update_fields)
glob_path = self.format(all_fields)
return glob_path
[docs]
def getPaths(self, fields=None, skip_keys=None, strict_check=True, max_count=None):
'''Returns the found paths that matched this template.
Args:
fields (dict): the fields to define. Undefined fields will become an wildcard.
skip_keys (list): the keys to ignore from the fields dict.
strict_check (bool): whether or not to strictly check that the paths found can be parsed (takes more time).
max_count (int): maximum files to return.
Returns:
list[str]: the list of paths found that matched the template and the fields.
'''
# resolve optional parameters
if fields is None:
fields = {}
if max_count is None:
max_count = self.MAX_FILES_COUNT
# build glob path to use
glob_path = self._getGlobPath(fields, skip_keys=skip_keys)
result = []
count = 0
for path in glob.iglob(glob_path):
if max_count <= count:
break
# parse path to strictly check
if strict_check and not self.checkPath(path):
continue
result.append(file_utils.toCleanPath(path))
count += 1
return result
# check ----
[docs]
def checkPath(self, path):
'''Check if path is consistent with this template.
Args:
path (str): the path to check.
Returns:
bool: True if check is passed, False otherwise.
'''
try:
self.parse(path)
except lucidity.ParseError:
return False
return True
[docs]
def checkData(self, data):
'''Check if data is consistent with this template.
Args:
data (dict): the data to check.
Returns:
bool: True if check is passed, False otherwise.
'''
try:
self.format(data)
except lucidity.FormatError:
return False
return True
[docs]
def roundTripCheckData(self, data):
'''Check if data is consistent with this template by converting back and forth.
Args:
data (dict): the data to check.
Returns:
bool: True if check is passed, False otherwise.
Notes:
TODO: move to tests!
'''
# convert to path and back to data
built_path = self.format(data)
built_data = self.parse(built_path)
# check if result is the same
if built_data != data:
return False
return True
[docs]
def roundTripCheckPath(self, path, platform=None):
'''Check if path is consistent with this template by converting back and forth.
Args:
path (str): the path to check.
platform (str): the name of the platform as defined in file_utils.
Returns:
bool: True if check is passed, False otherwise.
Notes:
TODO: move to tests!
'''
# convert to data and back to path
built_data = self.parse(path)
built_path = self.format(built_data)
# check if result is the same (we need to be careful because of windows)
compare_path = file_utils.toComparePath(path, platform=platform)
compare_built_path = file_utils.toComparePath(built_path, platform=platform)
if compare_path != compare_built_path:
return False
return True
# keys ---
[docs]
def getRawKeys(self):
'''Get the keys as defined in lucidity template.
Args:
None.
Returns:
list: the unordered list of keys.
'''
return list(self.l_template.keys())
[docs]
def getDefaultKeys(self):
'''Get the keys that have a default value.
Args:
None.
Returns:
list: the unordered list of keys.
'''
return list(self.default_values.keys())
[docs]
def getRootKeys(self):
'''Get the keys that are root.
Args:
None.
Returns:
list: the unordered list of keys.
'''
return list(self.roots.keys())
[docs]
def getKeys(self, ordered=False, no_dups=True):
'''Get the keys as defined in lucidity template but without roots.
Args:
ordered (bool): whether or not to order the keys.
no_dups (bool): whether or not to remove the duplicates keeping the first
appearance for each key. Only valid for ordered.
Returns:
list: the unordered list of keys.
'''
if ordered:
keys = self._getOrderedKeyTokens(self.l_template.pattern, no_dups=no_dups)
else:
keys = self.getRawKeys()
# root keys should be removed?
root_keys = self.getRootKeys()
keys = [k for k in keys[:] if k not in root_keys]
return keys
[docs]
def _getOrderedKeyTokens(self, pattern, no_dups=True):
'''return keys found in a pattern in order.
Args:
pattern (str): the lucidity pattern.
no_dups (bool): whether or not to remove the duplicates keeping the first
appearance for each key.
Returns:
list[str]: the keys in order.
'''
# remove extra specs in pattern
ns_pattern = self.l_template._construct_format_specification(pattern) # pylint: disable=protected-access
# list all keys
tokens = self.l_template._PLAIN_PLACEHOLDER_REGEX.findall(ns_pattern) # pylint: disable=protected-access
# remove dups if requested while keeping order
if no_dups:
tokens_nd = []
for key in tokens:
if key not in tokens_nd:
tokens_nd.append(key)
tokens = tokens_nd
return tokens
# partial paths ---
[docs]
def _getPartialPattern(self, split_key):
'''return a partial pattern splitting by a key and up to the path separator
Args:
split_key (str): the key to do the split at (inclusive).
Returns:
str or None: the lucidity template pattern or None if it could not be made.
'''
# key must exists (and must not be a root key)
if split_key not in self.getKeys():
print('Get Partial Pattern: Cant split by a key ({}) not in the pattern!'.format(split_key))
return None
pattern = self.l_template.expanded_pattern()
rexp = '(^.*?{{{}(:|}}).*?)/'.format(split_key)
match = re.match(rexp, pattern)
if not match:
print('Get Partial Pattern: Cant split by a key ({})!'.format(split_key))
return
partial_pattern = match.group(1)
return partial_pattern
[docs]
def getPartialTemplateFile(self, split_key):
'''Returns a TemplateFile object with a partial pattern.
Args:
split_key (str): the key to do the split at (inclusive).
Returns:
TemplateFile or None: the TemplateFile with the partial pattern or None if it could not be made.
Examples:
Get paths from partial template and get data too::
partial_template_file = self.getPartialTemplateFile('entity')
paths = partial_template_file.getPaths()
datas = [template_file.parse(p) for p in paths]
'''
# get partial pattern
partial_pattern = self._getPartialPattern(split_key)
if partial_pattern is None:
raise KeyError('Cant build partial pattern with key {}'.format(split_key))
# duplicate template and replace pattern
l_temp = copy.deepcopy(self.l_template)
l_temp._pattern = partial_pattern # pylint: disable=protected-access
l_temp._construct_regular_expression(l_temp.pattern) # pylint: disable=protected-access
# build new template file
temp_file = TemplateFile(l_temp, roots=self.roots, default_values=self.default_values)
return temp_file