Skip to content

Commit 3d14805

Browse files
authored
pythongh-130693: Support more options for search in tkinter.Text (pythonGH-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.
1 parent f6dd9c1 commit 3d14805

File tree

4 files changed

+154
-8
lines changed

4 files changed

+154
-8
lines changed

Doc/whatsnew/3.15.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,19 @@ timeit
734734
:ref:`environment variables <using-on-controlling-color>`.
735735
(Contributed by Yi Hong in :gh:`139374`.)
736736

737+
tkinter
738+
-------
739+
740+
* The :meth:`!tkinter.Text.search` method now supports two additional
741+
arguments: *nolinestop* which allows the search to
742+
continue across line boundaries;
743+
and *strictlimits* which restricts the search to within the specified range.
744+
(Contributed by Rihaan Meher in :gh:`130848`)
745+
746+
* A new method :meth:`!tkinter.Text.search_all` has been introduced.
747+
This method allows for searching for all matches of a pattern
748+
using Tcl's ``-all`` and ``-overlap`` options.
749+
(Contributed by Rihaan Meher in :gh:`130848`)
737750

738751
types
739752
------

Lib/test/test_tkinter/test_text.py

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,116 @@ def test_search(self):
3434

3535
# Invalid text index.
3636
self.assertRaises(tkinter.TclError, text.search, '', 0)
37+
self.assertRaises(tkinter.TclError, text.search, '', '')
38+
self.assertRaises(tkinter.TclError, text.search, '', 'invalid')
39+
self.assertRaises(tkinter.TclError, text.search, '', '1.0', 0)
40+
self.assertRaises(tkinter.TclError, text.search, '', '1.0', '')
41+
self.assertRaises(tkinter.TclError, text.search, '', '1.0', 'invalid')
3742

38-
# Check if we are getting the indices as strings -- you are likely
39-
# to get Tcl_Obj under Tk 8.5 if Tkinter doesn't convert it.
40-
text.insert('1.0', 'hi-test')
41-
self.assertEqual(text.search('-test', '1.0', 'end'), '1.2')
42-
self.assertEqual(text.search('test', '1.0', 'end'), '1.3')
43+
text.insert('1.0',
44+
'This is a test. This is only a test.\n'
45+
'Another line.\n'
46+
'Yet another line.\n'
47+
'64-bit')
48+
49+
self.assertEqual(text.search('test', '1.0'), '1.10')
50+
self.assertEqual(text.search('test', '1.0', 'end'), '1.10')
51+
self.assertEqual(text.search('test', '1.0', '1.10'), '')
52+
self.assertEqual(text.search('test', '1.11'), '1.31')
53+
self.assertEqual(text.search('test', '1.32', 'end'), '')
54+
self.assertEqual(text.search('test', '1.32'), '1.10')
55+
56+
self.assertEqual(text.search('', '1.0'), '1.0') # empty pattern
57+
self.assertEqual(text.search('nonexistent', '1.0'), '')
58+
self.assertEqual(text.search('-bit', '1.0'), '4.2') # starts with a hyphen
59+
60+
self.assertEqual(text.search('line', '3.0'), '3.12')
61+
self.assertEqual(text.search('line', '3.0', forwards=True), '3.12')
62+
self.assertEqual(text.search('line', '3.0', backwards=True), '2.8')
63+
self.assertEqual(text.search('line', '3.0', forwards=True, backwards=True), '2.8')
64+
65+
self.assertEqual(text.search('t.', '1.0'), '1.13')
66+
self.assertEqual(text.search('t.', '1.0', exact=True), '1.13')
67+
self.assertEqual(text.search('t.', '1.0', regexp=True), '1.10')
68+
self.assertEqual(text.search('t.', '1.0', exact=True, regexp=True), '1.10')
69+
70+
self.assertEqual(text.search('TEST', '1.0'), '')
71+
self.assertEqual(text.search('TEST', '1.0', nocase=True), '1.10')
72+
73+
self.assertEqual(text.search('.*line', '1.0', regexp=True), '2.0')
74+
self.assertEqual(text.search('.*line', '1.0', regexp=True, nolinestop=True), '1.0')
75+
76+
self.assertEqual(text.search('test', '1.0', '1.13'), '1.10')
77+
self.assertEqual(text.search('test', '1.0', '1.13', strictlimits=True), '')
78+
self.assertEqual(text.search('test', '1.0', '1.14', strictlimits=True), '1.10')
79+
80+
var = tkinter.Variable(self.root)
81+
self.assertEqual(text.search('test', '1.0', count=var), '1.10')
82+
self.assertEqual(var.get(), 4 if self.wantobjects else '4')
83+
84+
# TODO: Add test for elide=True
85+
86+
def test_search_all(self):
87+
text = self.text
88+
89+
# pattern and index are obligatory arguments.
90+
self.assertRaises(tkinter.TclError, text.search_all, None, '1.0')
91+
self.assertRaises(tkinter.TclError, text.search_all, 'a', None)
92+
self.assertRaises(tkinter.TclError, text.search_all, None, None)
93+
94+
# Keyword-only arguments
95+
self.assertRaises(TypeError, text.search_all, 'a', '1.0', 'end', None)
96+
97+
# Invalid text index.
98+
self.assertRaises(tkinter.TclError, text.search_all, '', 0)
99+
self.assertRaises(tkinter.TclError, text.search_all, '', '')
100+
self.assertRaises(tkinter.TclError, text.search_all, '', 'invalid')
101+
self.assertRaises(tkinter.TclError, text.search_all, '', '1.0', 0)
102+
self.assertRaises(tkinter.TclError, text.search_all, '', '1.0', '')
103+
self.assertRaises(tkinter.TclError, text.search_all, '', '1.0', 'invalid')
104+
105+
def eq(res, expected):
106+
self.assertIsInstance(res, tuple)
107+
self.assertEqual([str(i) for i in res], expected)
108+
109+
text.insert('1.0', 'ababa\naba\n64-bit')
110+
111+
eq(text.search_all('aba', '1.0'), ['1.0', '2.0'])
112+
eq(text.search_all('aba', '1.0', 'end'), ['1.0', '2.0'])
113+
eq(text.search_all('aba', '1.1', 'end'), ['1.2', '2.0'])
114+
eq(text.search_all('aba', '1.1'), ['1.2', '2.0', '1.0'])
115+
116+
res = text.search_all('', '1.0') # empty pattern
117+
eq(res[:5], ['1.0', '1.1', '1.2', '1.3', '1.4'])
118+
eq(res[-5:], ['3.2', '3.3', '3.4', '3.5', '3.6'])
119+
eq(text.search_all('nonexistent', '1.0'), [])
120+
eq(text.search_all('-bit', '1.0'), ['3.2']) # starts with a hyphen
121+
122+
eq(text.search_all('aba', '1.0', 'end', forwards=True), ['1.0', '2.0'])
123+
eq(text.search_all('aba', 'end', '1.0', backwards=True), ['2.0', '1.2'])
124+
125+
eq(text.search_all('aba', '1.0', overlap=True), ['1.0', '1.2', '2.0'])
126+
eq(text.search_all('aba', 'end', '1.0', overlap=True, backwards=True), ['2.0', '1.2', '1.0'])
127+
128+
eq(text.search_all('aba', '1.0', exact=True), ['1.0', '2.0'])
129+
eq(text.search_all('a.a', '1.0', exact=True), [])
130+
eq(text.search_all('a.a', '1.0', regexp=True), ['1.0', '2.0'])
131+
132+
eq(text.search_all('ABA', '1.0'), [])
133+
eq(text.search_all('ABA', '1.0', nocase=True), ['1.0', '2.0'])
134+
135+
eq(text.search_all('a.a', '1.0', regexp=True), ['1.0', '2.0'])
136+
eq(text.search_all('a.a', '1.0', regexp=True, nolinestop=True), ['1.0', '1.4'])
137+
138+
eq(text.search_all('aba', '1.0', '2.2'), ['1.0', '2.0'])
139+
eq(text.search_all('aba', '1.0', '2.2', strictlimits=True), ['1.0'])
140+
eq(text.search_all('aba', '1.0', '2.3', strictlimits=True), ['1.0', '2.0'])
141+
142+
var = tkinter.Variable(self.root)
143+
eq(text.search_all('aba', '1.0', count=var), ['1.0', '2.0'])
144+
self.assertEqual(var.get(), (3, 3) if self.wantobjects else '3 3')
145+
146+
# TODO: Add test for elide=True
43147

44148
def test_count(self):
45149
text = self.text

Lib/tkinter/__init__.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4049,8 +4049,9 @@ def scan_dragto(self, x, y):
40494049
self.tk.call(self._w, 'scan', 'dragto', x, y)
40504050

40514051
def search(self, pattern, index, stopindex=None,
4052-
forwards=None, backwards=None, exact=None,
4053-
regexp=None, nocase=None, count=None, elide=None):
4052+
forwards=None, backwards=None, exact=None,
4053+
regexp=None, nocase=None, count=None,
4054+
elide=None, *, nolinestop=None, strictlimits=None):
40544055
"""Search PATTERN beginning from INDEX until STOPINDEX.
40554056
Return the index of the first character of a match or an
40564057
empty string."""
@@ -4062,12 +4063,39 @@ def search(self, pattern, index, stopindex=None,
40624063
if nocase: args.append('-nocase')
40634064
if elide: args.append('-elide')
40644065
if count: args.append('-count'); args.append(count)
4066+
if nolinestop: args.append('-nolinestop')
4067+
if strictlimits: args.append('-strictlimits')
40654068
if pattern and pattern[0] == '-': args.append('--')
40664069
args.append(pattern)
40674070
args.append(index)
4068-
if stopindex: args.append(stopindex)
4071+
if stopindex is not None: args.append(stopindex)
40694072
return str(self.tk.call(tuple(args)))
40704073

4074+
def search_all(self, pattern, index, stopindex=None, *,
4075+
forwards=None, backwards=None, exact=None,
4076+
regexp=None, nocase=None, count=None,
4077+
elide=None, nolinestop=None, overlap=None,
4078+
strictlimits=None):
4079+
"""Search all occurrences of PATTERN from INDEX to STOPINDEX.
4080+
Return a tuple of indices where matches begin."""
4081+
args = [self._w, 'search', '-all']
4082+
if forwards: args.append('-forwards')
4083+
if backwards: args.append('-backwards')
4084+
if exact: args.append('-exact')
4085+
if regexp: args.append('-regexp')
4086+
if nocase: args.append('-nocase')
4087+
if elide: args.append('-elide')
4088+
if count: args.append('-count'); args.append(count)
4089+
if nolinestop: args.append('-nolinestop')
4090+
if overlap: args.append('-overlap')
4091+
if strictlimits: args.append('-strictlimits')
4092+
if pattern and pattern[0] == '-': args.append('--')
4093+
args.append(pattern)
4094+
args.append(index)
4095+
if stopindex is not None: args.append(stopindex)
4096+
result = self.tk.call(tuple(args))
4097+
return self.tk.splitlist(result)
4098+
40714099
def see(self, index):
40724100
"""Scroll such that the character at INDEX is visible."""
40734101
self.tk.call(self._w, 'see', index)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

0 commit comments

Comments
 (0)