xref: /aosp_15_r20/prebuilts/build-tools/common/py3-stdlib/runpy.py (revision cda5da8d549138a6648c5ee6d7a49cf8f4a657be)
1"""runpy.py - locating and running Python code using the module namespace
2
3Provides support for locating and running Python scripts using the Python
4module namespace instead of the native filesystem.
5
6This allows Python code to play nicely with non-filesystem based PEP 302
7importers when locating support scripts as well as when importing modules.
8"""
9# Written by Nick Coghlan <ncoghlan at gmail.com>
10#    to implement PEP 338 (Executing Modules as Scripts)
11
12
13import sys
14import importlib.machinery # importlib first so we can test #15386 via -m
15import importlib.util
16import io
17import os
18
19__all__ = [
20    "run_module", "run_path",
21]
22
23# avoid 'import types' just for ModuleType
24ModuleType = type(sys)
25
26class _TempModule(object):
27    """Temporarily replace a module in sys.modules with an empty namespace"""
28    def __init__(self, mod_name):
29        self.mod_name = mod_name
30        self.module = ModuleType(mod_name)
31        self._saved_module = []
32
33    def __enter__(self):
34        mod_name = self.mod_name
35        try:
36            self._saved_module.append(sys.modules[mod_name])
37        except KeyError:
38            pass
39        sys.modules[mod_name] = self.module
40        return self
41
42    def __exit__(self, *args):
43        if self._saved_module:
44            sys.modules[self.mod_name] = self._saved_module[0]
45        else:
46            del sys.modules[self.mod_name]
47        self._saved_module = []
48
49class _ModifiedArgv0(object):
50    def __init__(self, value):
51        self.value = value
52        self._saved_value = self._sentinel = object()
53
54    def __enter__(self):
55        if self._saved_value is not self._sentinel:
56            raise RuntimeError("Already preserving saved value")
57        self._saved_value = sys.argv[0]
58        sys.argv[0] = self.value
59
60    def __exit__(self, *args):
61        self.value = self._sentinel
62        sys.argv[0] = self._saved_value
63
64# TODO: Replace these helpers with importlib._bootstrap_external functions.
65def _run_code(code, run_globals, init_globals=None,
66              mod_name=None, mod_spec=None,
67              pkg_name=None, script_name=None):
68    """Helper to run code in nominated namespace"""
69    if init_globals is not None:
70        run_globals.update(init_globals)
71    if mod_spec is None:
72        loader = None
73        fname = script_name
74        cached = None
75    else:
76        loader = mod_spec.loader
77        fname = mod_spec.origin
78        cached = mod_spec.cached
79        if pkg_name is None:
80            pkg_name = mod_spec.parent
81    run_globals.update(__name__ = mod_name,
82                       __file__ = fname,
83                       __cached__ = cached,
84                       __doc__ = None,
85                       __loader__ = loader,
86                       __package__ = pkg_name,
87                       __spec__ = mod_spec)
88    exec(code, run_globals)
89    return run_globals
90
91def _run_module_code(code, init_globals=None,
92                    mod_name=None, mod_spec=None,
93                    pkg_name=None, script_name=None):
94    """Helper to run code in new namespace with sys modified"""
95    fname = script_name if mod_spec is None else mod_spec.origin
96    with _TempModule(mod_name) as temp_module, _ModifiedArgv0(fname):
97        mod_globals = temp_module.module.__dict__
98        _run_code(code, mod_globals, init_globals,
99                  mod_name, mod_spec, pkg_name, script_name)
100    # Copy the globals of the temporary module, as they
101    # may be cleared when the temporary module goes away
102    return mod_globals.copy()
103
104# Helper to get the full name, spec and code for a module
105def _get_module_details(mod_name, error=ImportError):
106    if mod_name.startswith("."):
107        raise error("Relative module names not supported")
108    pkg_name, _, _ = mod_name.rpartition(".")
109    if pkg_name:
110        # Try importing the parent to avoid catching initialization errors
111        try:
112            __import__(pkg_name)
113        except ImportError as e:
114            # If the parent or higher ancestor package is missing, let the
115            # error be raised by find_spec() below and then be caught. But do
116            # not allow other errors to be caught.
117            if e.name is None or (e.name != pkg_name and
118                    not pkg_name.startswith(e.name + ".")):
119                raise
120        # Warn if the module has already been imported under its normal name
121        existing = sys.modules.get(mod_name)
122        if existing is not None and not hasattr(existing, "__path__"):
123            from warnings import warn
124            msg = "{mod_name!r} found in sys.modules after import of " \
125                "package {pkg_name!r}, but prior to execution of " \
126                "{mod_name!r}; this may result in unpredictable " \
127                "behaviour".format(mod_name=mod_name, pkg_name=pkg_name)
128            warn(RuntimeWarning(msg))
129
130    try:
131        spec = importlib.util.find_spec(mod_name)
132    except (ImportError, AttributeError, TypeError, ValueError) as ex:
133        # This hack fixes an impedance mismatch between pkgutil and
134        # importlib, where the latter raises other errors for cases where
135        # pkgutil previously raised ImportError
136        msg = "Error while finding module specification for {!r} ({}: {})"
137        if mod_name.endswith(".py"):
138            msg += (f". Try using '{mod_name[:-3]}' instead of "
139                    f"'{mod_name}' as the module name.")
140        raise error(msg.format(mod_name, type(ex).__name__, ex)) from ex
141    if spec is None:
142        raise error("No module named %s" % mod_name)
143    if spec.submodule_search_locations is not None:
144        if mod_name == "__main__" or mod_name.endswith(".__main__"):
145            raise error("Cannot use package as __main__ module")
146        try:
147            pkg_main_name = mod_name + ".__main__"
148            return _get_module_details(pkg_main_name, error)
149        except error as e:
150            if mod_name not in sys.modules:
151                raise  # No module loaded; being a package is irrelevant
152            raise error(("%s; %r is a package and cannot " +
153                               "be directly executed") %(e, mod_name))
154    loader = spec.loader
155    if loader is None:
156        raise error("%r is a namespace package and cannot be executed"
157                                                                 % mod_name)
158    try:
159        code = loader.get_code(mod_name)
160    except ImportError as e:
161        raise error(format(e)) from e
162    if code is None:
163        raise error("No code object available for %s" % mod_name)
164    return mod_name, spec, code
165
166class _Error(Exception):
167    """Error that _run_module_as_main() should report without a traceback"""
168
169# XXX ncoghlan: Should this be documented and made public?
170# (Current thoughts: don't repeat the mistake that lead to its
171# creation when run_module() no longer met the needs of
172# mainmodule.c, but couldn't be changed because it was public)
173def _run_module_as_main(mod_name, alter_argv=True):
174    """Runs the designated module in the __main__ namespace
175
176       Note that the executed module will have full access to the
177       __main__ namespace. If this is not desirable, the run_module()
178       function should be used to run the module code in a fresh namespace.
179
180       At the very least, these variables in __main__ will be overwritten:
181           __name__
182           __file__
183           __cached__
184           __loader__
185           __package__
186    """
187    try:
188        if alter_argv or mod_name != "__main__": # i.e. -m switch
189            mod_name, mod_spec, code = _get_module_details(mod_name, _Error)
190        else:          # i.e. directory or zipfile execution
191            mod_name, mod_spec, code = _get_main_module_details(_Error)
192    except _Error as exc:
193        msg = "%s: %s" % (sys.executable, exc)
194        sys.exit(msg)
195    main_globals = sys.modules["__main__"].__dict__
196    if alter_argv:
197        sys.argv[0] = mod_spec.origin
198    return _run_code(code, main_globals, None,
199                     "__main__", mod_spec)
200
201def run_module(mod_name, init_globals=None,
202               run_name=None, alter_sys=False):
203    """Execute a module's code without importing it.
204
205       mod_name -- an absolute module name or package name.
206
207       Optional arguments:
208       init_globals -- dictionary used to pre-populate the module’s
209       globals dictionary before the code is executed.
210
211       run_name -- if not None, this will be used for setting __name__;
212       otherwise, __name__ will be set to mod_name + '__main__' if the
213       named module is a package and to just mod_name otherwise.
214
215       alter_sys -- if True, sys.argv[0] is updated with the value of
216       __file__ and sys.modules[__name__] is updated with a temporary
217       module object for the module being executed. Both are
218       restored to their original values before the function returns.
219
220       Returns the resulting module globals dictionary.
221    """
222    mod_name, mod_spec, code = _get_module_details(mod_name)
223    if run_name is None:
224        run_name = mod_name
225    if alter_sys:
226        return _run_module_code(code, init_globals, run_name, mod_spec)
227    else:
228        # Leave the sys module alone
229        return _run_code(code, {}, init_globals, run_name, mod_spec)
230
231def _get_main_module_details(error=ImportError):
232    # Helper that gives a nicer error message when attempting to
233    # execute a zipfile or directory by invoking __main__.py
234    # Also moves the standard __main__ out of the way so that the
235    # preexisting __loader__ entry doesn't cause issues
236    main_name = "__main__"
237    saved_main = sys.modules[main_name]
238    del sys.modules[main_name]
239    try:
240        return _get_module_details(main_name)
241    except ImportError as exc:
242        if main_name in str(exc):
243            raise error("can't find %r module in %r" %
244                              (main_name, sys.path[0])) from exc
245        raise
246    finally:
247        sys.modules[main_name] = saved_main
248
249
250def _get_code_from_file(run_name, fname):
251    # Check for a compiled file first
252    from pkgutil import read_code
253    decoded_path = os.path.abspath(os.fsdecode(fname))
254    with io.open_code(decoded_path) as f:
255        code = read_code(f)
256    if code is None:
257        # That didn't work, so try it as normal source code
258        with io.open_code(decoded_path) as f:
259            code = compile(f.read(), fname, 'exec')
260    return code, fname
261
262def run_path(path_name, init_globals=None, run_name=None):
263    """Execute code located at the specified filesystem location.
264
265       path_name -- filesystem location of a Python script, zipfile,
266       or directory containing a top level __main__.py script.
267
268       Optional arguments:
269       init_globals -- dictionary used to pre-populate the module’s
270       globals dictionary before the code is executed.
271
272       run_name -- if not None, this will be used to set __name__;
273       otherwise, '<run_path>' will be used for __name__.
274
275       Returns the resulting module globals dictionary.
276    """
277    if run_name is None:
278        run_name = "<run_path>"
279    pkg_name = run_name.rpartition(".")[0]
280    from pkgutil import get_importer
281    importer = get_importer(path_name)
282    # Trying to avoid importing imp so as to not consume the deprecation warning.
283    is_NullImporter = False
284    if type(importer).__module__ == 'imp':
285        if type(importer).__name__ == 'NullImporter':
286            is_NullImporter = True
287    if isinstance(importer, type(None)) or is_NullImporter:
288        # Not a valid sys.path entry, so run the code directly
289        # execfile() doesn't help as we want to allow compiled files
290        code, fname = _get_code_from_file(run_name, path_name)
291        return _run_module_code(code, init_globals, run_name,
292                                pkg_name=pkg_name, script_name=fname)
293    else:
294        # Finder is defined for path, so add it to
295        # the start of sys.path
296        sys.path.insert(0, path_name)
297        try:
298            # Here's where things are a little different from the run_module
299            # case. There, we only had to replace the module in sys while the
300            # code was running and doing so was somewhat optional. Here, we
301            # have no choice and we have to remove it even while we read the
302            # code. If we don't do this, a __loader__ attribute in the
303            # existing __main__ module may prevent location of the new module.
304            mod_name, mod_spec, code = _get_main_module_details()
305            with _TempModule(run_name) as temp_module, \
306                 _ModifiedArgv0(path_name):
307                mod_globals = temp_module.module.__dict__
308                return _run_code(code, mod_globals, init_globals,
309                                    run_name, mod_spec, pkg_name).copy()
310        finally:
311            try:
312                sys.path.remove(path_name)
313            except ValueError:
314                pass
315
316
317if __name__ == "__main__":
318    # Run the module specified as the next command line argument
319    if len(sys.argv) < 2:
320        print("No module specified for execution", file=sys.stderr)
321    else:
322        del sys.argv[0] # Make the requested module sys.argv[0]
323        _run_module_as_main(sys.argv[0])
324