Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Doc/c-api/module.rst
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,30 @@ The available slot types are:
If multiple ``Py_mod_exec`` slots are specified, they are processed in the
order they appear in the *m_slots* array.

When an extension module is executed as a script using Python's
:ref:`-m option <m option>`, the function(s) specified by its
``Py_mod_exec`` slots are executed on the ``__main__`` module.
To handle this case, the function can check if the module's ``__name__``
is ``"__main__"``, and react accordingly. For example:

.. code-block:: C

static int exec_module(PyObject *m)
{
const char *name = PyModule_GetName(m);
if (name == NULL) {
return -1;
}

if (!strcmp(name, "__main__")) {
printf("Running as a script");
}
return 0;
}

The ``-m`` option is not supported for modules that also define the
:c:data:`Py_mod_create` slot.

See :PEP:`489` for more details on multi-phase initialization.

Low-level module creation functions
Expand Down
15 changes: 15 additions & 0 deletions Doc/library/importlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,21 @@ find and load modules.

.. versionadded:: 3.5

.. method:: exec_in_module(spec, module)

Initializes *module* according to *spec*.
Information stored in the given *module* (e.g. its ``__spec__``
and ``__name__`` attributes) is ignored, and does not need to match
the given *spec*.

This is used by code that handles Python's :ref:`-m option <m option>`,
which initializes the ``__main__`` module according to the requested
module's *spec*.
(For loaders that do not support ``exec_in_module``, the ``-m`` option
uses :meth:`get_code` as fallback.)

.. versionadded:: 3.7

.. method:: is_package(fullname)

Returns ``True`` if the file path points to a package's ``__init__``
Expand Down
14 changes: 10 additions & 4 deletions Doc/using/cmdline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ source.
level modules).


.. _m option:

.. cmdoption:: -m <module-name>

Search :data:`sys.path` for the named module and execute its contents as
Expand All @@ -90,10 +92,10 @@ source.

.. note::

This option cannot be used with built-in modules and extension modules
written in C, since they do not have Python module files. However, it
can still be used for precompiled modules, even if the original source
file is not available.
Many built-in modules and extension modules written in C do not support
being run with the -m option.
The option can be used for precompiled modules, even if the original
source file is not available.

If this option is given, the first element of :data:`sys.argv` will be the
full path to the module file (while the module file is being located, the
Expand All @@ -119,6 +121,10 @@ source.
.. versionchanged:: 3.4
namespace packages are also supported

.. versionchanged:: 3.7
The ``-m`` option can now be used with extension and built-in modules
that support :c:data:`multi-phase initialization <Py_mod_exec>`.


.. describe:: -

Expand Down
4 changes: 4 additions & 0 deletions Include/modsupport.h
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ PyAPI_FUNC(int) PyModule_AddFunctions(PyObject *, PyMethodDef *);
PyAPI_FUNC(int) PyModule_ExecDef(PyObject *module, PyModuleDef *def);
#endif

#if !defined(Py_LIMITED_API)
PyAPI_FUNC(int) PyModule_ExecInModule(PyObject *module, PyModuleDef *def);
#endif

#define Py_CLEANUP_SUPPORTED 0x20000

#define PYTHON_API_VERSION 1013
Expand Down
6 changes: 6 additions & 0 deletions Lib/importlib/_bootstrap_external.py
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,12 @@ def exec_module(self, module):
_bootstrap._verbose_message('extension module {!r} executed from {!r}',
self.name, self.path)

def exec_in_module(self, spec, module):
_bootstrap._call_with_frames_removed(
_imp.exec_in_module, spec, module)
_bootstrap._verbose_message('extension module {!r} executed from {!r}',
self.name, self.path)

def is_package(self, fullname):
"""Return True if the extension module is a package."""
file_name = _path_split(self.path)[1]
Expand Down
44 changes: 36 additions & 8 deletions Lib/runpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,9 @@ def __exit__(self, *args):
sys.argv[0] = self._saved_value

# TODO: Replace these helpers with importlib._bootstrap_external functions.
def _run_code(code, run_globals, init_globals=None,
def _init_module_globals(run_globals, init_globals=None,
mod_name=None, mod_spec=None,
pkg_name=None, script_name=None):
"""Helper to run code in nominated namespace"""
if init_globals is not None:
run_globals.update(init_globals)
if mod_spec is None:
Expand All @@ -82,6 +81,15 @@ def _run_code(code, run_globals, init_globals=None,
__loader__ = loader,
__package__ = pkg_name,
__spec__ = mod_spec)
return run_globals

def _run_code(code, run_globals, init_globals=None,
mod_name=None, mod_spec=None,
pkg_name=None, script_name=None):
"""Helper to run code in nominated namespace"""
run_globals = _init_module_globals(run_globals, init_globals,
mod_name, mod_spec,
pkg_name, script_name)
exec(code, run_globals)
return run_globals

Expand Down Expand Up @@ -149,13 +157,16 @@ def _get_module_details(mod_name, error=ImportError):
if loader is None:
raise error("%r is a namespace package and cannot be executed"
% mod_name)
return mod_name, spec

def _get_code(mod_name, loader, error=ImportError):
try:
code = loader.get_code(mod_name)
except ImportError as e:
raise error(format(e)) from e
if code is None:
raise error("No code object available for %s" % mod_name)
return mod_name, spec, code
return code

class _Error(Exception):
"""Error that _run_module_as_main() should report without a traceback"""
Expand All @@ -180,15 +191,29 @@ def _run_module_as_main(mod_name, alter_argv=True):
"""
try:
if alter_argv or mod_name != "__main__": # i.e. -m switch
mod_name, mod_spec, code = _get_module_details(mod_name, _Error)
mod_name, mod_spec = _get_module_details(mod_name, _Error)
else: # i.e. directory or zipfile execution
mod_name, mod_spec, code = _get_main_module_details(_Error)
mod_name, mod_spec = _get_main_module_details(_Error)
except _Error as exc:
msg = "%s: %s" % (sys.executable, exc)
sys.exit(msg)
main_globals = sys.modules["__main__"].__dict__
if alter_argv:
sys.argv[0] = mod_spec.origin
if mod_spec.loader is not None and hasattr(mod_spec.loader, "exec_in_module"):
main_globals = _init_module_globals(main_globals, None,
"__main__", mod_spec)
try:
mod_spec.loader.exec_in_module(mod_spec, sys.modules["__main__"])
except ImportError as exc:
msg = "%s: %s" % (sys.executable, exc)
sys.exit(msg)
return main_globals
try:
code = _get_code(mod_name, mod_spec.loader, error=_Error)
except _Error as exc:
msg = "%s: %s" % (sys.executable, exc)
sys.exit(msg)
return _run_code(code, main_globals, None,
"__main__", mod_spec)

Expand All @@ -198,7 +223,8 @@ def run_module(mod_name, init_globals=None,

Returns the resulting top level namespace dictionary
"""
mod_name, mod_spec, code = _get_module_details(mod_name)
mod_name, mod_spec = _get_module_details(mod_name)
code = _get_code(mod_name, mod_spec.loader)
if run_name is None:
run_name = mod_name
if alter_sys:
Expand All @@ -216,7 +242,8 @@ def _get_main_module_details(error=ImportError):
saved_main = sys.modules[main_name]
del sys.modules[main_name]
try:
return _get_module_details(main_name)
main_name, mod_spec = _get_module_details(main_name)
return main_name, mod_spec
except ImportError as exc:
if main_name in str(exc):
raise error("can't find %r module in %r" %
Expand Down Expand Up @@ -272,7 +299,8 @@ def run_path(path_name, init_globals=None, run_name=None):
# have no choice and we have to remove it even while we read the
# code. If we don't do this, a __loader__ attribute in the
# existing __main__ module may prevent location of the new module.
mod_name, mod_spec, code = _get_main_module_details()
mod_name, mod_spec = _get_main_module_details()
code = _get_code(mod_name, mod_spec.loader)
with _TempModule(run_name) as temp_module, \
_ModifiedArgv0(path_name):
mod_globals = temp_module.module.__dict__
Expand Down
21 changes: 21 additions & 0 deletions Lib/test/test_cmd_line_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,26 @@ def test_package_recursion(self):
launch_name = _make_launch_script(script_dir, 'launch', 'test_pkg')
self._check_import_error(launch_name, msg)

def test_extension(self):
rc, out, err = assert_python_ok('-m', '_testmultiphase', *example_args, __isolated=False)
expected = "This is a test module named __main__.\n"
self.assertEqual(expected.encode('utf-8'), out)

def test_extension_module_state(self):
code = textwrap.dedent("""\
import sys
import runpy
import _testmultiphase
runpy._run_module_as_main('_testmultiphase')
store_int(123)
_testmultiphase.store_int(456)
print(load_int(), _testmultiphase.load_int(), file=sys.stderr)
""")
rc, out, err = assert_python_ok('-c', code, *example_args, __isolated=False)
self.assertEqual(b'This is a test module named __main__.\n', out)
self.assertEqual(b'123 456', err)


def test_issue8202(self):
# Make sure package __init__ modules see "-m" in sys.argv0 while
# searching for the module to execute
Expand Down Expand Up @@ -426,6 +446,7 @@ def test_dash_m_errors(self):
# Exercise error reporting for various invalid package executions
tests = (
('builtins', br'No code object available'),
('math', br'This module cannot be directly executed'),
('builtins.x', br'Error while finding module specification.*'
br'ModuleNotFoundError'),
('builtins.x.y', br'Error while finding module specification.*'
Expand Down
61 changes: 60 additions & 1 deletion Modules/_testmultiphase.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

#include "Python.h"
#include <stdio.h>

/* Example objects */
typedef struct {
Expand Down Expand Up @@ -154,6 +155,43 @@ call_state_registration_func(PyObject *mod, PyObject *args)
Py_RETURN_NONE;
}

PyDoc_STRVAR(store_int_doc,
"store_int(i)\n\
\n\
Store an int inside the module state");

static PyObject *
store_int(PyObject *self, PyObject *args) {
int to_store;
int *module_state = PyModule_GetState(self);
if (module_state == NULL) {
PyErr_Format(PyExc_SystemError,
"Module state is NULL.");
return NULL;
}
if (!PyArg_ParseTuple(args, "i:store_int", &to_store))
return NULL;
*module_state = to_store;
Py_RETURN_NONE;
}

PyDoc_STRVAR(load_int_doc,
"load_int()\n\
\n\
Load an int from the module state");

static PyObject *
load_int(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(args, ""))
return NULL;
int *module_state = PyModule_GetState(self);
if (module_state == NULL) {
PyErr_Format(PyExc_SystemError,
"Module state is NULL.");
return NULL;
}
return PyLong_FromLong(*module_state);
}

static PyType_Slot Str_Type_slots[] = {
{Py_tp_base, NULL}, /* filled out in module exec function */
Expand All @@ -173,12 +211,20 @@ static PyMethodDef testexport_methods[] = {
testexport_foo_doc},
{"call_state_registration_func", call_state_registration_func,
METH_VARARGS, call_state_registration_func_doc},
{"store_int", store_int,
METH_VARARGS, store_int_doc},
{"load_int", load_int,
METH_VARARGS, load_int_doc},
{NULL, NULL} /* sentinel */
};

static int execfunc(PyObject *m)
{
PyObject *temp = NULL;
const char *name = PyModule_GetName(m);
if (name == NULL) {
goto fail;
}

/* Due to cross platform compiler issues the slots must be filled
* here. It's required for portability to Windows without requiring
Expand Down Expand Up @@ -211,6 +257,9 @@ static int execfunc(PyObject *m)

if (PyModule_AddStringConstant(m, "str_const", "something different") != 0)
goto fail;
if (!strcmp(name, "__main__")) {
printf("This is a test module named %s.\n", name);
}

return 0;
fail:
Expand All @@ -235,7 +284,17 @@ PyModuleDef_Slot main_slots[] = {
{0, NULL},
};

static PyModuleDef main_def = TEST_MODULE_DEF("main", main_slots, testexport_methods);
static PyModuleDef main_def = {
PyModuleDef_HEAD_INIT, /* m_base */
"main", /* m_name */
PyDoc_STR("Test module main"), /* m_doc */
sizeof(int), /* m_size */
testexport_methods, /* m_methods */
main_slots, /* m_slots */
NULL, /* m_traverse */
NULL, /* m_clear */
NULL, /* m_free */
};

PyMODINIT_FUNC
PyInit__testmultiphase(PyObject *spec)
Expand Down
Loading