Skip to content

Commit d3dc3d2

Browse files
Merge remote-tracking branch 'upstream/main' into curses-term-attrs
# Conflicts: # Modules/clinic/_cursesmodule.c.h
2 parents 70c0e83 + a69d0fc commit d3dc3d2

17 files changed

Lines changed: 380 additions & 121 deletions

‎Doc/c-api/type.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,7 @@ but need extra remarks for use as slots:
639639
in the following situations:
640640
641641
- The base is not variable-sized (its
642-
:c:member:`~PyTypeObject.tp_itemsize`).
642+
:c:member:`~PyTypeObject.tp_itemsize` is zero).
643643
- The requested :c:member:`PyType_Spec.basicsize` is positive,
644644
suggesting that the memory layout of the base class is known.
645645
- The requested :c:member:`PyType_Spec.basicsize` is zero,

‎Doc/library/curses.rst‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,13 @@ The module :mod:`!curses` defines the following functions:
139139
.. function:: color_pair(pair_number)
140140

141141
Return the attribute value for displaying text in the specified color pair.
142-
Only the first 256 color pairs are supported. This
143-
attribute value can be combined with :const:`A_STANDOUT`, :const:`A_REVERSE`,
144-
and the other :const:`!A_\*` attributes. :func:`pair_number` is the counterpart
145-
to this function.
142+
Only color pairs that fit in the color-pair field of the returned value can
143+
be represented (usually the first 256); a larger *pair_number* raises
144+
:exc:`OverflowError` rather than being silently masked to a different pair.
145+
Use :meth:`~window.color_set` or :meth:`~window.attr_set` to display higher
146+
pairs. This attribute value can be combined with :const:`A_STANDOUT`,
147+
:const:`A_REVERSE`, and the other :const:`!A_\*` attributes.
148+
:func:`pair_number` is the counterpart to this function.
146149

147150

148151
.. function:: curs_set(visibility)

‎Lib/_pydatetime.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -272,12 +272,12 @@ def _wrap_strftime(object, format, timetuple):
272272
newformat.append(Zreplace)
273273
# Note that datetime(1000, 1, 1).strftime('%G') == '1000' so
274274
# year 1000 for %G can go on the fast path.
275-
elif ((ch in 'YG' or ch in 'FC') and
276-
object.year < 1000 and _need_normalize_century()):
275+
elif (ch in 'YGFC' and timetuple[0] < 1000 and
276+
_need_normalize_century()):
277277
if ch == 'G':
278278
year = int(_time.strftime("%G", timetuple))
279279
else:
280-
year = object.year
280+
year = timetuple[0]
281281
if ch == 'C':
282282
push('{:02}'.format(year // 100))
283283
else:

‎Lib/test/datetimetester.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4119,6 +4119,11 @@ def test_strftime_special(self):
41194119
self.assertEqual(t.strftime('\0'*1000), '\0'*1000)
41204120
self.assertEqual(t.strftime('\0%I%p%Z\0%X'), f'\0{s1}\0{s2}')
41214121
self.assertEqual(t.strftime('%I%p%Z\0%X\0'), f'{s1}\0{s2}\0')
4122+
# gh-152305: the year directives must not raise on a time.
4123+
for directive, expected in (('%Y', '1900'), ('%G', '1900'),
4124+
('%C', '19'), ('%F', '1900-01-01')):
4125+
with self.subTest(directive=directive):
4126+
self.assertEqual(t.strftime(directive), expected)
41224127

41234128
def test_format(self):
41244129
t = self.theclass(1, 2, 3, 4)

‎Lib/test/test_curses.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,10 @@ def test_argument_errors(self):
814814
# A character argument must be an int, a byte or a one-element string.
815815
self.assertRaises(TypeError, win.addch, [])
816816
self.assertRaises(OverflowError, win.addch, 2**64)
817+
# The attribute argument is rejected, not truncated, when out of range.
818+
self.assertRaises(OverflowError, win.addch, 'a', 2**64)
819+
self.assertRaises(OverflowError, win.addstr, 'a', 2**64)
820+
self.assertRaises(TypeError, win.addch, 'a', 'bold')
817821
# A string method rejects a non-string, non-bytes argument.
818822
self.assertRaises(TypeError, win.addstr, 5)
819823
self.assertRaises(TypeError, win.addstr)
@@ -969,6 +973,11 @@ def test_attributes(self):
969973
self.assertRaises(OverflowError, win.attr_set, -1)
970974
self.assertRaises(OverflowError, win.attr_on, -1)
971975
self.assertRaises(OverflowError, win.attr_set, 1 << 64)
976+
# attron()/attroff()/attrset() reject a bad attribute too.
977+
self.assertRaises(OverflowError, win.attron, 1 << 64)
978+
self.assertRaises(OverflowError, win.attroff, -1)
979+
self.assertRaises(OverflowError, win.attrset, 1 << 64)
980+
self.assertRaises(TypeError, win.attron, 'x')
972981

973982
@requires_colors
974983
def test_attr_color_pair(self):
@@ -1717,6 +1726,11 @@ def test_color_attrs(self):
17171726
self.assertEqual(curses.pair_number(attr | curses.A_BOLD), pair)
17181727
self.assertEqual(curses.color_pair(0), 0)
17191728
self.assertEqual(curses.pair_number(0), 0)
1729+
# A pair too large to fit is rejected, not silently masked (gh-119138).
1730+
max_pair = curses.pair_number(curses.A_COLOR)
1731+
self.assertEqual(curses.pair_number(curses.color_pair(max_pair)), max_pair)
1732+
self.assertRaises(OverflowError, curses.color_pair, max_pair + 1)
1733+
self.assertRaises(OverflowError, curses.color_pair, -1)
17201734

17211735
@requires_curses_func('use_default_colors')
17221736
@requires_colors

‎Lib/test/test_interpreters/test_stress.py‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def test_create_many_sequential(self):
2525
del alive
2626
support.gc_collect()
2727

28+
@threading_helper.requires_working_threading()
2829
@support.bigmemtest(size=200, memuse=32*2**20, dry_run=False)
2930
def test_create_many_threaded(self, size):
3031
alive = []
@@ -80,9 +81,12 @@ def test_create_interpreter_no_memory(self):
8081
import _testcapi
8182

8283
assertion = self.assertRaises(InterpreterError)
83-
_testcapi.set_nomemory(0, 1)
84-
with assertion:
85-
_interpreters.create()
84+
try:
85+
_testcapi.set_nomemory(0, 1)
86+
with assertion:
87+
_interpreters.create()
88+
finally:
89+
_testcapi.remove_mem_hooks()
8690

8791

8892
if __name__ == '__main__':

‎Lib/test/test_pyexpat.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,8 +1057,7 @@ class ExternalEntityParserCreateErrorTest(unittest.TestCase):
10571057
def setUpClass(cls):
10581058
cls.testcapi = import_helper.import_module('_testcapi')
10591059

1060-
@unittest.skipIf(support.Py_TRACE_REFS,
1061-
'Py_TRACE_REFS conflicts with testcapi.set_nomemory')
1060+
@support.nomemtest
10621061
def test_error_path_no_crash(self):
10631062
# When an allocation inside ExternalEntityParserCreate fails,
10641063
# the partially-initialized subparser is deallocated. This

‎Lib/test/test_sys.py‎

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import operator
77
import os
88
import random
9+
import shutil
910
import socket
1011
import struct
1112
import subprocess
@@ -2003,7 +2004,8 @@ def tearDown(self):
20032004
test.support.reap_children()
20042005

20052006
def _run_remote_exec_test(self, script_code, python_args=None, env=None,
2006-
prologue='',
2007+
python_executable=None, prologue='',
2008+
after_ready=None,
20072009
script_path=os_helper.TESTFN + '_remote.py'):
20082010
# Create the script that will be remotely executed
20092011
self.addCleanup(os_helper.unlink, script_path)
@@ -2051,7 +2053,10 @@ def _run_remote_exec_test(self, script_code, python_args=None, env=None,
20512053
''')
20522054

20532055
# Start the target process and capture its output
2054-
cmd = [sys.executable]
2056+
if python_executable is None:
2057+
python_executable = sys.executable
2058+
2059+
cmd = [python_executable]
20552060
if python_args:
20562061
cmd.extend(python_args)
20572062
cmd.append(target)
@@ -2076,6 +2081,9 @@ def _run_remote_exec_test(self, script_code, python_args=None, env=None,
20762081
response = client_socket.recv(1024)
20772082
self.assertEqual(response, b"ready")
20782083

2084+
if after_ready is not None:
2085+
after_ready(proc)
2086+
20792087
# Try remote exec on the target process
20802088
sys.remote_exec(proc.pid, script_path)
20812089

@@ -2098,6 +2106,19 @@ def _run_remote_exec_test(self, script_code, python_args=None, env=None,
20982106
proc.terminate()
20992107
proc.wait(timeout=SHORT_TIMEOUT)
21002108

2109+
def _run_remote_exec_with_deleted_mapping(self, deleted_path, **kwargs):
2110+
def delete_loaded_mapping(proc):
2111+
os_helper.unlink(deleted_path)
2112+
with open(f'/proc/{proc.pid}/maps', encoding='utf-8') as maps:
2113+
self.assertIn(f'{deleted_path} (deleted)', maps.read())
2114+
2115+
script = 'print("Remote script executed successfully!")'
2116+
returncode, stdout, stderr = self._run_remote_exec_test(
2117+
script, after_ready=delete_loaded_mapping, **kwargs)
2118+
self.assertEqual(returncode, 0)
2119+
self.assertIn(b"Remote script executed successfully!", stdout)
2120+
self.assertEqual(stderr, b"")
2121+
21012122
def test_remote_exec(self):
21022123
"""Test basic remote exec functionality"""
21032124
script = 'print("Remote script executed successfully!")'
@@ -2224,6 +2245,75 @@ def test_remote_exec_invalid_script_path(self):
22242245
with self.assertRaises(OSError):
22252246
sys.remote_exec(os.getpid(), "invalid_script_path")
22262247

2248+
@unittest.skipUnless(sys.platform == 'linux', 'Linux-only regression test')
2249+
@unittest.skipUnless(
2250+
sysconfig.get_config_var('Py_ENABLE_SHARED') == 1,
2251+
'requires a shared libpython build')
2252+
def test_remote_exec_deleted_libpython(self):
2253+
"""Test remote exec when the target libpython was deleted."""
2254+
build_dir = sysconfig.get_config_var('abs_builddir')
2255+
ldlibrary = sysconfig.get_config_var('LDLIBRARY')
2256+
instsoname = sysconfig.get_config_var('INSTSONAME')
2257+
if not build_dir or not ldlibrary or not instsoname:
2258+
self.skipTest('cannot determine shared libpython location')
2259+
2260+
source_libpython = os.path.join(build_dir, instsoname)
2261+
if not os.path.exists(source_libpython):
2262+
self.skipTest(f'{source_libpython!r} does not exist')
2263+
2264+
with os_helper.temp_dir() as lib_dir:
2265+
copied_libpython = os.path.join(lib_dir, instsoname)
2266+
shutil.copy2(source_libpython, copied_libpython)
2267+
if ldlibrary != instsoname:
2268+
os.symlink(instsoname, os.path.join(lib_dir, ldlibrary))
2269+
2270+
env = os.environ.copy()
2271+
ld_library_path = env.get('LD_LIBRARY_PATH')
2272+
env['LD_LIBRARY_PATH'] = lib_dir if not ld_library_path else (
2273+
lib_dir + os.pathsep + ld_library_path)
2274+
2275+
self._run_remote_exec_with_deleted_mapping(copied_libpython,
2276+
env=env)
2277+
2278+
@unittest.skipUnless(sys.platform == 'linux', 'Linux-only regression test')
2279+
@unittest.skipUnless(
2280+
sysconfig.get_config_var('Py_ENABLE_SHARED') == 0,
2281+
'requires a static Python build')
2282+
def test_remote_exec_deleted_static_executable(self):
2283+
"""Test remote exec when the target static executable was deleted."""
2284+
build_dir = sysconfig.get_config_var('abs_builddir')
2285+
srcdir = sysconfig.get_config_var('srcdir')
2286+
if not build_dir or not srcdir:
2287+
self.skipTest('cannot determine build-tree locations')
2288+
2289+
pybuilddir_txt = os.path.join(build_dir, 'pybuilddir.txt')
2290+
if not os.path.exists(pybuilddir_txt):
2291+
self.skipTest(f'{pybuilddir_txt!r} does not exist')
2292+
2293+
with open(pybuilddir_txt, encoding='utf-8') as pybuilddir_file:
2294+
pybuilddir = pybuilddir_file.read().strip()
2295+
source_ext_dir = os.path.join(build_dir, pybuilddir)
2296+
if not os.path.isdir(source_ext_dir):
2297+
self.skipTest(f'{source_ext_dir!r} does not exist')
2298+
2299+
with os_helper.temp_dir() as copied_root:
2300+
copied_build_dir = os.path.join(copied_root, 'build')
2301+
copied_pybuilddir = os.path.join(copied_build_dir, pybuilddir)
2302+
os.makedirs(os.path.dirname(copied_pybuilddir))
2303+
os.symlink(os.path.join(srcdir, 'Lib'),
2304+
os.path.join(copied_root, 'Lib'))
2305+
os.symlink(source_ext_dir, copied_pybuilddir)
2306+
shutil.copy2(pybuilddir_txt,
2307+
os.path.join(copied_build_dir, 'pybuilddir.txt'))
2308+
2309+
copied_python = os.path.join(copied_build_dir,
2310+
os.path.basename(sys.executable))
2311+
shutil.copy2(sys.executable, copied_python)
2312+
2313+
self._run_remote_exec_with_deleted_mapping(
2314+
copied_python, python_args=['-S'],
2315+
python_executable=copied_python)
2316+
22272317
def test_remote_exec_in_process_without_debug_fails_envvar(self):
22282318
"""Test remote exec in a process without remote debugging enabled"""
22292319
script = os_helper.TESTFN + '_remote.py'
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
On Linux, fix :func:`sys.remote_exec` unable to find remote writable memory
2+
when ``libpython`` replaced on disk.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix the pure-Python :meth:`datetime.time.strftime` implementation raising :exc:`AttributeError` for the
2+
year directives. Patch by tonghuaroot.

0 commit comments

Comments
 (0)