Skip to content
Draft
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
21 changes: 21 additions & 0 deletions docs/tutorial/exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,27 @@ $ python main.py

</div>

## Disable Tracebacks From Certain Modules
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the other sections in this part of the tutorial, this section should show the output before and after setting this new var.


If you are developing with Python frameworks other than **Typer** and **Click**,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we'll be vendoring Click in the near future, I suggest to not include the reference here.

Suggested change
If you are developing with Python frameworks other than **Typer** and **Click**,
If you are developing with other Python frameworks besides **Typer**,

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`:
Expand Down
18 changes: 18 additions & 0 deletions docs_src/exceptions/tutorial005_py310.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import gitlab
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that you're mocking the installation of gitlab for this to run in the test suite.

What I dislike about using gitlab as an example here, is that users reading the tutorial would have to install the lib just to replicate this one example.

Is there a different example, with already installed libraries, you can think of to showcase this functionality?

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!
Comment on lines +10 to +13
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general we tend to avoid comments in the source files, as they are displayed verbatim in the tutorial, which should have all the relevant information.

Suggested change
# 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()
32 changes: 32 additions & 0 deletions tests/test_tutorial/test_exceptions/test_tutorial005.py
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need for noqa: E402 here, just move the import to the top of the file.


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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid I disagree - this test doesn't actually test anything. What should get tested, is whether or not the traceback is suppressed. Switching to an actual lib that is in the environment (instead of doing all this mocking) would probably resolve that.



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
24 changes: 22 additions & 2 deletions typer/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion typer/models.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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):
Expand Down
3 changes: 2 additions & 1 deletion typer/rich_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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