Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Added `==` as an alias to `=` (#859)
* Added custom exception formatting for `basilisp.lang.compiler.exception.CompilerException` and `basilisp.lang.reader.SyntaxError` to show more useful details to users on errors (#870)
* Added `merge-with` core function (#860)
* Added `basilisp.core/*except-hook*` and `basilisp.core/*repl-except-hook*` to allow users the ability to customize behaviors for printing unhandled exceptions (#873)

### Changed
* Cause exceptions arising from compilation issues during macroexpansion will no longer be nested for each level of macroexpansion (#852)
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ branch = true
omit = [
"*/__version__.py",
"*/basilisp/contrib/sphinx/*",
"*/basilisp/sitecustomize.py",
]

[tool.coverage.paths]
Expand Down
10 changes: 5 additions & 5 deletions src/basilisp/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from basilisp.lang import runtime as runtime
from basilisp.lang import symbol as sym
from basilisp.lang import vector as vec
from basilisp.lang.exception import print_exception
from basilisp.prompt import get_prompter

CLI_INPUT_FILE_PATH = "<CLI Input>"
Expand Down Expand Up @@ -396,8 +395,9 @@ def repl(
warn_on_var_indirection=args.warn_on_var_indirection,
)
basilisp.init(opts)
print_exception = runtime.get_basilisp_repl_exception_hook()
ctx = compiler.CompilerContext(filename=REPL_INPUT_FILE_PATH, opts=opts)
prompter = get_prompter()
prompter = get_prompter(print_exception=print_exception)
eof = object()

# Bind user-settable dynamic Vars to their existing value to allow users to
Expand Down Expand Up @@ -449,15 +449,15 @@ def repl(
prompter.print(runtime.lrepr(result))
repl_module.mark_repl_result(result)
except reader.SyntaxError as e:
print_exception(e, reader.SyntaxError, e.__traceback__)
print_exception(reader.SyntaxError, e, e.__traceback__)
repl_module.mark_exception(e)
continue
except compiler.CompilerException as e:
print_exception(e, compiler.CompilerException, e.__traceback__)
print_exception(compiler.CompilerException, e, e.__traceback__)
repl_module.mark_exception(e)
continue
except Exception as e: # pylint: disable=broad-exception-caught
print_exception(e, Exception, e.__traceback__)
print_exception(type(e), e, e.__traceback__)
repl_module.mark_exception(e)
continue

Expand Down
3 changes: 2 additions & 1 deletion src/basilisp/contrib/nrepl_server.lpy
Original file line number Diff line number Diff line change
Expand Up @@ -355,13 +355,14 @@
log-response-mw
response-for-mw))

(defn- on-connect [tcp-req-handler opts]
(defn- on-connect
"Serve a new nREPL connection as found in ``tcp-req-handler`` according to ``opts``.

``opts`` is a map of options with the following optional keys.

:recv-buffer-size The buffer size to using for incoming nREPL
messages."
[tcp-req-handler opts]
(let [{:keys [recv-buffer-size]
:or {recv-buffer-size 1024}} opts
socket (.-request tcp-req-handler)
Expand Down
14 changes: 10 additions & 4 deletions src/basilisp/lang/exception.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import functools
import sys
import traceback
import types
from types import TracebackType
from typing import List, Optional, Type
from typing import Callable, List, Optional, Type

import attr

Expand All @@ -24,6 +25,11 @@ def __str__(self):
return f"{self.message} {lrepr(self.data)}"


ExceptionPrinter = Callable[
[Type[BaseException], BaseException, Optional[types.TracebackType]], None
]


@functools.singledispatch
def format_exception(
e: Optional[BaseException],
Expand All @@ -36,7 +42,7 @@ def format_exception(
For the majority of Python exceptions, this will just be the result from calling
`traceback.format_exception`. For Basilisp specific compilation errors, a custom
output will be returned."""
if isinstance(e, BaseException):
if isinstance(e, BaseException): # pragma: no cover
if tp is None:
tp = type(e)
if tb is None:
Expand All @@ -45,9 +51,9 @@ def format_exception(


def print_exception(
tp: Optional[Type[BaseException]],
e: Optional[BaseException],
tp: Optional[Type[BaseException]] = None,
tb: Optional[TracebackType] = None,
tb: Optional[TracebackType],
) -> None:
"""Print the given exception `e` using Basilisp's own exception formatting.

Expand Down
130 changes: 129 additions & 1 deletion src/basilisp/lang/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import platform
import re
import sys
import textwrap
import threading
import types
from collections.abc import Sequence, Sized
Expand Down Expand Up @@ -46,6 +47,7 @@
from basilisp.lang import symbol as sym
from basilisp.lang import vector as vec
from basilisp.lang.atom import Atom
from basilisp.lang.exception import ExceptionPrinter, print_exception
from basilisp.lang.interfaces import (
IAssociative,
IBlockingDeref,
Expand Down Expand Up @@ -84,6 +86,8 @@
COMPILER_OPTIONS_VAR_NAME = "*compiler-options*"
COMMAND_LINE_ARGS_VAR_NAME = "*command-line-args*"
DEFAULT_READER_FEATURES_VAR_NAME = "*default-reader-features*"
EXCEPT_HOOK_VAR_NAME = "*except-hook*"
REPL_EXCEPT_HOOK_VAR_NAME = "*repl-except-hook*"
GENERATED_PYTHON_VAR_NAME = "*generated-python*"
PRINT_GENERATED_PY_VAR_NAME = "*print-generated-python*"
MAIN_NS_VAR_NAME = "*main-ns*"
Expand Down Expand Up @@ -2030,6 +2034,60 @@ def print_generated_python() -> bool:
)


###################
# Traceback Hooks #
###################


def _create_except_hook_fn(hook_var: Var) -> ExceptionPrinter:
"""Create an except hook function which is mostly compatible with
`traceback.print_exception` which uses the function from the Var `hook_var`."""

def basilisp_except_hook(
tp: Type[BaseException],
e: BaseException,
tb: Optional[types.TracebackType],
) -> None:
if (hook := hook_var.value) is not None:
try:
hook(tp, e, tb)
except Exception: # pylint: disable=broad-exception-caught
print(
f"Exception occurred in Basilisp except hook {hook_var}",
file=sys.stderr,
)
sys.__excepthook__(tp, e, tb)
else:
# Emit an error to stderr and fall back to the default Python except hook
# if no hook is currently defined in Basilisp.
print(f"Dynamic Var {hook_var} not bound!", file=sys.stderr)
sys.__excepthook__(tp, e, tb)

return basilisp_except_hook


def create_basilisp_except_hook() -> ExceptionPrinter:
"""Create an except hook which is suitable to being installed as `sys.excepthook`
to print any unhandled tracebacks.

This function must be called after bootstrapping `basilisp.core`."""
ns_sym = sym.symbol(EXCEPT_HOOK_VAR_NAME, ns=CORE_NS)
hook_var = Var.find(ns_sym)
assert hook_var is not None
return _create_except_hook_fn(hook_var)


def get_basilisp_repl_exception_hook() -> ExceptionPrinter:
"""Create an exception hook which can be used to print exception tracebacks for
interactive REPL sessions.

This function must be called after bootstrapping `basilisp.core`."""
ns_sym = sym.symbol(REPL_EXCEPT_HOOK_VAR_NAME, ns=CORE_NS)
hook_var = Var.find(ns_sym)
assert hook_var is not None
return _create_except_hook_fn(hook_var)


#########################
# Bootstrap the Runtime #
#########################
Expand Down Expand Up @@ -2127,7 +2185,7 @@ def in_ns(s: sym.Symbol):
),
)

# Dynamic Var containing command line arguments passed via `basilisp run`
# Dynamic Var containing the name of the main namespace (if one is given)
Var.intern(
CORE_NS_SYM,
sym.symbol(MAIN_NS_VAR_NAME),
Expand Down Expand Up @@ -2162,6 +2220,76 @@ def in_ns(s: sym.Symbol):
),
)

# Dynamic Vars containing the current traceback printers
Var.intern(
CORE_NS_SYM,
sym.symbol(EXCEPT_HOOK_VAR_NAME),
print_exception,
dynamic=True,
meta=lmap.map(
{
_DOC_META_KEY: textwrap.indent(
textwrap.dedent(
f"""
A function of 3 arguments which controls the printing of unhandled
exceptions.

The three arguments should be in order: an exception *type*, an
exception *instance*, and an exception *traceback* (e.g. the value
of the ``__traceback__`` attribute). In the majority of cases, only
the second argument is needed but nevertheless ``sys.excepthook``
is called with all 3 in that order.

The default function has special handling for compiler errors
and reader syntax errors. All other exception types are printed
as by `traceback.print_exception <https://docs.python.org/3/library/traceback.html#traceback.print_exception>`_.

After bootstrapping the runtime, Python's `sys.excepthook <https://docs.python.org/3/library/sys.html#sys.excepthook>`_
will use the function bound to this Var for printing any unhandled
exceptions, including those not originating from Basilisp code.

For controlling the display of exceptions occurring during REPL
sessions, see :lpy:var:`{REPL_EXCEPT_HOOK_VAR_NAME}`.
"""
).strip(),
" ",
)
}
),
)
Var.intern(
CORE_NS_SYM,
sym.symbol(REPL_EXCEPT_HOOK_VAR_NAME),
print_exception,
dynamic=True,
meta=lmap.map(
{
_DOC_META_KEY: textwrap.indent(
textwrap.dedent(
f"""
A function of 3 arguments which controls the printing of exceptions
thrown during interactive REPL sessions.

The three arguments should be in order: an exception *type*, an
exception *instance*, and an exception *traceback* (e.g. the value
of the ``__traceback__`` attribute). In the majority of cases, only
the second argument is needed but nevertheless ``sys.excepthook``
is called with all 3 in that order.

The default function has special handling for compiler errors
and reader syntax errors. All other exception types are printed
as by `traceback.print_exception <https://docs.python.org/3/library/traceback.html#traceback.print_exception>`_.

This Var is distinct from :lpy:var:`{EXCEPT_HOOK_VAR_NAME}`, which controls
printing unhandled exceptions occurring outside the REPL context.
"""
).strip(),
" ",
)
}
),
)

# Dynamic Vars examined by the compiler for generating Python code for debugging
Var.intern(
CORE_NS_SYM,
Expand Down
2 changes: 2 additions & 0 deletions src/basilisp/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import importlib
import logging
import site
import sys
from pathlib import Path
from typing import List, Optional

Expand Down Expand Up @@ -29,6 +30,7 @@ def init(opts: Optional[CompilerOpts] = None) -> None:
"""
runtime.init_ns_var()
runtime.bootstrap_core(opts if opts is not None else compiler_opts())
sys.excepthook = runtime.create_basilisp_except_hook()
importer.hook_imports()
importlib.import_module("basilisp.core")

Expand Down
Loading