diff --git a/CHANGELOG.md b/CHANGELOG.md index 4afa8fb..82836dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.12] - 2026-01-02 + +### Added + +### Changed +- If the command is executed in verbose mode (`-v`), the standard Python traceback is used to display errors. + +### Fixed + ## [0.1.11] - 2025-09-10 ### Added diff --git a/pyproject.toml b/pyproject.toml index 61f5557..ef240cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "CliFire" -version = "0.1.11" +version = "0.1.12" description = "Minimal CLI framework to build Python commands quickly and elegantly." authors = [ { name = "Roberto Lizana", email = "rober.lizana@gmail.com" } diff --git a/src/clifire/application.py b/src/clifire/application.py index 284dce9..f6efd09 100644 --- a/src/clifire/application.py +++ b/src/clifire/application.py @@ -184,6 +184,7 @@ def fire(self, command_line: str = None): out.setup( not self.get_option('no_ansi'), show_icons=self.show_messages_with_icons, + verbose=self.get_option('verbose'), ) res = cmd.launch(cmd.command_line) if type(res) is int and res != 0: diff --git a/src/clifire/main.py b/src/clifire/main.py index 1d4c082..fbaa06e 100644 --- a/src/clifire/main.py +++ b/src/clifire/main.py @@ -31,7 +31,7 @@ def load_file(filename): def main(command_line: str = None): - app = application.App(name='CliFire', version='0.1.11') + app = application.App(name='CliFire', version='0.1.12') current_dir = os.getcwd() out.debug(f'Search commands in {current_dir} folder and parents') loaded = False diff --git a/src/clifire/out.py b/src/clifire/out.py index 6fa6dbd..ab11e06 100644 --- a/src/clifire/out.py +++ b/src/clifire/out.py @@ -11,12 +11,7 @@ from rich.prompt import Prompt from rich.table import Table -traceback.install( - show_locals=True, - theme='monokai', - suppress=[], - max_frames=2, -) +_traceback_handler = None CONSOLE = Console() CONSOLE_WIDTH = CONSOLE.width @@ -35,8 +30,10 @@ ICON_WARN = '▲' -def setup(ansi: bool = True, show_icons: bool = None) -> None: - global ICON_SHOW, CONSOLE, CONSOLE_WIDTH +def setup( + ansi: bool = True, show_icons: bool = None, verbose: bool = False +) -> None: + global ICON_SHOW, CONSOLE, CONSOLE_WIDTH, _traceback_handler if show_icons is not None: ICON_SHOW = show_icons if ansi: @@ -51,6 +48,19 @@ def setup(ansi: bool = True, show_icons: bool = None) -> None: CONSOLE.soft_wrap = False CONSOLE.width = 10000 debug('Console output setup with no ANSI') + if verbose: + if sys.excepthook != sys.__excepthook__: + sys.excepthook = sys.__excepthook__ + debug('Using standard Python traceback (verbose mode)') + else: + if _traceback_handler is None or sys.excepthook == sys.__excepthook__: + _traceback_handler = traceback.install( + show_locals=True, + theme='monokai', + suppress=[], + max_frames=2, + ) + debug('Using Rich pretty traceback') class LiveText: diff --git a/tests/test_app.py b/tests/test_app.py index 428d21d..ffc5831 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -123,3 +123,40 @@ def test_path(): ) assert app.path() == os.getcwd() + + +def test_traceback_verbose_mode(capsys, monkeypatch): + class CommandError(command.Command): + _name = 'error' + + def fire(self): + return 1 / 0 # ZeroDivisionError + + app = application.App() + app.add_command(CommandError) + monkeypatch.setattr(sys, 'argv', ['test', 'error', '-v']) + original_excepthook = sys.__excepthook__ + with pytest.raises(ZeroDivisionError): + app.fire('error -v') + assert sys.excepthook == original_excepthook + printed = output(capsys) + assert 'Using standard Python traceback (verbose mode)' in printed + + +def test_traceback_pretty_mode(capsys): + class CommandError(command.Command): + _name = 'error' + + def fire(self): + return 1 / 0 # ZeroDivisionError + + original_excepthook = sys.excepthook + app = application.App() + app.add_command(CommandError) + with pytest.raises(ZeroDivisionError): + app.fire('error') + assert sys.excepthook != sys.__excepthook__ + assert ( + sys.excepthook != original_excepthook + or original_excepthook != sys.__excepthook__ + )