"""path_info.py
In many places, by default we exclude the files from this package,
thus restricting tracebacks to code written by the users.
If Friendly-traceback is used by some other program,
it might be desirable to exclude additional files.
"""
import os
import sys
from typing import Set, TypeVar
import asttokens # Only use it as a representative to find site-packages
from .ft_gettext import current_lang
from .typing_info import StrPath
EXCLUDED_FILE_PATH: Set[str] = set()
EXCLUDED_DIR_NAMES: Set[str] = set()
SITE_PACKAGES = os.path.abspath(os.path.join(os.path.dirname(asttokens.__file__), ".."))
PYTHON_LIB = os.path.abspath(os.path.dirname(os.__file__))
FRIENDLY = os.path.abspath(os.path.dirname(__file__))
TESTS = os.path.abspath(os.path.join(FRIENDLY, "..", "tests"))
def exclude_file_from_traceback(full_path: StrPath) -> None:
"""Exclude a file from appearing in a traceback generated by
Friendly-traceback. Note that this does not apply to
the true Python traceback obtained using "debug_tb".
"""
# full_path could be a pathlib.Path instance
full_path = str(full_path)
if full_path.startswith("<"):
# https://github.com/friendly-traceback/friendly-traceback/issues/107
EXCLUDED_FILE_PATH.add(full_path)
return
# full_path could be a relative path; see issue #81
full_path = os.path.abspath(full_path)
if not os.path.isfile(full_path):
raise RuntimeError(
f"{full_path} is not a valid file path; it cannot be excluded."
)
EXCLUDED_FILE_PATH.add(full_path)
def exclude_directory_from_traceback(dir_name: StrPath) -> None:
"""Exclude all files found in a given directory, including sub-directories,
from appearing in a traceback generated by Friendly.
Note that this does not apply to the true Python traceback
obtained using "debug_tb".
"""
if not os.path.isdir(dir_name):
raise RuntimeError(f"{dir_name} is not a directory; it cannot be excluded.")
# dir_name could be a pathlib.Path instance.
dir_name = str(dir_name)
# Suppose we have dir_name = "this/path" instead of "this/path/".
# Later, when we want to exclude a directory, we get the following file path:
# "this/path2/name.py". If we don't append the ending "/", we would exclude
# this file by error in is_excluded_file below.
if dir_name[-1] != os.path.sep:
dir_name += os.path.sep
EXCLUDED_DIR_NAMES.add(dir_name)
dirname = os.path.abspath(os.path.dirname(__file__))
exclude_directory_from_traceback(dirname)
def is_excluded_file(full_path: StrPath, python_excluded: bool = True) -> bool:
"""Determines if the file belongs to the group that is excluded from tracebacks."""
# full_path could be a pathlib.Path instance
full_path = str(full_path)
if full_path.startswith("<") and full_path in EXCLUDED_FILE_PATH:
return True
if full_path.startswith("<frozen "):
return True
full_path = os.path.abspath(full_path)
for dirs in EXCLUDED_DIR_NAMES:
if full_path.startswith(dirs):
return True
# Design choice: we exclude all files from the Python standard library
# but not those that have been installed by the user if python_excluded is True.
if (
full_path.startswith(PYTHON_LIB)
and not full_path.startswith(SITE_PACKAGES)
and python_excluded
):
return True
return full_path in EXCLUDED_FILE_PATH
def include_file_in_traceback(full_path: str) -> None:
"""Reverses the effect of ``exclude_file_from_traceback()`` so that
the file can potentially appear in later tracebacks generated
by Friendly-traceback.
A typical pattern might be something like::
import some_module
reverted = not is_excluded_file(some_module.__file__)
if reverted:
exclude_file_from_traceback(some_module.__file__)
try:
some_module.do_something(...)
except Exception:
friendly_traceback.explain_traceback()
finally:
if reverted:
include_file_in_traceback(some_module.__file__)
"""
full_path = str(full_path)
full_path = os.path.abspath(full_path)
EXCLUDED_FILE_PATH.discard(full_path)
class PathUtil:
def __init__(self) -> None:
self.home = os.path.expanduser("~")
MaybeText = TypeVar("MaybeText", str, None)
def shorten_path(self, path: MaybeText) -> MaybeText: # pragma: no cover
from .config import session
if path is None: # can happen in some rare cases
return path
if path in ["<stdin>", "<string>"]:
return path
orig_path = path
path = path.replace("'", "") # We might get passed a path repr
path = os.path.abspath(path)
path_lower = path.casefold()
if "ipykernel" in path:
new_path = shorten_jupyter_kernel(orig_path)
if new_path:
return new_path
elif "<pyshell#" in path:
path = "<pyshell#" + path.split("<pyshell#")[1]
elif "<ipython-input-" in path:
parts = path.split("<ipython")
parts = parts[1].split("-")
path = "[" + parts[-2] + "]"
elif "<friendly-console:" in path:
split_path = path.split("<friendly-console:")[1]
if session.ipython_prompt:
path = "[" + split_path[:-1] + "]"
else:
path = "<friendly-console:" + split_path
elif path_lower.startswith(SITE_PACKAGES.casefold()):
path = "LOCAL:" + path[len(SITE_PACKAGES) :]
elif path_lower.startswith(PYTHON_LIB.casefold()):
path = "PYTHON_LIB:" + path[len(PYTHON_LIB) :]
elif path_lower.startswith(FRIENDLY.casefold()):
path = "FRIENDLY:" + path[len(FRIENDLY) :]
elif path_lower.startswith(TESTS.casefold()):
path = "TESTS:" + path[len(TESTS) :]
elif path_lower.startswith(self.home.casefold()):
if not os.path.exists(path):
return orig_path
path = "HOME:" + path[len(self.home) :]
return path
def shorten_jupyter_kernel(path: str) -> str: # pragma: no cover
from .source_cache import cache
if "__main__" in sys.modules:
main = sys.modules["__main__"]
if "In" in dir(main):
ipython_inputs = getattr(main, "In")
else:
return ""
else:
return ""
lines = cache.get_source_lines(path)
source = "".join(lines)
source = source.strip().replace("\r", "")
if not source:
return ""
found = 0
new_path = ""
for index, inp in enumerate(ipython_inputs):
inp = inp.strip().replace("\r", "")
if source == inp:
new_path = f"[{index}]"
found += 1
if found > 1:
new_path = new_path + "?"
return new_path
path_utils = PathUtil()
[docs]def show_paths() -> None: # pragma: no cover
"""To avoid displaying very long file paths to the user,
Friendly-traceback tries to shorten them using some easily
recognized synonyms. This function shows the path synonyms
currently used.
"""
_ = current_lang.translate
print("HOME =", path_utils.home)
print("LOCAL =", SITE_PACKAGES)
print("PYTHON_LIB =", PYTHON_LIB)
if FRIENDLY != SITE_PACKAGES:
print("FRIENDLY = ", FRIENDLY)
print(_("The default directory is {dirname}.").format(dirname=os.getcwd()))