File: path_info.py

package info (click to toggle)
python-friendly-traceback 0.7.62%2Bgit20240811.d7dbff6-1.1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 9,264 kB
  • sloc: python: 21,500; makefile: 4
file content (214 lines) | stat: -rw-r--r-- 7,545 bytes parent folder | download
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()))