Skip to content

Commit 3e4dde7

Browse files
Merge branch 'main' into argparse-lazy
2 parents 11eb674 + 998fc4a commit 3e4dde7

15 files changed

Lines changed: 237 additions & 33 deletions

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: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -775,7 +775,35 @@ def _iter_indented_subactions(self, action):
775775

776776
def _split_lines(self, text, width):
777777
text = self._whitespace_matcher.sub(' ', text).strip()
778-
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
779807

780808
def _fill_text(self, text, width, indent):
781809
text = self._whitespace_matcher.sub(' ', text).strip()

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: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7585,6 +7585,62 @@ def test_argparse_color_custom_usage(self):
75857585
),
75867586
)
75877587

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+
75887644
def test_custom_formatter_function(self):
75897645
def custom_formatter(prog):
75907646
return argparse.RawTextHelpFormatter(prog, indent_increment=5)

Lib/test/test_bytes.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2226,6 +2226,46 @@ def __len__(self):
22262226

22272227
self.assertRaises(BufferError, ba.hex, S(b':'))
22282228

2229+
def test_no_init_called(self):
2230+
# A bytearray created without calling bytearray.__init__
2231+
# should not crash the interpreter (see gh-153419).
2232+
def bytearray_new():
2233+
return bytearray.__new__(bytearray)
2234+
2235+
bytearray_new().insert(0, 1)
2236+
bytearray_new().extend(b"x")
2237+
bytearray_new().extend([1, 2, 3])
2238+
bytearray_new().resize(4)
2239+
bytearray_new().__init__(5)
2240+
bytearray_new().__init__(b"xyz")
2241+
bytearray_new().take_bytes()
2242+
bytearray_new().take_bytes(0)
2243+
2244+
a = bytearray_new()
2245+
a.append(1)
2246+
2247+
a = bytearray_new()
2248+
a += b"x"
2249+
2250+
a = bytearray_new()
2251+
a[:] = b"xyz"
2252+
2253+
def test_reinit_length(self):
2254+
# There is a shortcut taken when resizing, where alloc/2 < newsize.
2255+
# In this case, the existing buffer is reused, rather than reset.
2256+
# If this happens when newsize == 0 and alloc == 1, then various
2257+
# code assumptions can be violated. This test should catch those
2258+
# in debug builds. (see gh-153419)
2259+
a = bytearray(1)
2260+
a.__init__()
2261+
self.assertEqual(a, b"")
2262+
2263+
def test_reinit_with_view(self):
2264+
a = bytearray()
2265+
with memoryview(a):
2266+
self.assertRaises(BufferError, a.__init__, "x", "ascii")
2267+
self.assertEqual(a, b"")
2268+
22292269

22302270
class AssortedBytesTest(unittest.TestCase):
22312271
#

Lib/test/test_gdb/test_pretty_print.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -114,29 +114,28 @@ def test_bytes(self):
114114
@support.requires_resource('cpu')
115115
def test_strings(self):
116116
'Verify the pretty-printing of unicode strings'
117-
# We cannot simply call locale.getpreferredencoding() here,
118-
# as GDB might have been linked against a different version
119-
# of Python with a different encoding and coercion policy
120-
# with respect to PEP 538 and PEP 540.
117+
# gdb emits its output in the host charset, which is not necessarily the
118+
# getpreferredencoding() of the (possibly differently coerced) embedded
119+
# Python.
121120
stdout, stderr = run_gdb(
122121
'--eval-command',
123-
'python import locale; print(locale.getpreferredencoding())')
122+
'python import gdb; print(gdb.host_charset())')
124123

125-
encoding = stdout
124+
encoding = stdout.strip()
126125
if stderr or not encoding:
127126
raise RuntimeError(
128-
f'unable to determine the Python locale preferred encoding '
129-
f'of embedded Python in GDB\n'
127+
f'unable to determine the host charset of gdb\n'
130128
f'stdout={stdout!r}\n'
131129
f'stderr={stderr!r}')
132130

133131
def check_repr(text):
134132
try:
135133
text.encode(encoding)
136-
except UnicodeEncodeError:
134+
# LookupError or ValueError if the host charset is unknown or invalid.
135+
except (UnicodeEncodeError, LookupError, ValueError):
137136
self.assertGdbRepr(text, ascii(text))
138137
else:
139-
self.assertGdbRepr(text)
138+
self.assertGdbRepr(text, repr(text).encode(encoding).decode('ascii', 'surrogateescape'))
140139

141140
self.assertGdbRepr('')
142141
self.assertGdbRepr('And now for something hopefully the same')

Lib/test/test_gdb/util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def run_gdb(*args, exitcode=0, check=True, **env_vars):
7878
stdin=subprocess.PIPE,
7979
stdout=subprocess.PIPE,
8080
stderr=subprocess.PIPE,
81-
encoding="utf8", errors="backslashreplace",
81+
encoding="ascii", errors="surrogateescape",
8282
env=env)
8383

8484
stdout = proc.stdout

Lib/test/test_logging.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2138,6 +2138,45 @@ def setUp(self):
21382138
self.addCleanup(os_helper.unlink, self.address)
21392139
SysLogHandlerTest.setUp(self)
21402140

2141+
def test_bytes_address(self):
2142+
# The Unix socket address can also be specified as bytes.
2143+
if self.server_exception:
2144+
self.skipTest(self.server_exception)
2145+
hdlr = logging.handlers.SysLogHandler(os.fsencode(self.address))
2146+
self.addCleanup(hdlr.close)
2147+
self.assertTrue(hdlr.unixsocket)
2148+
logger = logging.getLogger("slh-bytes")
2149+
logger.addHandler(hdlr)
2150+
self.addCleanup(logger.removeHandler, hdlr)
2151+
logger.error("sp\xe4m")
2152+
self.handled.wait(support.LONG_TIMEOUT)
2153+
self.assertEqual(self.log_output, b'<11>sp\xc3\xa4m\x00')
2154+
2155+
@unittest.skipUnless(sys.platform in ('linux', 'android'),
2156+
'Linux specific test')
2157+
class AbstractNamespaceSysLogHandlerTest(BaseTest):
2158+
2159+
"""Test for SysLogHandler with a socket in the abstract namespace."""
2160+
2161+
def check(self, address):
2162+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
2163+
self.addCleanup(sock.close)
2164+
sock.bind(address)
2165+
sock.settimeout(support.LONG_TIMEOUT)
2166+
hdlr = logging.handlers.SysLogHandler(address)
2167+
self.addCleanup(hdlr.close)
2168+
self.assertTrue(hdlr.unixsocket)
2169+
hdlr.emit(logging.makeLogRecord({'msg': 'sp\xe4m'}))
2170+
self.assertEqual(sock.recv(1024), b'<12>sp\xc3\xa4m\x00')
2171+
2172+
def test_str_address(self):
2173+
# A str address is encoded with the filesystem encoding.
2174+
self.check('\0' + os_helper.TESTFN)
2175+
2176+
def test_bytes_address(self):
2177+
# The name is an arbitrary byte sequence, it need not be decodable.
2178+
self.check(b'\0test_logging_%d_\xff\xfe' % os.getpid())
2179+
21412180
@unittest.skipUnless(socket_helper.IPV6_ENABLED,
21422181
'IPv6 support required for this test.')
21432182
class IPv6SysLogHandlerTest(SysLogHandlerTest):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix multiple :class:`bytearray` crashes and reference leaks caused by skipping :meth:`~object.__init__` and broken state setup code.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:class:`logging.handlers.SysLogHandler` now accepts a :class:`bytes` address
2+
of a Unix domain socket, including an address in the abstract namespace.
3+
Previously only :class:`str` was recognized,
4+
and a :class:`bytes` address raised :exc:`ValueError`.

0 commit comments

Comments
 (0)