Skip to content

Commit 71a9cc0

Browse files
committed
Merge branch '3.15' of https://github.com/python/cpython into 3.15
2 parents aaaa252 + fca9ded commit 71a9cc0

14 files changed

Lines changed: 562 additions & 275 deletions

File tree

‎Doc/library/email.charset.rst‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,8 @@ new entries to the global character set, alias, and codec registries:
183183

184184
Optional *output_charset* is the character set that the output should be in.
185185
Conversions will proceed from input charset, to Unicode, to the output charset
186-
when the method :meth:`Charset.convert` is called. The default is to output in
187-
the same character set as the input.
186+
when the method :meth:`Charset.body_encode` is called. The default is to
187+
output in the same character set as the input.
188188

189189
Both *input_charset* and *output_charset* must have Unicode codec entries in the
190190
module's character set-to-codec mapping; use :func:`add_codec` to add codecs the

‎Doc/library/email.parser.rst‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ methods.
4848
FeedParser API
4949
^^^^^^^^^^^^^^
5050

51-
The :class:`BytesFeedParser`, imported from the :mod:`email.feedparser` module,
51+
The :class:`BytesFeedParser`, imported from the :mod:`!email.feedparser` module,
5252
provides an API that is conducive to incremental parsing of email messages,
5353
such as would be necessary when reading the text of an email message from a
5454
source that can block (such as a socket). The :class:`BytesFeedParser` can of
@@ -86,7 +86,7 @@ Here is the API for the :class:`BytesFeedParser`:
8686
more information on what else *policy* controls, see the
8787
:mod:`~email.policy` documentation.
8888

89-
Note: **The policy keyword should always be specified**; The default will
89+
Note: **The policy keyword should always be specified**. The default will
9090
change to :data:`email.policy.default` in a future version of Python.
9191

9292
.. versionadded:: 3.2
@@ -142,7 +142,7 @@ message body, instead setting the payload to the raw body.
142142
arguments have the same meaning and semantics as the *_factory*
143143
and *policy* arguments of :class:`BytesFeedParser`.
144144

145-
Note: **The policy keyword should always be specified**; The default will
145+
Note: **The policy keyword should always be specified**. The default will
146146
change to :data:`email.policy.default` in a future version of Python.
147147

148148
.. versionchanged:: 3.3

‎Doc/tools/.nitignore‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
Doc/library/ast.rst
66
Doc/library/asyncio-extending.rst
7-
Doc/library/email.charset.rst
8-
Doc/library/email.parser.rst
97
Doc/library/logging.config.rst
108
Doc/library/logging.handlers.rst
119
Doc/library/multiprocessing.rst

‎Grammar/python.gram‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -225,15 +225,25 @@ import_stmt[stmt_ty](memo):
225225
# -----------------
226226

227227
import_name[stmt_ty]:
228-
| lazy="lazy"? 'import' a=dotted_as_names { _PyAST_Import(a, lazy ? 1 : 0, EXTRA) }
228+
| "lazy" 'import' a=dotted_as_names {
229+
CHECK_VERSION(stmt_ty, 15, "Lazy imports are",
230+
_PyAST_Import(a, 1, EXTRA)) }
231+
| 'import' a=dotted_as_names { _PyAST_Import(a, 0, EXTRA) }
229232

230233
# note below: the ('.' | '...') is necessary because '...' is tokenized as ELLIPSIS
231234
import_from[stmt_ty]:
232235
| invalid_import_from
233-
| lazy="lazy"? 'from' a=('.' | '...')* b=dotted_name 'import' c=import_from_targets {
234-
_PyPegen_checked_from_import(p, a, b, c, lazy, EXTRA) }
235-
| lazy="lazy"? 'from' a=('.' | '...')+ 'import' b=import_from_targets {
236-
_PyAST_ImportFrom(NULL, b, _PyPegen_seq_count_dots(a), lazy ? 1 : 0, EXTRA) }
236+
| lazy="lazy" 'from' a=('.' | '...')* b=dotted_name 'import' c=import_from_targets {
237+
CHECK_VERSION(stmt_ty, 15, "Lazy imports are",
238+
_PyPegen_checked_from_import(p, a, b, c, lazy, EXTRA)) }
239+
| "lazy" 'from' a=('.' | '...')+ 'import' b=import_from_targets {
240+
CHECK_VERSION(stmt_ty, 15, "Lazy imports are",
241+
_PyAST_ImportFrom(NULL, b, _PyPegen_seq_count_dots(a), 1, EXTRA)) }
242+
| 'from' a=('.' | '...')* b=dotted_name 'import' c=import_from_targets {
243+
_PyPegen_checked_from_import(p, a, b, c, NULL, EXTRA) }
244+
| 'from' a=('.' | '...')+ 'import' b=import_from_targets {
245+
_PyAST_ImportFrom(NULL, b, _PyPegen_seq_count_dots(a), 0, EXTRA) }
246+
237247
import_from_targets[asdl_alias_seq*]:
238248
| '(' a=import_from_as_names [','] ')' { a }
239249
| import_from_as_names !','

‎Lib/asyncio/sslproto.py‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -406,13 +406,15 @@ def connection_lost(self, exc):
406406
if self._app_transport is not None:
407407
self._app_transport._closed = True
408408

409-
if self._state != SSLProtocolState.DO_HANDSHAKE:
410-
if (
411-
self._app_state == AppProtocolState.STATE_CON_MADE or
412-
self._app_state == AppProtocolState.STATE_EOF
413-
):
414-
self._app_state = AppProtocolState.STATE_CON_LOST
415-
self._loop.call_soon(self._app_protocol.connection_lost, exc)
409+
# The app protocol gets connection_lost() only if it got
410+
# connection_made(). After start_tls() that is already the case
411+
# while the handshake is in progress.
412+
if (
413+
self._app_state == AppProtocolState.STATE_CON_MADE or
414+
self._app_state == AppProtocolState.STATE_EOF
415+
):
416+
self._app_state = AppProtocolState.STATE_CON_LOST
417+
self._loop.call_soon(self._app_protocol.connection_lost, exc)
416418
self._set_state(SSLProtocolState.UNWRAPPED)
417419
self._transport = None
418420
self._app_transport = None

‎Lib/email/charset.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def add_charset(charset, header_enc=None, body_enc=None, output_charset=None):
151151
152152
Optional output_charset is the character set that the output should be
153153
in. Conversions will proceed from input charset, to Unicode, to the
154-
output charset when the method Charset.convert() is called. The default
154+
output charset when the method Charset.body_encode() is called. The default
155155
is to output in the same character set as the input.
156156
157157
Both input_charset and output_charset must have Unicode codec entries in

‎Lib/test/test_ast/test_ast.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,19 @@ def test_type_params_default_feature_version(self):
973973
with self.assertRaises(SyntaxError):
974974
ast.parse(sample, feature_version=(3, 12))
975975

976+
def test_lazy_imports_feature_version(self):
977+
samples = [
978+
"lazy import os",
979+
"lazy from .. import name",
980+
"lazy from mod import some",
981+
]
982+
for sample in samples:
983+
with self.subTest(sample):
984+
ast.parse(sample)
985+
ast.parse(sample, feature_version=(3, 15))
986+
with self.assertRaises(SyntaxError):
987+
ast.parse(sample, feature_version=(3, 14))
988+
976989
def test_invalid_major_feature_version(self):
977990
with self.assertRaises(ValueError):
978991
ast.parse('pass', feature_version=(2, 7))

‎Lib/test/test_asyncio/test_sslproto.py‎

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import logging
44
import socket
5+
import struct
56
import unittest
67
import weakref
78
from test import support
@@ -153,6 +154,29 @@ def test_connection_lost(self):
153154
test_utils.run_briefly(self.loop)
154155
self.assertIsInstance(waiter.exception(), ConnectionAbortedError)
155156

157+
def test_connection_lost_during_handshake(self):
158+
# The app protocol gets connection_lost() if and only if it got
159+
# connection_made(). With start_tls() (call_connection_made=False)
160+
# it is already connected when the handshake starts.
161+
for call_connection_made in (True, False):
162+
with self.subTest(call_connection_made=call_connection_made):
163+
app_proto = mock.Mock(spec=asyncio.Protocol)
164+
ssl_proto = sslproto.SSLProtocol(
165+
self.loop, app_proto, test_utils.dummy_ssl_context(),
166+
waiter=None, call_connection_made=call_connection_made)
167+
self.connection_made(
168+
ssl_proto,
169+
do_handshake=mock.Mock(side_effect=ssl.SSLWantReadError)
170+
)
171+
exc = ConnectionResetError()
172+
ssl_proto.connection_lost(exc)
173+
test_utils.run_briefly(self.loop)
174+
app_proto.connection_made.assert_not_called()
175+
if call_connection_made:
176+
app_proto.connection_lost.assert_not_called()
177+
else:
178+
app_proto.connection_lost.assert_called_once_with(exc)
179+
156180
def test_connection_lost_when_busy(self):
157181
# gh-118950: SSLProtocol.connection_lost not being called when OSError
158182
# is thrown on asyncio.write.
@@ -570,9 +594,10 @@ def serve(sock):
570594
sock.close()
571595

572596
class ClientProto(asyncio.Protocol):
573-
def __init__(self, on_data, on_eof):
597+
def __init__(self, on_data, on_eof, on_con_lost):
574598
self.on_data = on_data
575599
self.on_eof = on_eof
600+
self.on_con_lost = on_con_lost
576601
self.con_made_cnt = 0
577602

578603
def connection_made(proto, tr):
@@ -586,14 +611,18 @@ def data_received(self, data):
586611
def eof_received(self):
587612
self.on_eof.set_result(True)
588613

614+
def connection_lost(self, exc):
615+
self.on_con_lost.set_result(exc)
616+
589617
async def client(addr):
590618
await asyncio.sleep(0.5)
591619

592620
on_data = self.loop.create_future()
593621
on_eof = self.loop.create_future()
622+
on_con_lost = self.loop.create_future()
594623

595624
tr, proto = await self.loop.create_connection(
596-
lambda: ClientProto(on_data, on_eof), *addr)
625+
lambda: ClientProto(on_data, on_eof, on_con_lost), *addr)
597626

598627
tr.write(HELLO_MSG)
599628

@@ -604,11 +633,59 @@ async def client(addr):
604633
self.loop.start_tls(tr, proto, client_context),
605634
0.5)
606635

636+
self.assertIsNone(
637+
await asyncio.wait_for(on_con_lost, support.LOOPBACK_TIMEOUT))
638+
639+
with self.tcp_server(serve, timeout=self.TIMEOUT) as srv:
640+
self.loop.run_until_complete(
641+
asyncio.wait_for(client(srv.addr),
642+
timeout=support.SHORT_TIMEOUT))
643+
644+
def _test_start_tls_streams_failed_handshake(self, serve, expected_exc,
645+
**kwargs):
646+
# wait_closed() must not hang if the connection is lost while
647+
# the handshake started by start_tls() is still in progress.
648+
client_context = test_utils.simple_client_sslcontext()
649+
650+
async def client(addr):
651+
reader, writer = await asyncio.open_connection(*addr)
652+
with self.assertRaises(expected_exc):
653+
await writer.start_tls(client_context, **kwargs)
654+
writer.close()
655+
with self.assertRaises(expected_exc):
656+
await writer.wait_closed()
657+
607658
with self.tcp_server(serve, timeout=self.TIMEOUT) as srv:
608659
self.loop.run_until_complete(
609660
asyncio.wait_for(client(srv.addr),
610661
timeout=support.SHORT_TIMEOUT))
611662

663+
def test_start_tls_streams_handshake_reset(self):
664+
def serve(sock):
665+
sock.settimeout(self.TIMEOUT)
666+
sock.recv(1024 * 1024) # ClientHello
667+
# Send RST instead of FIN.
668+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,
669+
struct.pack('ii', 1, 0))
670+
sock.close()
671+
672+
self._test_start_tls_streams_failed_handshake(
673+
serve, ConnectionError)
674+
675+
def test_start_tls_streams_handshake_timeout(self):
676+
def serve(sock):
677+
sock.settimeout(self.TIMEOUT)
678+
try:
679+
while sock.recv(1024 * 1024):
680+
pass
681+
except ConnectionError:
682+
pass
683+
finally:
684+
sock.close()
685+
686+
self._test_start_tls_streams_failed_handshake(
687+
serve, ConnectionAbortedError, ssl_handshake_timeout=0.1)
688+
612689
@socket_helper.skip_if_tcp_blackhole
613690
def test_start_tls_server_1(self):
614691
HELLO_MSG = b'1' * self.PAYLOAD_SIZE

‎Lib/test/test_asyncio/test_streams.py‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import asyncio
1616
from test.test_asyncio import utils as test_utils
17+
from test import support
1718
from test.support import socket_helper
1819

1920

@@ -861,6 +862,56 @@ async def run_test():
861862
self.loop.run_until_complete(run_test())
862863
self.assertEqual(messages, [])
863864

865+
@unittest.skipIf(ssl is None, 'No ssl module')
866+
def test_start_tls_connection_lost_during_handshake(self):
867+
# The protocol must get connection_lost() when the connection is
868+
# lost while the start_tls() handshake is in progress, so that
869+
# wait_closed() completes instead of hanging forever.
870+
871+
async def server_handler(client_reader, client_writer):
872+
line = await client_reader.readline()
873+
self.assertEqual(line, b"STARTTLS\n")
874+
client_writer.write(b"go ahead\n")
875+
await client_writer.drain()
876+
with self.assertRaises(ConnectionAbortedError):
877+
await client_writer.start_tls(
878+
test_utils.simple_server_sslcontext(),
879+
ssl_handshake_timeout=0.1)
880+
client_writer.close()
881+
with self.assertRaises(ConnectionAbortedError):
882+
await asyncio.wait_for(client_writer.wait_closed(),
883+
support.SHORT_TIMEOUT)
884+
handler_done.set()
885+
886+
async def client(addr):
887+
reader, writer = await asyncio.open_connection(*addr)
888+
writer.write(b"STARTTLS\n")
889+
await writer.drain()
890+
line = await reader.readline()
891+
self.assertEqual(line, b"go ahead\n")
892+
# Never send a ClientHello: the server's handshake times out.
893+
data = await reader.read()
894+
self.assertEqual(data, b"")
895+
writer.close()
896+
await writer.wait_closed()
897+
898+
async def run_test():
899+
server = await asyncio.start_server(
900+
server_handler, socket_helper.HOSTv4, 0)
901+
server_addr = server.sockets[0].getsockname()
902+
903+
await client(server_addr)
904+
await asyncio.wait_for(handler_done.wait(),
905+
support.SHORT_TIMEOUT)
906+
server.close()
907+
await server.wait_closed()
908+
909+
handler_done = asyncio.Event()
910+
messages = []
911+
self.loop.set_exception_handler(lambda loop, ctx: messages.append(ctx))
912+
self.loop.run_until_complete(run_test())
913+
self.assertEqual(messages, [])
914+
864915
def test_streamwriter_start_tls_updates_reader_transport(self):
865916
reader = asyncio.StreamReader(loop=self.loop)
866917
protocol = asyncio.StreamReaderProtocol(reader, loop=self.loop)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :func:`ast.parse` ability to parse lazy imports with *feature_version*
2+
older than 3.15, now it properly raises :exc:`SyntaxError`.

0 commit comments

Comments
 (0)