]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-110722: Add PYTHON_PRESITE to import a module before site.py is run (#110769)
authorŁukasz Langa <lukasz@langa.pl>
Sat, 14 Oct 2023 21:32:57 +0000 (23:32 +0200)
committerGitHub <noreply@github.com>
Sat, 14 Oct 2023 21:32:57 +0000 (23:32 +0200)
Doc/c-api/init_config.rst
Doc/using/cmdline.rst
Doc/whatsnew/3.13.rst
Include/cpython/initconfig.h
Lib/test/test_embed.py
Misc/NEWS.d/next/Core and Builtins/2023-10-12-17-15-23.gh-issue-110722.sjMwQe.rst [new file with mode: 0644]
Python/initconfig.c
Python/pylifecycle.c

index 0240e25b6f160735a9bbbf9246f20a8c981d0035..1d4e0fbb0d400fff9c593f636326550852a904b0 100644 (file)
@@ -716,7 +716,7 @@ PyConfig
 
       Set to ``1`` by the :envvar:`PYTHONDUMPREFS` environment variable.
 
-      Need a special build of Python with the ``Py_TRACE_REFS`` macro defined:
+      Needs a special build of Python with the ``Py_TRACE_REFS`` macro defined:
       see the :option:`configure --with-trace-refs option <--with-trace-refs>`.
 
       Default: ``0``.
@@ -1048,7 +1048,7 @@ PyConfig
       Incremented by the :option:`-d` command line option. Set to the
       :envvar:`PYTHONDEBUG` environment variable value.
 
-      Need a :ref:`debug build of Python <debug-build>` (the ``Py_DEBUG`` macro
+      Needs a :ref:`debug build of Python <debug-build>` (the ``Py_DEBUG`` macro
       must be defined).
 
       Default: ``0``.
@@ -1100,6 +1100,7 @@ PyConfig
 
       Set by the :option:`-X pycache_prefix=PATH <-X>` command line option and
       the :envvar:`PYTHONPYCACHEPREFIX` environment variable.
+      The command-line option takes precedence.
 
       If ``NULL``, :data:`sys.pycache_prefix` is set to ``None``.
 
@@ -1143,13 +1144,27 @@ PyConfig
 
       Default: ``NULL``.
 
+   .. c:member:: wchar_t* run_presite
+
+      ``package.module`` path to module that should be imported before
+      ``site.py`` is run.
+
+      Set by the :option:`-X presite=package.module <-X>` command-line
+      option and the :envvar:`PYTHON_PRESITE` environment variable.
+      The command-line option takes precedence.
+
+      Needs a :ref:`debug build of Python <debug-build>` (the ``Py_DEBUG`` macro
+      must be defined).
+
+      Default: ``NULL``.
+
    .. c:member:: int show_ref_count
 
       Show total reference count at exit (excluding immortal objects)?
 
       Set to ``1`` by :option:`-X showrefcount <-X>` command line option.
 
-      Need a :ref:`debug build of Python <debug-build>` (the ``Py_REF_DEBUG``
+      Needs a :ref:`debug build of Python <debug-build>` (the ``Py_REF_DEBUG``
       macro must be defined).
 
       Default: ``0``.
index b133d2c5893797f8ce946231e4855140dcd92ee6..4fdfa097808c0119173f95f6c5fc56355327c021 100644 (file)
@@ -552,6 +552,12 @@ Miscellaneous options
      This option may be useful for users who need to limit CPU resources of a
      container system. See also :envvar:`PYTHON_CPU_COUNT`.
      If *n* is ``default``, nothing is overridden.
+   * :samp:`-X presite={package.module}` specifies a module that should be
+     imported 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`.
 
    It also allows passing arbitrary values and retrieving them through the
    :data:`sys._xoptions` dictionary.
@@ -602,6 +608,9 @@ Miscellaneous options
    .. versionadded:: 3.13
       The ``-X cpu_count`` option.
 
+   .. versionadded:: 3.13
+      The ``-X presite`` option.
+
 
 Options you shouldn't use
 ~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1091,13 +1100,33 @@ Debug-mode variables
    If set, Python will dump objects and reference counts still alive after
    shutting down the interpreter.
 
-   Need Python configured with the :option:`--with-trace-refs` build option.
+   Needs Python configured with the :option:`--with-trace-refs` build option.
 
-.. envvar:: PYTHONDUMPREFSFILE=FILENAME
+.. envvar:: PYTHONDUMPREFSFILE
 
    If set, Python will dump objects and reference counts still alive
-   after shutting down the interpreter into a file called *FILENAME*.
+   after shutting down the interpreter into a file under the path given
+   as the value to this environment variable.
 
-   Need Python configured with the :option:`--with-trace-refs` build option.
+   Needs Python configured with the :option:`--with-trace-refs` build option.
 
    .. versionadded:: 3.11
+
+.. envvar:: PYTHON_PRESITE
+
+   If this variable is set to a module, that module will be imported
+   early in the interpreter lifecycle, before the :mod:`site` module is
+   executed, and before the :mod:`__main__` module is created.
+   Therefore, the imported module is not treated as :mod:`__main__`.
+
+   This can be used to execute code early during Python initialization.
+
+   To import a submodule, use ``package.module`` as the value, like in
+   an import statement.
+
+   See also the :option:`-X presite <-X>` command-line option,
+   which takes precedence over this variable.
+
+   Needs Python configured with the :option:`--with-pydebug` build option.
+
+   .. versionadded:: 3.13
index f43ebc4bd5f95118f5f4bea5e26b2fd4e46585bd..08e7bea4fe2bf991cee2f3189e73f55c07d9078d 100644 (file)
@@ -1356,3 +1356,13 @@ removed, although there is currently no date scheduled for their removal.
 
 * Remove undocumented ``PY_TIMEOUT_MAX`` constant from the limited C API.
   (Contributed by Victor Stinner in :gh:`110014`.)
+
+
+Regression Test Changes
+=======================
+
+* Python built with :file:`configure` :option:`--with-pydebug` now
+  supports a :option:`-X presite=package.module <-X>` command-line
+  option. If used, it specifies a module that should be imported early
+  in the lifecycle of the interpreter, before ``site.py`` is executed.
+  (Contributed by Łukasz Langa in :gh:`110769`.)
index 808c1056498b4990de360f309af98b35cd2a29dd..87c059c521cbc9271dae6201657419c16d5a9237 100644 (file)
@@ -225,6 +225,12 @@ typedef struct PyConfig {
     // If non-zero, turns on statistics gathering.
     int _pystats;
 #endif
+
+#ifdef Py_DEBUG
+    // If not empty, import a non-__main__ module before site.py is executed.
+    // PYTHON_PRESITE=package.module or -X presite=package.module
+    wchar_t *run_presite;
+#endif
 } PyConfig;
 
 PyAPI_FUNC(void) PyConfig_InitPythonConfig(PyConfig *config);
index 5a8690a4836dd6daf57de6a7be5bd8b87eda4a2e..d2d6c1b61e46f069cf2d8c21f7798b0d89a1e84e 100644 (file)
@@ -515,6 +515,8 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
     }
     if Py_STATS:
         CONFIG_COMPAT['_pystats'] = 0
+    if support.Py_DEBUG:
+        CONFIG_COMPAT['run_presite'] = None
     if MS_WINDOWS:
         CONFIG_COMPAT.update({
             'legacy_windows_stdio': 0,
@@ -1818,6 +1820,22 @@ class MiscTests(EmbeddingTestsMixin, unittest.TestCase):
                 self.assertEqual(refs, 0, out)
                 self.assertEqual(blocks, 0, out)
 
+    @unittest.skipUnless(support.Py_DEBUG,
+                         '-X presite requires a Python debug build')
+    def test_presite(self):
+        cmd = [sys.executable, "-I", "-X", "presite=test.reperf", "-c", "print('cmd')"]
+        proc = subprocess.run(
+            cmd,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.STDOUT,
+            text=True,
+        )
+        self.assertEqual(proc.returncode, 0)
+        out = proc.stdout.strip()
+        self.assertIn("10 times sub", out)
+        self.assertIn("CPU seconds", out)
+        self.assertIn("cmd", out)
+
 
 class StdPrinterTests(EmbeddingTestsMixin, unittest.TestCase):
     # Test PyStdPrinter_Type which is used by _PySys_SetPreliminaryStderr():
diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-10-12-17-15-23.gh-issue-110722.sjMwQe.rst b/Misc/NEWS.d/next/Core and Builtins/2023-10-12-17-15-23.gh-issue-110722.sjMwQe.rst
new file mode 100644 (file)
index 0000000..79b941e
--- /dev/null
@@ -0,0 +1,3 @@
+Add :envvar:`PYTHON_PRESITE=package.module` to import a module early in the
+interpreter lifecycle before ``site.py`` is executed.  Python needs to be
+:ref:`built in debug mode <debug-build>` for this option to exist.
index f7eb8535e98a6a3e141bed29f85b777604c60a8f..e1199338f2a54fc1072ddd8998c9e13ad6ca5a4f 100644 (file)
@@ -117,6 +117,9 @@ static const PyConfigSpec PYCONFIG_SPEC[] = {
     SPEC(_is_python_build, UINT),
 #ifdef Py_STATS
     SPEC(_pystats, UINT),
+#endif
+#ifdef Py_DEBUG
+    SPEC(run_presite, WSTR_OPT),
 #endif
     {NULL, 0, 0},
 };
@@ -241,6 +244,11 @@ The following implementation-specific options are available:\n\
 \n\
 -X pystats: Enable pystats collection at startup."
 #endif
+#ifdef Py_DEBUG
+"\n\
+\n\
+-X presite=package.module: import this module before site.py is run."
+#endif
 ;
 
 /* Envvars that don't have equivalent command-line options are listed first */
@@ -297,6 +305,9 @@ static const char usage_envvars[] =
 #ifdef Py_STATS
 "PYTHONSTATS             : turns on statistics gathering\n"
 #endif
+#ifdef Py_DEBUG
+"PYTHON_PRESITE=pkg.mod  : import this module before site.py is run\n"
+#endif
 ;
 
 #if defined(MS_WINDOWS)
@@ -790,6 +801,9 @@ PyConfig_Clear(PyConfig *config)
     CLEAR(config->run_module);
     CLEAR(config->run_filename);
     CLEAR(config->check_hash_pycs_mode);
+#ifdef Py_DEBUG
+    CLEAR(config->run_presite);
+#endif
 
     _PyWideStringList_Clear(&config->orig_argv);
 #undef CLEAR
@@ -1806,6 +1820,36 @@ config_init_pycache_prefix(PyConfig *config)
 }
 
 
+#ifdef Py_DEBUG
+static PyStatus
+config_init_run_presite(PyConfig *config)
+{
+    assert(config->run_presite == NULL);
+
+    const wchar_t *xoption = config_get_xoption(config, L"presite");
+    if (xoption) {
+        const wchar_t *sep = wcschr(xoption, L'=');
+        if (sep && wcslen(sep) > 1) {
+            config->run_presite = _PyMem_RawWcsdup(sep + 1);
+            if (config->run_presite == NULL) {
+                return _PyStatus_NO_MEMORY();
+            }
+        }
+        else {
+            // PYTHON_PRESITE env var ignored
+            // if "-X presite=" option is used
+            config->run_presite = NULL;
+        }
+        return _PyStatus_OK();
+    }
+
+    return CONFIG_GET_ENV_DUP(config, &config->run_presite,
+                              L"PYTHON_PRESITE",
+                              "PYTHON_PRESITE");
+}
+#endif
+
+
 static PyStatus
 config_read_complex_options(PyConfig *config)
 {
@@ -1861,6 +1905,16 @@ config_read_complex_options(PyConfig *config)
             return status;
         }
     }
+
+#ifdef Py_DEBUG
+    if (config->run_presite == NULL) {
+        status = config_init_run_presite(config);
+        if (_PyStatus_EXCEPTION(status)) {
+            return status;
+        }
+    }
+#endif
+
     return _PyStatus_OK();
 }
 
index 14033162377489289a9c36c2884f731cfa542c0d..7b56034541756a9740fbf3a501f605c5ea7408cd 100644 (file)
@@ -1076,6 +1076,38 @@ pyinit_main_reconfigure(PyThreadState *tstate)
 }
 
 
+#ifdef Py_DEBUG
+static void
+run_presite(PyThreadState *tstate)
+{
+    PyInterpreterState *interp = tstate->interp;
+    const PyConfig *config = _PyInterpreterState_GetConfig(interp);
+
+    if (!config->run_presite) {
+        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");
+        Py_DECREF(presite_modname);
+    }
+    else {
+        PyObject *presite = PyImport_Import(presite_modname);
+        if (presite == NULL) {
+            fprintf(stderr, "pre-site import failed:\n");
+            _PyErr_Print(tstate);
+        }
+        Py_XDECREF(presite);
+        Py_DECREF(presite_modname);
+    }
+}
+#endif
+
+
 static PyStatus
 init_interp_main(PyThreadState *tstate)
 {
@@ -1157,6 +1189,10 @@ init_interp_main(PyThreadState *tstate)
         return status;
     }
 
+#ifdef Py_DEBUG
+    run_presite(tstate);
+#endif
+
     status = add_main_module(interp);
     if (_PyStatus_EXCEPTION(status)) {
         return status;