【问题标题】:Python - temporarily modify the current process's environmentPython - 临时修改当前进程的环境
【发布时间】:2011-01-04 19:02:46
【问题描述】:

我使用以下代码临时修改环境变量。

@contextmanager
def _setenv(**mapping):
    """``with`` context to temporarily modify the environment variables"""
    backup_values = {}
    backup_remove = set()
    for key, value in mapping.items():
        if key in os.environ:
            backup_values[key] = os.environ[key]
        else:
            backup_remove.add(key)
        os.environ[key] = value

    try:
        yield
    finally:
        # restore old environment
        for k, v in backup_values.items():
            os.environ[k] = v
        for k in backup_remove:
            del os.environ[k]

这个with 上下文主要用于测试用例。例如,

def test_myapp_respects_this_envvar():
    with _setenv(MYAPP_PLUGINS_DIR='testsandbox/plugins'):
        myapp.plugins.register()
        [...]

我的问题:有没有一种简单/优雅的方式来写_setenv?我考虑过实际执行backup = os.environ.copy() 然后os.environ = backup .. 但我不确定这是否会影响程序行为(例如:如果os.environ 在Python 解释器的其他地方引用)。

【问题讨论】:

    标签: environment-variables python


    【解决方案1】:

    我建议你以下实现:

    import contextlib
    import os
    
    
    @contextlib.contextmanager
    def set_env(**environ):
        """
        Temporarily set the process environment variables.
    
        >>> with set_env(PLUGINS_DIR=u'test/plugins'):
        ...   "PLUGINS_DIR" in os.environ
        True
    
        >>> "PLUGINS_DIR" in os.environ
        False
    
        :type environ: dict[str, unicode]
        :param environ: Environment variables to set
        """
        old_environ = dict(os.environ)
        os.environ.update(environ)
        try:
            yield
        finally:
            os.environ.clear()
            os.environ.update(old_environ)
    

    编辑:更高级的实现

    下面的上下文管理器可用于添加/删除/更新您的环境变量:

    import contextlib
    import os
    
    
    @contextlib.contextmanager
    def modified_environ(*remove, **update):
        """
        Temporarily updates the ``os.environ`` dictionary in-place.
    
        The ``os.environ`` dictionary is updated in-place so that the modification
        is sure to work in all situations.
    
        :param remove: Environment variables to remove.
        :param update: Dictionary of environment variables and values to add/update.
        """
        env = os.environ
        update = update or {}
        remove = remove or []
    
        # List of environment variables being updated or removed.
        stomped = (set(update.keys()) | set(remove)) & set(env.keys())
        # Environment variables and values to restore on exit.
        update_after = {k: env[k] for k in stomped}
        # Environment variables and values to remove on exit.
        remove_after = frozenset(k for k in update if k not in env)
    
        try:
            env.update(update)
            [env.pop(k, None) for k in remove]
            yield
        finally:
            env.update(update_after)
            [env.pop(k) for k in remove_after]
    

    用法示例:

    >>> with modified_environ('HOME', LD_LIBRARY_PATH='/my/path/to/lib'):
    ...     home = os.environ.get('HOME')
    ...     path = os.environ.get("LD_LIBRARY_PATH")
    >>> home is None
    True
    >>> path
    '/my/path/to/lib'
    
    >>> home = os.environ.get('HOME')
    >>> path = os.environ.get("LD_LIBRARY_PATH")
    >>> home is None
    False
    >>> path is None
    True
    

    EDIT2

    GitHub 上提供了此上下文管理器的演示。

    【讨论】:

    • 对于这个老问题的访问者,我没有看到这个答案有任何明显的缺陷,它比原来的更完整和有用。
    • 这应该是 python 的一部分 - 或其他东西。弄乱测试环境是令人讨厌的 - 但有时是必要的 - 东西,并且可能严重破坏,无效或以其他方式使位于环境混乱测试功能下游的测试:(
    • 在哪些情况下,第一种方法行不通?
    【解决方案2】:
    _environ = dict(os.environ)  # or os.environ.copy()
    try:
    
        ...
    
    finally:
        os.environ.clear()
        os.environ.update(_environ)
    

    【讨论】:

    • 好。不过,我使用的是.copy() 而不是dict()
    • 好的,但是如果在 [...] 期间发生故障(异常),环境变量不会恢复:为此需要 try ... finally ...
    【解决方案3】:

    我也想做同样的事情,但对于单元测试,这是我使用 unittest.mock.patch 函数完成的方法:

    def test_function_with_different_env_variable():
        with mock.patch.dict('os.environ', {'hello': 'world'}, clear=True):
            self.assertEqual(os.environ.get('hello'), 'world')
            self.assertEqual(len(os.environ), 1)
    

    基本上将unittest.mock.patch.dictclear=True 一起使用,我们将os.environ 制作为仅包含{'hello': 'world'} 的字典。

    • 删除clear=True 将让原来的os.environ 并在{'hello': 'world'} 中添加/替换指定的键/值对。

    • 删除{'hello': 'world'} 只会创建一个空字典,因此os.envrionwith 中将是空的。

    【讨论】:

    • 非常感谢@sylhare 和@NathanielFord!这是我找到有关如何在模拟时从字典中删除键的信息的唯一地方。将值设置为 None 会引发错误 - 它必须是字符串
    【解决方案4】:

    pytest 中,您可以使用monkeypatch 夹具临时设置环境变量。有关详细信息,请参阅the docs。为了您的方便,我在这里复制了一个 sn-p。

    import os
    import pytest
    from typing import Any, NewType
    
    # Alias for the ``type`` of monkeypatch fixture.
    MonkeyPatchFixture = NewType("MonkeyPatchFixture", Any)
    
    
    # This is the function we will test below to demonstrate the ``monkeypatch`` fixture.
    def get_lowercase_env_var(env_var_name: str) -> str:
        """
        Return the value of an environment variable. Variable value is made all lowercase.
    
        :param env_var_name:
            The name of the environment variable to return.
        :return:
            The value of the environment variable, with all letters in lowercase.
        """
        env_variable_value = os.environ[env_var_name]
        lowercase_env_variable = env_variable_value.lower()
        return lowercase_env_variable
    
    
    def test_get_lowercase_env_var(monkeypatch: MonkeyPatchFixture) -> None:
        """
        Test that the function under test indeed returns the lowercase-ified
        form of ENV_VAR_UNDER_TEST.
        """
        name_of_env_var_under_test = "ENV_VAR_UNDER_TEST"
        env_var_value_under_test = "EnvVarValue"
        expected_result = "envvarvalue"
        # KeyError because``ENV_VAR_UNDER_TEST`` was looked up in the os.environ dictionary before its value was set by ``monkeypatch``.
        with pytest.raises(KeyError):
            assert get_lowercase_env_var(name_of_env_var_under_test) == expected_result
        # Temporarily set the environment variable's value.
        monkeypatch.setenv(name_of_env_var_under_test, env_var_value_under_test)
        assert get_lowercase_env_var(name_of_env_var_under_test) == expected_result
    
    
    def test_get_lowercase_env_var_fails(monkeypatch: MonkeyPatchFixture) -> None:
        """
        This demonstrates that ENV_VAR_UNDER_TEST is reset in every test function.
        """
        env_var_name_under_test = "ENV_VAR_UNDER_TEST"
        expected_result = "envvarvalue"
        with pytest.raises(KeyError):
            assert get_lowercase_env_var(env_var_name_under_test) == expected_result
    

    【讨论】:

    • 嗯,那个例子很糟糕,不是吗。我用我认为更有帮助的东西更新了代码。如果您喜欢新版本,请告诉我。 (两项测试均通过)。
    【解决方案5】:

    对于单元测试,我更喜欢使用带有可选参数的装饰器函数。这样我就可以将修改后的环境值用于整个测试功能。如果函数引发异常,下面的装饰器还会恢复原始环境值:

    import os
    
    def patch_environ(new_environ=None, clear_orig=False):
        if not new_environ:
            new_environ = dict()
    
        def actual_decorator(func):
            from functools import wraps
    
            @wraps(func)
            def wrapper(*args, **kwargs):
                original_env = dict(os.environ)
    
                if clear_orig:
                    os.environ.clear()
    
                os.environ.update(new_environ)
                try:
                    result = func(*args, **kwargs)
                except:
                    raise
                finally: # restore even if Exception was raised
                    os.environ = original_env
    
                return result
    
            return wrapper
    
        return actual_decorator
    

    在单元测试中的使用:

    class Something:
        @staticmethod
        def print_home():
            home = os.environ.get('HOME', 'unknown')
            print("HOME = {0}".format(home))
    
    
    class SomethingTest(unittest.TestCase):
        @patch_environ({'HOME': '/tmp/test'})
        def test_environ_based_something(self):
            Something.print_home() # prints: HOME = /tmp/test
    
    unittest.main()
    

    【讨论】:

      【解决方案6】:

      使用这里的要点,您可以保存/恢复本地、全局范围变量和环境变量: https://gist.github.com/earonesty/ac0617a5672ae1a41be1eaf316dd63e4

      import os
      from varlib import vartemp, envtemp
      
      x = 3
      y = 4
      
      with vartemp({'x':93,'y':94}):
         print(x)
         print(y)
      print(x)
      print(y)
      
      with envtemp({'foo':'bar'}):
          print(os.getenv('foo'))
      
      print(os.getenv('foo'))
      

      这个输出:

      93
      94
      3
      4
      bar
      None
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2010-11-13
        • 2011-01-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-01-06
        相关资源
        最近更新 更多