meerschaum.config.environment
Patch the runtime configuration from environment variables.
1#! /usr/bin/env python3 2# -*- coding: utf-8 -*- 3# vim:fenc=utf-8 4 5""" 6Patch the runtime configuration from environment variables. 7""" 8 9import os 10import re 11import json 12import contextlib 13import copy 14import pathlib 15import threading 16 17from meerschaum.utils.typing import List, Union, Dict, Any, Optional 18from meerschaum._internal.static import STATIC_CONFIG 19from meerschaum import _core 20 21### Serialize the process-global swap of `os.environ` and `meerschaum.config.paths` 22### globals performed by `replace_env`. A daemon process runs several threads 23### concurrently (the job target plus the `check-jobs` `RepeatTimer`), so two 24### overlapping `replace_env` calls could otherwise leave these globals in a torn 25### state. 26_replace_env_lock = threading.RLock() 27 28 29def apply_environment_patches(env: Optional[Dict[str, Any]] = None) -> None: 30 """ 31 Apply patches defined in `MRSM_CONFIG` and `MRSM_PATCH`. 32 """ 33 config_var = STATIC_CONFIG['environment']['config'] 34 patch_var = STATIC_CONFIG['environment']['patch'] 35 ### Nothing to apply: do not touch the native registry (importing `meerschaum.config` runs 36 ### this, and a bare import must not load the extension). 37 _env = env if env is not None else os.environ 38 if config_var not in _env and patch_var not in _env: 39 return 40 return _core.apply_environment_patches(env) 41 42 43def apply_environment_config(env_var: str, env: Optional[Dict[str, Any]] = None) -> None: 44 """ 45 Parse a dictionary (simple or JSON) from an environment variable 46 and apply it to the current configuration. 47 """ 48 from meerschaum.config import get_config, set_config, _config 49 from meerschaum.config._patch import apply_patch_to_config 50 51 env = env if env is not None else os.environ 52 53 if env_var not in env: 54 return 55 56 from meerschaum.utils.misc import string_to_dict 57 try: 58 _patch = string_to_dict(str(os.environ[env_var]).lstrip()) 59 except Exception: 60 _patch = None 61 62 error_msg = ( 63 f"Environment variable {env_var} is set but cannot be parsed.\n" 64 f"Unset {env_var} or change to JSON or simplified dictionary format " 65 "(see --help, under params for formatting)\n" 66 f"{env_var} is set to:\n{os.environ[env_var]}\n" 67 f"Skipping patching os environment into config..." 68 ) 69 70 if not isinstance(_patch, dict): 71 print(error_msg) 72 return 73 74 valids = [] 75 76 def load_key(key: str) -> Union[Dict[str, Any], None]: 77 try: 78 c = get_config(key, warn=False) 79 except Exception: 80 c = None 81 return c 82 83 ### This was multi-threaded, but I ran into all sorts of locking issues. 84 keys = list(_patch.keys()) 85 for key in keys: 86 _ = load_key(key) 87 88 ### Load and patch config files. 89 set_config( 90 apply_patch_to_config( 91 _config(), 92 _patch, 93 ) 94 ) 95 96 97def apply_environment_uris(env: Optional[Dict[str, Any]] = None) -> None: 98 """ 99 Patch temporary connectors defined in environment variables which start with 100 `MRSM_SQL_` or `MRSM_API_`. 101 """ 102 env_vars = get_connector_env_vars(env=env) 103 if not env_vars: 104 return 105 return _core.apply_environment_uris(env) 106 107 108def get_connector_env_regex() -> str: 109 """ 110 Return the regex pattern for valid environment variable names for instance connectors. 111 """ 112 return STATIC_CONFIG['environment']['uri_regex'] 113 114 115def get_connector_env_vars(env: Optional[Dict[str, Any]] = None) -> List[str]: 116 """ 117 Get the names of the environment variables which match the Meerschaum connector regex. 118 119 Examples 120 -------- 121 >>> get_connector_environment_vars() 122 ['MRSM_SQL_FOO'] 123 """ 124 uri_regex = get_connector_env_regex() 125 env_vars = [] 126 127 env = env if env is not None else os.environ 128 129 for env_var in env: 130 matched = re.match(uri_regex, env_var) 131 if matched is None: 132 continue 133 if env_var in STATIC_CONFIG['environment'].values(): 134 continue 135 env_vars.append(env_var) 136 137 return env_vars 138 139 140def apply_connector_uri(env_var: str, env: Optional[Dict[str, Any]] = None) -> None: 141 """ 142 Parse and validate a URI obtained from an environment variable. 143 """ 144 from meerschaum.config import get_config, set_config, _config 145 from meerschaum.config._patch import apply_patch_to_config 146 from meerschaum.config._read_config import search_and_substitute_config 147 from meerschaum.utils.warnings import warn 148 149 env = env if env is not None else os.environ 150 151 if env_var not in env: 152 return 153 154 uri_regex = get_connector_env_regex() 155 matched = re.match(uri_regex, env_var) 156 groups = matched.groups() 157 typ, label = groups[0].lower(), groups[1].lower() 158 if not typ or not label: 159 return 160 161 uri = env[env_var] 162 163 if uri.lstrip().startswith('{') and uri.rstrip().endswith('}'): 164 try: 165 conn_attrs = json.loads(uri) 166 except Exception: 167 warn(f"Unable to parse JSON for environment connector '{typ}:{label}'.") 168 conn_attrs = {'uri': uri} 169 else: 170 conn_attrs = {'uri': uri} 171 172 set_config( 173 apply_patch_to_config( 174 {'meerschaum': get_config('meerschaum')}, 175 {'meerschaum': {'connectors': {typ: {label: conn_attrs}}}}, 176 ) 177 ) 178 179 180def get_env_vars(env: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: 181 """ 182 Return all environment variables which begin with `'MRSM_'`. 183 """ 184 prefix = STATIC_CONFIG['environment']['prefix'] 185 env = env if env is not None else os.environ 186 return { 187 env_var: env_val 188 for env_var, env_val in env.items() 189 if env_var.startswith(prefix) 190 } 191 192 193def get_daemon_env_vars(env: Optional[Dict[str, Any]] = None) -> Dict[str, str]: 194 """ 195 Return the daemon-specific environment vars in the current environment. 196 """ 197 env = env if env is not None else os.environ 198 199 daemon_env_var_names = ( 200 STATIC_CONFIG['environment']['systemd_log_path'], 201 STATIC_CONFIG['environment']['systemd_result_path'], 202 STATIC_CONFIG['environment']['systemd_delete_job'], 203 STATIC_CONFIG['environment']['systemd_stdin_path'], 204 STATIC_CONFIG['environment']['daemon_id'], 205 ) 206 return { 207 env_var: env.get(env_var, '') 208 for env_var in daemon_env_var_names 209 if env_var in env 210 } 211 212 213@contextlib.contextmanager 214def replace_env(env: Union[Dict[str, Any], None]): 215 """ 216 Temporarily replace environment variables and current configuration. 217 218 Parameters 219 ---------- 220 env: Dict[str, Any] 221 The new environment dictionary to be patched on `os.environ`. 222 """ 223 if env is None: 224 yield 225 return 226 227 from meerschaum.config import _config, set_config 228 import meerschaum.config.paths as paths 229 import meerschaum.utils.venv as venv_utils 230 import sys 231 232 old_environ = dict(os.environ) 233 old_config = copy.deepcopy(_config()) 234 old_root_dir_path = paths.ROOT_DIR_PATH 235 old_plugins_dir_paths = paths.PLUGINS_DIR_PATHS 236 old_venvs_dir_path = paths.VIRTENV_RESOURCES_PATH 237 old_config_dir_path = paths.CONFIG_DIR_PATH 238 with venv_utils.LOCKS['active_venvs'], venv_utils.LOCKS['sys.path']: 239 old_sys_path = list(sys.path) 240 old_active_venvs = set(venv_utils.active_venvs) 241 old_active_venvs_counts = dict(venv_utils.active_venvs_counts) 242 old_active_venvs_order = list(venv_utils.active_venvs_order) 243 old_threads_active_venvs = { 244 thread_id: dict(active_counts) 245 for thread_id, active_counts in venv_utils.threads_active_venvs.items() 246 } 247 248 root_dir_env_var = STATIC_CONFIG['environment']['root'] 249 plugins_dir_env_var = STATIC_CONFIG['environment']['plugins'] 250 config_dir_env_var = STATIC_CONFIG['environment']['config_dir'] 251 venvs_dir_env_var = STATIC_CONFIG['environment']['venvs'] 252 253 replaced_root = False 254 replaced_plugins = False 255 replaced_venvs = False 256 replaced_config_dir = False 257 258 ### Hold the lock only while swapping the process-global state, not across the 259 ### `yield` (which runs arbitrary user code and could deadlock or serialize the 260 ### whole daemon). 261 with _replace_env_lock: 262 os.environ.update(env) 263 264 if root_dir_env_var in env: 265 root_dir_path = pathlib.Path(env[root_dir_env_var]) 266 paths.set_root(root_dir_path) 267 replaced_root = True 268 269 if plugins_dir_env_var in env: 270 plugins_dir_paths = env[plugins_dir_env_var] 271 paths.set_plugins_dir_paths(plugins_dir_paths) 272 replaced_plugins = True 273 274 if venvs_dir_env_var in env: 275 venv_dir_path = pathlib.Path(env[venvs_dir_env_var]) 276 paths.set_venvs_dir_path(venv_dir_path) 277 replaced_venvs = True 278 279 if config_dir_env_var in env: 280 config_dir_path = pathlib.Path(env[config_dir_env_var]) 281 paths.set_config_dir_path(config_dir_path) 282 replaced_config_dir = True 283 284 apply_environment_patches(env) 285 apply_environment_uris(env) 286 287 ### The `plugins` package in `sys.modules` caches a `__path__` resolved 288 ### against the previous scope's `PLUGINS_RESOURCES_PATH`. If the root or 289 ### plugins-dir scope actually changed, drop that cache so the next plugin 290 ### import re-discovers plugins under the new scope (otherwise module-level 291 ### `from_plugin_import` of a sibling plugin fails with 292 ### "Unable to import plugin '<name>'" and connector-providing plugins 293 ### don't register their connectors). 294 plugins_scope_changed = ( 295 (replaced_root and paths.ROOT_DIR_PATH != old_root_dir_path) 296 or (replaced_plugins and paths.PLUGINS_DIR_PATHS != old_plugins_dir_paths) 297 ) 298 if plugins_scope_changed: 299 from meerschaum.plugins import invalidate_plugins_cache 300 invalidate_plugins_cache() 301 302 try: 303 yield 304 finally: 305 with _replace_env_lock: 306 ### Restore `os.environ` by diffing, NOT `clear()` + `update()`. 307 ### `clear()` blanks the entire environment for a window during which a 308 ### concurrent thread (e.g. a daemon's `sync_plugins_symlinks`) sees 309 ### `MRSM_PLUGINS_DIR` as absent and falls back to the host plugins dir, 310 ### so it never creates a project's plugin symlinks (the `plugin:<name>` 311 ### job then fails to import). Keys present in `old_environ` are never 312 ### removed, so vars like `MRSM_PLUGINS_DIR` never momentarily vanish. 313 for key in [k for k in os.environ if k not in old_environ]: 314 del os.environ[key] 315 for key, val in old_environ.items(): 316 if os.environ.get(key) != val: 317 os.environ[key] = val 318 319 if replaced_root: 320 paths.set_root(old_root_dir_path) 321 322 if replaced_plugins: 323 paths.set_plugins_dir_paths(old_plugins_dir_paths) 324 325 if replaced_venvs: 326 paths.set_venvs_dir_path(old_venvs_dir_path) 327 328 if replaced_config_dir: 329 paths.set_config_dir_path(old_config_dir_path) 330 331 ### Venv names are resolved relative to the current root. Restore both 332 ### the import path and activation bookkeeping after a scoped root swap. 333 with venv_utils.LOCKS['active_venvs'], venv_utils.LOCKS['sys.path']: 334 sys.path[:] = old_sys_path 335 venv_utils.active_venvs.clear() 336 venv_utils.active_venvs.update(old_active_venvs) 337 venv_utils.active_venvs_counts.clear() 338 venv_utils.active_venvs_counts.update(old_active_venvs_counts) 339 venv_utils.active_venvs_order[:] = old_active_venvs_order 340 venv_utils.threads_active_venvs.clear() 341 venv_utils.threads_active_venvs.update(old_threads_active_venvs) 342 343 _config().clear() 344 set_config(old_config) 345 346 ### Mirror the invalidation on exit: the scope is being restored, 347 ### so the `plugins` package cached during the temporary scope is 348 ### now the stale one. 349 if plugins_scope_changed: 350 from meerschaum.plugins import invalidate_plugins_cache 351 invalidate_plugins_cache()
30def apply_environment_patches(env: Optional[Dict[str, Any]] = None) -> None: 31 """ 32 Apply patches defined in `MRSM_CONFIG` and `MRSM_PATCH`. 33 """ 34 config_var = STATIC_CONFIG['environment']['config'] 35 patch_var = STATIC_CONFIG['environment']['patch'] 36 ### Nothing to apply: do not touch the native registry (importing `meerschaum.config` runs 37 ### this, and a bare import must not load the extension). 38 _env = env if env is not None else os.environ 39 if config_var not in _env and patch_var not in _env: 40 return 41 return _core.apply_environment_patches(env)
Apply patches defined in MRSM_CONFIG and MRSM_PATCH.
44def apply_environment_config(env_var: str, env: Optional[Dict[str, Any]] = None) -> None: 45 """ 46 Parse a dictionary (simple or JSON) from an environment variable 47 and apply it to the current configuration. 48 """ 49 from meerschaum.config import get_config, set_config, _config 50 from meerschaum.config._patch import apply_patch_to_config 51 52 env = env if env is not None else os.environ 53 54 if env_var not in env: 55 return 56 57 from meerschaum.utils.misc import string_to_dict 58 try: 59 _patch = string_to_dict(str(os.environ[env_var]).lstrip()) 60 except Exception: 61 _patch = None 62 63 error_msg = ( 64 f"Environment variable {env_var} is set but cannot be parsed.\n" 65 f"Unset {env_var} or change to JSON or simplified dictionary format " 66 "(see --help, under params for formatting)\n" 67 f"{env_var} is set to:\n{os.environ[env_var]}\n" 68 f"Skipping patching os environment into config..." 69 ) 70 71 if not isinstance(_patch, dict): 72 print(error_msg) 73 return 74 75 valids = [] 76 77 def load_key(key: str) -> Union[Dict[str, Any], None]: 78 try: 79 c = get_config(key, warn=False) 80 except Exception: 81 c = None 82 return c 83 84 ### This was multi-threaded, but I ran into all sorts of locking issues. 85 keys = list(_patch.keys()) 86 for key in keys: 87 _ = load_key(key) 88 89 ### Load and patch config files. 90 set_config( 91 apply_patch_to_config( 92 _config(), 93 _patch, 94 ) 95 )
Parse a dictionary (simple or JSON) from an environment variable and apply it to the current configuration.
98def apply_environment_uris(env: Optional[Dict[str, Any]] = None) -> None: 99 """ 100 Patch temporary connectors defined in environment variables which start with 101 `MRSM_SQL_` or `MRSM_API_`. 102 """ 103 env_vars = get_connector_env_vars(env=env) 104 if not env_vars: 105 return 106 return _core.apply_environment_uris(env)
Patch temporary connectors defined in environment variables which start with
MRSM_SQL_ or MRSM_API_.
109def get_connector_env_regex() -> str: 110 """ 111 Return the regex pattern for valid environment variable names for instance connectors. 112 """ 113 return STATIC_CONFIG['environment']['uri_regex']
Return the regex pattern for valid environment variable names for instance connectors.
116def get_connector_env_vars(env: Optional[Dict[str, Any]] = None) -> List[str]: 117 """ 118 Get the names of the environment variables which match the Meerschaum connector regex. 119 120 Examples 121 -------- 122 >>> get_connector_environment_vars() 123 ['MRSM_SQL_FOO'] 124 """ 125 uri_regex = get_connector_env_regex() 126 env_vars = [] 127 128 env = env if env is not None else os.environ 129 130 for env_var in env: 131 matched = re.match(uri_regex, env_var) 132 if matched is None: 133 continue 134 if env_var in STATIC_CONFIG['environment'].values(): 135 continue 136 env_vars.append(env_var) 137 138 return env_vars
Get the names of the environment variables which match the Meerschaum connector regex.
Examples
>>> get_connector_environment_vars()
['MRSM_SQL_FOO']
141def apply_connector_uri(env_var: str, env: Optional[Dict[str, Any]] = None) -> None: 142 """ 143 Parse and validate a URI obtained from an environment variable. 144 """ 145 from meerschaum.config import get_config, set_config, _config 146 from meerschaum.config._patch import apply_patch_to_config 147 from meerschaum.config._read_config import search_and_substitute_config 148 from meerschaum.utils.warnings import warn 149 150 env = env if env is not None else os.environ 151 152 if env_var not in env: 153 return 154 155 uri_regex = get_connector_env_regex() 156 matched = re.match(uri_regex, env_var) 157 groups = matched.groups() 158 typ, label = groups[0].lower(), groups[1].lower() 159 if not typ or not label: 160 return 161 162 uri = env[env_var] 163 164 if uri.lstrip().startswith('{') and uri.rstrip().endswith('}'): 165 try: 166 conn_attrs = json.loads(uri) 167 except Exception: 168 warn(f"Unable to parse JSON for environment connector '{typ}:{label}'.") 169 conn_attrs = {'uri': uri} 170 else: 171 conn_attrs = {'uri': uri} 172 173 set_config( 174 apply_patch_to_config( 175 {'meerschaum': get_config('meerschaum')}, 176 {'meerschaum': {'connectors': {typ: {label: conn_attrs}}}}, 177 ) 178 )
Parse and validate a URI obtained from an environment variable.
181def get_env_vars(env: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: 182 """ 183 Return all environment variables which begin with `'MRSM_'`. 184 """ 185 prefix = STATIC_CONFIG['environment']['prefix'] 186 env = env if env is not None else os.environ 187 return { 188 env_var: env_val 189 for env_var, env_val in env.items() 190 if env_var.startswith(prefix) 191 }
Return all environment variables which begin with 'MRSM_'.
194def get_daemon_env_vars(env: Optional[Dict[str, Any]] = None) -> Dict[str, str]: 195 """ 196 Return the daemon-specific environment vars in the current environment. 197 """ 198 env = env if env is not None else os.environ 199 200 daemon_env_var_names = ( 201 STATIC_CONFIG['environment']['systemd_log_path'], 202 STATIC_CONFIG['environment']['systemd_result_path'], 203 STATIC_CONFIG['environment']['systemd_delete_job'], 204 STATIC_CONFIG['environment']['systemd_stdin_path'], 205 STATIC_CONFIG['environment']['daemon_id'], 206 ) 207 return { 208 env_var: env.get(env_var, '') 209 for env_var in daemon_env_var_names 210 if env_var in env 211 }
Return the daemon-specific environment vars in the current environment.
214@contextlib.contextmanager 215def replace_env(env: Union[Dict[str, Any], None]): 216 """ 217 Temporarily replace environment variables and current configuration. 218 219 Parameters 220 ---------- 221 env: Dict[str, Any] 222 The new environment dictionary to be patched on `os.environ`. 223 """ 224 if env is None: 225 yield 226 return 227 228 from meerschaum.config import _config, set_config 229 import meerschaum.config.paths as paths 230 import meerschaum.utils.venv as venv_utils 231 import sys 232 233 old_environ = dict(os.environ) 234 old_config = copy.deepcopy(_config()) 235 old_root_dir_path = paths.ROOT_DIR_PATH 236 old_plugins_dir_paths = paths.PLUGINS_DIR_PATHS 237 old_venvs_dir_path = paths.VIRTENV_RESOURCES_PATH 238 old_config_dir_path = paths.CONFIG_DIR_PATH 239 with venv_utils.LOCKS['active_venvs'], venv_utils.LOCKS['sys.path']: 240 old_sys_path = list(sys.path) 241 old_active_venvs = set(venv_utils.active_venvs) 242 old_active_venvs_counts = dict(venv_utils.active_venvs_counts) 243 old_active_venvs_order = list(venv_utils.active_venvs_order) 244 old_threads_active_venvs = { 245 thread_id: dict(active_counts) 246 for thread_id, active_counts in venv_utils.threads_active_venvs.items() 247 } 248 249 root_dir_env_var = STATIC_CONFIG['environment']['root'] 250 plugins_dir_env_var = STATIC_CONFIG['environment']['plugins'] 251 config_dir_env_var = STATIC_CONFIG['environment']['config_dir'] 252 venvs_dir_env_var = STATIC_CONFIG['environment']['venvs'] 253 254 replaced_root = False 255 replaced_plugins = False 256 replaced_venvs = False 257 replaced_config_dir = False 258 259 ### Hold the lock only while swapping the process-global state, not across the 260 ### `yield` (which runs arbitrary user code and could deadlock or serialize the 261 ### whole daemon). 262 with _replace_env_lock: 263 os.environ.update(env) 264 265 if root_dir_env_var in env: 266 root_dir_path = pathlib.Path(env[root_dir_env_var]) 267 paths.set_root(root_dir_path) 268 replaced_root = True 269 270 if plugins_dir_env_var in env: 271 plugins_dir_paths = env[plugins_dir_env_var] 272 paths.set_plugins_dir_paths(plugins_dir_paths) 273 replaced_plugins = True 274 275 if venvs_dir_env_var in env: 276 venv_dir_path = pathlib.Path(env[venvs_dir_env_var]) 277 paths.set_venvs_dir_path(venv_dir_path) 278 replaced_venvs = True 279 280 if config_dir_env_var in env: 281 config_dir_path = pathlib.Path(env[config_dir_env_var]) 282 paths.set_config_dir_path(config_dir_path) 283 replaced_config_dir = True 284 285 apply_environment_patches(env) 286 apply_environment_uris(env) 287 288 ### The `plugins` package in `sys.modules` caches a `__path__` resolved 289 ### against the previous scope's `PLUGINS_RESOURCES_PATH`. If the root or 290 ### plugins-dir scope actually changed, drop that cache so the next plugin 291 ### import re-discovers plugins under the new scope (otherwise module-level 292 ### `from_plugin_import` of a sibling plugin fails with 293 ### "Unable to import plugin '<name>'" and connector-providing plugins 294 ### don't register their connectors). 295 plugins_scope_changed = ( 296 (replaced_root and paths.ROOT_DIR_PATH != old_root_dir_path) 297 or (replaced_plugins and paths.PLUGINS_DIR_PATHS != old_plugins_dir_paths) 298 ) 299 if plugins_scope_changed: 300 from meerschaum.plugins import invalidate_plugins_cache 301 invalidate_plugins_cache() 302 303 try: 304 yield 305 finally: 306 with _replace_env_lock: 307 ### Restore `os.environ` by diffing, NOT `clear()` + `update()`. 308 ### `clear()` blanks the entire environment for a window during which a 309 ### concurrent thread (e.g. a daemon's `sync_plugins_symlinks`) sees 310 ### `MRSM_PLUGINS_DIR` as absent and falls back to the host plugins dir, 311 ### so it never creates a project's plugin symlinks (the `plugin:<name>` 312 ### job then fails to import). Keys present in `old_environ` are never 313 ### removed, so vars like `MRSM_PLUGINS_DIR` never momentarily vanish. 314 for key in [k for k in os.environ if k not in old_environ]: 315 del os.environ[key] 316 for key, val in old_environ.items(): 317 if os.environ.get(key) != val: 318 os.environ[key] = val 319 320 if replaced_root: 321 paths.set_root(old_root_dir_path) 322 323 if replaced_plugins: 324 paths.set_plugins_dir_paths(old_plugins_dir_paths) 325 326 if replaced_venvs: 327 paths.set_venvs_dir_path(old_venvs_dir_path) 328 329 if replaced_config_dir: 330 paths.set_config_dir_path(old_config_dir_path) 331 332 ### Venv names are resolved relative to the current root. Restore both 333 ### the import path and activation bookkeeping after a scoped root swap. 334 with venv_utils.LOCKS['active_venvs'], venv_utils.LOCKS['sys.path']: 335 sys.path[:] = old_sys_path 336 venv_utils.active_venvs.clear() 337 venv_utils.active_venvs.update(old_active_venvs) 338 venv_utils.active_venvs_counts.clear() 339 venv_utils.active_venvs_counts.update(old_active_venvs_counts) 340 venv_utils.active_venvs_order[:] = old_active_venvs_order 341 venv_utils.threads_active_venvs.clear() 342 venv_utils.threads_active_venvs.update(old_threads_active_venvs) 343 344 _config().clear() 345 set_config(old_config) 346 347 ### Mirror the invalidation on exit: the scope is being restored, 348 ### so the `plugins` package cached during the temporary scope is 349 ### now the stale one. 350 if plugins_scope_changed: 351 from meerschaum.plugins import invalidate_plugins_cache 352 invalidate_plugins_cache()
Temporarily replace environment variables and current configuration.
Parameters
- env (Dict[str, Any]):
The new environment dictionary to be patched on
os.environ.