Skip to content
6 changes: 6 additions & 0 deletions Doc/library/tempfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ The module defines the following user-callable items:
.. versionchanged:: 3.12
Added *delete_on_close* parameter.

.. versionchanged:: 3.13
Returns a :term:`path-like object`.


.. class:: SpooledTemporaryFile(max_size=0, mode='w+b', buffering=-1, encoding=None, newline=None, suffix=None, prefix=None, dir=None, *, errors=None)

Expand Down Expand Up @@ -217,6 +220,9 @@ The module defines the following user-callable items:
.. versionchanged:: 3.12
Added the *delete* parameter.

.. versionchanged:: 3.13
Returns a :term:`path-like object`.


.. function:: mkstemp(suffix=None, prefix=None, dir=None, text=False)

Expand Down
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,13 @@ sys
This function is not guaranteed to exist in all implementations of Python.
(Contributed by Serhiy Storchaka in :gh:`78573`.)

tempfile
--------

* :func:`tempfile.NamedTemporaryFile` and :func:`~tempfile.TemporaryDirectory`
now return a :term:`path-like object`.
(Contributed by Barney Gale in :gh:`87646`.)

tkinter
-------

Expand Down
12 changes: 12 additions & 0 deletions Lib/tempfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,12 @@ def func_wrapper(*args, **kwargs):
setattr(self, name, a)
return a

def __fspath__(self):
"""
Return the filesystem path of this temporary file.
"""
return self.name

# The underlying __enter__ method returns the wrong object
# (self.file) so override it to return the wrapper
def __enter__(self):
Expand Down Expand Up @@ -938,6 +944,12 @@ def _cleanup(cls, name, warn_message, ignore_errors=False, delete=True):
def __repr__(self):
return "<{} {!r}>".format(self.__class__.__name__, self.name)

def __fspath__(self):
"""
Return the filesystem path of this temporary directory.
"""
return self.name

def __enter__(self):
return self.name

Expand Down
13 changes: 13 additions & 0 deletions Lib/test/test_tempfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -932,8 +932,14 @@ def do_create(self, dir=None, pre="", suf="", delete=True):
delete=delete)

self.nameCheck(file.name, dir, pre, suf)
self.nameCheck(os.fspath(file), dir, pre, suf)
return file

def test_pathlike(self):
tmp = self.do_create()
self.assertTrue(isinstance(tmp, os.PathLike))
cls = type(tmp)
self.assertTrue(issubclass(cls, os.PathLike))

def test_basic(self):
# NamedTemporaryFile can create files
Expand Down Expand Up @@ -1580,9 +1586,16 @@ def do_create(self, dir=None, pre="", suf="", recurse=1, dirs=1, files=1,
dir=dir, prefix=pre, suffix=suf,
ignore_cleanup_errors=ignore_cleanup_errors)
self.nameCheck(tmp.name, dir, pre, suf)
self.nameCheck(os.fspath(tmp), dir, pre, suf)
self.do_create2(tmp.name, recurse, dirs, files)
return tmp

def test_pathlike(self):
tmp = self.do_create()
self.assertTrue(isinstance(tmp, os.PathLike))
cls = type(tmp)
self.assertTrue(issubclass(cls, os.PathLike))

def do_create2(self, path, recurse=1, dirs=1, files=1):
# Create subdirectories and some files
if recurse:
Expand Down
14 changes: 6 additions & 8 deletions Lib/zipfile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1332,12 +1332,14 @@ def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=True,
"metadata_encoding is only supported for reading files")

# Check if we were passed a file-like object
if isinstance(file, os.PathLike):
file = os.fspath(file)
if isinstance(file, str):
if hasattr(file, 'read') or hasattr(file, 'write'):
self._filePassed = 1
self.fp = file
self.filename = getattr(file, 'name', None)
else:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this have to to with tempfile?

Copy link
Contributor Author

@barneygale barneygale Feb 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When given an argument that is both file-like and path-like, previously zipfile would treat the argument as path-like and attempt to re-open() it. On Windows that caused PermissionError to be raised because the NamedTemporaryFile was already open. This change makes zipfile use the file-like interface in preference to the path-like interface.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect some third-party code will fall into the same trap as zipfile did here, and so perhaps it's not viable to make NamedTemporaryFile path-like.

# No, it's a filename
self._filePassed = 0
self.filename = file
self.filename = os.fspath(file)
modeDict = {'r' : 'rb', 'w': 'w+b', 'x': 'x+b', 'a' : 'r+b',
'r+b': 'w+b', 'w+b': 'wb', 'x+b': 'xb'}
filemode = modeDict[mode]
Expand All @@ -1350,10 +1352,6 @@ def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=True,
continue
raise
break
else:
self._filePassed = 1
self.fp = file
self.filename = getattr(file, 'name', None)
self._fileRefCnt = 1
self._lock = threading.RLock()
self._seekable = True
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Return a :term:`path-like object` from :func:`tempfile.NamedTemporaryFile`
and :func:`~tempfile.TemporaryDirectory`. (Contributed by Barney Gale in
:gh:`87646`.)