Skip to content
Open
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
45 changes: 42 additions & 3 deletions src/rez/rex.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
from enum import Enum
from contextlib import contextmanager
from string import Formatter
from collections.abc import MutableMapping
from typing import Any, Iterable, Mapping
from collections.abc import Mapping, MutableMapping
from typing import Any, Iterable

from rez.system import system
from rez.config import config
Expand Down Expand Up @@ -200,7 +200,17 @@ def __init__(self, interpreter: ActionInterpreter,
'''
self.interpreter = interpreter
self.verbose = verbose
self.parent_environ = os.environ if parent_environ is None else parent_environ
# On Windows env-var keys are case-insensitive; os.environ already
# behaves that way but a plain dict supplied by the caller does not,
# so wrap it on the Windows path only. See #2089.
self.parent_environ: Mapping[str, str]
if parent_environ is None:
self.parent_environ = os.environ
elif platform_.name == "windows" and not isinstance(
parent_environ, _CaseInsensitiveEnvironProxy):
self.parent_environ = _CaseInsensitiveEnvironProxy(parent_environ)
else:
self.parent_environ = parent_environ
self.parent_variables = True if parent_variables is True \
else set(parent_variables or [])
self.environ = {}
Expand Down Expand Up @@ -447,6 +457,35 @@ def _keytoken(self, key):
return self.interpreter.get_key_token(key)


class _CaseInsensitiveEnvironProxy(Mapping):
"""Read-only case-insensitive view over an env-var mapping.

Used by `ActionManager` to wrap a caller-supplied `parent_environ`
on Windows so rex lookups match native Windows env-var semantics
regardless of the casing used to populate the input dict.

Kept private to this module on purpose; promote to a shared util
only when a second caller needs it.
"""
def __init__(self, data):
# keys are upper-cased once on the way in; same shape as
# os.environ on Windows. Last write wins on case collisions,
# which mirrors how os.environ resolves them too.
self._data = {k.upper(): v for k, v in data.items()}

def __getitem__(self, key):
return self._data[key.upper()]

def __contains__(self, key):
return isinstance(key, str) and key.upper() in self._data

def __iter__(self):
return iter(self._data)

def __len__(self):
return len(self._data)


#===============================================================================
# Interpreters
#===============================================================================
Expand Down
33 changes: 33 additions & 0 deletions src/rez/tests/test_rex.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,39 @@ def test_intersects_ephemerals(self) -> None:
self.assertRaises(RuntimeError, # no default
intersects, ephemerals.get_range("foo.bar"), "0")

def test_parent_environ_case_insensitive_on_windows(self):
"""Regression for #2089.

On Windows env vars are case-insensitive; os.environ already
behaves that way, but a plain dict copy of it doesn't. Passing
such a dict as parent_environ used to break env.<MixedCase>
lookups inside commands(). We now wrap on the Windows side.
"""
from rez.utils.platform_ import platform_
if platform_.name != "windows":
self.skipTest("Windows-only behaviour")

env = {"SomeVariable": "covfefe"}
ex = self._create_executor(env)

# any casing of the key resolves
self.assertEqual(ex.manager.parent_environ["SomeVariable"], "covfefe")
self.assertEqual(ex.manager.parent_environ["SOMEVARIABLE"], "covfefe")
self.assertEqual(ex.manager.parent_environ["somevariable"], "covfefe")
self.assertIn("SOMEVARIABLE", ex.manager.parent_environ)
self.assertNotIn("OtherVariable", ex.manager.parent_environ)

# end-to-end: rex code references a different casing than the
# caller's dict and the lookup must succeed (pre-fix this
# raised RexUndefinedVariableError)
def _rex():
v = getenv("SOMEVARIABLE") # noqa: F821 - rex builtin
setenv("RESULT", v) # noqa: F821 - rex builtin

ex2 = self._create_executor({"SomeVariable": "covfefe"})
ex2.execute_function(_rex)
self.assertEqual(ex2.get_output().get("RESULT"), "covfefe")


if __name__ == '__main__':
unittest.main()
Loading