Skip to content

Commit 355a515

Browse files
Merge remote-tracking branch 'upstream/main' into fix-issue-37858-latest
2 parents 5f6f42a + 42a18e1 commit 355a515

22 files changed

Lines changed: 210 additions & 52 deletions

Doc/library/concurrent.futures.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,18 @@ Executor Objects
6161
The returned iterator raises a :exc:`TimeoutError`
6262
if :meth:`~iterator.__next__` is called and the result isn't available
6363
after *timeout* seconds from the original call to :meth:`Executor.map`.
64-
*timeout* can be an int or a float. If *timeout* is not specified or
64+
*timeout* can be an int or a float.
65+
It cancels all future calls of *fn* and closes the iterator.
66+
If *timeout* is not specified or
6567
``None``, there is no limit to the wait time.
6668

6769
If a *fn* call raises an exception, then that exception will be
6870
raised when its value is retrieved from the iterator.
71+
It does not cancel future calls of *fn*.
72+
73+
The returned iterator has method :meth:`!close` which cancels all
74+
future calls of *fn* and discards the results of already finished calls
75+
if they are available.
6976

7077
When using :class:`ProcessPoolExecutor`, this method chops *iterables*
7178
into a number of chunks which it submits to the pool as separate
@@ -82,6 +89,10 @@ Executor Objects
8289
.. versionchanged:: 3.14
8390
Added the *buffersize* parameter.
8491

92+
.. versionchanged:: next
93+
The returned iterator is no longer automatically closed if a *fn*
94+
call raises an exception.
95+
8596
.. method:: shutdown(wait=True, *, cancel_futures=False)
8697

8798
Signal the executor that it should free any resources that it is using

Doc/library/turtle.rst

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ In a Python shell, import all the objects of the ``turtle`` module::
7979

8080
from turtle import *
8181

82-
If you run into a ``No module named '_tkinter'`` error, you'll have to
83-
install the :mod:`Tk interface package <tkinter>` on your system.
82+
If you run into a ``Standard library module '_tkinter' was not found`` error,
83+
you'll have to install the :mod:`Tk interface package <tkinter>` on your system.
8484

8585

8686
Basic drawing
@@ -167,14 +167,16 @@ filling can be turned on and off::
167167

168168
Next we'll create a loop::
169169

170+
start = pos()
171+
170172
while True:
171173
forward(200)
172174
left(170)
173-
if abs(pos()) < 1:
175+
if distance(start) < 1:
174176
break
175177

176-
``abs(pos()) < 1`` is a good way to know when the turtle is back at its
177-
home position.
178+
``distance(start) < 1`` is a good way to know when the turtle is back at its
179+
start position.
178180

179181
Finally, complete the filling::
180182

Doc/using/cmdline.rst

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,25 @@ Miscellaneous options
519519
.. option:: -x
520520

521521
Skip the first line of the source, allowing use of non-Unix forms of
522-
``#!cmd``. This is intended for a DOS specific hack only.
522+
``#!cmd``.
523+
524+
This can be used to turn a Python script into a Windows batch file.
525+
Similarly to adding a shebang line and setting the executable bit on Unix,
526+
the extension of the Python script can be changed to ``.bat`` and the
527+
following line can be added at the start of the script:
528+
529+
.. code-block:: batch
530+
531+
@py -x "%~f0" %* & exit /b
532+
533+
Or, to specify the path to the Python interpreter explicitly:
534+
535+
.. code-block:: batch
536+
537+
@"C:\Path\to\python.exe" -x "%~f0" %* & exit /b
538+
539+
Unlike a shebang line which is a Python comment, this line is not valid
540+
Python syntax, and the :option:`-x` option is needed to skip it.
523541

524542

525543
.. option:: -X

Doc/whatsnew/3.16.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,15 @@ ctypes
276276
(Contributed by Peter Bierma in :gh:`153903`.)
277277

278278

279+
concurrent.futures
280+
------------------
281+
282+
* The iterator returned by :meth:`concurrent.futures.Executor.map` is no longer
283+
automatically closed if a function call raises an exception.
284+
Use method :meth:`!close` to explicitly close the iterator.
285+
(Contributed by xzmeng and Serhiy Storchaka in :gh:`108518`.)
286+
287+
279288
encodings
280289
---------
281290

@@ -819,6 +828,13 @@ that may require changes to your code.
819828
:exc:`TypeError`.
820829
(Contributed by Serhiy Storchaka in :gh:`152587`.)
821830

831+
* On Windows, seeking a pipe now fails instead of silently appearing to
832+
succeed: :func:`os.lseek` and :meth:`~io.IOBase.seek` raise :exc:`OSError`,
833+
and :meth:`~io.IOBase.seekable` returns ``False``. As a consequence,
834+
opening a pipe in a read-write binary mode (``'r+b'`` or ``'w+b'``) now
835+
raises :exc:`io.UnsupportedOperation` unless buffering is disabled.
836+
(Contributed by An Long in :gh:`86768`.)
837+
822838

823839
Build changes
824840
=============

Lib/concurrent/futures/_base.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,11 @@ def wait(fs, timeout=None, return_when=ALL_COMPLETED):
309309
def _result_or_cancel(fut, timeout=None):
310310
try:
311311
try:
312-
return fut.result(timeout)
312+
return (fut.result(timeout), None)
313+
except TimeoutError:
314+
raise
315+
except BaseException as exc:
316+
return (None, exc)
313317
finally:
314318
fut.cancel()
315319
finally:
@@ -592,6 +596,7 @@ def _get_snapshot(self):
592596

593597
__class_getitem__ = classmethod(types.GenericAlias)
594598

599+
595600
class Executor(object):
596601
"""This is an abstract base class for concrete asynchronous executors."""
597602

@@ -638,7 +643,10 @@ def map(self, fn, *iterables, timeout=None, chunksize=1, buffersize=None):
638643
raise TypeError("buffersize must be an integer or None")
639644
if buffersize is not None and buffersize < 1:
640645
raise ValueError("buffersize must be None or > 0")
646+
return _MapResultIterator(self._map(fn, *iterables, timeout=timeout,
647+
buffersize=buffersize))
641648

649+
def _map(self, fn, *iterables, timeout=None, buffersize=None):
642650
if timeout is not None:
643651
end_time = timeout + time.monotonic()
644652

@@ -701,6 +709,24 @@ def __exit__(self, exc_type, exc_val, exc_tb):
701709
return False
702710

703711

712+
class _MapResultIterator:
713+
"""The iterator returned by map()."""
714+
def __init__(self, gen):
715+
self.gen = gen
716+
717+
def __iter__(self):
718+
return self
719+
720+
def __next__(self):
721+
value, exc = next(self.gen)
722+
if exc is not None:
723+
raise exc
724+
return value
725+
726+
def close(self):
727+
self.gen.close()
728+
729+
704730
class BrokenExecutor(RuntimeError):
705731
"""
706732
Raised when an executor has become non-functional after a severe failure.

Lib/concurrent/futures/process.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,14 @@ def _process_chunk(fn, chunk):
200200
This function is run in a separate process.
201201
202202
"""
203-
return [fn(*args) for args in chunk]
203+
results = []
204+
for args in chunk:
205+
try:
206+
result = (fn(*args), None)
207+
except BaseException as exc:
208+
result = (None, exc)
209+
results.append(result)
210+
return results
204211

205212

206213
def _sendback_result(result_queue, work_id, result=None, exception=None,
@@ -963,7 +970,7 @@ def map(self, fn, *iterables, timeout=None, chunksize=1, buffersize=None):
963970
itertools.batched(zip(*iterables), chunksize),
964971
timeout=timeout,
965972
buffersize=buffersize)
966-
return _chain_from_iterable_of_lists(results)
973+
return _base._MapResultIterator(_chain_from_iterable_of_lists(results))
967974

968975
def shutdown(self, wait=True, *, cancel_futures=False):
969976
with self._shutdown_lock:

Lib/json/encoder.py

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -223,29 +223,6 @@ def iterencode(self, o, _one_shot=False):
223223
else:
224224
_encoder = encode_basestring
225225

226-
def floatstr(o, allow_nan=self.allow_nan,
227-
_repr=float.__repr__, _inf=INFINITY, _neginf=-INFINITY):
228-
# Check for specials. Note that this type of test is processor
229-
# and/or platform-specific, so do tests which don't depend on the
230-
# internals.
231-
232-
if o != o:
233-
text = 'NaN'
234-
elif o == _inf:
235-
text = 'Infinity'
236-
elif o == _neginf:
237-
text = '-Infinity'
238-
else:
239-
return _repr(o)
240-
241-
if not allow_nan:
242-
raise ValueError(
243-
"Out of range float values are not JSON compliant: " +
244-
repr(o))
245-
246-
return text
247-
248-
249226
if self.indent is None or isinstance(self.indent, str):
250227
indent = self.indent
251228
else:
@@ -256,6 +233,28 @@ def floatstr(o, allow_nan=self.allow_nan,
256233
self.key_separator, self.item_separator, self.sort_keys,
257234
self.skipkeys, self.allow_nan)
258235
else:
236+
def floatstr(o, allow_nan=self.allow_nan,
237+
_repr=float.__repr__, _inf=INFINITY, _neginf=-INFINITY):
238+
# Check for specials. Note that this type of test is processor
239+
# and/or platform-specific, so do tests which don't depend on
240+
# the internals.
241+
242+
if o != o:
243+
text = 'NaN'
244+
elif o == _inf:
245+
text = 'Infinity'
246+
elif o == _neginf:
247+
text = '-Infinity'
248+
else:
249+
return _repr(o)
250+
251+
if not allow_nan:
252+
raise ValueError(
253+
"Out of range float values are not JSON compliant: " +
254+
repr(o))
255+
256+
return text
257+
259258
_iterencode = _make_iterencode(
260259
markers, self.default, _encoder, indent, floatstr,
261260
self.key_separator, self.item_separator, self.sort_keys,

Lib/test/test_array.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ def test_numbers(self):
209209
[-1<<63, (1<<63)-1, 0]),
210210
(['l'], SIGNED_INT64_BE, '>qqq',
211211
[-1<<63, (1<<63)-1, 0]),
212+
(['e'], IEEE_754_FLOAT16_LE, '<eeee',
213+
[1.0, float('inf'), float('-inf'), -0.0]),
214+
(['e'], IEEE_754_FLOAT16_BE, '>eeee',
215+
[1.0, float('inf'), float('-inf'), -0.0]),
212216
(['f'], IEEE_754_FLOAT_LE, '<ffff',
213217
[16711938.0, float('inf'), float('-inf'), -0.0]),
214218
(['f'], IEEE_754_FLOAT_BE, '>ffff',
@@ -239,6 +243,16 @@ def test_numbers(self):
239243
self.assertEqual(a, b,
240244
msg="{0!r} != {1!r}; testcase={2!r}".format(a, b, testcase))
241245

246+
def test_float16_endianness(self):
247+
# gh-154568: array_reconstructor() slow-path decoder for
248+
# IEEE_754_FLOAT16_LE ignored the encoding.
249+
le_bytes = struct.pack('<e', 1.5)
250+
be_bytes = struct.pack('>e', 1.5)
251+
b_le = array_reconstructor(array.array, 'd', IEEE_754_FLOAT16_LE, le_bytes)
252+
b_be = array_reconstructor(array.array, 'd', IEEE_754_FLOAT16_BE, be_bytes)
253+
self.assertEqual(b_le.tolist(), [1.5])
254+
self.assertEqual(b_be.tolist(), [1.5])
255+
242256
def test_unicode(self):
243257
teststr = "Bonne Journ\xe9e \U0002030a\U00020347"
244258
testcases = (

Lib/test/test_concurrent_futures/executor.py

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,21 +71,30 @@ def test_map(self):
7171

7272
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
7373
def test_map_exception(self):
74-
i = self.executor.map(divmod, [1, 1, 1, 1], [2, 3, 0, 5])
75-
self.assertEqual(i.__next__(), (0, 1))
76-
self.assertEqual(i.__next__(), (0, 1))
77-
with self.assertRaises(ZeroDivisionError):
78-
i.__next__()
74+
i = self.executor.map(divmod, [5, 5, 5, 5], [2, 3, 0, 5])
75+
self.assertEqual(next(i), (2, 1))
76+
self.assertEqual(next(i), (1, 2))
77+
self.assertRaises(ZeroDivisionError, next, i)
78+
self.assertEqual(next(i), (1, 0))
79+
self.assertRaises(StopIteration, next, i)
80+
self.assertRaises(StopIteration, next, i)
81+
82+
i = self.executor.map(divmod, [5, 5, 5, 5], [2, 0, 3, 5], chunksize=3)
83+
self.assertEqual(next(i), (2, 1))
84+
self.assertRaises(ZeroDivisionError, next, i)
85+
self.assertEqual(next(i), (1, 2))
86+
self.assertEqual(next(i), (1, 0))
87+
self.assertRaises(StopIteration, next, i)
88+
self.assertRaises(StopIteration, next, i)
7989

8090
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
8191
@support.requires_resource('walltime')
8292
def test_map_timeout(self):
8393
results = []
94+
i = self.executor.map(time.sleep, [0, 0, 6], timeout=5)
8495
try:
85-
for i in self.executor.map(time.sleep,
86-
[0, 0, 6],
87-
timeout=5):
88-
results.append(i)
96+
for result in i:
97+
results.append(result)
8998
except futures.TimeoutError:
9099
pass
91100
else:
@@ -95,6 +104,24 @@ def test_map_timeout(self):
95104
# take longer than the specified timeout.
96105
self.assertIn(results, ([None, None], [None], []))
97106

107+
# The remaining calls are cancelled, so the iterator is exhausted.
108+
self.assertRaises(StopIteration, next, i)
109+
self.assertRaises(StopIteration, next, i)
110+
111+
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
112+
def test_map_close(self):
113+
i = self.executor.map(divmod, [5, 5, 5, 5], [2, 0, 3, 5])
114+
self.assertEqual(next(i), (2, 1))
115+
i.close()
116+
self.assertRaises(StopIteration, next, i)
117+
self.assertRaises(StopIteration, next, i)
118+
119+
i = self.executor.map(divmod, [5, 5, 5, 5], [2, 0, 3, 5], chunksize=3)
120+
self.assertEqual(next(i), (2, 1))
121+
i.close()
122+
self.assertRaises(StopIteration, next, i)
123+
self.assertRaises(StopIteration, next, i)
124+
98125
def test_map_buffersize_type_validation(self):
99126
for buffersize in ("foo", 2.0):
100127
with self.subTest(buffersize=buffersize):

Lib/test/test_os/test_os.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2993,6 +2993,14 @@ def test_ftruncate(self):
29932993
def test_lseek(self):
29942994
self.check(os.lseek, 0, 0)
29952995

2996+
@unittest.skipUnless(hasattr(os, 'lseek'), 'test needs os.lseek()')
2997+
@unittest.skipUnless(hasattr(os, 'pipe'), "need os.pipe()")
2998+
def test_lseek_on_pipe(self):
2999+
rfd, wfd = os.pipe()
3000+
self.addCleanup(os.close, rfd)
3001+
self.addCleanup(os.close, wfd)
3002+
self.assertRaises(OSError, os.lseek, rfd, 123, os.SEEK_END)
3003+
29963004
@unittest.skipUnless(hasattr(os, 'read'), 'test needs os.read()')
29973005
def test_read(self):
29983006
self.check(os.read, 1)

0 commit comments

Comments
 (0)