Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
412 changes: 239 additions & 173 deletions Doc/library/curses.rst

Large diffs are not rendered by default.

141 changes: 65 additions & 76 deletions Doc/pylock.toml

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions Doc/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@
# Keep this version in sync with ``Doc/conf.py``.
sphinx<9.0.0

# Temporary direct requirement, pending release of Pygments > 2.20.0
# https://github.com/pygments/pygments/discussions/3145
pygments @ https://github.com/pygments/pygments/archive/2cad2642058441b59782a6a18f03c98c42d081f1.tar.gz
pygments>=2.21

blurb

Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@ curses
counterpart of :func:`curses.termattrs`.
(Contributed by Serhiy Storchaka in :gh:`152332`.)

* Add the ``WACS_*`` constants to the :mod:`curses` module, the counterparts of
the :ref:`ACS_* <curses-acs-codes>` line-drawing codes as
:class:`curses.complexchar` cells.
(Contributed by Serhiy Storchaka in :gh:`155863`.)

* Add the :mod:`curses` functions :func:`curses.alloc_pair`,
:func:`curses.find_pair`, :func:`curses.free_pair` and
:func:`curses.reset_color_pairs` for dynamic color-pair management,
Expand Down
14 changes: 7 additions & 7 deletions Lib/curses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
import os as _os
import sys as _sys

# Some constants, most notably the ACS_* ones, are only added to the C
# _curses module's dictionary after initscr() is called. (Some
# Some constants, most notably the ACS_* and WACS_* ones, are only added
# to the C _curses module's dictionary after initscr() is called. (Some
# versions of SGI's curses don't define values for those constants
# until initscr() has been called.) This wrapper function calls the
# underlying C initscr(), and then copies the constants from the
Expand All @@ -30,13 +30,13 @@ def initscr():
fd=_sys.__stdout__.fileno())
stdscr = _curses.initscr()
for key, value in _curses.__dict__.items():
if key.startswith('ACS_') or key in ('LINES', 'COLS'):
if key.startswith(('ACS_', 'WACS_')) or key in ('LINES', 'COLS'):
setattr(curses, key, value)
return stdscr

# newterm() is wrapped for the same reason as initscr(): the ACS_* constants
# and LINES/COLS only become available once a terminal is initialized, and are
# then copied to the curses package's dictionary.
# newterm() is wrapped for the same reason as initscr(): the ACS_* and WACS_*
# constants and LINES/COLS only become available once a terminal is
# initialized, and are then copied to the curses package's dictionary.

try:
newterm
Expand All @@ -47,7 +47,7 @@ def newterm(type=None, fd=None, infd=None, /):
import _curses, curses
screen = _curses.newterm(type, fd, infd)
for key, value in _curses.__dict__.items():
if key.startswith('ACS_') or key in ('LINES', 'COLS'):
if key.startswith(('ACS_', 'WACS_')) or key in ('LINES', 'COLS'):
setattr(curses, key, value)
return screen

Expand Down
7 changes: 6 additions & 1 deletion Lib/test/test_ctypes/test_delattr.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import unittest
from ctypes import Structure, c_char, c_int
from ctypes import POINTER, Structure, c_char, c_int


class X(Structure):
Expand All @@ -16,6 +16,11 @@ def test_chararray(self):
with self.assertRaises(TypeError):
del chararray.value

def test_pointer_contents(self):
ptr = POINTER(c_int)(c_int(42))
with self.assertRaises(TypeError):
del ptr.contents

def test_struct(self):
struct = X()
with self.assertRaises(TypeError):
Expand Down
179 changes: 177 additions & 2 deletions Lib/test/test_curses.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@ def wrapped(self, *args, **kwargs):
return wrapped


# The WACS_* double-line and thick-line character cells, without the common
# prefix, paired with the alternate name spelling out their four sides
# (blank, double or thick, clockwise from the top).
WACS_LINE_ALIASES = [
('D_ULCORNER', 'BDDB'), ('D_LLCORNER', 'DDBB'),
('D_URCORNER', 'BBDD'), ('D_LRCORNER', 'DBBD'),
('D_LTEE', 'DDDB'), ('D_RTEE', 'DBDD'),
('D_BTEE', 'DDBD'), ('D_TTEE', 'BDDD'),
('D_HLINE', 'BDBD'), ('D_VLINE', 'DBDB'), ('D_PLUS', 'DDDD'),
('T_ULCORNER', 'BTTB'), ('T_LLCORNER', 'TTBB'),
('T_URCORNER', 'BBTT'), ('T_LRCORNER', 'TBBT'),
('T_LTEE', 'TTTB'), ('T_RTEE', 'TBTT'),
('T_BTEE', 'TTBT'), ('T_TTEE', 'BTTT'),
('T_HLINE', 'BTBT'), ('T_VLINE', 'TBTB'), ('T_PLUS', 'TTTT'),
]


def requires_colors(test):
@functools.wraps(test)
def wrapped(self, *args, **kwargs):
Expand Down Expand Up @@ -467,8 +484,142 @@ def test_wide_characters(self):
if self._encodable(vline + hline):
stdscr.border(vline, vline, hline, hline)
stdscr.box(vline, hline)
# border() and box() cannot mix integer and wide-string characters.
self.assertRaises(TypeError, stdscr.box, vline, ord('-'))
# border() and box() cannot mix a complexchar with an integer
# character; a wide string character is narrowed instead, which only
# works if it is a single byte.
self.assertRaises(TypeError, stdscr.box,
curses.complexchar(vline), ord('-'))

@requires_wide_build
def test_border_default_characters(self):
# 0 requests the default character, as an omitted argument does,
# even in a border drawn with wide characters.
win = curses.newwin(5, 10, 5, 2)
maxy, maxx = win.getmaxyx()
corners = [(0, 0), (0, maxx-1), (maxy-1, 0), (maxy-1, maxx-1)]
win.border('|', '|', '-', '-', 0, 0, 0, 0)
with_zeros = [win.in_wch(y, x) for y, x in corners]
win.erase()
win.border('|', '|', '-', '-')
self.assertEqual([win.in_wch(y, x) for y, x in corners], with_zeros)
win.border(0, '|', 0, '-', 0, 0, 0, 0)
vline = curses.complexchar('|')
hline = curses.complexchar('-')
win.border(vline, vline, hline, hline, 0, 0, 0, 0)
# box() takes 0 for either side, and draws the same default
# characters as an omitted border() argument.
win.erase()
win.border('|', '|')
default_corner = win.in_wch(0, 0)
default_hline = win.in_wch(0, 1)
win.erase()
win.border(0, 0, '-', '-')
default_vline = win.in_wch(1, 0)
win.erase()
win.box('|', 0)
self.assertEqual(win.in_wch(0, 0), default_corner)
self.assertEqual(win.in_wch(0, 1), default_hline)
win.erase()
win.box(0, '-')
self.assertEqual(win.in_wch(1, 0), default_vline)
win.box(vline, 0)

@requires_wide_build
def test_border_mixed_characters(self):
# Integer and bytes characters other than 0 are only drawn by the
# narrow function, which draws string characters as single bytes.
win = curses.newwin(5, 10, 5, 2)
win.border('|', '|', '-', '-', 65, 66, 67, 68)
self.assertEqual(win.instr(0, 0), b'A--------B')
self.assertEqual(win.instr(1, 0), b'| |')
self.assertEqual(win.instr(4, 0), b'C--------D')
win.border('|', b'!')
self.assertEqual(win.instr(1, 0), b'| !')
# b'\0' is a byte character, not the sentinel, but the narrow function
# draws a zero character as the default one.
win.border('|', b'\0')
# A complexchar cannot be drawn as a byte.
cc = curses.complexchar('|')
self.assertRaises(TypeError, win.border, cc, 65)
self.assertRaises(TypeError, win.border, cc, b'!')
# Neither can a string character that is not a single byte.
vline = '\u2502'
if len(vline.encode(win.encoding, 'replace')) != 1:
self.assertRaises(OverflowError, win.border, vline, 65)
# box() follows the same rules.
win.box('|', 45)
self.assertEqual(win.instr(1, 0), b'| |')
win.box(b'|', '-')
self.assertRaises(TypeError, win.box, cc, 45)
self.assertRaises(TypeError, win.box, cc, b'-')
if len(vline.encode(win.encoding, 'replace')) != 1:
self.assertRaises(OverflowError, win.box, vline, 45)

@requires_wide_build
def test_wacs_constants(self):
# Every ACS_* code has a WACS_* character cell counterpart, plus the
# double-line and thick-line codes, which have no ACS_* counterpart.
acs = {name.removeprefix('ACS_')
for name in dir(curses) if name.startswith('ACS_')}
wacs = {name.removeprefix('WACS_')
for name in dir(curses) if name.startswith('WACS_')}
extra = {name for pair in WACS_LINE_ALIASES for name in pair}
self.assertEqual(wacs - extra, acs)
for name in sorted(wacs):
with self.subTest(name=name):
self.assertIsInstance(getattr(curses, 'WACS_' + name),
curses.complexchar)
# The alternate names refer to the same cells.
self.assertEqual(curses.WACS_BSSB, curses.WACS_ULCORNER)
self.assertEqual(curses.WACS_BSBS, curses.WACS_HLINE)
self.assertEqual(curses.WACS_SBSB, curses.WACS_VLINE)
self.assertEqual(curses.WACS_SSSS, curses.WACS_PLUS)

@requires_wide_build
def test_wacs_line_constants(self):
# The double-line and thick-line codes are optional, but a supporting
# implementation provides the whole family under both names.
present = [name for name, alias in WACS_LINE_ALIASES
if hasattr(curses, 'WACS_' + name)]
if not present:
self.skipTest('requires double-line and thick-line characters')
self.assertEqual(len(present), len(WACS_LINE_ALIASES))
for name, alias in WACS_LINE_ALIASES:
with self.subTest(name=name):
cell = getattr(curses, 'WACS_' + name)
self.assertIsInstance(cell, curses.complexchar)
self.assertEqual(getattr(curses, 'WACS_' + alias), cell)
# They are distinct from the single-line characters.
self.assertNotEqual(curses.WACS_D_HLINE, curses.WACS_HLINE)
self.assertNotEqual(curses.WACS_T_HLINE, curses.WACS_HLINE)
self.assertNotEqual(curses.WACS_D_HLINE, curses.WACS_T_HLINE)
stdscr = self.stdscr
stdscr.border(curses.WACS_D_VLINE, curses.WACS_D_VLINE,
curses.WACS_D_HLINE, curses.WACS_D_HLINE,
curses.WACS_D_ULCORNER, curses.WACS_D_URCORNER,
curses.WACS_D_LLCORNER, curses.WACS_D_LRCORNER)
self.assertEqual(stdscr.in_wch(0, 0), curses.WACS_D_ULCORNER)
self.assertEqual(stdscr.in_wch(0, 1), curses.WACS_D_HLINE)

@requires_wide_build
def test_wacs_in_cell_methods(self):
# A WACS_* cell can be used wherever a character cell is accepted.
stdscr = self.stdscr
stdscr.addch(0, 0, curses.WACS_ULCORNER)
self.assertEqual(stdscr.in_wch(0, 0), curses.WACS_ULCORNER)
stdscr.insch(1, 0, curses.WACS_DIAMOND)
self.assertEqual(stdscr.in_wch(1, 0), curses.WACS_DIAMOND)
stdscr.hline(2, 0, curses.WACS_HLINE, 5)
self.assertEqual(stdscr.in_wch(2, 4), curses.WACS_HLINE)
stdscr.vline(3, 0, curses.WACS_VLINE, 3)
self.assertEqual(stdscr.in_wch(5, 0), curses.WACS_VLINE)
stdscr.border(curses.WACS_VLINE, curses.WACS_VLINE,
curses.WACS_HLINE, curses.WACS_HLINE,
curses.WACS_ULCORNER, curses.WACS_URCORNER,
curses.WACS_LLCORNER, curses.WACS_LRCORNER)
self.assertEqual(stdscr.in_wch(0, 0), curses.WACS_ULCORNER)
stdscr.box(curses.WACS_VLINE, curses.WACS_HLINE)
self.assertEqual(stdscr.in_wch(0, 1), curses.WACS_HLINE)

def test_complexchar_in_cell_methods(self):
# Every single-character-cell method also accepts a complexchar, whose
Expand Down Expand Up @@ -910,6 +1061,30 @@ def test_output_string_attr_restored(self):
func(0, 0, *args, curses.A_BOLD)
self.assertEqual(win.getattrs(), curses.A_UNDERLINE)

@requires_colors
@requires_curses_window_meth('color_set')
@requires_curses_window_meth('attr_get')
def test_output_string_pair_restored(self):
# The rendition put back after a write includes the color pair, also
# when it is larger than the A_COLOR field of a chtype holds.
pairs = [7]
if curses.has_extended_color_support() and curses.COLOR_PAIRS > 300:
pairs.append(300)
win = curses.newwin(2, 10, 0, 0)
for pair in pairs:
curses.init_pair(pair, curses.COLOR_RED, curses.COLOR_BLACK)
for func, args in [(win.addstr, ('x',)), (win.addnstr, ('x', 1)),
(win.insstr, ('x',)), (win.insnstr, ('x', 1))]:
with self.subTest(func.__qualname__, pair=pair):
win.color_set(pair)
func(0, 0, *args, curses.A_BOLD)
self.assertEqual(win.attr_get()[1], pair)
win.color_set(pair)
# y=100 is outside the window, so the write fails.
self.assertRaises(curses.error, func, 100, 0, *args,
curses.A_BOLD)
self.assertEqual(win.attr_get()[1], pair)

def test_add_string_behavior(self):
# addstr() advances the cursor past the written text; addnstr()
# writes at most n characters.
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -6411,7 +6411,7 @@ def test_typing_module_has_signatures(self):
methods_unsupported_signature=methods_unsupported_signature)

def test_warnings_module_has_signatures(self):
unsupported_signature = {'warn', 'warn_explicit'}
unsupported_signature = {'warn_explicit'}
self._test_module_has_signatures(warnings, unsupported_signature=unsupported_signature)

def test_weakref_module_has_signatures(self):
Expand Down
5 changes: 5 additions & 0 deletions Lib/test/test_sqlite3/test_dbapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -1075,9 +1075,14 @@ def test_invalid_array_size(self):
UINT32_MAX = (1 << 32) - 1
setter = functools.partial(setattr, self.cu, 'arraysize')

self.cu.arraysize = 2
self.assertRaises(TypeError, setter, 1.0)
self.assertRaises(ValueError, setter, -3)
self.assertRaises(OverflowError, setter, UINT32_MAX + 1)
self.assertRaises(OverflowError, setter, 2**1000)
self.assertRaises(ValueError, setter, -2**1000)
# a failed assignment does not change the value
self.assertEqual(self.cu.arraysize, 2)

def test_fetchmany(self):
# no active SQL statement
Expand Down
8 changes: 8 additions & 0 deletions Lib/test/test_sqlite3/test_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ def test_delete_connection_text_factory(self):
with self.assertRaises(AttributeError):
del self.con.text_factory

def test_delete_cursor_row_factory(self):
# gh-149738: deleting row_factory should raise an exception
cur = self.con.cursor()
with self.assertRaises(AttributeError):
del cur.row_factory
# Executing a query here should succeed.
self.assertEqual(tuple(cur.execute("select 1").fetchone()), (1,))

def test_sqlite_row_index_unicode(self):
row = self.con.execute("select 1 as \xff").fetchone()
self.assertEqual(row["\xff"], 1)
Expand Down
17 changes: 16 additions & 1 deletion Lib/test/test_sqlite3/test_transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,10 +389,25 @@ def test_autocommit_setget(self):

def test_autocommit_setget_invalid(self):
msg = "autocommit must be True, False, or.*LEGACY"
for mode in "a", 12, (), None:
for mode in "a", 12, (), None, 2**1000, -2**1000:
with self.subTest(mode=mode):
with self.assertRaisesRegex(ValueError, msg):
sqlite.connect(":memory:", autocommit=mode)
with memory_database() as cx:
with self.assertRaisesRegex(ValueError, msg):
cx.autocommit = mode
# a failed assignment does not change the value
self.assertEqual(cx.autocommit,
sqlite.LEGACY_TRANSACTION_CONTROL)

def test_autocommit_delete(self):
with memory_database() as cx:
cx.autocommit = False
with self.assertRaisesRegex(AttributeError,
"cannot delete autocommit attribute"):
del cx.autocommit
# a failed deletion does not change the value
self.assertIs(cx.autocommit, False)

def test_autocommit_disabled(self):
expected = [
Expand Down
18 changes: 18 additions & 0 deletions Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -5518,6 +5518,12 @@ def test_keylog_defaults(self):
with self.assertRaises(TypeError):
ctx.keylog_filename = 1

ctx.keylog_filename = os_helper.TESTFN
with self.assertRaisesRegex(AttributeError, 'cannot be deleted'):
del ctx.keylog_filename
# a failed deletion does not change the value
self.assertEqual(ctx.keylog_filename, os_helper.TESTFN)

def test_keylog_filename(self):
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
client_context, server_context, hostname = testing_context()
Expand Down Expand Up @@ -5592,6 +5598,18 @@ def msg_cb(conn, direction, version, content_type, msg_type, data):
with self.assertRaises(TypeError):
client_context._msg_callback = object()

# the attribute of the underlying C type accepts only a callable
# and cannot be deleted
descr = _ssl._SSLContext.__dict__['_msg_callback']
with self.assertRaises(TypeError):
descr.__set__(client_context, object())
# a failed assignment does not change the value
self.assertIs(client_context._msg_callback, msg_cb)
with self.assertRaisesRegex(AttributeError, 'cannot be deleted'):
descr.__delete__(client_context)
# a failed deletion does not change the value
self.assertIs(client_context._msg_callback, msg_cb)

def test_msg_callback_exception(self):
client_context, server_context, hostname = testing_context()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
:mod:`sqlite3`: Disallow removing ``row_factory`` and ``text_factory`` attributes
of a connection to prevent a crash on a query.
of a connection or cursor to prevent a crash on a query.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Add the ``WACS_*`` constants to the :mod:`curses` module. They are the
counterparts of the ``ACS_*`` line-drawing codes as :class:`curses.complexchar`
cells, and are added, like the latter, by :func:`curses.initscr` and
:func:`curses.newterm`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix the signatures of :meth:`sqlite3.Connection.execute`, :func:`warnings.warn`
and :func:`locale.setlocale` which rendered ``<unrepresentable>`` instead of the
correct defaults for parameters.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix a crash when deleting the ``keylog_filename`` attribute of
:class:`ssl.SSLContext`.
It now raises :exc:`AttributeError`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix crashes in :class:`sqlite3.Connection` when deleting the
:attr:`~sqlite3.Connection.autocommit` attribute or setting it to an integer
which does not fit in C :c:expr:`long`.
Both now raise an exception.
Loading
Loading