Skip to content

Commit dfc7f1d

Browse files
Merge branch 'main' into gh-135736-taskgroup-generatorexit
2 parents c5a3678 + 59e67c2 commit dfc7f1d

14 files changed

Lines changed: 244 additions & 207 deletions

Doc/library/difflib.rst

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,18 @@ diffs. For comparing directories and files, see also, the :mod:`filecmp` module.
4040
complicated way on how many elements the sequences have in common; best case
4141
time is linear.
4242

43-
**Automatic junk heuristic:** :class:`SequenceMatcher` supports a heuristic that
44-
automatically treats certain sequence items as junk. The heuristic counts how many
45-
times each individual item appears in the sequence. If an item's duplicates (after
46-
the first one) account for more than 1% of the sequence and the sequence is at least
47-
200 items long, this item is marked as "popular" and is treated as junk for
48-
the purpose of sequence matching. This heuristic can be turned off by setting
49-
the ``autojunk`` argument to ``False`` when creating the :class:`SequenceMatcher`.
43+
**Junk**: :class:`SequenceMatcher` accepts an ``isjunk`` predicate and an
44+
``autojunk`` flag. Items that are considered as junk will not be considered
45+
to find similar content blocks. This can produce better results for humans
46+
(typically breaking on whitespace) and faster (because it reduces the number
47+
of possible combinations). But it can also cause pathological cases where
48+
too many items considered junk cause an unexpectedly large (but correct)
49+
diff result.
50+
You should consider tuning them or turning them off depending on your data.
51+
Moreover, only the second sequence is inspected for junk. This causes the diff
52+
output to not be symmetrical.
53+
When ``autojunk=True``, it will consider as junk the items that account for more
54+
than 1% of the sequence, if it is at least 200 items long.
5055

5156
.. versionchanged:: 3.2
5257
Added the *autojunk* parameter.
@@ -558,16 +563,6 @@ The :class:`SequenceMatcher` class has this constructor:
558563
to try :meth:`quick_ratio` or :meth:`real_quick_ratio` first to get an
559564
upper bound.
560565

561-
.. note::
562-
563-
Caution: The result of a :meth:`ratio` call may depend on the order of
564-
the arguments. For instance::
565-
566-
>>> SequenceMatcher(None, 'tide', 'diet').ratio()
567-
0.25
568-
>>> SequenceMatcher(None, 'diet', 'tide').ratio()
569-
0.5
570-
571566

572567
.. method:: quick_ratio()
573568

Doc/library/logging.handlers.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,8 @@ supports sending logging messages to a remote or local Unix syslog.
631631
the form of a ``(host, port)`` tuple. If *address* is not specified,
632632
``('localhost', 514)`` is used. The address is used to open a socket. An
633633
alternative to providing a ``(host, port)`` tuple is providing an address as a
634-
string, for example '/dev/log'. In this case, a Unix domain socket is used to
634+
string or a :class:`bytes` object, for example '/dev/log'.
635+
In this case, a Unix domain socket is used to
635636
send the message to the syslog. If *facility* is not specified,
636637
:const:`LOG_USER` is used. The type of socket opened depends on the
637638
*socktype* argument, which defaults to :const:`socket.SOCK_DGRAM` and thus
@@ -664,6 +665,9 @@ supports sending logging messages to a remote or local Unix syslog.
664665
.. versionchanged:: 3.14
665666
*timeout* was added.
666667

668+
.. versionchanged:: next
669+
*address* can now be a :class:`bytes` object.
670+
667671
.. method:: close()
668672

669673
Closes the socket to the remote host.

Lib/argparse.py

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,17 @@
8787

8888

8989
import os as _os
90-
import re as _re
9190
import sys as _sys
92-
from gettext import gettext as _
93-
from gettext import ngettext
9491

9592
lazy import _colorize
93+
lazy import copy
94+
lazy import difflib
95+
lazy import re as _re
96+
lazy import shutil
97+
lazy import textwrap
98+
lazy import warnings
99+
lazy from gettext import gettext as _
100+
lazy from gettext import ngettext
96101

97102
SUPPRESS = '==SUPPRESS=='
98103

@@ -143,10 +148,8 @@ def _copy_items(items):
143148
return []
144149
# The copy module is used only in the 'append' and 'append_const'
145150
# actions, and it is needed only when the default value isn't a list.
146-
# Delay its import for speeding up the common case.
147151
if type(items) is list:
148152
return items[:]
149-
import copy
150153
return copy.copy(items)
151154

152155

@@ -186,7 +189,6 @@ def __init__(
186189
):
187190
# default setting for width
188191
if width is None:
189-
import shutil
190192
width = shutil.get_terminal_size().columns
191193
width -= 2
192194

@@ -773,14 +775,38 @@ def _iter_indented_subactions(self, action):
773775

774776
def _split_lines(self, text, width):
775777
text = self._whitespace_matcher.sub(' ', text).strip()
776-
# The textwrap module is used only for formatting help.
777-
# Delay its import for speeding up the common usage of argparse.
778-
import textwrap
779-
return textwrap.wrap(text, width)
778+
decolored = self._decolor(text)
779+
if decolored == text:
780+
return textwrap.wrap(text, width)
781+
782+
# gh-142035: colors inflate textwrap's length counts, so wrap
783+
# the decolored text and re-apply colors per word; if textwrap
784+
# split a word, keep the plain lines (colors can't be mapped).
785+
plain = self._whitespace_matcher.sub(' ', decolored).strip()
786+
if not plain:
787+
# nothing visible to wrap (e.g. an empty interpolated value)
788+
return [text]
789+
plain_lines = textwrap.wrap(plain, width)
790+
plain_words = plain.split()
791+
colored_words = text.split()
792+
# Drop escape-only tokens (e.g. an empty interpolated value).
793+
if len(colored_words) != len(plain_words):
794+
colored_words = [
795+
word for word in colored_words if self._decolor(word)
796+
]
797+
colored_lines = []
798+
start = 0
799+
for plain_line in plain_lines:
800+
plain_line_words = plain_line.split()
801+
end = start + len(plain_line_words)
802+
if plain_words[start:end] != plain_line_words:
803+
return plain_lines
804+
colored_lines.append(' '.join(colored_words[start:end]))
805+
start = end
806+
return colored_lines
780807

781808
def _fill_text(self, text, width, indent):
782809
text = self._whitespace_matcher.sub(' ', text).strip()
783-
import textwrap
784810
return textwrap.fill(text, width,
785811
initial_indent=indent,
786812
subsequent_indent=indent)
@@ -1458,7 +1484,6 @@ class FileType(object):
14581484
"""
14591485

14601486
def __init__(self, mode='r', bufsize=-1, encoding=None, errors=None):
1461-
import warnings
14621487
warnings.warn(
14631488
"FileType is deprecated. Simply open files after parsing arguments.",
14641489
category=PendingDeprecationWarning,
@@ -1865,7 +1890,6 @@ class _ArgumentGroup(_ActionsContainer):
18651890

18661891
def __init__(self, container, title=None, description=None, **kwargs):
18671892
if 'prefix_chars' in kwargs:
1868-
import warnings
18691893
depr_msg = (
18701894
"The use of the undocumented 'prefix_chars' parameter in "
18711895
"ArgumentParser.add_argument_group() is deprecated."
@@ -2796,7 +2820,6 @@ def _check_value(self, action, value):
27962820

27972821
if self.suggest_on_error and isinstance(value, str):
27982822
if all(isinstance(choice, str) for choice in action.choices):
2799-
import difflib
28002823
suggestions = difflib.get_close_matches(value, action.choices, 1)
28012824
if suggestions:
28022825
args['closest'] = suggestions[0]
@@ -2936,8 +2959,6 @@ def _warning(self, message):
29362959

29372960
def __getattr__(name):
29382961
if name == "__version__":
2939-
from warnings import _deprecated
2940-
2941-
_deprecated("__version__", remove=(3, 20))
2962+
warnings._deprecated("__version__", remove=(3, 20))
29422963
return "1.1" # Do not change
29432964
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

Lib/logging/handlers.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -886,8 +886,9 @@ def __init__(self, address=('localhost', SYSLOG_UDP_PORT),
886886
"""
887887
Initialize a handler.
888888
889-
If address is specified as a string, a UNIX socket is used. To log to a
890-
local syslogd, "SysLogHandler(address="/dev/log")" can be used.
889+
If address is specified as a string or bytes, a UNIX socket is used.
890+
To log to a local syslogd, "SysLogHandler(address="/dev/log")" can be
891+
used.
891892
If facility is not specified, LOG_USER is used. If socktype is
892893
specified as socket.SOCK_DGRAM or socket.SOCK_STREAM, that specific
893894
socket type will be used. For Unix sockets, you can also specify a
@@ -938,7 +939,7 @@ def createSocket(self):
938939
address = self.address
939940
socktype = self.socktype
940941

941-
if isinstance(address, str):
942+
if not isinstance(address, (list, tuple)):
942943
self.unixsocket = True
943944
# Syslog server may be unavailable during handler initialisation.
944945
# C's openlog() function also ignores connection errors.

Lib/test/test_argparse.py

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import _colorize
44
import contextlib
55
import functools
6-
import inspect
76
import io
87
import operator
98
import os
@@ -12,6 +11,7 @@
1211
import sys
1312
import textwrap
1413
import tempfile
14+
import types
1515
import unittest
1616
import argparse
1717
import warnings
@@ -85,6 +85,8 @@ class TestLazyImports(unittest.TestCase):
8585
"_colorize",
8686
"copy",
8787
"difflib",
88+
"gettext",
89+
"re",
8890
"shutil",
8991
"textwrap",
9092
"warnings",
@@ -99,7 +101,7 @@ def test_create_parser(self):
99101
# Test imports are still unused after
100102
# creating a parser
101103
create_parser = "argparse.ArgumentParser()"
102-
imported_modules = {"shutil"}
104+
imported_modules = {"gettext", "re", "shutil"}
103105

104106
import_helper.ensure_lazy_imports(
105107
"argparse",
@@ -114,7 +116,7 @@ def test_add_subparser(self):
114116
parser.add_subparsers(dest='command', required=False)
115117
"""
116118
)
117-
imported_modules = {"shutil"}
119+
imported_modules = {"gettext", "re", "shutil"}
118120

119121
import_helper.ensure_lazy_imports(
120122
"argparse",
@@ -132,7 +134,7 @@ def test_parse_args(self):
132134
parser.parse_args(['BAR', '--foo', 'FOO'])
133135
"""
134136
)
135-
imported_modules = {"shutil"}
137+
imported_modules = {"gettext", "re", "shutil"}
136138
import_helper.ensure_lazy_imports(
137139
"argparse",
138140
self.LAZY_IMPORTS - imported_modules,
@@ -7098,7 +7100,10 @@ def test_all_exports_everything_but_modules(self):
70987100
name
70997101
for name, value in vars(argparse).items()
71007102
if not (name.startswith("_") or name == 'ngettext')
7101-
if not inspect.ismodule(value)
7103+
if not isinstance(
7104+
value,
7105+
(types.ModuleType, types.LazyImportType),
7106+
)
71027107
]
71037108
self.assertEqual(sorted(items), sorted(argparse.__all__))
71047109

@@ -7580,6 +7585,62 @@ def test_argparse_color_custom_usage(self):
75807585
),
75817586
)
75827587

7588+
def test_argparse_color_wrapping_matches_uncolored(self):
7589+
# gh-142035: color codes must not affect where help text wraps.
7590+
# Stripping the escapes from colored help must yield exactly the
7591+
# same text as the uncolored help across representative widths.
7592+
def build(color, path="output.txt"):
7593+
parser = argparse.ArgumentParser(prog="PROG", color=color)
7594+
parser.add_argument(
7595+
"--mode",
7596+
default="auto",
7597+
choices=("auto", "fast", "slow"),
7598+
help="select the operating mode from the available choices "
7599+
"%(choices)s and note the default is %(default)s here",
7600+
)
7601+
parser.add_argument(
7602+
"--path",
7603+
default=path,
7604+
help="write output to %(default)s and continue processing",
7605+
)
7606+
return parser
7607+
7608+
env = self.enterContext(os_helper.EnvironmentVarGuard())
7609+
paths = (
7610+
"output.txt",
7611+
"/var/lib/application/cache/unusually_long_generated_filename",
7612+
"production-read-only-replica",
7613+
)
7614+
for path in paths:
7615+
for columns in ("80", "60", "45", "30", "20"):
7616+
with self.subTest(path=path, columns=columns):
7617+
env["COLUMNS"] = columns
7618+
colored = build(color=True, path=path).format_help()
7619+
plain = build(color=False, path=path).format_help()
7620+
self.assertIn(
7621+
f"{self.theme.interpolated_value}auto"
7622+
f"{self.theme.reset}",
7623+
colored,
7624+
)
7625+
self.assertEqual(_colorize.decolor(colored), plain)
7626+
7627+
def test_argparse_color_preserved_when_wrapping_between_words(self):
7628+
parser = argparse.ArgumentParser(prog="PROG", color=True)
7629+
parser.add_argument(
7630+
"--mode", default="auto",
7631+
help="select the %(default)s operating mode from the available "
7632+
"options and continue with several more words",
7633+
)
7634+
7635+
env = self.enterContext(os_helper.EnvironmentVarGuard())
7636+
env["COLUMNS"] = "40"
7637+
help_text = parser.format_help()
7638+
7639+
self.assertIn(
7640+
f"{self.theme.interpolated_value}auto{self.theme.reset}",
7641+
help_text,
7642+
)
7643+
75837644
def test_custom_formatter_function(self):
75847645
def custom_formatter(prog):
75857646
return argparse.RawTextHelpFormatter(prog, indent_increment=5)

Lib/test/test_functools.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,40 @@ def f(**kwargs):
579579
with self.assertRaises(RuntimeError):
580580
result = p(**{BadStr("poison"): "new_value"})
581581

582+
def test_call_safety_against_reentrant_mutation(self):
583+
def old_function(*args, **kwargs):
584+
return "old_function", args, kwargs
585+
586+
def new_function(*args, **kwargs):
587+
return "new_function", args, kwargs
588+
589+
g_partial = None
590+
591+
class EvilKey(str):
592+
armed = False
593+
def __hash__(self):
594+
if EvilKey.armed and g_partial is not None:
595+
EvilKey.armed = False
596+
new_args_tuple = ("new_arg",)
597+
new_keywords_dict = {"new_keyword": None}
598+
new_tuple_state = (new_function, new_args_tuple, new_keywords_dict, None)
599+
g_partial.__setstate__(new_tuple_state)
600+
gc.collect()
601+
return str.__hash__(self)
602+
603+
g_partial = functools.partial(old_function, "old_arg", old_keyword=None)
604+
605+
kwargs = {EvilKey("evil_key"): None}
606+
EvilKey.armed = True
607+
608+
result = g_partial(**kwargs)
609+
expected = ("old_function", ("old_arg",), {"old_keyword": None, "evil_key": None})
610+
self.assertEqual(result, expected)
611+
612+
result = g_partial()
613+
expected = ("new_function", ("new_arg",), {"new_keyword": None})
614+
self.assertEqual(result, expected)
615+
582616
@unittest.skipUnless(c_functools, 'requires the C _functools module')
583617
class TestPartialC(TestPartial, unittest.TestCase):
584618
if c_functools:

0 commit comments

Comments
 (0)