Skip to content

Commit c41f7f3

Browse files
gh-158694: Preserve empty Windows environment values
1 parent eb77b4b commit c41f7f3

3 files changed

Lines changed: 90 additions & 26 deletions

File tree

‎Lib/test/test_os/test_os.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,6 +1489,24 @@ def test_putenv_unsetenv(self):
14891489
stdout=subprocess.PIPE, text=True)
14901490
self.assertEqual(proc.stdout.rstrip(), repr(None))
14911491

1492+
@unittest.skipUnless(sys.platform == "win32", "Windows specific test")
1493+
@support.requires_subprocess()
1494+
def test_environ_empty_value(self):
1495+
name = "PYTHONTESTVAR"
1496+
code = f'import os; print(repr(os.environ.get({name!r})))'
1497+
1498+
with os_helper.EnvironmentVarGuard() as env:
1499+
env.pop(name, None)
1500+
os.environ[name] = ''
1501+
self.assertEqual(os.environ[name], '')
1502+
1503+
proc = subprocess.run([sys.executable, '-c', code], check=True,
1504+
stdout=subprocess.PIPE, text=True)
1505+
self.assertEqual(proc.stdout.rstrip(), repr(''))
1506+
1507+
os.reload_environ()
1508+
self.assertEqual(os.environ[name], '')
1509+
14921510
# On OS X < 10.6, unsetenv() doesn't return a value (bpo-13415).
14931511
@support.requires_mac_ver(10, 6)
14941512
def test_putenv_unsetenv_error(self):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
On Windows, :data:`os.environ` now preserves empty environment variable values in child processes and across :func:`os.reload_environ`.

‎Modules/posixmodule.c‎

Lines changed: 71 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1792,21 +1792,59 @@ static PyObject *
17921792
convertenviron(void)
17931793
{
17941794
PyObject *d;
1795-
#ifdef MS_WINDOWS
1796-
wchar_t **e;
1797-
#else
1798-
char **e;
1799-
#endif
18001795

18011796
d = PyDict_New();
18021797
if (d == NULL)
18031798
return NULL;
18041799
#ifdef MS_WINDOWS
1805-
/* _wenviron must be initialized in this way if the program is started
1806-
through main() instead of wmain(). */
1807-
(void)_wgetenv(L"");
1808-
e = _wenviron;
1809-
#elif defined(USE_DARWIN_NS_GET_ENVIRON)
1800+
wchar_t *environment = GetEnvironmentStringsW();
1801+
if (environment == NULL) {
1802+
Py_DECREF(d);
1803+
return PyErr_SetFromWindowsErr(0);
1804+
}
1805+
1806+
for (wchar_t *entry = environment; *entry != L'\0';
1807+
entry += wcslen(entry) + 1)
1808+
{
1809+
/* Entries whose names start with '=' are hidden drive-current-
1810+
directory variables. The CRT does not expose them, so continue
1811+
to omit them from os.environ. */
1812+
if (*entry == L'=') {
1813+
continue;
1814+
}
1815+
1816+
const wchar_t *p = wcschr(entry, L'=');
1817+
if (p == NULL) {
1818+
continue;
1819+
}
1820+
1821+
PyObject *k = PyUnicode_FromWideChar(entry, (Py_ssize_t)(p - entry));
1822+
if (k == NULL) {
1823+
FreeEnvironmentStringsW(environment);
1824+
Py_DECREF(d);
1825+
return NULL;
1826+
}
1827+
PyObject *v = PyUnicode_FromWideChar(p + 1, -1);
1828+
if (v == NULL) {
1829+
Py_DECREF(k);
1830+
FreeEnvironmentStringsW(environment);
1831+
Py_DECREF(d);
1832+
return NULL;
1833+
}
1834+
if (PyDict_SetDefaultRef(d, k, v, NULL) < 0) {
1835+
Py_DECREF(v);
1836+
Py_DECREF(k);
1837+
FreeEnvironmentStringsW(environment);
1838+
Py_DECREF(d);
1839+
return NULL;
1840+
}
1841+
Py_DECREF(k);
1842+
Py_DECREF(v);
1843+
}
1844+
FreeEnvironmentStringsW(environment);
1845+
#else
1846+
char **e;
1847+
#ifdef USE_DARWIN_NS_GET_ENVIRON
18101848
/* environ is not accessible as an extern in a shared object on OSX; use
18111849
_NSGetEnviron to resolve it. The value changes if you add environment
18121850
variables between calls to Py_Initialize, so don't cache the value. */
@@ -1817,29 +1855,16 @@ convertenviron(void)
18171855
if (e == NULL)
18181856
return d;
18191857
for (; *e != NULL; e++) {
1820-
PyObject *k;
1821-
PyObject *v;
1822-
#ifdef MS_WINDOWS
1823-
const wchar_t *p = wcschr(*e, L'=');
1824-
#else
18251858
const char *p = strchr(*e, '=');
1826-
#endif
18271859
if (p == NULL)
18281860
continue;
1829-
#ifdef MS_WINDOWS
1830-
k = PyUnicode_FromWideChar(*e, (Py_ssize_t)(p-*e));
1831-
#else
1832-
k = PyBytes_FromStringAndSize(*e, (Py_ssize_t)(p-*e));
1833-
#endif
1861+
1862+
PyObject *k = PyBytes_FromStringAndSize(*e, (Py_ssize_t)(p - *e));
18341863
if (k == NULL) {
18351864
Py_DECREF(d);
18361865
return NULL;
18371866
}
1838-
#ifdef MS_WINDOWS
1839-
v = PyUnicode_FromWideChar(p+1, -1);
1840-
#else
1841-
v = PyBytes_FromStringAndSize(p+1, strlen(p+1));
1842-
#endif
1867+
PyObject *v = PyBytes_FromStringAndSize(p + 1, strlen(p + 1));
18431868
if (v == NULL) {
18441869
Py_DECREF(k);
18451870
Py_DECREF(d);
@@ -1854,6 +1879,7 @@ convertenviron(void)
18541879
Py_DECREF(k);
18551880
Py_DECREF(v);
18561881
}
1882+
#endif
18571883
return d;
18581884
}
18591885

@@ -13777,6 +13803,25 @@ win32_putenv(PyObject *name, PyObject *value)
1377713803
PyMem_Free(env);
1377813804
return NULL;
1377913805
}
13806+
13807+
if (value != NULL && PyUnicode_GET_LENGTH(value) == 0) {
13808+
/* The CRT treats "NAME=" as removing NAME. Preserve the empty value
13809+
in the PEB so that os.environ, os.reload_environ(), and child
13810+
processes observe the value requested by the Python caller. */
13811+
wchar_t *name_wstr = PyUnicode_AsWideCharString(name, NULL);
13812+
if (name_wstr == NULL) {
13813+
PyMem_Free(env);
13814+
return NULL;
13815+
}
13816+
if (!SetEnvironmentVariableW(name_wstr, L"")) {
13817+
DWORD error = GetLastError();
13818+
PyMem_Free(name_wstr);
13819+
PyMem_Free(env);
13820+
return PyErr_SetFromWindowsErr(error);
13821+
}
13822+
PyMem_Free(name_wstr);
13823+
}
13824+
1378013825
PyMem_Free(env);
1378113826

1378213827
Py_RETURN_NONE;

0 commit comments

Comments
 (0)