aboutsummaryrefslogtreecommitdiffstats
path: root/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/feature.py
blob: 7a0871ee7d0c969451c0393c5ba50f158525969a (plain)
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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

# flake8: noqa F:821
# flake8: noqa F:401

"""
__feature__.py  (renamed to feature.py)

This is the feature file for the Qt for Python project. There is some
similarity to Python's `__future__` file, but also some distinction.

The normal usage is like

    from __feature__ import <feature_name> [, ...]
    ...

Alternatively, there is the `set_selection` function which uses select_id's
and takes an optional `mod_name` parameter.

The select id `-1` has the special meaning "ignore this module".
"""

import inspect
import sys
from contextlib import contextmanager

all_feature_names = [
    "snake_case",
    "true_property",
    "_feature_04",
    "_feature_08",
    "_feature_10",
    "_feature_20",
    "_feature_40",
    "_feature_80",
]

__all__ = ["all_feature_names", "info", "reset", "set_selection"] + all_feature_names

snake_case = 0x01
true_property = 0x02
_feature_04 = 0x04
_feature_08 = 0x08
_feature_10 = 0x10
_feature_20 = 0x20
_feature_40 = 0x40
_feature_80 = 0x80

# let's remove the dummies for the normal user
_really_all_feature_names = all_feature_names[:]
all_feature_names = list(_ for _ in all_feature_names if not _.startswith("_"))

# Install an import hook that controls the `__feature__` import.
"""
Note: This are two imports.
>>> import dis
>>> def test():
...     from __feature__ import snake_case
...
>>> dis.dis(test)
  2           0 LOAD_CONST               1 (0)
              2 LOAD_CONST               2 (('snake_case',))
              4 IMPORT_NAME              0 (__feature__)
              6 IMPORT_FROM              1 (snake_case)
              8 STORE_FAST               0 (snake_case)
             10 POP_TOP
             12 LOAD_CONST               0 (None)
             14 RETURN_VALUE
"""

"""
The redirection of __import__
-----------------------------

This construction avoids irritating extra redirections in tracebacks.

The normal `__import__` is replaced by C function `__feature_import__`.
`__feature_import__` calls this `feature_import` function first, to
see if a feature is requested. If this function does not handle it, it returns
None to indicate that a normal import should be performed, and
`__feature_import__` calls the original import `__orig_import__`.
All these variables are transparently kept in module `builtins`.
"""


def feature_import(name, *args, **kwargs):
    # PYSIDE-1368: The `__name__` attribute does not need to exist in all modules.
    # PYSIDE-1398: sys._getframe(1) may not exist when embedding.
    # PYSIDE-1338: The "1" below is the redirection in loader.py .
    # PYSIDE-1548: Ensure that features are not affected by other imports.
    # PYSIDE-2029: Need to always switch. The cache was wrong interpreted.
    calling_frame = _cf = sys._getframe(1).f_back
    importing_module = _cf.f_globals.get("__name__", "__main__") if _cf else "__main__"
    existing = pyside_feature_dict.get(importing_module, 0)

    if name == "__feature__" and args[2]:
        __init__()

        # This is an `import from` statement that corresponds to `IMPORT_NAME`.
        # The following `IMPORT_FROM` will handle errors. (Confusing, ofc.)
        flag = get_select_id(args[2])

        flag |= existing & 255 if isinstance(existing, int) and existing >= 0 else 0
        pyside_feature_dict[importing_module] = flag

        if importing_module == "__main__":
            # We need to add all modules here which should see __feature__.
            pyside_feature_dict["rlcompleter"] = flag

        # Initialize feature (multiple times allowed) and clear cache.
        sys.modules["PySide6.QtCore"].__init_feature__()
        return sys.modules["__feature__"]
    # Redirect to the original import
    return None


_is_initialized = False


def __init__():
    global _is_initialized
    if not _is_initialized:
        # use _one_ recursive import...
        import PySide6.QtCore
        # Initialize all prior imported modules
        for name, module in sys.modules.items():
            if name not in pyside_feature_dict:
                pyside_feature_dict[name] = 0 if _mod_uses_pyside(module) else -1
        _is_initialized = True


def feature_imported(module):
    # PYSIDE-2029: Need to inspect imported modules for PySide usage.
    """
    Set the module feature default after import.

    A module that uses PySide has a switching default of 0 = "no feature".
    Otherwise the default is -1 = "ignore this module".
    """

    # PYSIDE-1368: The `__name__` attribute does not need to exist in all modules.
    if hasattr(module, "__name__"):
        name = module.__name__
        if name not in pyside_feature_dict:
            pyside_feature_dict[name] = 0 if _mod_uses_pyside(module) else -1


def _mod_uses_pyside(module):
    """
    Find out if this module uses PySide.

    Simple approach: Search the source code for the string "PySide6".
    Maybe we later support source-less modules by inspecting all code objects.
    """
    try:
        source = inspect.getsource(module)
    except TypeError:
        # this is a builtin module like sys
        return False
    except OSError:
        # this is a module withot source file
        return False
    except SyntaxError:
        # PYSIDE-2189: A UnicodeError happens in tokenize.py in find_cookie
        #              which is then creating a SyntaxError in inspect.
        # This is undocumented and a Python error, seen in Python 3.10.2 on Windows,
        # importing `pythoncom` of the win32 package.
        return False
    except Exception:
        # PYSIDE-2393: pytest behaves weird when allowing any other error.
        return False
    return "PySide6" in source


def set_selection(select_id, mod_name=None):
    """
    Internal use: Set the feature directly by Id.
    Id == -1: ignore this module in switching.
    """
    mod_name = mod_name or sys._getframe(1).f_globals['__name__']
    __init__()
    # Reset the features to the given id
    flag = 0
    if isinstance(select_id, int):
        flag = select_id & 255
    pyside_feature_dict[mod_name] = flag
    sys.modules["PySide6.QtCore"].__init_feature__()
    return _current_selection(flag)


# The set_section(0) case seems to be unsafe. We will migrate to
# use the opaque feature.reset() call in all test cases.
def reset():
    set_selection(0)
    pyside_feature_dict.clear()
    _is_initialized = False


def info(mod_name=None):
    """
    Internal use: Return the current selection
    """
    mod_name = mod_name or sys._getframe(1).f_globals['__name__']
    flag = pyside_feature_dict.get(mod_name, 0)
    return _current_selection(flag)


def _current_selection(flag):
    names = []
    if flag >= 0:
        for idx, name in enumerate(_really_all_feature_names):
            if (1 << idx) & flag:
                names.append(name)
    return names


def get_select_id(feature_names):
    flag = 0
    for feature in feature_names:
        if feature in _really_all_feature_names:
            flag |= globals()[feature]
        else:
            raise SyntaxError(f"PySide feature {feature} is not defined")
    return flag


@contextmanager
def force_selection(select_id, mod_name):
    """
    This function is for generating pyi files with features.
    The selection id is set globally after performing the unswitched
    import.

    """
    __init__()
    saved_feature_dict = pyside_feature_dict.copy()
    for name in pyside_feature_dict:
        set_selection(0, name)
    __import__(mod_name)
    for name in pyside_feature_dict.copy():
        set_selection(select_id, name)
    try:
        yield
    finally:
        pyside_feature_dict.update(saved_feature_dict)

#eof