diff --git a/src/rez/rex.py b/src/rez/rex.py index da26ef5185..24cdc7cbb2 100644 --- a/src/rez/rex.py +++ b/src/rez/rex.py @@ -1114,8 +1114,15 @@ def __init__(self, manager) -> None: self._var_cache = dict((k, EnvironmentVariable(k, self)) for k in manager.parent_environ.keys()) + def _defined_keys(self): + # A key is cached on first access, even if it was never defined (eg a + # package's commands() referencing a variable that does not exist). + # Such keys must not be exposed by the mapping interface, otherwise + # iterating the environment raises RexUndefinedVariableError. + return [k for k in self._var_cache if not self.manager.undefined(k)] + def keys(self): - return self._var_cache.keys() + return dict.fromkeys(self._defined_keys()).keys() def __repr__(self) -> str: return '%s(%s)' % (self.__class__.__name__, str(self._var_cache)) @@ -1129,17 +1136,17 @@ def __setitem__(self, key, value) -> None: self[key].set(value) def __contains__(self, key) -> bool: - return (key in self._var_cache) + return (key in self._var_cache and not self.manager.undefined(key)) def __delitem__(self, key) -> None: del self._var_cache[key] def __iter__(self): - for key in self._var_cache.keys(): + for key in self._defined_keys(): yield key def __len__(self) -> int: - return len(self._var_cache) + return len(self._defined_keys()) class EnvironmentVariable(object): diff --git a/src/rez/tests/test_rex.py b/src/rez/tests/test_rex.py index ac3a8966df..de12d332b3 100644 --- a/src/rez/tests/test_rex.py +++ b/src/rez/tests/test_rex.py @@ -390,6 +390,27 @@ def _rex_3(): _test(_rex_2, env={"A": "foo"}, expected={"A": True, "B": False}) _test(_rex_3, env={}, expected="not b") + def test_11(self) -> None: + """Test that a failed env lookup doesn't leak into the env mapping.""" + + def _rex(): + try: + env.NOT_SET + except Exception: + pass + return { + "keys": sorted(env.keys()), + "contains": "NOT_SET" in env, + "len": len(env), + "values": [str(x) for x in env.values()], + } + + ex = self._create_executor(env={"A": "foo"}) + self.assertEqual( + ex.execute_function(_rex), + {"keys": ["A"], "contains": False, "len": 1, "values": ["foo"]}, + ) + def test_version_binding(self) -> None: """Test the Rex binding of the Version class.""" v = VersionBinding(Version("1.2.3alpha"))