Skip to content

Commit

Permalink
display nice error messages on XMLSyntaxError
Browse files Browse the repository at this point in the history
  • Loading branch information
flupke committed Jan 26, 2018
1 parent 858ae1f commit a5e2cb8
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 2 deletions.
37 changes: 37 additions & 0 deletions jenskipper/cli/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
import jinja2.exceptions
import yaml.error

try:
from lxml import etree
HAVE_LXML = True
except ImportError:
HAVE_LXML = False

from .. import repository
from .. import conf
from .. import exceptions
Expand Down Expand Up @@ -234,6 +240,35 @@ def wrapper(**kwargs):
return wrapper


def handle_lxml_syntax_errors():
'''
Prints nice error messages on :class:`lxml.etree.XMLSyntaxError`.
'''

def decorator(func):
if HAVE_LXML:

@click.option('--full-xml/--no-full-xml', default=False,
help='Display full XML in XML syntax errors.')
@functools.wraps(func)
def wrapper(full_xml, **kwargs):
try:
return func(**kwargs)
except etree.XMLSyntaxError as exc:
click.echo(utils.format_lxml_syntax_error(
exc, full_xml=full_xml
), err=True)
sys.exit(1)

else:

wrapper = func

return wrapper

return decorator


def handle_all_errors(for_repos_command=True):
'''
Return a decorator that regroups all the error handling decorators.
Expand All @@ -248,6 +283,7 @@ def decorator(func):
@handle_conf_errors
@handle_jinja_errors
@handle_yaml_errors
@handle_lxml_syntax_errors()
@functools.wraps(func)
def wrapper(**kwargs):
return func(**kwargs)
Expand All @@ -256,6 +292,7 @@ def wrapper(**kwargs):

@handle_conf_errors
@handle_yaml_errors
@handle_lxml_syntax_errors()
@functools.wraps(func)
def wrapper(**kwargs):
return func(**kwargs)
Expand Down
6 changes: 4 additions & 2 deletions jenskipper/cli/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,14 @@ def get_job_diff(base_dir, jenkins_url, job_name, context_overrides=None,
context_overrides = {}
local_xml, _ = repository.get_job_conf(base_dir, job_name,
context_overrides)
local_xml = _prepare_xml(local_xml)
with utils.add_lxml_syntax_error_context(local_xml, job_name):
local_xml = _prepare_xml(local_xml)
remote_xml, _ = jenkins_api.handle_auth(base_dir,
jenkins_api.get_job_config,
jenkins_url,
job_name)
remote_xml = _prepare_xml(remote_xml)
with utils.add_lxml_syntax_error_context(remote_xml, job_name):
remote_xml = _prepare_xml(remote_xml)
from_text = remote_xml
to_text = local_xml
from_file = 'remote/%s.xml' % job_name
Expand Down
87 changes: 87 additions & 0 deletions jenskipper/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import urlparse
import copy
import collections
import contextlib

import click

Expand Down Expand Up @@ -138,6 +139,92 @@ def flatten_dict(dct):
return dict(items)


@contextlib.contextmanager
def add_lxml_syntax_error_context(full_xml, context):
'''
Augment :class:`lxml.etree.XMLSyntaxError` exceptions for
:func:`format_lxml_syntax_error`.
Usage::
with add_lxml_syntax_error_context(full_xml, 'somefile.xml'):
a_function_that_raises_XMLSyntaxError(full_xml)
'''
if HAVE_LXML:
try:
yield
except etree.XMLSyntaxError as exc:
exc.full_xml = full_xml
exc.context = context
raise
else:
yield


def format_lxml_syntax_error(exc, context_lines=5, full_xml=False):
'''
Format a :class:`lxml.etree.XMLSyntaxError`, showing the error's
context_lines.
*exc* should have been augmented with :func:`add_lxml_syntax_error_context`
first.
*name* is just a generic name indicating where the error occurred (for
example the name of a job).
*context_lines* is the number of lines to show around the error. If
*full_xml* is true, show the entire XML.
'''
lines = exc.full_xml.splitlines()
err_line = exc.lineno - 1
err_offset = exc.offset - 1
if full_xml:
start_line = 0
end_line = None
else:
start_line = err_line - context_lines
end_line = err_line + context_lines
before_context = lines[start_line:err_line]
error_line = lines[err_line]
after_context = lines[err_line + 1:end_line]
lines = [
'XML syntax error in %s:' % click.style(exc.context, bold=True),
'',
click.style(exc.message, bold=True),
'',
]

# Get the error context lines
xml_lines = []
xml_lines += before_context
xml_lines.append(
click.style(error_line[:err_offset], fg='red') +
click.style(error_line[err_offset], fg='red', bold=True) +
click.style(error_line[err_offset + 1:], fg='red')
)
xml_lines_error_index = len(xml_lines)
xml_lines += after_context

# Add line numbers gutter
gutter_width = len('%s' % (len(xml_lines) + start_line + 1))
gutter_fmt = '%%%si' % gutter_width
margin_width = 2
xml_lines = [
click.style(gutter_fmt % (i + start_line + 1),
fg='black', bold=True) +
' ' * margin_width + l
for i, l in enumerate(xml_lines)
]

# Add error marker
xml_lines.insert(xml_lines_error_index,
' ' * (err_offset + margin_width + gutter_width) + '^')

lines += xml_lines
return '\n'.join(lines)


def _flatten_dict(dct, ancestors=()):
items = []
for key, value in dct.items():
Expand Down

0 comments on commit a5e2cb8

Please sign in to comment.