diff --git a/docs/tutorial/exceptions.md b/docs/tutorial/exceptions.md index 18c3374ff3..92b5132e9c 100644 --- a/docs/tutorial/exceptions.md +++ b/docs/tutorial/exceptions.md @@ -192,6 +192,27 @@ $ python main.py +## Disable Tracebacks From Certain Modules + +If you are developing with Python frameworks other than **Typer** and **Click**, +you might get very verbose tracebacks, which could make it difficult to find the +line in your own code that triggered the exception. + +With pretty exceptions, you can use the parameter `pretty_exceptions_suppress`, +which takes a list of Python modules, or `str` paths, to indicate which modules +should have their traceback frames suppressed by the **Rich** traceback +formatter. Only filename and line number will be shown for these modules, but no +code or variables. ⚡ + +For example, if you are developing a GitLab utility using the `python-gitlab` +package, you might notice that tracebacks are very long and filled with internal +calls inside the `gitlab` module that you probably do not care about. In this case, +you can suppress the traceback frames inside the `gitlab` module: + +{* docs_src/exceptions/tutorial005_py310.py hl[4] *} + +And now you can see clearly which of your calls to `gitlab` caused the exception. 💡 + ## Disable Pretty Exceptions You can also entirely disable pretty exceptions with the parameter `pretty_exceptions_enable=False`: diff --git a/docs_src/exceptions/tutorial005_py310.py b/docs_src/exceptions/tutorial005_py310.py new file mode 100644 index 0000000000..b710caf30d --- /dev/null +++ b/docs_src/exceptions/tutorial005_py310.py @@ -0,0 +1,18 @@ +import gitlab +import typer + +app = typer.Typer(pretty_exceptions_suppress=[gitlab]) + + +@app.command() +def main(): + gitlab_client = gitlab.Gitlab() + + # This will raise an exception if not authenticated: + # GitlabAuthenticationError: 401: 401 Unauthorized + # But the traceback will not show any lines from the gitlab module! + print(gitlab_client.pagesdomains.list()) + + +if __name__ == "__main__": + app() diff --git a/tests/test_tutorial/test_exceptions/test_tutorial005.py b/tests/test_tutorial/test_exceptions/test_tutorial005.py new file mode 100644 index 0000000000..bf33d5c80a --- /dev/null +++ b/tests/test_tutorial/test_exceptions/test_tutorial005.py @@ -0,0 +1,32 @@ +import sys +from types import ModuleType +from unittest.mock import MagicMock + +from typer.testing import CliRunner + +# Mock the gitlab module before importing the tutorial +_gitlab = ModuleType("gitlab") +_gitlab.Gitlab = MagicMock # type: ignore[attr-defined] +_gitlab.__file__ = "gitlab/__init__.py" # type: ignore[attr-defined] +sys.modules.setdefault("gitlab", _gitlab) + +from docs_src.exceptions import tutorial005_py310 as mod # noqa: E402 + +runner = CliRunner() + +# There's no way to test a third-party package from PyPI. Also, the actual +# feature we use is part of Rich. We just pass the flag along. So here we test +# that the tutorial code runs without errors. + +# Perhaps we could find some standard library module that throws a long +# traceback and test that instead, but for now this is probably good enough. + + +def test_pretty_exceptions_suppress(): + result = runner.invoke(mod.app) + assert result.exit_code == 0 + + +def test_script(): + result = runner.invoke(mod.app, ["--help"]) + assert "Usage" in result.stdout diff --git a/typer/main.py b/typer/main.py index f4f21bb844..d40746faa1 100644 --- a/typer/main.py +++ b/typer/main.py @@ -5,13 +5,13 @@ import subprocess import sys import traceback -from collections.abc import Callable, Sequence +from collections.abc import Callable, Iterable, Sequence from datetime import datetime from enum import Enum from functools import update_wrapper from pathlib import Path from traceback import FrameSummary, StackSummary -from types import TracebackType +from types import ModuleType, TracebackType from typing import Annotated, Any from uuid import UUID @@ -514,6 +514,24 @@ def callback(): """ ), ] = True, + pretty_exceptions_suppress: Annotated[ + Iterable[str | ModuleType], + Doc( + """ + A list of modules or module path strings to suppress in Rich tracebacks. + Frames from these modules will be hidden in the traceback output. + + **Example** + + ```python + import typer + import httpx + + app = typer.Typer(pretty_exceptions_suppress=(httpx,)) + ``` + """ + ), + ] = (), ): self._add_completion = add_completion self.rich_markup_mode: MarkupMode = rich_markup_mode @@ -522,6 +540,7 @@ def callback(): self.pretty_exceptions_enable = pretty_exceptions_enable self.pretty_exceptions_show_locals = pretty_exceptions_show_locals self.pretty_exceptions_short = pretty_exceptions_short + self.pretty_exceptions_suppress = pretty_exceptions_suppress self.info = TyperInfo( name=name, cls=cls, @@ -1147,6 +1166,7 @@ def __call__(self, *args: Any, **kwargs: Any) -> Any: pretty_exceptions_enable=self.pretty_exceptions_enable, pretty_exceptions_show_locals=self.pretty_exceptions_show_locals, pretty_exceptions_short=self.pretty_exceptions_short, + pretty_exceptions_suppress=self.pretty_exceptions_suppress, ), ) raise e diff --git a/typer/models.py b/typer/models.py index 3285a96a24..09b52643d0 100644 --- a/typer/models.py +++ b/typer/models.py @@ -1,6 +1,7 @@ import inspect import io -from collections.abc import Callable, Sequence +from collections.abc import Callable, Iterable, Sequence +from types import ModuleType from typing import ( TYPE_CHECKING, Any, @@ -634,10 +635,12 @@ def __init__( pretty_exceptions_enable: bool = True, pretty_exceptions_show_locals: bool = True, pretty_exceptions_short: bool = True, + pretty_exceptions_suppress: Iterable[str | ModuleType] = (), ) -> None: self.pretty_exceptions_enable = pretty_exceptions_enable self.pretty_exceptions_show_locals = pretty_exceptions_show_locals self.pretty_exceptions_short = pretty_exceptions_short + self.pretty_exceptions_suppress = pretty_exceptions_suppress class TyperPath(click.Path): diff --git a/typer/rich_utils.py b/typer/rich_utils.py index d85043238c..ecb40e59a8 100644 --- a/typer/rich_utils.py +++ b/typer/rich_utils.py @@ -742,12 +742,13 @@ def get_traceback( exception_config: DeveloperExceptionConfig, internal_dir_names: list[str], ) -> Traceback: + suppress = (*internal_dir_names, *exception_config.pretty_exceptions_suppress) rich_tb = Traceback.from_exception( type(exc), exc, exc.__traceback__, show_locals=exception_config.pretty_exceptions_show_locals, - suppress=internal_dir_names, + suppress=suppress, width=MAX_WIDTH, ) return rich_tb