https://github.com/python/cpython/commit/feee573f36ecdc767d73c95d43e68dd02e41549d
commit: feee573f36ecdc767d73c95d43e68dd02e41549d
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-04-07T14:05:39Z
summary:
gh-148014: Accept a function name in -X presite option (#148015)
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-04-02-17-52-33.gh-issue-148014.2Y6ND_.rst
M Doc/c-api/init_config.rst
M Doc/using/cmdline.rst
M Lib/test/_test_embed_structseq.py
M Lib/test/cov.py
M Lib/test/libregrtest/runtests.py
M Lib/test/support/__init__.py
M Lib/test/test_cmd_line.py
M Lib/test/test_embed.py
M Python/pylifecycle.c
diff --git a/Doc/c-api/init_config.rst b/Doc/c-api/init_config.rst
index f6dc604a609cb1..209e48767ccfd6 100644
--- a/Doc/c-api/init_config.rst
+++ b/Doc/c-api/init_config.rst
@@ -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.
diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst
index d0355ce47a6504..7cbc03f5f1281e 100644
--- a/Doc/using/cmdline.rst
+++ b/Doc/using/cmdline.rst
@@ -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,
@@ -1458,4 +1462,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
diff --git a/Lib/test/_test_embed_structseq.py
b/Lib/test/_test_embed_structseq.py
index 4cac84d7a469ac..c6050ca62aafca 100644
--- a/Lib/test/_test_embed_structseq.py
+++ b/Lib/test/_test_embed_structseq.py
@@ -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()
diff --git a/Lib/test/cov.py b/Lib/test/cov.py
index e4699c7afe174a..8717b1f20dd979 100644
--- a/Lib/test/cov.py
+++ b/Lib/test/cov.py
@@ -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
@@ -45,4 +44,5 @@ def disable():
mon.free_tool_id(mon.COVERAGE_ID)
-enable()
+if __name__ == "__main__":
+ enable()
diff --git a/Lib/test/libregrtest/runtests.py b/Lib/test/libregrtest/runtests.py
index e6d34d8e6a3be5..0a9edce1085be5 100644
--- a/Lib/test/libregrtest/runtests.py
+++ b/Lib/test/libregrtest/runtests.py
@@ -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]:
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
index 8ff061e074074f..2cac70f4ab2afb 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -1397,7 +1397,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):
diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py
index 5f035c35367d64..8740f65b7b0d1d 100644
--- a/Lib/test/test_cmd_line.py
+++ b/Lib/test/test_cmd_line.py
@@ -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('.')
@@ -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.')
diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py
index 45d0d8308dbdea..a2de58c2926456 100644
--- a/Lib/test/test_embed.py
+++ b/Lib/test/test_embed.py
@@ -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(
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-02-17-52-33.gh-issue-148014.2Y6ND_.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-02-17-52-33.gh-issue-148014.2Y6ND_.rst
new file mode 100644
index 00000000000000..964e5bdabbe1f0
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-02-17-52-33.gh-issue-148014.2Y6ND_.rst
@@ -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.
diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c
index 5da0f3e5be3a70..8be9e6d7373826 100644
--- a/Python/pylifecycle.c
+++ b/Python/pylifecycle.c
@@ -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_DECREF(obj);
+ Py_DECREF(attr);
+ return NULL;
+}
+
+
static void
run_presite(PyThreadState *tstate)
{
@@ -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
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]