5
BROWSER_ERROR_TEMPLATE = """\
18
border: 3px double red;
21
font-family: monospace;
27
def add_error_marker(text, position, start_line=1):
28
"""Add a caret marking a given position in a string of input.
30
Returns (new_text, caret_line).
34
caret_line = start_line
35
for line in text.split("\n"):
36
lines.append(indent + line)
38
if 0 <= position <= len(line):
39
lines.append(indent + (" " * position) + "^")
40
caret_line = start_line
43
position -= 1 # for the newline
46
return "\n".join(lines), caret_line
49
class SassError(Exception):
50
"""Error class that wraps another exception and attempts to bolt on some
53
def __init__(self, exc, rule=None, expression=None, expression_pos=None):
58
self.rule_stack.append(rule)
60
self.expression = expression
61
self.expression_pos = expression_pos
63
_, _, self.original_traceback = sys.exc_info()
65
def add_rule(self, rule):
66
"""Add a new rule to the "stack" of rules -- this is used to track,
67
e.g., how a file was ultimately imported.
69
self.rule_stack.append(rule)
71
def format_prefix(self):
72
"""Return the general name of the error and the contents of the rule or
73
property that caused the failure. This is the initial part of the
74
error message and should be error-specific.
76
# TODO this contains NULs and line numbers; could be much prettier
79
"Error parsing block:\n" +
80
" " + self.rule_stack[0].unparsed_contents + "\n"
83
return "Unknown error\n"
85
def format_sass_stack(self):
86
"""Return a "traceback" of Sass imports."""
87
if not self.rule_stack:
90
ret = ["From ", self.rule_stack[0].file_and_line, "\n"]
91
last_file = self.rule_stack[0].source_file
93
# TODO this could go away if rules knew their import chains...
94
for rule in self.rule_stack[1:]:
95
if rule.source_file is not last_file:
96
ret.extend(("...imported from ", rule.file_and_line, "\n"))
97
last_file = rule.source_file
101
def format_python_stack(self):
102
"""Return a traceback of Python frames, from where the error occurred
103
to where it was first caught and wrapped.
105
ret = ["Traceback:\n"]
106
ret.extend(traceback.format_tb(self.original_traceback))
109
def format_original_error(self):
110
"""Return the typical "TypeError: blah blah" for the original wrapped
113
# TODO eventually we'll have sass-specific errors that will want nicer
114
# "names" in browser display and stderr
115
return "".join((type(self.exc).__name__, ": ", str(self.exc), "\n"))
119
prefix = self.format_prefix()
120
sass_stack = self.format_sass_stack()
121
python_stack = self.format_python_stack()
122
original_error = self.format_original_error()
124
# TODO not very well-specified whether these parts should already
125
# end in newlines, or how many
126
return prefix + "\n" + sass_stack + python_stack + original_error
128
# "unprintable error" is not helpful
132
"""Return a stylesheet that will show the wrapped error at the top of
135
# TODO should this include the traceback? any security concerns?
136
prefix = self.format_prefix()
137
original_error = self.format_original_error()
138
sass_stack = self.format_sass_stack()
140
message = prefix + "\n" + sass_stack + original_error
142
# Super simple escaping: only quotes and newlines are illegal in css
144
message = message.replace('\\', '\\\\')
145
message = message.replace('"', '\\"')
146
# use the maximum six digits here so it doesn't eat any following
147
# characters that happen to look like hex
148
message = message.replace('\n', '\\00000A')
150
return BROWSER_ERROR_TEMPLATE.format('"' + message + '"')
153
class SassParseError(SassError):
154
"""Error raised when parsing a Sass expression fails."""
156
def format_prefix(self):
157
decorated_expr, line = add_error_marker(self.expression, self.expression_pos or -1)
158
return """Error parsing expression:\n{0}\n""".format(decorated_expr)
161
class SassEvaluationError(SassError):
162
"""Error raised when evaluating a parsed expression fails."""
164
def format_prefix(self):
165
# TODO would be nice for the AST to have position information
166
# TODO might be nice to print the AST and indicate where the failure
168
decorated_expr, line = add_error_marker(self.expression, self.expression_pos or -1)
169
return """Error evaluating expression:\n{0}\n""".format(decorated_expr)