Skip to content
6 changes: 6 additions & 0 deletions Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ process and user.
``os.environ``, and when one of the :meth:`pop` or :meth:`clear` methods is
called.

.. versionchanged:: 3.9
Updated to support :pep:`584`'s merge (``|``) and update (``|=``) operators.


.. data:: environb

Expand All @@ -148,6 +151,9 @@ process and user.

.. versionadded:: 3.2

.. versionchanged:: 3.9
Updated to support :pep:`584`'s merge (``|``) and update (``|=``) operators.


.. function:: chdir(path)
fchdir(fd)
Expand Down
20 changes: 19 additions & 1 deletion Lib/os.py
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,7 @@ def get_exec_path(env=None):


# Change environ to automatically call putenv() and unsetenv()
from _collections_abc import MutableMapping
from _collections_abc import MutableMapping, Mapping

class _Environ(MutableMapping):
def __init__(self, data, encodekey, decodekey, encodevalue, decodevalue):
Expand Down Expand Up @@ -714,6 +714,24 @@ def setdefault(self, key, value):
self[key] = value
return self[key]

def __ior__(self, other):
self.update(other)
return self

def __or__(self, other):
if not isinstance(other, Mapping):
return NotImplemented
new = dict(self)
new.update(other)
return new

def __ror__(self, other):
if not isinstance(other, Mapping):
return NotImplemented
new = dict(other)
new.update(self)
return new

def _createenviron():
if name == 'nt':
# Where Env Var Names Must Be UPPERCASE
Expand Down
90 changes: 90 additions & 0 deletions Lib/test/test_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,96 @@ def test_iter_error_when_changing_os_environ_items(self):
def test_iter_error_when_changing_os_environ_values(self):
self._test_environ_iteration(os.environ.values())

def _test_underlying_process_env(self, var, expected):
if not (unix_shell and os.path.exists(unix_shell)):
return

with os.popen(f"{unix_shell} -c 'echo ${var}'") as popen:
value = popen.read().strip()

self.assertEqual(expected, value)

def test_or_operator(self):
overridden_key = '_TEST_VAR_'
original_value = 'original_value'
os.environ[overridden_key] = original_value

new_vars_dict = {'_A_': '1', '_B_': '2', overridden_key: '3'}
expected = dict(os.environ)
expected.update(new_vars_dict)

actual = os.environ | new_vars_dict
self.assertDictEqual(expected, actual)
self.assertEqual('3', actual[overridden_key])
Copy link
Member

Choose a reason for hiding this comment

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

Can you find a way to test whether this operation has a side effect on the process environment? (It shouldn't have. But it should for __ior__.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. I extended the tests to check this is behaving as expected.


new_vars_items = new_vars_dict.items()
self.assertIs(NotImplemented, os.environ.__or__(new_vars_items))

self._test_underlying_process_env('_A_', '')
self._test_underlying_process_env(overridden_key, original_value)

def test_ior_operator(self):
overridden_key = '_TEST_VAR_'
os.environ[overridden_key] = 'original_value'

new_vars_dict = {'_A_': '1', '_B_': '2', overridden_key: '3'}
expected = dict(os.environ)
expected.update(new_vars_dict)

os.environ |= new_vars_dict
self.assertEqual(expected, os.environ)
self.assertEqual('3', os.environ[overridden_key])

self._test_underlying_process_env('_A_', '1')
self._test_underlying_process_env(overridden_key, '3')

def test_ior_operator_invalid_dicts(self):
os_environ_copy = os.environ.copy()
with self.assertRaises(TypeError):
dict_with_bad_key = {1: '_A_'}
os.environ |= dict_with_bad_key

with self.assertRaises(TypeError):
dict_with_bad_val = {'_A_': 1}
os.environ |= dict_with_bad_val

# Check nothing was added.
self.assertEqual(os_environ_copy, os.environ)

def test_ior_operator_key_value_iterable(self):
overridden_key = '_TEST_VAR_'
os.environ[overridden_key] = 'original_value'

new_vars_items = (('_A_', '1'), ('_B_', '2'), (overridden_key, '3'))
expected = dict(os.environ)
expected.update(new_vars_items)

os.environ |= new_vars_items
self.assertEqual(expected, os.environ)
self.assertEqual('3', os.environ[overridden_key])

self._test_underlying_process_env('_A_', '1')
self._test_underlying_process_env(overridden_key, '3')

def test_ror_operator(self):
overridden_key = '_TEST_VAR_'
original_value = 'original_value'
os.environ[overridden_key] = original_value

new_vars_dict = {'_A_': '1', '_B_': '2', overridden_key: '3'}
expected = dict(new_vars_dict)
expected.update(os.environ)

actual = new_vars_dict | os.environ
self.assertDictEqual(expected, actual)
self.assertEqual(original_value, actual[overridden_key])

new_vars_items = new_vars_dict.items()
self.assertIs(NotImplemented, os.environ.__ror__(new_vars_items))

self._test_underlying_process_env('_A_', '')
self._test_underlying_process_env(overridden_key, original_value)


class WalkTests(unittest.TestCase):
"""Tests for os.walk()."""
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ Lars Buitinck
Dick Bulterman
Bill Bumgarner
Jimmy Burgett
Charles Burkland
Edmond Burnett
Tommy Burnette
Roger Burnham
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Updated :data:`os.environ` and :data:`os.environb` to support :pep:`584`'s
merge (``|``) and update (``|=``) operators.