From 30c873c7ad7e469c05362fbf0b68e3c11722ca63 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 10 Mar 2019 02:19:06 +0100 Subject: [PATCH 1/4] pytester: allow for passing in env --- src/_pytest/pytester.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index d474df4b94e..f74297983ba 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1035,26 +1035,26 @@ def collect_by_name(self, modcol, name): def popen(self, cmdargs, stdout, stderr, **kw): """Invoke subprocess.Popen. - This calls subprocess.Popen making sure the current working directory + This calls subprocess.Popen, making sure the current working directory is in the PYTHONPATH. You probably want to use :py:meth:`run` instead. - """ env = os.environ.copy() - env["PYTHONPATH"] = os.pathsep.join( - filter(None, [os.getcwd(), env.get("PYTHONPATH", "")]) - ) + env_update = kw.get("env", {}) + if "PYTHONPATH" not in env_update: + env_update["PYTHONPATH"] = os.pathsep.join( + filter(None, [os.getcwd(), env.get("PYTHONPATH", "")]) + ) # Do not load user config. - env["HOME"] = str(self.tmpdir) - env["USERPROFILE"] = env["HOME"] - kw["env"] = env + env_update["HOME"] = str(self.tmpdir) + env_update["USERPROFILE"] = env["HOME"] + env.update(env_update) popen = subprocess.Popen( - cmdargs, stdin=subprocess.PIPE, stdout=stdout, stderr=stderr, **kw + cmdargs, stdin=subprocess.PIPE, stdout=stdout, stderr=stderr, env=env, **kw ) popen.stdin.close() - return popen def run(self, *cmdargs, **kwargs): From 38e93519a03838d4dc3cdf42321cfd8f2313a13b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 4 Apr 2019 16:05:46 +0200 Subject: [PATCH 2/4] Factor out _get_isolated_env --- src/_pytest/pytester.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index f74297983ba..45e3176ecb4 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -790,6 +790,14 @@ def inline_genitems(self, *args): items = [x.item for x in rec.getcalls("pytest_itemcollected")] return items, rec + def _get_isolated_env(self): + tmpdir = str(self.tmpdir) + return ( + # Do not load user config. + ("HOME", tmpdir), + ("USERPROFILE", tmpdir), + ) + def inline_run(self, *args, **kwargs): """Run ``pytest.main()`` in-process, returning a HookRecorder. @@ -811,8 +819,8 @@ def inline_run(self, *args, **kwargs): try: # Do not load user config (during runs only). mp_run = MonkeyPatch() - mp_run.setenv("HOME", str(self.tmpdir)) - mp_run.setenv("USERPROFILE", str(self.tmpdir)) + for k, v in self._get_isolated_env(): + mp_run.setenv(k, v) finalizers.append(mp_run.undo) # When running pytest inline any plugins active in the main test @@ -1040,16 +1048,18 @@ def popen(self, cmdargs, stdout, stderr, **kw): You probably want to use :py:meth:`run` instead. """ - env = os.environ.copy() - env_update = kw.get("env", {}) - if "PYTHONPATH" not in env_update: - env_update["PYTHONPATH"] = os.pathsep.join( - filter(None, [os.getcwd(), env.get("PYTHONPATH", "")]) - ) - # Do not load user config. - env_update["HOME"] = str(self.tmpdir) - env_update["USERPROFILE"] = env["HOME"] - env.update(env_update) + if "env" in kw: + env = kw["env"] + else: + env = os.environ.copy() + env.update(self._get_isolated_env()) + + env_update = kw.pop("env_update", {}) + if "PYTHONPATH" not in env_update: + env["PYTHONPATH"] = os.pathsep.join( + filter(None, [os.getcwd(), env.get("PYTHONPATH", "")]) + ) + env.update(env_update) popen = subprocess.Popen( cmdargs, stdin=subprocess.PIPE, stdout=stdout, stderr=stderr, env=env, **kw From 0deee2a9ab0645246b093b632f4bf30839a87b26 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 4 Apr 2019 16:31:29 +0200 Subject: [PATCH 3/4] env/env_update --- src/_pytest/pytester.py | 14 ++++++++++++-- testing/test_pytester.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 45e3176ecb4..be9bd35420c 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1044,12 +1044,22 @@ def popen(self, cmdargs, stdout, stderr, **kw): """Invoke subprocess.Popen. This calls subprocess.Popen, making sure the current working directory - is in the PYTHONPATH. + is in the PYTHONPATH by default. + + Optional keyword arguments: + + :param env: OS environment to be used as is (no PYTHONPATH adjustment, + nor isolation for HOME etc). + :param env_update: OS environment values to update the current + environment with. + PYTHONPATH gets adjusted if not passed in explicitly. You probably want to use :py:meth:`run` instead. """ if "env" in kw: - env = kw["env"] + env = kw.pop("env") + if "env_update" in kw: + raise ValueError("env and env_update are mutually exclusive") else: env = os.environ.copy() env.update(self._get_isolated_env()) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 2e4877463a8..1ffabeb90c9 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -21,6 +21,11 @@ from _pytest.pytester import SysModulesSnapshot from _pytest.pytester import SysPathsSnapshot +try: + import mock +except ImportError: + import unittest.mock as mock + def test_make_hook_recorder(testdir): item = testdir.getitem("def test_func(): pass") @@ -482,3 +487,36 @@ def test_pytester_addopts(request, monkeypatch): testdir.finalize() assert os.environ["PYTEST_ADDOPTS"] == "--orig-unused" + + +def test_popen_env(testdir, monkeypatch): + monkeypatch.delenv("PYTHONPATH", raising=False) + popen_args = (["cmd"], None, None) + + with mock.patch("subprocess.Popen") as m: + testdir.popen(*popen_args) + env = m.call_args[1]["env"] + assert set(env.keys()) == set( + list(os.environ.keys()) + ["PYTHONPATH", "USERPROFILE", "HOME"] + ) + assert env["PYTHONPATH"] == os.getcwd() + + # Updates PYTHONPATH by default. + monkeypatch.setenv("PYTHONPATH", "custom") + testdir.popen(*popen_args) + env = m.call_args[1]["env"] + assert env["PYTHONPATH"] == os.pathsep.join((os.getcwd(), "custom")) + + # Uses explicit PYTHONPATH via env_update. + testdir.popen(*popen_args, env_update={"PYTHONPATH": "mypp", "CUSTOM_ENV": "1"}) + env = m.call_args[1]["env"] + assert env["PYTHONPATH"] == "mypp" + assert env["CUSTOM_ENV"] == "1" + + # Uses explicit env only. + testdir.popen(*popen_args, env={"CUSTOM_ENV": "1"}) + env = m.call_args[1]["env"] + assert env == {"CUSTOM_ENV": "1"} + + with pytest.raises(ValueError, match="env and env_update are mutually exclusive"): + testdir.popen(*popen_args, env={}, env_update={}) From 8325229a0920388574c061f41d19166817fb29c4 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 4 Apr 2019 14:49:58 +0200 Subject: [PATCH 4/4] pytester: set PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 --- src/_pytest/pytester.py | 2 ++ testing/acceptance_test.py | 1 + testing/test_assertion.py | 2 +- testing/test_helpconfig.py | 1 + testing/test_terminal.py | 2 ++ 5 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index be9bd35420c..aeab5794eef 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -500,6 +500,8 @@ def __init__(self, request, tmpdir_factory): mp.delenv("TOX_ENV_DIR", raising=False) # Discard outer pytest options. mp.delenv("PYTEST_ADDOPTS", raising=False) + # Do not load entrypoint plugins by default. + mp.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") def __repr__(self): return "" % (self.tmpdir,) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 408fa076e1f..9269244153e 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -157,6 +157,7 @@ def my_iter(group, name=None): monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter) params = ("-p", "mycov") if load_cov_early else () + testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") testdir.runpytest_inprocess(*params) if load_cov_early: assert loaded == ["mycov", "myplugin1", "myplugin2"] diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 330b711afb7..fcc446e4979 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -155,7 +155,7 @@ def test_foo(pytestconfig): @pytest.mark.parametrize("mode", ["plain", "rewrite"]) @pytest.mark.parametrize("plugin_state", ["development", "installed"]) def test_installed_plugin_rewrite(self, testdir, mode, plugin_state, monkeypatch): - monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) + monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") # Make sure the hook is installed early enough so that plugins # installed via setuptools are rewritten. testdir.tmpdir.join("hampkg").ensure(dir=1) diff --git a/testing/test_helpconfig.py b/testing/test_helpconfig.py index 9c7806d5489..a9b06b00989 100644 --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -7,6 +7,7 @@ def test_version(testdir, pytestconfig): + testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") result = testdir.runpytest("--version") assert result.ret == 0 # p = py.path.local(py.__file__).dirpath() diff --git a/testing/test_terminal.py b/testing/test_terminal.py index d0fdce23eb6..e66fc9a4cf6 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -568,6 +568,7 @@ def test_method(self): assert result.ret == 0 def test_header_trailer_info(self, testdir, request): + testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") testdir.makepyfile( """ def test_passes(): @@ -677,6 +678,7 @@ def test_verbose_reporting(self, verbose_testfile, testdir, pytestconfig): def test_verbose_reporting_xdist(self, verbose_testfile, testdir, pytestconfig): if not pytestconfig.pluginmanager.get_plugin("xdist"): pytest.skip("xdist plugin not installed") + testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") result = testdir.runpytest( verbose_testfile, "-v", "-n 1", "-Walways::pytest.PytestWarning"