1# -*- encoding: utf-8 -*- 2# Copyright 2020 the authors. 3# This file is part of Hy, which is free software licensed under the Expat 4# license. See the LICENSE. 5import os 6import re 7import sys 8import traceback 9import pkgutil 10 11from functools import reduce 12from colorama import Fore 13from contextlib import contextmanager 14from hy import _initialize_env_var 15 16_hy_filter_internal_errors = _initialize_env_var('HY_FILTER_INTERNAL_ERRORS', 17 True) 18COLORED = _initialize_env_var('HY_COLORED_ERRORS', False) 19 20 21class HyError(Exception): 22 pass 23 24 25class HyInternalError(HyError): 26 """Unexpected errors occurring during compilation or parsing of Hy code. 27 28 Errors sub-classing this are not intended to be user-facing, and will, 29 hopefully, never be seen by users! 30 """ 31 32 33class HyLanguageError(HyError): 34 """Errors caused by invalid use of the Hy language. 35 36 This, and any errors inheriting from this, are user-facing. 37 """ 38 39 def __init__(self, message, expression=None, filename=None, source=None, 40 lineno=1, colno=1): 41 """ 42 Parameters 43 ---------- 44 message: str 45 The message to display for this error. 46 expression: HyObject, optional 47 The Hy expression generating this error. 48 filename: str, optional 49 The filename for the source code generating this error. 50 Expression-provided information will take precedence of this value. 51 source: str, optional 52 The actual source code generating this error. Expression-provided 53 information will take precedence of this value. 54 lineno: int, optional 55 The line number of the error. Expression-provided information will 56 take precedence of this value. 57 colno: int, optional 58 The column number of the error. Expression-provided information 59 will take precedence of this value. 60 """ 61 self.msg = message 62 self.compute_lineinfo(expression, filename, source, lineno, colno) 63 64 if isinstance(self, SyntaxError): 65 syntax_error_args = (self.filename, self.lineno, self.offset, 66 self.text) 67 super(HyLanguageError, self).__init__(message, syntax_error_args) 68 else: 69 super(HyLanguageError, self).__init__(message) 70 71 def compute_lineinfo(self, expression, filename, source, lineno, colno): 72 73 # NOTE: We use `SyntaxError`'s field names (i.e. `text`, `offset`, 74 # `msg`) for compatibility and print-outs. 75 self.text = getattr(expression, 'source', source) 76 self.filename = getattr(expression, 'filename', filename) 77 78 if self.text: 79 lines = self.text.splitlines() 80 81 self.lineno = getattr(expression, 'start_line', lineno) 82 self.offset = getattr(expression, 'start_column', colno) 83 end_column = getattr(expression, 'end_column', 84 len(lines[self.lineno-1])) 85 end_line = getattr(expression, 'end_line', self.lineno) 86 87 # Trim the source down to the essentials. 88 self.text = '\n'.join(lines[self.lineno-1:end_line]) 89 90 if end_column: 91 if self.lineno == end_line: 92 self.arrow_offset = end_column 93 else: 94 self.arrow_offset = len(self.text[0]) 95 96 self.arrow_offset -= self.offset 97 else: 98 self.arrow_offset = None 99 else: 100 # We could attempt to extract the source given a filename, but we 101 # don't. 102 self.lineno = lineno 103 self.offset = colno 104 self.arrow_offset = None 105 106 def __str__(self): 107 """Provide an exception message that includes SyntaxError-like source 108 line information when available. 109 """ 110 # Syntax errors are special and annotate the traceback (instead of what 111 # we would do in the message that follows the traceback). 112 if isinstance(self, SyntaxError): 113 return super(HyLanguageError, self).__str__() 114 # When there isn't extra source information, use the normal message. 115 elif not self.text: 116 return super(HyLanguageError, self).__str__() 117 118 # Re-purpose Python's builtin syntax error formatting. 119 output = traceback.format_exception_only( 120 SyntaxError, 121 SyntaxError(self.msg, (self.filename, self.lineno, self.offset, 122 self.text))) 123 124 arrow_idx, _ = next(((i, x) for i, x in enumerate(output) 125 if x.strip() == '^'), 126 (None, None)) 127 if arrow_idx: 128 msg_idx = arrow_idx + 1 129 else: 130 msg_idx, _ = next((i, x) for i, x in enumerate(output) 131 if x.startswith('SyntaxError: ')) 132 133 # Get rid of erroneous error-type label. 134 output[msg_idx] = re.sub('^SyntaxError: ', '', output[msg_idx]) 135 136 # Extend the text arrow, when given enough source info. 137 if arrow_idx and self.arrow_offset: 138 output[arrow_idx] = '{}{}^\n'.format(output[arrow_idx].rstrip('\n'), 139 '-' * (self.arrow_offset - 1)) 140 141 if COLORED: 142 output[msg_idx:] = [Fore.YELLOW + o + Fore.RESET for o in output[msg_idx:]] 143 if arrow_idx: 144 output[arrow_idx] = Fore.GREEN + output[arrow_idx] + Fore.RESET 145 for idx, line in enumerate(output[::msg_idx]): 146 if line.strip().startswith( 147 'File "{}", line'.format(self.filename)): 148 output[idx] = Fore.RED + line + Fore.RESET 149 150 # This resulting string will come after a "<class-name>:" prompt, so 151 # put it down a line. 152 output.insert(0, '\n') 153 154 # Avoid "...expected str instance, ColoredString found" 155 return reduce(lambda x, y: x + y, output) 156 157 158class HyCompileError(HyInternalError): 159 """Unexpected errors occurring within the compiler.""" 160 161 162class HyTypeError(HyLanguageError, TypeError): 163 """TypeError occurring during the normal use of Hy.""" 164 165 166class HyNameError(HyLanguageError, NameError): 167 """NameError occurring during the normal use of Hy.""" 168 169 170class HyRequireError(HyLanguageError): 171 """Errors arising during the use of `require` 172 173 This, and any errors inheriting from this, are user-facing. 174 """ 175 176 177class HyMacroExpansionError(HyLanguageError): 178 """Errors caused by invalid use of Hy macros. 179 180 This, and any errors inheriting from this, are user-facing. 181 """ 182 183 184class HyEvalError(HyLanguageError): 185 """Errors occurring during code evaluation at compile-time. 186 187 These errors distinguish unexpected errors within the compilation process 188 (i.e. `HyInternalError`s) from unrelated errors in user code evaluated by 189 the compiler (e.g. in `eval-and-compile`). 190 191 This, and any errors inheriting from this, are user-facing. 192 """ 193 194 195class HyIOError(HyInternalError, IOError): 196 """ Subclass used to distinguish between IOErrors raised by Hy itself as 197 opposed to Hy programs. 198 """ 199 200 201class HySyntaxError(HyLanguageError, SyntaxError): 202 """Error during the Lexing of a Hython expression.""" 203 204 205class HyWrapperError(HyError, TypeError): 206 """Errors caused by language model object wrapping. 207 208 These can be caused by improper user-level use of a macro, so they're 209 not really "internal". If they arise due to anything else, they're an 210 internal/compiler problem, though. 211 """ 212 213 214def _module_filter_name(module_name): 215 try: 216 compiler_loader = pkgutil.get_loader(module_name) 217 if not compiler_loader: 218 return None 219 220 filename = compiler_loader.get_filename(module_name) 221 if not filename: 222 return None 223 224 if compiler_loader.is_package(module_name): 225 # Use the package directory (e.g. instead of `.../__init__.py`) so 226 # that we can filter all modules in a package. 227 return os.path.dirname(filename) 228 else: 229 # Normalize filename endings, because tracebacks will use `pyc` when 230 # the loader says `py`. 231 return filename.replace('.pyc', '.py') 232 except Exception: 233 return None 234 235 236_tb_hidden_modules = {m for m in map(_module_filter_name, 237 ['hy.compiler', 'hy.lex', 238 'hy.cmdline', 'hy.lex.parser', 239 'hy.importer', 'hy._compat', 240 'hy.macros', 'hy.models', 241 'rply']) 242 if m is not None} 243 244 245def hy_exc_filter(exc_type, exc_value, exc_traceback): 246 """Produce exceptions print-outs with all frames originating from the 247 modules in `_tb_hidden_modules` filtered out. 248 249 The frames are actually filtered by each module's filename and only when a 250 subclass of `HyLanguageError` is emitted. 251 252 This does not remove the frames from the actual tracebacks, so debugging 253 will show everything. 254 """ 255 # frame = (filename, line number, function name*, text) 256 new_tb = [] 257 for frame in traceback.extract_tb(exc_traceback): 258 if not (frame[0].replace('.pyc', '.py') in _tb_hidden_modules or 259 os.path.dirname(frame[0]) in _tb_hidden_modules): 260 new_tb += [frame] 261 262 lines = traceback.format_list(new_tb) 263 264 lines.insert(0, "Traceback (most recent call last):\n") 265 266 lines.extend(traceback.format_exception_only(exc_type, exc_value)) 267 output = ''.join(lines) 268 269 return output 270 271 272def hy_exc_handler(exc_type, exc_value, exc_traceback): 273 """A `sys.excepthook` handler that uses `hy_exc_filter` to 274 remove internal Hy frames from a traceback print-out. 275 """ 276 if os.environ.get('HY_DEBUG', False): 277 return sys.__excepthook__(exc_type, exc_value, exc_traceback) 278 279 try: 280 output = hy_exc_filter(exc_type, exc_value, exc_traceback) 281 sys.stderr.write(output) 282 sys.stderr.flush() 283 except Exception: 284 sys.__excepthook__(exc_type, exc_value, exc_traceback) 285 286 287@contextmanager 288def filtered_hy_exceptions(): 289 """Temporarily apply a `sys.excepthook` that filters Hy internal frames 290 from tracebacks. 291 292 Filtering can be controlled by the variable 293 `hy.errors._hy_filter_internal_errors` and environment variable 294 `HY_FILTER_INTERNAL_ERRORS`. 295 """ 296 global _hy_filter_internal_errors 297 if _hy_filter_internal_errors: 298 current_hook = sys.excepthook 299 sys.excepthook = hy_exc_handler 300 yield 301 sys.excepthook = current_hook 302 else: 303 yield 304