1
1
"""Classes that replace tkinter gui objects used by an object being tested.
2
+
2
3
A gui object is anything with a master or parent paramenter, which is typically
3
4
required in spite of what the doc strings say.
4
5
"""
@@ -15,8 +16,10 @@ def get(self):
15
16
return self .value
16
17
17
18
class Mbox_func :
18
- """Generic mock for messagebox functions. All have same call signature.
19
- Mbox instantiates once for each function. Tester uses attributes.
19
+ """Generic mock for messagebox functions, which all have the same signature.
20
+
21
+ Instead of displaying a message box, the mock's call method saves the
22
+ arguments as instance attributes, which test functions can then examime.
20
23
"""
21
24
def __init__ (self ):
22
25
self .result = None # The return for all show funcs
@@ -30,6 +33,7 @@ def __call__(self, title, message, *args, **kwds):
30
33
31
34
class Mbox :
32
35
"""Mock for tkinter.messagebox with an Mbox_func for each function.
36
+
33
37
This module was 'tkMessageBox' in 2.x; hence the 'import as' in 3.x.
34
38
Example usage in test_module.py for testing functios in module.py:
35
39
---
@@ -49,9 +53,9 @@ def setUpClass(cls):
49
53
def tearDownClass(cls):
50
54
module.tkMessageBox = orig_mbox
51
55
---
52
- When tkMessageBox functions are the only gui making calls in a method,
53
- this replacement makes the method gui-free and unit-testable.
54
- For 'ask' functions, set func.result return before calling method.
56
+ For 'ask' functions, set func.result return value before calling the method
57
+ that uses the message function. When tkMessageBox functions are the
58
+ only gui alls in a method, this replacement makes the method gui-free,
55
59
"""
56
60
askokcancel = Mbox_func () # True or False
57
61
askquestion = Mbox_func () # 'yes' or 'no'
@@ -61,3 +65,215 @@ def tearDownClass(cls):
61
65
showerror = Mbox_func () # None
62
66
showinfo = Mbox_func () # None
63
67
showwarning = Mbox_func () # None
68
+
69
+ from _tkinter import TclError
70
+
71
+ class Text :
72
+ """A semi-functional non-gui replacement for tkinter.Text text editors.
73
+
74
+ The mock's data model is that a text is a list of \n -terminated lines.
75
+ The mock adds an empty string at the beginning of the list so that the
76
+ index of actual lines start at 1, as with Tk. The methods never see this.
77
+ Tk initializes files with a terminal \n that cannot be deleted. It is
78
+ invisible in the sense that one cannot move the cursor beyond it.
79
+
80
+ This class is only tested (and valid) with strings of ascii chars.
81
+ For testing, we are not concerned with Tk Text's treatment of,
82
+ for instance, 0-width characters or character + accent.
83
+ """
84
+ def __init__ (self , master = None , cnf = {}, ** kw ):
85
+ '''Initialize mock, non-gui, text-only Text widget.
86
+
87
+ At present, all args are ignored. Almost all affect visual behavior.
88
+ There are just a few Text-only options that affect text behavior.
89
+ '''
90
+ self .data = ['' , '\n ' ]
91
+
92
+ def index (self , index ):
93
+ "Return string version of index decoded according to current text."
94
+ return "%s.%s" % self ._decode (index , endflag = 1 )
95
+
96
+ def _decode (self , index , endflag = 0 ):
97
+ """Return a (line, char) tuple of int indexes into self.data.
98
+
99
+ This implements .index without converting the result back to a string.
100
+ The result is contrained by the number of lines and linelengths of
101
+ self.data. For many indexes, the result is initally (1, 0).
102
+
103
+ The input index may have any of several possible forms:
104
+ * line.char float: converted to 'line.char' string;
105
+ * 'line.char' string, where line and char are decimal integers;
106
+ * 'line.char lineend', where lineend='lineend' (and char is ignored);
107
+ * 'line.end', where end='end' (same as above);
108
+ * 'insert', the positions before terminal \n ;
109
+ * 'end', whose meaning depends on the endflag passed to ._endex.
110
+ * 'sel.first' or 'sel.last', where sel is a tag -- not implemented.
111
+ """
112
+ if isinstance (index , (float , bytes )):
113
+ index = str (index )
114
+ try :
115
+ index = index .lower ()
116
+ except AttributeError :
117
+ raise TclError ('bad text index "%s"' % index ) from None
118
+
119
+ lastline = len (self .data ) - 1 # same as number of text lines
120
+ if index == 'insert' :
121
+ return lastline , len (self .data [lastline ]) - 1
122
+ elif index == 'end' :
123
+ return self ._endex (endflag )
124
+
125
+ line , char = index .split ('.' )
126
+ line = int (line )
127
+
128
+ # Out of bounds line becomes first or last ('end') index
129
+ if line < 1 :
130
+ return 1 , 0
131
+ elif line > lastline :
132
+ return self ._endex (endflag )
133
+
134
+ linelength = len (self .data [line ]) - 1 # position before/at \n
135
+ if char .endswith (' lineend' ) or char == 'end' :
136
+ return line , linelength
137
+ # Tk requires that ignored chars before ' lineend' be valid int
138
+
139
+ # Out of bounds char becomes first or last index of line
140
+ char = int (char )
141
+ if char < 0 :
142
+ char = 0
143
+ elif char > linelength :
144
+ char = linelength
145
+ return line , char
146
+
147
+ def _endex (self , endflag ):
148
+ '''Return position for 'end' or line overflow corresponding to endflag.
149
+
150
+ -1: position before terminal \n ; for .insert(), .delete
151
+ 0: position after terminal \n ; for .get, .delete index 1
152
+ 1: same viewed as begininning of non-existent next line (for .index)
153
+ '''
154
+ n = len (self .data )
155
+ if endflag == 1 :
156
+ return n , 0
157
+ else :
158
+ n -= 1
159
+ return n , len (self .data [n ]) + endflag
160
+
161
+
162
+ def insert (self , index , chars ):
163
+ "Insert chars before the character at index."
164
+
165
+ if not chars : # ''.splitlines() is [], not ['']
166
+ return
167
+ chars = chars .splitlines (True )
168
+ if chars [- 1 ][- 1 ] == '\n ' :
169
+ chars .append ('' )
170
+ line , char = self ._decode (index , - 1 )
171
+ before = self .data [line ][:char ]
172
+ after = self .data [line ][char :]
173
+ self .data [line ] = before + chars [0 ]
174
+ self .data [line + 1 :line + 1 ] = chars [1 :]
175
+ self .data [line + len (chars )- 1 ] += after
176
+
177
+
178
+ def get (self , index1 , index2 = None ):
179
+ "Return slice from index1 to index2 (default is 'index1+1')."
180
+
181
+ startline , startchar = self ._decode (index1 )
182
+ if index2 is None :
183
+ endline , endchar = startline , startchar + 1
184
+ else :
185
+ endline , endchar = self ._decode (index2 )
186
+
187
+ if startline == endline :
188
+ return self .data [startline ][startchar :endchar ]
189
+ else :
190
+ lines = [self .data [startline ][startchar :]]
191
+ for i in range (startline + 1 , endline ):
192
+ lines .append (self .data [i ])
193
+ lines .append (self .data [endline ][:endchar ])
194
+ return '' .join (lines )
195
+
196
+
197
+ def delete (self , index1 , index2 = None ):
198
+ '''Delete slice from index1 to index2 (default is 'index1+1').
199
+
200
+ Adjust default index2 ('index+1) for line ends.
201
+ Do not delete the terminal \n at the very end of self.data ([-1][-1]).
202
+ '''
203
+ startline , startchar = self ._decode (index1 , - 1 )
204
+ if index2 is None :
205
+ if startchar < len (self .data [startline ])- 1 :
206
+ # not deleting \n
207
+ endline , endchar = startline , startchar + 1
208
+ elif startline < len (self .data ) - 1 :
209
+ # deleting non-terminal \n, convert 'index1+1 to start of next line
210
+ endline , endchar = startline + 1 , 0
211
+ else :
212
+ # do not delete terminal \n if index1 == 'insert'
213
+ return
214
+ else :
215
+ endline , endchar = self ._decode (index2 , - 1 )
216
+ # restricting end position to insert position excludes terminal \n
217
+
218
+ if startline == endline and startchar < endchar :
219
+ self .data [startline ] = self .data [startline ][:startchar ] + \
220
+ self .data [startline ][endchar :]
221
+ elif startline < endline :
222
+ self .data [startline ] = self .data [startline ][:startchar ] + \
223
+ self .data [endline ][endchar :]
224
+ startline += 1
225
+ for i in range (startline , endline + 1 ):
226
+ del self .data [startline ]
227
+
228
+ def compare (self , index1 , op , index2 ):
229
+ line1 , char1 = self ._decode (index1 )
230
+ line2 , char2 = self ._decode (index2 )
231
+ if op == '<' :
232
+ return line1 < line2 or line1 == line2 and char1 < char2
233
+ elif op == '<=' :
234
+ return line1 < line2 or line1 == line2 and char1 <= char2
235
+ elif op == '>' :
236
+ return line1 > line2 or line1 == line2 and char1 > char2
237
+ elif op == '>=' :
238
+ return line1 > line2 or line1 == line2 and char1 >= char2
239
+ elif op == '==' :
240
+ return line1 == line2 and char1 == char2
241
+ elif op == '!=' :
242
+ return line1 != line2 or char1 != char2
243
+ else :
244
+ raise TclError ('''bad comparison operator "%s":'''
245
+ '''must be <, <=, ==, >=, >, or !=''' % op )
246
+
247
+ # The following Text methods normally do something and return None.
248
+ # Whether doing nothing is sufficient for a test will depend on the test.
249
+
250
+ def mark_set (self , name , index ):
251
+ "Set mark *name* before the character at index."
252
+ pass
253
+
254
+ def mark_unset (self , * markNames ):
255
+ "Delete all marks in markNames."
256
+
257
+ def tag_remove (self , tagName , index1 , index2 = None ):
258
+ "Remove tag tagName from all characters between index1 and index2."
259
+ pass
260
+
261
+ # The following Text methods affect the graphics screen and return None.
262
+ # Doing nothing should always be sufficient for tests.
263
+
264
+ def scan_dragto (self , x , y ):
265
+ "Adjust the view of the text according to scan_mark"
266
+
267
+ def scan_mark (self , x , y ):
268
+ "Remember the current X, Y coordinates."
269
+
270
+ def see (self , index ):
271
+ "Scroll screen to make the character at INDEX is visible."
272
+ pass
273
+
274
+ # The following is a Misc method inheritet by Text.
275
+ # It should properly go in a Misc mock, but is included here for now.
276
+
277
+ def bind (sequence = None , func = None , add = None ):
278
+ "Bind to this widget at event sequence a call to function func."
279
+ pass
0 commit comments