forked from pool/python312
Compare commits
16 Commits
Author | SHA256 | Date | |
---|---|---|---|
|
06a5cb31be | ||
803cb95998 | |||
cd88adc808 | |||
|
118ac765b0 | ||
2f2e126886 | |||
8c2f054df4 | |||
957ff77855 | |||
2aeb619628 | |||
38ff7e3150 | |||
ec208c83f9 | |||
|
e64f032e0a | ||
a00145be7f | |||
|
5d2f502703 | ||
c75ef22ae5 | |||
10154267fc | |||
456c5f3ff6 |
@ -1,474 +0,0 @@
|
||||
From 4a153a1d3b18803a684cd1bcc2cdf3ede3dbae19 Mon Sep 17 00:00:00 2001
|
||||
From: Victor Stinner <vstinner@python.org>
|
||||
Date: Fri, 15 Dec 2023 16:10:40 +0100
|
||||
Subject: [PATCH] [CVE-2023-27043] gh-102988: Reject malformed addresses in
|
||||
email.parseaddr() (#111116)
|
||||
|
||||
Detect email address parsing errors and return empty tuple to
|
||||
indicate the parsing error (old API). Add an optional 'strict'
|
||||
parameter to getaddresses() and parseaddr() functions. Patch by
|
||||
Thomas Dwyer.
|
||||
|
||||
Co-Authored-By: Thomas Dwyer <github@tomd.tel>
|
||||
---
|
||||
Doc/library/email.utils.rst | 19 -
|
||||
Lib/email/utils.py | 151 +++++++-
|
||||
Lib/test/test_email/test_email.py | 187 +++++++++-
|
||||
Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst | 8
|
||||
4 files changed, 344 insertions(+), 21 deletions(-)
|
||||
create mode 100644 Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst
|
||||
|
||||
--- a/Doc/library/email.utils.rst
|
||||
+++ b/Doc/library/email.utils.rst
|
||||
@@ -58,13 +58,18 @@ of the new API.
|
||||
begins with angle brackets, they are stripped off.
|
||||
|
||||
|
||||
-.. function:: parseaddr(address)
|
||||
+.. function:: parseaddr(address, *, strict=True)
|
||||
|
||||
Parse address -- which should be the value of some address-containing field such
|
||||
as :mailheader:`To` or :mailheader:`Cc` -- into its constituent *realname* and
|
||||
*email address* parts. Returns a tuple of that information, unless the parse
|
||||
fails, in which case a 2-tuple of ``('', '')`` is returned.
|
||||
|
||||
+ If *strict* is true, use a strict parser which rejects malformed inputs.
|
||||
+
|
||||
+ .. versionchanged:: 3.13
|
||||
+ Add *strict* optional parameter and reject malformed inputs by default.
|
||||
+
|
||||
|
||||
.. function:: formataddr(pair, charset='utf-8')
|
||||
|
||||
@@ -82,12 +87,15 @@ of the new API.
|
||||
Added the *charset* option.
|
||||
|
||||
|
||||
-.. function:: getaddresses(fieldvalues)
|
||||
+.. function:: getaddresses(fieldvalues, *, strict=True)
|
||||
|
||||
This method returns a list of 2-tuples of the form returned by ``parseaddr()``.
|
||||
*fieldvalues* is a sequence of header field values as might be returned by
|
||||
- :meth:`Message.get_all <email.message.Message.get_all>`. Here's a simple
|
||||
- example that gets all the recipients of a message::
|
||||
+ :meth:`Message.get_all <email.message.Message.get_all>`.
|
||||
+
|
||||
+ If *strict* is true, use a strict parser which rejects malformed inputs.
|
||||
+
|
||||
+ Here's a simple example that gets all the recipients of a message::
|
||||
|
||||
from email.utils import getaddresses
|
||||
|
||||
@@ -97,6 +105,9 @@ of the new API.
|
||||
resent_ccs = msg.get_all('resent-cc', [])
|
||||
all_recipients = getaddresses(tos + ccs + resent_tos + resent_ccs)
|
||||
|
||||
+ .. versionchanged:: 3.13
|
||||
+ Add *strict* optional parameter and reject malformed inputs by default.
|
||||
+
|
||||
|
||||
.. function:: parsedate(date)
|
||||
|
||||
--- a/Lib/email/utils.py
|
||||
+++ b/Lib/email/utils.py
|
||||
@@ -48,6 +48,7 @@ TICK = "'"
|
||||
specialsre = re.compile(r'[][\\()<>@,:;".]')
|
||||
escapesre = re.compile(r'[\\"]')
|
||||
|
||||
+
|
||||
def _has_surrogates(s):
|
||||
"""Return True if s may contain surrogate-escaped binary data."""
|
||||
# This check is based on the fact that unless there are surrogates, utf8
|
||||
@@ -106,12 +107,127 @@ def formataddr(pair, charset='utf-8'):
|
||||
return address
|
||||
|
||||
|
||||
+def _iter_escaped_chars(addr):
|
||||
+ pos = 0
|
||||
+ escape = False
|
||||
+ for pos, ch in enumerate(addr):
|
||||
+ if escape:
|
||||
+ yield (pos, '\\' + ch)
|
||||
+ escape = False
|
||||
+ elif ch == '\\':
|
||||
+ escape = True
|
||||
+ else:
|
||||
+ yield (pos, ch)
|
||||
+ if escape:
|
||||
+ yield (pos, '\\')
|
||||
+
|
||||
+
|
||||
+def _strip_quoted_realnames(addr):
|
||||
+ """Strip real names between quotes."""
|
||||
+ if '"' not in addr:
|
||||
+ # Fast path
|
||||
+ return addr
|
||||
+
|
||||
+ start = 0
|
||||
+ open_pos = None
|
||||
+ result = []
|
||||
+ for pos, ch in _iter_escaped_chars(addr):
|
||||
+ if ch == '"':
|
||||
+ if open_pos is None:
|
||||
+ open_pos = pos
|
||||
+ else:
|
||||
+ if start != open_pos:
|
||||
+ result.append(addr[start:open_pos])
|
||||
+ start = pos + 1
|
||||
+ open_pos = None
|
||||
|
||||
-def getaddresses(fieldvalues):
|
||||
- """Return a list of (REALNAME, EMAIL) for each fieldvalue."""
|
||||
- all = COMMASPACE.join(str(v) for v in fieldvalues)
|
||||
- a = _AddressList(all)
|
||||
- return a.addresslist
|
||||
+ if start < len(addr):
|
||||
+ result.append(addr[start:])
|
||||
+
|
||||
+ return ''.join(result)
|
||||
+
|
||||
+
|
||||
+supports_strict_parsing = True
|
||||
+
|
||||
+def getaddresses(fieldvalues, *, strict=True):
|
||||
+ """Return a list of (REALNAME, EMAIL) or ('','') for each fieldvalue.
|
||||
+
|
||||
+ When parsing fails for a fieldvalue, a 2-tuple of ('', '') is returned in
|
||||
+ its place.
|
||||
+
|
||||
+ If strict is true, use a strict parser which rejects malformed inputs.
|
||||
+ """
|
||||
+
|
||||
+ # If strict is true, if the resulting list of parsed addresses is greater
|
||||
+ # than the number of fieldvalues in the input list, a parsing error has
|
||||
+ # occurred and consequently a list containing a single empty 2-tuple [('',
|
||||
+ # '')] is returned in its place. This is done to avoid invalid output.
|
||||
+ #
|
||||
+ # Malformed input: getaddresses(['alice@example.com <bob@example.com>'])
|
||||
+ # Invalid output: [('', 'alice@example.com'), ('', 'bob@example.com')]
|
||||
+ # Safe output: [('', '')]
|
||||
+
|
||||
+ if not strict:
|
||||
+ all = COMMASPACE.join(str(v) for v in fieldvalues)
|
||||
+ a = _AddressList(all)
|
||||
+ return a.addresslist
|
||||
+
|
||||
+ fieldvalues = [str(v) for v in fieldvalues]
|
||||
+ fieldvalues = _pre_parse_validation(fieldvalues)
|
||||
+ addr = COMMASPACE.join(fieldvalues)
|
||||
+ a = _AddressList(addr)
|
||||
+ result = _post_parse_validation(a.addresslist)
|
||||
+
|
||||
+ # Treat output as invalid if the number of addresses is not equal to the
|
||||
+ # expected number of addresses.
|
||||
+ n = 0
|
||||
+ for v in fieldvalues:
|
||||
+ # When a comma is used in the Real Name part it is not a deliminator.
|
||||
+ # So strip those out before counting the commas.
|
||||
+ v = _strip_quoted_realnames(v)
|
||||
+ # Expected number of addresses: 1 + number of commas
|
||||
+ n += 1 + v.count(',')
|
||||
+ if len(result) != n:
|
||||
+ return [('', '')]
|
||||
+
|
||||
+ return result
|
||||
+
|
||||
+
|
||||
+def _check_parenthesis(addr):
|
||||
+ # Ignore parenthesis in quoted real names.
|
||||
+ addr = _strip_quoted_realnames(addr)
|
||||
+
|
||||
+ opens = 0
|
||||
+ for pos, ch in _iter_escaped_chars(addr):
|
||||
+ if ch == '(':
|
||||
+ opens += 1
|
||||
+ elif ch == ')':
|
||||
+ opens -= 1
|
||||
+ if opens < 0:
|
||||
+ return False
|
||||
+ return (opens == 0)
|
||||
+
|
||||
+
|
||||
+def _pre_parse_validation(email_header_fields):
|
||||
+ accepted_values = []
|
||||
+ for v in email_header_fields:
|
||||
+ if not _check_parenthesis(v):
|
||||
+ v = "('', '')"
|
||||
+ accepted_values.append(v)
|
||||
+
|
||||
+ return accepted_values
|
||||
+
|
||||
+
|
||||
+def _post_parse_validation(parsed_email_header_tuples):
|
||||
+ accepted_values = []
|
||||
+ # The parser would have parsed a correctly formatted domain-literal
|
||||
+ # The existence of an [ after parsing indicates a parsing failure
|
||||
+ for v in parsed_email_header_tuples:
|
||||
+ if '[' in v[1]:
|
||||
+ v = ('', '')
|
||||
+ accepted_values.append(v)
|
||||
+
|
||||
+ return accepted_values
|
||||
|
||||
|
||||
def _format_timetuple_and_zone(timetuple, zone):
|
||||
@@ -205,16 +321,33 @@ def parsedate_to_datetime(data):
|
||||
tzinfo=datetime.timezone(datetime.timedelta(seconds=tz)))
|
||||
|
||||
|
||||
-def parseaddr(addr):
|
||||
+def parseaddr(addr, *, strict=True):
|
||||
"""
|
||||
Parse addr into its constituent realname and email address parts.
|
||||
|
||||
Return a tuple of realname and email address, unless the parse fails, in
|
||||
which case return a 2-tuple of ('', '').
|
||||
+
|
||||
+ If strict is True, use a strict parser which rejects malformed inputs.
|
||||
"""
|
||||
- addrs = _AddressList(addr).addresslist
|
||||
- if not addrs:
|
||||
- return '', ''
|
||||
+ if not strict:
|
||||
+ addrs = _AddressList(addr).addresslist
|
||||
+ if not addrs:
|
||||
+ return ('', '')
|
||||
+ return addrs[0]
|
||||
+
|
||||
+ if isinstance(addr, list):
|
||||
+ addr = addr[0]
|
||||
+
|
||||
+ if not isinstance(addr, str):
|
||||
+ return ('', '')
|
||||
+
|
||||
+ addr = _pre_parse_validation([addr])[0]
|
||||
+ addrs = _post_parse_validation(_AddressList(addr).addresslist)
|
||||
+
|
||||
+ if not addrs or len(addrs) > 1:
|
||||
+ return ('', '')
|
||||
+
|
||||
return addrs[0]
|
||||
|
||||
|
||||
--- a/Lib/test/test_email/test_email.py
|
||||
+++ b/Lib/test/test_email/test_email.py
|
||||
@@ -16,6 +16,7 @@ from unittest.mock import patch
|
||||
|
||||
import email
|
||||
import email.policy
|
||||
+import email.utils
|
||||
|
||||
from email.charset import Charset
|
||||
from email.generator import Generator, DecodedGenerator, BytesGenerator
|
||||
@@ -3352,15 +3353,137 @@ Foo
|
||||
],
|
||||
)
|
||||
|
||||
+ def test_parsing_errors(self):
|
||||
+ """Test for parsing errors from CVE-2023-27043 and CVE-2019-16056"""
|
||||
+ alice = 'alice@example.org'
|
||||
+ bob = 'bob@example.com'
|
||||
+ empty = ('', '')
|
||||
+
|
||||
+ # Test utils.getaddresses() and utils.parseaddr() on malformed email
|
||||
+ # addresses: default behavior (strict=True) rejects malformed address,
|
||||
+ # and strict=False which tolerates malformed address.
|
||||
+ for invalid_separator, expected_non_strict in (
|
||||
+ ('(', [(f'<{bob}>', alice)]),
|
||||
+ (')', [('', alice), empty, ('', bob)]),
|
||||
+ ('<', [('', alice), empty, ('', bob), empty]),
|
||||
+ ('>', [('', alice), empty, ('', bob)]),
|
||||
+ ('[', [('', f'{alice}[<{bob}>]')]),
|
||||
+ (']', [('', alice), empty, ('', bob)]),
|
||||
+ ('@', [empty, empty, ('', bob)]),
|
||||
+ (';', [('', alice), empty, ('', bob)]),
|
||||
+ (':', [('', alice), ('', bob)]),
|
||||
+ ('.', [('', alice + '.'), ('', bob)]),
|
||||
+ ('"', [('', alice), ('', f'<{bob}>')]),
|
||||
+ ):
|
||||
+ address = f'{alice}{invalid_separator}<{bob}>'
|
||||
+ with self.subTest(address=address):
|
||||
+ self.assertEqual(utils.getaddresses([address]),
|
||||
+ [empty])
|
||||
+ self.assertEqual(utils.getaddresses([address], strict=False),
|
||||
+ expected_non_strict)
|
||||
+
|
||||
+ self.assertEqual(utils.parseaddr([address]),
|
||||
+ empty)
|
||||
+ self.assertEqual(utils.parseaddr([address], strict=False),
|
||||
+ ('', address))
|
||||
+
|
||||
+ # Comma (',') is treated differently depending on strict parameter.
|
||||
+ # Comma without quotes.
|
||||
+ address = f'{alice},<{bob}>'
|
||||
+ self.assertEqual(utils.getaddresses([address]),
|
||||
+ [('', alice), ('', bob)])
|
||||
+ self.assertEqual(utils.getaddresses([address], strict=False),
|
||||
+ [('', alice), ('', bob)])
|
||||
+ self.assertEqual(utils.parseaddr([address]),
|
||||
+ empty)
|
||||
+ self.assertEqual(utils.parseaddr([address], strict=False),
|
||||
+ ('', address))
|
||||
+
|
||||
+ # Real name between quotes containing comma.
|
||||
+ address = '"Alice, alice@example.org" <bob@example.com>'
|
||||
+ expected_strict = ('Alice, alice@example.org', 'bob@example.com')
|
||||
+ self.assertEqual(utils.getaddresses([address]), [expected_strict])
|
||||
+ self.assertEqual(utils.getaddresses([address], strict=False), [expected_strict])
|
||||
+ self.assertEqual(utils.parseaddr([address]), expected_strict)
|
||||
+ self.assertEqual(utils.parseaddr([address], strict=False),
|
||||
+ ('', address))
|
||||
+
|
||||
+ # Valid parenthesis in comments.
|
||||
+ address = 'alice@example.org (Alice)'
|
||||
+ expected_strict = ('Alice', 'alice@example.org')
|
||||
+ self.assertEqual(utils.getaddresses([address]), [expected_strict])
|
||||
+ self.assertEqual(utils.getaddresses([address], strict=False), [expected_strict])
|
||||
+ self.assertEqual(utils.parseaddr([address]), expected_strict)
|
||||
+ self.assertEqual(utils.parseaddr([address], strict=False),
|
||||
+ ('', address))
|
||||
+
|
||||
+ # Invalid parenthesis in comments.
|
||||
+ address = 'alice@example.org )Alice('
|
||||
+ self.assertEqual(utils.getaddresses([address]), [empty])
|
||||
+ self.assertEqual(utils.getaddresses([address], strict=False),
|
||||
+ [('', 'alice@example.org'), ('', ''), ('', 'Alice')])
|
||||
+ self.assertEqual(utils.parseaddr([address]), empty)
|
||||
+ self.assertEqual(utils.parseaddr([address], strict=False),
|
||||
+ ('', address))
|
||||
+
|
||||
+ # Two addresses with quotes separated by comma.
|
||||
+ address = '"Jane Doe" <jane@example.net>, "John Doe" <john@example.net>'
|
||||
+ self.assertEqual(utils.getaddresses([address]),
|
||||
+ [('Jane Doe', 'jane@example.net'),
|
||||
+ ('John Doe', 'john@example.net')])
|
||||
+ self.assertEqual(utils.getaddresses([address], strict=False),
|
||||
+ [('Jane Doe', 'jane@example.net'),
|
||||
+ ('John Doe', 'john@example.net')])
|
||||
+ self.assertEqual(utils.parseaddr([address]), empty)
|
||||
+ self.assertEqual(utils.parseaddr([address], strict=False),
|
||||
+ ('', address))
|
||||
+
|
||||
+ # Test email.utils.supports_strict_parsing attribute
|
||||
+ self.assertEqual(email.utils.supports_strict_parsing, True)
|
||||
+
|
||||
def test_getaddresses_nasty(self):
|
||||
- eq = self.assertEqual
|
||||
- eq(utils.getaddresses(['foo: ;']), [('', '')])
|
||||
- eq(utils.getaddresses(
|
||||
- ['[]*-- =~$']),
|
||||
- [('', ''), ('', ''), ('', '*--')])
|
||||
- eq(utils.getaddresses(
|
||||
- ['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>']),
|
||||
- [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')])
|
||||
+ for addresses, expected in (
|
||||
+ (['"Sürname, Firstname" <to@example.com>'],
|
||||
+ [('Sürname, Firstname', 'to@example.com')]),
|
||||
+
|
||||
+ (['foo: ;'],
|
||||
+ [('', '')]),
|
||||
+
|
||||
+ (['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>'],
|
||||
+ [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')]),
|
||||
+
|
||||
+ ([r'Pete(A nice \) chap) <pete(his account)@silly.test(his host)>'],
|
||||
+ [('Pete (A nice ) chap his account his host)', 'pete@silly.test')]),
|
||||
+
|
||||
+ (['(Empty list)(start)Undisclosed recipients :(nobody(I know))'],
|
||||
+ [('', '')]),
|
||||
+
|
||||
+ (['Mary <@machine.tld:mary@example.net>, , jdoe@test . example'],
|
||||
+ [('Mary', 'mary@example.net'), ('', ''), ('', 'jdoe@test.example')]),
|
||||
+
|
||||
+ (['John Doe <jdoe@machine(comment). example>'],
|
||||
+ [('John Doe (comment)', 'jdoe@machine.example')]),
|
||||
+
|
||||
+ (['"Mary Smith: Personal Account" <smith@home.example>'],
|
||||
+ [('Mary Smith: Personal Account', 'smith@home.example')]),
|
||||
+
|
||||
+ (['Undisclosed recipients:;'],
|
||||
+ [('', '')]),
|
||||
+
|
||||
+ ([r'<boss@nil.test>, "Giant; \"Big\" Box" <bob@example.net>'],
|
||||
+ [('', 'boss@nil.test'), ('Giant; "Big" Box', 'bob@example.net')]),
|
||||
+ ):
|
||||
+ with self.subTest(addresses=addresses):
|
||||
+ self.assertEqual(utils.getaddresses(addresses),
|
||||
+ expected)
|
||||
+ self.assertEqual(utils.getaddresses(addresses, strict=False),
|
||||
+ expected)
|
||||
+
|
||||
+ addresses = ['[]*-- =~$']
|
||||
+ self.assertEqual(utils.getaddresses(addresses),
|
||||
+ [('', '')])
|
||||
+ self.assertEqual(utils.getaddresses(addresses, strict=False),
|
||||
+ [('', ''), ('', ''), ('', '*--')])
|
||||
|
||||
def test_getaddresses_embedded_comment(self):
|
||||
"""Test proper handling of a nested comment"""
|
||||
@@ -3551,6 +3674,54 @@ multipart/report
|
||||
m = cls(*constructor, policy=email.policy.default)
|
||||
self.assertIs(m.policy, email.policy.default)
|
||||
|
||||
+ def test_iter_escaped_chars(self):
|
||||
+ self.assertEqual(list(utils._iter_escaped_chars(r'a\\b\"c\\"d')),
|
||||
+ [(0, 'a'),
|
||||
+ (2, '\\\\'),
|
||||
+ (3, 'b'),
|
||||
+ (5, '\\"'),
|
||||
+ (6, 'c'),
|
||||
+ (8, '\\\\'),
|
||||
+ (9, '"'),
|
||||
+ (10, 'd')])
|
||||
+ self.assertEqual(list(utils._iter_escaped_chars('a\\')),
|
||||
+ [(0, 'a'), (1, '\\')])
|
||||
+
|
||||
+ def test_strip_quoted_realnames(self):
|
||||
+ def check(addr, expected):
|
||||
+ self.assertEqual(utils._strip_quoted_realnames(addr), expected)
|
||||
+
|
||||
+ check('"Jane Doe" <jane@example.net>, "John Doe" <john@example.net>',
|
||||
+ ' <jane@example.net>, <john@example.net>')
|
||||
+ check(r'"Jane \"Doe\"." <jane@example.net>',
|
||||
+ ' <jane@example.net>')
|
||||
+
|
||||
+ # special cases
|
||||
+ check(r'before"name"after', 'beforeafter')
|
||||
+ check(r'before"name"', 'before')
|
||||
+ check(r'b"name"', 'b') # single char
|
||||
+ check(r'"name"after', 'after')
|
||||
+ check(r'"name"a', 'a') # single char
|
||||
+ check(r'"name"', '')
|
||||
+
|
||||
+ # no change
|
||||
+ for addr in (
|
||||
+ 'Jane Doe <jane@example.net>, John Doe <john@example.net>',
|
||||
+ 'lone " quote',
|
||||
+ ):
|
||||
+ self.assertEqual(utils._strip_quoted_realnames(addr), addr)
|
||||
+
|
||||
+
|
||||
+ def test_check_parenthesis(self):
|
||||
+ addr = 'alice@example.net'
|
||||
+ self.assertTrue(utils._check_parenthesis(f'{addr} (Alice)'))
|
||||
+ self.assertFalse(utils._check_parenthesis(f'{addr} )Alice('))
|
||||
+ self.assertFalse(utils._check_parenthesis(f'{addr} (Alice))'))
|
||||
+ self.assertFalse(utils._check_parenthesis(f'{addr} ((Alice)'))
|
||||
+
|
||||
+ # Ignore real name between quotes
|
||||
+ self.assertTrue(utils._check_parenthesis(f'")Alice((" {addr}'))
|
||||
+
|
||||
|
||||
# Test the iterator/generators
|
||||
class TestIterators(TestEmailBase):
|
||||
--- /dev/null
|
||||
+++ b/Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst
|
||||
@@ -0,0 +1,8 @@
|
||||
+:func:`email.utils.getaddresses` and :func:`email.utils.parseaddr` now
|
||||
+return ``('', '')`` 2-tuples in more situations where invalid email
|
||||
+addresses are encountered instead of potentially inaccurate values. Add
|
||||
+optional *strict* parameter to these two functions: use ``strict=False`` to
|
||||
+get the old behavior, accept malformed inputs.
|
||||
+``getattr(email.utils, 'supports_strict_parsing', False)`` can be use to check
|
||||
+if the *strict* paramater is available. Patch by Thomas Dwyer and Victor
|
||||
+Stinner to improve the CVE-2023-27043 fix.
|
@ -1,7 +1,36 @@
|
||||
Index: Python-3.12.3/Lib/test/test_xml_etree.py
|
||||
===================================================================
|
||||
--- Python-3.12.3.orig/Lib/test/test_xml_etree.py
|
||||
+++ Python-3.12.3/Lib/test/test_xml_etree.py
|
||||
---
|
||||
Lib/test/test_pyexpat.py | 4 ++++
|
||||
Lib/test/test_sax.py | 3 +++
|
||||
Lib/test/test_xml_etree.py | 10 ++++++++++
|
||||
3 files changed, 17 insertions(+)
|
||||
|
||||
--- a/Lib/test/test_pyexpat.py
|
||||
+++ b/Lib/test/test_pyexpat.py
|
||||
@@ -794,6 +794,10 @@ class ReparseDeferralTest(unittest.TestC
|
||||
self.assertEqual(started, ['doc'])
|
||||
|
||||
def test_reparse_deferral_disabled(self):
|
||||
+ if expat.version_info < (2, 6, 0):
|
||||
+ self.skipTest(f'Expat {expat.version_info} does not '
|
||||
+ 'support reparse deferral')
|
||||
+
|
||||
started = []
|
||||
|
||||
def start_element(name, _):
|
||||
--- a/Lib/test/test_sax.py
|
||||
+++ b/Lib/test/test_sax.py
|
||||
@@ -1240,6 +1240,9 @@ class ExpatReaderTest(XmlTestBase):
|
||||
|
||||
self.assertEqual(result.getvalue(), start + b"<doc></doc>")
|
||||
|
||||
+ @unittest.skipIf(pyexpat.version_info < (2, 6, 0),
|
||||
+ f'Expat {pyexpat.version_info} does not '
|
||||
+ 'support reparse deferral')
|
||||
def test_flush_reparse_deferral_disabled(self):
|
||||
result = BytesIO()
|
||||
xmlgen = XMLGenerator(result)
|
||||
--- a/Lib/test/test_xml_etree.py
|
||||
+++ b/Lib/test/test_xml_etree.py
|
||||
@@ -121,6 +121,11 @@ ATTLIST_XML = """\
|
||||
</foo>
|
||||
"""
|
||||
@ -36,32 +65,3 @@ Index: Python-3.12.3/Lib/test/test_xml_etree.py
|
||||
def test_flush_reparse_deferral_disabled(self):
|
||||
parser = ET.XMLPullParser(events=('start', 'end'))
|
||||
|
||||
Index: Python-3.12.3/Lib/test/test_sax.py
|
||||
===================================================================
|
||||
--- Python-3.12.3.orig/Lib/test/test_sax.py
|
||||
+++ Python-3.12.3/Lib/test/test_sax.py
|
||||
@@ -1240,6 +1240,9 @@ class ExpatReaderTest(XmlTestBase):
|
||||
|
||||
self.assertEqual(result.getvalue(), start + b"<doc></doc>")
|
||||
|
||||
+ @unittest.skipIf(pyexpat.version_info < (2, 6, 0),
|
||||
+ f'Expat {pyexpat.version_info} does not '
|
||||
+ 'support reparse deferral')
|
||||
def test_flush_reparse_deferral_disabled(self):
|
||||
result = BytesIO()
|
||||
xmlgen = XMLGenerator(result)
|
||||
Index: Python-3.12.3/Lib/test/test_pyexpat.py
|
||||
===================================================================
|
||||
--- Python-3.12.3.orig/Lib/test/test_pyexpat.py
|
||||
+++ Python-3.12.3/Lib/test/test_pyexpat.py
|
||||
@@ -794,6 +794,10 @@ class ReparseDeferralTest(unittest.TestC
|
||||
self.assertEqual(started, ['doc'])
|
||||
|
||||
def test_reparse_deferral_disabled(self):
|
||||
+ if expat.version_info < (2, 6, 0):
|
||||
+ self.skipTest(f'Expat {expat.version_info} does not '
|
||||
+ 'support reparse deferral')
|
||||
+
|
||||
started = []
|
||||
|
||||
def start_element(name, _):
|
||||
|
@ -1,171 +0,0 @@
|
||||
---
|
||||
Lib/tempfile.py | 16 +
|
||||
Lib/test/test_tempfile.py | 113 ++++++++++
|
||||
Misc/NEWS.d/next/Library/2022-12-01-16-57-44.gh-issue-91133.LKMVCV.rst | 2
|
||||
3 files changed, 131 insertions(+)
|
||||
|
||||
Index: Python-3.12.4/Lib/tempfile.py
|
||||
===================================================================
|
||||
--- Python-3.12.4.orig/Lib/tempfile.py
|
||||
+++ Python-3.12.4/Lib/tempfile.py
|
||||
@@ -285,6 +285,22 @@ def _resetperms(path):
|
||||
_dont_follow_symlinks(chflags, path, 0)
|
||||
_dont_follow_symlinks(_os.chmod, path, 0o700)
|
||||
|
||||
+def _dont_follow_symlinks(func, path, *args):
|
||||
+ # Pass follow_symlinks=False, unless not supported on this platform.
|
||||
+ if func in _os.supports_follow_symlinks:
|
||||
+ func(path, *args, follow_symlinks=False)
|
||||
+ elif _os.name == 'nt' or not _os.path.islink(path):
|
||||
+ func(path, *args)
|
||||
+
|
||||
+def _resetperms(path):
|
||||
+ try:
|
||||
+ chflags = _os.chflags
|
||||
+ except AttributeError:
|
||||
+ pass
|
||||
+ else:
|
||||
+ _dont_follow_symlinks(chflags, path, 0)
|
||||
+ _dont_follow_symlinks(_os.chmod, path, 0o700)
|
||||
+
|
||||
|
||||
# User visible interfaces.
|
||||
|
||||
Index: Python-3.12.4/Lib/test/test_tempfile.py
|
||||
===================================================================
|
||||
--- Python-3.12.4.orig/Lib/test/test_tempfile.py
|
||||
+++ Python-3.12.4/Lib/test/test_tempfile.py
|
||||
@@ -1803,6 +1803,103 @@ class TestTemporaryDirectory(BaseTestCas
|
||||
new_flags = os.stat(dir1).st_flags
|
||||
self.assertEqual(new_flags, old_flags)
|
||||
|
||||
+ @os_helper.skip_unless_symlink
|
||||
+ def test_cleanup_with_symlink_modes(self):
|
||||
+ # cleanup() should not follow symlinks when fixing mode bits (#91133)
|
||||
+ with self.do_create(recurse=0) as d2:
|
||||
+ file1 = os.path.join(d2, 'file1')
|
||||
+ open(file1, 'wb').close()
|
||||
+ dir1 = os.path.join(d2, 'dir1')
|
||||
+ os.mkdir(dir1)
|
||||
+ for mode in range(8):
|
||||
+ mode <<= 6
|
||||
+ with self.subTest(mode=format(mode, '03o')):
|
||||
+ def test(target, target_is_directory):
|
||||
+ d1 = self.do_create(recurse=0)
|
||||
+ symlink = os.path.join(d1.name, 'symlink')
|
||||
+ os.symlink(target, symlink,
|
||||
+ target_is_directory=target_is_directory)
|
||||
+ try:
|
||||
+ os.chmod(symlink, mode, follow_symlinks=False)
|
||||
+ except NotImplementedError:
|
||||
+ pass
|
||||
+ try:
|
||||
+ os.chmod(symlink, mode)
|
||||
+ except FileNotFoundError:
|
||||
+ pass
|
||||
+ os.chmod(d1.name, mode)
|
||||
+ d1.cleanup()
|
||||
+ self.assertFalse(os.path.exists(d1.name))
|
||||
+
|
||||
+ with self.subTest('nonexisting file'):
|
||||
+ test('nonexisting', target_is_directory=False)
|
||||
+ with self.subTest('nonexisting dir'):
|
||||
+ test('nonexisting', target_is_directory=True)
|
||||
+
|
||||
+ with self.subTest('existing file'):
|
||||
+ os.chmod(file1, mode)
|
||||
+ old_mode = os.stat(file1).st_mode
|
||||
+ test(file1, target_is_directory=False)
|
||||
+ new_mode = os.stat(file1).st_mode
|
||||
+ self.assertEqual(new_mode, old_mode,
|
||||
+ '%03o != %03o' % (new_mode, old_mode))
|
||||
+
|
||||
+ with self.subTest('existing dir'):
|
||||
+ os.chmod(dir1, mode)
|
||||
+ old_mode = os.stat(dir1).st_mode
|
||||
+ test(dir1, target_is_directory=True)
|
||||
+ new_mode = os.stat(dir1).st_mode
|
||||
+ self.assertEqual(new_mode, old_mode,
|
||||
+ '%03o != %03o' % (new_mode, old_mode))
|
||||
+
|
||||
+ @unittest.skipUnless(hasattr(os, 'chflags'), 'requires os.chflags')
|
||||
+ @os_helper.skip_unless_symlink
|
||||
+ def test_cleanup_with_symlink_flags(self):
|
||||
+ # cleanup() should not follow symlinks when fixing flags (#91133)
|
||||
+ flags = stat.UF_IMMUTABLE | stat.UF_NOUNLINK
|
||||
+ self.check_flags(flags)
|
||||
+
|
||||
+ with self.do_create(recurse=0) as d2:
|
||||
+ file1 = os.path.join(d2, 'file1')
|
||||
+ open(file1, 'wb').close()
|
||||
+ dir1 = os.path.join(d2, 'dir1')
|
||||
+ os.mkdir(dir1)
|
||||
+ def test(target, target_is_directory):
|
||||
+ d1 = self.do_create(recurse=0)
|
||||
+ symlink = os.path.join(d1.name, 'symlink')
|
||||
+ os.symlink(target, symlink,
|
||||
+ target_is_directory=target_is_directory)
|
||||
+ try:
|
||||
+ os.chflags(symlink, flags, follow_symlinks=False)
|
||||
+ except NotImplementedError:
|
||||
+ pass
|
||||
+ try:
|
||||
+ os.chflags(symlink, flags)
|
||||
+ except FileNotFoundError:
|
||||
+ pass
|
||||
+ os.chflags(d1.name, flags)
|
||||
+ d1.cleanup()
|
||||
+ self.assertFalse(os.path.exists(d1.name))
|
||||
+
|
||||
+ with self.subTest('nonexisting file'):
|
||||
+ test('nonexisting', target_is_directory=False)
|
||||
+ with self.subTest('nonexisting dir'):
|
||||
+ test('nonexisting', target_is_directory=True)
|
||||
+
|
||||
+ with self.subTest('existing file'):
|
||||
+ os.chflags(file1, flags)
|
||||
+ old_flags = os.stat(file1).st_flags
|
||||
+ test(file1, target_is_directory=False)
|
||||
+ new_flags = os.stat(file1).st_flags
|
||||
+ self.assertEqual(new_flags, old_flags)
|
||||
+
|
||||
+ with self.subTest('existing dir'):
|
||||
+ os.chflags(dir1, flags)
|
||||
+ old_flags = os.stat(dir1).st_flags
|
||||
+ test(dir1, target_is_directory=True)
|
||||
+ new_flags = os.stat(dir1).st_flags
|
||||
+ self.assertEqual(new_flags, old_flags)
|
||||
+
|
||||
@support.cpython_only
|
||||
def test_del_on_collection(self):
|
||||
# A TemporaryDirectory is deleted when garbage collected
|
||||
@@ -1977,6 +2074,22 @@ class TestTemporaryDirectory(BaseTestCas
|
||||
|
||||
def check_flags(self, flags):
|
||||
# skip the test if these flags are not supported (ex: FreeBSD 13)
|
||||
+ filename = os_helper.TESTFN
|
||||
+ try:
|
||||
+ open(filename, "w").close()
|
||||
+ try:
|
||||
+ os.chflags(filename, flags)
|
||||
+ except OSError as exc:
|
||||
+ # "OSError: [Errno 45] Operation not supported"
|
||||
+ self.skipTest(f"chflags() doesn't support flags "
|
||||
+ f"{flags:#b}: {exc}")
|
||||
+ else:
|
||||
+ os.chflags(filename, 0)
|
||||
+ finally:
|
||||
+ os_helper.unlink(filename)
|
||||
+
|
||||
+ def check_flags(self, flags):
|
||||
+ # skip the test if these flags are not supported (ex: FreeBSD 13)
|
||||
filename = os_helper.TESTFN
|
||||
try:
|
||||
open(filename, "w").close()
|
||||
Index: Python-3.12.4/Misc/NEWS.d/next/Library/2022-12-01-16-57-44.gh-issue-91133.LKMVCV.rst
|
||||
===================================================================
|
||||
--- /dev/null
|
||||
+++ Python-3.12.4/Misc/NEWS.d/next/Library/2022-12-01-16-57-44.gh-issue-91133.LKMVCV.rst
|
||||
@@ -0,0 +1,2 @@
|
||||
+Fix a bug in :class:`tempfile.TemporaryDirectory` cleanup, which now no longer
|
||||
+dereferences symlinks when working around file system permission errors.
|
@ -1,148 +0,0 @@
|
||||
---
|
||||
Lib/test/test_zipfile/_path/test_path.py | 78 ++++++++++
|
||||
Lib/zipfile/_path/__init__.py | 18 ++
|
||||
Misc/NEWS.d/next/Library/2024-08-11-14-08-04.gh-issue-122905.7tDsxA.rst | 1
|
||||
Misc/NEWS.d/next/Library/2024-08-26-13-45-20.gh-issue-123270.gXHvNJ.rst | 3
|
||||
4 files changed, 98 insertions(+), 2 deletions(-)
|
||||
|
||||
--- a/Lib/test/test_zipfile/_path/test_path.py
|
||||
+++ b/Lib/test/test_zipfile/_path/test_path.py
|
||||
@@ -4,6 +4,7 @@ import contextlib
|
||||
import pathlib
|
||||
import pickle
|
||||
import sys
|
||||
+import time
|
||||
import unittest
|
||||
import zipfile
|
||||
|
||||
@@ -577,3 +578,80 @@ class TestPath(unittest.TestCase):
|
||||
zipfile.Path(alpharep)
|
||||
with self.assertRaises(KeyError):
|
||||
alpharep.getinfo('does-not-exist')
|
||||
+
|
||||
+ def test_malformed_paths(self):
|
||||
+ """
|
||||
+ Path should handle malformed paths gracefully.
|
||||
+
|
||||
+ Paths with leading slashes are not visible.
|
||||
+
|
||||
+ Paths with dots are treated like regular files.
|
||||
+ """
|
||||
+ data = io.BytesIO()
|
||||
+ zf = zipfile.ZipFile(data, "w")
|
||||
+ zf.writestr("/one-slash.txt", b"content")
|
||||
+ zf.writestr("//two-slash.txt", b"content")
|
||||
+ zf.writestr("../parent.txt", b"content")
|
||||
+ zf.filename = ''
|
||||
+ root = zipfile.Path(zf)
|
||||
+ assert list(map(str, root.iterdir())) == ['../']
|
||||
+ assert root.joinpath('..').joinpath('parent.txt').read_bytes() == b'content'
|
||||
+
|
||||
+ def test_unsupported_names(self):
|
||||
+ """
|
||||
+ Path segments with special characters are readable.
|
||||
+
|
||||
+ On some platforms or file systems, characters like
|
||||
+ ``:`` and ``?`` are not allowed, but they are valid
|
||||
+ in the zip file.
|
||||
+ """
|
||||
+ data = io.BytesIO()
|
||||
+ zf = zipfile.ZipFile(data, "w")
|
||||
+ zf.writestr("path?", b"content")
|
||||
+ zf.writestr("V: NMS.flac", b"fLaC...")
|
||||
+ zf.filename = ''
|
||||
+ root = zipfile.Path(zf)
|
||||
+ contents = root.iterdir()
|
||||
+ assert next(contents).name == 'path?'
|
||||
+ assert next(contents).name == 'V: NMS.flac'
|
||||
+ assert root.joinpath('V: NMS.flac').read_bytes() == b"fLaC..."
|
||||
+
|
||||
+ def test_backslash_not_separator(self):
|
||||
+ """
|
||||
+ In a zip file, backslashes are not separators.
|
||||
+ """
|
||||
+ data = io.BytesIO()
|
||||
+ zf = zipfile.ZipFile(data, "w")
|
||||
+ zf.writestr(DirtyZipInfo.for_name("foo\\bar", zf), b"content")
|
||||
+ zf.filename = ''
|
||||
+ root = zipfile.Path(zf)
|
||||
+ (first,) = root.iterdir()
|
||||
+ assert not first.is_dir()
|
||||
+ assert first.name == 'foo\\bar'
|
||||
+
|
||||
+
|
||||
+class DirtyZipInfo(zipfile.ZipInfo):
|
||||
+ """
|
||||
+ Bypass name sanitization.
|
||||
+ """
|
||||
+
|
||||
+ def __init__(self, filename, *args, **kwargs):
|
||||
+ super().__init__(filename, *args, **kwargs)
|
||||
+ self.filename = filename
|
||||
+
|
||||
+ @classmethod
|
||||
+ def for_name(cls, name, archive):
|
||||
+ """
|
||||
+ Construct the same way that ZipFile.writestr does.
|
||||
+
|
||||
+ TODO: extract this functionality and re-use
|
||||
+ """
|
||||
+ self = cls(filename=name, date_time=time.localtime(time.time())[:6])
|
||||
+ self.compress_type = archive.compression
|
||||
+ self.compress_level = archive.compresslevel
|
||||
+ if self.filename.endswith('/'): # pragma: no cover
|
||||
+ self.external_attr = 0o40775 << 16 # drwxrwxr-x
|
||||
+ self.external_attr |= 0x10 # MS-DOS directory flag
|
||||
+ else:
|
||||
+ self.external_attr = 0o600 << 16 # ?rw-------
|
||||
+ return self
|
||||
--- a/Lib/zipfile/_path/__init__.py
|
||||
+++ b/Lib/zipfile/_path/__init__.py
|
||||
@@ -1,3 +1,12 @@
|
||||
+"""
|
||||
+A Path-like interface for zipfiles.
|
||||
+
|
||||
+This codebase is shared between zipfile.Path in the stdlib
|
||||
+and zipp in PyPI. See
|
||||
+https://github.com/python/importlib_metadata/wiki/Development-Methodology
|
||||
+for more detail.
|
||||
+"""
|
||||
+
|
||||
import io
|
||||
import posixpath
|
||||
import zipfile
|
||||
@@ -34,7 +43,7 @@ def _parents(path):
|
||||
def _ancestry(path):
|
||||
"""
|
||||
Given a path with elements separated by
|
||||
- posixpath.sep, generate all elements of that path
|
||||
+ posixpath.sep, generate all elements of that path.
|
||||
|
||||
>>> list(_ancestry('b/d'))
|
||||
['b/d', 'b']
|
||||
@@ -46,9 +55,14 @@ def _ancestry(path):
|
||||
['b']
|
||||
>>> list(_ancestry(''))
|
||||
[]
|
||||
+
|
||||
+ Multiple separators are treated like a single.
|
||||
+
|
||||
+ >>> list(_ancestry('//b//d///f//'))
|
||||
+ ['//b//d///f', '//b//d', '//b']
|
||||
"""
|
||||
path = path.rstrip(posixpath.sep)
|
||||
- while path and path != posixpath.sep:
|
||||
+ while path.rstrip(posixpath.sep):
|
||||
yield path
|
||||
path, tail = posixpath.split(path)
|
||||
|
||||
--- /dev/null
|
||||
+++ b/Misc/NEWS.d/next/Library/2024-08-11-14-08-04.gh-issue-122905.7tDsxA.rst
|
||||
@@ -0,0 +1 @@
|
||||
+:class:`zipfile.Path` objects now sanitize names from the zipfile.
|
||||
--- /dev/null
|
||||
+++ b/Misc/NEWS.d/next/Library/2024-08-26-13-45-20.gh-issue-123270.gXHvNJ.rst
|
||||
@@ -0,0 +1,3 @@
|
||||
+Applied a more surgical fix for malformed payloads in :class:`zipfile.Path`
|
||||
+causing infinite loops (gh-122905) without breaking contents using
|
||||
+legitimate characters.
|
303
CVE-2024-9287-venv_path_unquoted.patch
Normal file
303
CVE-2024-9287-venv_path_unquoted.patch
Normal file
@ -0,0 +1,303 @@
|
||||
From 1408cc9bf9e8b19968761548c30b78d37074c21c Mon Sep 17 00:00:00 2001
|
||||
From: Y5 <124019959+y5c4l3@users.noreply.github.com>
|
||||
Date: Tue, 22 Oct 2024 04:48:04 +0800
|
||||
Subject: [PATCH] gh-124651: Quote template strings in `venv` activation
|
||||
scripts (GH-124712)
|
||||
|
||||
This patch properly quotes template strings in `venv` activation
|
||||
scripts. This mitigates potential command injection.
|
||||
|
||||
(cherry picked from commit d48cc82ed25e26b02eb97c6263d95dcaa1e9111b)
|
||||
---
|
||||
Lib/test/test_venv.py | 81 ++++++++++
|
||||
Lib/venv/__init__.py | 42 ++++-
|
||||
Lib/venv/scripts/common/activate | 10 -
|
||||
Lib/venv/scripts/nt/activate.bat | 6
|
||||
Lib/venv/scripts/posix/activate.csh | 8
|
||||
Lib/venv/scripts/posix/activate.fish | 8
|
||||
Misc/NEWS.d/next/Library/2024-09-28-02-03-04.gh-issue-124651.bLBGtH.rst | 1
|
||||
7 files changed, 135 insertions(+), 21 deletions(-)
|
||||
create mode 100644 Misc/NEWS.d/next/Library/2024-09-28-02-03-04.gh-issue-124651.bLBGtH.rst
|
||||
|
||||
--- a/Lib/test/test_venv.py
|
||||
+++ b/Lib/test/test_venv.py
|
||||
@@ -17,6 +17,7 @@ import subprocess
|
||||
import sys
|
||||
import sysconfig
|
||||
import tempfile
|
||||
+import shlex
|
||||
from test.support import (captured_stdout, captured_stderr,
|
||||
skip_if_broken_multiprocessing_synchronize, verbose,
|
||||
requires_subprocess, is_emscripten, is_wasi,
|
||||
@@ -97,6 +98,10 @@ class BaseTest(unittest.TestCase):
|
||||
result = f.read()
|
||||
return result
|
||||
|
||||
+ def assertEndsWith(self, string, tail):
|
||||
+ if not string.endswith(tail):
|
||||
+ self.fail(f"String {string!r} does not end with {tail!r}")
|
||||
+
|
||||
class BasicTest(BaseTest):
|
||||
"""Test venv module functionality."""
|
||||
|
||||
@@ -446,6 +451,82 @@ class BasicTest(BaseTest):
|
||||
'import sys; print(sys.executable)'])
|
||||
self.assertEqual(out.strip(), envpy.encode())
|
||||
|
||||
+ # gh-124651: test quoted strings
|
||||
+ @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows')
|
||||
+ def test_special_chars_bash(self):
|
||||
+ """
|
||||
+ Test that the template strings are quoted properly (bash)
|
||||
+ """
|
||||
+ rmtree(self.env_dir)
|
||||
+ bash = shutil.which('bash')
|
||||
+ if bash is None:
|
||||
+ self.skipTest('bash required for this test')
|
||||
+ env_name = '"\';&&$e|\'"'
|
||||
+ env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
|
||||
+ builder = venv.EnvBuilder(clear=True)
|
||||
+ builder.create(env_dir)
|
||||
+ activate = os.path.join(env_dir, self.bindir, 'activate')
|
||||
+ test_script = os.path.join(self.env_dir, 'test_special_chars.sh')
|
||||
+ with open(test_script, "w") as f:
|
||||
+ f.write(f'source {shlex.quote(activate)}\n'
|
||||
+ 'python -c \'import sys; print(sys.executable)\'\n'
|
||||
+ 'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n'
|
||||
+ 'deactivate\n')
|
||||
+ out, err = check_output([bash, test_script])
|
||||
+ lines = out.splitlines()
|
||||
+ self.assertTrue(env_name.encode() in lines[0])
|
||||
+ self.assertEndsWith(lines[1], env_name.encode())
|
||||
+
|
||||
+ # gh-124651: test quoted strings
|
||||
+ @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows')
|
||||
+ def test_special_chars_csh(self):
|
||||
+ """
|
||||
+ Test that the template strings are quoted properly (csh)
|
||||
+ """
|
||||
+ rmtree(self.env_dir)
|
||||
+ csh = shutil.which('tcsh') or shutil.which('csh')
|
||||
+ if csh is None:
|
||||
+ self.skipTest('csh required for this test')
|
||||
+ env_name = '"\';&&$e|\'"'
|
||||
+ env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
|
||||
+ builder = venv.EnvBuilder(clear=True)
|
||||
+ builder.create(env_dir)
|
||||
+ activate = os.path.join(env_dir, self.bindir, 'activate.csh')
|
||||
+ test_script = os.path.join(self.env_dir, 'test_special_chars.csh')
|
||||
+ with open(test_script, "w") as f:
|
||||
+ f.write(f'source {shlex.quote(activate)}\n'
|
||||
+ 'python -c \'import sys; print(sys.executable)\'\n'
|
||||
+ 'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n'
|
||||
+ 'deactivate\n')
|
||||
+ out, err = check_output([csh, test_script])
|
||||
+ lines = out.splitlines()
|
||||
+ self.assertTrue(env_name.encode() in lines[0])
|
||||
+ self.assertEndsWith(lines[1], env_name.encode())
|
||||
+
|
||||
+ # gh-124651: test quoted strings on Windows
|
||||
+ @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
|
||||
+ def test_special_chars_windows(self):
|
||||
+ """
|
||||
+ Test that the template strings are quoted properly on Windows
|
||||
+ """
|
||||
+ rmtree(self.env_dir)
|
||||
+ env_name = "'&&^$e"
|
||||
+ env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
|
||||
+ builder = venv.EnvBuilder(clear=True)
|
||||
+ builder.create(env_dir)
|
||||
+ activate = os.path.join(env_dir, self.bindir, 'activate.bat')
|
||||
+ test_batch = os.path.join(self.env_dir, 'test_special_chars.bat')
|
||||
+ with open(test_batch, "w") as f:
|
||||
+ f.write('@echo off\n'
|
||||
+ f'"{activate}" & '
|
||||
+ f'{self.exe} -c "import sys; print(sys.executable)" & '
|
||||
+ f'{self.exe} -c "import os; print(os.environ[\'VIRTUAL_ENV\'])" & '
|
||||
+ 'deactivate')
|
||||
+ out, err = check_output([test_batch])
|
||||
+ lines = out.splitlines()
|
||||
+ self.assertTrue(env_name.encode() in lines[0])
|
||||
+ self.assertEndsWith(lines[1], env_name.encode())
|
||||
+
|
||||
@unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
|
||||
def test_unicode_in_batch_file(self):
|
||||
"""
|
||||
--- a/Lib/venv/__init__.py
|
||||
+++ b/Lib/venv/__init__.py
|
||||
@@ -11,6 +11,7 @@ import subprocess
|
||||
import sys
|
||||
import sysconfig
|
||||
import types
|
||||
+import shlex
|
||||
|
||||
|
||||
CORE_VENV_DEPS = ('pip',)
|
||||
@@ -422,11 +423,41 @@ class EnvBuilder:
|
||||
:param context: The information for the environment creation request
|
||||
being processed.
|
||||
"""
|
||||
- text = text.replace('__VENV_DIR__', context.env_dir)
|
||||
- text = text.replace('__VENV_NAME__', context.env_name)
|
||||
- text = text.replace('__VENV_PROMPT__', context.prompt)
|
||||
- text = text.replace('__VENV_BIN_NAME__', context.bin_name)
|
||||
- text = text.replace('__VENV_PYTHON__', context.env_exe)
|
||||
+ replacements = {
|
||||
+ '__VENV_DIR__': context.env_dir,
|
||||
+ '__VENV_NAME__': context.env_name,
|
||||
+ '__VENV_PROMPT__': context.prompt,
|
||||
+ '__VENV_BIN_NAME__': context.bin_name,
|
||||
+ '__VENV_PYTHON__': context.env_exe,
|
||||
+ }
|
||||
+
|
||||
+ def quote_ps1(s):
|
||||
+ """
|
||||
+ This should satisfy PowerShell quoting rules [1], unless the quoted
|
||||
+ string is passed directly to Windows native commands [2].
|
||||
+ [1]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules
|
||||
+ [2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing#passing-arguments-that-contain-quote-characters
|
||||
+ """
|
||||
+ s = s.replace("'", "''")
|
||||
+ return f"'{s}'"
|
||||
+
|
||||
+ def quote_bat(s):
|
||||
+ return s
|
||||
+
|
||||
+ # gh-124651: need to quote the template strings properly
|
||||
+ quote = shlex.quote
|
||||
+ script_path = context.script_path
|
||||
+ if script_path.endswith('.ps1'):
|
||||
+ quote = quote_ps1
|
||||
+ elif script_path.endswith('.bat'):
|
||||
+ quote = quote_bat
|
||||
+ else:
|
||||
+ # fallbacks to POSIX shell compliant quote
|
||||
+ quote = shlex.quote
|
||||
+
|
||||
+ replacements = {key: quote(s) for key, s in replacements.items()}
|
||||
+ for key, quoted in replacements.items():
|
||||
+ text = text.replace(key, quoted)
|
||||
return text
|
||||
|
||||
def install_scripts(self, context, path):
|
||||
@@ -466,6 +497,7 @@ class EnvBuilder:
|
||||
with open(srcfile, 'rb') as f:
|
||||
data = f.read()
|
||||
if not srcfile.endswith(('.exe', '.pdb')):
|
||||
+ context.script_path = srcfile
|
||||
try:
|
||||
data = data.decode('utf-8')
|
||||
data = self.replace_variables(data, context)
|
||||
--- a/Lib/venv/scripts/common/activate
|
||||
+++ b/Lib/venv/scripts/common/activate
|
||||
@@ -39,14 +39,14 @@ deactivate nondestructive
|
||||
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
|
||||
# transform D:\path\to\venv to /d/path/to/venv on MSYS
|
||||
# and to /cygdrive/d/path/to/venv on Cygwin
|
||||
- export VIRTUAL_ENV=$(cygpath "__VENV_DIR__")
|
||||
+ export VIRTUAL_ENV=$(cygpath __VENV_DIR__)
|
||||
else
|
||||
# use the path as-is
|
||||
- export VIRTUAL_ENV="__VENV_DIR__"
|
||||
+ export VIRTUAL_ENV=__VENV_DIR__
|
||||
fi
|
||||
|
||||
_OLD_VIRTUAL_PATH="$PATH"
|
||||
-PATH="$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH"
|
||||
+PATH="$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH"
|
||||
export PATH
|
||||
|
||||
# unset PYTHONHOME if set
|
||||
@@ -59,9 +59,9 @@ fi
|
||||
|
||||
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||
- PS1="__VENV_PROMPT__${PS1:-}"
|
||||
+ PS1=__VENV_PROMPT__"${PS1:-}"
|
||||
export PS1
|
||||
- VIRTUAL_ENV_PROMPT="__VENV_PROMPT__"
|
||||
+ VIRTUAL_ENV_PROMPT=__VENV_PROMPT__
|
||||
export VIRTUAL_ENV_PROMPT
|
||||
fi
|
||||
|
||||
--- a/Lib/venv/scripts/nt/activate.bat
|
||||
+++ b/Lib/venv/scripts/nt/activate.bat
|
||||
@@ -8,7 +8,7 @@ if defined _OLD_CODEPAGE (
|
||||
"%SystemRoot%\System32\chcp.com" 65001 > nul
|
||||
)
|
||||
|
||||
-set VIRTUAL_ENV=__VENV_DIR__
|
||||
+set "VIRTUAL_ENV=__VENV_DIR__"
|
||||
|
||||
if not defined PROMPT set PROMPT=$P$G
|
||||
|
||||
@@ -24,8 +24,8 @@ set PYTHONHOME=
|
||||
if defined _OLD_VIRTUAL_PATH set PATH=%_OLD_VIRTUAL_PATH%
|
||||
if not defined _OLD_VIRTUAL_PATH set _OLD_VIRTUAL_PATH=%PATH%
|
||||
|
||||
-set PATH=%VIRTUAL_ENV%\__VENV_BIN_NAME__;%PATH%
|
||||
-set VIRTUAL_ENV_PROMPT=__VENV_PROMPT__
|
||||
+set "PATH=%VIRTUAL_ENV%\__VENV_BIN_NAME__;%PATH%"
|
||||
+set "VIRTUAL_ENV_PROMPT=__VENV_PROMPT__"
|
||||
|
||||
:END
|
||||
if defined _OLD_CODEPAGE (
|
||||
--- a/Lib/venv/scripts/posix/activate.csh
|
||||
+++ b/Lib/venv/scripts/posix/activate.csh
|
||||
@@ -9,17 +9,17 @@ alias deactivate 'test $?_OLD_VIRTUAL_PA
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
-setenv VIRTUAL_ENV "__VENV_DIR__"
|
||||
+setenv VIRTUAL_ENV __VENV_DIR__
|
||||
|
||||
set _OLD_VIRTUAL_PATH="$PATH"
|
||||
-setenv PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH"
|
||||
+setenv PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH"
|
||||
|
||||
|
||||
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||
|
||||
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||
- set prompt = "__VENV_PROMPT__$prompt"
|
||||
- setenv VIRTUAL_ENV_PROMPT "__VENV_PROMPT__"
|
||||
+ set prompt = __VENV_PROMPT__"$prompt"
|
||||
+ setenv VIRTUAL_ENV_PROMPT __VENV_PROMPT__
|
||||
endif
|
||||
|
||||
alias pydoc python -m pydoc
|
||||
--- a/Lib/venv/scripts/posix/activate.fish
|
||||
+++ b/Lib/venv/scripts/posix/activate.fish
|
||||
@@ -33,10 +33,10 @@ end
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
-set -gx VIRTUAL_ENV "__VENV_DIR__"
|
||||
+set -gx VIRTUAL_ENV __VENV_DIR__
|
||||
|
||||
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||
-set -gx PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__" $PATH
|
||||
+set -gx PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__ $PATH
|
||||
|
||||
# Unset PYTHONHOME if set.
|
||||
if set -q PYTHONHOME
|
||||
@@ -56,7 +56,7 @@ if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||
set -l old_status $status
|
||||
|
||||
# Output the venv prompt; color taken from the blue of the Python logo.
|
||||
- printf "%s%s%s" (set_color 4B8BBE) "__VENV_PROMPT__" (set_color normal)
|
||||
+ printf "%s%s%s" (set_color 4B8BBE) __VENV_PROMPT__ (set_color normal)
|
||||
|
||||
# Restore the return status of the previous command.
|
||||
echo "exit $old_status" | .
|
||||
@@ -65,5 +65,5 @@ if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||
end
|
||||
|
||||
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||
- set -gx VIRTUAL_ENV_PROMPT "__VENV_PROMPT__"
|
||||
+ set -gx VIRTUAL_ENV_PROMPT __VENV_PROMPT__
|
||||
end
|
||||
--- /dev/null
|
||||
+++ b/Misc/NEWS.d/next/Library/2024-09-28-02-03-04.gh-issue-124651.bLBGtH.rst
|
||||
@@ -0,0 +1 @@
|
||||
+Properly quote template strings in :mod:`venv` activation scripts.
|
@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fa8a2e12c5e620b09f53e65bcd87550d2e5a1e2e04bf8ba991dcc55113876397
|
||||
size 20422396
|
@ -1,18 +0,0 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQKTBAABCgB9FiEEcWlgX2LHUTVtBUomqCHmgOX6YwUFAmayiFtfFIAAAAAALgAo
|
||||
aXNzdWVyLWZwckBub3RhdGlvbnMub3BlbnBncC5maWZ0aGhvcnNlbWFuLm5ldDcx
|
||||
Njk2MDVGNjJDNzUxMzU2RDA1NEEyNkE4MjFFNjgwRTVGQTYzMDUACgkQqCHmgOX6
|
||||
YwUr4g//VyVs9tvbtiSp8pGe8f1gYErEw54r124sL/CBuNii8Irts1j5ymGxcm+l
|
||||
hshPK5UlqRnhd5dCJWFTvLTXa5Ko2R1L3JyyxfGd1hmDuMhrWsDHijI0R7L/mGM5
|
||||
6X2LTaadBVNvk8HaNKvR8SEWvo68rdnOuYElFA9ir7uqwjO26ZWz9FfH80YDGwo8
|
||||
Blef2NYw8rNhiaZMFV0HYV7D+YyUAZnFNfW8M7Fd4oskUyj1tD9J89T9FFLYN09d
|
||||
BcCIf+EdiEfqRpKxH89bW2g52kDrm4jYGONtpyF8eruyS3YwYSbvbuWioBYKmlxC
|
||||
s51mieXz6G325GTZnmPxLek3ywPv6Gil9y0wH3fIr2BsWsmXust4LBpjDGt56Fy6
|
||||
seokGBg8xzsBSk3iEqNoFmNsy/QOiuCcDejX4XqBDNodOlETQPJb07TkTI2iOmg9
|
||||
NG4Atiz1HvGVxK68UuK9IIcNHyaWUmH8h4VQFGvc6KV6feP5Nm21Y12PZ5XIqJBO
|
||||
Y8M/VJIJ5koaNPQfnBbbI5YBkUr4BVpIXIpY5LM/L5sUo2C3R7hMi0VGK88HGfSQ
|
||||
KV4JmZgf6RMBNmrWY12sryS1QQ6q3P110GTUGQWB3sxxNbhmfcrK+4viqHc83yDz
|
||||
ifmk33HuqaQGU7OzUMHeNcoCJIPo3H1FpoHOn9wLLCtA1pT+as4=
|
||||
=t0Rk
|
||||
-----END PGP SIGNATURE-----
|
3
Python-3.12.7.tar.xz
Normal file
3
Python-3.12.7.tar.xz
Normal file
@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:24887b92e2afd4a2ac602419ad4b596372f67ac9b077190f459aba390faf5550
|
||||
size 20444032
|
18
Python-3.12.7.tar.xz.asc
Normal file
18
Python-3.12.7.tar.xz.asc
Normal file
@ -0,0 +1,18 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQKTBAABCgB9FiEEcWlgX2LHUTVtBUomqCHmgOX6YwUFAmb7fPtfFIAAAAAALgAo
|
||||
aXNzdWVyLWZwckBub3RhdGlvbnMub3BlbnBncC5maWZ0aGhvcnNlbWFuLm5ldDcx
|
||||
Njk2MDVGNjJDNzUxMzU2RDA1NEEyNkE4MjFFNjgwRTVGQTYzMDUACgkQqCHmgOX6
|
||||
YwUVbw//ZIkOgBaCTVKbjfLbOj9aui1v4kFEex0AB4H/RdmJwrO4S+p76UuHb2c3
|
||||
eG65uL8KWwo1CmX7O62YFVjVkMCaYn5J96ReDVP4XygglYv1HtxFhOwH9OhsAjhT
|
||||
1HIvdx22mO+xpqMBus5+1W6pwgfmMV6eWvVQasDp3mXPRv/x7imjZUfT5sr8kH49
|
||||
BWztinQ3PIN4JsPdAXGT/H08FVR5zC/4BG5GRMcDH+g5kqFCF97TxLoY2auP8XS+
|
||||
WDAomj5wfPdsGk5CDZ5ImRikwJlsr5A9QZxCv4FWH4j4ndUr/i9Spuut9t+hBgrd
|
||||
jYc2vMPkm97RDlSd78Bfz5kUCH9hJSC3r/kpa0rk1QLKHk8h8OGMgXG4FLGl4hVw
|
||||
M/NZQ9A7A6FDApvLXc5CbTEpgRlldkIr2kae9KR803E28sdEl6pQqr0lvhoLPe5i
|
||||
OhPvsmJdazIo/R7AJkNdmU7T+E0Ikova3TOxVyr3ryyjW4UrppIdxiWeAJ8ood4U
|
||||
tcJ1Dq9+Zj5oo/88TR/KwkYrM7FFUKRa1AgirUk88v+16z2qVgf0cSUAW10rTWAZ
|
||||
FP8K+ADR31AXAQrxLOAzaP/NXgAjRuTmuTLrdWE5K+kbSTKbUigKY4z+IKVvq/qA
|
||||
SoN2v6/22do+z4OQMnPDGBU3t8iTqIeuvOMCfYsQBTAZqqq5y4Q=
|
||||
=kx8a
|
||||
-----END PGP SIGNATURE-----
|
374
doc-py38-to-py36.patch
Normal file
374
doc-py38-to-py36.patch
Normal file
@ -0,0 +1,374 @@
|
||||
---
|
||||
Doc/conf.py | 4 +-
|
||||
Doc/tools/check-warnings.py | 3 +
|
||||
Doc/tools/extensions/audit_events.py | 54 ++++++++++++++++----------------
|
||||
Doc/tools/extensions/c_annotations.py | 33 ++++++++-----------
|
||||
Doc/tools/extensions/glossary_search.py | 10 +----
|
||||
Doc/tools/extensions/patchlevel.py | 9 ++---
|
||||
6 files changed, 55 insertions(+), 58 deletions(-)
|
||||
|
||||
--- a/Doc/conf.py
|
||||
+++ b/Doc/conf.py
|
||||
@@ -76,7 +76,7 @@ today_fmt = '%B %d, %Y'
|
||||
highlight_language = 'python3'
|
||||
|
||||
# Minimum version of sphinx required
|
||||
-needs_sphinx = '6.2.1'
|
||||
+needs_sphinx = '4.2.0'
|
||||
|
||||
# Create table of contents entries for domain objects (e.g. functions, classes,
|
||||
# attributes, etc.). Default is True.
|
||||
@@ -329,7 +329,7 @@ html_short_title = f'{release} Documenta
|
||||
# (See .readthedocs.yml and https://docs.readthedocs.io/en/stable/reference/environment-variables.html)
|
||||
is_deployment_preview = os.getenv("READTHEDOCS_VERSION_TYPE") == "external"
|
||||
repository_url = os.getenv("READTHEDOCS_GIT_CLONE_URL", "")
|
||||
-repository_url = repository_url.removesuffix(".git")
|
||||
+repository_url = repository_url[:-len(".git")]
|
||||
html_context = {
|
||||
"is_deployment_preview": is_deployment_preview,
|
||||
"repository_url": repository_url or None,
|
||||
--- a/Doc/tools/check-warnings.py
|
||||
+++ b/Doc/tools/check-warnings.py
|
||||
@@ -228,7 +228,8 @@ def fail_if_regression(
|
||||
print(filename)
|
||||
for warning in warnings:
|
||||
if filename in warning:
|
||||
- if match := WARNING_PATTERN.fullmatch(warning):
|
||||
+ match = WARNING_PATTERN.fullmatch(warning)
|
||||
+ if match:
|
||||
print(" {line}: {msg}".format_map(match))
|
||||
return -1
|
||||
return 0
|
||||
--- a/Doc/tools/extensions/audit_events.py
|
||||
+++ b/Doc/tools/extensions/audit_events.py
|
||||
@@ -1,9 +1,6 @@
|
||||
"""Support for documenting audit events."""
|
||||
|
||||
-from __future__ import annotations
|
||||
-
|
||||
import re
|
||||
-from typing import TYPE_CHECKING
|
||||
|
||||
from docutils import nodes
|
||||
from sphinx.errors import NoUri
|
||||
@@ -12,12 +9,11 @@ from sphinx.transforms.post_transforms i
|
||||
from sphinx.util import logging
|
||||
from sphinx.util.docutils import SphinxDirective
|
||||
|
||||
-if TYPE_CHECKING:
|
||||
- from collections.abc import Iterator
|
||||
+from typing import Any, List, Tuple
|
||||
|
||||
- from sphinx.application import Sphinx
|
||||
- from sphinx.builders import Builder
|
||||
- from sphinx.environment import BuildEnvironment
|
||||
+from sphinx.application import Sphinx
|
||||
+from sphinx.builders import Builder
|
||||
+from sphinx.environment import BuildEnvironment
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -32,16 +28,16 @@ _SYNONYMS = [
|
||||
|
||||
class AuditEvents:
|
||||
def __init__(self) -> None:
|
||||
- self.events: dict[str, list[str]] = {}
|
||||
- self.sources: dict[str, list[tuple[str, str]]] = {}
|
||||
+ self.events: dict[str, List[str]] = {}
|
||||
+ self.sources: dict[str, List[Tuple[str, str]]] = {}
|
||||
|
||||
- def __iter__(self) -> Iterator[tuple[str, list[str], tuple[str, str]]]:
|
||||
+ def __iter__(self) -> Any:
|
||||
for name, args in self.events.items():
|
||||
for source in self.sources[name]:
|
||||
yield name, args, source
|
||||
|
||||
def add_event(
|
||||
- self, name, args: list[str], source: tuple[str, str]
|
||||
+ self, name, args: List[str], source: Tuple[str, str]
|
||||
) -> None:
|
||||
if name in self.events:
|
||||
self._check_args_match(name, args)
|
||||
@@ -49,7 +45,7 @@ class AuditEvents:
|
||||
self.events[name] = args
|
||||
self.sources.setdefault(name, []).append(source)
|
||||
|
||||
- def _check_args_match(self, name: str, args: list[str]) -> None:
|
||||
+ def _check_args_match(self, name: str, args: List[str]) -> None:
|
||||
current_args = self.events[name]
|
||||
msg = (
|
||||
f"Mismatched arguments for audit-event {name}: "
|
||||
@@ -60,7 +56,7 @@ class AuditEvents:
|
||||
if len(current_args) != len(args):
|
||||
logger.warning(msg)
|
||||
return
|
||||
- for a1, a2 in zip(current_args, args, strict=False):
|
||||
+ for a1, a2 in zip(current_args, args):
|
||||
if a1 == a2:
|
||||
continue
|
||||
if any(a1 in s and a2 in s for s in _SYNONYMS):
|
||||
@@ -73,7 +69,7 @@ class AuditEvents:
|
||||
name_clean = re.sub(r"\W", "_", name)
|
||||
return f"audit_event_{name_clean}_{source_count}"
|
||||
|
||||
- def rows(self) -> Iterator[tuple[str, list[str], list[tuple[str, str]]]]:
|
||||
+ def rows(self) -> Any:
|
||||
for name in sorted(self.events.keys()):
|
||||
yield name, self.events[name], self.sources[name]
|
||||
|
||||
@@ -97,7 +93,7 @@ def audit_events_purge(
|
||||
def audit_events_merge(
|
||||
app: Sphinx,
|
||||
env: BuildEnvironment,
|
||||
- docnames: list[str],
|
||||
+ docnames: List[str],
|
||||
other: BuildEnvironment,
|
||||
) -> None:
|
||||
"""In Sphinx parallel builds, this merges audit_events from subprocesses."""
|
||||
@@ -126,14 +122,16 @@ class AuditEvent(SphinxDirective):
|
||||
),
|
||||
]
|
||||
|
||||
- def run(self) -> list[nodes.paragraph]:
|
||||
+ def run(self) -> List[nodes.paragraph]:
|
||||
+ def _no_walrus_op(args):
|
||||
+ for arg in args.strip("'\"").split(","):
|
||||
+ aarg = arg.strip()
|
||||
+ if aarg:
|
||||
+ yield aarg
|
||||
+
|
||||
name = self.arguments[0]
|
||||
if len(self.arguments) >= 2 and self.arguments[1]:
|
||||
- args = [
|
||||
- arg
|
||||
- for argument in self.arguments[1].strip("'\"").split(",")
|
||||
- if (arg := argument.strip())
|
||||
- ]
|
||||
+ args = list(_no_walrus_op(self.arguments[1]))
|
||||
else:
|
||||
args = []
|
||||
ids = []
|
||||
@@ -169,7 +167,7 @@ class audit_event_list(nodes.General, no
|
||||
|
||||
|
||||
class AuditEventListDirective(SphinxDirective):
|
||||
- def run(self) -> list[audit_event_list]:
|
||||
+ def run(self) -> List[audit_event_list]:
|
||||
return [audit_event_list()]
|
||||
|
||||
|
||||
@@ -181,7 +179,11 @@ class AuditEventListTransform(SphinxPost
|
||||
return
|
||||
|
||||
table = self._make_table(self.app.builder, self.env.docname)
|
||||
- for node in self.document.findall(audit_event_list):
|
||||
+ try:
|
||||
+ findall = self.document.findall
|
||||
+ except AttributeError:
|
||||
+ findall = self.document.traverse
|
||||
+ for node in findall(audit_event_list):
|
||||
node.replace_self(table)
|
||||
|
||||
def _make_table(self, builder: Builder, docname: str) -> nodes.table:
|
||||
@@ -217,8 +219,8 @@ class AuditEventListTransform(SphinxPost
|
||||
builder: Builder,
|
||||
docname: str,
|
||||
name: str,
|
||||
- args: list[str],
|
||||
- sources: list[tuple[str, str]],
|
||||
+ args: List[str],
|
||||
+ sources: List[Tuple[str, str]],
|
||||
) -> nodes.row:
|
||||
row = nodes.row()
|
||||
name_node = nodes.paragraph("", nodes.Text(name))
|
||||
--- a/Doc/tools/extensions/c_annotations.py
|
||||
+++ b/Doc/tools/extensions/c_annotations.py
|
||||
@@ -9,12 +9,10 @@ Configuration:
|
||||
* Set ``stable_abi_file`` to the path to stable ABI list.
|
||||
"""
|
||||
|
||||
-from __future__ import annotations
|
||||
-
|
||||
import csv
|
||||
import dataclasses
|
||||
from pathlib import Path
|
||||
-from typing import TYPE_CHECKING
|
||||
+from typing import Any, Dict, List, TYPE_CHECKING, Union
|
||||
|
||||
import sphinx
|
||||
from docutils import nodes
|
||||
@@ -23,9 +21,7 @@ from sphinx import addnodes
|
||||
from sphinx.locale import _ as sphinx_gettext
|
||||
from sphinx.util.docutils import SphinxDirective
|
||||
|
||||
-if TYPE_CHECKING:
|
||||
- from sphinx.application import Sphinx
|
||||
- from sphinx.util.typing import ExtensionMetadata
|
||||
+from sphinx.application import Sphinx
|
||||
|
||||
ROLE_TO_OBJECT_TYPE = {
|
||||
"func": "function",
|
||||
@@ -36,20 +32,20 @@ ROLE_TO_OBJECT_TYPE = {
|
||||
}
|
||||
|
||||
|
||||
-@dataclasses.dataclass(slots=True)
|
||||
+@dataclasses.dataclass()
|
||||
class RefCountEntry:
|
||||
# Name of the function.
|
||||
name: str
|
||||
# List of (argument name, type, refcount effect) tuples.
|
||||
# (Currently not used. If it was, a dataclass might work better.)
|
||||
- args: list = dataclasses.field(default_factory=list)
|
||||
+ args: List = dataclasses.field(default_factory=list)
|
||||
# Return type of the function.
|
||||
result_type: str = ""
|
||||
# Reference count effect for the return value.
|
||||
- result_refs: int | None = None
|
||||
+ result_refs: Union[int, None] = None
|
||||
|
||||
|
||||
-@dataclasses.dataclass(frozen=True, slots=True)
|
||||
+@dataclasses.dataclass(frozen=True)
|
||||
class StableABIEntry:
|
||||
# Role of the object.
|
||||
# Source: Each [item_kind] in stable_abi.toml is mapped to a C Domain role.
|
||||
@@ -68,7 +64,7 @@ class StableABIEntry:
|
||||
struct_abi_kind: str
|
||||
|
||||
|
||||
-def read_refcount_data(refcount_filename: Path) -> dict[str, RefCountEntry]:
|
||||
+def read_refcount_data(refcount_filename: Path) -> Dict[str, RefCountEntry]:
|
||||
refcount_data = {}
|
||||
refcounts = refcount_filename.read_text(encoding="utf8")
|
||||
for line in refcounts.splitlines():
|
||||
@@ -104,7 +100,7 @@ def read_refcount_data(refcount_filename
|
||||
return refcount_data
|
||||
|
||||
|
||||
-def read_stable_abi_data(stable_abi_file: Path) -> dict[str, StableABIEntry]:
|
||||
+def read_stable_abi_data(stable_abi_file: Path) -> Dict[str, StableABIEntry]:
|
||||
stable_abi_data = {}
|
||||
with open(stable_abi_file, encoding="utf8") as fp:
|
||||
for record in csv.DictReader(fp):
|
||||
@@ -132,7 +128,8 @@ def add_annotations(app: Sphinx, doctree
|
||||
objtype = par["objtype"]
|
||||
|
||||
# Stable ABI annotation.
|
||||
- if record := stable_abi_data.get(name):
|
||||
+ record = stable_abi_data.get(name)
|
||||
+ if record:
|
||||
if ROLE_TO_OBJECT_TYPE[record.role] != objtype:
|
||||
msg = (
|
||||
f"Object type mismatch in limited API annotation for {name}: "
|
||||
@@ -239,7 +236,7 @@ def _unstable_api_annotation() -> nodes.
|
||||
)
|
||||
|
||||
|
||||
-def _return_value_annotation(result_refs: int | None) -> nodes.emphasis:
|
||||
+def _return_value_annotation(result_refs: Union[int, None]) -> nodes.emphasis:
|
||||
classes = ["refcount"]
|
||||
if result_refs is None:
|
||||
rc = sphinx_gettext("Return value: Always NULL.")
|
||||
@@ -259,7 +256,7 @@ class LimitedAPIList(SphinxDirective):
|
||||
optional_arguments = 0
|
||||
final_argument_whitespace = True
|
||||
|
||||
- def run(self) -> list[nodes.Node]:
|
||||
+ def run(self) -> List[nodes.Node]:
|
||||
state = self.env.domaindata["c_annotations"]
|
||||
content = [
|
||||
f"* :c:{record.role}:`{record.name}`"
|
||||
@@ -282,7 +279,7 @@ def init_annotations(app: Sphinx) -> Non
|
||||
)
|
||||
|
||||
|
||||
-def setup(app: Sphinx) -> ExtensionMetadata:
|
||||
+def setup(app: Sphinx) -> Any:
|
||||
app.add_config_value("refcount_file", "", "env", types={str})
|
||||
app.add_config_value("stable_abi_file", "", "env", types={str})
|
||||
app.add_directive("limited-api-list", LimitedAPIList)
|
||||
@@ -294,10 +291,10 @@ def setup(app: Sphinx) -> ExtensionMetad
|
||||
from sphinx.domains.c import CObject
|
||||
|
||||
# monkey-patch C object...
|
||||
- CObject.option_spec |= {
|
||||
+ CObject.option_spec.update({
|
||||
"no-index-entry": directives.flag,
|
||||
"no-contents-entry": directives.flag,
|
||||
- }
|
||||
+ })
|
||||
|
||||
return {
|
||||
"version": "1.0",
|
||||
--- a/Doc/tools/extensions/glossary_search.py
|
||||
+++ b/Doc/tools/extensions/glossary_search.py
|
||||
@@ -1,18 +1,14 @@
|
||||
"""Feature search results for glossary items prominently."""
|
||||
|
||||
-from __future__ import annotations
|
||||
-
|
||||
import json
|
||||
from pathlib import Path
|
||||
-from typing import TYPE_CHECKING
|
||||
+from typing import Any, TYPE_CHECKING
|
||||
|
||||
from docutils import nodes
|
||||
from sphinx.addnodes import glossary
|
||||
from sphinx.util import logging
|
||||
|
||||
-if TYPE_CHECKING:
|
||||
- from sphinx.application import Sphinx
|
||||
- from sphinx.util.typing import ExtensionMetadata
|
||||
+from sphinx.application import Sphinx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -60,7 +56,7 @@ def write_glossary_json(app: Sphinx, _ex
|
||||
dest.write_text(json.dumps(app.env.glossary_terms), encoding='utf-8')
|
||||
|
||||
|
||||
-def setup(app: Sphinx) -> ExtensionMetadata:
|
||||
+def setup(app: Sphinx) -> Any:
|
||||
app.connect('doctree-resolved', process_glossary_nodes)
|
||||
app.connect('build-finished', write_glossary_json)
|
||||
|
||||
--- a/Doc/tools/extensions/patchlevel.py
|
||||
+++ b/Doc/tools/extensions/patchlevel.py
|
||||
@@ -3,7 +3,7 @@
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
-from typing import Literal, NamedTuple
|
||||
+from typing import NamedTuple, Tuple
|
||||
|
||||
CPYTHON_ROOT = Path(
|
||||
__file__, # cpython/Doc/tools/extensions/patchlevel.py
|
||||
@@ -26,7 +26,7 @@ class version_info(NamedTuple): # noqa:
|
||||
major: int #: Major release number
|
||||
minor: int #: Minor release number
|
||||
micro: int #: Patch release number
|
||||
- releaselevel: Literal["alpha", "beta", "candidate", "final"]
|
||||
+ releaselevel: str
|
||||
serial: int #: Serial release number
|
||||
|
||||
|
||||
@@ -37,7 +37,8 @@ def get_header_version_info() -> version
|
||||
defines = {}
|
||||
patchlevel_h = PATCHLEVEL_H.read_text(encoding="utf-8")
|
||||
for line in patchlevel_h.splitlines():
|
||||
- if (m := pat.match(line)) is not None:
|
||||
+ m = pat.match(line)
|
||||
+ if m is not None:
|
||||
name, value = m.groups()
|
||||
defines[name] = value
|
||||
|
||||
@@ -50,7 +51,7 @@ def get_header_version_info() -> version
|
||||
)
|
||||
|
||||
|
||||
-def format_version_info(info: version_info) -> tuple[str, str]:
|
||||
+def format_version_info(info: version_info) -> Tuple[str, str]:
|
||||
version = f"{info.major}.{info.minor}"
|
||||
release = f"{info.major}.{info.minor}.{info.micro}"
|
||||
if info.releaselevel != "final":
|
@ -1,7 +1,9 @@
|
||||
Index: Python-3.12.3/Lib/test/test_compile.py
|
||||
===================================================================
|
||||
--- Python-3.12.3.orig/Lib/test/test_compile.py
|
||||
+++ Python-3.12.3/Lib/test/test_compile.py
|
||||
---
|
||||
Lib/test/test_compile.py | 5 +++++
|
||||
1 file changed, 5 insertions(+)
|
||||
|
||||
--- a/Lib/test/test_compile.py
|
||||
+++ b/Lib/test/test_compile.py
|
||||
@@ -14,6 +14,9 @@ from test.support import (script_helper,
|
||||
requires_specialization, C_RECURSION_LIMIT)
|
||||
from test.support.os_helper import FakePath
|
||||
|
@ -21,7 +21,7 @@
|
||||
Create a Python.framework rather than a traditional Unix install. Optional
|
||||
--- a/Misc/NEWS
|
||||
+++ b/Misc/NEWS
|
||||
@@ -13832,7 +13832,7 @@ C API
|
||||
@@ -14138,7 +14138,7 @@ C API
|
||||
- bpo-40939: Removed documentation for the removed ``PyParser_*`` C API.
|
||||
|
||||
- bpo-43795: The list in :ref:`limited-api-list` now shows the public name
|
||||
|
@ -2,11 +2,9 @@
|
||||
Lib/test/test_posix.py | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
Index: Python-3.12.2/Lib/test/test_posix.py
|
||||
===================================================================
|
||||
--- Python-3.12.2.orig/Lib/test/test_posix.py
|
||||
+++ Python-3.12.2/Lib/test/test_posix.py
|
||||
@@ -433,7 +433,7 @@ class PosixTester(unittest.TestCase):
|
||||
--- a/Lib/test/test_posix.py
|
||||
+++ b/Lib/test/test_posix.py
|
||||
@@ -435,7 +435,7 @@ class PosixTester(unittest.TestCase):
|
||||
def test_posix_fadvise(self):
|
||||
fd = os.open(os_helper.TESTFN, os.O_RDONLY)
|
||||
try:
|
||||
|
@ -1,3 +1,240 @@
|
||||
-------------------------------------------------------------------
|
||||
Thu Oct 24 16:09:00 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
|
||||
|
||||
- Add CVE-2024-9287-venv_path_unquoted.patch to properly quote
|
||||
path names provided when creating a virtual environment
|
||||
(bsc#1232241, CVE-2024-9287)
|
||||
|
||||
-------------------------------------------------------------------
|
||||
Tue Oct 1 15:32:06 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
|
||||
|
||||
- Update to 3.12.7:
|
||||
- Tests
|
||||
- gh-124378: Updated test_ttk to pass with Tcl/Tk 8.6.15.
|
||||
- Security
|
||||
- gh-122792: Changed IPv4-mapped ipaddress.IPv6Address to
|
||||
consistently use the mapped IPv4 address value for deciding
|
||||
properties. Properties which have their behavior fixed are
|
||||
is_multicast, is_reserved, is_link_local, is_global, and
|
||||
is_unspecified.
|
||||
- Library
|
||||
- gh-116850: Fix argparse for namespaces with not directly
|
||||
writable dict (e.g. classes).
|
||||
- gh-58573: Fix conflicts between abbreviated long options in
|
||||
the parent parser and subparsers in argparse.
|
||||
- gh-61181: Fix support of choices with string value in
|
||||
argparse. Substrings of the specified string no longer
|
||||
considered valid values.
|
||||
- gh-80259: Fix argparse support of positional arguments with
|
||||
nargs='?', default=argparse.SUPPRESS and specified type.
|
||||
- gh-124498: Fix typing.TypeAliasType not to be generic, when
|
||||
type_params is an empty tuple.
|
||||
- gh-124345: argparse vim supports abbreviated single-dash
|
||||
long options separated by = from its value.
|
||||
- gh-104860: Fix disallowing abbreviation of single-dash long
|
||||
options in argparse with allow_abbrev=False.
|
||||
- gh-63143: Fix parsing mutually exclusive arguments in
|
||||
argparse. Arguments with the value identical to the default
|
||||
value (e.g. booleans, small integers, empty or 1-character
|
||||
strings) are no longer considered “not present”.
|
||||
- gh-72795: Positional arguments with nargs equal to '*' or
|
||||
argparse.REMAINDER are no longer required. This allows to
|
||||
use positional argument with nargs='*' and without default
|
||||
in mutually exclusive group and improves error message
|
||||
about required arguments.
|
||||
- gh-59317: Fix parsing positional argument with nargs equal
|
||||
to '?' or '*' if it is preceded by an option and another
|
||||
positional argument.
|
||||
- gh-53780: argparse now ignores the first "--" (double dash)
|
||||
between an option and command.
|
||||
- gh-124217: Add RFC 9637 reserved IPv6 block 3fff::/20 in
|
||||
ipaddress module.
|
||||
- gh-124248: Fixed potential crash when using struct to
|
||||
process zero-width ‘Pascal string’ fields (0p).
|
||||
- gh-81691: Fix handling of multiple "--" (double dashes)
|
||||
in argparse. Only the first one has now been removed, all
|
||||
subsequent ones are now taken literally.
|
||||
- gh-87041: Fix a bug in argparse where lengthy subparser
|
||||
argument help is incorrectly indented.
|
||||
- gh-124171: Add workaround for broken fmod() implementations
|
||||
on Windows, that loose zero sign (e.g. fmod(-10, 1) returns
|
||||
0.0). Patch by Sergey B Kirpichev.
|
||||
- gh-123934: Fix unittest.mock.MagicMock reseting magic
|
||||
methods return values after .reset_mock(return_value=True)
|
||||
was called.
|
||||
- gh-123935: Fix parent slots detection for dataclasses that
|
||||
inherit from classes with __dictoffset__.
|
||||
- gh-123892: Add "_wmi" to sys.stdlib_module_names. Patch by
|
||||
Victor Stinner.
|
||||
- gh-116810: Resolve a memory leak introduced in CPython
|
||||
3.10’s ssl when the ssl.SSLSocket.session property was
|
||||
accessed. Speeds up read and write access to said property
|
||||
by no longer unnecessarily cloning session objects via
|
||||
serialization.
|
||||
- gh-121735: When working with zip archives,
|
||||
importlib.resources now properly honors module-adjacent
|
||||
references (e.g. files(pkg.mod) and not just files(pkg)).
|
||||
- gh-119004: Fix a crash in OrderedDict.__eq__ when operands
|
||||
are mutated during the check. Patch by Bénédikt Tran.
|
||||
- bpo-44864: Do not translate user-provided strings in
|
||||
argparse.ArgumentParser.
|
||||
- IDLE
|
||||
- gh-112938: Fix uninteruptable hang when Shell gets rapid
|
||||
continuous output.
|
||||
- Core and Builtins
|
||||
- gh-116510: Fix a bug that can cause a crash when
|
||||
sub-interpreters use “basic” single-phase extension
|
||||
modules. Shared objects could refer to PyGC_Head nodes that
|
||||
had been freed as part of interpreter cleanup.
|
||||
- gh-124188: Fix reading and decoding a line from the source
|
||||
file witn non-UTF-8 encoding for syntax errors raised in
|
||||
the compiler.
|
||||
- gh-77894: Fix possible crash in the garbage collector when
|
||||
it tries to break a reference loop containing a memoryview
|
||||
object. Now a memoryview object can only be cleared if
|
||||
there are no buffers that refer it.
|
||||
- gh-98442: Fix too wide source locations of the cleanup
|
||||
instructions of a with statement.
|
||||
- gh-113993: Strings interned with sys.intern() are again
|
||||
garbage-collected when no longer used, as per the
|
||||
documentation. Strings interned with the C function
|
||||
PyUnicode_InternInPlace() are still immortal. Internals of
|
||||
the string interning mechanism have been changed. This may
|
||||
affect performance and identities of str objects.
|
||||
- C API
|
||||
- gh-113993: PyUnicode_InternInPlace() no longer prevents its
|
||||
argument from being garbage collected.
|
||||
- Several functions that take char * are now documented
|
||||
as possibly preventing string objects from being
|
||||
garbage collected; refer to their documentation
|
||||
for details: PyUnicode_InternFromString(),
|
||||
PyDict_SetItemString(), PyObject_SetAttrString(),
|
||||
PyObject_DelAttrString(), PyUnicode_InternFromString(), and
|
||||
PyModule_Add* convenience functions.
|
||||
- Build
|
||||
- gh-124487: Windows builds now use Windows 8.1 as their API
|
||||
baseline (installation already required Windows 8.1).
|
||||
- gh-123917: Fix the check for the crypt() function in the
|
||||
configure script. Patch by Paul Smith and Victor Stinner.
|
||||
- Change previous removal of *.pyc files with rm -f instead of
|
||||
||/bin/true (bsc#1230906).
|
||||
|
||||
-------------------------------------------------------------------
|
||||
Mon Sep 30 09:42:13 UTC 2024 - Bernhard Wiedemann <bwiedemann@suse.com>
|
||||
|
||||
- Drop .pyc files from docdir for reproducible builds
|
||||
|
||||
-------------------------------------------------------------------
|
||||
Fri Sep 13 17:09:37 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
|
||||
|
||||
- Add doc-py38-to-py36.patch making building documentation
|
||||
compatible with Python 3.6, which runs Sphinx on SLE.
|
||||
|
||||
-------------------------------------------------------------------
|
||||
Sat Sep 7 21:49:34 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
|
||||
|
||||
- Update to 3.12.6:
|
||||
- Tests
|
||||
- gh-101525: Skip test_gdb if the binary is relocated by
|
||||
BOLT. Patch by Donghee Na.
|
||||
- Security
|
||||
- gh-123678: Upgrade libexpat to 2.6.3
|
||||
- gh-121285: Remove backtracking from tarfile header parsing
|
||||
for hdrcharset, PAX, and GNU sparse headers (bsc#1230227,
|
||||
CVE-2024-6232).
|
||||
- Library
|
||||
- gh-123270: Applied a more surgical fix for malformed
|
||||
payloads in zipfile.Path causing infinite loops (gh-122905)
|
||||
without breaking contents using legitimate characters
|
||||
(bsc#1229704, CVE-2024-8088).
|
||||
- gh-123213: xml.etree.ElementTree.Element.extend() and
|
||||
Element assignment no longer hide the internal exception if
|
||||
an erronous generator is passed. Patch by Bar Harel.
|
||||
- gh-85110: Preserve relative path in URL without netloc in
|
||||
urllib.parse.urlunsplit() and urllib.parse.urlunparse().
|
||||
- gh-123067: Fix quadratic complexity in parsing "-quoted
|
||||
cookie values with backslashes by http.cookies
|
||||
(bsc#1229596, CVE-2024-7592)
|
||||
- gh-122903: zipfile.Path.glob now correctly matches
|
||||
directories instead of silently omitting them.
|
||||
- gh-122905: zipfile.Path objects now sanitize names from the
|
||||
zipfile.
|
||||
- gh-122695: Fixed double-free when using gc.get_referents()
|
||||
with a freed asyncio.Future iterator.
|
||||
- gh-116263: logging.handlers.RotatingFileHandler no longer
|
||||
rolls over empty log files.
|
||||
- gh-118814: Fix the typing.TypeVar constructor when name is
|
||||
passed by keyword.
|
||||
- gh-122478: Remove internal frames from tracebacks
|
||||
shown in code.InteractiveInterpreter with non-default
|
||||
sys.excepthook(). Save correct tracebacks in
|
||||
sys.last_traceback and update __traceback__ attribute of
|
||||
sys.last_value and sys.last_exc.
|
||||
- gh-113785: csv now correctly parses numeric fields (when
|
||||
used with csv.QUOTE_NONNUMERIC) which start with an escape
|
||||
character.
|
||||
- gh-112182: asyncio.futures.Future.set_exception() now
|
||||
transforms StopIteration into RuntimeError instead of
|
||||
hanging or other misbehavior. Patch contributed by Jamie
|
||||
Phan.
|
||||
- gh-108172: webbrowser honors OS preferred browser on Linux
|
||||
when its desktop entry name contains the text of a known
|
||||
browser name.
|
||||
- gh-102988: email.utils.getaddresses() and
|
||||
email.utils.parseaddr() now return ('', '') 2-tuples
|
||||
in more situations where invalid email addresses are
|
||||
encountered instead of potentially inaccurate values. Add
|
||||
optional strict parameter to these two functions: use
|
||||
strict=False to get the old behavior, accept malformed
|
||||
inputs. getattr(email.utils, 'supports_strict_parsing',
|
||||
False) can be use to check if the strict paramater is
|
||||
available. Patch by Thomas Dwyer and Victor Stinner to
|
||||
improve the CVE-2023-27043 fix.
|
||||
- gh-99437: runpy.run_path() now decodes path-like objects,
|
||||
making sure __file__ and sys.argv[0] of the module being
|
||||
run are always strings.
|
||||
- IDLE
|
||||
- gh-120083: Add explicit black IDLE Hovertip foreground
|
||||
color needed for recent macOS. Fixes Sonoma showing
|
||||
unreadable white on pale yellow. Patch by John Riggles.
|
||||
- Core and Builtins
|
||||
- gh-123321: Prevent Parser/myreadline race condition from
|
||||
segfaulting on multi-threaded use. Patch by Bar Harel and
|
||||
Amit Wienner.
|
||||
- gh-122982: Extend the deprecation period for bool inversion
|
||||
(~) by two years.
|
||||
- gh-123229: Fix valgrind warning by initializing the
|
||||
f-string buffers to 0 in the tokenizer. Patch by Pablo
|
||||
Galindo
|
||||
- gh-123142: Fix too-wide source location in exception
|
||||
tracebacks coming from broken iterables in comprehensions.
|
||||
- gh-123048: Fix a bug where pattern matching code could emit
|
||||
a JUMP_FORWARD with no source location.
|
||||
- gh-123083: Fix a potential use-after-free in
|
||||
STORE_ATTR_WITH_HINT.
|
||||
- gh-122527: Fix a crash that occurred when a
|
||||
PyStructSequence was deallocated after its type’s
|
||||
dictionary was cleared by the GC. The type’s tp_basicsize
|
||||
now accounts for non-sequence fields that aren’t included
|
||||
in the Py_SIZE of the sequence.
|
||||
- gh-93691: Fix source locations of instructions generated
|
||||
for with statements.
|
||||
- Build
|
||||
- gh-123297: Propagate the value of LDFLAGS to LDCXXSHARED in
|
||||
sysconfig. Patch by Pablo Galindo
|
||||
- Remove upstreamed patches:
|
||||
- CVE-2023-27043-email-parsing-errors.patch
|
||||
- CVE-2024-8088-inf-loop-zipfile_Path.patch
|
||||
- CVE-2023-6597-TempDir-cleaning-symlink.patch
|
||||
- gh120226-fix-sendfile-test-kernel-610.patch
|
||||
|
||||
-------------------------------------------------------------------
|
||||
Mon Sep 2 09:44:26 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
|
||||
|
||||
- Add gh120226-fix-sendfile-test-kernel-610.patch to avoid
|
||||
failing test_sendfile_close_peer_in_the_middle_of_receiving
|
||||
tests on Linux >= 6.10 (GH-120227).
|
||||
|
||||
-------------------------------------------------------------------
|
||||
Wed Aug 28 16:54:34 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
|
||||
|
||||
|
@ -110,7 +110,7 @@
|
||||
# _md5.cpython-38m-x86_64-linux-gnu.so
|
||||
%define dynlib() %{sitedir}/lib-dynload/%{1}.cpython-%{abi_tag}-%{archname}-%{_os}%{?_gnu}%{?armsuffix}.so
|
||||
Name: %{python_pkg_name}%{psuffix}
|
||||
Version: 3.12.5
|
||||
Version: 3.12.7
|
||||
Release: 0
|
||||
Summary: Python 3 Interpreter
|
||||
License: Python-2.0
|
||||
@ -168,13 +168,6 @@ Patch34: skip-test_pyobject_freed_is_freed.patch
|
||||
# PATCH-FIX-SLE fix_configure_rst.patch bpo#43774 mcepl@suse.com
|
||||
# remove duplicate link targets and make documentation with old Sphinx in SLE
|
||||
Patch35: fix_configure_rst.patch
|
||||
# PATCH-FIX-UPSTREAM CVE-2023-27043-email-parsing-errors.patch bsc#1210638 mcepl@suse.com
|
||||
# Detect email address parsing errors and return empty tuple to
|
||||
# indicate the parsing error (old API)
|
||||
Patch36: CVE-2023-27043-email-parsing-errors.patch
|
||||
# PATCH-FIX-UPSTREAM CVE-2023-6597-TempDir-cleaning-symlink.patch bsc#1219666 mcepl@suse.com
|
||||
# tempfile.TemporaryDirectory: fix symlink bug in cleanup (from gh#python/cpython!99930)
|
||||
Patch38: CVE-2023-6597-TempDir-cleaning-symlink.patch
|
||||
# PATCH-FIX-OPENSUSE CVE-2023-52425-libexpat-2.6.0-backport-15.6.patch
|
||||
# This problem on libexpat is patched on 15.6 without version
|
||||
# update, this patch changes the tests to match the libexpat provided
|
||||
@ -186,9 +179,12 @@ Patch40: fix-test-recursion-limit-15.6.patch
|
||||
# PATCH-FIX-SLE docs-docutils_014-Sphinx_420.patch bsc#[0-9]+ mcepl@suse.com
|
||||
# related to gh#python/cpython#119317
|
||||
Patch41: docs-docutils_014-Sphinx_420.patch
|
||||
# PATCH-FIX-UPSTREAM CVE-2024-8088-inf-loop-zipfile_Path.patch bsc#1229704 mcepl@suse.com
|
||||
# avoid denial of service in zipfile
|
||||
Patch42: CVE-2024-8088-inf-loop-zipfile_Path.patch
|
||||
# PATCH-FIX-SLE doc-py38-to-py36.patch mcepl@suse.com
|
||||
# Make documentation extensions working with Python 3.6
|
||||
Patch44: doc-py38-to-py36.patch
|
||||
# PATCH-FIX-UPSTREAM CVE-2024-9287-venv_path_unquoted.patch gh#python/cpython#124651 mcepl@suse.com
|
||||
# venv should properly quote path names provided when creating a venv
|
||||
Patch45: CVE-2024-9287-venv_path_unquoted.patch
|
||||
BuildRequires: autoconf-archive
|
||||
BuildRequires: automake
|
||||
BuildRequires: fdupes
|
||||
@ -219,6 +215,9 @@ BuildRequires: mpdecimal-devel
|
||||
BuildRequires: python3-Sphinx >= 4.0.0
|
||||
%if 0%{?suse_version} >= 1500
|
||||
BuildRequires: python3-python-docs-theme >= 2022.1
|
||||
%if 0%{?suse_version} < 1599
|
||||
BuildRequires: python3-dataclasses
|
||||
%endif
|
||||
%endif
|
||||
%endif
|
||||
%if %{with general}
|
||||
@ -480,7 +479,7 @@ rm Lib/site-packages/README.txt
|
||||
tar xvf %{SOURCE21}
|
||||
|
||||
# Don't fail on warnings when building documentation
|
||||
# sed -i -e '/^SPHINXERRORHANDLING/s/-W//' Doc/Makefile
|
||||
sed -i -e '/^SPHINXERRORHANDLING/s/-W//' Doc/Makefile
|
||||
|
||||
%build
|
||||
%if %{with doc}
|
||||
@ -801,6 +800,11 @@ LD_LIBRARY_PATH=. ./python -O -c "from py_compile import compile; compile('$FAIL
|
||||
echo %{sitedir}/_import_failed > %{buildroot}/%{sitedir}/site-packages/zzzz-import-failed-hooks.pth
|
||||
%endif
|
||||
|
||||
# For the purposes of reproducibility, it is necessary to eliminate any *.pyc files inside documentation dirs
|
||||
if [ -d %{buildroot}%{_defaultdocdir} ] ; then
|
||||
find %{buildroot}%{_defaultdocdir} -type f -name \*.pyc -ls -exec rm -vf '{}' \;
|
||||
fi
|
||||
|
||||
%if %{with general}
|
||||
%files -n %{python_pkg_name}-tk
|
||||
%{sitedir}/tkinter
|
||||
|
Loading…
Reference in New Issue
Block a user