]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-148014: Accept a function name in -X presite option (#148015)
authorVictor Stinner <vstinner@python.org>
Tue, 7 Apr 2026 14:05:39 +0000 (16:05 +0200)
committerGitHub <noreply@github.com>
Tue, 7 Apr 2026 14:05:39 +0000 (14:05 +0000)
Doc/c-api/init_config.rst
Doc/using/cmdline.rst
Lib/test/_test_embed_structseq.py
Lib/test/cov.py
Lib/test/libregrtest/runtests.py
Lib/test/support/__init__.py
Lib/test/test_cmd_line.py
Lib/test/test_embed.py
Misc/NEWS.d/next/Core_and_Builtins/2026-04-02-17-52-33.gh-issue-148014.2Y6ND_.rst [new file with mode: 0644]
Python/pylifecycle.c

index f6dc604a609cb1da0c93d947c7fa1b97d68f54e7..209e48767ccfd6c7dc103f5c29b9776308c0085a 100644 (file)
@@ -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.
 
index d0355ce47a6504e79ea023e6423228468fddd615..7cbc03f5f1281ec7182656d66b9d76201699d77b 100644 (file)
@@ -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
index 4cac84d7a469acc1ab7fbc3d474125d9348b101e..c6050ca62aafca399262cc46ceaa428973607009 100644 (file)
@@ -47,16 +47,21 @@ class TestStructSeq(unittest.TestCase):
                 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()
index e4699c7afe174a70eba0ae32b4c725dfacd3aba5..8717b1f20dd97973ede4254a4bc3fc804a9b7821 100644 (file)
@@ -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()
index e6d34d8e6a3be53b685a5142dd66309ef8e4e1a3..0a9edce1085be5481ed69373a3a86635a0bdb6e2 100644 (file)
@@ -159,7 +159,7 @@ class RunTests:
         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]:
index 8ff061e074074fdb4d17bd83755dc181ecfd4ecf..2cac70f4ab2afbef9a719ec8aff3f3ceb439a264 100644 (file)
@@ -1397,7 +1397,7 @@ def no_tracing(func):
                 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):
index 5f035c35367d647b9fdeb9d8fd4164cb1271cba0..8740f65b7b0d1d00f4b8d38c29b157d7f2d8a632 100644 (file)
@@ -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 @@ class CmdLineTest(unittest.TestCase):
         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.')
index 45d0d8308dbdea6b5cdc1baf82d735a70086e5fb..a2de58c292645693eb29a2b4cca4041e987cef3f 100644 (file)
@@ -2051,7 +2051,7 @@ class MiscTests(EmbeddingTestsMixin, unittest.TestCase):
     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 (file)
index 0000000..964e5bd
--- /dev/null
@@ -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.
index 5da0f3e5be3a70f85efc5959b695a2f0134d678a..8be9e6d73738267a5095e360e4262d93dcedcc47 100644 (file)
@@ -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