web-dev-qa-db-ja.com

Python-現在のプロセスの環境を一時的に変更する

次のコードを使用して、環境変数を一時的に変更します。

_@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インタプリタ)の他の場所。

29
_environ = dict(os.environ)  # or os.environ.copy()
try:

    ...

finally:
    os.environ.clear()
    os.environ.update(_environ)

次の実装をお勧めします。

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 で入手できます。

31
Laurent LAPORTE

私は同じことをしようとしていましたが、ユニットテストのために、 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内で空になります。

5
Sylhare

単体テストでは、オプションのパラメーターを指定したデコレーター関数を使用することをお勧めします。このようにして、変更された環境値をテスト関数全体に使用できます。以下のデコレータは、関数が例外を発生させた場合に、元の環境値も復元します。

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()
2
LietKynes

ここで要点を使用すると、ローカル、グローバルスコープ変数および環境変数を保存/復元できます: 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
0
Erik Aronesty

pytestでは、monkeypatchフィクスチャを使用して環境変数を一時的に設定できます。詳細については、 ドキュメント を参照してください。便宜上、ここにスニペットをコピーしました。

import pytest


def test_upper_to_lower(monkeypatch):
    """Set the USER env var to assert the behavior."""
    monkeypatch.setenv("USER", "TestingUser")
    assert get_os_user_lower() == "testinguser"
0
Vladimir Shteyn