Skip to content

Commit 9c79274

Browse files
ericzolfgpshead
andauthored
bpo-40701: tempfile mixes str and bytes in an inconsistent manner (GH-20442)
The case of tempfile.tempdir variable being bytes is now handled consistently. The getters return the right type and no more error of mixing str and bytes unless explicitly caused by the user. Adds a regression test. Expands the documentation to clarify the behavior. Co-authored-by: Eric L <ewl+git@lavar.de> Co-authored-by: Gregory P. Smith <greg@krypto.org>
1 parent 62e3b63 commit 9c79274

File tree

4 files changed

+98
-10
lines changed

4 files changed

+98
-10
lines changed

Doc/library/tempfile.rst

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,11 @@ The module defines the following user-callable items:
248248
The result of this search is cached, see the description of
249249
:data:`tempdir` below.
250250

251+
.. versionchanged:: 3.10
252+
253+
Always returns a str. Previously it would return any :data:`tempdir`
254+
value regardless of type so long as it was not ``None``.
255+
251256
.. function:: gettempdirb()
252257

253258
Same as :func:`gettempdir` but the return value is in bytes.
@@ -269,18 +274,30 @@ The module uses a global variable to store the name of the directory
269274
used for temporary files returned by :func:`gettempdir`. It can be
270275
set directly to override the selection process, but this is discouraged.
271276
All functions in this module take a *dir* argument which can be used
272-
to specify the directory and this is the recommended approach.
277+
to specify the directory. This is the recommended approach that does
278+
not surprise other unsuspecting code by changing global API behavior.
273279

274280
.. data:: tempdir
275281

276282
When set to a value other than ``None``, this variable defines the
277283
default value for the *dir* argument to the functions defined in this
278-
module.
284+
module, including its type, bytes or str. It cannot be a
285+
:term:`path-like object`.
279286

280287
If ``tempdir`` is ``None`` (the default) at any call to any of the above
281288
functions except :func:`gettempprefix` it is initialized following the
282289
algorithm described in :func:`gettempdir`.
283290

291+
.. note::
292+
293+
Beware that if you set ``tempdir`` to a bytes value, there is a
294+
nasty side effect: The global default return type of
295+
:func:`mkstemp` and :func:`mkdtemp` changes to bytes when no
296+
explicit ``prefix``, ``suffix``, or ``dir`` arguments of type
297+
str are supplied. Please do not write code expecting or
298+
depending on this. This awkward behavior is maintained for
299+
compatibility with the historcal implementation.
300+
284301
.. _tempfile-examples:
285302

286303
Examples

Lib/tempfile.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,11 @@ def _infer_return_type(*args):
9999
"path components.")
100100
return_type = str
101101
if return_type is None:
102-
return str # tempfile APIs return a str by default.
102+
if tempdir is None or isinstance(tempdir, str):
103+
return str # tempfile APIs return a str by default.
104+
else:
105+
# we could check for bytes but it'll fail later on anyway
106+
return bytes
103107
return return_type
104108

105109

@@ -265,17 +269,17 @@ def _mkstemp_inner(dir, pre, suf, flags, output_type):
265269
# User visible interfaces.
266270

267271
def gettempprefix():
268-
"""The default prefix for temporary directories."""
269-
return template
272+
"""The default prefix for temporary directories as string."""
273+
return _os.fsdecode(template)
270274

271275
def gettempprefixb():
272276
"""The default prefix for temporary directories as bytes."""
273-
return _os.fsencode(gettempprefix())
277+
return _os.fsencode(template)
274278

275279
tempdir = None
276280

277-
def gettempdir():
278-
"""Accessor for tempfile.tempdir."""
281+
def _gettempdir():
282+
"""Private accessor for tempfile.tempdir."""
279283
global tempdir
280284
if tempdir is None:
281285
_once_lock.acquire()
@@ -286,9 +290,13 @@ def gettempdir():
286290
_once_lock.release()
287291
return tempdir
288292

293+
def gettempdir():
294+
"""Returns tempfile.tempdir as str."""
295+
return _os.fsdecode(_gettempdir())
296+
289297
def gettempdirb():
290-
"""A bytes version of tempfile.gettempdir()."""
291-
return _os.fsencode(gettempdir())
298+
"""Returns tempfile.tempdir as bytes."""
299+
return _os.fsencode(_gettempdir())
292300

293301
def mkstemp(suffix=None, prefix=None, dir=None, text=False):
294302
"""User-callable function to create and return a unique temporary

Lib/test/test_tempfile.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,37 @@ def test_choose_directory(self):
667667
finally:
668668
os.rmdir(dir)
669669

670+
def test_for_tempdir_is_bytes_issue40701_api_warts(self):
671+
orig_tempdir = tempfile.tempdir
672+
self.assertIsInstance(tempfile.tempdir, (str, type(None)))
673+
try:
674+
fd, path = tempfile.mkstemp()
675+
os.close(fd)
676+
os.unlink(path)
677+
self.assertIsInstance(path, str)
678+
tempfile.tempdir = tempfile.gettempdirb()
679+
self.assertIsInstance(tempfile.tempdir, bytes)
680+
self.assertIsInstance(tempfile.gettempdir(), str)
681+
self.assertIsInstance(tempfile.gettempdirb(), bytes)
682+
fd, path = tempfile.mkstemp()
683+
os.close(fd)
684+
os.unlink(path)
685+
self.assertIsInstance(path, bytes)
686+
fd, path = tempfile.mkstemp(suffix='.txt')
687+
os.close(fd)
688+
os.unlink(path)
689+
self.assertIsInstance(path, str)
690+
fd, path = tempfile.mkstemp(prefix='test-temp-')
691+
os.close(fd)
692+
os.unlink(path)
693+
self.assertIsInstance(path, str)
694+
fd, path = tempfile.mkstemp(dir=tempfile.gettempdir())
695+
os.close(fd)
696+
os.unlink(path)
697+
self.assertIsInstance(path, str)
698+
finally:
699+
tempfile.tempdir = orig_tempdir
700+
670701

671702
class TestMkdtemp(TestBadTempdir, BaseTestCase):
672703
"""Test mkdtemp()."""
@@ -775,6 +806,32 @@ def test_collision_with_existing_directory(self):
775806
dir2 = tempfile.mkdtemp()
776807
self.assertTrue(dir2.endswith('bbb'))
777808

809+
def test_for_tempdir_is_bytes_issue40701_api_warts(self):
810+
orig_tempdir = tempfile.tempdir
811+
self.assertIsInstance(tempfile.tempdir, (str, type(None)))
812+
try:
813+
path = tempfile.mkdtemp()
814+
os.rmdir(path)
815+
self.assertIsInstance(path, str)
816+
tempfile.tempdir = tempfile.gettempdirb()
817+
self.assertIsInstance(tempfile.tempdir, bytes)
818+
self.assertIsInstance(tempfile.gettempdir(), str)
819+
self.assertIsInstance(tempfile.gettempdirb(), bytes)
820+
path = tempfile.mkdtemp()
821+
os.rmdir(path)
822+
self.assertIsInstance(path, bytes)
823+
path = tempfile.mkdtemp(suffix='-dir')
824+
os.rmdir(path)
825+
self.assertIsInstance(path, str)
826+
path = tempfile.mkdtemp(prefix='test-mkdtemp-')
827+
os.rmdir(path)
828+
self.assertIsInstance(path, str)
829+
path = tempfile.mkdtemp(dir=tempfile.gettempdir())
830+
os.rmdir(path)
831+
self.assertIsInstance(path, str)
832+
finally:
833+
tempfile.tempdir = orig_tempdir
834+
778835

779836
class TestMktemp(BaseTestCase):
780837
"""Test mktemp()."""
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
When the :data:`tempfile.tempdir` global variable is set to a value of
2+
type bytes, it is now handled consistently. Previously exceptions
3+
could be raised from some tempfile APIs when the directory did not
4+
already exist in this situation. Also ensures that the
5+
:func:`tempfile.gettempdir()` and :func:`tempfile.gettempdirb()`
6+
functions *always* return ``str`` and ``bytes`` respectively.

0 commit comments

Comments
 (0)