diff --git a/frappe/commands/__init__.py b/frappe/commands/__init__.py index 0080ac22f0..9d9f61cdd4 100644 --- a/frappe/commands/__init__.py +++ b/frappe/commands/__init__.py @@ -27,13 +27,12 @@ def pass_context(f): try: ret = f(frappe._dict(ctx.obj), *args, **kwargs) - except frappe.exceptions.SiteNotSpecifiedError as e: - click.secho(str(e), fg="yellow") - sys.exit(1) - except frappe.exceptions.IncorrectSitePath: - site = ctx.obj.get("sites", "")[0] - click.secho(f"Site {site} does not exist!", fg="yellow") - sys.exit(1) + except ( + frappe.exceptions.SiteNotSpecifiedError, + frappe.exceptions.IncorrectSitePath, + frappe.exceptions.CommandFailedError, + ) as e: + raise click.UsageError(e, ctx) from e if profile: pr.disable() diff --git a/frappe/tests/utils.py b/frappe/tests/utils.py index 84189c8046..1786b5d23a 100644 --- a/frappe/tests/utils.py +++ b/frappe/tests/utils.py @@ -68,29 +68,20 @@ def debug_on(*exceptions): return f(*args, **kwargs) except exceptions as e: exc_type, exc_value, exc_traceback = sys.exc_info() + # Pretty print the exception + print("\n\033[91m" + "=" * 60 + "\033[0m") # Red line + print("\033[93m" + str(exc_type.__name__) + ": " + str(exc_value) + "\033[0m") + print("\033[91m" + "=" * 60 + "\033[0m") # Red line - traceback.print_exception(exc_type, exc_value, exc_traceback) + # Print the formatted traceback + traceback_lines = traceback.format_exception(exc_type, exc_value, exc_traceback) + for line in traceback_lines: + print("\033[96m" + line.rstrip() + "\033[0m") # Cyan color - # Find the most relevant traceback frame - apps_path = frappe.get_app_path("frappe", "..", "..") - - tb = exc_traceback.tb_next - target_frame = None - - while tb: - if tb.tb_frame.f_code.co_filename.startswith(apps_path): - target_frame = tb - break - tb = tb.tb_next - - if target_frame: - print(f"Starting debugger in: {target_frame.tb_frame.f_code.co_filename}:{tb.tb_lineno}") - p = pdb.Pdb() - p.reset() - p.interaction(target_frame.tb_frame, None) - else: - print("Could not find a suitable traceback frame, using the last frame.") - pdb.post_mortem(exc_traceback) + print("\033[91m" + "=" * 60 + "\033[0m") # Red line + print("\033[92mEntering post-mortem debugging\033[0m") + print("\033[91m" + "=" * 60 + "\033[0m") # Red line + pdb.post_mortem() raise e diff --git a/frappe/utils/bench_helper.py b/frappe/utils/bench_helper.py index 6de374a2a2..bbcdb00e5d 100644 --- a/frappe/utils/bench_helper.py +++ b/frappe/utils/bench_helper.py @@ -1,6 +1,8 @@ import importlib import json +import linecache import os +import sys import traceback import warnings from textwrap import dedent @@ -13,25 +15,85 @@ import frappe.utils click.disable_unicode_literals_warning = True -class FrappeCommandGroup(click.Group): - def get_command(self, ctx, cmd_name): - rv = super().get_command(ctx, cmd_name) - if rv is not None: - return rv +def FrappeClickWrapper(cls, handler): + class Cls(cls): + # only implemented on groups + def get_command(self, ctx, cmd_name): + rv = super().get_command(ctx, cmd_name) + if rv is not None: + return rv - all_commands = self.list_commands(ctx) - from difflib import get_close_matches + all_commands = self.list_commands(ctx) + from difflib import get_close_matches - possibilities = get_close_matches(cmd_name, all_commands) - raise click.NoSuchOption( - cmd_name, possibilities=possibilities, message=f"No such command: {cmd_name}." - ) + possibilities = get_close_matches(cmd_name, all_commands) + raise click.NoSuchOption( + cmd_name, possibilities=possibilities, message=f"No such command: {cmd_name}." + ) + + def make_context(self, info_name, args, parent=None, **extra): + try: + return super().make_context(info_name, args, parent=parent, **extra) + except click.ClickException as e: + raise e + except Exception as exc: + # call the handler + handler(self, info_name, exc) + sys.exit(1) + + def invoke(self, ctx): + try: + return super().invoke(ctx) + except click.ClickException as e: + raise e + except Exception as exc: + # call the handler + handler(self, ctx.info_name, exc) + sys.exit(1) + + return Cls + + +def handle_exception(cmd, info_name, exc): + tb = sys.exc_info()[2] + while tb.tb_next: + tb = tb.tb_next + frame = tb.tb_frame + filename = frame.f_code.co_filename + lineno = frame.f_lineno + + ( + click.secho("\n:: ", nl=False), + click.secho(f"{exc}", fg="red", bold=True, nl=False), + click.secho(" ::"), + ) + click.secho("\nContext:", fg="yellow", bold=True) + click.secho(f" File '{filename}', line {lineno}\n") + context_lines = 5 + start = max(1, lineno - context_lines) + end = lineno + context_lines + 1 + + for i in range(start, end): + line = linecache.getline(filename, i).rstrip() + if i == lineno: + click.secho(f"{i:4d}> {line}", fg="red") + else: + click.echo(f"{i:4d}: {line}") + + show_exception = (not sys.stdout.isatty()) or click.confirm( + "\nDo you want to see the full traceback?", default=False + ) + if show_exception: + click.secho("\nFull traceback:", fg="red") + click.echo(traceback.format_exc()) + + click.echo(exc) def main(): commands = get_app_groups() commands.update({"get-frappe-commands": get_frappe_commands, "get-frappe-help": get_frappe_help}) - FrappeCommandGroup(commands=commands)(prog_name="bench") + FrappeClickWrapper(click.Group, handle_exception)(commands=commands)(prog_name="bench") def get_app_groups() -> dict[str, click.Group]: @@ -41,12 +103,18 @@ def get_app_groups() -> dict[str, click.Group]: for app in get_apps(): if app_commands := get_app_commands(app): commands |= app_commands - return dict(frappe=click.group(name="frappe", commands=commands, cls=FrappeCommandGroup)(app_group)) + return dict( + frappe=click.group( + name="frappe", commands=commands, cls=FrappeClickWrapper(click.Group, handle_exception) + )(app_group) + ) def get_app_group(app: str) -> click.Group: if app_commands := get_app_commands(app): - return click.group(name=app, commands=app_commands)(app_group) + return click.group( + name=app, commands=app_commands, cls=FrappeClickWrapper(click.Group, handle_exception) + )(app_group) @click.option("--site") diff --git a/pyproject.toml b/pyproject.toml index ef4ce7f7d9..6125a9f412 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,6 +109,7 @@ watchdog = "~=3.0.0" hypothesis = "~=6.77.0" responses = "==0.23.1" freezegun = "~=1.2.2" +pdbpp = "~=0.10.3" [tool.ruff] line-length = 110