rich icon indicating copy to clipboard operation
rich copied to clipboard

[BUG] Rich tracebacks raise another exception when using pyinstaller

Open gormaniac opened this issue 3 years ago • 5 comments

Describe the bug

A Python code object when Python is run by pyinstaller, does not have a co_filename attribute that points to an absolute path, instead it is a relative path. For example it will be rich/console.py instead of /abs/path/to/project/rich/console.py.

Because the path is relative, we will hit this branch of code in rich.traceback.Traceback.extract. This code prepends the current working directory to the relative path in the code object. This file will likely never exist. It is possible that the current working directory happens to be where the extracted pyinstaller bundle is, but it is unlikely.

Since the generated filename doesn't exist, rich will throw an exception stating [Errno 2] No such file or directory:. This muddies the relevant traceback and suggests to users that the issue in whatever code uses rich is related to a missing file rather than whatever raised the original exception.

It is worth noting that in tests I've run without pyinstaller, I've found that code.co_filename attributes point to absolute paths so this isn't a big issue for typical Python code.

I've come up with a pyinstaller specific fix but I don't know if you'd like to include a py2exe related fix as well (I'm also unsure if py2exe even has this issue). I can submit a PR for this if you'd like. We'd just need to change the appropriate bit in rich.traceback.Traceback.extract to the below:

filename = frame_summary.f_code.co_filename
if filename and not filename.startswith("<"):
    if not os.path.isabs(filename):
        # Use has/getattr because `frozen` won't exist if not pyinstaller
        if hasattr(sys, 'frozen') and getattr(sys, 'frozen', False) == True:
            filename = os.path.join(sys._MEIPASS, filename)
        else:
            filename = os.path.join(_IMPORT_CWD, filename)

See the pyinstaller docs for info on sys.frozen and sys._MEIPASS.

Build a pyinstaller bundle with the below code and run it. Change directories and run it again. After both commands, try opening the file listed in the traceback, it shouldn't exist. You will also see an error that says the non-existent file doesn't exist, which isn't what we tried to display in our code.

from rich.console import Console
console = Console()

try:
    raise Exception('A detailed error message')
except Exception:
    console.print_exception(show_locals=True)
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│                                                                                                  │
│ /the/current/working/dir/test.py:5 in <module>                                                   │
│                                                                                                  │
│ [Errno 2] No such file or directory: '/the/current/working/dir/test.py'                          │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
Exception: A detailed error message

Platform

Click to expand

Mac using iTerm2.

╭───────────────────────── <class 'rich.console.Console'> ─────────────────────────╮
│ A high level console interface.                                                  │
│                                                                                  │
│ ╭──────────────────────────────────────────────────────────────────────────────╮ │
│ │ <console width=193 ColorSystem.TRUECOLOR>                                    │ │
│ ╰──────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                  │
│     color_system = 'truecolor'                                                   │
│         encoding = 'utf-8'                                                       │
│             file = <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'> │
│           height = 73                                                            │
│    is_alt_screen = False                                                         │
│ is_dumb_terminal = False                                                         │
│   is_interactive = True                                                          │
│       is_jupyter = False                                                         │
│      is_terminal = True                                                          │
│   legacy_windows = False                                                         │
│         no_color = False                                                         │
│          options = ConsoleOptions(                                               │
│                        size=ConsoleDimensions(width=193, height=73),             │
│                        legacy_windows=False,                                     │
│                        min_width=1,                                              │
│                        max_width=193,                                            │
│                        is_terminal=True,                                         │
│                        encoding='utf-8',                                         │
│                        max_height=73,                                            │
│                        justify=None,                                             │
│                        overflow=None,                                            │
│                        no_wrap=False,                                            │
│                        highlight=None,                                           │
│                        markup=None,                                              │
│                        height=None                                               │
│                    )                                                             │
│            quiet = False                                                         │
│           record = False                                                         │
│         safe_box = True                                                          │
│             size = ConsoleDimensions(width=193, height=73)                       │
│        soft_wrap = False                                                         │
│           stderr = False                                                         │
│            style = None                                                          │
│         tab_size = 8                                                             │
│            width = 193                                                           │
╰──────────────────────────────────────────────────────────────────────────────────╯
╭─── <class 'rich._windows.WindowsConsoleFeatures'> ────╮
│ Windows features available.                           │
│                                                       │
│ ╭───────────────────────────────────────────────────╮ │
│ │ WindowsConsoleFeatures(vt=False, truecolor=False) │ │
│ ╰───────────────────────────────────────────────────╯ │
│                                                       │
│ truecolor = False                                     │
│        vt = False                                     │
╰───────────────────────────────────────────────────────╯
╭────── Environment Variables ───────╮
│ {                                  │
│     'TERM': 'xterm-256color',      │
│     'COLORTERM': 'truecolor',      │
│     'CLICOLOR': None,              │
│     'NO_COLOR': None,              │
│     'TERM_PROGRAM': 'iTerm.app',   │
│     'COLUMNS': None,               │
│     'LINES': None,                 │
│     'JPY_PARENT_PID': None,        │
│     'VSCODE_VERBOSE_LOGGING': None │
│ }                                  │
╰────────────────────────────────────╯
platform="Darwin"

rich==12.2.0

gormaniac avatar May 04 '22 16:05 gormaniac

I've realized that my proposed solution isn't complete and will still cause the bug if the referenced file is compressed and packaged within the pyinstaller binary. Working on perhaps catching the OSError within rich.traceback.Traceback._render_stack. If that doesn't work I might just do the below to the originally proposed solution so we use ? for the filename argument when we construct a Frame in rich.traceback.Traceback.extract:

if hasattr(sys, 'frozen') and getattr(sys, 'frozen', False) == True:
    filename = ''

gormaniac avatar May 04 '22 18:05 gormaniac

Using an empty string as I proposed above still causes the error:

╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│                                                                                                  │
│ ?:15 in main                                                                                     │
│                                                                                                  │
│ [Errno 2] No such file or directory: '?'                                                         │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
Exception: A detailed error message

This also means that the default behavior of rich is broken. We need to figure out a better solution for when a file can't be found.

gormaniac avatar May 04 '22 18:05 gormaniac

Okay I think I figured out a working solution specific to pyinstaller, but not for if we can't find the filename at all.

Changed the original solution to this:

if hasattr(sys, 'frozen') and getattr(sys, 'frozen', False) == True:
    mod_name = os.path.splitext(filename)[0].replace(os.path.sep, '.')
    filename = f'<pyinstaller module ({mod_name})>'
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ <pyinstaller module (project.test)>:15 in main                                                  │
│ ╭────────── locals ───────────╮                                                                  │
│ │ args = Namespace(pdb=False) │                                                                  │
│ ╰─────────────────────────────╯                                                                  │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
Exception: A detailed error message

gormaniac avatar May 04 '22 18:05 gormaniac

Just confirming this is still an issue.

AcidWeb avatar Jul 05 '22 14:07 AcidWeb

I am having the same problem with rich.traceback and Cython compiled Python code

ronny-rentner avatar Sep 09 '22 02:09 ronny-rentner