Skip to content

Commit d8f9aae

Browse files
authored
Merge branch 'main' into fix-proactor-error-hang
2 parents 37a9b5b + 83dbe6a commit d8f9aae

29 files changed

Lines changed: 566 additions & 101 deletions

Doc/library/argparse.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -835,7 +835,9 @@ how the command-line arguments should be handled. The supplied actions are:
835835
>>> parser.parse_args(['-vvv'])
836836
Namespace(verbose=3)
837837

838-
Note, the *default* will be ``None`` unless explicitly set to *0*.
838+
Unless explicitly set, the *default* will be ``None``. If the default
839+
value is a non-zero number, the count starts from that number rather
840+
than from zero.
839841

840842
* ``'help'`` - This prints a complete help message for all the options in the
841843
current parser and then exits. By default a help action is automatically

Include/internal/pycore_compile.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ enum _PyCompile_FBlockType {
110110
COMPILE_FBLOCK_EXCEPTION_HANDLER,
111111
COMPILE_FBLOCK_EXCEPTION_GROUP_HANDLER,
112112
COMPILE_FBLOCK_ASYNC_COMPREHENSION_GENERATOR,
113+
COMPILE_FBLOCK_INLINED_COMPREHENSION,
113114
COMPILE_FBLOCK_STOP_ITERATION,
114115
};
115116

Lib/asyncio/base_events.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1497,7 +1497,12 @@ async def create_datagram_endpoint(self, protocol_factory,
14971497
else:
14981498
raise exceptions[0]
14991499

1500-
protocol = protocol_factory()
1500+
try:
1501+
protocol = protocol_factory()
1502+
except:
1503+
# gh-156400: no transport owns the socket yet, so close it.
1504+
sock.close()
1505+
raise
15011506
waiter = self.create_future()
15021507
transport = self._make_datagram_transport(
15031508
sock, protocol, r_addr, waiter)
@@ -1714,7 +1719,12 @@ async def connect_accepted_socket(
17141719
return transport, protocol
17151720

17161721
async def connect_read_pipe(self, protocol_factory, pipe):
1717-
protocol = protocol_factory()
1722+
try:
1723+
protocol = protocol_factory()
1724+
except:
1725+
# gh-156400: no transport owns the pipe yet, so close it.
1726+
pipe.close()
1727+
raise
17181728
waiter = self.create_future()
17191729
transport = self._make_read_pipe_transport(pipe, protocol, waiter)
17201730

@@ -1730,7 +1740,12 @@ async def connect_read_pipe(self, protocol_factory, pipe):
17301740
return transport, protocol
17311741

17321742
async def connect_write_pipe(self, protocol_factory, pipe):
1733-
protocol = protocol_factory()
1743+
try:
1744+
protocol = protocol_factory()
1745+
except:
1746+
# gh-156400: no transport owns the pipe yet, so close it.
1747+
pipe.close()
1748+
raise
17341749
waiter = self.create_future()
17351750
transport = self._make_write_pipe_transport(pipe, protocol, waiter)
17361751

Lib/asyncio/graph.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,9 @@ def capture_call_graph(
155155
f = sys._getframe(depth) if limit != 0 else None
156156
try:
157157
while f is not None:
158-
is_async = f.f_generator is not None
158+
# gh-156988: sync gen should not clear the call chain
159+
is_async = isinstance(
160+
f.f_generator, (types.CoroutineType, types.AsyncGeneratorType))
159161
call_stack.append(FrameCallGraphEntry(f))
160162

161163
if is_async:

Lib/asyncio/selector_events.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,7 +1127,9 @@ def _write_sendmsg(self):
11271127
self._loop._remove_writer(self._sock_fd)
11281128
if self._empty_waiter is not None:
11291129
self._empty_waiter.set_result(None)
1130-
if self._closing:
1130+
# gh-156512: don't let _call_connection_lost be called twice
1131+
if self._closing and not self._conn_lost:
1132+
self._conn_lost += 1
11311133
self._call_connection_lost(None)
11321134
elif self._eof:
11331135
self._sock.shutdown(socket.SHUT_WR)
@@ -1173,7 +1175,9 @@ def _write_send(self):
11731175
self._loop._remove_writer(self._sock_fd)
11741176
if self._empty_waiter is not None:
11751177
self._empty_waiter.set_result(None)
1176-
if self._closing:
1178+
# gh-156512: don't let _call_connection_lost be called twice
1179+
if self._closing and not self._conn_lost:
1180+
self._conn_lost += 1
11771181
self._call_connection_lost(None)
11781182
elif self._eof:
11791183
self._sock.shutdown(socket.SHUT_WR)

Lib/idlelib/editor.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@
2626
from idlelib import query
2727
from idlelib import replace
2828
from idlelib import search
29-
from idlelib.tree import wheel_event
30-
from idlelib.util import py_extensions
29+
from idlelib.util import bind_wheel, py_extensions, wheel_event
3130
from idlelib import window
3231
from idlelib.help import _get_dochome
3332

@@ -115,10 +114,7 @@ def __init__(self, flist=None, filename=None, key=None, root=None):
115114
# Elsewhere, use right-click for popup menus.
116115
text.bind("<3>",self.right_menu_event)
117116

118-
text.bind('<MouseWheel>', wheel_event)
119-
if text._windowingsystem == 'x11':
120-
text.bind('<Button-4>', wheel_event)
121-
text.bind('<Button-5>', wheel_event)
117+
bind_wheel(text, wheel_event)
122118
text.bind('<Configure>', self.handle_winconfig)
123119
text.bind("<<cut>>", self.cut)
124120
text.bind("<<copy>>", self.copy)

Lib/idlelib/idle_test/test_sidebar.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
from idlelib.percolator import Percolator
1515
import idlelib.pyshell
1616
from idlelib.pyshell import PyShell, PyShellFileList
17-
from idlelib.util import fix_scaling, fix_word_breaks, fix_x11_paste
17+
from idlelib.util import (fix_scaling, fix_word_breaks, fix_x11_paste,
18+
x11_buttons)
1819
import idlelib.sidebar
1920
from idlelib.sidebar import get_end_linenumber, get_lineno
2021

@@ -689,23 +690,21 @@ def test_mousewheel(self):
689690
last_lineno = get_end_linenumber(text)
690691
self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0')))
691692

692-
# Simulate a mouse wheel notch. Tk 8.7 replaced the X11
693-
# <Button-4>/<Button-5> wheel events with <MouseWheel> (whose delta is
694-
# platform-dependent); older Tk on X11 still uses the button events.
695-
x11_buttons = (sidebar.canvas._windowingsystem == 'x11'
696-
and tk.TkVersion < 8.7)
693+
# Simulate a mouse wheel notch with the events that Tk sends for
694+
# one; the delta of a <MouseWheel> event is platform-dependent.
695+
buttons = x11_buttons(sidebar.canvas)
697696
delta = 1 if sidebar.canvas._windowingsystem == 'aqua' else 120
698697

699698
# Scroll up.
700-
if x11_buttons:
699+
if buttons:
701700
sidebar.canvas.event_generate('<Button-4>', x=0, y=0)
702701
else:
703702
sidebar.canvas.event_generate('<MouseWheel>', x=0, y=0, delta=delta)
704703
yield
705704
self.assertIsNone(text.dlineinfo(text.index(f'{last_lineno}.0')))
706705

707706
# Scroll back down.
708-
if x11_buttons:
707+
if buttons:
709708
sidebar.canvas.event_generate('<Button-5>', x=0, y=0)
710709
else:
711710
sidebar.canvas.event_generate('<MouseWheel>', x=0, y=0, delta=-delta)

Lib/idlelib/idle_test/test_tree.py

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import unittest
55
from test.support import requires
66
requires('gui')
7-
from tkinter import Tk, EventType, SCROLL
7+
from tkinter import Tk
88

99

1010
class TreeTest(unittest.TestCase):
@@ -29,32 +29,5 @@ def test_init(self):
2929
node.expand()
3030

3131

32-
class TestScrollEvent(unittest.TestCase):
33-
34-
def test_wheel_event(self):
35-
# Fake widget class containing `yview` only.
36-
class _Widget:
37-
def __init__(widget, *expected):
38-
widget.expected = expected
39-
def yview(widget, *args):
40-
self.assertTupleEqual(widget.expected, args)
41-
# Fake event class
42-
class _Event:
43-
pass
44-
# (type, delta, num, amount)
45-
tests = ((EventType.MouseWheel, 120, -1, -5),
46-
(EventType.MouseWheel, -120, -1, 5),
47-
(EventType.ButtonPress, -1, 4, -5),
48-
(EventType.ButtonPress, -1, 5, 5))
49-
50-
event = _Event()
51-
for ty, delta, num, amount in tests:
52-
event.type = ty
53-
event.delta = delta
54-
event.num = num
55-
res = tree.wheel_event(event, _Widget(SCROLL, amount, "units"))
56-
self.assertEqual(res, "break")
57-
58-
5932
if __name__ == '__main__':
6033
unittest.main(verbosity=2)

Lib/idlelib/idle_test/test_util.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,166 @@
11
"""Test util, coverage 100%"""
22

3+
import sys
34
import unittest
5+
from unittest import mock
6+
from test.support import requires
7+
from test.support.isolation import runInSubprocess
8+
import tkinter
9+
from tkinter import EventType
410
from idlelib import util
11+
from idlelib.idle_test.mock_tk import Event
512

613

714
class UtilTest(unittest.TestCase):
15+
816
def test_extensions(self):
917
for extension in {'.pyi', '.py', '.pyw'}:
1018
self.assertIn(extension, util.py_extensions)
1119

20+
@unittest.skipUnless(sys.platform == 'win32', 'Windows only')
21+
@runInSubprocess()
22+
def test_fix_win_hidpi(self):
23+
# Awareness is process-wide and cannot be undone.
24+
import ctypes
25+
PROCESS_DPI_UNAWARE = 0
26+
util.fix_win_hidpi()
27+
awareness = ctypes.c_int()
28+
ctypes.OleDLL('shcore').GetProcessDpiAwareness(
29+
None, ctypes.byref(awareness))
30+
self.assertNotEqual(awareness.value, PROCESS_DPI_UNAWARE)
31+
32+
33+
class WheelTest(unittest.TestCase):
34+
"Test the wheel functions with a widget on this display."
35+
36+
@classmethod
37+
def setUpClass(cls):
38+
requires('gui')
39+
cls.root = tkinter.Tk()
40+
cls.root.withdraw()
41+
42+
@classmethod
43+
def tearDownClass(cls):
44+
cls.root.destroy()
45+
del cls.root
46+
47+
def setUp(self):
48+
self.text = tkinter.Text(self.root)
49+
self.addCleanup(self.text.destroy)
50+
51+
def test_x11_buttons(self):
52+
# Only X11 before Tk 8.7 sends the wheel as button events.
53+
text = self.text
54+
if text._windowingsystem == 'x11' and tkinter.TkVersion < 8.7:
55+
self.assertTrue(util.x11_buttons(text))
56+
else:
57+
self.assertFalse(util.x11_buttons(text))
58+
59+
def test_bind_wheel(self):
60+
# The events Tk sends here are the ones bound.
61+
text = self.text
62+
util.bind_wheel(text, util.wheel_event)
63+
if util.x11_buttons(text):
64+
self.assertEqual(sorted(text.bind()),
65+
['<Button-4>', '<Button-5>'])
66+
else:
67+
self.assertEqual(sorted(text.bind()), ['<MouseWheel>'])
68+
69+
70+
class WheelEventTest(unittest.TestCase):
71+
"Test the direction and the amount of the scroll."
72+
73+
# An unmapped widget has no height and does not scroll by lines,
74+
# so record the yview call instead of a real scroll.
75+
def event(self, event_type, delta=0, num='??'):
76+
# Tk leaves num '??' for a wheel event and delta 0 for a button.
77+
return Event(type=event_type, delta=delta, num=num,
78+
widget=mock.Mock())
79+
80+
def scroll(self, event, widget=None):
81+
"Return the arguments of the yview call."
82+
self.assertEqual(util.wheel_event(event, widget), 'break')
83+
scrolled = event.widget if widget is None else widget
84+
scrolled.yview.assert_called_once()
85+
return scrolled.yview.call_args.args
86+
87+
def test_mousewheel(self):
88+
# Delta is positive for up on all systems.
89+
for delta in 120, 1, 1200:
90+
self.assertEqual(self.scroll(self.event(EventType.MouseWheel,
91+
delta)),
92+
('scroll', -5, 'units'))
93+
self.assertEqual(self.scroll(self.event(EventType.MouseWheel,
94+
-delta)),
95+
('scroll', 5, 'units'))
96+
97+
def test_buttons(self):
98+
self.assertEqual(self.scroll(self.event(EventType.ButtonPress, num=4)),
99+
('scroll', -5, 'units'))
100+
self.assertEqual(self.scroll(self.event(EventType.ButtonPress, num=5)),
101+
('scroll', 5, 'units'))
102+
103+
def test_widget_argument(self):
104+
# A tree label scrolls the canvas, not itself.
105+
event = self.event(EventType.MouseWheel, 120)
106+
canvas = mock.Mock()
107+
self.assertEqual(self.scroll(event, canvas), ('scroll', -5, 'units'))
108+
event.widget.yview.assert_not_called()
109+
110+
111+
class FixTest(unittest.TestCase):
112+
"Test the fix_ functions, which need a display."
113+
114+
@classmethod
115+
def setUpClass(cls):
116+
requires('gui')
117+
cls.root = tkinter.Tk()
118+
cls.root.withdraw()
119+
120+
@classmethod
121+
def tearDownClass(cls):
122+
cls.root.destroy()
123+
del cls.root
124+
125+
def test_fix_scaling(self):
126+
from tkinter import font
127+
root = self.root
128+
self.addCleanup(root.tk_scaling, root.tk_scaling())
129+
# Both fonts go with the root; Font.delete_font is a flag.
130+
pixels = font.Font(root=root, name='TestPixelFont', size=-16)
131+
points = font.Font(root=root, name='TestPointFont', size=12)
132+
133+
root.tk_scaling(1.0)
134+
util.fix_scaling(root) # No scaling, no change.
135+
self.assertEqual(int(pixels['size']), -16)
136+
137+
root.tk_scaling(2.0)
138+
util.fix_scaling(root) # A size in pixels becomes one in points.
139+
self.assertEqual(int(pixels['size']), 12) # round(-0.75 * -16)
140+
self.assertEqual(int(points['size']), 12) # Points are left alone.
141+
142+
def test_fix_word_breaks(self):
143+
root = self.root
144+
util.fix_word_breaks(root)
145+
self.assertEqual(root.tk.call('set', 'tcl_wordchars'), r'\w')
146+
self.assertEqual(root.tk.call('set', 'tcl_nonwordchars'), r'\W')
147+
148+
def test_fix_x11_paste(self):
149+
root = self.root
150+
classes = 'Text', 'Entry', 'Spinbox'
151+
before = {cls: root.bind_class(cls, '<<Paste>>') for cls in classes}
152+
util.fix_x11_paste(root)
153+
for cls in classes:
154+
with self.subTest(cls=cls):
155+
after = root.bind_class(cls, '<<Paste>>')
156+
if root._windowingsystem == 'x11':
157+
# Deleting the selection makes paste replace it.
158+
self.assertEqual(
159+
after,
160+
'catch {%W delete sel.first sel.last}\n' + before[cls])
161+
else:
162+
self.assertEqual(after, before[cls])
163+
12164

13165
if __name__ == '__main__':
14166
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)