File: editors_helpers.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 (169 lines) | stat: -rw-r--r-- 5,884 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
"""
editors_helpers.py
------------------

The functions in this module have been created so that user editors/IDEs
could use Friendly without having to change the content of
their own programs.

None of these are part of the public API.

If you make use of any other function here, please file an issue so
it can be determined if it should be added to the public API.
"""

import sys
from typing import Any, Dict, Tuple, Union

from .config import session
from .ft_gettext import current_lang
from .source_cache import cache


def check_syntax(
    *, source=None, filename="Fake_filename", path=None, include=None, lang=None
) -> Union[bool, Tuple[Any, str]]:
    """This uses Python's ``compile()`` builtin which does some analysis of
    its code argument and will raise an exception if it identifies
    some syntax errors, but also some less common "overflow" and "value"
    errors.

    Note that there are a few syntax errors that are not caught by this,
    as they are identified by Python very late in its execution
    process. See for example
    `this blog post <https://aroberge.blogspot.com/2019/12/a-tiny-python-exception-oddity.html>`_

    This function can either be used on a file, using the ``path`` argument, or
    on some code passed as a string, using the ``source`` argument.
    For the latter case, one can also specify a corresponding ``filename``:
    this could be useful if this function is invoked from a GUI-based
    editor.

    Note that the ``path`` argument, if provided, takes precedence
    over the ``source`` argument.

    Two additional named arguments, ``include`` and ``lang``, can be
    provided to temporarily set the values to be used during this function
    call. The original values are restored at the end.

    If friendly exception hook has not been set up prior
    to calling check_syntax, it will only be used for the duration
    of this function call.

    Returns a tuple containing a code object and a filename if no exception
    has been raised, False otherwise.

    """
    _ = current_lang.translate

    saved_except_hook, saved_include = _save_settings()
    saved_lang = _temp_set_lang(lang)

    if path is not None:
        try:
            with open(path, encoding="utf8") as f:
                source = f.read()
                filename = path
        except Exception:  # noqa
            # Do not show the Python traceback which would include
            #  the call to open() in the traceback
            if include is None:
                session.set_include("no_tb")
            else:
                session.set_include(include)
            session.explain_traceback()
            _reset(saved_except_hook, saved_lang, saved_include)
            return False

    cache.add(filename, source)
    try:
        code = compile(source, filename, "exec")
    except Exception:  # noqa
        if include is None:
            session.set_include("explain")  # our default
        else:
            session.set_include(include)
        session.explain_traceback()
        _reset(saved_except_hook, saved_lang, saved_include)
        return ""

    _reset(saved_except_hook, saved_lang, saved_include)
    return code


def exec_code(*, source=None, path=None, include=None, lang=None) -> Dict:
    """This uses check_syntax to see if the code is valid and, if so,
    executes it into a globals dict containing only
    ``{"__name__": "__main__"}``.
    If no ``SyntaxError`` exception is raised, this dict is returned;
    otherwise, an empty dict is returned.

    It can either be used on a file, using the ``path`` argument, or
    on some code passed as a string, using the ``source`` argument.

    Note that the ``path`` argument, if provided, takes precedence
    over the ``source`` argument.

    Two additional named arguments, ``include`` and ``lang``, can be
    provided to temporarily set the values to be used during this function
    call. The original values are restored at the end.

    If friendly exception hook has not been set up prior
    to calling check_syntax, it will only be used for the duration
    of this function call.
    """
    code = check_syntax(source=source, path=path, include=include, lang=lang)
    if not code:
        return {}

    saved_except_hook, saved_include = _save_settings()
    saved_lang = _temp_set_lang(lang)

    module_globals = {"__name__": "__main__"}
    try:
        exec(code, module_globals)
    except Exception:  # noqa
        if include is None:
            session.set_include("explain")  # our default
        else:
            session.set_include(include)
        session.explain_traceback()
        _reset(saved_except_hook, saved_lang, saved_include)
        return module_globals

    _reset(saved_except_hook, saved_lang, saved_include)
    return module_globals


def _temp_set_lang(lang: str) -> str:
    """If lang is not none, temporarily set session.lang to the provided
    value. Keep track of the original lang setting and return it.

    A value of None for saved_lang indicates that no resetting will
    be required.
    """
    saved_lang = None
    if lang is not None:
        saved_lang = session.lang
        if saved_lang != lang:
            session.install_gettext(lang)
        else:
            saved_lang = None
    return saved_lang


def _save_settings():
    current_except_hook = sys.excepthook
    current_include = session.include

    return current_except_hook, current_include


def _reset(saved_except_hook, saved_lang, saved_include):
    """Resets both include and lang to their original values"""
    if saved_lang is not None:
        session.install_gettext(saved_lang)
    session.set_include(saved_include)
    # set_include(0) restores sys.excepthook to sys.__excepthook__
    # which might not be what is wanted. So, we reset sys.excepthook last
    sys.excepthook = saved_except_hook