From 4300138e34afafe7851a3e95869e4557abbe56f0 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Sat, 25 Jul 2026 15:50:21 -0700 Subject: [PATCH 1/2] fix(rex): don't expose failed env lookups in the env mapping Accessing an undefined variable via env.FOO raises RexUndefinedVariableError, but EnvironmentDict.__getitem__ caches an EnvironmentVariable for that name regardless. A later env.values()/iteration - typically from an unrelated package resolved afterwards - then resolves that cached name and raises a second RexUndefinedVariableError, failing the whole resolve. Filter undefined names out of keys()/__iter__/__len__/__contains__ so the mapping interface only exposes variables that actually exist. Setting the variable later makes it visible again. Adds a regression test covering keys/values/len/contains after a failed lookup. Signed-off-by: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> --- src/rez/rex.py | 15 +++++++++++---- src/rez/tests/test_rex.py | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) 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")) From 7b0d5ca2c14ccdd039f74acb08b03ce57b5ebe47 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:17:49 -0700 Subject: [PATCH 2/2] chore: re-trigger CLA check