Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,6 @@ jobs:
- Thread
free-threading:
- false
- true
sanitizer:
- TSan
include:
Expand All @@ -566,6 +565,17 @@ jobs:
sanitizer: ${{ matrix.sanitizer }}
free-threading: ${{ matrix.free-threading }}

# XXX: Temporarily allow this job to fail to not block PRs.
build-san-free-threading:
# ${{ '' } is a hack to nest jobs under the same sidebar category.
name: Sanitizers${{ '' }} # zizmor: ignore[obfuscation]
needs: build-context
if: needs.build-context.outputs.run-ubuntu == 'true'
uses: ./.github/workflows/reusable-san.yml
with:
sanitizer: TSan
free-threading: true

cross-build-linux:
name: Cross build Linux
runs-on: ubuntu-26.04
Expand Down Expand Up @@ -669,6 +679,7 @@ jobs:
- test-hypothesis
- build-asan
- build-san
- build-san-free-threading
- cross-build-linux
- cifuzz
if: always()
Expand All @@ -680,6 +691,7 @@ jobs:
allowed-failures: >-
build-android,
build-emscripten,
build-san-free-threading,
build-windows-msi,
build-ubuntu-ssltests,
test-hypothesis,
Expand Down
5 changes: 5 additions & 0 deletions Doc/deprecations/pending-removal-in-3.21.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,8 @@ Pending removal in Python 3.21
* Soft-deprecated since Python 3.15, using ``'F'`` and ``'D'`` type codes are now
deprecated. These codes will be removed in Python 3.21. Use instead
two-letter forms ``'Zf'`` and ``'Zd'``.

* :mod:`tempfile`:

* ``tempfile._TemporaryFileWrapper`` will be removed in Python 3.21. Use the
public :class:`tempfile.TemporaryFileWrapper` instead.
5 changes: 3 additions & 2 deletions Doc/library/sys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2240,8 +2240,9 @@ always available. Unless explicitly noted otherwise, all variables are read-only

The name of the lock implementation:

* ``"semaphore"``: a lock uses a semaphore
* ``"mutex+cond"``: a lock uses a mutex and a condition variable
* ``"semaphore"``: a lock uses a semaphore (Python 3.14 and older)
* ``"mutex+cond"``: a lock uses a mutex and a condition variable (Python 3.14 and older)
* ``"pymutex"``: a lock uses the :c:type:`PyMutex` implementation (Python 3.15 and newer)
* ``None`` if this information is unknown

.. attribute:: thread_info.version
Expand Down
51 changes: 40 additions & 11 deletions Doc/library/tempfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@

This module creates temporary files and directories. It works on all
supported platforms. :class:`TemporaryFile`, :class:`NamedTemporaryFile`,
:class:`TemporaryDirectory`, and :class:`SpooledTemporaryFile` are high-level
interfaces which provide automatic cleanup and can be used as
:term:`context managers <context manager>`. :func:`mkstemp` and
:func:`mkdtemp` are lower-level functions which require manual cleanup.
:class:`TemporaryFileWrapper`, :class:`TemporaryDirectory`, and
:class:`SpooledTemporaryFile` are high-level interfaces which provide
automatic cleanup and can be used as :term:`context managers
<context manager>`. :func:`mkstemp` and :func:`mkdtemp` are lower-level
functions which require manual cleanup.

All the user-callable functions and constructors take additional arguments which
allow direct control over the location and name of temporary files and
Expand Down Expand Up @@ -84,13 +85,13 @@ The module defines the following user-callable items:
:func:`TemporaryFile` with *delete* and *delete_on_close* parameters that
determine whether and how the named file should be automatically deleted.

The returned object is always a :term:`file-like object` whose :attr:`!file`
attribute is the underlying true file object. This file-like object
can be used in a :keyword:`with` statement, just like a normal file. The
name of the temporary file can be retrieved from the :attr:`!name` attribute
of the returned file-like object. On Unix, unlike with the
:func:`TemporaryFile`, the directory entry does not get unlinked immediately
after the file creation.
The returned object is always a :class:`TemporaryFileWrapper` instance
(a :term:`file-like object`) whose :attr:`~TemporaryFileWrapper.file` attribute is the underlying
true file object. This file-like object can be used in a :keyword:`with`
statement, just like a normal file. The name of the temporary file can be
retrieved from the :attr:`!name` attribute of the returned file-like object.
On Unix, unlike with the :func:`TemporaryFile`, the directory entry does not
get unlinked immediately after the file creation.

If *delete* is true (the default) and *delete_on_close* is true (the
default), the file is deleted as soon as it is closed. If *delete* is true
Expand Down Expand Up @@ -140,6 +141,34 @@ The module defines the following user-callable items:
.. versionchanged:: 3.12
Added *delete_on_close* parameter.

.. class:: TemporaryFileWrapper(file, name, delete=True, delete_on_close=True)

A mutable wrapper returned by :func:`NamedTemporaryFile`. It wraps the
underlying file object, delegating attribute access to it, and ensures
the temporary file is deleted when appropriate.

.. attribute:: file

The underlying :term:`file-like object`.

.. attribute:: name

The file name of the temporary file.

.. method:: close()

Close the temporary file, possibly deleting it depending on the
*delete* and *delete_on_close* arguments passed to
:func:`NamedTemporaryFile`.

.. note::

``tempfile._TemporaryFileWrapper`` is kept as a backwards compatible
deprecated alias for this class.
It will be removed in Python 3.21

.. versionadded:: next


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

Expand Down
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,13 @@ New deprecations
two-letter forms ``'Zf'`` and ``'Zd'``.
(Contributed by Sergey B Kirpichev in :gh:`121249`.)

* :mod:`tempfile`

* The private ``tempfile._TemporaryFileWrapper`` name is deprecated
and is slated for removal in Python 3.21.
Use the new public :class:`tempfile.TemporaryFileWrapper` instead,
which is the return type of :func:`tempfile.NamedTemporaryFile`.

.. Add deprecations above alphabetically, not here at the end.

.. include:: ../deprecations/pending-removal-in-3.17.rst
Expand Down
35 changes: 20 additions & 15 deletions Lib/asyncio/base_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -1219,21 +1219,26 @@ async def _create_connection_transport(
ssl_handshake_timeout=None,
ssl_shutdown_timeout=None, context=None):

sock.setblocking(False)
context = context if context is not None else contextvars.copy_context()

protocol = protocol_factory()
waiter = self.create_future()
if ssl:
sslcontext = None if isinstance(ssl, bool) else ssl
transport = self._make_ssl_transport(
sock, protocol, sslcontext, waiter,
server_side=server_side, server_hostname=server_hostname,
ssl_handshake_timeout=ssl_handshake_timeout,
ssl_shutdown_timeout=ssl_shutdown_timeout,
context=context)
else:
transport = self._make_socket_transport(sock, protocol, waiter, context=context)
try:
sock.setblocking(False)
context = context if context is not None else contextvars.copy_context()

protocol = protocol_factory()
waiter = self.create_future()
if ssl:
sslcontext = None if isinstance(ssl, bool) else ssl
transport = self._make_ssl_transport(
sock, protocol, sslcontext, waiter,
server_side=server_side, server_hostname=server_hostname,
ssl_handshake_timeout=ssl_handshake_timeout,
ssl_shutdown_timeout=ssl_shutdown_timeout,
context=context)
else:
transport = self._make_socket_transport(sock, protocol, waiter, context=context)
except:
# gh-153133: close the socket if the transport is never created.
sock.close()
raise

try:
await waiter
Expand Down
18 changes: 16 additions & 2 deletions Lib/tempfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"TMP_MAX", "gettempprefix", # constants
"tempdir", "gettempdir",
"gettempprefixb", "gettempdirb",
"TemporaryFileWrapper",
]


Expand Down Expand Up @@ -484,7 +485,7 @@ def __del__(self):
_warnings.warn(self.warn_message, ResourceWarning)


class _TemporaryFileWrapper:
class TemporaryFileWrapper:
"""Temporary file wrapper

This class provides a wrapper around files opened for
Expand Down Expand Up @@ -555,6 +556,19 @@ def __iter__(self):
for line in self.file:
yield line

def __getattr__(name):
if name == "_TemporaryFileWrapper":
_warnings._deprecated(
"tempfile._TemporaryFileWrapper",
message=(
"{name!r} is deprecated and slated for removal in Python {remove}. "
"Use tempfile.TemporaryFileWrapper instead."
),
remove=(3, 21),
)
return TemporaryFileWrapper
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

def NamedTemporaryFile(mode='w+b', buffering=-1, encoding=None,
newline=None, suffix=None, prefix=None,
dir=None, delete=True, *, errors=None,
Expand Down Expand Up @@ -607,7 +621,7 @@ def opener(*args):
raw = getattr(file, 'buffer', file)
raw = getattr(raw, 'raw', raw)
raw.name = name
return _TemporaryFileWrapper(file, name, delete, delete_on_close)
return TemporaryFileWrapper(file, name, delete, delete_on_close)
except:
file.close()
raise
Expand Down
5 changes: 5 additions & 0 deletions Lib/test/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ def __init__(self, typecode, newarg=None):

class MiscTest(unittest.TestCase):

def test_array_type_importable(self):
from array import ArrayType

self.assertIs(array.array, ArrayType)

def test_array_is_sequence(self):
self.assertIsInstance(array.array("B"), collections.abc.MutableSequence)
self.assertIsInstance(array.array("B"), collections.abc.Reversible)
Expand Down
41 changes: 41 additions & 0 deletions Lib/test/test_asyncio/test_base_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,47 @@ def getaddrinfo(*args, **kw):
self.loop.run_until_complete(coro)
self.assertTrue(sock.close.called)

def test_create_connection_sock_transport_error_closes_sock(self):
# gh-153133: a user-provided socket is closed if the transport is
# never created.
sock = mock.Mock()
sock.type = socket.SOCK_STREAM

def factory():
raise ZeroDivisionError

coro = self.loop.create_connection(factory, sock=sock)
with self.assertRaises(ZeroDivisionError):
self.loop.run_until_complete(coro)
self.assertTrue(sock.close.called)

@patch_socket
def test_create_connection_transport_error_closes_sock(self, m_socket):
# gh-153133: an internally created socket is closed if the transport
# is never created.
sock = mock.Mock()
m_socket.socket.return_value = sock

def getaddrinfo(*args, **kw):
fut = self.loop.create_future()
addr = (socket.AF_INET, socket.SOCK_STREAM, 0, '',
('127.0.0.1', 80))
fut.set_result([addr])
return fut
self.loop.getaddrinfo = getaddrinfo

async def sock_connect(sock, address):
return None

def factory():
raise ZeroDivisionError

with mock.patch.object(self.loop, 'sock_connect', sock_connect):
coro = self.loop.create_connection(factory, '127.0.0.1', 80)
with self.assertRaises(ZeroDivisionError):
self.loop.run_until_complete(coro)
self.assertTrue(sock.close.called)

@patch_socket
def test_create_connection_happy_eyeballs_empty_exceptions(self, m_socket):
# See gh-135836: Fix IndexError when Happy Eyeballs algorithm
Expand Down
27 changes: 12 additions & 15 deletions Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -1604,19 +1604,24 @@ def dummycallback(sock, servername, ctx, cycle=ctx):
gc.collect()
self.assertIs(wr(), None)

@unittest.skipUnless(support.Py_GIL_DISABLED,
"test is only useful if the GIL is disabled")
@threading_helper.requires_working_threading()
def test_sni_callback_race(self):
# Replacing sni_callback while handshakes are in-flight must not
# Replacing sni_callback while a handshake is in-flight must not
# crash (use-after-free on the callback in free-threaded builds).
#
# Use a single handshake thread: OpenSSL has internal data races
# on shared SSL_CTX state when multiple handshakes run
# concurrently against the same context (gh-150191). Concurrency
# on the *setter* is what exercises the fix from gh-149816, so
# multiple toggler threads race against each other and against
# the one handshake worker.
client_ctx, server_ctx, hostname = testing_context()

server_ctx.sni_callback = lambda *a: None
done = threading.Event()
deadline = time.monotonic() + 0.1

def do_handshakes():
while not done.is_set():
while time.monotonic() < deadline:
c_in = ssl.MemoryBIO()
c_out = ssl.MemoryBIO()
s_in = ssl.MemoryBIO()
Expand All @@ -1643,19 +1648,11 @@ def do_handshakes():
c_in.write(s_out.read())

def toggle_callback():
while not done.is_set():
while time.monotonic() < deadline:
server_ctx.sni_callback = lambda *a: None
server_ctx.sni_callback = None

workers = max(4, (os.cpu_count() or 4) * 2)
threads = [threading.Thread(target=do_handshakes)
for _ in range(workers)]
threads.append(threading.Thread(target=toggle_callback))

with threading_helper.catch_threading_exception() as cm:
with threading_helper.start_threads(threads):
done.set()
self.assertIsNone(cm.exc_value)
threading_helper.run_concurrently([do_handshakes] + 4 * [toggle_callback])

def test_cert_store_stats(self):
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
Expand Down
18 changes: 15 additions & 3 deletions Lib/test/test_tempfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ def test_exports(self):
"template" : 1,
"SpooledTemporaryFile" : 1,
"TemporaryDirectory" : 1,
"TemporaryFileWrapper" : 1,
}

unexp = []
Expand Down Expand Up @@ -980,12 +981,23 @@ def do_create(self, dir=None, pre="", suf="", delete=True):

def test_basic(self):
# NamedTemporaryFile can create files
self.do_create()
f = self.do_create()
self.assertIsInstance(f, tempfile.TemporaryFileWrapper)
self.do_create(pre="a")
self.do_create(suf="b")
self.do_create(pre="a", suf="b")
self.do_create(pre="aa", suf=".txt")

def test_in_all(self):
self.assertIn("TemporaryFileWrapper", tempfile.__all__)

def test_deprecated_TemporaryFileWrapper_alias(self):
# gh-152586: _TemporaryFileWrapper is a deprecated alias
# for the public TemporaryFileWrapper class.
with self.assertWarns(DeprecationWarning):
obj = tempfile._TemporaryFileWrapper
self.assertIs(obj, tempfile.TemporaryFileWrapper)

def test_method_lookup(self):
# Issue #18879: Looking up a temporary file method should keep it
# alive long enough.
Expand Down Expand Up @@ -1141,7 +1153,7 @@ def my_func(dir):
try:
with self.assertWarnsRegex(
expected_warning=ResourceWarning,
expected_regex=r"Implicitly cleaning up <_TemporaryFileWrapper file=.*>",
expected_regex=r"Implicitly cleaning up <TemporaryFileWrapper file=.*>",
):
tmp_name = my_func(dir)
support.gc_collect()
Expand Down Expand Up @@ -1185,7 +1197,7 @@ def test_bad_encoding(self):
def test_unexpected_error(self):
dir = tempfile.mkdtemp()
self.addCleanup(os_helper.rmtree, dir)
with mock.patch('tempfile._TemporaryFileWrapper') as mock_ntf, \
with mock.patch('tempfile.TemporaryFileWrapper') as mock_ntf, \
mock.patch('io.open', mock.mock_open()) as mock_open:
mock_ntf.side_effect = KeyboardInterrupt()
with self.assertRaises(KeyboardInterrupt):
Expand Down
Loading
Loading