- Update to 3.9.20:

- Tests
    - gh-112769: The tests now correctly compare zlib version when
      :const:`zlib.ZLIB_RUNTIME_VERSION` contains non-integer suffixes. For
      example zlib-ng defines the version as ``1.3.0.zlib-ng``.
    - gh-117187: Fix XML tests for vanilla Expat <2.6.0.
  - Security
    - gh-123678: Upgrade libexpat to 2.6.3
    - gh-121957: Fixed missing audit events around interactive use of Python,
      now also properly firing for ``python -i``, as well as for ``python -m
      asyncio``. The event in question is ``cpython.run_stdin``.
    - gh-122133: Authenticate the socket connection for the
      ``socket.socketpair()`` fallback on platforms where ``AF_UNIX`` is not
      available like Windows.
      Patch by Gregory P. Smith <greg@krypto.org> and Seth Larson
      <seth@python.org>. Reported by Ellie <el@horse64.org>
    - gh-121285: Remove backtracking from tarfile header parsing for
      ``hdrcharset``, PAX, and GNU sparse headers
      (bsc#1230227, CVE-2024-6232).
    - gh-118486: :func:`os.mkdir` on Windows now accepts *mode* of ``0o700`` to
      restrict the new directory to the current user. This fixes CVE-2024-4030
      affecting :func:`tempfile.mkdtemp` in scenarios where the base temporary
      directory is more permissive than the default.
    - gh-114572: :meth:`ssl.SSLContext.cert_store_stats` and
      :meth:`ssl.SSLContext.get_ca_certs` now correctly lock access to the
      certificate store, when the :class:`ssl.SSLContext` is shared across
      multiple threads (bsc#1226447, CVE-2024-0397).
    - gh-116741: Update bundled libexpat to 2.6.2
  - Library
    - gh-123270: Applied a more surgical fix for malformed payloads in

OBS-URL: https://build.opensuse.org/package/show/devel:languages:python:Factory/python39?expand=0&rev=201
This commit is contained in:
Matej Cepl 2024-09-09 20:10:25 +00:00 committed by Git OBS Bridge
commit ad933f5c9f
49 changed files with 11490 additions and 0 deletions

23
.gitattributes vendored Normal file
View File

@ -0,0 +1,23 @@
## Default LFS
*.7z filter=lfs diff=lfs merge=lfs -text
*.bsp filter=lfs diff=lfs merge=lfs -text
*.bz2 filter=lfs diff=lfs merge=lfs -text
*.gem filter=lfs diff=lfs merge=lfs -text
*.gz filter=lfs diff=lfs merge=lfs -text
*.jar filter=lfs diff=lfs merge=lfs -text
*.lz filter=lfs diff=lfs merge=lfs -text
*.lzma filter=lfs diff=lfs merge=lfs -text
*.obscpio filter=lfs diff=lfs merge=lfs -text
*.oxt filter=lfs diff=lfs merge=lfs -text
*.pdf filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.rpm filter=lfs diff=lfs merge=lfs -text
*.tbz filter=lfs diff=lfs merge=lfs -text
*.tbz2 filter=lfs diff=lfs merge=lfs -text
*.tgz filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
*.txz filter=lfs diff=lfs merge=lfs -text
*.whl filter=lfs diff=lfs merge=lfs -text
*.xz filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.zst filter=lfs diff=lfs merge=lfs -text

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.osc

View File

@ -0,0 +1,54 @@
From 5775f51691d7d64fb676586e008b41261ce64ac2 Mon Sep 17 00:00:00 2001
From: "Matt.Wang" <mattwang44@gmail.com>
Date: Wed, 19 Oct 2022 14:49:08 +0800
Subject: [PATCH 1/2] fix(doc-tools): use sphinx.locale._ as gettext() for
backward-compatibility in pyspecific.py
[why] spinix 5.3 changed locale.translators from a defaultdict(gettext.NullTranslations) to a dict, which leads to failure of pyspecific.py. Use sphinx.locale._ as gettext to fix the issue.
---
Doc/tools/extensions/pyspecific.py | 8 ++++----
Misc/NEWS.d/next/Documentation/2022-10-19-07-15-52.gh-issue-98366.UskMXF.rst | 1 +
2 files changed, 5 insertions(+), 4 deletions(-)
--- a/Doc/tools/extensions/pyspecific.py
+++ b/Doc/tools/extensions/pyspecific.py
@@ -26,7 +26,7 @@ try:
from sphinx.errors import NoUri
except ImportError:
from sphinx.environment import NoUri
-from sphinx.locale import translators
+from sphinx.locale import _ as sphinx_gettext
from sphinx.util import status_iterator, logging
from sphinx.util.nodes import split_explicit_title
from sphinx.writers.text import TextWriter, TextTranslator
@@ -110,7 +110,7 @@ class ImplementationDetail(Directive):
def run(self):
pnode = nodes.compound(classes=['impl-detail'])
- label = translators['sphinx'].gettext(self.label_text)
+ label = sphinx_gettext(self.label_text)
content = self.content
add_text = nodes.strong(label, label)
if self.arguments:
@@ -179,7 +179,7 @@ class AuditEvent(Directive):
else:
args = []
- label = translators['sphinx'].gettext(self._label[min(2, len(args))])
+ label = sphinx_gettext(self._label[min(2, len(args))])
text = label.format(name="``{}``".format(name),
args=", ".join("``{}``".format(a) for a in args if a))
@@ -358,7 +358,7 @@ class DeprecatedRemoved(Directive):
else:
label = self._removed_label
- label = translators['sphinx'].gettext(label)
+ label = sphinx_gettext(label)
text = label.format(deprecated=self.arguments[0], removed=self.arguments[1])
if len(self.arguments) == 3:
inodes, messages = self.state.inline_text(self.arguments[2],
--- /dev/null
+++ b/Misc/NEWS.d/next/Documentation/2022-10-19-07-15-52.gh-issue-98366.UskMXF.rst
@@ -0,0 +1 @@
+Use sphinx.locale._ as the gettext function in pyspecific.py.

View File

@ -0,0 +1,79 @@
From c0dea0309b9a0a7cbc87727c9957f0a388fb9b0f Mon Sep 17 00:00:00 2001
From: Nikita Sobolev <mail@sobolevn.me>
Date: Fri, 11 Nov 2022 11:04:30 +0300
Subject: [PATCH] gh-98086: Now ``patch.dict`` can decorate async functions
(GH-98095) (cherry picked from commit
67b4d2772c5124b908f8ed9b13166a79bbeb88d2)
Co-authored-by: Nikita Sobolev <mail@sobolevn.me>
---
Lib/unittest/mock.py | 18 ++++++++++
Lib/unittest/test/testmock/testasync.py | 17 +++++++++
Misc/NEWS.d/next/Library/2022-10-08-19-39-27.gh-issue-98086.y---WC.rst | 1
3 files changed, 36 insertions(+)
create mode 100644 Misc/NEWS.d/next/Library/2022-10-08-19-39-27.gh-issue-98086.y---WC.rst
--- a/Lib/unittest/mock.py
+++ b/Lib/unittest/mock.py
@@ -1761,6 +1761,12 @@ class _patch_dict(object):
def __call__(self, f):
if isinstance(f, type):
return self.decorate_class(f)
+ if inspect.iscoroutinefunction(f):
+ return self.decorate_async_callable(f)
+ return self.decorate_callable(f)
+
+
+ def decorate_callable(self, f):
@wraps(f)
def _inner(*args, **kw):
self._patch_dict()
@@ -1769,6 +1775,18 @@ class _patch_dict(object):
finally:
self._unpatch_dict()
+ return _inner
+
+
+ def decorate_async_callable(self, f):
+ @wraps(f)
+ async def _inner(*args, **kw):
+ self._patch_dict()
+ try:
+ return await f(*args, **kw)
+ finally:
+ self._unpatch_dict()
+
return _inner
--- a/Lib/unittest/test/testmock/testasync.py
+++ b/Lib/unittest/test/testmock/testasync.py
@@ -146,6 +146,23 @@ class AsyncPatchCMTest(unittest.TestCase
run(test_async())
+ def test_patch_dict_async_def(self):
+ foo = {'a': 'a'}
+ @patch.dict(foo, {'a': 'b'})
+ async def test_async():
+ self.assertEqual(foo['a'], 'b')
+
+ self.assertTrue(iscoroutinefunction(test_async))
+ run(test_async())
+
+ def test_patch_dict_async_def_context(self):
+ foo = {'a': 'a'}
+ async def test_async():
+ with patch.dict(foo, {'a': 'b'}):
+ self.assertEqual(foo['a'], 'b')
+
+ run(test_async())
+
class AsyncMockTest(unittest.TestCase):
def test_iscoroutinefunction_default(self):
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-10-08-19-39-27.gh-issue-98086.y---WC.rst
@@ -0,0 +1 @@
+Make sure ``patch.dict()`` can be applied on async functions.

View File

@ -0,0 +1,461 @@
---
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(-)
--- a/Doc/library/email.utils.rst
+++ b/Doc/library/email.utils.rst
@@ -60,13 +60,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')
@@ -84,12 +89,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
@@ -99,6 +107,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 contains 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):
@@ -202,16 +318,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.header import Header, decode_header, make_header
@@ -3263,15 +3264,137 @@ Foo
[('Al Person', 'aperson@dom.ain'),
('Bud Person', 'bperson@dom.ain')])
+ 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"""
@@ -3460,6 +3583,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.

View File

@ -0,0 +1,57 @@
---
Lib/test/test_pyexpat.py | 4 ++++
Lib/test/test_sax.py | 3 +++
Lib/test/test_xml_etree.py | 7 +++++++
3 files changed, 14 insertions(+)
--- a/Lib/test/test_pyexpat.py
+++ b/Lib/test/test_pyexpat.py
@@ -766,6 +766,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
@@ -1236,6 +1236,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
@@ -1416,9 +1416,13 @@ class XMLPullParserTest(unittest.TestCas
self.assert_event_tags(parser, [('end', 'root')])
self.assertIsNone(parser.close())
+ @unittest.skipIf(pyexpat.version_info < (2, 6, 0),
+ f'Fail with patched version of Expat {pyexpat.version_info}')
def test_simple_xml_chunk_1(self):
self.test_simple_xml(chunk_size=1, flush=True)
+ @unittest.skipIf(pyexpat.version_info < (2, 6, 0),
+ f'Fail with patched version of Expat {pyexpat.version_info}')
def test_simple_xml_chunk_5(self):
self.test_simple_xml(chunk_size=5, flush=True)
@@ -1643,6 +1647,9 @@ class XMLPullParserTest(unittest.TestCas
self.assert_event_tags(parser, [('end', '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):
parser = ET.XMLPullParser(events=('start', 'end'))

View File

@ -0,0 +1,173 @@
From 732c7d512e7cdf656a3f02a38c329b14a14a8573 Mon Sep 17 00:00:00 2001
From: Seth Michael Larson <seth@python.org>
Date: Fri, 19 Apr 2024 11:21:40 -0700
Subject: [PATCH] [3.9] gh-114572: Fix locking in cert_store_stats and
get_ca_certs
---
Misc/NEWS.d/next/Security/2024-04-19-11-21-13.gh-issue-114572.t1QMQD.rst | 4
Modules/_ssl.c | 91 +++++++++-
2 files changed, 92 insertions(+), 3 deletions(-)
create mode 100644 Misc/NEWS.d/next/Security/2024-04-19-11-21-13.gh-issue-114572.t1QMQD.rst
Index: Python-3.9.19/Misc/NEWS.d/next/Security/2024-04-19-11-21-13.gh-issue-114572.t1QMQD.rst
===================================================================
--- /dev/null
+++ Python-3.9.19/Misc/NEWS.d/next/Security/2024-04-19-11-21-13.gh-issue-114572.t1QMQD.rst
@@ -0,0 +1,4 @@
+:meth:`ssl.SSLContext.cert_store_stats` and
+:meth:`ssl.SSLContext.get_ca_certs` now correctly lock access to the
+certificate store, when the :class:`ssl.SSLContext` is shared across
+multiple threads.
Index: Python-3.9.19/Modules/_ssl.c
===================================================================
--- Python-3.9.19.orig/Modules/_ssl.c
+++ Python-3.9.19/Modules/_ssl.c
@@ -166,6 +166,10 @@ extern const SSL_METHOD *TLSv1_2_method(
# define PY_OPENSSL_1_1_API 1
#endif
+#if (OPENSSL_VERSION_NUMBER >= 0x30300000L) && !defined(LIBRESSL_VERSION_NUMBER)
+# define OPENSSL_VERSION_3_3 1
+#endif
+
/* SNI support (client- and server-side) appeared in OpenSSL 1.0.0 and 0.9.8f
* This includes the SSL_set_SSL_CTX() function.
*/
@@ -210,6 +214,16 @@ extern const SSL_METHOD *TLSv1_2_method(
#define HAVE_OPENSSL_CRYPTO_LOCK
#endif
+/* OpenSSL 1.1+ allows locking X509_STORE, 1.0.2 doesn't. */
+#ifdef OPENSSL_VERSION_1_1
+#define HAVE_OPENSSL_X509_STORE_LOCK
+#endif
+
+/* OpenSSL 3.3 added the X509_STORE_get1_objects API */
+#ifdef OPENSSL_VERSION_3_3
+#define HAVE_OPENSSL_X509_STORE_GET1_OBJECTS 1
+#endif
+
#if defined(OPENSSL_VERSION_1_1) && !defined(OPENSSL_NO_SSL2)
#define OPENSSL_NO_SSL2
#endif
@@ -4675,6 +4689,54 @@ set_sni_callback(PySSLContext *self, PyO
#endif
}
+/* Shim of X509_STORE_get1_objects API from OpenSSL 3.3
+ * Only available with the X509_STORE_lock() API */
+#if defined(HAVE_OPENSSL_X509_STORE_LOCK) && !defined(OPENSSL_VERSION_3_3)
+#define HAVE_OPENSSL_X509_STORE_GET1_OBJECTS 1
+
+static X509_OBJECT *x509_object_dup(const X509_OBJECT *obj)
+{
+ int ok;
+ X509_OBJECT *ret = X509_OBJECT_new();
+ if (ret == NULL) {
+ return NULL;
+ }
+ switch (X509_OBJECT_get_type(obj)) {
+ case X509_LU_X509:
+ ok = X509_OBJECT_set1_X509(ret, X509_OBJECT_get0_X509(obj));
+ break;
+ case X509_LU_CRL:
+ /* X509_OBJECT_get0_X509_CRL was not const-correct prior to 3.0.*/
+ ok = X509_OBJECT_set1_X509_CRL(
+ ret, X509_OBJECT_get0_X509_CRL((X509_OBJECT *)obj));
+ break;
+ default:
+ /* We cannot duplicate unrecognized types in a polyfill, but it is
+ * safe to leave an empty object. The caller will ignore it. */
+ ok = 1;
+ break;
+ }
+ if (!ok) {
+ X509_OBJECT_free(ret);
+ return NULL;
+ }
+ return ret;
+}
+
+static STACK_OF(X509_OBJECT) *
+X509_STORE_get1_objects(X509_STORE *store)
+{
+ STACK_OF(X509_OBJECT) *ret;
+ if (!X509_STORE_lock(store)) {
+ return NULL;
+ }
+ ret = sk_X509_OBJECT_deep_copy(X509_STORE_get0_objects(store),
+ x509_object_dup, X509_OBJECT_free);
+ X509_STORE_unlock(store);
+ return ret;
+}
+#endif
+
PyDoc_STRVAR(PySSLContext_sni_callback_doc,
"Set a callback that will be called when a server name is provided by the SSL/TLS client in the SNI extension.\n\
\n\
@@ -4704,7 +4766,15 @@ _ssl__SSLContext_cert_store_stats_impl(P
int x509 = 0, crl = 0, ca = 0, i;
store = SSL_CTX_get_cert_store(self->ctx);
+#if HAVE_OPENSSL_X509_STORE_GET1_OBJECTS
+ objs = X509_STORE_get1_objects(store);
+ if (objs == NULL) {
+ PyErr_SetString(PyExc_MemoryError, "failed to query cert store");
+ return NULL;
+ }
+#else
objs = X509_STORE_get0_objects(store);
+#endif
for (i = 0; i < sk_X509_OBJECT_num(objs); i++) {
obj = sk_X509_OBJECT_value(objs, i);
switch (X509_OBJECT_get_type(obj)) {
@@ -4718,12 +4788,13 @@ _ssl__SSLContext_cert_store_stats_impl(P
crl++;
break;
default:
- /* Ignore X509_LU_FAIL, X509_LU_RETRY, X509_LU_PKEY.
- * As far as I can tell they are internal states and never
- * stored in a cert store */
+ /* Ignore unrecognized types. */
break;
}
}
+#if HAVE_OPENSSL_X509_STORE_GET1_OBJECTS
+ sk_X509_OBJECT_pop_free(objs, X509_OBJECT_free);
+#endif
return Py_BuildValue("{sisisi}", "x509", x509, "crl", crl,
"x509_ca", ca);
}
@@ -4755,7 +4826,15 @@ _ssl__SSLContext_get_ca_certs_impl(PySSL
}
store = SSL_CTX_get_cert_store(self->ctx);
+#if HAVE_OPENSSL_X509_STORE_GET1_OBJECTS
+ objs = X509_STORE_get1_objects(store);
+ if (objs == NULL) {
+ PyErr_SetString(PyExc_MemoryError, "failed to query cert store");
+ return NULL;
+ }
+#else
objs = X509_STORE_get0_objects(store);
+#endif
for (i = 0; i < sk_X509_OBJECT_num(objs); i++) {
X509_OBJECT *obj;
X509 *cert;
@@ -4783,9 +4862,15 @@ _ssl__SSLContext_get_ca_certs_impl(PySSL
}
Py_CLEAR(ci);
}
+#if HAVE_OPENSSL_X509_STORE_GET1_OBJECTS
+ sk_X509_OBJECT_pop_free(objs, X509_OBJECT_free);
+#endif
return rlist;
error:
+#if HAVE_OPENSSL_X509_STORE_GET1_OBJECTS
+ sk_X509_OBJECT_pop_free(objs, X509_OBJECT_free);
+#endif
Py_XDECREF(ci);
Py_XDECREF(rlist);
return NULL;

View File

@ -0,0 +1,396 @@
From 5e58376d424fb951966277e5d46cf0b11d860ef3 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <encukou@gmail.com>
Date: Wed, 24 Apr 2024 14:29:30 +0200
Subject: [PATCH 1/3] gh-113171: gh-65056: Fix "private" (non-global) IP
address ranges (GH-113179) (GH-113186) (GH-118177)
* GH-113171: Fix "private" (non-global) IP address ranges (GH-113179)
The _private_networks variables, used by various is_private
implementations, were missing some ranges and at the same time had
overly strict ranges (where there are more specific ranges considered
globally reachable by the IANA registries).
This patch updates the ranges with what was missing or otherwise
incorrect.
100.64.0.0/10 is left alone, for now, as it's been made special in [1].
The _address_exclude_many() call returns 8 networks for IPv4, 121
networks for IPv6.
[1] https://github.com/python/cpython/issues/61602
* GH-65056: Improve the IP address' is_global/is_private documentation (GH-113186)
It wasn't clear what the semantics of is_global/is_private are and, when
one gets to the bottom of it, it's not quite so simple (hence the
exceptions listed).
(cherry picked from commit 2a4cbf17af19a01d942f9579342f77c39fbd23c4)
(cherry picked from commit 40d75c2b7f5c67e254d0a025e0f2e2c7ada7f69f)
---------
(cherry picked from commit f86b17ac511e68192ba71f27e752321a3252cee3)
Co-authored-by: Jakub Stasiak <jakub@stasiak.at>
---
Doc/library/ipaddress.rst | 43 +++-
Doc/tools/susp-ignored.csv | 8
Doc/whatsnew/3.9.rst | 9
Lib/ipaddress.py | 107 +++++++---
Lib/test/test_ipaddress.py | 52 ++++
Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst | 9
6 files changed, 201 insertions(+), 27 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst
--- a/Doc/library/ipaddress.rst
+++ b/Doc/library/ipaddress.rst
@@ -188,18 +188,53 @@ write code that handles both IP versions
.. attribute:: is_private
- ``True`` if the address is allocated for private networks. See
+ ``True`` if the address is defined as not globally reachable by
iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
- (for IPv6).
+ (for IPv6) with the following exceptions:
+
+ * ``is_private`` is ``False`` for the shared address space (``100.64.0.0/10``)
+ * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
+ semantics of the underlying IPv4 addresses and the following condition holds
+ (see :attr:`IPv6Address.ipv4_mapped`)::
+
+ address.is_private == address.ipv4_mapped.is_private
+
+ ``is_private`` has value opposite to :attr:`is_global`, except for the shared address space
+ (``100.64.0.0/10`` range) where they are both ``False``.
+
+ .. versionchanged:: 3.9.20
+
+ Fixed some false positives and false negatives.
+
+ * ``192.0.0.0/24`` is considered private with the exception of ``192.0.0.9/32`` and
+ ``192.0.0.10/32`` (previously: only the ``192.0.0.0/29`` sub-range was considered private).
+ * ``64:ff9b:1::/48`` is considered private.
+ * ``2002::/16`` is considered private.
+ * There are exceptions within ``2001::/23`` (otherwise considered private): ``2001:1::1/128``,
+ ``2001:1::2/128``, ``2001:3::/32``, ``2001:4:112::/48``, ``2001:20::/28``, ``2001:30::/28``.
+ The exceptions are not considered private.
.. attribute:: is_global
- ``True`` if the address is allocated for public networks. See
+ ``True`` if the address is defined as globally reachable by
iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
- (for IPv6).
+ (for IPv6) with the following exception:
+
+ For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
+ semantics of the underlying IPv4 addresses and the following condition holds
+ (see :attr:`IPv6Address.ipv4_mapped`)::
+
+ address.is_global == address.ipv4_mapped.is_global
+
+ ``is_global`` has value opposite to :attr:`is_private`, except for the shared address space
+ (``100.64.0.0/10`` range) where they are both ``False``.
.. versionadded:: 3.4
+ .. versionchanged:: 3.9.20
+
+ Fixed some false positives and false negatives, see :attr:`is_private` for details.
+
.. attribute:: is_unspecified
``True`` if the address is unspecified. See :RFC:`5735` (for IPv4)
--- a/Doc/tools/susp-ignored.csv
+++ b/Doc/tools/susp-ignored.csv
@@ -169,6 +169,14 @@ library/ipaddress,,:db00,2001:db00::0/24
library/ipaddress,,::,2001:db00::0/24
library/ipaddress,,:db00,2001:db00::0/ffff:ff00::
library/ipaddress,,::,2001:db00::0/ffff:ff00::
+library/ipaddress,,:ff9b,64:ff9b:1::/48
+library/ipaddress,,::,64:ff9b:1::/48
+library/ipaddress,,::,2001::
+library/ipaddress,,::,2001:1::
+library/ipaddress,,::,2001:3::
+library/ipaddress,,::,2001:4:112::
+library/ipaddress,,::,2001:20::
+library/ipaddress,,::,2001:30::
library/itertools,,:step,elements from seq[start:stop:step]
library/itertools,,:stop,elements from seq[start:stop:step]
library/itertools,,::,kernel = tuple(kernel)[::-1]
--- a/Doc/whatsnew/3.9.rst
+++ b/Doc/whatsnew/3.9.rst
@@ -1616,3 +1616,12 @@ tarfile
:exc:`DeprecationWarning`.
In Python 3.14, the default will switch to ``'data'``.
(Contributed by Petr Viktorin in :pep:`706`.)
+
+Notable changes in 3.9.20
+=========================
+
+ipaddress
+---------
+
+* Fixed ``is_global`` and ``is_private`` behavior in ``IPv4Address``,
+ ``IPv6Address``, ``IPv4Network`` and ``IPv6Network``.
--- a/Lib/ipaddress.py
+++ b/Lib/ipaddress.py
@@ -1322,18 +1322,41 @@ class IPv4Address(_BaseV4, _BaseAddress)
@property
@functools.lru_cache()
def is_private(self):
- """Test if this address is allocated for private networks.
-
- Returns:
- A boolean, True if the address is reserved per
- iana-ipv4-special-registry.
-
- """
- return any(self in net for net in self._constants._private_networks)
+ """``True`` if the address is defined as not globally reachable by
+ iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
+ (for IPv6) with the following exceptions:
+
+ * ``is_private`` is ``False`` for ``100.64.0.0/10``
+ * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
+ semantics of the underlying IPv4 addresses and the following condition holds
+ (see :attr:`IPv6Address.ipv4_mapped`)::
+
+ address.is_private == address.ipv4_mapped.is_private
+
+ ``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10``
+ IPv4 range where they are both ``False``.
+ """
+ return (
+ any(self in net for net in self._constants._private_networks)
+ and all(self not in net for net in self._constants._private_networks_exceptions)
+ )
@property
@functools.lru_cache()
def is_global(self):
+ """``True`` if the address is defined as globally reachable by
+ iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
+ (for IPv6) with the following exception:
+
+ For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
+ semantics of the underlying IPv4 addresses and the following condition holds
+ (see :attr:`IPv6Address.ipv4_mapped`)::
+
+ address.is_global == address.ipv4_mapped.is_global
+
+ ``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10``
+ IPv4 range where they are both ``False``.
+ """
return self not in self._constants._public_network and not self.is_private
@property
@@ -1537,13 +1560,15 @@ class _IPv4Constants:
_public_network = IPv4Network('100.64.0.0/10')
+ # Not globally reachable address blocks listed on
+ # https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
_private_networks = [
IPv4Network('0.0.0.0/8'),
IPv4Network('10.0.0.0/8'),
IPv4Network('127.0.0.0/8'),
IPv4Network('169.254.0.0/16'),
IPv4Network('172.16.0.0/12'),
- IPv4Network('192.0.0.0/29'),
+ IPv4Network('192.0.0.0/24'),
IPv4Network('192.0.0.170/31'),
IPv4Network('192.0.2.0/24'),
IPv4Network('192.168.0.0/16'),
@@ -1554,6 +1579,11 @@ class _IPv4Constants:
IPv4Network('255.255.255.255/32'),
]
+ _private_networks_exceptions = [
+ IPv4Network('192.0.0.9/32'),
+ IPv4Network('192.0.0.10/32'),
+ ]
+
_reserved_network = IPv4Network('240.0.0.0/4')
_unspecified_address = IPv4Address('0.0.0.0')
@@ -1995,23 +2025,42 @@ class IPv6Address(_BaseV6, _BaseAddress)
@property
@functools.lru_cache()
def is_private(self):
- """Test if this address is allocated for private networks.
-
- Returns:
- A boolean, True if the address is reserved per
- iana-ipv6-special-registry.
-
- """
- return any(self in net for net in self._constants._private_networks)
+ """``True`` if the address is defined as not globally reachable by
+ iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
+ (for IPv6) with the following exceptions:
+
+ * ``is_private`` is ``False`` for ``100.64.0.0/10``
+ * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
+ semantics of the underlying IPv4 addresses and the following condition holds
+ (see :attr:`IPv6Address.ipv4_mapped`)::
+
+ address.is_private == address.ipv4_mapped.is_private
+
+ ``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10``
+ IPv4 range where they are both ``False``.
+ """
+ ipv4_mapped = self.ipv4_mapped
+ if ipv4_mapped is not None:
+ return ipv4_mapped.is_private
+ return (
+ any(self in net for net in self._constants._private_networks)
+ and all(self not in net for net in self._constants._private_networks_exceptions)
+ )
@property
def is_global(self):
- """Test if this address is allocated for public networks.
+ """``True`` if the address is defined as globally reachable by
+ iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
+ (for IPv6) with the following exception:
+
+ For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
+ semantics of the underlying IPv4 addresses and the following condition holds
+ (see :attr:`IPv6Address.ipv4_mapped`)::
- Returns:
- A boolean, true if the address is not reserved per
- iana-ipv6-special-registry.
+ address.is_global == address.ipv4_mapped.is_global
+ ``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10``
+ IPv4 range where they are both ``False``.
"""
return not self.is_private
@@ -2252,19 +2301,31 @@ class _IPv6Constants:
_multicast_network = IPv6Network('ff00::/8')
+ # Not globally reachable address blocks listed on
+ # https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
_private_networks = [
IPv6Network('::1/128'),
IPv6Network('::/128'),
IPv6Network('::ffff:0:0/96'),
+ IPv6Network('64:ff9b:1::/48'),
IPv6Network('100::/64'),
IPv6Network('2001::/23'),
- IPv6Network('2001:2::/48'),
IPv6Network('2001:db8::/32'),
- IPv6Network('2001:10::/28'),
+ # IANA says N/A, let's consider it not globally reachable to be safe
+ IPv6Network('2002::/16'),
IPv6Network('fc00::/7'),
IPv6Network('fe80::/10'),
]
+ _private_networks_exceptions = [
+ IPv6Network('2001:1::1/128'),
+ IPv6Network('2001:1::2/128'),
+ IPv6Network('2001:3::/32'),
+ IPv6Network('2001:4:112::/48'),
+ IPv6Network('2001:20::/28'),
+ IPv6Network('2001:30::/28'),
+ ]
+
_reserved_networks = [
IPv6Network('::/8'), IPv6Network('100::/8'),
IPv6Network('200::/7'), IPv6Network('400::/6'),
--- a/Lib/test/test_ipaddress.py
+++ b/Lib/test/test_ipaddress.py
@@ -2263,6 +2263,10 @@ class IpaddrUnitTest(unittest.TestCase):
self.assertEqual(True, ipaddress.ip_address(
'172.31.255.255').is_private)
self.assertEqual(False, ipaddress.ip_address('172.32.0.0').is_private)
+ self.assertFalse(ipaddress.ip_address('192.0.0.0').is_global)
+ self.assertTrue(ipaddress.ip_address('192.0.0.9').is_global)
+ self.assertTrue(ipaddress.ip_address('192.0.0.10').is_global)
+ self.assertFalse(ipaddress.ip_address('192.0.0.255').is_global)
self.assertEqual(True,
ipaddress.ip_address('169.254.100.200').is_link_local)
@@ -2278,6 +2282,40 @@ class IpaddrUnitTest(unittest.TestCase):
self.assertEqual(False, ipaddress.ip_address('128.0.0.0').is_loopback)
self.assertEqual(True, ipaddress.ip_network('0.0.0.0').is_unspecified)
+ def testPrivateNetworks(self):
+ self.assertEqual(True, ipaddress.ip_network("0.0.0.0/0").is_private)
+ self.assertEqual(False, ipaddress.ip_network("1.0.0.0/8").is_private)
+
+ self.assertEqual(True, ipaddress.ip_network("0.0.0.0/8").is_private)
+ self.assertEqual(True, ipaddress.ip_network("10.0.0.0/8").is_private)
+ self.assertEqual(True, ipaddress.ip_network("127.0.0.0/8").is_private)
+ self.assertEqual(True, ipaddress.ip_network("169.254.0.0/16").is_private)
+ self.assertEqual(True, ipaddress.ip_network("172.16.0.0/12").is_private)
+ self.assertEqual(True, ipaddress.ip_network("192.0.0.0/29").is_private)
+ self.assertEqual(False, ipaddress.ip_network("192.0.0.9/32").is_private)
+ self.assertEqual(True, ipaddress.ip_network("192.0.0.170/31").is_private)
+ self.assertEqual(True, ipaddress.ip_network("192.0.2.0/24").is_private)
+ self.assertEqual(True, ipaddress.ip_network("192.168.0.0/16").is_private)
+ self.assertEqual(True, ipaddress.ip_network("198.18.0.0/15").is_private)
+ self.assertEqual(True, ipaddress.ip_network("198.51.100.0/24").is_private)
+ self.assertEqual(True, ipaddress.ip_network("203.0.113.0/24").is_private)
+ self.assertEqual(True, ipaddress.ip_network("240.0.0.0/4").is_private)
+ self.assertEqual(True, ipaddress.ip_network("255.255.255.255/32").is_private)
+
+ self.assertEqual(False, ipaddress.ip_network("::/0").is_private)
+ self.assertEqual(False, ipaddress.ip_network("::ff/128").is_private)
+
+ self.assertEqual(True, ipaddress.ip_network("::1/128").is_private)
+ self.assertEqual(True, ipaddress.ip_network("::/128").is_private)
+ self.assertEqual(True, ipaddress.ip_network("::ffff:0:0/96").is_private)
+ self.assertEqual(True, ipaddress.ip_network("100::/64").is_private)
+ self.assertEqual(True, ipaddress.ip_network("2001:2::/48").is_private)
+ self.assertEqual(False, ipaddress.ip_network("2001:3::/48").is_private)
+ self.assertEqual(True, ipaddress.ip_network("2001:db8::/32").is_private)
+ self.assertEqual(True, ipaddress.ip_network("2001:10::/28").is_private)
+ self.assertEqual(True, ipaddress.ip_network("fc00::/7").is_private)
+ self.assertEqual(True, ipaddress.ip_network("fe80::/10").is_private)
+
def testReservedIpv6(self):
self.assertEqual(True, ipaddress.ip_network('ffff::').is_multicast)
@@ -2351,6 +2389,20 @@ class IpaddrUnitTest(unittest.TestCase):
self.assertEqual(True, ipaddress.ip_address('0::0').is_unspecified)
self.assertEqual(False, ipaddress.ip_address('::1').is_unspecified)
+ self.assertFalse(ipaddress.ip_address('64:ff9b:1::').is_global)
+ self.assertFalse(ipaddress.ip_address('2001::').is_global)
+ self.assertTrue(ipaddress.ip_address('2001:1::1').is_global)
+ self.assertTrue(ipaddress.ip_address('2001:1::2').is_global)
+ self.assertFalse(ipaddress.ip_address('2001:2::').is_global)
+ self.assertTrue(ipaddress.ip_address('2001:3::').is_global)
+ self.assertFalse(ipaddress.ip_address('2001:4::').is_global)
+ self.assertTrue(ipaddress.ip_address('2001:4:112::').is_global)
+ self.assertFalse(ipaddress.ip_address('2001:10::').is_global)
+ self.assertTrue(ipaddress.ip_address('2001:20::').is_global)
+ self.assertTrue(ipaddress.ip_address('2001:30::').is_global)
+ self.assertFalse(ipaddress.ip_address('2001:40::').is_global)
+ self.assertFalse(ipaddress.ip_address('2002::').is_global)
+
# some generic IETF reserved addresses
self.assertEqual(True, ipaddress.ip_address('100::').is_reserved)
self.assertEqual(True, ipaddress.ip_network('4000::1/128').is_reserved)
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst
@@ -0,0 +1,9 @@
+Fixed various false positives and false negatives in
+
+* :attr:`ipaddress.IPv4Address.is_private` (see these docs for details)
+* :attr:`ipaddress.IPv4Address.is_global`
+* :attr:`ipaddress.IPv6Address.is_private`
+* :attr:`ipaddress.IPv6Address.is_global`
+
+Also in the corresponding :class:`ipaddress.IPv4Network` and :class:`ipaddress.IPv6Network`
+attributes.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,125 @@
From 15eec9d5076b780463c3dc73afcef688651c5295 Mon Sep 17 00:00:00 2001
From: Serhiy Storchaka <storchaka@gmail.com>
Date: Sat, 17 Aug 2024 16:30:52 +0300
Subject: [PATCH] gh-123067: Fix quadratic complexity in parsing "-quoted
cookie values with backslashes (GH-123075)
This fixes CVE-2024-7592.
(cherry picked from commit 44e458357fca05ca0ae2658d62c8c595b048b5ef)
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
---
Lib/http/cookies.py | 34 ++------
Lib/test/test_http_cookies.py | 38 ++++++++++
Misc/NEWS.d/next/Library/2024-08-16-19-13-21.gh-issue-123067.Nx9O4R.rst | 1
3 files changed, 47 insertions(+), 26 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2024-08-16-19-13-21.gh-issue-123067.Nx9O4R.rst
--- a/Lib/http/cookies.py
+++ b/Lib/http/cookies.py
@@ -184,8 +184,13 @@ def _quote(str):
return '"' + str.translate(_Translator) + '"'
-_OctalPatt = re.compile(r"\\[0-3][0-7][0-7]")
-_QuotePatt = re.compile(r"[\\].")
+_unquote_sub = re.compile(r'\\(?:([0-3][0-7][0-7])|(.))').sub
+
+def _unquote_replace(m):
+ if m[1]:
+ return chr(int(m[1], 8))
+ else:
+ return m[2]
def _unquote(str):
# If there aren't any doublequotes,
@@ -205,30 +210,7 @@ def _unquote(str):
# \012 --> \n
# \" --> "
#
- i = 0
- n = len(str)
- res = []
- while 0 <= i < n:
- o_match = _OctalPatt.search(str, i)
- q_match = _QuotePatt.search(str, i)
- if not o_match and not q_match: # Neither matched
- res.append(str[i:])
- break
- # else:
- j = k = -1
- if o_match:
- j = o_match.start(0)
- if q_match:
- k = q_match.start(0)
- if q_match and (not o_match or k < j): # QuotePatt matched
- res.append(str[i:k])
- res.append(str[k+1])
- i = k + 2
- else: # OctalPatt matched
- res.append(str[i:j])
- res.append(chr(int(str[j+1:j+4], 8)))
- i = j + 4
- return _nulljoin(res)
+ return _unquote_sub(_unquote_replace, str)
# The _getdate() routine is used to set the expiration time in the cookie's HTTP
# header. By default, _getdate() returns the current time in the appropriate
--- a/Lib/test/test_http_cookies.py
+++ b/Lib/test/test_http_cookies.py
@@ -5,6 +5,7 @@ from test.support import run_unittest, r
import unittest
from http import cookies
import pickle
+from test import support
class CookieTests(unittest.TestCase):
@@ -58,6 +59,43 @@ class CookieTests(unittest.TestCase):
for k, v in sorted(case['dict'].items()):
self.assertEqual(C[k].value, v)
+ def test_unquote(self):
+ cases = [
+ (r'a="b=\""', 'b="'),
+ (r'a="b=\\"', 'b=\\'),
+ (r'a="b=\="', 'b=='),
+ (r'a="b=\n"', 'b=n'),
+ (r'a="b=\042"', 'b="'),
+ (r'a="b=\134"', 'b=\\'),
+ (r'a="b=\377"', 'b=\xff'),
+ (r'a="b=\400"', 'b=400'),
+ (r'a="b=\42"', 'b=42'),
+ (r'a="b=\\042"', 'b=\\042'),
+ (r'a="b=\\134"', 'b=\\134'),
+ (r'a="b=\\\""', 'b=\\"'),
+ (r'a="b=\\\042"', 'b=\\"'),
+ (r'a="b=\134\""', 'b=\\"'),
+ (r'a="b=\134\042"', 'b=\\"'),
+ ]
+ for encoded, decoded in cases:
+ with self.subTest(encoded):
+ C = cookies.SimpleCookie()
+ C.load(encoded)
+ self.assertEqual(C['a'].value, decoded)
+
+ @support.requires_resource('cpu')
+ def test_unquote_large(self):
+ n = 10**6
+ for encoded in r'\\', r'\134':
+ with self.subTest(encoded):
+ data = 'a="b=' + encoded*n + ';"'
+ C = cookies.SimpleCookie()
+ C.load(data)
+ value = C['a'].value
+ self.assertEqual(value[:3], 'b=\\')
+ self.assertEqual(value[-2:], '\\;')
+ self.assertEqual(len(value), n + 3)
+
def test_load(self):
C = cookies.SimpleCookie()
C.load('Customer="WILE_E_COYOTE"; Version=1; Path=/acme')
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-08-16-19-13-21.gh-issue-123067.Nx9O4R.rst
@@ -0,0 +1 @@
+Fix quadratic complexity in parsing ``"``-quoted cookie values with backslashes by :mod:`http.cookies`.

View File

@ -0,0 +1,339 @@
From f9ddc53ea850fb02d640a9b3263756d43fb6d868 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <encukou@gmail.com>
Date: Wed, 31 Jul 2024 00:19:48 +0200
Subject: [PATCH] [3.9] gh-121650: Encode newlines in headers, and verify
headers are sound (GH-122233)
GH-GH- Encode header parts that contain newlines
Per RFC 2047:
> [...] these encoding schemes allow the
> encoding of arbitrary octet values, mail readers that implement this
> decoding should also ensure that display of the decoded data on the
> recipient's terminal will not cause unwanted side-effects
It seems that the "quoted-word" scheme is a valid way to include
a newline character in a header value, just like we already allow
undecodable bytes or control characters.
They do need to be properly quoted when serialized to text, though.
GH-GH- Verify that email headers are well-formed
This should fail for custom fold() implementations that aren't careful
about newlines.
(cherry picked from commit 097633981879b3c9de9a1dd120d3aa585ecc2384)
Co-authored-by: Petr Viktorin <encukou@gmail.com>
Co-authored-by: Bas Bloemsaat <bas@bloemsaat.org>
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
---
Doc/library/email.errors.rst | 6
Doc/library/email.policy.rst | 18 ++
Doc/whatsnew/3.9.rst | 12 +
Lib/email/_header_value_parser.py | 12 +
Lib/email/_policybase.py | 8 +
Lib/email/errors.py | 4
Lib/email/generator.py | 13 +-
Lib/test/test_email/test_generator.py | 62 ++++++++++
Lib/test/test_email/test_policy.py | 26 ++++
Misc/NEWS.d/next/Library/2024-07-27-16-10-41.gh-issue-121650.nf6oc9.rst | 5
10 files changed, 162 insertions(+), 4 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2024-07-27-16-10-41.gh-issue-121650.nf6oc9.rst
--- a/Doc/library/email.errors.rst
+++ b/Doc/library/email.errors.rst
@@ -59,6 +59,12 @@ The following exception classes are defi
:class:`~email.mime.image.MIMEImage`).
+.. exception:: HeaderWriteError()
+
+ Raised when an error occurs when the :mod:`~email.generator` outputs
+ headers.
+
+
Here is the list of the defects that the :class:`~email.parser.FeedParser`
can find while parsing messages. Note that the defects are added to the message
where the problem was found, so for example, if a message nested inside a
--- a/Doc/library/email.policy.rst
+++ b/Doc/library/email.policy.rst
@@ -229,6 +229,24 @@ added matters. To illustrate::
.. versionadded:: 3.6
+
+ .. attribute:: verify_generated_headers
+
+ If ``True`` (the default), the generator will raise
+ :exc:`~email.errors.HeaderWriteError` instead of writing a header
+ that is improperly folded or delimited, such that it would
+ be parsed as multiple headers or joined with adjacent data.
+ Such headers can be generated by custom header classes or bugs
+ in the ``email`` module.
+
+ As it's a security feature, this defaults to ``True`` even in the
+ :class:`~email.policy.Compat32` policy.
+ For backwards compatible, but unsafe, behavior, it must be set to
+ ``False`` explicitly.
+
+ .. versionadded:: 3.11.10
+
+
The following :class:`Policy` method is intended to be called by code using
the email library to create policy instances with custom settings:
--- a/Doc/whatsnew/3.9.rst
+++ b/Doc/whatsnew/3.9.rst
@@ -1625,3 +1625,15 @@ ipaddress
* Fixed ``is_global`` and ``is_private`` behavior in ``IPv4Address``,
``IPv6Address``, ``IPv4Network`` and ``IPv6Network``.
+
+email
+-----
+
+* Headers with embedded newlines are now quoted on output.
+
+ The :mod:`~email.generator` will now refuse to serialize (write) headers
+ that are improperly folded or delimited, such that they would be parsed as
+ multiple headers or joined with adjacent data.
+ If you need to turn this safety feature off,
+ set :attr:`~email.policy.Policy.verify_generated_headers`.
+ (Contributed by Bas Bloemsaat and Petr Viktorin in :gh:`121650`.)
--- a/Lib/email/_header_value_parser.py
+++ b/Lib/email/_header_value_parser.py
@@ -92,6 +92,8 @@ TOKEN_ENDS = TSPECIALS | WSP
ASPECIALS = TSPECIALS | set("*'%")
ATTRIBUTE_ENDS = ASPECIALS | WSP
EXTENDED_ATTRIBUTE_ENDS = ATTRIBUTE_ENDS - set('%')
+NLSET = {'\n', '\r'}
+SPECIALSNL = SPECIALS | NLSET
def quote_string(value):
return '"'+str(value).replace('\\', '\\\\').replace('"', r'\"')+'"'
@@ -2778,9 +2780,13 @@ def _refold_parse_tree(parse_tree, *, po
wrap_as_ew_blocked -= 1
continue
tstr = str(part)
- if part.token_type == 'ptext' and set(tstr) & SPECIALS:
- # Encode if tstr contains special characters.
- want_encoding = True
+ if not want_encoding:
+ if part.token_type == 'ptext':
+ # Encode if tstr contains special characters.
+ want_encoding = not SPECIALSNL.isdisjoint(tstr)
+ else:
+ # Encode if tstr contains newlines.
+ want_encoding = not NLSET.isdisjoint(tstr)
try:
tstr.encode(encoding)
charset = encoding
--- a/Lib/email/_policybase.py
+++ b/Lib/email/_policybase.py
@@ -157,6 +157,13 @@ class Policy(_PolicyBase, metaclass=abc.
message_factory -- the class to use to create new message objects.
If the value is None, the default is Message.
+ verify_generated_headers
+ -- if true, the generator verifies that each header
+ they are properly folded, so that a parser won't
+ treat it as multiple headers, start-of-body, or
+ part of another header.
+ This is a check against custom Header & fold()
+ implementations.
"""
raise_on_defect = False
@@ -165,6 +172,7 @@ class Policy(_PolicyBase, metaclass=abc.
max_line_length = 78
mangle_from_ = False
message_factory = None
+ verify_generated_headers = True
def handle_defect(self, obj, defect):
"""Based on policy, either raise defect or call register_defect.
--- a/Lib/email/errors.py
+++ b/Lib/email/errors.py
@@ -29,6 +29,10 @@ class CharsetError(MessageError):
"""An illegal charset was given."""
+class HeaderWriteError(MessageError):
+ """Error while writing headers."""
+
+
# These are parsing defects which the parser was able to work around.
class MessageDefect(ValueError):
"""Base class for a message defect."""
--- a/Lib/email/generator.py
+++ b/Lib/email/generator.py
@@ -14,12 +14,14 @@ import random
from copy import deepcopy
from io import StringIO, BytesIO
from email.utils import _has_surrogates
+from email.errors import HeaderWriteError
UNDERSCORE = '_'
NL = '\n' # XXX: no longer used by the code below.
NLCRE = re.compile(r'\r\n|\r|\n')
fcre = re.compile(r'^From ', re.MULTILINE)
+NEWLINE_WITHOUT_FWSP = re.compile(r'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]')
@@ -223,7 +225,16 @@ class Generator:
def _write_headers(self, msg):
for h, v in msg.raw_items():
- self.write(self.policy.fold(h, v))
+ folded = self.policy.fold(h, v)
+ if self.policy.verify_generated_headers:
+ linesep = self.policy.linesep
+ if not folded.endswith(self.policy.linesep):
+ raise HeaderWriteError(
+ f'folded header does not end with {linesep!r}: {folded!r}')
+ if NEWLINE_WITHOUT_FWSP.search(folded.removesuffix(linesep)):
+ raise HeaderWriteError(
+ f'folded header contains newline: {folded!r}')
+ self.write(folded)
# A blank line always separates headers from body
self.write(self._NL)
--- a/Lib/test/test_email/test_generator.py
+++ b/Lib/test/test_email/test_generator.py
@@ -6,6 +6,7 @@ from email.message import EmailMessage
from email.generator import Generator, BytesGenerator
from email.headerregistry import Address
from email import policy
+import email.errors
from test.test_email import TestEmailBase, parameterize
@@ -216,6 +217,44 @@ class TestGeneratorBase:
g.flatten(msg)
self.assertEqual(s.getvalue(), self.typ(expected))
+ def test_keep_encoded_newlines(self):
+ msg = self.msgmaker(self.typ(textwrap.dedent("""\
+ To: nobody
+ Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com
+
+ None
+ """)))
+ expected = textwrap.dedent("""\
+ To: nobody
+ Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com
+
+ None
+ """)
+ s = self.ioclass()
+ g = self.genclass(s, policy=self.policy.clone(max_line_length=80))
+ g.flatten(msg)
+ self.assertEqual(s.getvalue(), self.typ(expected))
+
+ def test_keep_long_encoded_newlines(self):
+ msg = self.msgmaker(self.typ(textwrap.dedent("""\
+ To: nobody
+ Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com
+
+ None
+ """)))
+ expected = textwrap.dedent("""\
+ To: nobody
+ Subject: Bad subject
+ =?utf-8?q?=0A?=Bcc:
+ injection@example.com
+
+ None
+ """)
+ s = self.ioclass()
+ g = self.genclass(s, policy=self.policy.clone(max_line_length=30))
+ g.flatten(msg)
+ self.assertEqual(s.getvalue(), self.typ(expected))
+
class TestGenerator(TestGeneratorBase, TestEmailBase):
@@ -224,6 +263,29 @@ class TestGenerator(TestGeneratorBase, T
ioclass = io.StringIO
typ = str
+ def test_verify_generated_headers(self):
+ """gh-121650: by default the generator prevents header injection"""
+ class LiteralHeader(str):
+ name = 'Header'
+ def fold(self, **kwargs):
+ return self
+
+ for text in (
+ 'Value\r\nBad Injection\r\n',
+ 'NoNewLine'
+ ):
+ with self.subTest(text=text):
+ message = message_from_string(
+ "Header: Value\r\n\r\nBody",
+ policy=self.policy,
+ )
+
+ del message['Header']
+ message['Header'] = LiteralHeader(text)
+
+ with self.assertRaises(email.errors.HeaderWriteError):
+ message.as_string()
+
class TestBytesGenerator(TestGeneratorBase, TestEmailBase):
--- a/Lib/test/test_email/test_policy.py
+++ b/Lib/test/test_email/test_policy.py
@@ -26,6 +26,7 @@ class PolicyAPITests(unittest.TestCase):
'raise_on_defect': False,
'mangle_from_': True,
'message_factory': None,
+ 'verify_generated_headers': True,
}
# These default values are the ones set on email.policy.default.
# If any of these defaults change, the docs must be updated.
@@ -277,6 +278,31 @@ class PolicyAPITests(unittest.TestCase):
with self.assertRaises(email.errors.HeaderParseError):
policy.fold("Subject", subject)
+ def test_verify_generated_headers(self):
+ """Turning protection off allows header injection"""
+ policy = email.policy.default.clone(verify_generated_headers=False)
+ for text in (
+ 'Header: Value\r\nBad: Injection\r\n',
+ 'Header: NoNewLine'
+ ):
+ with self.subTest(text=text):
+ message = email.message_from_string(
+ "Header: Value\r\n\r\nBody",
+ policy=policy,
+ )
+ class LiteralHeader(str):
+ name = 'Header'
+ def fold(self, **kwargs):
+ return self
+
+ del message['Header']
+ message['Header'] = LiteralHeader(text)
+
+ self.assertEqual(
+ message.as_string(),
+ f"{text}\nBody",
+ )
+
# XXX: Need subclassing tests.
# For adding subclassed objects, make sure the usual rules apply (subclass
# wins), but that the order still works (right overrides left).
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-07-27-16-10-41.gh-issue-121650.nf6oc9.rst
@@ -0,0 +1,5 @@
+:mod:`email` headers with embedded newlines are now quoted on output. The
+:mod:`~email.generator` will now refuse to serialize (write) headers that
+are unsafely folded or delimited; see
+:attr:`~email.policy.Policy.verify_generated_headers`. (Contributed by Bas
+Bloemsaat and Petr Viktorin in :gh:`121650`.)

View File

@ -0,0 +1,136 @@
From dcb320a0c85713c5dfe89a83d6eb295ad1511be8 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" <jaraco@jaraco.com>
Date: Tue, 27 Aug 2024 17:10:30 -0400
Subject: [PATCH] [3.8] [3.9] [3.11] gh-123270: Replaced SanitizedNames with a
more surgical fix. (GH-123354)
Applies changes from zipp 3.20.1 and jaraco/zippGH-124
(cherry picked from commit 2231286d78d328c2f575e0b05b16fe447d1656d6)
(cherry picked from commit 17b77bb41409259bad1cd6c74761c18b6ab1e860)
(cherry picked from commit 66d3383)
Co-authored-by: Jason R. Coombs <jaraco@jaraco.com>
---
Lib/test/test_zipfile.py | 75 ++++++++++
Lib/zipfile.py | 9 -
Misc/NEWS.d/next/Library/2024-08-26-13-45-20.gh-issue-123270.gXHvNJ.rst | 3
3 files changed, 85 insertions(+), 2 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2024-08-26-13-45-20.gh-issue-123270.gXHvNJ.rst
--- a/Lib/test/test_zipfile.py
+++ b/Lib/test/test_zipfile.py
@@ -3054,6 +3054,81 @@ class TestPath(unittest.TestCase):
data = ['/'.join(string.ascii_lowercase + str(n)) for n in range(10000)]
zipfile.CompleteDirs._implied_dirs(data)
+ 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("../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
+
if __name__ == "__main__":
unittest.main()
--- a/Lib/zipfile.py
+++ b/Lib/zipfile.py
@@ -2146,7 +2146,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']
@@ -2158,9 +2158,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-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.

View File

@ -0,0 +1,57 @@
From 910f38d9768d39d4d31426743ae4081ed1ab66b6 Mon Sep 17 00:00:00 2001
From: Michal Cyprian <m.cyprian@gmail.com>
Date: Mon, 26 Jun 2017 16:32:56 +0200
Subject: [PATCH] 00251: Change user install location
Set values of prefix and exec_prefix in distutils install command
to /usr/local if executable is /usr/bin/python* and RPM build
is not detected to make pip and distutils install into separate location.
Fedora Change: https://fedoraproject.org/wiki/Changes/Making_sudo_pip_safe
---
Lib/distutils/command/install.py | 15 +++++++++++++--
Lib/site.py | 9 ++++++++-
2 files changed, 21 insertions(+), 3 deletions(-)
--- a/Lib/distutils/command/install.py
+++ b/Lib/distutils/command/install.py
@@ -419,8 +419,19 @@ class install(Command):
raise DistutilsOptionError(
"must not supply exec-prefix without prefix")
- self.prefix = os.path.normpath(sys.prefix)
- self.exec_prefix = os.path.normpath(sys.exec_prefix)
+ # self.prefix is set to sys.prefix + /local/
+ # if neither RPM build nor virtual environment is
+ # detected to make pip and distutils install packages
+ # into the separate location.
+ if (not (hasattr(sys, 'real_prefix') or
+ sys.prefix != sys.base_prefix) and
+ 'RPM_BUILD_ROOT' not in os.environ):
+ addition = "/local"
+ else:
+ addition = ""
+
+ self.prefix = os.path.normpath(sys.prefix) + addition
+ self.exec_prefix = os.path.normpath(sys.exec_prefix) + addition
else:
if self.exec_prefix is None:
--- a/Lib/site.py
+++ b/Lib/site.py
@@ -362,7 +362,14 @@ def getsitepackages(prefixes=None):
return sitepackages
def addsitepackages(known_paths, prefixes=None):
- """Add site-packages to sys.path"""
+ """Add site-packages to sys.path
+
+ '/usr/local' is included in PREFIXES if RPM build is not detected
+ to make packages installed into this location visible.
+
+ """
+ if ENABLE_USER_SITE and 'RPM_BUILD_ROOT' not in os.environ:
+ PREFIXES.insert(0, "/usr/local")
for sitedir in getsitepackages(prefixes):
if os.path.isdir(sitedir):
addsitedir(sitedir, known_paths)

26
PACKAGING-NOTES Normal file
View File

@ -0,0 +1,26 @@
Notes for packagers of Python3
==============================
0. Faster build turnaround
--------------------------
By default, python builds with profile-guided optimization. This needs
an additional run of the test suite and it is generally slow.
PGO build takes around 50 minutes.
For development, use "--without profileopt" option to disable PGO. This
shortens the build time to ~5 minutes including test suite.
1. import_failed.map
----------------------
This is a mechanism installed as part of python3-base, that places shim modules
on python's path (through a generated zzzz-import-failed-hooks.pth file, so that
it is imported as much at the end as makes sense; and an _import_failed subdir
of /usr/lib/pythonX.Y). Then when the user tries to import a module that is part
of a subpackage, the ImportError will contain a helpful message telling them
which missing subpackage to install.
This can sometimes cause problems on non-standard configurations, if the pth
gets included too early (for instance if you are using a script to include all
pths by hand in some strange order). Just something to look out for.

3
Python-3.9.19.tar.xz Normal file
View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d4892cd1618f6458cb851208c030df1482779609d0f3939991bd38184f8c679e
size 19682840

16
Python-3.9.19.tar.xz.asc Normal file
View File

@ -0,0 +1,16 @@
-----BEGIN PGP SIGNATURE-----
iQIzBAABCgAdFiEE4/8oOcBIslwITevpsmmV4xAlBWgFAmX5uMIACgkQsmmV4xAl
BWj1tQ//T2qX0m08xWGV7az0D1sH3qjoY+4fEYrknw5uAHqZFiQecRsF27jxv6iH
gP/6GAUw+lbH+9UofhCc0NbPOklliS7gFLNqJdKYFB6JXRNxiRYKh3uVx5o2n0ES
kR3kRl77S47rtCbSMrKTh6ZoWowyIUZGFsIonk5KsLv+oELXY1AK/Im9i3/iTJ1Z
jd/e2oHWuseIxbGZAO8AEP8zOsMMIHfsL3ry8H9xhhPyQM6t5DldqLH3UVE6kq95
fs+olGO4FEKif3VDuLaHVlgtGZOUr6aDIYUmWxctPicboSb6RJAq37CCYgWykOyB
WQec0ONbU7lxt5jhemLSDRy0mEio7+nXIKsO9rDN0Wk1QMpHUl77/C5qVlzfHal7
NhPt8Yl0hBnOjzTq+di+xhAKJcdKp+zZH7/ugAbthuqhNfnkqiF68PANHrCm3gbY
myN0eSaQ9yIa/MbHW8Am9NL/nuFbxdJUL/OIKQ9kFHgD7Qid86TZF0G2vbiBH/eF
IVYoMxRZLd7eu5dIcwXSef+Ai97pODbx9y7bOCFyBO9FuFrlhPObgc7KXCeAzP+y
k5eWvZtWTvvQ+2si2iT22EPBO0D0pnhYWZKpGK5EuKuw8nasNS1yLbhDTVpARynd
8buQh3t2wPfILlQr0+JzDY8GSdQ/nIHGgx2IERdSX/v+9Yo2AvU=
=gYAl
-----END PGP SIGNATURE-----

3
Python-3.9.20.tar.xz Normal file
View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6b281279efd85294d2d6993e173983a57464c0133956fbbb5536ec9646beaf0c
size 19648968

16
Python-3.9.20.tar.xz.asc Normal file
View File

@ -0,0 +1,16 @@
-----BEGIN PGP SIGNATURE-----
iQIzBAABCAAdFiEE4/8oOcBIslwITevpsmmV4xAlBWgFAmbcKf0ACgkQsmmV4xAl
BWh4rg//R5E1EjsifYqhLeIyT+JnrBvbTZeEcdxPXevsgilojYmrxBUKuXXViul0
YZFaoDf6wjbHh6NMNgUpqcOH/5S/LsFZvuEcrw0jyGlMr0AMA4KLmNvQ9Wxf+wp4
mUmhymQx555nVivsdPiziNnDwubZeA870ZllYEMWP5vXw7p2LbnlZvn7A+LSKjqM
S/6xbiKYVexK3vHY/uG0xo4z24FySfvs0/PF11JfRJCxm9+bli7FmHOoFMwpOO6S
caZLok4987YWOcPIPY6h+o2sFhDqHs8POGKd8k+0KQNQs5UbEQ4t/eKgnaoATkGn
nfcAGXSjX5RSv5uXPzBUc0PulYo6EalIn1b5fu96La/FEg9GLMR/n9g75Fgm/j9L
QGYu/DSaastY/c7Ot4QVyB6pxbQKjM438yneQrjhKBILGla4Crh1k6yRCx93j/TH
hF9kiuRf7jtLIGTp0cnquELGnatmL1RhOySn/1Y+asMR+oK8d+XQab//w4VsAt7C
SIfVXg25PUgZoaiYj/qIjLK9vkcj/EZ1IacivP5qBWb3O1E8gzSV8Z9duGT8Ef3P
ch4M/pd6hefVVVfyCoazB3gwDs68O6U2BIRdYLRlet8AuKTBysQKFwOo3EcCMmJV
W20KutPnERCzt8jeJdzFd0z3po9mvxNTKDLYaABtNI6NN00LcsM=
=svjf
-----END PGP SIGNATURE-----

43
README.SUSE Normal file
View File

@ -0,0 +1,43 @@
Python 3 in SUSE
==============
* Subpackages *
Python 3 is split into several subpackages, based on external dependencies.
The main package 'python3' has soft dependencies on all subpackages needed to
assemble the standard library; however, these might not all be installed by default.
If you attempt to import a module that is currently not installed, an ImportError is thrown,
with instructions to install the missing subpackage. Installing the subpackage might result
in installing libraries that the subpackage requires to function.
* ensurepip *
The 'ensurepip' module from Python 3 standard library (PEP 453) is supposed to deploy
a bundled copy of the pip installer. This makes no sense in a managed distribution like SUSE.
Instead, you need to install package 'python3-pip'. Usually this will be installed automatically
with 'python3'.
Using 'ensurepip' when pip is not installed will result in an ImportError with instructions
to install 'python3-pip'.
* Documentation *
You can find documentation in seprarate packages: python3-doc and
python3-doc-pdf. These contan following documents:
Tutorial, What's New in Python, Global Module Index, Library Reference,
Macintosh Module Reference, Installing Python Modules, Distributing Python
Modules, Language Reference, Extending and Embedding, Python/C API,
Documenting Python
The python3-doc package constains many text files from source tarball.
* Interactive mode *
Interactive mode is by default enhanced with of history and command completion.
If you don't like these features, you can unset the PYTHONSTARTUP variable
in your .profile or disable it system wide in /etc/profile.d/python.sh.

4
_multibuild Normal file
View File

@ -0,0 +1,4 @@
<multibuild>
<package>base</package>
<package>doc</package>
</multibuild>

3
baselibs.conf Normal file
View File

@ -0,0 +1,3 @@
python39-base
python39
libpython3_9-1_0

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d030d6ff641577625745b435f4a45e9025e11143e60d0bba7dddf53e8bf71941
size 24976

View File

@ -0,0 +1,163 @@
From 5754521af1d51aa8e445cba07a093bbc0c88596d Mon Sep 17 00:00:00 2001
From: Zackery Spytz <zspytz@gmail.com>
Date: Mon, 16 Dec 2019 18:24:08 -0700
Subject: [PATCH] bpo-31046: ensurepip does not honour the value of $(prefix)
Co-Authored-By: Xavier de Gaye <xdegaye@gmail.com>
---
Doc/library/ensurepip.rst | 9 +++--
Lib/ensurepip/__init__.py | 18 +++++++---
Lib/test/test_ensurepip.py | 11 ++++++
Makefile.pre.in | 4 +-
Misc/NEWS.d/next/Build/2019-12-16-17-50-42.bpo-31046.XA-Qfr.rst | 1
5 files changed, 34 insertions(+), 9 deletions(-)
create mode 100644 Misc/NEWS.d/next/Build/2019-12-16-17-50-42.bpo-31046.XA-Qfr.rst
--- a/Doc/library/ensurepip.rst
+++ b/Doc/library/ensurepip.rst
@@ -56,8 +56,9 @@ is at least as recent as the one bundled
By default, ``pip`` is installed into the current virtual environment
(if one is active) or into the system site packages (if there is no
active virtual environment). The installation location can be controlled
-through two additional command line options:
+through some additional command line options:
+* ``--prefix <dir>``: Installs ``pip`` using the given directory prefix.
* ``--root <dir>``: Installs ``pip`` relative to the given root directory
rather than the root of the currently active virtual environment (if any)
or the default root for the current Python installation.
@@ -89,7 +90,7 @@ Module API
Returns a string specifying the bundled version of pip that will be
installed when bootstrapping an environment.
-.. function:: bootstrap(root=None, upgrade=False, user=False, \
+.. function:: bootstrap(root=None, prefix=None, upgrade=False, user=False, \
altinstall=False, default_pip=False, \
verbosity=0)
@@ -99,6 +100,8 @@ Module API
If *root* is ``None``, then installation uses the default install location
for the current environment.
+ *prefix* specifies the directory prefix to use when installing.
+
*upgrade* indicates whether or not to upgrade an existing installation
of an earlier version of ``pip`` to the bundled version.
@@ -119,6 +122,8 @@ Module API
*verbosity* controls the level of output to :data:`sys.stdout` from the
bootstrapping operation.
+ .. versionchanged:: 3.9 the *prefix* parameter was added.
+
.. audit-event:: ensurepip.bootstrap root ensurepip.bootstrap
.. note::
--- a/Lib/ensurepip/__init__.py
+++ b/Lib/ensurepip/__init__.py
@@ -57,27 +57,27 @@ def _disable_pip_configuration_settings(
os.environ['PIP_CONFIG_FILE'] = os.devnull
-def bootstrap(*, root=None, upgrade=False, user=False,
+def bootstrap(*, root=None, prefix=None, upgrade=False, user=False,
altinstall=False, default_pip=False,
verbosity=0):
"""
Bootstrap pip into the current Python installation (or the given root
- directory).
+ and directory prefix).
Note that calling this function will alter both sys.path and os.environ.
"""
# Discard the return value
- _bootstrap(root=root, upgrade=upgrade, user=user,
+ _bootstrap(root=root, prefix=prefix, upgrade=upgrade, user=user,
altinstall=altinstall, default_pip=default_pip,
verbosity=verbosity)
-def _bootstrap(*, root=None, upgrade=False, user=False,
+def _bootstrap(*, root=None, prefix=None, upgrade=False, user=False,
altinstall=False, default_pip=False,
verbosity=0):
"""
Bootstrap pip into the current Python installation (or the given root
- directory). Returns pip command status code.
+ and directory prefix). Returns pip command status code.
Note that calling this function will alter both sys.path and os.environ.
"""
@@ -120,6 +120,8 @@ def _bootstrap(*, root=None, upgrade=Fal
args = ["install", "--no-cache-dir", "--no-index", "--find-links", tmpdir]
if root:
args += ["--root", root]
+ if prefix:
+ args += ["--prefix", prefix]
if upgrade:
args += ["--upgrade"]
if user:
@@ -192,6 +194,11 @@ def _main(argv=None):
help="Install everything relative to this alternate root directory.",
)
parser.add_argument(
+ "--prefix",
+ default=None,
+ help="Install everything using this prefix.",
+ )
+ parser.add_argument(
"--altinstall",
action="store_true",
default=False,
@@ -210,6 +217,7 @@ def _main(argv=None):
return _bootstrap(
root=args.root,
+ prefix=args.prefix,
upgrade=args.upgrade,
user=args.user,
verbosity=args.verbosity,
--- a/Lib/test/test_ensurepip.py
+++ b/Lib/test/test_ensurepip.py
@@ -61,6 +61,17 @@ class TestBootstrap(EnsurepipMixin, unit
unittest.mock.ANY,
)
+ def test_bootstrapping_with_prefix(self):
+ ensurepip.bootstrap(prefix="/foo/bar/")
+ self.run_pip.assert_called_once_with(
+ [
+ "install", "--no-cache-dir", "--no-index", "--find-links",
+ unittest.mock.ANY, "--prefix", "/foo/bar/",
+ "setuptools", "pip",
+ ],
+ unittest.mock.ANY,
+ )
+
def test_bootstrapping_with_user(self):
ensurepip.bootstrap(user=True)
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -1263,7 +1263,7 @@ install: @FRAMEWORKINSTALLFIRST@ commoni
install|*) ensurepip="" ;; \
esac; \
$(RUNSHARED) $(PYTHON_FOR_BUILD) -m ensurepip \
- $$ensurepip --root=$(DESTDIR)/ ; \
+ $$ensurepip --root=$(DESTDIR)/ --prefix=$(prefix) ; \
fi
altinstall: commoninstall
@@ -1273,7 +1273,7 @@ altinstall: commoninstall
install|*) ensurepip="--altinstall" ;; \
esac; \
$(RUNSHARED) $(PYTHON_FOR_BUILD) -m ensurepip \
- $$ensurepip --root=$(DESTDIR)/ ; \
+ $$ensurepip --root=$(DESTDIR)/ --prefix=$(prefix) ; \
fi
commoninstall: check-clean-src @FRAMEWORKALTINSTALLFIRST@ \
--- /dev/null
+++ b/Misc/NEWS.d/next/Build/2019-12-16-17-50-42.bpo-31046.XA-Qfr.rst
@@ -0,0 +1 @@
+A directory prefix can now be specified when using :mod:`ensurepip`.

View File

@ -0,0 +1,102 @@
From 33d95c6facdfda3c8c0feffa7a99184e4abc2f63 Mon Sep 17 00:00:00 2001
From: Brandt Bucher <brandt@python.org>
Date: Wed, 25 Aug 2021 04:14:34 -0700
Subject: [PATCH] bpo-37596: Make `set` and `frozenset` marshalling
deterministic (GH-27926)
---
Lib/test/test_marshal.py | 26 ++++++++
Misc/NEWS.d/next/Library/2021-08-23-21-39-59.bpo-37596.ojRcwB.rst | 2
Python/marshal.c | 32 ++++++++++
3 files changed, 60 insertions(+)
create mode 100644 Misc/NEWS.d/next/Library/2021-08-23-21-39-59.bpo-37596.ojRcwB.rst
--- a/Lib/test/test_marshal.py
+++ b/Lib/test/test_marshal.py
@@ -1,4 +1,5 @@
from test import support
+from test.support.script_helper import assert_python_ok
import array
import io
import marshal
@@ -324,6 +325,31 @@ class BugsTestCase(unittest.TestCase):
for i in range(len(data)):
self.assertRaises(EOFError, marshal.loads, data[0: i])
+ def test_deterministic_sets(self):
+ # bpo-37596: To support reproducible builds, sets and frozensets need to
+ # have their elements serialized in a consistent order (even when they
+ # have been scrambled by hash randomization):
+ for kind in ("set", "frozenset"):
+ for elements in (
+ "float('nan'), b'a', b'b', b'c', 'x', 'y', 'z'",
+ # Also test for bad interactions with backreferencing:
+ "('string', 1), ('string', 2), ('string', 3)",
+ ):
+ s = f"{kind}([{elements}])"
+ with self.subTest(s):
+ # First, make sure that our test case still has different
+ # orders under hash seeds 0 and 1. If this check fails, we
+ # need to update this test with different elements:
+ args = ["-c", f"print({s})"]
+ _, repr_0, _ = assert_python_ok(*args, PYTHONHASHSEED="0")
+ _, repr_1, _ = assert_python_ok(*args, PYTHONHASHSEED="1")
+ self.assertNotEqual(repr_0, repr_1)
+ # Then, perform the actual test:
+ args = ["-c", f"import marshal; print(marshal.dumps({s}))"]
+ _, dump_0, _ = assert_python_ok(*args, PYTHONHASHSEED="0")
+ _, dump_1, _ = assert_python_ok(*args, PYTHONHASHSEED="1")
+ self.assertEqual(dump_0, dump_1)
+
LARGE_SIZE = 2**31
pointer_size = 8 if sys.maxsize > 0xFFFFFFFF else 4
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-08-23-21-39-59.bpo-37596.ojRcwB.rst
@@ -0,0 +1,2 @@
+Ensure that :class:`set` and :class:`frozenset` objects are always
+:mod:`marshalled <marshal>` reproducibly.
--- a/Python/marshal.c
+++ b/Python/marshal.c
@@ -502,9 +502,41 @@ w_complex_object(PyObject *v, char flag,
W_TYPE(TYPE_SET, p);
n = PySet_GET_SIZE(v);
W_SIZE(n, p);
+ // bpo-37596: To support reproducible builds, sets and frozensets need
+ // to have their elements serialized in a consistent order (even when
+ // they have been scrambled by hash randomization). To ensure this, we
+ // use an order equivalent to sorted(v, key=marshal.dumps):
+ PyObject *pairs = PyList_New(0);
+ if (pairs == NULL) {
+ p->error = WFERR_NOMEMORY;
+ return;
+ }
while (_PySet_NextEntry(v, &pos, &value, &hash)) {
+ PyObject *dump = PyMarshal_WriteObjectToString(value, p->version);
+ if (dump == NULL) {
+ p->error = WFERR_UNMARSHALLABLE;
+ goto anyset_done;
+ }
+ PyObject *pair = PyTuple_Pack(2, dump, value);
+ Py_DECREF(dump);
+ if (pair == NULL || PyList_Append(pairs, pair)) {
+ p->error = WFERR_NOMEMORY;
+ Py_XDECREF(pair);
+ goto anyset_done;
+ }
+ Py_DECREF(pair);
+ }
+ if (PyList_Sort(pairs)) {
+ p->error = WFERR_NOMEMORY;
+ goto anyset_done;
+ }
+ for (Py_ssize_t i = 0; i < n; i++) {
+ PyObject *pair = PyList_GET_ITEM(pairs, i);
+ value = PyTuple_GET_ITEM(pair, 1);
w_object(value, p);
}
+ anyset_done:
+ Py_DECREF(pairs);
}
else if (PyCode_Check(v)) {
PyCodeObject *co = (PyCodeObject *)v;

View File

@ -0,0 +1,37 @@
From ac2b8869724d7a57d9b5efbdce2f20423214e8bb Mon Sep 17 00:00:00 2001
From: "Bernhard M. Wiedemann" <bwiedemann@suse.de>
Date: Tue, 16 Jul 2024 21:39:33 +0200
Subject: [PATCH] Allow to override build date with SOURCE_DATE_EPOCH
to make builds reproducible.
See https://reproducible-builds.org/ for why this is good
and https://reproducible-builds.org/specs/source-date-epoch/
for the definition of this variable.
---
Doc/conf.py | 3 ++-
Doc/library/functions.rst | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
--- a/Doc/conf.py
+++ b/Doc/conf.py
@@ -80,7 +80,8 @@ html_short_title = '%s Documentation' %
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
-html_last_updated_fmt = '%b %d, %Y'
+html_time = int(os.environ.get('SOURCE_DATE_EPOCH', time.time()))
+html_last_updated_fmt = time.strftime('%b %d, %Y (%H:%M UTC)', time.gmtime(html_time))
# Path to find HTML templates.
templates_path = ['tools/templates']
--- a/Doc/library/functions.rst
+++ b/Doc/library/functions.rst
@@ -1254,7 +1254,7 @@ are always available. They are listed h
(where :func:`open` is declared), :mod:`os`, :mod:`os.path`, :mod:`tempfile`,
and :mod:`shutil`.
- .. audit-event:: open file,mode,flags open
+ .. audit-event:: open path,mode,flags open
The ``mode`` and ``flags`` arguments may have been modified or inferred from
the original call.

36
decimal.patch Normal file
View File

@ -0,0 +1,36 @@
---
Modules/_decimal/_decimal.c | 4 ++--
setup.py | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
--- a/Modules/_decimal/_decimal.c
+++ b/Modules/_decimal/_decimal.c
@@ -3279,7 +3279,7 @@ dec_format(PyObject *dec, PyObject *args
}
else {
size_t n = strlen(spec.dot);
- if (n > 1 || (n == 1 && !isascii((uchar)spec.dot[0]))) {
+ if (n > 1 || (n == 1 && !isascii((unsigned char)spec.dot[0]))) {
/* fix locale dependent non-ascii characters */
dot = dotsep_as_utf8(spec.dot);
if (dot == NULL) {
@@ -3288,7 +3288,7 @@ dec_format(PyObject *dec, PyObject *args
spec.dot = PyBytes_AS_STRING(dot);
}
n = strlen(spec.sep);
- if (n > 1 || (n == 1 && !isascii((uchar)spec.sep[0]))) {
+ if (n > 1 || (n == 1 && !isascii((unsigned char)spec.sep[0]))) {
/* fix locale dependent non-ascii characters */
sep = dotsep_as_utf8(spec.sep);
if (sep == NULL) {
--- a/setup.py
+++ b/setup.py
@@ -2205,7 +2205,7 @@ class PyBuildExt(build_ext):
undef_macros = []
if '--with-system-libmpdec' in sysconfig.get_config_var("CONFIG_ARGS"):
include_dirs = []
- libraries = [':libmpdec.so.2']
+ libraries = ['mpdec']
sources = ['_decimal/_decimal.c']
depends = ['_decimal/docstrings.h']
else:

View File

@ -0,0 +1,11 @@
--- a/Lib/distutils/util.py
+++ b/Lib/distutils/util.py
@@ -433,7 +433,7 @@ byte_compile(files, optimize=%r, force=%
else:
from py_compile import compile
- for file in py_files:
+ for file in sorted(py_files):
if file[-3:] != ".py":
# This lets us be lazy and not filter filenames in
# the "install_lib" command.

View File

@ -0,0 +1,90 @@
---
Doc/library/tarfile.rst | 11 -----------
1 file changed, 11 deletions(-)
--- a/Doc/library/tarfile.rst
+++ b/Doc/library/tarfile.rst
@@ -504,7 +504,6 @@ be finalized; only the internally used f
Return an :class:`io.BufferedReader` object.
.. attribute:: TarFile.errorlevel
- :type: int
If *errorlevel* is ``0``, errors are ignored when using :meth:`TarFile.extract`
and :meth:`TarFile.extractall`.
@@ -683,19 +682,16 @@ A ``TarInfo`` object has the following p
.. attribute:: TarInfo.name
- :type: str
Name of the archive member.
.. attribute:: TarInfo.size
- :type: int
Size in bytes.
.. attribute:: TarInfo.mtime
- :type: int | float
Time of last modification in seconds since the :ref:`epoch <epoch>`,
as in :attr:`os.stat_result.st_mtime`.
@@ -707,7 +703,6 @@ A ``TarInfo`` object has the following p
attribute.
.. attribute:: TarInfo.mode
- :type: int
Permission bits, as for :func:`os.chmod`.
@@ -727,7 +722,6 @@ A ``TarInfo`` object has the following p
.. attribute:: TarInfo.linkname
- :type: str
Name of the target file name, which is only present in :class:`TarInfo` objects
of type :const:`LNKTYPE` and :const:`SYMTYPE`.
@@ -739,7 +733,6 @@ A ``TarInfo`` object has the following p
.. attribute:: TarInfo.uid
- :type: int
User ID of the user who originally stored this member.
@@ -750,7 +743,6 @@ A ``TarInfo`` object has the following p
attribute.
.. attribute:: TarInfo.gid
- :type: int
Group ID of the user who originally stored this member.
@@ -761,7 +753,6 @@ A ``TarInfo`` object has the following p
attribute.
.. attribute:: TarInfo.uname
- :type: str
User name.
@@ -772,7 +763,6 @@ A ``TarInfo`` object has the following p
attribute.
.. attribute:: TarInfo.gname
- :type: str
Group name.
@@ -783,7 +773,6 @@ A ``TarInfo`` object has the following p
attribute.
.. attribute:: TarInfo.pax_headers
- :type: dict
A dictionary containing key-value pairs of an associated pax extended header.

View File

@ -0,0 +1,28 @@
From 6c8ea7c1dacd42f3ba00440231ec0e6b1a38300d Mon Sep 17 00:00:00 2001
From: Inada Naoki <songofacandy@gmail.com>
Date: Sat, 14 Jul 2018 00:46:11 +0900
Subject: [PATCH] Use FLAG_REF always for interned strings
---
Python/marshal.c | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
--- a/Python/marshal.c
+++ b/Python/marshal.c
@@ -298,9 +298,14 @@ w_ref(PyObject *v, char *flag, WFILE *p)
if (p->version < 3 || p->hashtable == NULL)
return 0; /* not writing object references */
- /* if it has only one reference, it definitely isn't shared */
- if (Py_REFCNT(v) == 1)
+ /* If it has only one reference, it definitely isn't shared.
+ * But we use TYPE_REF always for interned string, to PYC file stable
+ * as possible.
+ */
+ if (Py_REFCNT(v) == 1 &&
+ !(PyUnicode_CheckExact(v) && PyUnicode_CHECK_INTERNED(v))) {
return 0;
+ }
entry = _Py_hashtable_get_entry(p->hashtable, v);
if (entry != NULL) {

View File

@ -0,0 +1,35 @@
From 1b3f6523a5c83323cdc44031b33a1c062e5dc698 Mon Sep 17 00:00:00 2001
From: Xi Ruoyao <xry111@xry111.site>
Date: Fri, 7 Jun 2024 23:51:32 +0800
Subject: [PATCH] gh-120226: Fix
test_sendfile_close_peer_in_the_middle_of_receiving on Linux >= 6.10
(GH-120227)
The worst case is that the kernel buffers 17 pages with a page size of 64k.
(cherry picked from commit a7584245661102a5768c643fbd7db8395fd3c90e)
Co-authored-by: Xi Ruoyao <xry111@xry111.site>
---
Lib/test/test_asyncio/test_sendfile.py | 11 ++++-------
1 file changed, 4 insertions(+), 7 deletions(-)
--- a/Lib/test/test_asyncio/test_sendfile.py
+++ b/Lib/test/test_asyncio/test_sendfile.py
@@ -87,13 +87,10 @@ class MyProto(asyncio.Protocol):
class SendfileBase:
- # 256 KiB plus small unaligned to buffer chunk
- # Newer versions of Windows seems to have increased its internal
- # buffer and tries to send as much of the data as it can as it
- # has some form of buffering for this which is less than 256KiB
- # on newer server versions and Windows 11.
- # So DATA should be larger than 256 KiB to make this test reliable.
- DATA = b"x" * (1024 * 256 + 1)
+ # Linux >= 6.10 seems buffering up to 17 pages of data.
+ # So DATA should be large enough to make this test reliable even with a
+ # 64 KiB page configuration.
+ DATA = b"x" * (1024 * 17 * 64 + 1)
# Reduce socket buffer size to test on relative small data sets.
BUF_SIZE = 4 * 1024 # 4 KiB

35
idle3.appdata.xml Normal file
View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2017 Zbigniew Jędrzejewski-Szmek -->
<application>
<id type="desktop">idle3.desktop</id>
<name>IDLE3</name>
<metadata_licence>CC0</metadata_licence>
<project_license>Python-2.0</project_license>
<summary>Python 3 Integrated Development and Learning Environment</summary>
<description>
<p>
IDLE is Pythons Integrated Development and Learning Environment.
The GUI is uniform between Windows, Unix, and Mac OS X.
IDLE provides an easy way to start writing, running, and debugging
Python code.
</p>
<p>
IDLE is written in pure Python, and uses the tkinter GUI toolkit.
It provides:
</p>
<ul>
<li>a Python shell window (interactive interpreter) with colorizing of code input, output, and error messages,</li>
<li>a multi-window text editor with multiple undo, Python colorizing, smart indent, call tips, auto completion, and other features,</li>
<li>search within any window, replace within editor windows, and search through multiple files (grep),</li>
<li>a debugger with persistent breakpoints, stepping, and viewing of global and local namespaces.</li>
</ul>
</description>
<url type="homepage">https://docs.python.org/3/library/idle.html</url>
<screenshots>
<screenshot type="default">http://in.waw.pl/~zbyszek/fedora/idle3-appdata/idle3-main-window.png</screenshot>
<screenshot>http://in.waw.pl/~zbyszek/fedora/idle3-appdata/idle3-class-browser.png</screenshot>
<screenshot>http://in.waw.pl/~zbyszek/fedora/idle3-appdata/idle3-code-viewer.png</screenshot>
</screenshots>
<update_contact>zbyszek@in.waw.pl</update_contact>
</application>

12
idle3.desktop Normal file
View File

@ -0,0 +1,12 @@
[Desktop Entry]
Version=1.0
Name=IDLE 3
GenericName=Python 3 IDE
Comment=Python 3 Integrated Development and Learning Environment
Exec=idle3 %F
TryExec=idle3
Terminal=false
Type=Application
Icon=idle3
Categories=Development;IDE;
MimeType=text/x-python;

7
import_failed.map Normal file
View File

@ -0,0 +1,7 @@
python39-curses: curses _curses _curses_panel
python39-dbm: dbm _dbm _gdbm
python39-idle: idlelib
python39-testsuite: test _ctypes_test _testbuffer _testcapi _testinternalcapi _testimportmultiple _testmultiphase xxlimited
python39-tk: tkinter _tkinter
python39-tools: turtledemo
python39: sqlite3 readline _sqlite3 nis

23
import_failed.py Normal file
View File

@ -0,0 +1,23 @@
import sys, os
from sysconfig import get_path
failed_map_path = os.path.join(get_path('stdlib'), '_import_failed', 'import_failed.map')
if __spec__:
failed_name = __spec__.name
else:
failed_name = __name__
with open(failed_map_path) as fd:
for line in fd:
package = line.split(':')[0]
imports = line.split(':')[1]
if failed_name in imports:
raise ImportError(f"""Module '{failed_name}' is not installed.
Use:
sudo zypper install {package}
to install it.""")
raise ImportError(f"""Module '{failed_name}' is not installed.
It is supposed to be part of python3 distribution, but missing from failed import map.
Please file a bug on the SUSE Bugzilla.""")

28
macros.python3 Normal file
View File

@ -0,0 +1,28 @@
%have_python3 1
# commented out legacy macro definitions
#py3_prefix /usr
#py3_incdir /usr/include/python3.5m
#py3_ver 3.5
# these should now be provided by macros.python_all
#python3_sitearch /usr/lib64/python3.5/site-packages
#python3_sitelib /usr/lib/python3.5/site-packages
#python3_version 3.5
# hard to say if anyone ever used these?
#py3_soflags cpython-35m-x86_64-linux-gnu
#py3_abiflags m
%cpython3_soabi %(python3 -c "import sysconfig; print(sysconfig.get_config_var('SOABI'))")
%py3_soflags %cpython3_soabi
# compilation macros that might be in use somewhere
%py3_compile(O) \
find %1 -name '*.pyc' -exec rm -f {} ";"\
python3 -c "import sys, os, compileall; br='%{buildroot}'; compileall.compile_dir(sys.argv[1], ddir=br and (sys.argv[1][len(os.path.abspath(br)):]+'/') or None)" %1\
%{-O:\
find %1 -name '*.pyo' -exec rm -f {} ";"\
python3 -O -c "import sys, os, compileall; br='%{buildroot}'; compileall.compile_dir(sys.argv[1], ddir=br and (sys.argv[1][len(os.path.abspath(br)):]+'/') or None)" %1\
}

665
no-skipif-doctests.patch Normal file
View File

@ -0,0 +1,665 @@
only in patch2:
unchanged:
---
Doc/conf.py | 2 -
Doc/library/turtle.rst | 82 -------------------------------------------------
2 files changed, 1 insertion(+), 83 deletions(-)
--- a/Doc/conf.py
+++ b/Doc/conf.py
@@ -46,7 +46,7 @@ today_fmt = '%B %d, %Y'
highlight_language = 'python3'
# Minimum version of sphinx required
-needs_sphinx = '1.8'
+needs_sphinx = '1.7.6'
# Ignore any .rst files in the venv/ directory.
exclude_patterns = ['venv/*', 'README.rst']
--- a/Doc/library/turtle.rst
+++ b/Doc/library/turtle.rst
@@ -250,7 +250,6 @@ Turtle motion
turtle is headed.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.position()
(0.00,0.00)
@@ -277,7 +276,6 @@ Turtle motion
>>> turtle.goto(0, 0)
.. doctest::
- :skipif: _tkinter is None
>>> turtle.position()
(0.00,0.00)
@@ -296,13 +294,11 @@ Turtle motion
orientation depends on the turtle mode, see :func:`mode`.
.. doctest::
- :skipif: _tkinter is None
:hide:
>>> turtle.setheading(22)
.. doctest::
- :skipif: _tkinter is None
>>> turtle.heading()
22.0
@@ -321,13 +317,11 @@ Turtle motion
orientation depends on the turtle mode, see :func:`mode`.
.. doctest::
- :skipif: _tkinter is None
:hide:
>>> turtle.setheading(22)
.. doctest::
- :skipif: _tkinter is None
>>> turtle.heading()
22.0
@@ -350,13 +344,11 @@ Turtle motion
not change the turtle's orientation.
.. doctest::
- :skipif: _tkinter is None
:hide:
>>> turtle.goto(0, 0)
.. doctest::
- :skipif: _tkinter is None
>>> tp = turtle.pos()
>>> tp
@@ -380,13 +372,11 @@ Turtle motion
unchanged.
.. doctest::
- :skipif: _tkinter is None
:hide:
>>> turtle.goto(0, 240)
.. doctest::
- :skipif: _tkinter is None
>>> turtle.position()
(0.00,240.00)
@@ -402,13 +392,11 @@ Turtle motion
Set the turtle's second coordinate to *y*, leave first coordinate unchanged.
.. doctest::
- :skipif: _tkinter is None
:hide:
>>> turtle.goto(0, 40)
.. doctest::
- :skipif: _tkinter is None
>>> turtle.position()
(0.00,40.00)
@@ -435,7 +423,6 @@ Turtle motion
=================== ====================
.. doctest::
- :skipif: _tkinter is None
>>> turtle.setheading(90)
>>> turtle.heading()
@@ -448,14 +435,12 @@ Turtle motion
its start-orientation (which depends on the mode, see :func:`mode`).
.. doctest::
- :skipif: _tkinter is None
:hide:
>>> turtle.setheading(90)
>>> turtle.goto(0, -10)
.. doctest::
- :skipif: _tkinter is None
>>> turtle.heading()
90.0
@@ -487,7 +472,6 @@ Turtle motion
calculated automatically. May be used to draw regular polygons.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.home()
>>> turtle.position()
@@ -516,7 +500,6 @@ Turtle motion
.. doctest::
- :skipif: _tkinter is None
>>> turtle.home()
>>> turtle.dot()
@@ -534,7 +517,6 @@ Turtle motion
it by calling ``clearstamp(stamp_id)``.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.color("blue")
>>> turtle.stamp()
@@ -550,7 +532,6 @@ Turtle motion
Delete stamp with given *stampid*.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.position()
(150.00,-0.00)
@@ -595,7 +576,6 @@ Turtle motion
undo actions is determined by the size of the undobuffer.
.. doctest::
- :skipif: _tkinter is None
>>> for i in range(4):
... turtle.fd(50); turtle.lt(80)
@@ -628,7 +608,6 @@ Turtle motion
turtle turn instantly.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.speed()
3
@@ -649,7 +628,6 @@ Tell Turtle's state
Return the turtle's current location (x,y) (as a :class:`Vec2D` vector).
.. doctest::
- :skipif: _tkinter is None
>>> turtle.pos()
(440.00,-0.00)
@@ -665,7 +643,6 @@ Tell Turtle's state
orientation which depends on the mode - "standard"/"world" or "logo".
.. doctest::
- :skipif: _tkinter is None
>>> turtle.goto(10, 10)
>>> turtle.towards(0,0)
@@ -677,7 +654,6 @@ Tell Turtle's state
Return the turtle's x coordinate.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.home()
>>> turtle.left(50)
@@ -693,7 +669,6 @@ Tell Turtle's state
Return the turtle's y coordinate.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.home()
>>> turtle.left(60)
@@ -710,7 +685,6 @@ Tell Turtle's state
:func:`mode`).
.. doctest::
- :skipif: _tkinter is None
>>> turtle.home()
>>> turtle.left(67)
@@ -727,7 +701,6 @@ Tell Turtle's state
other turtle, in turtle step units.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.home()
>>> turtle.distance(30,40)
@@ -751,7 +724,6 @@ Settings for measurement
Default value is 360 degrees.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.home()
>>> turtle.left(90)
@@ -774,7 +746,6 @@ Settings for measurement
``degrees(2*math.pi)``.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.home()
>>> turtle.left(90)
@@ -785,7 +756,6 @@ Settings for measurement
1.5707963267948966
.. doctest::
- :skipif: _tkinter is None
:hide:
>>> turtle.degrees(360)
@@ -821,7 +791,6 @@ Drawing state
thickness. If no argument is given, the current pensize is returned.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.pensize()
1
@@ -853,7 +822,6 @@ Drawing state
attributes in one statement.
.. doctest::
- :skipif: _tkinter is None
:options: +NORMALIZE_WHITESPACE
>>> turtle.pen(fillcolor="black", pencolor="red", pensize=10)
@@ -876,7 +844,6 @@ Drawing state
Return ``True`` if pen is down, ``False`` if it's up.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.penup()
>>> turtle.isdown()
@@ -917,7 +884,6 @@ Color control
newly set pencolor.
.. doctest::
- :skipif: _tkinter is None
>>> colormode()
1.0
@@ -966,7 +932,6 @@ Color control
with the newly set fillcolor.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.fillcolor("violet")
>>> turtle.fillcolor()
@@ -1005,7 +970,6 @@ Color control
with the newly set colors.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.color("red", "green")
>>> turtle.color()
@@ -1022,7 +986,6 @@ Filling
~~~~~~~
.. doctest::
- :skipif: _tkinter is None
:hide:
>>> turtle.home()
@@ -1032,7 +995,6 @@ Filling
Return fillstate (``True`` if filling, ``False`` else).
.. doctest::
- :skipif: _tkinter is None
>>> turtle.begin_fill()
>>> if turtle.filling():
@@ -1057,7 +1019,6 @@ Filling
above may be either all yellow or have some white regions.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.color("black", "red")
>>> turtle.begin_fill()
@@ -1074,7 +1035,6 @@ More drawing control
variables to the default values.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.goto(0,-22)
>>> turtle.left(100)
@@ -1125,7 +1085,6 @@ Visibility
drawing observably.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.hideturtle()
@@ -1136,7 +1095,6 @@ Visibility
Make the turtle visible.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.showturtle()
@@ -1167,7 +1125,6 @@ Appearance
deal with shapes see Screen method :func:`register_shape`.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.shape()
'classic'
@@ -1193,7 +1150,6 @@ Appearance
``resizemode("user")`` is called by :func:`shapesize` when used with arguments.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.resizemode()
'noresize'
@@ -1217,7 +1173,6 @@ Appearance
of the shapes's outline.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.shapesize()
(1.0, 1.0, 1)
@@ -1242,7 +1197,6 @@ Appearance
heading of the turtle are sheared.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.shape("circle")
>>> turtle.shapesize(5,2)
@@ -1259,7 +1213,6 @@ Appearance
change the turtle's heading (direction of movement).
.. doctest::
- :skipif: _tkinter is None
>>> turtle.reset()
>>> turtle.shape("circle")
@@ -1279,7 +1232,6 @@ Appearance
(direction of movement).
.. doctest::
- :skipif: _tkinter is None
>>> turtle.reset()
>>> turtle.shape("circle")
@@ -1305,7 +1257,6 @@ Appearance
turtle (its direction of movement).
.. doctest::
- :skipif: _tkinter is None
>>> turtle.reset()
>>> turtle.shape("circle")
@@ -1334,7 +1285,6 @@ Appearance
given matrix.
.. doctest::
- :skipif: _tkinter is None
>>> turtle = Turtle()
>>> turtle.shape("square")
@@ -1350,7 +1300,6 @@ Appearance
can be used to define a new shape or components of a compound shape.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.shape("square")
>>> turtle.shapetransform(4, -1, 0, 2)
@@ -1375,7 +1324,6 @@ Using events
procedural way:
.. doctest::
- :skipif: _tkinter is None
>>> def turn(x, y):
... left(180)
@@ -1396,7 +1344,6 @@ Using events
``None``, existing bindings are removed.
.. doctest::
- :skipif: _tkinter is None
>>> class MyTurtle(Turtle):
... def glow(self,x,y):
@@ -1424,7 +1371,6 @@ Using events
mouse-click event on that turtle.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.ondrag(turtle.goto)
@@ -1452,7 +1398,6 @@ Special Turtle methods
Return the last recorded polygon.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.home()
>>> turtle.begin_poly()
@@ -1472,7 +1417,6 @@ Special Turtle methods
turtle properties.
.. doctest::
- :skipif: _tkinter is None
>>> mick = Turtle()
>>> joe = mick.clone()
@@ -1485,7 +1429,6 @@ Special Turtle methods
return the "anonymous turtle":
.. doctest::
- :skipif: _tkinter is None
>>> pet = getturtle()
>>> pet.fd(50)
@@ -1499,7 +1442,6 @@ Special Turtle methods
TurtleScreen methods can then be called for that object.
.. doctest::
- :skipif: _tkinter is None
>>> ts = turtle.getscreen()
>>> ts
@@ -1517,7 +1459,6 @@ Special Turtle methods
``None``, the undobuffer is disabled.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.setundobuffer(42)
@@ -1527,7 +1468,6 @@ Special Turtle methods
Return number of entries in the undobuffer.
.. doctest::
- :skipif: _tkinter is None
>>> while undobufferentries():
... undo()
@@ -1550,7 +1490,6 @@ below:
For example:
.. doctest::
- :skipif: _tkinter is None
>>> s = Shape("compound")
>>> poly1 = ((0,0),(10,-5),(0,10),(-10,-5))
@@ -1561,7 +1500,6 @@ below:
3. Now add the Shape to the Screen's shapelist and use it:
.. doctest::
- :skipif: _tkinter is None
>>> register_shape("myshape", s)
>>> shape("myshape")
@@ -1581,7 +1519,6 @@ Most of the examples in this section ref
``screen``.
.. doctest::
- :skipif: _tkinter is None
:hide:
>>> screen = Screen()
@@ -1598,7 +1535,6 @@ Window control
Set or return background color of the TurtleScreen.
.. doctest::
- :skipif: _tkinter is None
>>> screen.bgcolor("orange")
>>> screen.bgcolor()
@@ -1690,7 +1626,6 @@ Window control
distorted.
.. doctest::
- :skipif: _tkinter is None
>>> screen.reset()
>>> screen.setworldcoordinates(-50,-7.5,50,7.5)
@@ -1701,7 +1636,6 @@ Window control
... left(45); fd(2) # a regular octagon
.. doctest::
- :skipif: _tkinter is None
:hide:
>>> screen.reset()
@@ -1723,7 +1657,6 @@ Animation control
Optional argument:
.. doctest::
- :skipif: _tkinter is None
>>> screen.delay()
10
@@ -1745,7 +1678,6 @@ Animation control
:func:`delay`).
.. doctest::
- :skipif: _tkinter is None
>>> screen.tracer(8, 25)
>>> dist = 2
@@ -1782,7 +1714,6 @@ Using screen events
must have the focus. (See method :func:`listen`.)
.. doctest::
- :skipif: _tkinter is None
>>> def f():
... fd(50)
@@ -1803,7 +1734,6 @@ Using screen events
must have focus. (See method :func:`listen`.)
.. doctest::
- :skipif: _tkinter is None
>>> def f():
... fd(50)
@@ -1828,7 +1758,6 @@ Using screen events
named ``turtle``:
.. doctest::
- :skipif: _tkinter is None
>>> screen.onclick(turtle.goto) # Subsequently clicking into the TurtleScreen will
>>> # make the turtle move to the clicked point.
@@ -1848,7 +1777,6 @@ Using screen events
Install a timer that calls *fun* after *t* milliseconds.
.. doctest::
- :skipif: _tkinter is None
>>> running = True
>>> def f():
@@ -1930,7 +1858,6 @@ Settings and special methods
============ ========================= ===================
.. doctest::
- :skipif: _tkinter is None
>>> mode("logo") # resets turtle heading to north
>>> mode()
@@ -1945,7 +1872,6 @@ Settings and special methods
values of color triples have to be in the range 0..\ *cmode*.
.. doctest::
- :skipif: _tkinter is None
>>> screen.colormode(1)
>>> turtle.pencolor(240, 160, 80)
@@ -1966,7 +1892,6 @@ Settings and special methods
do with a Tkinter Canvas.
.. doctest::
- :skipif: _tkinter is None
>>> cv = screen.getcanvas()
>>> cv
@@ -1978,7 +1903,6 @@ Settings and special methods
Return a list of names of all currently available turtle shapes.
.. doctest::
- :skipif: _tkinter is None
>>> screen.getshapes()
['arrow', 'blank', 'circle', ..., 'turtle']
@@ -2002,7 +1926,6 @@ Settings and special methods
coordinates: Install the corresponding polygon shape.
.. doctest::
- :skipif: _tkinter is None
>>> screen.register_shape("triangle", ((5,-3), (0,5), (-5,-3)))
@@ -2018,7 +1941,6 @@ Settings and special methods
Return the list of turtles on the screen.
.. doctest::
- :skipif: _tkinter is None
>>> for turtle in screen.turtles():
... turtle.color("red")
@@ -2080,7 +2002,6 @@ Methods specific to Screen, not inherite
center window vertically
.. doctest::
- :skipif: _tkinter is None
>>> screen.setup (width=200, height=200, startx=0, starty=0)
>>> # sets window to 200x200 pixels, in upper left of screen
@@ -2096,7 +2017,6 @@ Methods specific to Screen, not inherite
Set title of turtle window to *titlestring*.
.. doctest::
- :skipif: _tkinter is None
>>> screen.title("Welcome to the turtle zoo!")
@@ -2167,7 +2087,6 @@ Public classes
Example:
.. doctest::
- :skipif: _tkinter is None
>>> poly = ((0,0),(10,-5),(0,10),(-10,-5))
>>> s = Shape("compound")
@@ -2514,7 +2433,6 @@ Changes since Python 3.0
.. doctest::
- :skipif: _tkinter is None
:hide:
>>> for turtle in turtles():

78
pre_checkin.sh Normal file
View File

@ -0,0 +1,78 @@
#!/bin/bash
export LC_ALL=C
master=python*.spec
# create import_failed.map from package definitions
pkgname=$(grep python_pkg_name $master |grep define |awk -F' ' '{print $3}')
MAPFILE=import_failed.map
function new_map_line () {
package=$1
package=$(echo $1 |sed -e "s:%{python_pkg_name}:$pkgname:")
modules=$2
if [ -z "$package" -o -z "$modules" ]; then
return
fi
if [[ "$package" =~ "-base" ]]; then
return
fi
echo "$package:$modules" >> $MAPFILE.tmp
}
for spec in *.spec; do
basename=${spec%.spec}
package=
modules=
while read line; do
case $line in
"%files -n "*)
new_map_line $package "$modules"
package=${line#"%files -n "}
modules=
;;
"%files "*)
new_map_line $package "$modules"
package=$basename-${line#"%files "}
modules=
;;
"%files")
new_map_line $package "$modules"
package=$basename
modules=
;;
"%{sitedir}/config-"*)
# ignore
;;
"%{sitedir}/"*)
word=${line#"%{sitedir}/"}
if ! echo $word | grep -q /; then
modules="$modules $word"
fi
;;
"%{dynlib "*"}")
word=${line#"%{dynlib "}
word=${word%"}"}
modules="$modules $word"
;;
esac
done < $spec
new_map_line $package "$modules"
done
cat $MAPFILE.tmp |sort -u > $MAPFILE
rm $MAPFILE.tmp
# run test inclusion check
tar xJf Python-*.xz
python3 skipped_tests.py
# generate baselibs.conf
VERSION=$(grep ^Version $master|awk -F':' '{print $2}' |sed -e 's/ //g')
python_version=${VERSION:0:3} # 3.3
python_version_abitag=${python_version//./} # 33
python_version_soname=${python_version//./_} # 3_3
echo "$pkgname-base" > baselibs.conf
echo "$pkgname" >> baselibs.conf
echo "libpython$python_version_soname-1_0" >> baselibs.conf

View File

@ -0,0 +1,25 @@
---
Makefile.pre.in | 7 +++++++
1 file changed, 7 insertions(+)
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -781,11 +781,18 @@ Modules/getbuildinfo.o: $(PARSER_OBJS) \
$(DTRACE_OBJS) \
$(srcdir)/Modules/getbuildinfo.c
$(CC) -c $(PY_CORE_CFLAGS) \
+ -DDATE="\"`date -u -r Makefile.pre.in +"%b %d %Y"`\"" \
+ -DTIME="\"`date -u -r Makefile.pre.in +"%T"`\"" \
-DGITVERSION="\"`LC_ALL=C $(GITVERSION)`\"" \
-DGITTAG="\"`LC_ALL=C $(GITTAG)`\"" \
-DGITBRANCH="\"`LC_ALL=C $(GITBRANCH)`\"" \
-o $@ $(srcdir)/Modules/getbuildinfo.c
+Python/getcompiler.o: $(srcdir)/Python/getcompiler.c Makefile
+ $(CC) -c $(PY_CORE_CFLAGS) \
+ -DCOMPILER='"[GCC]"' \
+ -o $@ $(srcdir)/Python/getcompiler.c
+
Modules/getpath.o: $(srcdir)/Modules/getpath.c Makefile
$(CC) -c $(PY_CORE_CFLAGS) -DPYTHONPATH='"$(PYTHONPATH)"' \
-DPREFIX='"$(prefix)"' \

View File

@ -0,0 +1,15 @@
---
Lib/site.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
--- a/Lib/site.py
+++ b/Lib/site.py
@@ -77,7 +77,7 @@ import io
import stat
# Prefixes for site-packages; add additional prefixes like /usr/local here
-PREFIXES = [sys.prefix, sys.exec_prefix]
+PREFIXES = [sys.prefix, sys.exec_prefix, '/usr/local']
# Enable per user site-packages directory
# set it to False to disable the feature or True to force the feature
ENABLE_USER_SITE = None

View File

@ -0,0 +1,15 @@
---
Lib/test/test_posix.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
--- a/Lib/test/test_posix.py
+++ b/Lib/test/test_posix.py
@@ -422,7 +422,7 @@ class PosixTester(unittest.TestCase):
def test_posix_fadvise(self):
fd = os.open(support.TESTFN, os.O_RDONLY)
try:
- posix.posix_fadvise(fd, 0, 0, posix.POSIX_FADV_WILLNEED)
+ posix.posix_fadvise(fd, 0, 0, posix.POSIX_FADV_RANDOM)
finally:
os.close(fd)

1008
python.keyring Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,53 @@
From 7bd6f0e5500f778e940374237b94651f60ae1990 Mon Sep 17 00:00:00 2001
From: "Miss Islington (bot)"
<31488909+miss-islington@users.noreply.github.com>
Date: Fri, 6 Jul 2018 21:00:45 -0700
Subject: [PATCH] closes bpo-34056: Always return bytes from
_HackedGetData.get_data(). (GH-8130)
* Always return bytes from _HackedGetData.get_data().
Ensure the imp.load_source shim always returns bytes by reopening the file in
binary mode if needed. Hash-based pycs have to receive the source code in bytes.
It's tempting to change imp.get_suffixes() to always return 'rb' as a mode, but
that breaks some stdlib tests and likely 3rdparty code, too.
(cherry picked from commit b0274f2cddd36b49fe5080efbe160277ef546471)
Co-authored-by: Benjamin Peterson <benjamin@python.org>
---
Lib/imp.py | 13 ++++++-------
Lib/test/test_imp.py | 15 +++++++++++++++
.../2018-07-05-22-45-46.bpo-34056.86isrU.rst | 3 +++
3 files changed, 24 insertions(+), 7 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2018-07-05-22-45-46.bpo-34056.86isrU.rst
--- a/Lib/test/test_imp.py
+++ b/Lib/test/test_imp.py
@@ -376,6 +376,20 @@ class ImportTests(unittest.TestCase):
mod = imp.load_module('mymod', file, path, description)
self.assertEqual(mod.x, 42)
+ def test_find_and_load_checked_pyc(self):
+ # issue 34056
+ with support.temp_cwd():
+ with open('mymod.py', 'wb') as fp:
+ fp.write(b'x = 42\n')
+ py_compile.compile(
+ 'mymod.py',
+ doraise=True,
+ invalidation_mode=py_compile.PycInvalidationMode.CHECKED_HASH,
+ )
+ file, path, description = imp.find_module('mymod', path=['.'])
+ mod = imp.load_module('mymod', file, path, description)
+ self.assertEqual(mod.x, 42)
+
class ReloadTests(unittest.TestCase):
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2018-07-05-22-45-46.bpo-34056.86isrU.rst
@@ -0,0 +1,3 @@
+Ensure the loader shim created by ``imp.load_module`` always returns bytes
+from its ``get_data()`` function. This fixes using ``imp.load_module`` with
+:pep:`552` hash-based pycs.

3892
python39.changes Normal file

File diff suppressed because it is too large Load Diff

1033
python39.spec Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,14 @@
---
Lib/test/test_capi.py | 1 +
1 file changed, 1 insertion(+)
--- a/Lib/test/test_capi.py
+++ b/Lib/test/test_capi.py
@@ -794,6 +794,7 @@ class PyMemDebugTests(unittest.TestCase)
def test_pyobject_forbidden_bytes_is_freed(self):
self.check_pyobject_is_freed('check_pyobject_forbidden_bytes_is_freed')
+ @unittest.skip('Failing on Leap 15.*')
def test_pyobject_freed_is_freed(self):
self.check_pyobject_is_freed('check_pyobject_freed_is_freed')

69
skipped_tests.py Normal file
View File

@ -0,0 +1,69 @@
#!/usr/bin/python3
"""
Simple regexp-based skipped test checker.
It lists tests that are mentioned (presumably for exclusion)
in BASE, and in MAIN (presumably for inclusion)
and reports discrepancies.
This will have a number of
"""
MAIN = "python39.spec"
import glob
import re
from os.path import basename
alltests = set()
qemu_exclusions = set()
for item in glob.glob("Python-*/Lib/test/test_*"):
testname = basename(item)
if testname.endswith(".py"):
testname = testname[:-3]
alltests.add(testname)
testre = re.compile(r'[\s"](test_\w+)\b')
def find_tests_in_spec(specname):
global qemu_exclusions
found_tests = set()
with open(specname) as spec:
in_qemu = False
for line in spec:
line = line.strip()
if "#" in line:
line = line[:line.index("#")]
tests = set(testre.findall(line))
found_tests |= tests
if line == "%if 0%{?qemu_user_space_build} > 0":
in_qemu = True
if in_qemu:
if line == "%endif":
in_qemu = False
qemu_exclusions |= tests
return found_tests
excluded = find_tests_in_spec(MAIN)
#print("--- excluded tests:", " ".join(sorted(excluded)))
#print("--- included tests:", " ".join(sorted(included)))
mentioned = excluded
nonexistent = mentioned - alltests
missing = excluded - qemu_exclusions
print("--- the following tests are excluded for QEMU and not tested in python")
print("--- (that probably means we don't need to worry about them)")
for test in sorted(qemu_exclusions - excluded):
print(test)
print("--- the following tests might be excluded in python:")
for test in sorted(missing):
print(test)
if nonexistent:
print("--- the following tests don't exist:")
for test in sorted(nonexistent):
print(test)

View File

@ -0,0 +1,20 @@
---
Doc/tools/extensions/pyspecific.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
--- a/Doc/tools/extensions/pyspecific.py
+++ b/Doc/tools/extensions/pyspecific.py
@@ -385,7 +385,12 @@ class DeprecatedRemoved(Directive):
translatable=False)
node.append(para)
env = self.state.document.settings.env
- env.get_domain('changeset').note_changeset(node)
+ # deprecated pre-Sphinx-2 method
+ if hasattr(env, 'note_versionchange'):
+ env.note_versionchange('deprecated', version[0], node, self.lineno)
+ # new method
+ else:
+ env.get_domain('changeset').note_changeset(node)
return [node] + messages

View File

@ -0,0 +1,12 @@
--- a/Lib/test/test_subprocess.py
+++ b/Lib/test/test_subprocess.py
@@ -253,7 +253,8 @@ class ProcessTestCase(BaseTestCase):
"time.sleep(3600)"],
# Some heavily loaded buildbots (sparc Debian 3.x) require
# this much time to start and print.
- timeout=3)
+ # OBS might require even more
+ timeout=10)
self.fail("Expected TimeoutExpired.")
self.assertEqual(c.exception.output, b'BDFL')

View File

@ -0,0 +1,75 @@
From 7da97f61816f3cadaa6788804b22a2434b40e8c5 Mon Sep 17 00:00:00 2001
From: "Miss Islington (bot)"
<31488909+miss-islington@users.noreply.github.com>
Date: Mon, 21 Feb 2022 08:16:09 -0800
Subject: [PATCH] bpo-46811: Make test suite support Expat >=2.4.5 (GH-31453)
(GH-31472)
Curly brackets were never allowed in namespace URIs
according to RFC 3986, and so-called namespace-validating
XML parsers have the right to reject them a invalid URIs.
libexpat >=2.4.5 has become strcter in that regard due to
related security issues; with ET.XML instantiating a
namespace-aware parser under the hood, this test has no
future in CPython.
References:
- https://datatracker.ietf.org/doc/html/rfc3968
- https://www.w3.org/TR/xml-names/
Also, test_minidom.py: Support Expat >=2.4.5
(cherry picked from commit 2cae93832f46b245847bdc252456ddf7742ef45e)
Co-authored-by: Sebastian Pipping <sebastian@pipping.org>
---
Lib/test/test_minidom.py | 23 +++++++++--------------
1 file changed, 9 insertions(+), 14 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2022-02-20-21-03-31.bpo-46811.8BxgdQ.rst
--- a/Lib/test/test_minidom.py
+++ b/Lib/test/test_minidom.py
@@ -6,7 +6,6 @@ import io
from test import support
import unittest
-import pyexpat
import xml.dom.minidom
from xml.dom.minidom import parse, Node, Document, parseString
@@ -1149,13 +1148,11 @@ class MinidomTest(unittest.TestCase):
# Verify that character decoding errors raise exceptions instead
# of crashing
- if pyexpat.version_info >= (2, 4, 5):
- self.assertRaises(ExpatError, parseString,
- b'<fran\xe7ais></fran\xe7ais>')
- self.assertRaises(ExpatError, parseString,
- b'<franais>Comment \xe7a va ? Tr\xe8s bien ?</franais>')
- else:
- self.assertRaises(UnicodeDecodeError, parseString,
+ # It doesnt make any sense to insist on the exact text of the
+ # error message, or even the exact Exception … it is enough that
+ # the error has been discovered.
+ with self.assertRaises((UnicodeDecodeError, ExpatError)):
+ parseString(
b'<fran\xe7ais>Comment \xe7a va ? Tr\xe8s bien ?</fran\xe7ais>')
doc.unlink()
@@ -1617,12 +1614,10 @@ class MinidomTest(unittest.TestCase):
self.confirm(doc2.namespaceURI == xml.dom.EMPTY_NAMESPACE)
def testExceptionOnSpacesInXMLNSValue(self):
- if pyexpat.version_info >= (2, 4, 5):
- context = self.assertRaisesRegex(ExpatError, 'syntax error')
- else:
- context = self.assertRaisesRegex(ValueError, 'Unsupported syntax')
-
- with context:
+ # It doesnt make any sense to insist on the exact text of the
+ # error message, or even the exact Exception … it is enough that
+ # the error has been discovered.
+ with self.assertRaises((ExpatError, ValueError)):
parseString('<element xmlns:abc="http:abc.com/de f g/hi/j k"><abc:foo /></element>')
def testDocRemoveChild(self):