1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
|
"""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()
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()))
|