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]

Reply via email to