Skip to content

bpo-36144: Update os.environ and os.environb for PEP 584 #18911

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Mar 13, 2020
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.