Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment on lines +200 to +202
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally, a new environment block gets passed to a spawned child process, such as via the env parameter of subprocess.Popen. I'd emphasize that over updating os.environ, which seems to be a point of controversy.


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.
Expand Down Expand Up @@ -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() <os.environ>` method.

.. availability:: Unix, Windows.


Expand Down Expand Up @@ -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() <os.environ>` method.

.. availability:: Windows.

.. versionadded:: 3.14


.. function:: getegid()

Return the effective group id of the current process. This corresponds to the
Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------

Expand Down
27 changes: 27 additions & 0 deletions Lib/os.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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)
Comment on lines +851 to +852
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose this doesn't hurt. But it shouldn't be possible to get duplicates unless there's a serious bug. The new environment is built by assigning values to the environment block, which is handled as a parameter by the same code that implements WinAPI SetEnvironmentVariableW().

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.
Expand Down
30 changes: 30 additions & 0 deletions Lib/test/test_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
28 changes: 27 additions & 1 deletion Modules/clinic/posixmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 53 additions & 1 deletion Modules/posixmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
# include "osdefs.h" // SEP
# include <aclapi.h> // SetEntriesInAcl
# include <sddl.h> // SDDL_REVISION_1
# include <userenv.h> // CreateEnvironmentBlock()
# if defined(MS_WINDOWS_DESKTOP) || defined(MS_WINDOWS_SYSTEM)
# define HAVE_SYMLINK
# endif /* MS_WINDOWS_DESKTOP | MS_WINDOWS_SYSTEM */
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 */
};

Expand Down
2 changes: 1 addition & 1 deletion PCbuild/_freeze_module.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<AdditionalDependencies>version.lib;ws2_32.lib;pathcch.lib;bcrypt.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>version.lib;ws2_32.lib;pathcch.lib;bcrypt.lib;userenv.lib;%(AdditionalDependencies)</AdditionalDependencies>
<LinkTimeCodeGeneration>Default</LinkTimeCodeGeneration>
</Link>
</ItemDefinitionGroup>
Expand Down
2 changes: 1 addition & 1 deletion PCbuild/pythoncore.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
<PreprocessorDefinitions Condition="'$(UseTIER2)' != '' and '$(UseTIER2)' != '0'">_Py_TIER2=$(UseTIER2);%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<AdditionalDependencies>version.lib;ws2_32.lib;pathcch.lib;bcrypt.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>version.lib;ws2_32.lib;pathcch.lib;bcrypt.lib;userenv.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
Expand Down