gh-97928: Partially restore the behavior of tkinter.Text.count() by default (GH-115031)

By default, it preserves an inconsistent behavior of older Python
versions: packs the count into a 1-tuple if only one or none
options are specified (including 'update'), returns None instead of 0.
Except that setting wantobjects to 0 no longer affects the result.

Add a new parameter return_ints: specifying return_ints=True makes
Text.count() always returning the single count as an integer
instead of a 1-tuple or None.
This commit is contained in:
Serhiy Storchaka 2024-02-11 12:43:14 +02:00 committed by GitHub
parent 5d2794a16b
commit d2c4baa41f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 59 additions and 28 deletions

View file

@ -469,6 +469,12 @@ tkinter
a dict instead of a tuple. a dict instead of a tuple.
(Contributed by Serhiy Storchaka in :gh:`43457`.) (Contributed by Serhiy Storchaka in :gh:`43457`.)
* Add new optional keyword-only parameter *return_ints* in
the :meth:`!Text.count` method.
Passing ``return_ints=True`` makes it always returning the single count
as an integer instead of a 1-tuple or ``None``.
(Contributed by Serhiy Storchaka in :gh:`97928`.)
* Add support of the "vsapi" element type in * Add support of the "vsapi" element type in
the :meth:`~tkinter.ttk.Style.element_create` method of the :meth:`~tkinter.ttk.Style.element_create` method of
:class:`tkinter.ttk.Style`. :class:`tkinter.ttk.Style`.
@ -1286,13 +1292,6 @@ that may require changes to your code.
Changes in the Python API Changes in the Python API
------------------------- -------------------------
* :meth:`!tkinter.Text.count` now always returns an integer if one or less
counting options are specified.
Previously it could return a single count as a 1-tuple, an integer (only if
option ``"update"`` was specified) or ``None`` if no items found.
The result is now the same if ``wantobjects`` is set to ``0``.
(Contributed by Serhiy Storchaka in :gh:`97928`.)
* Functions :c:func:`PyDict_GetItem`, :c:func:`PyDict_GetItemString`, * Functions :c:func:`PyDict_GetItem`, :c:func:`PyDict_GetItemString`,
:c:func:`PyMapping_HasKey`, :c:func:`PyMapping_HasKeyString`, :c:func:`PyMapping_HasKey`, :c:func:`PyMapping_HasKeyString`,
:c:func:`PyObject_HasAttr`, :c:func:`PyObject_HasAttrString`, and :c:func:`PyObject_HasAttr`, :c:func:`PyObject_HasAttrString`, and

View file

@ -27,7 +27,7 @@ def get_displaylines(text, index):
"""Display height, in lines, of a logical line in a Tk text widget.""" """Display height, in lines, of a logical line in a Tk text widget."""
return text.count(f"{index} linestart", return text.count(f"{index} linestart",
f"{index} lineend", f"{index} lineend",
"displaylines") "displaylines", return_ints=True)
def get_widget_padding(widget): def get_widget_padding(widget):
"""Get the total padding of a Tk widget, including its border.""" """Get the total padding of a Tk widget, including its border."""

View file

@ -52,27 +52,47 @@ def test_count(self):
options = ('chars', 'indices', 'lines', options = ('chars', 'indices', 'lines',
'displaychars', 'displayindices', 'displaylines', 'displaychars', 'displayindices', 'displaylines',
'xpixels', 'ypixels') 'xpixels', 'ypixels')
self.assertEqual(len(text.count('1.0', 'end', *options, return_ints=True)), 8)
self.assertEqual(len(text.count('1.0', 'end', *options)), 8) self.assertEqual(len(text.count('1.0', 'end', *options)), 8)
self.assertEqual(text.count('1.0', 'end', 'chars', 'lines'), (124, 4)) self.assertEqual(text.count('1.0', 'end', 'chars', 'lines', return_ints=True),
(124, 4))
self.assertEqual(text.count('1.3', '4.5', 'chars', 'lines'), (92, 3)) self.assertEqual(text.count('1.3', '4.5', 'chars', 'lines'), (92, 3))
self.assertEqual(text.count('4.5', '1.3', 'chars', 'lines', return_ints=True),
(-92, -3))
self.assertEqual(text.count('4.5', '1.3', 'chars', 'lines'), (-92, -3)) self.assertEqual(text.count('4.5', '1.3', 'chars', 'lines'), (-92, -3))
self.assertEqual(text.count('1.3', '1.3', 'chars', 'lines', return_ints=True),
(0, 0))
self.assertEqual(text.count('1.3', '1.3', 'chars', 'lines'), (0, 0)) self.assertEqual(text.count('1.3', '1.3', 'chars', 'lines'), (0, 0))
self.assertEqual(text.count('1.0', 'end', 'lines'), 4) self.assertEqual(text.count('1.0', 'end', 'lines', return_ints=True), 4)
self.assertEqual(text.count('end', '1.0', 'lines'), -4) self.assertEqual(text.count('1.0', 'end', 'lines'), (4,))
self.assertEqual(text.count('1.3', '1.5', 'lines'), 0) self.assertEqual(text.count('end', '1.0', 'lines', return_ints=True), -4)
self.assertEqual(text.count('1.3', '1.3', 'lines'), 0) self.assertEqual(text.count('end', '1.0', 'lines'), (-4,))
self.assertEqual(text.count('1.0', 'end'), 124) # 'indices' by default self.assertEqual(text.count('1.3', '1.5', 'lines', return_ints=True), 0)
self.assertEqual(text.count('1.0', 'end', 'indices'), 124) self.assertEqual(text.count('1.3', '1.5', 'lines'), None)
self.assertEqual(text.count('1.3', '1.3', 'lines', return_ints=True), 0)
self.assertEqual(text.count('1.3', '1.3', 'lines'), None)
# Count 'indices' by default.
self.assertEqual(text.count('1.0', 'end', return_ints=True), 124)
self.assertEqual(text.count('1.0', 'end'), (124,))
self.assertEqual(text.count('1.0', 'end', 'indices', return_ints=True), 124)
self.assertEqual(text.count('1.0', 'end', 'indices'), (124,))
self.assertRaises(tkinter.TclError, text.count, '1.0', 'end', 'spam') self.assertRaises(tkinter.TclError, text.count, '1.0', 'end', 'spam')
self.assertRaises(tkinter.TclError, text.count, '1.0', 'end', '-lines') self.assertRaises(tkinter.TclError, text.count, '1.0', 'end', '-lines')
self.assertIsInstance(text.count('1.3', '1.5', 'ypixels'), int) self.assertIsInstance(text.count('1.3', '1.5', 'ypixels', return_ints=True), int)
self.assertIsInstance(text.count('1.3', '1.5', 'ypixels'), tuple)
self.assertIsInstance(text.count('1.3', '1.5', 'update', 'ypixels', return_ints=True), int)
self.assertIsInstance(text.count('1.3', '1.5', 'update', 'ypixels'), int) self.assertIsInstance(text.count('1.3', '1.5', 'update', 'ypixels'), int)
self.assertEqual(text.count('1.3', '1.3', 'update', 'ypixels'), 0) self.assertEqual(text.count('1.3', '1.3', 'update', 'ypixels', return_ints=True), 0)
self.assertEqual(text.count('1.3', '1.3', 'update', 'ypixels'), None)
self.assertEqual(text.count('1.3', '1.5', 'update', 'indices', return_ints=True), 2)
self.assertEqual(text.count('1.3', '1.5', 'update', 'indices'), 2) self.assertEqual(text.count('1.3', '1.5', 'update', 'indices'), 2)
self.assertEqual(text.count('1.3', '1.3', 'update', 'indices'), 0) self.assertEqual(text.count('1.3', '1.3', 'update', 'indices', return_ints=True), 0)
self.assertEqual(text.count('1.3', '1.5', 'update'), 2) self.assertEqual(text.count('1.3', '1.3', 'update', 'indices'), None)
self.assertEqual(text.count('1.3', '1.3', 'update'), 0) self.assertEqual(text.count('1.3', '1.5', 'update', return_ints=True), 2)
self.assertEqual(text.count('1.3', '1.5', 'update'), (2,))
self.assertEqual(text.count('1.3', '1.3', 'update', return_ints=True), 0)
self.assertEqual(text.count('1.3', '1.3', 'update'), None)
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -3745,7 +3745,7 @@ def compare(self, index1, op, index2):
return self.tk.getboolean(self.tk.call( return self.tk.getboolean(self.tk.call(
self._w, 'compare', index1, op, index2)) self._w, 'compare', index1, op, index2))
def count(self, index1, index2, *options): # new in Tk 8.5 def count(self, index1, index2, *options, return_ints=False): # new in Tk 8.5
"""Counts the number of relevant things between the two indices. """Counts the number of relevant things between the two indices.
If INDEX1 is after INDEX2, the result will be a negative number If INDEX1 is after INDEX2, the result will be a negative number
@ -3753,19 +3753,26 @@ def count(self, index1, index2, *options): # new in Tk 8.5
The actual items which are counted depends on the options given. The actual items which are counted depends on the options given.
The result is a tuple of integers, one for the result of each The result is a tuple of integers, one for the result of each
counting option given, if more than one option is specified, counting option given, if more than one option is specified or
otherwise it is an integer. Valid counting options are "chars", return_ints is false (default), otherwise it is an integer.
"displaychars", "displayindices", "displaylines", "indices", Valid counting options are "chars", "displaychars",
"lines", "xpixels" and "ypixels". The default value, if no "displayindices", "displaylines", "indices", "lines", "xpixels"
option is specified, is "indices". There is an additional possible and "ypixels". The default value, if no option is specified, is
option "update", which if given then all subsequent options ensure "indices". There is an additional possible option "update",
that any possible out of date information is recalculated.""" which if given then all subsequent options ensure that any
possible out of date information is recalculated.
"""
options = ['-%s' % arg for arg in options] options = ['-%s' % arg for arg in options]
res = self.tk.call(self._w, 'count', *options, index1, index2) res = self.tk.call(self._w, 'count', *options, index1, index2)
if not isinstance(res, int): if not isinstance(res, int):
res = self._getints(res) res = self._getints(res)
if len(res) == 1: if len(res) == 1:
res, = res res, = res
if not return_ints:
if not res:
res = None
elif len(options) <= 1:
res = (res,)
return res return res
def debug(self, boolean=None): def debug(self, boolean=None):

View file

@ -0,0 +1,5 @@
Partially revert the behavior of :meth:`tkinter.Text.count`. By default it
preserves the behavior of older Python versions, except that setting
``wantobjects`` to 0 no longer has effect. Add a new parameter *return_ints*:
specifying ``return_ints=True`` makes ``Text.count()`` always returning the
single count as an integer instead of a 1-tuple or ``None``.