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