Skip to content

Commit 25fe370

Browse files
authored
Merge pull request #4056 from nicoddemus/unicode-vars
Ensure Monkeypatch setenv and delenv use bytes keys in Python 2
2 parents 5e7d427 + 1a323fb commit 25fe370

File tree

8 files changed

+82
-15
lines changed

8 files changed

+82
-15
lines changed

changelog/4056.bugfix.rst

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

src/_pytest/monkeypatch.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
import os
55
import sys
66
import re
7+
import warnings
78
from contextlib import contextmanager
89

910
import six
11+
12+
import pytest
1013
from _pytest.fixtures import fixture
1114

1215
RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$")
@@ -209,13 +212,31 @@ def delitem(self, dic, name, raising=True):
209212
self._setitem.append((dic, name, dic.get(name, notset)))
210213
del dic[name]
211214

215+
def _warn_if_env_name_is_not_str(self, name):
216+
"""On Python 2, warn if the given environment variable name is not a native str (#4056)"""
217+
if six.PY2 and not isinstance(name, str):
218+
warnings.warn(
219+
pytest.PytestWarning(
220+
"Environment variable name {!r} should be str".format(name)
221+
)
222+
)
223+
212224
def setenv(self, name, value, prepend=None):
213225
""" Set environment variable ``name`` to ``value``. If ``prepend``
214226
is a character, read the current environment variable value
215227
and prepend the ``value`` adjoined with the ``prepend`` character."""
216-
value = str(value)
228+
if not isinstance(value, str):
229+
warnings.warn(
230+
pytest.PytestWarning(
231+
"Environment variable value {!r} should be str, converted to str implicitly".format(
232+
value
233+
)
234+
)
235+
)
236+
value = str(value)
217237
if prepend and name in os.environ:
218238
value = value + prepend + os.environ[name]
239+
self._warn_if_env_name_is_not_str(name)
219240
self.setitem(os.environ, name, value)
220241

221242
def delenv(self, name, raising=True):
@@ -225,6 +246,7 @@ def delenv(self, name, raising=True):
225246
If ``raising`` is set to False, no exception will be raised if the
226247
environment variable is missing.
227248
"""
249+
self._warn_if_env_name_is_not_str(name)
228250
self.delitem(os.environ, name, raising=raising)
229251

230252
def syspath_prepend(self, path):

src/_pytest/recwarn.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,8 @@ def __init__(self, expected_warning=None, match_expr=None):
212212
def __exit__(self, *exc_info):
213213
super(WarningsChecker, self).__exit__(*exc_info)
214214

215+
__tracebackhide__ = True
216+
215217
# only check if we're not currently handling an exception
216218
if all(a is None for a in exc_info):
217219
if self.expected_warning is not None:

testing/acceptance_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,7 @@ def join_pythonpath(what):
577577
return what
578578

579579
empty_package = testdir.mkpydir("empty_package")
580-
monkeypatch.setenv("PYTHONPATH", join_pythonpath(empty_package))
580+
monkeypatch.setenv("PYTHONPATH", str(join_pythonpath(empty_package)))
581581
# the path which is not a package raises a warning on pypy;
582582
# no idea why only pypy and not normal python warn about it here
583583
with warnings.catch_warnings():
@@ -586,7 +586,7 @@ def join_pythonpath(what):
586586
assert result.ret == 0
587587
result.stdout.fnmatch_lines(["*2 passed*"])
588588

589-
monkeypatch.setenv("PYTHONPATH", join_pythonpath(testdir))
589+
monkeypatch.setenv("PYTHONPATH", str(join_pythonpath(testdir)))
590590
result = testdir.runpytest("--pyargs", "tpkg.test_missing", syspathinsert=True)
591591
assert result.ret != 0
592592
result.stderr.fnmatch_lines(["*not*found*test_missing*"])

testing/code/test_source.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def test_source_strip_multiline():
129129
def test_syntaxerror_rerepresentation():
130130
ex = pytest.raises(SyntaxError, _pytest._code.compile, "xyz xyz")
131131
assert ex.value.lineno == 1
132-
assert ex.value.offset in (4, 7) # XXX pypy/jython versus cpython?
132+
assert ex.value.offset in (4, 5, 7) # XXX pypy/jython versus cpython?
133133
assert ex.value.text.strip(), "x x"
134134

135135

testing/test_cacheprovider.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ def pytest_configure(config):
215215

216216
class TestLastFailed(object):
217217
def test_lastfailed_usecase(self, testdir, monkeypatch):
218-
monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", 1)
218+
monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1")
219219
p = testdir.makepyfile(
220220
"""
221221
def test_1():
@@ -301,7 +301,7 @@ def test_always_fails():
301301
assert "test_a.py" not in result.stdout.str()
302302

303303
def test_lastfailed_difference_invocations(self, testdir, monkeypatch):
304-
monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", 1)
304+
monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1")
305305
testdir.makepyfile(
306306
test_a="""\
307307
def test_a1():
@@ -335,7 +335,7 @@ def test_b1():
335335
result.stdout.fnmatch_lines(["*1 failed*1 desel*"])
336336

337337
def test_lastfailed_usecase_splice(self, testdir, monkeypatch):
338-
monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", 1)
338+
monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1")
339339
testdir.makepyfile(
340340
"""\
341341
def test_1():
@@ -474,8 +474,8 @@ def test_hello():
474474
)
475475

476476
def rlf(fail_import, fail_run):
477-
monkeypatch.setenv("FAILIMPORT", fail_import)
478-
monkeypatch.setenv("FAILTEST", fail_run)
477+
monkeypatch.setenv("FAILIMPORT", str(fail_import))
478+
monkeypatch.setenv("FAILTEST", str(fail_run))
479479

480480
testdir.runpytest("-q")
481481
config = testdir.parseconfigure()
@@ -519,8 +519,8 @@ def test_pass():
519519
)
520520

521521
def rlf(fail_import, fail_run, args=()):
522-
monkeypatch.setenv("FAILIMPORT", fail_import)
523-
monkeypatch.setenv("FAILTEST", fail_run)
522+
monkeypatch.setenv("FAILIMPORT", str(fail_import))
523+
monkeypatch.setenv("FAILTEST", str(fail_run))
524524

525525
result = testdir.runpytest("-q", "--lf", *args)
526526
config = testdir.parseconfigure()

testing/test_junitxml.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -850,7 +850,7 @@ def test_logxml_path_expansion(tmpdir, monkeypatch):
850850
assert xml_tilde.logfile == home_tilde
851851

852852
# this is here for when $HOME is not set correct
853-
monkeypatch.setenv("HOME", tmpdir)
853+
monkeypatch.setenv("HOME", str(tmpdir))
854854
home_var = os.path.normpath(os.path.expandvars("$HOME/test.xml"))
855855

856856
xml_var = LogXML("$HOME%stest.xml" % tmpdir.sep, None)

testing/test_monkeypatch.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import sys
44
import textwrap
55

6+
import six
7+
68
import pytest
79
from _pytest.monkeypatch import MonkeyPatch
810

@@ -163,7 +165,8 @@ def test_delitem():
163165

164166
def test_setenv():
165167
monkeypatch = MonkeyPatch()
166-
monkeypatch.setenv("XYZ123", 2)
168+
with pytest.warns(pytest.PytestWarning):
169+
monkeypatch.setenv("XYZ123", 2)
167170
import os
168171

169172
assert os.environ["XYZ123"] == "2"
@@ -192,13 +195,49 @@ def test_delenv():
192195
del os.environ[name]
193196

194197

198+
class TestEnvironWarnings(object):
199+
"""
200+
os.environ keys and values should be native strings, otherwise it will cause problems with other modules (notably
201+
subprocess). On Python 2 os.environ accepts anything without complaining, while Python 3 does the right thing
202+
and raises an error.
203+
"""
204+
205+
VAR_NAME = u"PYTEST_INTERNAL_MY_VAR"
206+
207+
@pytest.mark.skipif(six.PY3, reason="Python 2 only test")
208+
def test_setenv_unicode_key(self, monkeypatch):
209+
with pytest.warns(
210+
pytest.PytestWarning,
211+
match="Environment variable name {!r} should be str".format(self.VAR_NAME),
212+
):
213+
monkeypatch.setenv(self.VAR_NAME, "2")
214+
215+
@pytest.mark.skipif(six.PY3, reason="Python 2 only test")
216+
def test_delenv_unicode_key(self, monkeypatch):
217+
with pytest.warns(
218+
pytest.PytestWarning,
219+
match="Environment variable name {!r} should be str".format(self.VAR_NAME),
220+
):
221+
monkeypatch.delenv(self.VAR_NAME, raising=False)
222+
223+
def test_setenv_non_str_warning(self, monkeypatch):
224+
value = 2
225+
msg = (
226+
"Environment variable value {!r} should be str, converted to str implicitly"
227+
)
228+
with pytest.warns(pytest.PytestWarning, match=msg.format(value)):
229+
monkeypatch.setenv(str(self.VAR_NAME), value)
230+
231+
195232
def test_setenv_prepend():
196233
import os
197234

198235
monkeypatch = MonkeyPatch()
199-
monkeypatch.setenv("XYZ123", 2, prepend="-")
236+
with pytest.warns(pytest.PytestWarning):
237+
monkeypatch.setenv("XYZ123", 2, prepend="-")
200238
assert os.environ["XYZ123"] == "2"
201-
monkeypatch.setenv("XYZ123", 3, prepend="-")
239+
with pytest.warns(pytest.PytestWarning):
240+
monkeypatch.setenv("XYZ123", 3, prepend="-")
202241
assert os.environ["XYZ123"] == "3-2"
203242
monkeypatch.undo()
204243
assert "XYZ123" not in os.environ

0 commit comments

Comments
 (0)