diff --git a/Doc/library/os.rst b/Doc/library/os.rst index 360d71e70960c7..815400f7ec3942 100644 --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -197,6 +197,10 @@ process and user. changes to the environment made by :func:`os.putenv`, by :func:`os.unsetenv`, or made outside Python in the same process. + On Windows, :func:`get_user_default_environ` can be used to update + :data:`os.environ` to the latest user and system environment variables, such + as the ``Path`` variable. + This mapping may be used to modify the environment as well as query the environment. :func:`putenv` will be called automatically when the mapping is modified. @@ -326,6 +330,8 @@ process and user. and ``'surrogateescape'`` error handler. Use :func:`os.getenvb` if you would like to use a different encoding. + See also the :data:`os.environ.refresh() ` method. + .. availability:: Unix, Windows. @@ -357,6 +363,22 @@ process and user. .. versionadded:: 3.2 +.. function:: get_user_default_environ() + + Get the default environment of the current process user as a dictionary. + + It can be used to update :data:`os.environ` to the latest user and system + environment variables, such as the ``Path`` variable. Example:: + + os.environ.update(os.get_user_default_environ()) + + See also the :data:`os.environ.refresh() ` method. + + .. availability:: Windows. + + .. versionadded:: 3.14 + + .. function:: getegid() Return the effective group id of the current process. This corresponds to the diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index b357553735e8bb..64e9b3ed5a8045 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -100,6 +100,11 @@ os by :func:`os.unsetenv`, or made outside Python in the same process. (Contributed by Victor Stinner in :gh:`120057`.) +* Added the :func:`os.get_user_default_environ` function to get the user + default environment. It can be used to update :data:`os.environ` to the + latest user and system environment variables, such as the ``Path`` variable. + (Contributed by Victor Stinner in :gh:`120057`.) + symtable -------- diff --git a/Lib/os.py b/Lib/os.py index 4b48afb040e565..9e4988b315fd2f 100644 --- a/Lib/os.py +++ b/Lib/os.py @@ -96,6 +96,10 @@ def _get_exports_list(module): from nt import _create_environ except ImportError: pass + try: + from nt import _get_user_default_environ + except ImportError: + pass else: raise ImportError('no os specific module found') @@ -826,6 +830,29 @@ def decode(value): del _create_environ_mapping +if _exists("_get_user_default_environ"): + def get_user_default_environ(): + """ + Get the default environment of the current process user as a dictionary. + """ + env_str = _get_user_default_environ() + env = {} + for entry in env_str.split('\0'): + parts = entry.split('=', 1) + if len(parts) != 2: + # Silently skip variable with no name + continue + + name, value = parts + if not name: + # Silently skip variable with empty name + continue + + # If a variable is set twice, use the first value + env.setdefault(name, value) + return env + + def getenv(key, default=None): """Get an environment variable, return None if it doesn't exist. The optional second argument can specify an alternate default. diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index f93937fb587386..293dae9cecd938 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -31,6 +31,7 @@ import unittest import uuid import warnings +from unittest import mock from test import support from test.support import import_helper from test.support import os_helper @@ -1345,6 +1346,35 @@ def test_refresh(self): self.assertNotIn(b'test_env', os.environb) self.assertNotIn('test_env', os.environ) + @unittest.skipUnless(hasattr(os, 'get_user_default_environ'), + 'need os.get_user_default_environ()') + def test_get_user_default_environ(self): + env = os.get_user_default_environ() + self.assertIsInstance(env, dict) + for name, value in env.items(): + self.assertIsInstance(name, str) + self.assertIsInstance(value, str) + self.assertTrue(bool(name), name) # must be not empty + + # test variable defined twice + env_str = 'A=1\0B=2\0A=3\0' + with mock.patch('os._get_user_default_environ', return_value=env_str): + env = os.get_user_default_environ() + self.assertEqual(env, {'A': '1', 'B': '2'}) + + # variable name with empty names are silently ignored + env_str = '=1\0A=2\0' + with mock.patch('os._get_user_default_environ', return_value=env_str): + env = os.get_user_default_environ() + self.assertEqual(env, {'A': '2'}) + + # variable with no name + env_str = 'ABC\0A=2\0' + with mock.patch('os._get_user_default_environ', return_value=env_str): + env = os.get_user_default_environ() + self.assertEqual(env, {'A': '2'}) + + class WalkTests(unittest.TestCase): """Tests for os.walk().""" is_fwalk = False diff --git a/Misc/NEWS.d/next/Library/2024-06-14-13-28-09.gh-issue-120057.GN0-d0.rst b/Misc/NEWS.d/next/Library/2024-06-14-13-28-09.gh-issue-120057.GN0-d0.rst new file mode 100644 index 00000000000000..98ee78e450d675 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-06-14-13-28-09.gh-issue-120057.GN0-d0.rst @@ -0,0 +1,4 @@ +Added the :func:`os.get_user_default_environ` function to get the user +default environment. It can be used to update :data:`os.environ` to the +latest user and system environment variables, such as the ``Path`` variable. +Patch by Victor Stinner. diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h index 07b28fef3a57ea..038f37d8dd51e5 100644 --- a/Modules/clinic/posixmodule.c.h +++ b/Modules/clinic/posixmodule.c.h @@ -12170,6 +12170,28 @@ os__create_environ(PyObject *module, PyObject *Py_UNUSED(ignored)) return os__create_environ_impl(module); } +#if defined(MS_WINDOWS) + +PyDoc_STRVAR(os__get_user_default_environ__doc__, +"_get_user_default_environ($module, /)\n" +"--\n" +"\n" +"Get user default environment string."); + +#define OS__GET_USER_DEFAULT_ENVIRON_METHODDEF \ + {"_get_user_default_environ", (PyCFunction)os__get_user_default_environ, METH_NOARGS, os__get_user_default_environ__doc__}, + +static PyObject * +os__get_user_default_environ_impl(PyObject *module); + +static PyObject * +os__get_user_default_environ(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return os__get_user_default_environ_impl(module); +} + +#endif /* defined(MS_WINDOWS) */ + #ifndef OS_TTYNAME_METHODDEF #define OS_TTYNAME_METHODDEF #endif /* !defined(OS_TTYNAME_METHODDEF) */ @@ -12837,4 +12859,8 @@ os__create_environ(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF #define OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF #endif /* !defined(OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF) */ -/*[clinic end generated code: output=5ae2e5ffcd9c8a84 input=a9049054013a1b77]*/ + +#ifndef OS__GET_USER_DEFAULT_ENVIRON_METHODDEF + #define OS__GET_USER_DEFAULT_ENVIRON_METHODDEF +#endif /* !defined(OS__GET_USER_DEFAULT_ENVIRON_METHODDEF) */ +/*[clinic end generated code: output=66014c7643fb6634 input=a9049054013a1b77]*/ diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index a8fd5c494769b5..ff9879f400fb6f 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -39,6 +39,7 @@ # include "osdefs.h" // SEP # include // SetEntriesInAcl # include // SDDL_REVISION_1 +# include // CreateEnvironmentBlock() # if defined(MS_WINDOWS_DESKTOP) || defined(MS_WINDOWS_SYSTEM) # define HAVE_SYMLINK # endif /* MS_WINDOWS_DESKTOP | MS_WINDOWS_SYSTEM */ @@ -1682,8 +1683,12 @@ convertenviron(void) #else const char *p = strchr(*e, '='); #endif - if (p == NULL) + if (p == NULL) { + // Silently skip variable with no name continue; + } + + // Note: Allow empty variable name #ifdef MS_WINDOWS k = PyUnicode_FromWideChar(*e, (Py_ssize_t)(p-*e)); #else @@ -1703,6 +1708,8 @@ convertenviron(void) Py_DECREF(d); return NULL; } + + // If a variable is set twice, use the first value if (PyDict_SetDefaultRef(d, k, v, NULL) < 0) { Py_DECREF(v); Py_DECREF(k); @@ -16823,6 +16830,50 @@ os__create_environ_impl(PyObject *module) } +#ifdef MS_WINDOWS +/*[clinic input] +os._get_user_default_environ + +Get user default environment string. +[clinic start generated code]*/ + +static PyObject * +os__get_user_default_environ_impl(PyObject *module) +/*[clinic end generated code: output=6cce8c186a556ef0 input=e15b3d87fce12734]*/ +{ + HANDLE htoken; + if (!OpenProcessToken(GetCurrentProcess(), + TOKEN_DUPLICATE|TOKEN_QUERY, + &htoken)) { + goto error; + } + + PWCHAR env_str; + if (!CreateEnvironmentBlock(&env_str, htoken, 0)) { + CloseHandle(htoken); + goto error; + } + CloseHandle(htoken); + + Py_ssize_t len = wcslen(env_str); + while (env_str[len+1] != 0) { + len++; + len += wcslen(env_str + len); + } + + PyObject *str = PyUnicode_FromWideChar(env_str, len); + if (!DestroyEnvironmentBlock(env_str)) { + Py_XDECREF(str); + goto error; + } + return str; + +error: + return PyErr_SetFromWindowsErr(0); +} +#endif // MS_WINDOWS + + static PyMethodDef posix_methods[] = { OS_STAT_METHODDEF @@ -17038,6 +17089,7 @@ static PyMethodDef posix_methods[] = { OS__INPUTHOOK_METHODDEF OS__IS_INPUTHOOK_INSTALLED_METHODDEF OS__CREATE_ENVIRON_METHODDEF + OS__GET_USER_DEFAULT_ENVIRON_METHODDEF {NULL, NULL} /* Sentinel */ }; diff --git a/PCbuild/_freeze_module.vcxproj b/PCbuild/_freeze_module.vcxproj index e5e18de60ec349..4d4c9a9665f203 100644 --- a/PCbuild/_freeze_module.vcxproj +++ b/PCbuild/_freeze_module.vcxproj @@ -95,7 +95,7 @@ Console - version.lib;ws2_32.lib;pathcch.lib;bcrypt.lib;%(AdditionalDependencies) + version.lib;ws2_32.lib;pathcch.lib;bcrypt.lib;userenv.lib;%(AdditionalDependencies) Default diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj index 96960f0579a936..7b979538879efe 100644 --- a/PCbuild/pythoncore.vcxproj +++ b/PCbuild/pythoncore.vcxproj @@ -108,7 +108,7 @@ _Py_TIER2=$(UseTIER2);%(PreprocessorDefinitions) - version.lib;ws2_32.lib;pathcch.lib;bcrypt.lib;%(AdditionalDependencies) + version.lib;ws2_32.lib;pathcch.lib;bcrypt.lib;userenv.lib;%(AdditionalDependencies)