1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
|
import sys
import traceback
BROWSER_ERROR_TEMPLATE = """\
body:before {{
content: {0};
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
font-size: 14px;
margin: 1em;
padding: 1em;
border: 3px double red;
white-space: pre;
font-family: monospace;
background: #fcebeb;
color: black;
}}
"""
def add_error_marker(text, position, start_line=1):
"""Add a caret marking a given position in a string of input.
Returns (new_text, caret_line).
"""
indent = " "
lines = []
caret_line = start_line
for line in text.split("\n"):
lines.append(indent + line)
if 0 <= position <= len(line):
lines.append(indent + (" " * position) + "^")
caret_line = start_line
position -= len(line)
position -= 1 # for the newline
start_line += 1
return "\n".join(lines), caret_line
class SassError(Exception):
"""Error class that wraps another exception and attempts to bolt on some
useful context.
"""
def __init__(self, exc, rule=None, expression=None, expression_pos=None):
self.exc = exc
self.rule_stack = []
if rule:
self.rule_stack.append(rule)
self.expression = expression
self.expression_pos = expression_pos
_, _, self.original_traceback = sys.exc_info()
def add_rule(self, rule):
"""Add a new rule to the "stack" of rules -- this is used to track,
e.g., how a file was ultimately imported.
"""
self.rule_stack.append(rule)
def format_prefix(self):
"""Return the general name of the error and the contents of the rule or
property that caused the failure. This is the initial part of the
error message and should be error-specific.
"""
# TODO this contains NULs and line numbers; could be much prettier
if self.rule_stack:
return (
"Error parsing block:\n" +
" " + self.rule_stack[0].unparsed_contents + "\n"
)
else:
return "Unknown error\n"
def format_sass_stack(self):
"""Return a "traceback" of Sass imports."""
if not self.rule_stack:
return ""
ret = ["From ", self.rule_stack[0].file_and_line, "\n"]
last_file = self.rule_stack[0].source_file
# TODO this could go away if rules knew their import chains...
for rule in self.rule_stack[1:]:
if rule.source_file is not last_file:
ret.extend(("...imported from ", rule.file_and_line, "\n"))
last_file = rule.source_file
return "".join(ret)
def format_python_stack(self):
"""Return a traceback of Python frames, from where the error occurred
to where it was first caught and wrapped.
"""
ret = ["Traceback:\n"]
ret.extend(traceback.format_tb(self.original_traceback))
return "".join(ret)
def format_original_error(self):
"""Return the typical "TypeError: blah blah" for the original wrapped
error.
"""
# TODO eventually we'll have sass-specific errors that will want nicer
# "names" in browser display and stderr
return "".join((type(self.exc).__name__, ": ", str(self.exc), "\n"))
def __str__(self):
try:
prefix = self.format_prefix()
sass_stack = self.format_sass_stack()
python_stack = self.format_python_stack()
original_error = self.format_original_error()
# TODO not very well-specified whether these parts should already
# end in newlines, or how many
return prefix + "\n" + sass_stack + python_stack + original_error
except Exception:
# "unprintable error" is not helpful
return str(self.exc)
def to_css(self):
"""Return a stylesheet that will show the wrapped error at the top of
the browser window.
"""
# TODO should this include the traceback? any security concerns?
prefix = self.format_prefix()
original_error = self.format_original_error()
sass_stack = self.format_sass_stack()
message = prefix + "\n" + sass_stack + original_error
# Super simple escaping: only quotes and newlines are illegal in css
# strings
message = message.replace('\\', '\\\\')
message = message.replace('"', '\\"')
# use the maximum six digits here so it doesn't eat any following
# characters that happen to look like hex
message = message.replace('\n', '\\00000A')
return BROWSER_ERROR_TEMPLATE.format('"' + message + '"')
class SassParseError(SassError):
"""Error raised when parsing a Sass expression fails."""
def format_prefix(self):
decorated_expr, line = add_error_marker(self.expression, self.expression_pos or -1)
return """Error parsing expression:\n{0}\n""".format(decorated_expr)
class SassEvaluationError(SassError):
"""Error raised when evaluating a parsed expression fails."""
def format_prefix(self):
# TODO would be nice for the AST to have position information
# TODO might be nice to print the AST and indicate where the failure
# was?
decorated_expr, line = add_error_marker(self.expression, self.expression_pos or -1)
return """Error evaluating expression:\n{0}\n""".format(decorated_expr)
|