[BUG] Rich tracebacks raise another exception when using pyinstaller
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
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 = ''
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.
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
Just confirming this is still an issue.
I am having the same problem with rich.traceback and Cython compiled Python code