diff --git a/Doc/library/os.rst b/Doc/library/os.rst index 360d71e70960c7..377b63a7121c00 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 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 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..05628b1638cbab 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 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..b434e2ad5ca9a9 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,20 @@ def decode(value): del _create_environ_mapping +if _exists("_get_user_default_environ"): + def get_user_default_environ(): + env = {} + env_str = _get_user_default_environ() + for entry in env_str.split('\0'): + parts = entry.split('=', 1) + if len(parts) != 2: + # Skip names that begin with '=' + continue + name, value = parts + env[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..6905fe9ed94fd5 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -1345,6 +1345,17 @@ 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 + + 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..dc1053a6fd35c7 --- /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 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..c7a4494647a639 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -16823,6 +16823,70 @@ 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]*/ +{ + HINSTANCE hUserEnv = LoadLibraryW(L"USERENV"); + if (!hUserEnv) { + goto error; + } + + HINSTANCE (CALLBACK *pCreateEnvironmentBlock) (LPVOID, HANDLE, BOOL); + HINSTANCE (CALLBACK *pDestroyEnvironmentBlock) (LPVOID); + + *(FARPROC*)&pCreateEnvironmentBlock = GetProcAddress(hUserEnv, + "CreateEnvironmentBlock"); + if (!pCreateEnvironmentBlock) { + goto error; + } + + *(FARPROC*)&pDestroyEnvironmentBlock = GetProcAddress(hUserEnv, + "DestroyEnvironmentBlock"); + if (!pDestroyEnvironmentBlock) { + goto error; + } + + HANDLE htoken; + if (!OpenProcessToken(GetCurrentProcess(), + TOKEN_DUPLICATE|TOKEN_QUERY, + &htoken)) { + goto error; + } + + PWCHAR env_str; + if (!pCreateEnvironmentBlock(&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 (!pDestroyEnvironmentBlock(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 +17102,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 */ };