Commit 2bff84e8 authored by Christof Schulze's avatar Christof Schulze 😎
Browse files

added more yaml parsing and more yaml-errors

parent 654f9d5b
Loading
Loading
Loading
Loading

errors.py

deleted100644 → 0
+0 −67
Original line number Diff line number Diff line
# GNU Lesser General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/lgpl-3.0.txt)
# (c) 2019, Christof Schulze <christof.schulze@fau.de>
#
# This file is part of Ammsml
#
# Ammsml is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ammsml 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Ammsml.  If not, see <http://www.gnu.org/licenses/>.

from ammsml.utils.parsing import to_text
from ammsml.utils.parsing_yaml import AMMSMLBaseYAMLObject


class AMMSMLError(Exception):
    """
    This is the base class for all errors raised from Ammsml code,
    and can be instantiated with two optional parameters beyond the
    error message to control whether detailed information is displayed
    when the error occurred while parsing a data file of some kind.

    Usage:
        raise AMMSMLError('some message here', obj=obj, show_content=True)

    Where "obj" is some subclass of ammsml.utils.parsing.yaml.objects.AMMSMLBaseYAMLObject,
    which should be returned by the DataLoader() class.

    Something like this, TODO to be improved
    """

    def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None):
        super(AMMSMLError, self).__init__(message)

        self._obj = obj
        self._show_content = show_content

        if obj and isinstance(obj, AMMSMLBaseYAMLObject):
            extended_error = self._get_extended_error()
            if extended_error and not suppress_extended_error:
                self.message = '%s\n\n%s' % (to_text(message), to_text(extended_error))
            else:
                self.message = '%s' % to_text(message)
        else:
            self.message = '%s' % to_text(message)


class AMMSMLParserError(AMMSMLError):

    pass


class TemplateParsingError(AMMSMLError):
    """a templating failure"""
    pass


class AMMSMLOptionsError(AMMSMLError):

    pass

errors/__init__.py

0 → 100644
+197 −0
Original line number Diff line number Diff line
# GNU Lesser General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/lgpl-3.0.txt)
# (c) 2019, Christof Schulze <christof.schulze@fau.de>
#
# This file is part of Ammsml
#
# Ammsml is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ammsml 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Ammsml.  If not, see <http://www.gnu.org/licenses/>.

import re

from ammsml.utils.parsing import to_text
from ammsml.utils.parsing.yaml.objects import AMMSMLBaseYAMLObject

from ammsml.utils.errors.yaml_strings import (
    YAML_COMMON_DICT_ERROR,
    YAML_COMMON_LEADING_TAB_ERROR,
    YAML_COMMON_PARTIALLY_QUOTED_LINE_ERROR,
    YAML_COMMON_UNBALANCED_QUOTES_ERROR,
    YAML_COMMON_UNQUOTED_COLON_ERROR,
    YAML_COMMON_UNQUOTED_VARIABLE_ERROR,
    YAML_POSITION_DETAILS,
    YAML_AND_SHORTHAND_ERROR,
)


class AMMSMLError(Exception):
    """
    This is the base class for all errors raised from Ammsml code,
    and can be instantiated with two optional parameters beyond the
    error message to control whether detailed information is displayed
    when the error occurred while parsing a data file of some kind.

    Usage:
        raise AMMSMLError('some message here', obj=obj, show_content=True)

    Where "obj" is some subclass of ammsml.utils.parsing.yaml.objects.AMMSMLBaseYAMLObject,
    which should be returned by the DataLoader() class.

    Something like this, TODO to be improved
    """

    def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None):
        super(AMMSMLError, self).__init__(message)

        self._obj = obj
        self._show_content = show_content

        if obj and isinstance(obj, AMMSMLBaseYAMLObject):
            extended_error = self._get_extended_error()
            if extended_error and not suppress_extended_error:
                self.message = '%s\n\n%s' % (to_text(message), to_text(extended_error))
            else:
                self.message = '%s' % to_text(message)
        else:
            self.message = '%s' % to_text(message)
        if orig_exc:
            self.orig_exc = orig_exc

    def __str__(self):
        return self.message

    def __repr__(self):
        return self.message

    def _get_error_lines_from_file(self, file_name, line_number):
        """
        Returns the line in the file which corresponds to the reported error
        location, as well as the line preceding it (if the error did not
        occur on the first line), to provide context to the error.
        """

        target_line = ''
        prev_line = ''

        with open(file_name, 'r') as f:
            lines = f.readlines()

            target_line = lines[line_number]
            if line_number > 0:
                prev_line = lines[line_number - 1]

        return target_line, prev_line

    def _get_extended_error(self):
        """
        Given an object reporting the location of the exception in a file, return
        detailed information regarding it including:
          * the line which caused the error as well as the one preceding it
          * causes and suggested remedies for common syntax errors
        If this error was created with show_content=False, the reporting of content
        is suppressed, as the file contents may be sensitive (ie. vault data).
        TODO clean this up, much to long
        """

        error_message = ''

        try:
            (src_file, line_number, col_number) = self._obj.ammsml_pos
            error_message += YAML_POSITION_DETAILS % (src_file, line_number, col_number)
            if src_file not in ('<string>', '<unicode>') and self._show_content:
                (target_line, prev_line) = self._get_error_lines_from_file(src_file, line_number - 1)
                target_line = to_text(target_line)
                prev_line = to_text(prev_line)
                if target_line:
                    stripped_line = target_line.replace(" ", "")

                    # Check for k=v syntax in addition to YAML syntax and set the appropriate error position,
                    # arrow index
                    if re.search(r'\w+(\s+)?=(\s+)?[\w/-]+', prev_line):
                        error_position = prev_line.rstrip().find('=')
                        arrow_line = (" " * error_position) + "^ here"
                        error_message = YAML_POSITION_DETAILS % (src_file, line_number - 1, error_position + 1)
                        error_message += "\nThe offending line appears to be:\n\n%s\n%s\n\n" %\
                                         (prev_line.rstrip(), arrow_line)
                        error_message += YAML_AND_SHORTHAND_ERROR
                    else:
                        arrow_line = (" " * (col_number - 1)) + "^ here"
                        error_message += "\nThe offending line appears to be:\n\n%s\n%s\n%s\n" % (
                        prev_line.rstrip(), target_line.rstrip(), arrow_line)

                    # TODO: There may be cases where there is a valid tab in a line that has other errors.
                    if '\t' in target_line:
                        error_message += YAML_COMMON_LEADING_TAB_ERROR
                    # common error/remediation checking here:
                    # check for unquoted vars starting lines
                    if ('{{' in target_line and '}}' in target_line) and (
                            '"{{' not in target_line or "'{{" not in target_line):
                        error_message += YAML_COMMON_UNQUOTED_VARIABLE_ERROR
                    # check for common dictionary mistakes
                    elif ":{{" in stripped_line and "}}" in stripped_line:
                        error_message += YAML_COMMON_DICT_ERROR
                    # check for common unquoted colon mistakes
                    elif (len(target_line) and
                          len(target_line) > 1 and
                          len(target_line) > col_number and
                          target_line[col_number] == ":" and
                          target_line.count(':') > 1):
                        error_message += YAML_COMMON_UNQUOTED_COLON_ERROR
                    # otherwise, check for some common quoting mistakes
                    else:
                        # FIXME: This needs to split on the first ':' to account for modules like lineinfile
                        # that may have lines that contain legitimate colons, e.g., line: 'i ALL= (ALL) NOPASSWD: ALL'
                        # and throw off the quote matching logic.
                        parts = target_line.split(":")
                        if len(parts) > 1:
                            middle = parts[1].strip()
                            match = False
                            unbalanced = False

                            if middle.startswith("'") and not middle.endswith("'"):
                                match = True
                            elif middle.startswith('"') and not middle.endswith('"'):
                                match = True

                            if (len(middle) > 0 and
                                    middle[0] in ['"', "'"] and
                                    middle[-1] in ['"', "'"] and
                                    target_line.count("'") > 2 or
                                    target_line.count('"') > 2):
                                unbalanced = True

                            if match:
                                error_message += YAML_COMMON_PARTIALLY_QUOTED_LINE_ERROR
                            if unbalanced:
                                error_message += YAML_COMMON_UNBALANCED_QUOTES_ERROR

        except (IOError, TypeError):
            error_message += '\n(could not open file to display line)'
        except IndexError:
            error_message += '\n(specified line no longer in file, maybe it changed?)'

        return error_message


class AMMSMLParserError(AMMSMLError):

    pass


class TemplateParsingError(AMMSMLError):
    """a templating failure"""
    pass


class AMMSMLOptionsError(AMMSMLError):

    pass

errors/yaml_strings.py

0 → 100644
+95 −0
Original line number Diff line number Diff line
__all__ = [
    'YAML_SYNTAX_ERROR',
    'YAML_POSITION_DETAILS',
    'YAML_COMMON_DICT_ERROR',
    'YAML_COMMON_UNQUOTED_VARIABLE_ERROR',
    'YAML_COMMON_UNQUOTED_COLON_ERROR',
    'YAML_COMMON_PARTIALLY_QUOTED_LINE_ERROR',
    'YAML_COMMON_UNBALANCED_QUOTES_ERROR',
]

YAML_SYNTAX_ERROR = """\
Syntax Error while loading YAML.
  %s"""

YAML_POSITION_DETAILS = """\
The error appears to be in '%s': line %s, column %s, but may
be elsewhere in the file depending on the exact syntax problem.
"""

YAML_COMMON_DICT_ERROR = """\
This one looks easy to fix. YAML thought it was looking for the start of a
hash/dictionary and was confused to see a second "{". Most likely this was
meant to be an ammsml template evaluation instead, so we have to give the
parser a small hint that we wanted a string instead. The solution here is to
just quote the entire value.
For instance, if the original line was:
    app_path: {{ base_path }}/foo
It should be written as:
    app_path: "{{ base_path }}/foo"
"""

YAML_COMMON_UNQUOTED_VARIABLE_ERROR = """\
We could be wrong, but this one looks like it might be an issue with
missing quotes. Always quote template expression brackets when they
start a value. For instance:
    with_items:
      - {{ foo }}
Should be written as:
    with_items:
      - "{{ foo }}"
"""

YAML_COMMON_UNQUOTED_COLON_ERROR = """\
This one looks easy to fix. There seems to be an extra unquoted colon in the line
and this is confusing the parser. It was only expecting to find one free
colon. The solution is just add some quotes around the colon, or quote the
entire line after the first colon.
For instance, if the original line was:
    copy: src=file.txt dest=/path/filename:with_colon.txt
It can be written as:
    copy: src=file.txt dest='/path/filename:with_colon.txt'
Or:
    copy: 'src=file.txt dest=/path/filename:with_colon.txt'
"""

YAML_COMMON_PARTIALLY_QUOTED_LINE_ERROR = """\
This one looks easy to fix. It seems that there is a value started
with a quote, and the YAML parser is expecting to see the line ended
with the same kind of quote. For instance:
    when: "ok" in result.stdout
Could be written as:
   when: '"ok" in result.stdout'
Or equivalently:
   when: "'ok' in result.stdout"
"""

YAML_COMMON_UNBALANCED_QUOTES_ERROR = """\
We could be wrong, but this one looks like it might be an issue with
unbalanced quotes. If starting a value with a quote, make sure the
line ends with the same set of quotes. For instance this arbitrary
example:
    foo: "bad" "wolf"
Could be written as:
    foo: '"bad" "wolf"'
"""

YAML_COMMON_LEADING_TAB_ERROR = """\
There appears to be a tab character at the start of the line.
YAML does not use tabs for formatting. Tabs should be replaced with spaces.
For example:
    - name: update tooling
      vars:
        version: 1.2.3
#    ^--- there is a tab there.
Should be written as:
    - name: update tooling
      vars:
        version: 1.2.3
# ^--- all spaces here.
"""

YAML_AND_SHORTHAND_ERROR = """\
There appears to be both 'k=v' shorthand syntax and YAML in this task. \
Only one syntax may be used.
"""
 No newline at end of file
+0 −0

Empty file added.

parsing/yaml/dumper.py

0 → 100644
+28 −0
Original line number Diff line number Diff line
import yaml


# from ammsml.vars.hostvars import HostVars, HostVarsVars
from ammsml.utils.parsing.yaml.objects import AMMSMLUnicode, AMMSMLSequence, AMMSMLMapping


class AmmsmlDumper(yaml.SafeDumper):
    """
    A simple stub class that allows us to add representers
    for our overridden object types.
    """
    pass


def represent_hostvars(self, data):
    return self.represent_dict(dict(data))


represent_unicode = yaml.representer.SafeRepresenter.represent_str
represent_binary = yaml.representer.SafeRepresenter.represent_binary

AmmsmlDumper.add_representer(
    AMMSMLUnicode,
    represent_unicode,
)

# TODO add more representers
 No newline at end of file
Loading