File: utils.py

package info (click to toggle)
spyder-kernels 3.1.3-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,088 kB
  • sloc: python: 6,327; sh: 9; makefile: 5
file content (231 lines) | stat: -rw-r--r-- 7,754 bytes parent folder | download | duplicates (2)
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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# Copyright (c) 2009- Spyder Kernels Contributors
#
# Licensed under the terms of the MIT License
# (see spyder_kernels/__init__.py for details)

"""Utility functions."""

import ast
import builtins
import os
import re
import sys
import sysconfig


def create_pathlist():
    """
    Create list of Python library paths to be skipped from module
    reloading and Pdb steps.
    """
    # Get standard installation paths
    try:
        paths = sysconfig.get_paths()
        standard_paths = [paths['stdlib'],
                          paths['purelib'],
                          paths['scripts'],
                          paths['data']]
    except Exception:
        standard_paths = []

    # Get user installation path
    # See spyder-ide/spyder#8776
    try:
        import site
        if getattr(site, 'getusersitepackages', False):
            # Virtualenvs don't have this function but
            # conda envs do
            user_path = [site.getusersitepackages()]
        elif getattr(site, 'USER_SITE', False):
            # However, it seems virtualenvs have this
            # constant
            user_path = [site.USER_SITE]
        else:
            user_path = []
    except Exception:
        user_path = []

    return standard_paths + user_path


def path_is_library(path, initial_pathlist=None):
    """Decide if a path is in user code or a library according to its path."""
    # Compute DEFAULT_PATHLIST only once and make it global to reuse it
    # in any future call of this function.
    if 'DEFAULT_PATHLIST' not in globals():
        global DEFAULT_PATHLIST
        DEFAULT_PATHLIST = create_pathlist()

    if initial_pathlist is None:
        initial_pathlist = []

    pathlist = initial_pathlist + DEFAULT_PATHLIST

    if path is None:
        # Path probably comes from a C module that is statically linked
        # into the interpreter. There is no way to know its path, so we
        # choose to ignore it.
        return True
    elif any([p in path for p in pathlist]):
        # We don't want to consider paths that belong to the standard
        # library or installed to site-packages.
        return True
    elif os.name == 'nt':
        if re.search(r'.*\\pkgs\\.*', path):
            return True
        else:
            return False
    elif not os.name == 'nt':
        # Paths containing the strings below can be part of the default
        # Linux installation, Homebrew or the user site-packages in a
        # virtualenv.
        patterns = [
            r'^/usr/lib.*',
            r'^/usr/local/lib.*',
            r'^/usr/.*/dist-packages/.*',
            r'^/home/.*/.local/lib.*',
            r'^/Library/.*',
            r'^/Users/.*/Library/.*',
            r'^/Users/.*/.local/.*',
        ]

        if [p for p in patterns if re.search(p, path)]:
            return True
        else:
            return False
    else:
        return False


def capture_last_Expr(code_ast, out_varname, global_ns):
    """Parse line and modify code to capture in globals the last expression."""
    # Modify ast code to capture the last expression
    capture_last_expression = False
    if (
        len(code_ast.body)
        and isinstance(code_ast.body[-1], ast.Expr)
    ):
        global_ns["__spyder_builtins__"] = builtins
        capture_last_expression = True
        expr_node = code_ast.body[-1]
        # Create new assign node
        assign_node = ast.parse(
            '__spyder_builtins__.globals()[{}] = None'.format(
                repr(out_varname))).body[0]
        # Replace None by the value
        assign_node.value = expr_node.value
        # Fix line number and column offset
        assign_node.lineno = expr_node.lineno
        assign_node.col_offset = expr_node.col_offset
        if sys.version_info[:2] >= (3, 8):
            # Exists from 3.8, necessary from 3.11
            assign_node.end_lineno = expr_node.end_lineno
            if assign_node.lineno == assign_node.end_lineno:
                # Add '__spyder_builtins__.globals()[{}] = ' and remove 'None'
                assign_node.end_col_offset += expr_node.end_col_offset - 4
            else:
                assign_node.end_col_offset = expr_node.end_col_offset
        code_ast.body[-1] = assign_node
    return code_ast, capture_last_expression


def exec_encapsulate_locals(
    code_ast, globals, locals, exec_fun=None, filename=None
):
    """
    Execute by encapsulating locals if needed.

    Notes
    ----- 
    * In general, the dict returned by locals() might or might not be modified.
      In this case, the encapsulated dict can not.
    """
    use_locals_hack = locals is not None and locals is not globals
    if use_locals_hack:
        globals["__spyder_builtins__"] = builtins

        # Mitigates a behaviour of CPython that makes it difficult
        # to work with exec and the local namespace
        # See:
        #  - https://bugs.python.org/issue41918
        #  - https://bugs.python.org/issue46153
        #  - https://bugs.python.org/issue21161
        #  - spyder-ide/spyder#13909
        #  - spyder-ide/spyder-kernels#345
        #
        # The idea here is that the best way to emulate being in a
        # function is to actually execute the code in a function.
        # A function called `_spyderpdb_code` is created and
        # called. It will first load the locals, execute the code,
        # and then update the locals.
        #
        # One limitation of this approach is that locals() is only
        # a copy of the curframe locals. This means that closures
        # for example are early binding instead of late binding.

        # Create a function
        indent = "    "
        code = ["def _spyderpdb_code():"]

        # Add locals in globals
        # If the debugger is recursive, the globals could already
        # have a _spyderpdb_locals as it might be shared between
        # levels
        if "_spyderpdb_locals" in globals:
            globals["_spyderpdb_locals"].append(locals)
        else:
            globals["_spyderpdb_locals"] = [locals]

        # Load locals if they have a valid name
        # In comprehensions, locals could contain ".0" for example
        code += [indent + "{k} = _spyderpdb_locals[-1]['{k}']".format(
            k=k) for k in locals if k.isidentifier()]

        # The code comes here

        # Update the locals
        code += [indent + "_spyderpdb_locals[-1].update("
                 "__spyder_builtins__.locals())"]

        # Run the function
        code += ["_spyderpdb_code()"]

        # Parse the function
        fun_ast = ast.parse('\n'.join(code) + '\n')

        # Inject code_ast in the function before the locals update
        fun_ast.body[0].body = (
            fun_ast.body[0].body[:-1]  # The locals
            + code_ast.body  # Code to run
            + fun_ast.body[0].body[-1:]  # Locals update
        )
        code_ast = fun_ast

    try:
        if exec_fun is None:
            exec_fun = exec
        if filename is None:
            filename = "<stdin>"
        exec_fun(compile(code_ast, filename, "exec"), globals, None)
    finally:
        if use_locals_hack:
            # Cleanup code
            globals.pop("_spyderpdb_code", None)
            if len(globals["_spyderpdb_locals"]) > 1:
                del globals["_spyderpdb_locals"][-1]
            else:
                del globals["_spyderpdb_locals"]


def canonic(filename):
    """
    Return canonical form of filename.

    This is a copy of bdb.canonic, so that the debugger will process 
    filenames in the same way
    """
    if filename == "<" + filename[1:-1] + ">":
        return filename
    canonic = os.path.abspath(filename)
    canonic = os.path.normcase(canonic)
    return canonic