gh-130693: Support more options for search in tkinter.Text (GH-130848)

* Add parameters nolinestop and strictlimits in the tkinter.Text.search() method.
* Add the tkinter.Text.search_all() method.
* Add more tests for tkinter.Text.search().
* stopindex is now only ignored if it is None.
This commit is contained in:
R.C.M 2025-11-17 09:42:26 -05:00 committed by GitHub
parent f6dd9c12a8
commit 3d14805947
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 154 additions and 8 deletions

View file

@ -734,6 +734,19 @@ timeit
:ref:`environment variables <using-on-controlling-color>`. :ref:`environment variables <using-on-controlling-color>`.
(Contributed by Yi Hong in :gh:`139374`.) (Contributed by Yi Hong in :gh:`139374`.)
tkinter
-------
* The :meth:`!tkinter.Text.search` method now supports two additional
arguments: *nolinestop* which allows the search to
continue across line boundaries;
and *strictlimits* which restricts the search to within the specified range.
(Contributed by Rihaan Meher in :gh:`130848`)
* A new method :meth:`!tkinter.Text.search_all` has been introduced.
This method allows for searching for all matches of a pattern
using Tcl's ``-all`` and ``-overlap`` options.
(Contributed by Rihaan Meher in :gh:`130848`)
types types
------ ------

View file

@ -34,12 +34,116 @@ def test_search(self):
# Invalid text index. # Invalid text index.
self.assertRaises(tkinter.TclError, text.search, '', 0) self.assertRaises(tkinter.TclError, text.search, '', 0)
self.assertRaises(tkinter.TclError, text.search, '', '')
self.assertRaises(tkinter.TclError, text.search, '', 'invalid')
self.assertRaises(tkinter.TclError, text.search, '', '1.0', 0)
self.assertRaises(tkinter.TclError, text.search, '', '1.0', '')
self.assertRaises(tkinter.TclError, text.search, '', '1.0', 'invalid')
# Check if we are getting the indices as strings -- you are likely text.insert('1.0',
# to get Tcl_Obj under Tk 8.5 if Tkinter doesn't convert it. 'This is a test. This is only a test.\n'
text.insert('1.0', 'hi-test') 'Another line.\n'
self.assertEqual(text.search('-test', '1.0', 'end'), '1.2') 'Yet another line.\n'
self.assertEqual(text.search('test', '1.0', 'end'), '1.3') '64-bit')
self.assertEqual(text.search('test', '1.0'), '1.10')
self.assertEqual(text.search('test', '1.0', 'end'), '1.10')
self.assertEqual(text.search('test', '1.0', '1.10'), '')
self.assertEqual(text.search('test', '1.11'), '1.31')
self.assertEqual(text.search('test', '1.32', 'end'), '')
self.assertEqual(text.search('test', '1.32'), '1.10')
self.assertEqual(text.search('', '1.0'), '1.0') # empty pattern
self.assertEqual(text.search('nonexistent', '1.0'), '')
self.assertEqual(text.search('-bit', '1.0'), '4.2') # starts with a hyphen
self.assertEqual(text.search('line', '3.0'), '3.12')
self.assertEqual(text.search('line', '3.0', forwards=True), '3.12')
self.assertEqual(text.search('line', '3.0', backwards=True), '2.8')
self.assertEqual(text.search('line', '3.0', forwards=True, backwards=True), '2.8')
self.assertEqual(text.search('t.', '1.0'), '1.13')
self.assertEqual(text.search('t.', '1.0', exact=True), '1.13')
self.assertEqual(text.search('t.', '1.0', regexp=True), '1.10')
self.assertEqual(text.search('t.', '1.0', exact=True, regexp=True), '1.10')
self.assertEqual(text.search('TEST', '1.0'), '')
self.assertEqual(text.search('TEST', '1.0', nocase=True), '1.10')
self.assertEqual(text.search('.*line', '1.0', regexp=True), '2.0')
self.assertEqual(text.search('.*line', '1.0', regexp=True, nolinestop=True), '1.0')
self.assertEqual(text.search('test', '1.0', '1.13'), '1.10')
self.assertEqual(text.search('test', '1.0', '1.13', strictlimits=True), '')
self.assertEqual(text.search('test', '1.0', '1.14', strictlimits=True), '1.10')
var = tkinter.Variable(self.root)
self.assertEqual(text.search('test', '1.0', count=var), '1.10')
self.assertEqual(var.get(), 4 if self.wantobjects else '4')
# TODO: Add test for elide=True
def test_search_all(self):
text = self.text
# pattern and index are obligatory arguments.
self.assertRaises(tkinter.TclError, text.search_all, None, '1.0')
self.assertRaises(tkinter.TclError, text.search_all, 'a', None)
self.assertRaises(tkinter.TclError, text.search_all, None, None)
# Keyword-only arguments
self.assertRaises(TypeError, text.search_all, 'a', '1.0', 'end', None)
# Invalid text index.
self.assertRaises(tkinter.TclError, text.search_all, '', 0)
self.assertRaises(tkinter.TclError, text.search_all, '', '')
self.assertRaises(tkinter.TclError, text.search_all, '', 'invalid')
self.assertRaises(tkinter.TclError, text.search_all, '', '1.0', 0)
self.assertRaises(tkinter.TclError, text.search_all, '', '1.0', '')
self.assertRaises(tkinter.TclError, text.search_all, '', '1.0', 'invalid')
def eq(res, expected):
self.assertIsInstance(res, tuple)
self.assertEqual([str(i) for i in res], expected)
text.insert('1.0', 'ababa\naba\n64-bit')
eq(text.search_all('aba', '1.0'), ['1.0', '2.0'])
eq(text.search_all('aba', '1.0', 'end'), ['1.0', '2.0'])
eq(text.search_all('aba', '1.1', 'end'), ['1.2', '2.0'])
eq(text.search_all('aba', '1.1'), ['1.2', '2.0', '1.0'])
res = text.search_all('', '1.0') # empty pattern
eq(res[:5], ['1.0', '1.1', '1.2', '1.3', '1.4'])
eq(res[-5:], ['3.2', '3.3', '3.4', '3.5', '3.6'])
eq(text.search_all('nonexistent', '1.0'), [])
eq(text.search_all('-bit', '1.0'), ['3.2']) # starts with a hyphen
eq(text.search_all('aba', '1.0', 'end', forwards=True), ['1.0', '2.0'])
eq(text.search_all('aba', 'end', '1.0', backwards=True), ['2.0', '1.2'])
eq(text.search_all('aba', '1.0', overlap=True), ['1.0', '1.2', '2.0'])
eq(text.search_all('aba', 'end', '1.0', overlap=True, backwards=True), ['2.0', '1.2', '1.0'])
eq(text.search_all('aba', '1.0', exact=True), ['1.0', '2.0'])
eq(text.search_all('a.a', '1.0', exact=True), [])
eq(text.search_all('a.a', '1.0', regexp=True), ['1.0', '2.0'])
eq(text.search_all('ABA', '1.0'), [])
eq(text.search_all('ABA', '1.0', nocase=True), ['1.0', '2.0'])
eq(text.search_all('a.a', '1.0', regexp=True), ['1.0', '2.0'])
eq(text.search_all('a.a', '1.0', regexp=True, nolinestop=True), ['1.0', '1.4'])
eq(text.search_all('aba', '1.0', '2.2'), ['1.0', '2.0'])
eq(text.search_all('aba', '1.0', '2.2', strictlimits=True), ['1.0'])
eq(text.search_all('aba', '1.0', '2.3', strictlimits=True), ['1.0', '2.0'])
var = tkinter.Variable(self.root)
eq(text.search_all('aba', '1.0', count=var), ['1.0', '2.0'])
self.assertEqual(var.get(), (3, 3) if self.wantobjects else '3 3')
# TODO: Add test for elide=True
def test_count(self): def test_count(self):
text = self.text text = self.text

View file

@ -4049,8 +4049,9 @@ def scan_dragto(self, x, y):
self.tk.call(self._w, 'scan', 'dragto', x, y) self.tk.call(self._w, 'scan', 'dragto', x, y)
def search(self, pattern, index, stopindex=None, def search(self, pattern, index, stopindex=None,
forwards=None, backwards=None, exact=None, forwards=None, backwards=None, exact=None,
regexp=None, nocase=None, count=None, elide=None): regexp=None, nocase=None, count=None,
elide=None, *, nolinestop=None, strictlimits=None):
"""Search PATTERN beginning from INDEX until STOPINDEX. """Search PATTERN beginning from INDEX until STOPINDEX.
Return the index of the first character of a match or an Return the index of the first character of a match or an
empty string.""" empty string."""
@ -4062,12 +4063,39 @@ def search(self, pattern, index, stopindex=None,
if nocase: args.append('-nocase') if nocase: args.append('-nocase')
if elide: args.append('-elide') if elide: args.append('-elide')
if count: args.append('-count'); args.append(count) if count: args.append('-count'); args.append(count)
if nolinestop: args.append('-nolinestop')
if strictlimits: args.append('-strictlimits')
if pattern and pattern[0] == '-': args.append('--') if pattern and pattern[0] == '-': args.append('--')
args.append(pattern) args.append(pattern)
args.append(index) args.append(index)
if stopindex: args.append(stopindex) if stopindex is not None: args.append(stopindex)
return str(self.tk.call(tuple(args))) return str(self.tk.call(tuple(args)))
def search_all(self, pattern, index, stopindex=None, *,
forwards=None, backwards=None, exact=None,
regexp=None, nocase=None, count=None,
elide=None, nolinestop=None, overlap=None,
strictlimits=None):
"""Search all occurrences of PATTERN from INDEX to STOPINDEX.
Return a tuple of indices where matches begin."""
args = [self._w, 'search', '-all']
if forwards: args.append('-forwards')
if backwards: args.append('-backwards')
if exact: args.append('-exact')
if regexp: args.append('-regexp')
if nocase: args.append('-nocase')
if elide: args.append('-elide')
if count: args.append('-count'); args.append(count)
if nolinestop: args.append('-nolinestop')
if overlap: args.append('-overlap')
if strictlimits: args.append('-strictlimits')
if pattern and pattern[0] == '-': args.append('--')
args.append(pattern)
args.append(index)
if stopindex is not None: args.append(stopindex)
result = self.tk.call(tuple(args))
return self.tk.splitlist(result)
def see(self, index): def see(self, index):
"""Scroll such that the character at INDEX is visible.""" """Scroll such that the character at INDEX is visible."""
self.tk.call(self._w, 'see', index) self.tk.call(self._w, 'see', index)

View file

@ -0,0 +1 @@
Add support for ``-nolinestop``, and ``-strictlimits`` options to :meth:`!tkinter.Text.search`. Also add the :meth:`!tkinter.Text.search_all` method for ``-all`` and ``-overlap`` options.