Skip to content
Open
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
6 changes: 3 additions & 3 deletions Doc/c-api/init_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1807,10 +1807,10 @@ PyConfig

.. c:member:: wchar_t* run_presite

``package.module`` path to module that should be imported before
``site.py`` is run.
``module`` or ``module:func`` entry point that should be executed before
the :mod:`site` module is imported.

Set by the :option:`-X presite=package.module <-X>` command-line
Set by the :option:`-X presite=module:func <-X>` command-line
option and the :envvar:`PYTHON_PRESITE` environment variable.
The command-line option takes precedence.

Expand Down
11 changes: 9 additions & 2 deletions Doc/using/cmdline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -654,13 +654,17 @@ Miscellaneous options

.. versionadded:: 3.13

* :samp:`-X presite={package.module}` specifies a module that should be
imported before the :mod:`site` module is executed and before the
* :samp:`-X presite={module}` or :samp:`-X presite={module:func}` specifies
an entry point that should be executed before the :mod:`site` module is
executed and before the
:mod:`__main__` module exists. Therefore, the imported module isn't
:mod:`__main__`. This can be used to execute code early during Python
initialization. Python needs to be :ref:`built in debug mode <debug-build>`
for this option to exist. See also :envvar:`PYTHON_PRESITE`.

.. versionchanged:: next
Accept also ``module:func`` entry point format.

.. versionadded:: 3.13

* :samp:`-X gil={0,1}` forces the GIL to be disabled or enabled,
Expand Down Expand Up @@ -1451,4 +1455,7 @@ Debug-mode variables

Needs Python configured with the :option:`--with-pydebug` build option.

.. versionchanged:: next
Accept also ``module:func`` entry point format.

.. versionadded:: 3.13
29 changes: 17 additions & 12 deletions Lib/test/_test_embed_structseq.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,21 @@ def test_sys_funcs(self):
self.check_structseq(type(obj))


try:
unittest.main(
module=(
'__main__'
if __name__ == '__main__'
# Avoiding a circular import:
else sys.modules['test._test_embed_structseq']
def main():
try:
unittest.main(
module=(
'__main__'
if __name__ == '__main__'
# Avoiding a circular import:
else sys.modules['test._test_embed_structseq']
)
)
)
except SystemExit as exc:
if exc.args[0] != 0:
raise
print("Tests passed")
except SystemExit as exc:
if exc.args[0] != 0:
raise
print("Tests passed")


if __name__ == "__main__":
main()
8 changes: 4 additions & 4 deletions Lib/test/cov.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
"""A minimal hook for gathering line coverage of the standard library.
Designed to be used with -Xpresite= which means:
* it installs itself on import
* it's not imported as `__main__` so can't use the ifmain idiom
Designed to be used with -Xpresite=test.cov:enable which means:
* it can't import anything besides `sys` to avoid tainting gathered coverage
* filenames are not normalized
Expand Down Expand Up @@ -45,4 +44,5 @@ def disable():
mon.free_tool_id(mon.COVERAGE_ID)


enable()
if __name__ == "__main__":
enable()
2 changes: 1 addition & 1 deletion Lib/test/libregrtest/runtests.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def create_python_cmd(self) -> list[str]:
if '-u' not in python_opts:
cmd.append('-u') # Unbuffered stdout and stderr
if self.coverage:
cmd.append("-Xpresite=test.cov")
cmd.append("-Xpresite=test.cov:enable")
return cmd

def bisect_cmd_args(self) -> list[str]:
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1396,7 +1396,7 @@ def trace_wrapper(*args, **kwargs):
sys.settrace(original_trace)

coverage_wrapper = trace_wrapper
if 'test.cov' in sys.modules: # -Xpresite=test.cov used
if 'test.cov' in sys.modules: # -Xpresite=test.cov:enable used
cov = sys.monitoring.COVERAGE_ID
@functools.wraps(func)
def coverage_wrapper(*args, **kwargs):
Expand Down
22 changes: 22 additions & 0 deletions Lib/test/test_cmd_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ def _kill_python_and_exit_code(p):
return data, returncode


def presite_func():
print("presite func")

class Namespace:
pass

presite = Namespace()
presite.attr = Namespace()
presite.attr.func = presite_func


class CmdLineTest(unittest.TestCase):
def test_directories(self):
assert_python_failure('.')
Expand Down Expand Up @@ -1266,6 +1277,17 @@ def test_invalid_thread_local_bytecode(self):
rc, out, err = assert_python_failure(PYTHON_TLBC="2")
self.assertIn(b"PYTHON_TLBC=N: N is missing or invalid", err)

@unittest.skipUnless(support.Py_DEBUG,
'-X presite requires a Python debug build')
def test_presite(self):
entrypoint = "test.test_cmd_line:presite_func"
proc = assert_python_ok("-X", f"presite={entrypoint}", "-c", "pass")
self.assertEqual(proc.out.rstrip(), b"presite func")

entrypoint = "test.test_cmd_line:presite.attr.func"
proc = assert_python_ok("-X", f"presite={entrypoint}", "-c", "pass")
self.assertEqual(proc.out.rstrip(), b"presite func")


@unittest.skipIf(interpreter_requires_environment(),
'Cannot run -I tests when PYTHON env vars are required.')
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -2051,7 +2051,7 @@ def test_no_memleak(self):
def test_presite(self):
cmd = [
sys.executable,
"-I", "-X", "presite=test._test_embed_structseq",
"-I", "-X", "presite=test._test_embed_structseq:main",
"-c", "print('unique-python-message')",
]
proc = subprocess.run(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Accept a function name in :option:`-X presite <-X>` command line option and
:envvar:`PYTHON_PRESITE` environment variable. Patch by Victor Stinner.
118 changes: 106 additions & 12 deletions Python/pylifecycle.c
Original file line number Diff line number Diff line change
Expand Up @@ -1218,6 +1218,54 @@ pyinit_main_reconfigure(PyThreadState *tstate)


#ifdef Py_DEBUG
// Equivalent to the Python code:
//
// for part in attr.split('.'):
// obj = getattr(obj, part)
static PyObject*
presite_resolve_name(PyObject *obj, PyObject *attr)
{
obj = Py_NewRef(obj);
attr = Py_NewRef(attr);
PyObject *res;

while (1) {
Py_ssize_t len = PyUnicode_GET_LENGTH(attr);
Py_ssize_t pos = PyUnicode_FindChar(attr, '.', 0, len, 1);
if (pos < 0) {
break;
}

PyObject *name = PyUnicode_Substring(attr, 0, pos);
if (name == NULL) {
goto error;
}
res = PyObject_GetAttr(obj, name);
Py_DECREF(name);
if (res == NULL) {
goto error;
}
Py_SETREF(obj, res);

PyObject *suffix = PyUnicode_Substring(attr, pos + 1, len);
if (suffix == NULL) {
goto error;
}
Py_SETREF(attr, suffix);
}

res = PyObject_GetAttr(obj, attr);
Py_DECREF(obj);
Py_DECREF(attr);
return res;

error:
Py_XDECREF(obj);
Py_DECREF(attr);
return NULL;
}


static void
run_presite(PyThreadState *tstate)
{
Expand All @@ -1228,22 +1276,68 @@ run_presite(PyThreadState *tstate)
return;
}

PyObject *presite_modname = PyUnicode_FromWideChar(
config->run_presite,
wcslen(config->run_presite)
);
if (presite_modname == NULL) {
fprintf(stderr, "Could not convert pre-site module name to unicode\n");
PyObject *presite = PyUnicode_FromWideChar(config->run_presite, -1);
if (presite == NULL) {
fprintf(stderr, "Could not convert pre-site command to Unicode\n");
_PyErr_Print(tstate);
return;
}

// Accept "mod_name" and "mod_name:func_name" entry point syntax
Py_ssize_t len = PyUnicode_GET_LENGTH(presite);
Py_ssize_t pos = PyUnicode_FindChar(presite, ':', 0, len, 1);
PyObject *mod_name = NULL;
PyObject *func_name = NULL;
PyObject *module = NULL;
if (pos > 0) {
mod_name = PyUnicode_Substring(presite, 0, pos);
if (mod_name == NULL) {
goto error;
}

func_name = PyUnicode_Substring(presite, pos + 1, len);
if (func_name == NULL) {
goto error;
}
}
else {
PyObject *presite = PyImport_Import(presite_modname);
if (presite == NULL) {
fprintf(stderr, "pre-site import failed:\n");
_PyErr_Print(tstate);
mod_name = Py_NewRef(presite);
}

// mod_name can contain dots (ex: "math.integer")
module = PyImport_Import(mod_name);
if (module == NULL) {
goto error;
}

if (func_name != NULL) {
PyObject *func = presite_resolve_name(module, func_name);
if (func == NULL) {
goto error;
}

PyObject *res = PyObject_CallNoArgs(func);
Py_DECREF(func);
if (res == NULL) {
goto error;
}
Py_XDECREF(presite);
Py_DECREF(presite_modname);
Py_DECREF(res);
}

Py_DECREF(presite);
Py_DECREF(mod_name);
Py_XDECREF(func_name);
Py_DECREF(module);
return;

error:
fprintf(stderr, "pre-site failed:\n");
_PyErr_Print(tstate);

Py_DECREF(presite);
Py_XDECREF(mod_name);
Py_XDECREF(func_name);
Py_XDECREF(module);
}
#endif

Expand Down
Loading