#!/usr/bin/env python
# encoding: utf-8
# Copyright (C) 2016 Chintalagiri Shashank
#
# This file is part of tendril.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Docstring for validate
"""
# TODO Seriously refactor this file
from tendril.conventions.electronics import parse_ident
from tendril.conventions.electronics import ident_transform
from tendril.conventions.electronics import DEVICE_CLASSES
from colorama import Fore
from colorama import Style
from tendril.utils import terminal
from tendril.utils import log
logger = log.get_logger(__name__, log.DEFAULT)
[docs]class ValidatableBase(object):
def __init__(self):
self._validated = False
self._validation_context = None
self._validation_errors = ErrorCollector()
@property
def ident(self):
raise NotImplementedError
@ident.setter
def ident(self, value):
raise NotImplementedError
[docs] def _validate(self):
raise NotImplementedError
[docs] def validate(self):
if not self._validated:
logger.debug("Validating {0}".format(self.ident))
self._validate()
@property
def validation_errors(self):
if not self._validated:
self._validate()
return self._validation_errors
[docs]class ValidationContext(object):
def __init__(self, mod, locality=None):
self.mod = mod
self.locality = locality
def __repr__(self):
if self.locality:
return '/'.join([self.mod, self.locality])
else:
return self.mod
[docs] def render(self):
return self.locality
[docs]class ValidationError(Exception):
msg = "Validation Error"
def __init__(self, policy):
self._policy = policy
self.detail = None
@property
def policy(self):
return self._policy
[docs] def render(self):
return {
'is_error': self.policy.is_error,
'group': self.msg,
'headline': self._policy.context.render(),
'detail': self.detail,
}
[docs]class MissingFileError(ValidationError):
msg = "Missing File"
def __init__(self, policy):
super(MissingFileError, self).__init__(policy)
def __repr__(self):
return "<MissingFileWarning {0} {1}>".format(
self._policy.context, self._policy.path
)
[docs] def render(self):
return {
'is_error': self.policy.is_error,
'group': self.msg,
'headline': "Missing {0}".format(self._policy.context.render()),
'detail': self._policy.path,
}
[docs]class MangledFileError(ValidationError):
msg = "Unable to Parse File"
def __init__(self, policy):
super(MangledFileError, self).__init__(policy)
def __repr__(self):
return "<MangledFileError {0} {1}>".format(
self._policy.context, self._policy.path
)
[docs] def render(self):
return {
'is_error': self.policy.is_error,
'group': self.msg,
'headline': "Mangled {0}".format(self._policy.context.render()),
'detail': self._policy.path,
}
[docs]class ContextualConfigError(ValidationError):
msg = "Incorrect Configuration"
def __init__(self, policy):
super(ContextualConfigError, self).__init__(policy)
[docs] def _format_path(self):
if isinstance(self._policy.path, tuple):
return '/'.join(self._policy.path)
else:
return self._policy.path
[docs] def render(self):
return {
'is_error': self.policy.is_error,
'group': self.msg,
'headline': self._policy.context.render(),
'detail': "Configuration seems to be incorrect.",
}
[docs]class ConfigKeyError(ContextualConfigError):
msg = "Configuration Key Missing"
def __init__(self, policy):
super(ConfigKeyError, self).__init__(policy)
def __repr__(self):
return "<ConfigKeyError {0} {1}>" \
"".format(self._policy.context, self._format_path())
[docs] def render(self):
if self._policy.options:
option_str = "Valid options are {0}" \
"".format(', '.join(self._policy.options))
else:
option_str = ''
return {
'is_error': self.policy.is_error,
'group': self.msg,
'headline': "{0} missing in {1}"
"".format(self._format_path(),
self._policy.context.render()),
'detail': "This required configuration option could not be "
"found in the configs file. " + option_str,
}
[docs]class ConfigValueInvalidError(ContextualConfigError):
msg = "Configuration Value Unrecognized"
def __init__(self, policy, value):
super(ConfigValueInvalidError, self).__init__(policy)
self._value = value
def __repr__(self):
return "<ConfigValueInvalidError {0} {1}>" \
"".format(self._policy.context, self._format_path())
[docs] def render(self):
if self._policy.options:
option_str = "Valid options are {0}".format(', '.join(self._policy.options))
else:
option_str = ''
return {
'is_error': self.policy.is_error,
'group': self.msg,
'headline': "'{0}' Invalid for {1} in {2}"
"".format(self._value, self._format_path(),
self._policy.context.render()),
'detail': "The value provided for this configuration option is "
"unrecognized or not allowed in this context. " + option_str,
}
[docs]class IdentErrorBase(ValidationError):
def __init__(self, policy, ident, refdeslist):
self.ident = ident
self.refdeslist = refdeslist
self._policy = policy
[docs]class IdentNotRecognized(IdentErrorBase):
msg = "Ident Not Recognized"
def __init__(self, policy, ident, refdeslist):
super(IdentNotRecognized, self).__init__(policy, ident, refdeslist)
def __repr__(self):
return "<IdentNotRecognized {0} {1}>" \
"".format(self.ident, ', '.join(self.refdeslist))
[docs] def render(self):
return {
'is_error': self.policy.is_error,
'group': self.msg,
'headline': "'{0}'".format(self.ident),
'detail': "This ident is not recognized by the library and is "
"therefore deemed invalid. Used by refdes {0}"
"".format(', '.join(self.refdeslist)),
'detail_core': ', '.join(self.refdeslist),
}
[docs]class DeviceNotRecognized(IdentErrorBase):
msg = "Device Not Recognized"
def __init__(self, policy, ident, refdeslist):
super(DeviceNotRecognized, self).__init__(policy, ident, refdeslist)
def __repr__(self):
return "<DeviceNotRecognized {0} {1}>" \
"".format(self.ident, ', '.join(self.refdeslist))
[docs] def render(self):
return {
'is_error': self.policy.is_error,
'group': self.msg,
'headline': "'{0}' does not have a recognized Device."
"".format(self.ident),
'detail': "This ident does not have a recognized device "
"string. It is therefore unlikely to be correctly "
"handled. Used by refdes {0}"
"".format(', '.join(self.refdeslist)),
}
[docs]class QuantityTypeError(IdentErrorBase):
msg = "Quantity Type Mismatch"
def __init__(self, policy, ident, refdeslist):
super(QuantityTypeError, self).__init__(policy, ident, refdeslist)
def __repr__(self):
return "<QuantityTypeError {0} {1}>" \
"".format(self.ident, ', '.join(self.refdeslist))
[docs] def render(self):
return {
'is_error': self.policy.is_error,
'group': self.msg,
'headline': "Quantity for '{0}' could not be determined."
"".format(self.ident),
'detail': "The quantity for this ident could not be determined. "
"This is often due to mismatches / errors in the types "
"for one or more of the specified quantities. See {0}."
"".format(', '.join(self.refdeslist)),
}
[docs]class BomGroupError(ValidationError):
msg = "Group not found in Configs file"
def __init__(self, policy, tgroup, refdes, ident=None):
super(BomGroupError, self).__init__(policy)
self._tgroup = tgroup
self._refdes = refdes
self._ident = ident
def __repr__(self):
return '<BomGroupError {0} {1}>'.format(self._tgroup, self._refdes)
[docs] def render(self):
return {
'is_error': self.policy.is_error,
'group': self.msg,
'headline': "Group '{0}' for {1} not found in the configs file."
"".format(self._tgroup, self._refdes),
'detail': "The declared group should either be added to the "
"configs file, or changed to one of the defined "
"component groups - {0}."
"".format(', '.join(self.policy.known_groups)),
}
[docs]class BomMotifUnrecognizedError(ValidationError):
msg = "Motif Definition Unrecognized"
def __init__(self, policy, motifst, refdes):
super(BomMotifUnrecognizedError, self).__init__(policy)
self._motifst = motifst
self._refdes = refdes
def __repr__(self):
return '<BomMotifUnrecognizedError {0} {1}>' \
''.format(self._policy.context.render, self._motifst)
[docs] def render(self):
return {
'group': self.msg,
'is_error': self.policy.is_error,
'headline': "Motif '{0}' for {1} is not recognized."
"".format(self._motifst, self._refdes),
'detail': "The listed motif is not recognized by tendril and is "
"not handled. Component {0} is not included in the BOM."
"".format(self._refdes),
}
[docs]class ConfigMotifMissingError(ValidationError):
msg = "Motif in Configs not found in Schematic"
def __init__(self, policy, refdes):
super(ConfigMotifMissingError, self).__init__(policy)
self.refdes = refdes
def __repr__(self):
return "<ConfigMotifMissingError {0} {1}>" \
"".format(self.policy.context.render, self.refdes)
[docs] def render(self):
return {
'group': self.msg,
'is_error': self.policy.is_error,
'headline': "Motif '{0}' could not be found in the schematic."
"".format(self.refdes),
'detail': "No elements corresponding to motif {0} were found in "
"the schematic. None of the transformations related to "
"this motif configuration will be made to the BOM."
"".format(self.refdes),
}
[docs]class ConfigGroupError(ValidationError):
msg = "Group in config definitions unrecognized"
def __init__(self, policy, groupname):
super(ConfigGroupError, self).__init__(policy)
self.groupname = groupname
def __repr__(self):
return "<ConfigGroupError {0}>".format(self.groupname)
[docs] def render(self):
return {
'group': self.msg,
'is_error': self.policy.is_error,
'headline': "Group '{0}' unrecognized."
"".format(self.groupname),
'detail': "This group is listed for inclusion in this "
"configuration but is not recognized. Recheck "
"configuration grouplist. Defined groups are : {0}."
"".format(', '.join(self.policy.known_groups)),
}
[docs]class ConfigSJUnexpectedError(ValidationError):
msg = "Fillstatus of non-configurable component changed"
def __init__(self, policy, refdes, fillstatus):
super(ConfigSJUnexpectedError, self).__init__(policy)
self.refdes = refdes
self.fillstatus = fillstatus
def __repr__(self):
return "<ConfigSJUnexpectedError {0} {1}>" \
"".format(self.refdes, self.fillstatus)
[docs] def render(self):
return {
'group': self.msg,
'is_error': self.policy.is_error,
'headline': "Unexpected inclusion of '{0}' with fillstatus '{1}' "
"in sjlist.".format(self.refdes, self.fillstatus),
'detail': "This component's fillstatus is not expected to be "
"changed by the configs via the sjlist route. If "
"such modification is intended, change it's fillstatus "
"in the schematic to 'CONF'.",
}
[docs]class ValidationPolicy(object):
def __init__(self, context, is_error=True):
self.context = context
self.is_error = is_error
[docs]class ConfigMotifPolicy(ValidationPolicy):
def __init__(self, context):
super(ConfigMotifPolicy, self).__init__(context)
self.is_error = True
[docs]class ConfigGroupPolicy(ValidationPolicy):
def __init__(self, context, known_groups):
super(ConfigGroupPolicy, self).__init__(context)
self.is_error = True
self.known_groups = known_groups
[docs]class ConfigSJPolicy(ValidationPolicy):
def __init__(self, context):
super(ConfigSJPolicy, self).__init__(context)
self.is_error = False
[docs]class BomMotifPolicy(ValidationPolicy):
def __init__(self, context):
super(BomMotifPolicy, self).__init__(context)
self.is_error = True
[docs]class BomGroupPolicy(ValidationPolicy):
def __init__(self, context, known_groups, file_groups=None,
allow_blank=True, default='default'):
super(BomGroupPolicy, self).__init__(context)
self._known_groups = known_groups
self._file_groups = file_groups
self._allow_blank = allow_blank
self._default = default
self.is_error = False
[docs] def check(self, item):
group = item.data['group']
schfile = item.data['schfile']
if not schfile or schfile == 'unknown':
schfiles = []
else:
schfiles = schfile.split(';')
done = False
if not group or group == 'unknown':
for f in schfiles:
if f in self.file_groups.keys():
group = self.file_groups[f]
done = True
if not done:
group = 'default'
if group not in self._known_groups:
raise BomGroupError(self, group,
item.data['refdes'],
ident_transform(item.data['device'],
item.data['value'],
item.data['footprint'])
)
return group
@property
def default(self):
return self._default
@property
def known_groups(self):
return self._known_groups
@property
def file_groups(self):
return self._file_groups
[docs]class IdentPolicy(ValidationPolicy):
def __init__(self, context, rfunc):
super(IdentPolicy, self).__init__(context)
self.is_error = False
self._rfunc = rfunc
[docs] def check(self, ident, refdeslist, cstatus):
d, v, f = parse_ident(ident)
if d not in DEVICE_CLASSES:
self.is_error = True
raise DeviceNotRecognized(self, ident, refdeslist)
if not self._rfunc(ident):
self.is_error = False
raise IdentNotRecognized(self, ident, refdeslist)
[docs]class IdentQtyPolicy(ValidationPolicy):
def __init__(self, context, is_error):
super(IdentQtyPolicy, self).__init__(context)
self.is_error = is_error
[docs]class ConfigOptionPolicy(ValidationPolicy):
def __init__(self, context, path, options=None, default=None, is_error=True):
super(ConfigOptionPolicy, self).__init__(context, is_error)
self.path = path
self.options = options
self.default = default
[docs]class FilePolicy(ValidationPolicy):
def __init__(self, context, path, is_error):
super(FilePolicy, self).__init__(context, is_error)
self.path = path
[docs]def get_dict_val(d, policy=None):
assert isinstance(d, dict)
if isinstance(policy.path, tuple):
try:
for key in policy.path:
if key not in d.keys():
raise KeyError
d = d.get(key)
except KeyError:
raise ConfigKeyError(policy=policy)
rval = d
else:
try:
if policy.path not in d.keys():
raise KeyError
rval = d.get(policy.path)
except KeyError:
raise ConfigKeyError(policy=policy)
if policy.options is None or rval in policy.options:
return rval
else:
raise ConfigValueInvalidError(policy=policy, value=rval)
[docs]class ErrorCollector(ValidationError):
def __init__(self):
self._errors = []
[docs] def add(self, e):
if isinstance(e, ErrorCollector):
for error in e.errors:
self._errors.append(error)
else:
self._errors.append(e)
@property
def errors(self):
return self._errors
@property
def terrors(self):
return len(self._errors)
@property
def derrors(self):
return [x for x in self._errors if x.policy.is_error]
@property
def dwarnings(self):
return [x for x in self._errors if not x.policy.is_error]
@property
def nerrors(self):
return len(self.derrors)
@property
def nwarnings(self):
return len(self.dwarnings)
@staticmethod
[docs] def _group_errors(errors):
rval = {}
for error in errors:
etype = error['group']
if etype in rval.keys():
rval[etype].append(error)
else:
rval[etype] = [error]
return rval
@property
def errors_by_type(self):
lerrors = [x.render() for x in self.derrors]
return self._group_errors(lerrors)
@property
def warnings_by_type(self):
lwarnings = [x.render() for x in self.dwarnings]
return self._group_errors(lwarnings)
def __repr__(self):
rval = 'Collected Errors:\n'
for e in self._errors:
rval += ' {0}\n'.format(repr(e))
return rval
[docs] def _render_cli_group(self, g):
for idx, i in enumerate(g):
if 'detail_core' in i.keys():
detail = i['detail_core']
else:
detail = i['detail']
print("{0}.{1} : {2}"
"".format(idx + 1, i['headline'], detail))
[docs] def render_cli(self, name):
width = terminal.get_terminal_width()
hline = '-' * width
print(hline + Style.BRIGHT)
titleformat = "{0:<" + str(width - 13) + "} {1:>2} {2}"
print(titleformat.format(name, self.terrors, 'ALERTS') + Style.NORMAL)
if self.nerrors:
print(Fore.RED + hline)
print(titleformat.format('', self.nerrors, 'ERRORS'))
for n, g in self.errors_by_type.items():
print(hline + Style.BRIGHT)
print(titleformat.format(n, len(g), 'INSTANCES') + Style.NORMAL)
self._render_cli_group(g)
if self.nwarnings:
print(Fore.YELLOW + hline)
print(titleformat.format('', self.nwarnings, 'WARNINGS'))
for n, g in self.warnings_by_type.items():
print(hline + Style.BRIGHT)
print(titleformat.format(n, len(g), 'INSTANCES') + Style.NORMAL)
self._render_cli_group(g)
print(Fore.RESET + Style.BRIGHT + hline + Style.NORMAL)