<commit_before>"""A module containing validation functionality."""

import sys
from lxml import etree
import yaml
import iati.core.default
import iati.core.resources


class ValidationError(object):
    """A base class to encapsulate information about Validation Errors."""

    def __init__(self, err_name, calling_locals=dict()):
        """Create a new ValidationError.

        Args:
            err_name (str): The name of the error to use as a base.
            calling_locals (dict): The dictionary of local variables from the calling scope. Obtained by calling `locals()`. Default is an empty dictionary.

        Raises:
            ValueError: If there is no base error with the provided name.

        Todo:
            Split message formatting into a child class and raise an error when variables are missing.

        """
        try:
            err_detail = get_error_codes()[err_name]
        except (KeyError, TypeError):
            raise ValueError('{err_name} is not a known type of ValidationError.'.format(**locals()))

        # set general attributes for this type of error
        self.name = err_name

        for key, val in err_detail.items():
            setattr(self, key, val)

        self.status = 'error' if err_name.split('-')[0] =='err' else 'warning'

        # format error messages with context-specific info
        try:
            self.help = self.help.format(**calling_locals)
            self.info = self.info.format(**calling_locals)
        except KeyError as missing_var_err:
            # raise NameError('The calling scope must contain a `{missing_var_err.args[0]}` variable for providing information for the error message.'.format(**locals()))
            pass

        # set general attributes for this type of error that require context from the calling scope
        # TODO: Determine what the defaults should be should the appropriate values not be available
        try:
            self.line_number = calling_locals['line_number']
            self.context = calling_locals['dataset'].source_around_line(self.line_number)
        except KeyError:
            pass
        try:
            self.column_number = calling_locals['column_number']
        except KeyError:
            pass
        try:
            self.err = calling_locals['err']
            self.lxml_err_code = calling_locals['err'].type_name
        except (AttributeError, KeyError):
            pass


class ValidationErrorLog(object):
    """A container to keep track of a set of ValidationErrors.

    This acts as an iterable that ValidationErrors can be looped over.

    ValidationErrors may be added to the log.

    """

    def __init__(self):
        """Initialise the error log."""
        self._values = []

    def __iter__(self):
        """Return an iterator."""
        return iter(self._values)

    def __len__(self):
        """The number of items in the ErrorLog."""
        return len(self._values)

    def __getitem__(self, key):
        """Return an item with the specified key."""
        return self._values[key]

    def __eq__(self, other):
        """Test equality with another object."""
        if len(self._values) != len(other._values):
            return False

        for val in self._values:
            if not val in other._values:
                return False

        return True

    def add(self, value):
        """Add a single ValidationError to the Error Log.

        Args:
            value (iati.validator.ValidationError): The ValidationError to add to the Error Log.

        Raises:
            TypeError: When attempting to set an item that is not a ValidationError.

        """
        if not isinstance(value, iati.validator.ValidationError):
            raise TypeError('Only ValidationErrors may be added to a ValidationErrorLog.')

        self._values.append(value)

    def contains_error_called(self, err_name):
        """Check the log for an error or warning with the specified name.

        Args:
            err_name (str): The name of the error to look for.

        Returns:
            bool: Whether there is an error or warning with the specified name within the log.

        """
        errors_with_name = self.get_errors_or_warnings_by_name(err_name)

        return len(errors_with_name) > 0

    def contains_error_of_type(self, err_type):
        """Check the log for an error or warning with the specified base exception type.

        Args:
            err_type (type): The type of the error to look for.

        Returns:
            bool: Whether there is an error or warning with the specified type within the log.

        """
        errors_with_type = self.get_errors_or_warning_by_type(err_type)

        return len(errors_with_type) > 0

    def contains_errors(self):
        """Determine whether there are errors contained within the ErrorLog.

        Note:
            The error log may contain warnings, or may be empty.

        Returns:
            bool: Whether there are errors within this error log.

        """
        errors = self.get_errors()

        return len(errors) > 0

    def contains_warnings(self):
        """Determine whether there are warnings contained within the ErrorLog.

        Note:
            The error log may contain errors, or may be empty.

        Returns:
            bool: Whether there are warnings within this error log.

        """
        warnings = self.get_warnings()

        return len(warnings) > 0

    def extend(self, values):
        """Extend the ErrorLog with ValidationErrors from an iterable.

        Args:
            values (iterable): An iterable containing ValidationErrors.

        Note:
            All ValidationErrors within the iterable shall be added. Any other contents shall not, and will fail to be added silently.

        Raises:
            TypeError: When values is not an iterable.

        """
        for value in values:
            try:
               self.add(value)
            except TypeError:
                pass

    def get_errors(self):
        """Return a list of errors contained.

        Returns:
            list: Containing errors only.

        Todo:
            Add explicit tests.
        """
        return [err for err in self if err.status == 'error']

    def get_errors_or_warnings_by_category(self, err_category):
        """Return a list of errors or warnings of the specified category.

        Args:
            err_category (str): The category of the error to look for.

        Returns:
            list: Containing the error/s or warning/s of the specified category that are present within the log.

        Todo:
            Add explicit tests.

        """
        return [err for err in self._values if err.category == err_category]

    def get_errors_or_warnings_by_name(self, err_name):
        """Return a list of errors or warnings with the specified name.

        Args:
            err_name (str): The name of the error to look for.

        Returns:
            list: Containing the error/s or warning/s with the specified name that are present within the log.

        Todo:
            Add explicit tests.

        """
        return [err for err in self._values if err.name == err_name]

    def get_errors_or_warning_by_type(self, err_type):
        """Return a list of errors or warnings of the specified type.

        Args:
            err_type (type): The type of the error to look for.

        Returns:
            list: Containing the error/s or warning/s of the specified type that are present within the log.

        Todo:
            Add explicit tests.

        """
        return [err for err in self._values if err.base_exception == err_type]

    def get_warnings(self):
        """Return a list of warnings contained.

        Returns:
            list: Containing warnings only.

        Todo:
            Add explicit tests.

        """
        return [err for err in self if err.status == 'warning']


def _check_codes(dataset, codelist):
    """Determine whether a given Dataset has values from the specified Codelist where expected.<commit_msg>Make docstrings more consistent and Sphinx-friendly<commit_after>"""A module containing validation functionality."""

import sys
from lxml import etree
import yaml
import iati.core.default
import iati.core.resources


class ValidationError(object):
    """A base class to encapsulate information about Validation Errors."""

    def __init__(self, err_name, calling_locals=dict()):
        """Create a new ValidationError.

        Args:
            err_name (str): The name of the error to use as a base.
            calling_locals (dict): The dictionary of local variables from the calling scope. Obtained by calling `locals()`. Default is an empty dictionary.

        Raises:
            ValueError: If there is no base error with the provided name.

        Todo:
            Split message formatting into a child class and raise an error when variables are missing.

        """
        try:
            err_detail = get_error_codes()[err_name]
        except (KeyError, TypeError):
            raise ValueError('{err_name} is not a known type of ValidationError.'.format(**locals()))

        # set general attributes for this type of error
        self.name = err_name

        for key, val in err_detail.items():
            setattr(self, key, val)

        self.status = 'error' if err_name.split('-')[0] =='err' else 'warning'

        # format error messages with context-specific info
        try:
            self.help = self.help.format(**calling_locals)
            self.info = self.info.format(**calling_locals)
        except KeyError as missing_var_err:
            # raise NameError('The calling scope must contain a `{missing_var_err.args[0]}` variable for providing information for the error message.'.format(**locals()))
            pass

        # set general attributes for this type of error that require context from the calling scope
        # TODO: Determine what the defaults should be should the appropriate values not be available
        try:
            self.line_number = calling_locals['line_number']
            self.context = calling_locals['dataset'].source_around_line(self.line_number)
        except KeyError:
            pass
        try:
            self.column_number = calling_locals['column_number']
        except KeyError:
            pass
        try:
            self.err = calling_locals['err']
            self.lxml_err_code = calling_locals['err'].type_name
        except (AttributeError, KeyError):
            pass


class ValidationErrorLog(object):
    """A container to keep track of a set of ValidationErrors.

    This acts as an iterable that ValidationErrors can be looped over.

    ValidationErrors may be added to the log.

    """

    def __init__(self):
        """Initialise the error log."""
        self._values = []

    def __iter__(self):
        """Return an iterator."""
        return iter(self._values)

    def __len__(self):
        """The number of items in the ErrorLog."""
        return len(self._values)

    def __getitem__(self, key):
        """Return an item with the specified key."""
        return self._values[key]

    def __eq__(self, other):
        """Test equality with another object."""
        if len(self._values) != len(other._values):
            return False

        for val in self._values:
            if not val in other._values:
                return False

        return True

    def add(self, value):
        """Add a single ValidationError to the Error Log.

        Args:
            value (iati.validator.ValidationError): The ValidationError to add to the Error Log.

        Raises:
            TypeError: When attempting to set an item that is not a ValidationError.

        """
        if not isinstance(value, iati.validator.ValidationError):
            raise TypeError('Only ValidationErrors may be added to a ValidationErrorLog.')

        self._values.append(value)

    def contains_error_called(self, err_name):
        """Check the log for an error or warning with the specified name.

        Args:
            err_name (str): The name of the error to look for.

        Returns:
            bool: Whether there is an error or warning with the specified name within the log.

        """
        errors_with_name = self.get_errors_or_warnings_by_name(err_name)

        return len(errors_with_name) > 0

    def contains_error_of_type(self, err_type):
        """Check the log for an error or warning with the specified base exception type.

        Args:
            err_type (type): The type of the error to look for.

        Returns:
            bool: Whether there is an error or warning with the specified type within the log.

        """
        errors_with_type = self.get_errors_or_warning_by_type(err_type)

        return len(errors_with_type) > 0

    def contains_errors(self):
        """Determine whether there are errors contained within the ErrorLog.

        Note:
            The error log may contain warnings, or may be empty.

        Returns:
            bool: Whether there are errors within this error log.

        """
        errors = self.get_errors()

        return len(errors) > 0

    def contains_warnings(self):
        """Determine whether there are warnings contained within the ErrorLog.

        Note:
            The error log may contain errors, or may be empty.

        Returns:
            bool: Whether there are warnings within this error log.

        """
        warnings = self.get_warnings()

        return len(warnings) > 0

    def extend(self, values):
        """Extend the ErrorLog with ValidationErrors from an iterable.

        Args:
            values (iterable): An iterable containing ValidationErrors.

        Note:
            All ValidationErrors within the iterable shall be added. Any other contents shall not, and will fail to be added silently.

        Raises:
            TypeError: When values is not an iterable.

        """
        for value in values:
            try:
               self.add(value)
            except TypeError:
                pass

    def get_errors(self):
        """Return a list of errors contained.

        Returns:
            list(ValidationError): A list of all errors (but not warnings) that are present within the log.

        Todo:
            Add explicit tests.

        """
        return [err for err in self if err.status == 'error']

    def get_errors_or_warnings_by_category(self, err_category):
        """Return a list of errors or warnings of the specified category.

        Args:
            err_category (str): The category of the error to look for.

        Returns:
            list(ValidationError): A list of errors and warnings of the specified category that are present within the log.

        Todo:
            Add explicit tests.

        """
        return [err for err in self._values if err.category == err_category]

    def get_errors_or_warnings_by_name(self, err_name):
        """Return a list of errors or warnings with the specified name.

        Args:
            err_name (str): The name of the error to look for.

        Returns:
            list(ValidationError): A list of errors and warnings with the specified name that are present within the log.

        Todo:
            Add explicit tests.

        """
        return [err for err in self._values if err.name == err_name]

    def get_errors_or_warning_by_type(self, err_type):
        """Return a list of errors or warnings of the specified type.

        Args:
            err_type (type): The type of the error to look for.

        Returns:
            list(ValidationError): A list of errors and warnings of the specified type that are present within the log.

        Todo:
            Add explicit tests.

        """
        return [err for err in self._values if err.base_exception == err_type]

    def get_warnings(self):
        """Return a list of warnings contained.

        Returns:
            list(ValidationError): A list of all warnings (but not errors) that are present within the log.

        Todo:
            Add explicit tests.

        """
        return [err for err in self if err.status == 'warning']


def _check_codes(dataset, codelist):
    """Determine whether a given Dataset has values from the specified Codelist where expected.
{'fraction_non_alphanumeric': 0.07269957343083486, 'fraction_numerical': 0.0007312614259597807, 'mean_word_length': 2.156568570879015, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 4}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '760359', 'n_tokens_mistral': 4270, 'n_tokens_neox': 4056, 'n_words': 1673}