3232"""
3333import copy
3434import os
35+ from contextlib import contextmanager
3536
37+ from easybuild .os_hook import OSProxy
3638from easybuild .base import fancylogger
3739from easybuild .tools .build_log import EasyBuildError , dry_run_msg
3840from easybuild .tools .config import build_option
4547
4648_log = fancylogger .getLogger ('environment' , fname = False )
4749
48- _changes = {}
50+ _contextes = {'' : {}}
51+ _curr_context = ''
4952
53+ def set_context (context_name , context = {}):
54+ """
55+ Set context for tracking environment changes.
56+ """
57+ global _curr_context
58+ _curr_context = context_name
59+ if context_name not in _contextes :
60+ if context is not None :
61+ context = copy .deepcopy (context )
62+ else :
63+ context = {}
64+ _contextes [context_name ] = context
65+
66+ def get_context ():
67+ """
68+ Return current context for tracking environment changes.
69+ """
70+ return _contextes [_curr_context ]
5071
5172def write_changes (filename ):
5273 """
5374 Write current changes to filename and reset environment afterwards
5475 """
5576 try :
5677 with open (filename , 'w' ) as script :
57- for key , changed_value in _changes .items ():
78+ for key , changed_value in get_context () .items ():
5879 script .write ('export %s=%s\n ' % (key , shell_quote (changed_value )))
5980 except IOError as err :
6081 raise EasyBuildError ("Failed to write to %s: %s" , filename , err )
@@ -65,32 +86,79 @@ def reset_changes():
6586 """
6687 Reset the changes tracked by this module
6788 """
68- global _changes
69- _changes = {}
89+ get_context ().clear ()
7090
7191
7292def get_changes ():
7393 """
7494 Return tracked changes made in environment.
7595 """
76- return _changes
96+ return get_context ()
97+
98+ def apply_context (context = None ):
99+ """Return the current environment with the changes tracked in the context applied.
100+
101+ Args:
102+ context (str, optional): The context to apply. Defaults to the current onee.
103+ """
104+ if context is None :
105+ context = _curr_context
106+ changes = get_context ()
107+ # print(f'Applying context {context} with changes: {changes}')
108+ curr_env = ORIG_OS_ENVIRON .copy ()
109+ for key , changed_value in changes .items ():
110+ if changed_value is None :
111+ curr_env .pop (key , None )
112+ else :
113+ curr_env [key ] = changed_value
114+ return curr_env
115+
116+
117+ def getvar (key , default = '' ):
118+ """
119+ Return value of key in the environment, or default if not found
120+ """
121+ return get_context ().get (key , os ._real_os .environ .get (key , default ))
122+
123+
124+ @contextmanager
125+ def with_environment (copy_current = False ):
126+ """Context manager to run code in a dedicated context"""
127+ # Get a key that does not exist in _contextes
128+ base = '_context_'
129+ cnt = 0
130+ while (context := f"{ base } { cnt } " ) in _contextes :
131+ cnt += 1
132+
133+ prev_context = _curr_context
134+ kwargs = {}
135+ if copy_current :
136+ kwargs ['context' ] = get_context ()
137+ set_context (context , ** kwargs )
138+ try :
139+ yield
140+ finally :
141+ set_context (prev_context )
142+ _contextes .pop (context , None )
77143
78144
79- def setvar (key , value , verbose = True , log_changes = True ):
145+ def setvar (key , value , verbose = True , log_changes = True , force_env = False ):
80146 """
81147 put key in the environment with value
82148 tracks added keys until write_changes has been called
83149
84150 :param verbose: include message in dry run output for defining this environment variable
85151 :param log_changes: show the change in the log
152+ :param force_env: if True, also set the variable in os.environ, eg for calls to python functions that rely
153+ on some specific environment such as TMPDIR for tempfile.
86154 """
87155 try :
88156 oldval_info = "previous value: '%s'" % os .environ [key ]
89157 except KeyError :
90158 oldval_info = "previously undefined"
91- # os.putenv() is not necessary. os.environ will call this.
92- os .environ [key ] = value
93- _changes [key ] = value
159+ if force_env :
160+ os . _real_os .environ [key ] = value
161+ get_context () [key ] = value
94162 if log_changes :
95163 _log .info ("Environment variable %s set to %s (%s)" , key , value , oldval_info )
96164
@@ -101,7 +169,27 @@ def setvar(key, value, verbose=True, log_changes=True):
101169 dry_run_msg (" export %s=%s" % (key , quoted_value ), silent = build_option ('silent' ))
102170
103171
104- def unset_env_vars (keys , verbose = True ):
172+ def appendvar (key , value , sep = os .pathsep , verbose = True , log_changes = True , force_env = False ):
173+ """append value to key in the environment, using sep as separator"""
174+ oldval = apply_context ().get (key )
175+ if oldval :
176+ newval = oldval + sep + value
177+ else :
178+ newval = value
179+ setvar (key , newval , verbose = verbose , log_changes = log_changes , force_env = force_env )
180+
181+
182+ def prependvar (key , value , sep = os .pathsep , verbose = True , log_changes = True , force_env = False ):
183+ """prepend value to key in the environment, using sep as separator"""
184+ oldval = apply_context ().get (key )
185+ if oldval :
186+ newval = value + sep + oldval
187+ else :
188+ newval = value
189+ setvar (key , newval , verbose = verbose , log_changes = log_changes , force_env = force_env )
190+
191+
192+ def unset_env_vars (keys , verbose = True , force_env = False ):
105193 """
106194 Unset the keys given in the environment
107195 Returns a dict with the old values of the unset keys
@@ -115,7 +203,9 @@ def unset_env_vars(keys, verbose=True):
115203 if key in os .environ :
116204 _log .info ("Unsetting environment variable %s (value: %s)" % (key , os .environ [key ]))
117205 old_environ [key ] = os .environ [key ]
118- del os .environ [key ]
206+ if force_env :
207+ del os ._real_os .environ [key ]
208+ get_context ()[key ] = None
119209 if verbose and build_option ('extended_dry_run' ):
120210 dry_run_msg (" unset %s # value was: %s" % (key , old_environ [key ]), silent = build_option ('silent' ))
121211
@@ -223,3 +313,29 @@ def sanitize_env():
223313 # unset all $PYTHON* environment variables
224314 keys_to_unset = [key for key in os .environ if key .startswith ('PYTHON' )]
225315 unset_env_vars (keys_to_unset , verbose = False )
316+
317+
318+ class MockEnviron (dict ):
319+ """Hook into os.environ and replace it with calls from this module to track changes to the environment."""
320+ def __getitem__ (self , key ):
321+ return getvar (key )
322+
323+ def __setitem__ (self , key , value ):
324+ setvar (key , value , verbose = False , log_changes = False )
325+
326+ def __delitem__ (self , key ):
327+ unset_env_vars ([key ], verbose = False )
328+
329+ def get (self , key , default = None ):
330+ return getvar (key , default )
331+
332+ def copy (self ):
333+ return apply_context ()
334+
335+ def __deepcopy__ (self , memo ):
336+ return apply_context ()
337+
338+
339+ OSProxy .register_override ('environ' , MockEnviron ())
340+ OSProxy .register_override ('getenv' , lambda key , default = None : getvar (key , default ))
341+ OSProxy .register_override ('unsetenv' , lambda key : unset_env_vars ([key ], verbose = False ))
0 commit comments