diff --git a/test/v2/errors/__init__.py b/test/v2/errors/__init__.py new file mode 100644 index 0000000000..674334b15a --- /dev/null +++ b/test/v2/errors/__init__.py @@ -0,0 +1,18 @@ +# (c) 2012-2014, Michael DeHaan +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + diff --git a/test/v2/errors/test_errors.py b/test/v2/errors/test_errors.py new file mode 100644 index 0000000000..874195e476 --- /dev/null +++ b/test/v2/errors/test_errors.py @@ -0,0 +1,33 @@ +# TODO: header + +import unittest + +from mock import mock_open, patch + +from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject +from ansible.errors import AnsibleError + +class TestErrors(unittest.TestCase): + + def setUp(self): + self.message = 'this is the error message' + + def tearDown(self): + pass + + def test_basic_error(self): + e = AnsibleError(self.message) + assert e.message == self.message + + def test_error_with_object(self): + obj = AnsibleBaseYAMLObject() + obj._data_source = 'foo.yml' + obj._line_number = 1 + obj._column_number = 1 + + m = mock_open() + m.return_value.readlines.return_value = ['this is line 1\n', 'this is line 2\n', 'this is line 3\n'] + with patch('__builtin__.open', m): + e = AnsibleError(self.message, obj) + + assert e.message == 'this is the error message\nThe error occurred on line 1 of the file foo.yml:\nthis is line 1\n^' diff --git a/v2/ansible/errors/__init__.py b/v2/ansible/errors/__init__.py index ae629ba4bb..54406ef6c2 100644 --- a/v2/ansible/errors/__init__.py +++ b/v2/ansible/errors/__init__.py @@ -15,15 +15,46 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +import os +from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject + class AnsibleError(Exception): - def __init__(self, message, object=None): - self.message = message - self.object = object + def __init__(self, message, obj=None): + self._obj = obj + if isinstance(self._obj, AnsibleBaseYAMLObject): + extended_error = self._get_extended_error() + if extended_error: + self.message = '%s\n%s' % (message, extended_error) + else: + self.message = message - # TODO: nice __repr__ message that includes the line number if the object - # it was constructed with had the line number + def __repr__(self): + return self.message - # TODO: tests for the line number functionality + def _get_line_from_file(self, filename, line_number): + with open(filename, 'r') as f: + lines = f.readlines() + if line_number < len(lines): + return lines[line_number] + return None + + def _get_extended_error(self): + error_message = '' + + try: + (src_file, line_number, col_number) = self._obj.get_position_info() + error_message += 'The error occurred on line %d of the file %s:\n' % (line_number, src_file) + if src_file not in ('', ''): + responsible_line = self._get_line_from_file(src_file, line_number - 1) + if responsible_line: + error_message += responsible_line + error_message += (' ' * (col_number-1)) + '^' + except IOError: + 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 AnsibleParserError(AnsibleError): ''' something was detected early that is wrong about a playbook or data file ''' diff --git a/v2/ansible/parsing/yaml/objects.py b/v2/ansible/parsing/yaml/objects.py index cc9fc445d2..5870ea8cbe 100644 --- a/v2/ansible/parsing/yaml/objects.py +++ b/v2/ansible/parsing/yaml/objects.py @@ -8,6 +8,9 @@ class AnsibleBaseYAMLObject(object): _line_number = None _column_number = None + def get_position_info(self): + return (self._data_source, self._line_number, self._column_number) + class AnsibleMapping(AnsibleBaseYAMLObject, dict): ''' sub class for dictionaries ''' pass