File: test_inspect.py

package info (click to toggle)
pyinstaller 6.18.0%2Bds-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 11,824 kB
  • sloc: python: 41,828; ansic: 12,123; makefile: 171; sh: 131; xml: 19
file content (200 lines) | stat: -rw-r--r-- 7,953 bytes parent folder | download | duplicates (3)
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
#-----------------------------------------------------------------------------
# Copyright (c) 2021-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
#
# Tests for stdlib `inspect` module.

import pathlib

import pytest

# Directory with testing modules used in some tests.
_MODULES_DIR = pathlib.Path(__file__).parent / 'modules'


def _patch_collection_mode(monkeypatch, module_name):
    # Patch Analysis to set module_collection_mode for specified package/module.
    import PyInstaller.building.build_main

    class _Analysis(PyInstaller.building.build_main.Analysis):
        def __init__(self, *args, **kwargs):
            kwargs['module_collection_mode'] = {
                module_name: 'pyz+py',
            }
            super().__init__(*args, **kwargs)

    monkeypatch.setattr('PyInstaller.building.build_main.Analysis', _Analysis)


# Test that we can retrieve source for a module that was collected both into PYZ archive and as a source .py file.
@pytest.mark.parametrize('module_name', ['mypackage', 'mypackage.mod_a'], ids=['package', 'submodule'])
def test_inspect_getsource(pyi_builder, module_name, monkeypatch):
    pathex = _MODULES_DIR / 'pyi_inspect_getsource'
    _patch_collection_mode(monkeypatch, 'mypackage')
    pyi_builder.test_source(
        f"""
        import inspect

        import {module_name}

        # Retrieve source via module instance.
        source = inspect.getsource({module_name})
        print(source)

        # Check that the source starts with expected comment
        EXPECTED_COMMENT = "# {module_name}: "
        if not source.startswith(EXPECTED_COMMENT):
            raise ValueError(f"Source does not start with expected comment: {{EXPECTED_COMMENT}}")
        """,
        pyi_args=['--paths', str(pathex)],
    )


def test_inspect_getsource_class_from_base_library_module(pyi_builder, monkeypatch):
    _patch_collection_mode(monkeypatch, 'enum')
    pyi_builder.test_source(
        """
        import sys
        import os
        import enum
        import inspect

        # Ensure that module is collected in `base_library.zip`; normalize separators before comparison.
        BASE_LIBRARY_ZIP = os.path.normpath(os.path.join(sys._MEIPASS, 'base_library.zip'))
        if not enum.__file__.startswith(BASE_LIBRARY_ZIP):
            raise ValueError(f"The 'enum' module is not collected in base_library.zip: {enum.__file__}")

        # Retrieve source via class (enum.Enum)
        source = inspect.getsource(enum.Enum)
        print(source)
        """
    )


def test_inspect_getsource_class_method_from_base_library_module(pyi_builder, monkeypatch):
    _patch_collection_mode(monkeypatch, 'enum')
    pyi_builder.test_source(
        """
        import sys
        import os
        import enum
        import inspect

        # Ensure that module is collected in `base_library.zip`; normalize separators before comparison.
        BASE_LIBRARY_ZIP = os.path.normpath(os.path.join(sys._MEIPASS, 'base_library.zip'))
        if not enum.__file__.startswith(BASE_LIBRARY_ZIP):
            raise ValueError(f"The 'enum' module is not collected in base_library.zip: {enum.__file__}")

        # Retrieve source via class method (enum.Enum.__new__)
        source = inspect.getsource(enum.Enum.__new__)
        print(source)
        """
    )


# Similar to `test_inspect_getsource`, except that we are retrieving source for a function within the module.
def test_inspect_getsource_function(pyi_builder, monkeypatch):
    pathex = _MODULES_DIR / 'pyi_inspect_getsource'
    _patch_collection_mode(monkeypatch, 'mypackage')
    pyi_builder.test_source(
        """
        import inspect

        from mypackage.mod_b import test_function

        # Retrieve source for function.
        source = inspect.getsource(test_function)
        print(source)

        # Check that the source starts with function definition
        EXPECTED_START = "def test_function():"
        if not source.startswith(EXPECTED_START):
            raise ValueError(f"Source does not start with function definition: {EXPECTED_START}")

        # Check that the comment is preset.
        EXPECTED_COMMENT = "# A comment."
        if EXPECTED_COMMENT not in source:
            raise ValueError(f"Source does not contain expected comment: {EXPECTED_COMMENT}")
        """,
        pyi_args=['--paths', str(pathex)],
    )


# Test inspect.getmodule() on stack-frames obtained by inspect.stack(). Reproduces the issue reported by #5963 while
# expanding the test to cover a package and its submodule in addition to the __main__ module.
def test_inspect_getmodule_from_stackframes(pyi_builder):
    pathex = _MODULES_DIR / 'pyi_inspect_getmodule_from_stackframes'
    # NOTE: run_from_path MUST be True, otherwise cwd + rel_path coincides with sys._MEIPASS + rel_path and masks the
    # path resolving issue in onedir builds.
    pyi_builder.test_source(
        """
        import helper_package

        # helper_package.test_call_chain() calls eponymous function in helper_package.helper_module, which in turn uses
        # inspect.stack() and inspect.getmodule() to obtain list of modules involved in the chain call.
        modules = helper_package.test_call_chain()

        # Expected call chain
        expected_module_names = [
            'helper_package.helper_module',
            'helper_package',
            '__main__'
        ]

        # All modules must have been resolved
        assert not any(module is None for module in modules)

        # Verify module names
        module_names = [module.__name__ for module in modules]
        assert module_names == expected_module_names
        """,
        pyi_args=['--paths', str(pathex)],
        run_from_path=True
    )


# Test the robustness of `inspect` run-time hook w.r.t. to the issue #7642.
#
# If our run-time hook imports a module in the global namespace and attempts to use this module in a function that
# might get called later on in the program (e.g., a function override or registered callback function), we are at the
# mercy of user's program, which might re-bind the module's name to something else (variable, function), leading to
# an error.
#
# This particular test will raise:
# ```
# Traceback (most recent call last):
#  File "test_source.py", line 17, in <module>
#  File "test_source.py", line 14, in some_interactive_debugger_function
#  File "inspect.py", line 1755, in stack
#  File "inspect.py", line 1730, in getouterframes
#  File "inspect.py", line 1688, in getframeinfo
#  File "PyInstaller/hooks/rthooks/pyi_rth_inspect.py", line 22, in _pyi_getsourcefile
# AttributeError: 'function' object has no attribute 'getfile'
# ```
def test_inspect_rthook_robustness(pyi_builder):
    pyi_builder.test_source(
        """
        # A custom function in global namespace that happens to have name clash with `inspect` module.
        def inspect(something):
            print(f"Inspecting {something}: type is {type(something)}")


        # A call to `inspect.stack` function somewhere deep in an interactive debugger framework.
        # This eventually ends up calling our `_pyi_getsourcefile` override in the `inspect` run-time hook. The
        # override calls `inspect.getfile`; if the run-time hook imported `inspect` in a global namespace, the
        # name at this point is bound the the custom function that program defined, leading to an error.
        def some_interactive_debugger_function():
            import inspect
            print(f"Current stack: {inspect.stack()}")


        some_interactive_debugger_function()
        """
    )