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
4 changes: 2 additions & 2 deletions .github/workflows/tail-call.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ jobs:
find /usr/local/bin -lname '*/Library/Frameworks/Python.framework/*' -delete
brew install llvm@${{ matrix.llvm }}
export SDKROOT="$(xcrun --show-sdk-path)"
export PATH="/usr/local/opt/llvm/bin:$PATH"
export PATH="/opt/homebrew/opt/llvm/bin:$PATH"
export PATH="/usr/local/opt/llvm@${{ matrix.llvm }}/bin:$PATH"
export PATH="/opt/homebrew/opt/llvm@${{ matrix.llvm }}/bin:$PATH"
CC=clang-20 ./configure --with-tail-call-interp
make all --jobs 4
./python.exe -m test --multiprocess 0 --timeout 4500 --verbose2 --verbose3
Expand Down
3 changes: 3 additions & 0 deletions Doc/library/imaplib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,9 @@ An :class:`IMAP4` instance has the following methods:
the password. Will only work if the server ``CAPABILITY`` response includes the
phrase ``AUTH=CRAM-MD5``.

.. versionchanged:: next
An :exc:`IMAP4.error` is raised if MD5 support is not available.


.. method:: IMAP4.logout()

Expand Down
19 changes: 19 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,25 @@ Other language changes
* Several error messages incorrectly using the term "argument" have been corrected.
(Contributed by Stan Ulbrych in :gh:`133382`.)

* The interpreter now tries to provide a suggestion when
:func:`delattr` fails due to a missing attribute.
When an attribute name that closely resembles an existing attribute is used,
the interpreter will suggest the correct attribute name in the error message.
For example:

.. doctest::

>>> class A:
... pass
>>> a = A()
>>> a.abcde = 1
>>> del a.abcdf # doctest: +ELLIPSIS
Traceback (most recent call last):
...
AttributeError: 'A' object has no attribute 'abcdf'. Did you mean: 'abcde'?

(Contributed by Nikita Sobolev and Pranjal Prajapati in :gh:`136588`.)

* Unraisable exceptions are now highlighted with color by default. This can be
controlled by :ref:`environment variables <using-on-controlling-color>`.
(Contributed by Peter Bierma in :gh:`134170`.)
Expand Down
14 changes: 9 additions & 5 deletions Lib/dbm/sqlite3.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,22 @@ def __init__(self, path, /, *, flag, mode):
# We use the URI format when opening the database.
uri = _normalize_uri(path)
uri = f"{uri}?mode={flag}"
if flag == "ro":
# Add immutable=1 to allow read-only SQLite access even if wal/shm missing
uri += "&immutable=1"

try:
self._cx = sqlite3.connect(uri, autocommit=True, uri=True)
except sqlite3.Error as exc:
raise error(str(exc))

# This is an optimization only; it's ok if it fails.
with suppress(sqlite3.OperationalError):
self._cx.execute("PRAGMA journal_mode = wal")
if flag != "ro":
# This is an optimization only; it's ok if it fails.
with suppress(sqlite3.OperationalError):
self._cx.execute("PRAGMA journal_mode = wal")

if flag == "rwc":
self._execute(BUILD_TABLE)
if flag == "rwc":
self._execute(BUILD_TABLE)

def _execute(self, *args, **kwargs):
if not self._cx:
Expand Down
16 changes: 12 additions & 4 deletions Lib/imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
# GET/SETANNOTATION contributed by Tomas Lindroos <[email protected]> June 2005.
# IDLE contributed by Forest <[email protected]> August 2024.

__version__ = "2.59"
__version__ = "2.60"

import binascii, errno, random, re, socket, subprocess, sys, time, calendar
from datetime import datetime, timezone, timedelta
Expand Down Expand Up @@ -725,9 +725,17 @@ def login_cram_md5(self, user, password):
def _CRAM_MD5_AUTH(self, challenge):
""" Authobject to use with CRAM-MD5 authentication. """
import hmac
pwd = (self.password.encode('utf-8') if isinstance(self.password, str)
else self.password)
return self.user + " " + hmac.HMAC(pwd, challenge, 'md5').hexdigest()

if isinstance(self.password, str):
password = self.password.encode('utf-8')
else:
password = self.password

try:
authcode = hmac.HMAC(password, challenge, 'md5')
except ValueError: # HMAC-MD5 is not available
raise self.error("CRAM-MD5 authentication is not supported")
return f"{self.user} {authcode.hexdigest()}"


def logout(self):
Expand Down
23 changes: 18 additions & 5 deletions Lib/smtplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,15 @@ def _quote_periods(bindata):
def _fix_eols(data):
return re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data)


try:
hmac.digest(b'', b'', 'md5')
except ValueError:
_have_cram_md5_support = False
else:
_have_cram_md5_support = True


try:
import ssl
except ImportError:
Expand Down Expand Up @@ -665,8 +674,11 @@ def auth_cram_md5(self, challenge=None):
# CRAM-MD5 does not support initial-response.
if challenge is None:
return None
return self.user + " " + hmac.HMAC(
self.password.encode('ascii'), challenge, 'md5').hexdigest()
if not _have_cram_md5_support:
raise SMTPException("CRAM-MD5 is not supported")
password = self.password.encode('ascii')
authcode = hmac.HMAC(password, challenge, 'md5')
return f"{self.user} {authcode.hexdigest()}"

def auth_plain(self, challenge=None):
""" Authobject to use with PLAIN authentication. Requires self.user and
Expand Down Expand Up @@ -718,9 +730,10 @@ def login(self, user, password, *, initial_response_ok=True):
advertised_authlist = self.esmtp_features["auth"].split()

# Authentication methods we can handle in our preferred order:
preferred_auths = ['CRAM-MD5', 'PLAIN', 'LOGIN']

# We try the supported authentications in our preferred order, if
if _have_cram_md5_support:
preferred_auths = ['CRAM-MD5', 'PLAIN', 'LOGIN']
else:
preferred_auths = ['PLAIN', 'LOGIN']
# the server supports them.
authlist = [auth for auth in preferred_auths
if auth in advertised_authlist]
Expand Down
51 changes: 51 additions & 0 deletions Lib/test/test_dbm_sqlite3.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import os
import stat
import sys
import unittest
from contextlib import closing
Expand All @@ -14,6 +16,11 @@
from dbm.sqlite3 import _normalize_uri


root_in_posix = False
if hasattr(os, 'geteuid'):
root_in_posix = (os.geteuid() == 0)


class _SQLiteDbmTests(unittest.TestCase):

def setUp(self):
Expand Down Expand Up @@ -90,6 +97,50 @@ def test_readonly_iter(self):
self.assertEqual([k for k in self.db], [b"key1", b"key2"])


@unittest.skipIf(root_in_posix, "test is meanless with root privilege")
class ReadOnlyFilesystem(unittest.TestCase):

def setUp(self):
self.test_dir = os_helper.TESTFN
self.addCleanup(os_helper.rmtree, self.test_dir)
os.mkdir(self.test_dir)
self.db_path = os.path.join(self.test_dir, "test.db")

db = dbm_sqlite3.open(self.db_path, "c")
db[b"key"] = b"value"
db.close()

def test_readonly_file_read(self):
os.chmod(self.db_path, stat.S_IREAD)
with dbm_sqlite3.open(self.db_path, "r") as db:
self.assertEqual(db[b"key"], b"value")

def test_readonly_file_write(self):
os.chmod(self.db_path, stat.S_IREAD)
with dbm_sqlite3.open(self.db_path, "w") as db:
with self.assertRaises(dbm_sqlite3.error):
db[b"newkey"] = b"newvalue"

def test_readonly_dir_read(self):
os.chmod(self.test_dir, stat.S_IREAD | stat.S_IEXEC)
with dbm_sqlite3.open(self.db_path, "r") as db:
self.assertEqual(db[b"key"], b"value")

def test_readonly_dir_write(self):
os.chmod(self.test_dir, stat.S_IREAD | stat.S_IEXEC)
with dbm_sqlite3.open(self.db_path, "w") as db:
try:
db[b"newkey"] = b"newvalue"
modified = True # on Windows and macOS
except dbm_sqlite3.error:
modified = False
with dbm_sqlite3.open(self.db_path, "r") as db:
if modified:
self.assertEqual(db[b"newkey"], b"newvalue")
else:
self.assertNotIn(b"newkey", db)


class ReadWrite(_SQLiteDbmTests):

def setUp(self):
Expand Down
13 changes: 13 additions & 0 deletions Lib/test/test_gc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1580,6 +1580,19 @@ def test_ast_fini(self):
""")
assert_python_ok("-c", code)

def test_warnings_fini(self):
# See https://github.com/python/cpython/issues/137384
code = textwrap.dedent('''
import asyncio
from contextvars import ContextVar

context_loop = ContextVar("context_loop", default=None)
loop = asyncio.new_event_loop()
context_loop.set(loop)
''')

assert_python_ok("-c", code)


def setUpModule():
global enabled, debug
Expand Down
56 changes: 27 additions & 29 deletions Lib/test/test_imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
import socket

from test.support import verbose, run_with_tz, run_with_locale, cpython_only
from test.support import hashlib_helper
from test.support import threading_helper
from test.support import hashlib_helper, threading_helper
import unittest
from unittest import mock
from datetime import datetime, timezone, timedelta
Expand Down Expand Up @@ -256,7 +255,20 @@ def cmd_IDLE(self, tag, args):
self._send_tagged(tag, 'BAD', 'Expected DONE')


class NewIMAPTestsMixin():
class AuthHandler_CRAM_MD5(SimpleIMAPHandler):
capabilities = 'LOGINDISABLED AUTH=CRAM-MD5'
def cmd_AUTHENTICATE(self, tag, args):
self._send_textline('+ PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2Uucm'
'VzdG9uLm1jaS5uZXQ=')
r = yield
if (r == b'dGltIGYxY2E2YmU0NjRiOWVmYT'
b'FjY2E2ZmZkNmNmMmQ5ZjMy\r\n'):
self._send_tagged(tag, 'OK', 'CRAM-MD5 successful')
else:
self._send_tagged(tag, 'NO', 'No access')


class NewIMAPTestsMixin:
client = None

def _setup(self, imap_handler, connect=True):
Expand Down Expand Up @@ -439,40 +451,26 @@ def cmd_AUTHENTICATE(self, tag, args):

@hashlib_helper.requires_hashdigest('md5', openssl=True)
def test_login_cram_md5_bytes(self):
class AuthHandler(SimpleIMAPHandler):
capabilities = 'LOGINDISABLED AUTH=CRAM-MD5'
def cmd_AUTHENTICATE(self, tag, args):
self._send_textline('+ PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2Uucm'
'VzdG9uLm1jaS5uZXQ=')
r = yield
if (r == b'dGltIGYxY2E2YmU0NjRiOWVmYT'
b'FjY2E2ZmZkNmNmMmQ5ZjMy\r\n'):
self._send_tagged(tag, 'OK', 'CRAM-MD5 successful')
else:
self._send_tagged(tag, 'NO', 'No access')
client, _ = self._setup(AuthHandler)
self.assertTrue('AUTH=CRAM-MD5' in client.capabilities)
client, _ = self._setup(AuthHandler_CRAM_MD5)
self.assertIn('AUTH=CRAM-MD5', client.capabilities)
ret, _ = client.login_cram_md5("tim", b"tanstaaftanstaaf")
self.assertEqual(ret, "OK")

@hashlib_helper.requires_hashdigest('md5', openssl=True)
def test_login_cram_md5_plain_text(self):
class AuthHandler(SimpleIMAPHandler):
capabilities = 'LOGINDISABLED AUTH=CRAM-MD5'
def cmd_AUTHENTICATE(self, tag, args):
self._send_textline('+ PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2Uucm'
'VzdG9uLm1jaS5uZXQ=')
r = yield
if (r == b'dGltIGYxY2E2YmU0NjRiOWVmYT'
b'FjY2E2ZmZkNmNmMmQ5ZjMy\r\n'):
self._send_tagged(tag, 'OK', 'CRAM-MD5 successful')
else:
self._send_tagged(tag, 'NO', 'No access')
client, _ = self._setup(AuthHandler)
self.assertTrue('AUTH=CRAM-MD5' in client.capabilities)
client, _ = self._setup(AuthHandler_CRAM_MD5)
self.assertIn('AUTH=CRAM-MD5', client.capabilities)
ret, _ = client.login_cram_md5("tim", "tanstaaftanstaaf")
self.assertEqual(ret, "OK")

@hashlib_helper.block_algorithm("md5")
def test_login_cram_md5_blocked(self):
client, _ = self._setup(AuthHandler_CRAM_MD5)
self.assertIn('AUTH=CRAM-MD5', client.capabilities)
msg = re.escape("CRAM-MD5 authentication is not supported")
with self.assertRaisesRegex(imaplib.IMAP4.error, msg):
client.login_cram_md5("tim", b"tanstaaftanstaaf")

def test_aborted_authentication(self):
class MyServer(SimpleIMAPHandler):
def cmd_AUTHENTICATE(self, tag, args):
Expand Down
47 changes: 42 additions & 5 deletions Lib/test/test_smtplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import threading

import unittest
import unittest.mock as mock
from test import support, mock_socket
from test.support import hashlib_helper
from test.support import socket_helper
Expand Down Expand Up @@ -926,11 +927,14 @@ def _auth_cram_md5(self, arg=None):
except ValueError as e:
self.push('535 Splitting response {!r} into user and password '
'failed: {}'.format(logpass, e))
return False
valid_hashed_pass = hmac.HMAC(
sim_auth[1].encode('ascii'),
self._decode_base64(sim_cram_md5_challenge).encode('ascii'),
'md5').hexdigest()
return
pwd = sim_auth[1].encode('ascii')
msg = self._decode_base64(sim_cram_md5_challenge).encode('ascii')
try:
valid_hashed_pass = hmac.HMAC(pwd, msg, 'md5').hexdigest()
except ValueError:
self.push('504 CRAM-MD5 is not supported')
return
self._authenticated(user, hashed_pass == valid_hashed_pass)
# end AUTH related stuff.

Expand Down Expand Up @@ -1181,6 +1185,39 @@ def testAUTH_CRAM_MD5(self):
self.assertEqual(resp, (235, b'Authentication Succeeded'))
smtp.close()

@hashlib_helper.block_algorithm('md5')
@mock.patch("smtplib._have_cram_md5_support", False)
def testAUTH_CRAM_MD5_blocked(self):
# CRAM-MD5 is the only "known" method by the server,
# but it is not supported by the client. In particular,
# no challenge will ever be sent.
self.serv.add_feature("AUTH CRAM-MD5")
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
timeout=support.LOOPBACK_TIMEOUT)
self.addCleanup(smtp.close)
msg = re.escape("No suitable authentication method found.")
with self.assertRaisesRegex(smtplib.SMTPException, msg):
smtp.login(sim_auth[0], sim_auth[1])

@hashlib_helper.block_algorithm('md5')
@mock.patch("smtplib._have_cram_md5_support", False)
def testAUTH_CRAM_MD5_blocked_and_fallback(self):
# Test that PLAIN is tried after CRAM-MD5 failed
self.serv.add_feature("AUTH CRAM-MD5 PLAIN")
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
timeout=support.LOOPBACK_TIMEOUT)
self.addCleanup(smtp.close)
with (
mock.patch.object(smtp, "auth_cram_md5") as smtp_auth_cram_md5,
mock.patch.object(
smtp, "auth_plain", wraps=smtp.auth_plain
) as smtp_auth_plain
):
resp = smtp.login(sim_auth[0], sim_auth[1])
smtp_auth_plain.assert_called_once()
smtp_auth_cram_md5.assert_not_called()
self.assertEqual(resp, (235, b'Authentication Succeeded'))

@hashlib_helper.requires_hashdigest('md5', openssl=True)
def testAUTH_multiple(self):
# Test that multiple authentication methods are tried.
Expand Down
Loading
Loading