Merge pull request #4056 from nicoddemus/unicode-vars

Ensure Monkeypatch setenv and delenv use bytes keys in Python 2
This commit is contained in:
Bruno Oliveira 2018-10-02 07:56:32 -03:00 committed by GitHub
commit 25fe3706a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 82 additions and 15 deletions

View File

@ -0,0 +1,4 @@
``MonkeyPatch.setenv`` and ``MonkeyPatch.delenv`` issue a warning if the environment variable name is not ``str`` on Python 2.
In Python 2, adding ``unicode`` keys to ``os.environ`` causes problems with ``subprocess`` (and possible other modules),
making this a subtle bug specially susceptible when used with ``from __future__ import unicode_literals``.

View File

@ -4,9 +4,12 @@ from __future__ import absolute_import, division, print_function
import os import os
import sys import sys
import re import re
import warnings
from contextlib import contextmanager from contextlib import contextmanager
import six import six
import pytest
from _pytest.fixtures import fixture from _pytest.fixtures import fixture
RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$") RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$")
@ -209,13 +212,31 @@ class MonkeyPatch(object):
self._setitem.append((dic, name, dic.get(name, notset))) self._setitem.append((dic, name, dic.get(name, notset)))
del dic[name] del dic[name]
def _warn_if_env_name_is_not_str(self, name):
"""On Python 2, warn if the given environment variable name is not a native str (#4056)"""
if six.PY2 and not isinstance(name, str):
warnings.warn(
pytest.PytestWarning(
"Environment variable name {!r} should be str".format(name)
)
)
def setenv(self, name, value, prepend=None): def setenv(self, name, value, prepend=None):
""" Set environment variable ``name`` to ``value``. If ``prepend`` """ Set environment variable ``name`` to ``value``. If ``prepend``
is a character, read the current environment variable value is a character, read the current environment variable value
and prepend the ``value`` adjoined with the ``prepend`` character.""" and prepend the ``value`` adjoined with the ``prepend`` character."""
value = str(value) if not isinstance(value, str):
warnings.warn(
pytest.PytestWarning(
"Environment variable value {!r} should be str, converted to str implicitly".format(
value
)
)
)
value = str(value)
if prepend and name in os.environ: if prepend and name in os.environ:
value = value + prepend + os.environ[name] value = value + prepend + os.environ[name]
self._warn_if_env_name_is_not_str(name)
self.setitem(os.environ, name, value) self.setitem(os.environ, name, value)
def delenv(self, name, raising=True): def delenv(self, name, raising=True):
@ -225,6 +246,7 @@ class MonkeyPatch(object):
If ``raising`` is set to False, no exception will be raised if the If ``raising`` is set to False, no exception will be raised if the
environment variable is missing. environment variable is missing.
""" """
self._warn_if_env_name_is_not_str(name)
self.delitem(os.environ, name, raising=raising) self.delitem(os.environ, name, raising=raising)
def syspath_prepend(self, path): def syspath_prepend(self, path):

View File

@ -212,6 +212,8 @@ class WarningsChecker(WarningsRecorder):
def __exit__(self, *exc_info): def __exit__(self, *exc_info):
super(WarningsChecker, self).__exit__(*exc_info) super(WarningsChecker, self).__exit__(*exc_info)
__tracebackhide__ = True
# only check if we're not currently handling an exception # only check if we're not currently handling an exception
if all(a is None for a in exc_info): if all(a is None for a in exc_info):
if self.expected_warning is not None: if self.expected_warning is not None:

View File

@ -577,7 +577,7 @@ class TestInvocationVariants(object):
return what return what
empty_package = testdir.mkpydir("empty_package") empty_package = testdir.mkpydir("empty_package")
monkeypatch.setenv("PYTHONPATH", join_pythonpath(empty_package)) monkeypatch.setenv("PYTHONPATH", str(join_pythonpath(empty_package)))
# the path which is not a package raises a warning on pypy; # the path which is not a package raises a warning on pypy;
# no idea why only pypy and not normal python warn about it here # no idea why only pypy and not normal python warn about it here
with warnings.catch_warnings(): with warnings.catch_warnings():
@ -586,7 +586,7 @@ class TestInvocationVariants(object):
assert result.ret == 0 assert result.ret == 0
result.stdout.fnmatch_lines(["*2 passed*"]) result.stdout.fnmatch_lines(["*2 passed*"])
monkeypatch.setenv("PYTHONPATH", join_pythonpath(testdir)) monkeypatch.setenv("PYTHONPATH", str(join_pythonpath(testdir)))
result = testdir.runpytest("--pyargs", "tpkg.test_missing", syspathinsert=True) result = testdir.runpytest("--pyargs", "tpkg.test_missing", syspathinsert=True)
assert result.ret != 0 assert result.ret != 0
result.stderr.fnmatch_lines(["*not*found*test_missing*"]) result.stderr.fnmatch_lines(["*not*found*test_missing*"])

View File

@ -129,7 +129,7 @@ def test_source_strip_multiline():
def test_syntaxerror_rerepresentation(): def test_syntaxerror_rerepresentation():
ex = pytest.raises(SyntaxError, _pytest._code.compile, "xyz xyz") ex = pytest.raises(SyntaxError, _pytest._code.compile, "xyz xyz")
assert ex.value.lineno == 1 assert ex.value.lineno == 1
assert ex.value.offset in (4, 7) # XXX pypy/jython versus cpython? assert ex.value.offset in (4, 5, 7) # XXX pypy/jython versus cpython?
assert ex.value.text.strip(), "x x" assert ex.value.text.strip(), "x x"

View File

@ -215,7 +215,7 @@ def test_cache_show(testdir):
class TestLastFailed(object): class TestLastFailed(object):
def test_lastfailed_usecase(self, testdir, monkeypatch): def test_lastfailed_usecase(self, testdir, monkeypatch):
monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", 1) monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1")
p = testdir.makepyfile( p = testdir.makepyfile(
""" """
def test_1(): def test_1():
@ -301,7 +301,7 @@ class TestLastFailed(object):
assert "test_a.py" not in result.stdout.str() assert "test_a.py" not in result.stdout.str()
def test_lastfailed_difference_invocations(self, testdir, monkeypatch): def test_lastfailed_difference_invocations(self, testdir, monkeypatch):
monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", 1) monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1")
testdir.makepyfile( testdir.makepyfile(
test_a="""\ test_a="""\
def test_a1(): def test_a1():
@ -335,7 +335,7 @@ class TestLastFailed(object):
result.stdout.fnmatch_lines(["*1 failed*1 desel*"]) result.stdout.fnmatch_lines(["*1 failed*1 desel*"])
def test_lastfailed_usecase_splice(self, testdir, monkeypatch): def test_lastfailed_usecase_splice(self, testdir, monkeypatch):
monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", 1) monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1")
testdir.makepyfile( testdir.makepyfile(
"""\ """\
def test_1(): def test_1():
@ -474,8 +474,8 @@ class TestLastFailed(object):
) )
def rlf(fail_import, fail_run): def rlf(fail_import, fail_run):
monkeypatch.setenv("FAILIMPORT", fail_import) monkeypatch.setenv("FAILIMPORT", str(fail_import))
monkeypatch.setenv("FAILTEST", fail_run) monkeypatch.setenv("FAILTEST", str(fail_run))
testdir.runpytest("-q") testdir.runpytest("-q")
config = testdir.parseconfigure() config = testdir.parseconfigure()
@ -519,8 +519,8 @@ class TestLastFailed(object):
) )
def rlf(fail_import, fail_run, args=()): def rlf(fail_import, fail_run, args=()):
monkeypatch.setenv("FAILIMPORT", fail_import) monkeypatch.setenv("FAILIMPORT", str(fail_import))
monkeypatch.setenv("FAILTEST", fail_run) monkeypatch.setenv("FAILTEST", str(fail_run))
result = testdir.runpytest("-q", "--lf", *args) result = testdir.runpytest("-q", "--lf", *args)
config = testdir.parseconfigure() config = testdir.parseconfigure()

View File

@ -850,7 +850,7 @@ def test_logxml_path_expansion(tmpdir, monkeypatch):
assert xml_tilde.logfile == home_tilde assert xml_tilde.logfile == home_tilde
# this is here for when $HOME is not set correct # this is here for when $HOME is not set correct
monkeypatch.setenv("HOME", tmpdir) monkeypatch.setenv("HOME", str(tmpdir))
home_var = os.path.normpath(os.path.expandvars("$HOME/test.xml")) home_var = os.path.normpath(os.path.expandvars("$HOME/test.xml"))
xml_var = LogXML("$HOME%stest.xml" % tmpdir.sep, None) xml_var = LogXML("$HOME%stest.xml" % tmpdir.sep, None)

View File

@ -3,6 +3,8 @@ import os
import sys import sys
import textwrap import textwrap
import six
import pytest import pytest
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
@ -163,7 +165,8 @@ def test_delitem():
def test_setenv(): def test_setenv():
monkeypatch = MonkeyPatch() monkeypatch = MonkeyPatch()
monkeypatch.setenv("XYZ123", 2) with pytest.warns(pytest.PytestWarning):
monkeypatch.setenv("XYZ123", 2)
import os import os
assert os.environ["XYZ123"] == "2" assert os.environ["XYZ123"] == "2"
@ -192,13 +195,49 @@ def test_delenv():
del os.environ[name] del os.environ[name]
class TestEnvironWarnings(object):
"""
os.environ keys and values should be native strings, otherwise it will cause problems with other modules (notably
subprocess). On Python 2 os.environ accepts anything without complaining, while Python 3 does the right thing
and raises an error.
"""
VAR_NAME = u"PYTEST_INTERNAL_MY_VAR"
@pytest.mark.skipif(six.PY3, reason="Python 2 only test")
def test_setenv_unicode_key(self, monkeypatch):
with pytest.warns(
pytest.PytestWarning,
match="Environment variable name {!r} should be str".format(self.VAR_NAME),
):
monkeypatch.setenv(self.VAR_NAME, "2")
@pytest.mark.skipif(six.PY3, reason="Python 2 only test")
def test_delenv_unicode_key(self, monkeypatch):
with pytest.warns(
pytest.PytestWarning,
match="Environment variable name {!r} should be str".format(self.VAR_NAME),
):
monkeypatch.delenv(self.VAR_NAME, raising=False)
def test_setenv_non_str_warning(self, monkeypatch):
value = 2
msg = (
"Environment variable value {!r} should be str, converted to str implicitly"
)
with pytest.warns(pytest.PytestWarning, match=msg.format(value)):
monkeypatch.setenv(str(self.VAR_NAME), value)
def test_setenv_prepend(): def test_setenv_prepend():
import os import os
monkeypatch = MonkeyPatch() monkeypatch = MonkeyPatch()
monkeypatch.setenv("XYZ123", 2, prepend="-") with pytest.warns(pytest.PytestWarning):
monkeypatch.setenv("XYZ123", 2, prepend="-")
assert os.environ["XYZ123"] == "2" assert os.environ["XYZ123"] == "2"
monkeypatch.setenv("XYZ123", 3, prepend="-") with pytest.warns(pytest.PytestWarning):
monkeypatch.setenv("XYZ123", 3, prepend="-")
assert os.environ["XYZ123"] == "3-2" assert os.environ["XYZ123"] == "3-2"
monkeypatch.undo() monkeypatch.undo()
assert "XYZ123" not in os.environ assert "XYZ123" not in os.environ