Skip to content

Commit 452f97f

Browse files
committed
Merge pull request jazzband#545 from chipx86/inline-compile-errors
Add error output for compiler errors within the browser.
2 parents f2366b5 + 3b5c5a2 commit 452f97f

File tree

8 files changed

+108
-13
lines changed

8 files changed

+108
-13
lines changed

docs/configuration.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,18 @@ Defaults to ``True``
145145

146146
this only work when PIPELINE_ENABLED is False.
147147

148+
``SHOW_ERRORS_INLINE``
149+
......................
150+
151+
``True`` if errors compiling CSS/JavaScript files should be shown inline at
152+
the top of the browser window, or ``False`` if they should trigger exceptions
153+
(the older behavior).
154+
155+
This only applies when compiling through the ``{% stylesheet %}`` or
156+
``{% javascript %}`` template tags. It won't impact ``collectstatic``.
157+
158+
Defaults to ``settings.DEBUG``.
159+
148160
``CSS_COMPRESSOR``
149161
..................
150162

pipeline/compilers/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from django.contrib.staticfiles.storage import staticfiles_storage
99
from django.core.files.base import ContentFile
1010
from django.utils.encoding import smart_bytes
11-
from django.utils.six import string_types
11+
from django.utils.six import string_types, text_type
1212

1313
from pipeline.conf import settings
1414
from pipeline.exceptions import CompilerError
@@ -125,7 +125,9 @@ def execute_command(self, command, cwd=None, stdout_captured=None):
125125
if compiling.returncode != 0:
126126
stdout_captured = None # Don't save erroneous result.
127127
raise CompilerError(
128-
"{0!r} exit code {1}\n{2}".format(argument_list, compiling.returncode, stderr))
128+
"{0!r} exit code {1}\n{2}".format(argument_list, compiling.returncode, stderr),
129+
command=argument_list,
130+
error_output=stderr)
129131

130132
# User wants to see everything that happened.
131133
if self.verbose:
@@ -134,7 +136,8 @@ def execute_command(self, command, cwd=None, stdout_captured=None):
134136
print(stderr)
135137
except OSError as e:
136138
stdout_captured = None # Don't save erroneous result.
137-
raise CompilerError(e)
139+
raise CompilerError(e, command=argument_list,
140+
error_output=text_type(e))
138141
finally:
139142
# Decide what to do with captured stdout.
140143
if stdout:

pipeline/conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
'PIPELINE_ROOT': _settings.STATIC_ROOT,
2424
'PIPELINE_URL': _settings.STATIC_URL,
2525

26+
'SHOW_ERRORS_INLINE': _settings.DEBUG,
27+
2628
'CSS_COMPRESSOR': 'pipeline.compressors.yuglify.YuglifyCompressor',
2729
'JS_COMPRESSOR': 'pipeline.compressors.yuglify.YuglifyCompressor',
2830
'COMPILERS': [],

pipeline/exceptions.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ class PackageNotFound(PipelineException):
1010

1111

1212
class CompilerError(PipelineException):
13-
pass
13+
def __init__(self, msg, command=None, error_output=None):
14+
super(CompilerError, self).__init__(msg)
15+
16+
self.command = command
17+
self.error_output = error_output.strip()
1418

1519

1620
class CompressorError(PipelineException):

pipeline/jinja2/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def package_css(self, package_name, *args, **kwargs):
3434
package = self.package_for(package_name, 'css')
3535
except PackageNotFound:
3636
return '' # fail silently, do not return anything if an invalid group is specified
37-
return self.render_compressed(package, 'css')
37+
return self.render_compressed(package, package_name, 'css')
3838

3939
def render_css(self, package, path):
4040
template_name = package.template_name or "pipeline/css.jinja"
@@ -55,7 +55,7 @@ def package_js(self, package_name, *args, **kwargs):
5555
package = self.package_for(package_name, 'js')
5656
except PackageNotFound:
5757
return '' # fail silently, do not return anything if an invalid group is specified
58-
return self.render_compressed(package, 'js')
58+
return self.render_compressed(package, package_name, 'js')
5959

6060
def render_js(self, package, path):
6161
template_name = package.template_name or "pipeline/js.jinja"
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<div id="django-pipeline-error-{{package_name}}" class="django-pipeline-error"
2+
style="display: none; border: 2px #DD0000 solid; margin: 1em; padding: 1em; background: white;">
3+
<h1>Error compiling {{package_type}} package "{{package_name}}"</h1>
4+
<p><strong>Command:</strong></p>
5+
<pre style="white-space: pre-wrap;">{{command}}</pre>
6+
<p><strong>Errors:</strong></p>
7+
<pre style="white-space: pre-wrap;">{{errors}}</pre>
8+
</div>
9+
10+
<script>
11+
document.addEventListener('readystatechange', function() {
12+
var el,
13+
container;
14+
15+
if (document.readyState !== 'interactive') {
16+
return;
17+
}
18+
19+
el = document.getElementById('django-pipeline-error-{{package_name}}');
20+
container = document.getElementById('django-pipeline-errors');
21+
22+
if (!container) {
23+
container = document.createElement('div');
24+
container.id = 'django-pipeline-errors';
25+
document.body.insertBefore(container, document.body.firstChild);
26+
}
27+
28+
container.appendChild(el);
29+
el.style.display = 'block';
30+
});
31+
</script>

pipeline/templatetags/pipeline.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
from __future__ import unicode_literals
22

33
import logging
4+
import subprocess
45

56
from django.contrib.staticfiles.storage import staticfiles_storage
67

78
from django import template
8-
from django.template.base import VariableDoesNotExist
9+
from django.template.base import Context, VariableDoesNotExist
910
from django.template.loader import render_to_string
1011
from django.utils.safestring import mark_safe
1112

1213
from ..collector import default_collector
1314
from ..conf import settings
15+
from ..exceptions import CompilerError
1416
from ..packager import Packager, PackageNotFound
1517
from ..utils import guess_type
1618

@@ -51,7 +53,7 @@ def render(self, context):
5153
except VariableDoesNotExist:
5254
pass
5355

54-
def render_compressed(self, package, package_type):
56+
def render_compressed(self, package, package_name, package_type):
5557
if settings.PIPELINE_ENABLED:
5658
method = getattr(self, "render_{0}".format(package_type))
5759
return method(package, package.output_filename)
@@ -61,10 +63,29 @@ def render_compressed(self, package, package_type):
6163

6264
packager = Packager()
6365
method = getattr(self, "render_individual_{0}".format(package_type))
64-
paths = packager.compile(package.paths)
66+
67+
try:
68+
paths = packager.compile(package.paths)
69+
except CompilerError as e:
70+
if settings.SHOW_ERRORS_INLINE:
71+
method = getattr(self, 'render_error_{0}'.format(
72+
package_type))
73+
74+
return method(package_name, e)
75+
else:
76+
raise
77+
6578
templates = packager.pack_templates(package)
6679
return method(package, paths, templates=templates)
6780

81+
def render_error(self, package_type, package_name, e):
82+
return render_to_string('pipeline/compile_error.html', Context({
83+
'package_type': package_type,
84+
'package_name': package_name,
85+
'command': subprocess.list2cmdline(e.command),
86+
'errors': e.error_output,
87+
}))
88+
6889

6990
class StylesheetNode(PipelineMixin, template.Node):
7091
def __init__(self, name):
@@ -79,7 +100,7 @@ def render(self, context):
79100
except PackageNotFound:
80101
logger.warn("Package %r is unknown. Check PIPELINE_CSS in your settings.", package_name)
81102
return '' # fail silently, do not return anything if an invalid group is specified
82-
return self.render_compressed(package, 'css')
103+
return self.render_compressed(package, package_name, 'css')
83104

84105
def render_css(self, package, path):
85106
template_name = package.template_name or "pipeline/css.html"
@@ -94,6 +115,10 @@ def render_individual_css(self, package, paths, **kwargs):
94115
tags = [self.render_css(package, path) for path in paths]
95116
return '\n'.join(tags)
96117

118+
def render_error_css(self, package_name, e):
119+
return super(StylesheetNode, self).render_error(
120+
'CSS', package_name, e)
121+
97122

98123
class JavascriptNode(PipelineMixin, template.Node):
99124
def __init__(self, name):
@@ -108,7 +133,7 @@ def render(self, context):
108133
except PackageNotFound:
109134
logger.warn("Package %r is unknown. Check PIPELINE_JS in your settings.", package_name)
110135
return '' # fail silently, do not return anything if an invalid group is specified
111-
return self.render_compressed(package, 'js')
136+
return self.render_compressed(package, package_name, 'js')
112137

113138
def render_js(self, package, path):
114139
template_name = package.template_name or "pipeline/js.html"
@@ -132,6 +157,10 @@ def render_individual_js(self, package, paths, templates=None):
132157
tags.append(self.render_inline(package, templates))
133158
return '\n'.join(tags)
134159

160+
def render_error_js(self, package_name, e):
161+
return super(JavascriptNode, self).render_error(
162+
'JavaScript', package_name, e)
163+
135164

136165
@register.tag
137166
def stylesheet(parser, token):

tests/tests/test_compiler.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,16 @@ def setUp(self):
156156
self.compiler = Compiler()
157157

158158
def test_compile(self):
159-
self.assertRaises(CompilerError, self.compiler.compile, [_('pipeline/js/dummy.coffee')])
159+
with self.assertRaises(CompilerError) as cm:
160+
self.compiler.compile([_('pipeline/js/dummy.coffee')])
161+
162+
e = cm.exception
163+
self.assertEqual(
164+
e.command,
165+
['this-exists-nowhere-as-a-command-and-should-fail',
166+
'pipeline/js/dummy.coffee',
167+
'pipeline/js/dummy.junk'])
168+
self.assertEqual(e.error_output, '')
160169

161170
def tearDown(self):
162171
default_collector.clear()
@@ -170,7 +179,12 @@ def setUp(self):
170179
self.compiler = Compiler()
171180

172181
def test_compile(self):
173-
self.assertRaises(CompilerError, self.compiler.compile, [_('pipeline/js/dummy.coffee')])
182+
with self.assertRaises(CompilerError) as cm:
183+
self.compiler.compile([_('pipeline/js/dummy.coffee')])
184+
185+
e = cm.exception
186+
self.assertEqual(e.command, ['/usr/bin/env', 'false'])
187+
self.assertEqual(e.error_output, '')
174188

175189
def tearDown(self):
176190
default_collector.clear()

0 commit comments

Comments
 (0)