diff --git a/docs/tutorial/printing.md b/docs/tutorial/printing.md index 058998473e..dd8c619303 100644 --- a/docs/tutorial/printing.md +++ b/docs/tutorial/printing.md @@ -120,6 +120,15 @@ In general, **Typer** tends to be the entry point to your program, taking the fi The best results for your command line application would be achieved combining both **Typer** and **Rich**. +### Configuring Rich formatted output + +By default, **Typer** uses the **Rich** library to format text output. However it's an optional dependency, and is used only if it is installed in your environment. In case you would rather have finer-grained control over when **Typer** uses rich formatting, you can do so with the two package level functions shown below to control the output format globally. This affects all `typer.Typer()` instances. + + 1. `typer.set_rich_help(switch: bool)` + 2. `typer.set_rich_traceback(switch: bool)` + +The first one controls all help and **Click's** exception messages. The second one controls the format of the stack trace. + ## "Standard Output" and "Standard Error" The way printing works underneath is that the **operating system** (Linux, Windows, macOS) treats what we print as if our CLI program was **writing text** to a "**virtual file**" called "**standard output**". diff --git a/tests/assets/set_rich_help.py b/tests/assets/set_rich_help.py new file mode 100644 index 0000000000..1224f82cc5 --- /dev/null +++ b/tests/assets/set_rich_help.py @@ -0,0 +1,14 @@ +import typer + +typer.set_rich_help(False) + +app = typer.Typer() + + +@app.command() +def main(arg: str): # pragma: no cover + pass + + +if __name__ == "__main__": + app() diff --git a/tests/assets/set_rich_traceback.py b/tests/assets/set_rich_traceback.py new file mode 100644 index 0000000000..e531316930 --- /dev/null +++ b/tests/assets/set_rich_traceback.py @@ -0,0 +1,14 @@ +import typer + +typer.set_rich_traceback(False) + +app = typer.Typer() + + +@app.command() +def raise_(): + raise ValueError # raise some error to test traceback output + + +if __name__ == "__main__": + app() diff --git a/tests/test_set_rich_output.py b/tests/test_set_rich_output.py new file mode 100644 index 0000000000..de21a921cd --- /dev/null +++ b/tests/test_set_rich_output.py @@ -0,0 +1,27 @@ +import subprocess +import sys +from pathlib import Path + + +def test_set_rich_help_false_outputs_plain_text(): + file_path = Path(__file__).parent / "assets/set_rich_help.py" + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", str(file_path), "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + # assert simple help text + assert "─" not in result.stdout + + +def test_set_rich_traceback_false_outputs_plain_text(): + file_path = Path(__file__).parent / "assets/set_rich_traceback.py" + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", str(file_path)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + # assert simple help text + assert "─" not in result.stderr diff --git a/typer/__init__.py b/typer/__init__.py index d4ac56d0ba..a728651cd7 100644 --- a/typer/__init__.py +++ b/typer/__init__.py @@ -29,6 +29,8 @@ from . import colors as colors from .main import Typer as Typer from .main import run as run +from .main import set_rich_help as set_rich_help +from .main import set_rich_traceback as set_rich_traceback from .models import CallbackParam as CallbackParam from .models import Context as Context from .models import FileBinaryRead as FileBinaryRead diff --git a/typer/core.py b/typer/core.py index 31fece5a76..e14b41ef03 100644 --- a/typer/core.py +++ b/typer/core.py @@ -39,6 +39,14 @@ except ImportError: # pragma: no cover rich = None # type: ignore +_is_rich_help = True + + +def set_rich_help(switch: bool) -> None: + global _is_rich_help + _is_rich_help = switch + + MarkupMode = Literal["markdown", "rich", None] @@ -208,7 +216,7 @@ def _main( if not standalone_mode: raise # Typer override - if rich: + if rich and _is_rich_help: rich_utils.rich_format_error(e) else: e.show() @@ -238,7 +246,7 @@ def _main( if not standalone_mode: raise # Typer override - if rich: + if rich and _is_rich_help: rich_utils.rich_abort_error() else: click.echo(_("Aborted!"), file=sys.stderr) @@ -669,7 +677,7 @@ def main( ) def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None: - if not rich: + if not (rich and _is_rich_help): return super().format_help(ctx, formatter) return rich_utils.rich_format_help( obj=self, @@ -731,7 +739,7 @@ def main( ) def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None: - if not rich: + if not (rich and _is_rich_help): return super().format_help(ctx, formatter) return rich_utils.rich_format_help( obj=self, diff --git a/typer/main.py b/typer/main.py index 9db26975ca..7595e83a29 100644 --- a/typer/main.py +++ b/typer/main.py @@ -15,6 +15,7 @@ from .completion import get_completion_inspect_parameters from .core import MarkupMode, TyperArgument, TyperCommand, TyperGroup, TyperOption +from .core import set_rich_help as core_set_rich_help from .models import ( AnyType, ArgumentInfo, @@ -49,6 +50,17 @@ _original_except_hook = sys.excepthook _typer_developer_exception_attr_name = "__typer_developer_exception__" +_is_rich_traceback = True + + +def set_rich_help(switch: bool) -> None: + core_set_rich_help(switch) + + +def set_rich_traceback(switch: bool) -> None: + global _is_rich_traceback + _is_rich_traceback = switch + def except_hook( exc_type: Type[BaseException], exc_value: BaseException, tb: Optional[TracebackType] @@ -68,7 +80,7 @@ def except_hook( click_path = os.path.dirname(click.__file__) supress_internal_dir_names = [typer_path, click_path] exc = exc_value - if rich: + if rich and _is_rich_traceback: rich_tb = Traceback.from_exception( type(exc), exc,