| 
									
										
										
										
											2013-06-05 14:22:26 -04:00
										 |  |  | """Classes that replace tkinter gui objects used by an object being tested.
 | 
					
						
							| 
									
										
										
										
											2013-07-12 20:10:23 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-06-03 20:54:21 -04:00
										 |  |  | A gui object is anything with a master or parent paramenter, which is | 
					
						
							|  |  |  | typically required in spite of what the doc strings say. | 
					
						
							| 
									
										
										
										
											2013-06-05 14:22:26 -04:00
										 |  |  | """
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-06-03 20:54:21 -04:00
										 |  |  | class Event: | 
					
						
							|  |  |  |     '''Minimal mock with attributes for testing event handlers.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     This is not a gui object, but is used as an argument for callbacks | 
					
						
							|  |  |  |     that access attributes of the event passed. If a callback ignores | 
					
						
							|  |  |  |     the event, other than the fact that is happened, pass 'event'. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Keyboard, mouse, window, and other sources generate Event instances. | 
					
						
							|  |  |  |     Event instances have the following attributes: serial (number of | 
					
						
							|  |  |  |     event), time (of event), type (of event as number), widget (in which | 
					
						
							|  |  |  |     event occurred), and x,y (position of mouse). There are other | 
					
						
							|  |  |  |     attributes for specific events, such as keycode for key events. | 
					
						
							|  |  |  |     tkinter.Event.__doc__ has more but is still not complete. | 
					
						
							|  |  |  |     '''
 | 
					
						
							|  |  |  |     def __init__(self, **kwds): | 
					
						
							|  |  |  |         "Create event with attributes needed for test" | 
					
						
							|  |  |  |         self.__dict__.update(kwds) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-06-05 14:22:26 -04:00
										 |  |  | class Var: | 
					
						
							|  |  |  |     "Use for String/Int/BooleanVar: incomplete" | 
					
						
							|  |  |  |     def __init__(self, master=None, value=None, name=None): | 
					
						
							|  |  |  |         self.master = master | 
					
						
							|  |  |  |         self.value = value | 
					
						
							|  |  |  |         self.name = name | 
					
						
							|  |  |  |     def set(self, value): | 
					
						
							|  |  |  |         self.value = value | 
					
						
							|  |  |  |     def get(self): | 
					
						
							|  |  |  |         return self.value | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Mbox_func: | 
					
						
							| 
									
										
										
										
											2013-07-12 20:10:23 -04:00
										 |  |  |     """Generic mock for messagebox functions, which all have the same signature.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Instead of displaying a message box, the mock's call method saves the | 
					
						
							|  |  |  |     arguments as instance attributes, which test functions can then examime. | 
					
						
							| 
									
										
										
										
											2014-06-03 20:54:21 -04:00
										 |  |  |     The test can set the result returned to ask function | 
					
						
							| 
									
										
										
										
											2013-06-05 14:22:26 -04:00
										 |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2014-06-03 20:54:21 -04:00
										 |  |  |     def __init__(self, result=None): | 
					
						
							|  |  |  |         self.result = result  # Return None for all show funcs | 
					
						
							| 
									
										
										
										
											2013-06-05 14:22:26 -04:00
										 |  |  |     def __call__(self, title, message, *args, **kwds): | 
					
						
							|  |  |  |         # Save all args for possible examination by tester | 
					
						
							|  |  |  |         self.title = title | 
					
						
							|  |  |  |         self.message = message | 
					
						
							|  |  |  |         self.args = args | 
					
						
							|  |  |  |         self.kwds = kwds | 
					
						
							|  |  |  |         return self.result  # Set by tester for ask functions | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Mbox: | 
					
						
							|  |  |  |     """Mock for tkinter.messagebox with an Mbox_func for each function.
 | 
					
						
							| 
									
										
										
										
											2013-07-12 20:10:23 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-06-05 14:22:26 -04:00
										 |  |  |     This module was 'tkMessageBox' in 2.x; hence the 'import as' in  3.x. | 
					
						
							| 
									
										
										
										
											2013-08-17 16:11:40 +03:00
										 |  |  |     Example usage in test_module.py for testing functions in module.py: | 
					
						
							| 
									
										
										
										
											2013-06-05 14:22:26 -04:00
										 |  |  |     --- | 
					
						
							|  |  |  | from idlelib.idle_test.mock_tk import Mbox | 
					
						
							|  |  |  | import module | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | orig_mbox = module.tkMessageBox | 
					
						
							|  |  |  | showerror = Mbox.showerror  # example, for attribute access in test methods | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Test(unittest.TestCase): | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def setUpClass(cls): | 
					
						
							|  |  |  |         module.tkMessageBox = Mbox | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def tearDownClass(cls): | 
					
						
							|  |  |  |         module.tkMessageBox = orig_mbox | 
					
						
							|  |  |  |     --- | 
					
						
							| 
									
										
										
										
											2013-07-12 20:10:23 -04:00
										 |  |  |     For 'ask' functions, set func.result return value before calling the method | 
					
						
							|  |  |  |     that uses the message function. When tkMessageBox functions are the | 
					
						
							|  |  |  |     only gui alls in a method, this replacement makes the method gui-free, | 
					
						
							| 
									
										
										
										
											2013-06-05 14:22:26 -04:00
										 |  |  |     """
 | 
					
						
							|  |  |  |     askokcancel = Mbox_func()     # True or False | 
					
						
							|  |  |  |     askquestion = Mbox_func()     # 'yes' or 'no' | 
					
						
							|  |  |  |     askretrycancel = Mbox_func()  # True or False | 
					
						
							|  |  |  |     askyesno = Mbox_func()        # True or False | 
					
						
							|  |  |  |     askyesnocancel = Mbox_func()  # True, False, or None | 
					
						
							|  |  |  |     showerror = Mbox_func()    # None | 
					
						
							|  |  |  |     showinfo = Mbox_func()     # None | 
					
						
							|  |  |  |     showwarning = Mbox_func()  # None | 
					
						
							| 
									
										
										
										
											2013-07-12 20:10:23 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | from _tkinter import TclError | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Text: | 
					
						
							|  |  |  |     """A semi-functional non-gui replacement for tkinter.Text text editors.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     The mock's data model is that a text is a list of \n-terminated lines. | 
					
						
							|  |  |  |     The mock adds an empty string at  the beginning of the list so that the | 
					
						
							|  |  |  |     index of actual lines start at 1, as with Tk. The methods never see this. | 
					
						
							|  |  |  |     Tk initializes files with a terminal \n that cannot be deleted. It is | 
					
						
							|  |  |  |     invisible in the sense that one cannot move the cursor beyond it. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     This class is only tested (and valid) with strings of ascii chars. | 
					
						
							|  |  |  |     For testing, we are not concerned with Tk Text's treatment of, | 
					
						
							|  |  |  |     for instance, 0-width characters or character + accent. | 
					
						
							|  |  |  |    """
 | 
					
						
							|  |  |  |     def __init__(self, master=None, cnf={}, **kw): | 
					
						
							|  |  |  |         '''Initialize mock, non-gui, text-only Text widget.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         At present, all args are ignored. Almost all affect visual behavior. | 
					
						
							|  |  |  |         There are just a few Text-only options that affect text behavior. | 
					
						
							|  |  |  |         '''
 | 
					
						
							|  |  |  |         self.data = ['', '\n'] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def index(self, index): | 
					
						
							|  |  |  |         "Return string version of index decoded according to current text." | 
					
						
							|  |  |  |         return "%s.%s" % self._decode(index, endflag=1) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _decode(self, index, endflag=0): | 
					
						
							|  |  |  |         """Return a (line, char) tuple of int indexes into self.data.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         This implements .index without converting the result back to a string. | 
					
						
							| 
									
										
										
										
											2014-10-19 18:04:38 +03:00
										 |  |  |         The result is constrained by the number of lines and linelengths of | 
					
						
							| 
									
										
										
										
											2013-08-17 16:11:40 +03:00
										 |  |  |         self.data. For many indexes, the result is initially (1, 0). | 
					
						
							| 
									
										
										
										
											2013-07-12 20:10:23 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |         The input index may have any of several possible forms: | 
					
						
							|  |  |  |         * line.char float: converted to 'line.char' string; | 
					
						
							|  |  |  |         * 'line.char' string, where line and char are decimal integers; | 
					
						
							|  |  |  |         * 'line.char lineend', where lineend='lineend' (and char is ignored); | 
					
						
							|  |  |  |         * 'line.end', where end='end' (same as above); | 
					
						
							|  |  |  |         * 'insert', the positions before terminal \n; | 
					
						
							|  |  |  |         * 'end', whose meaning depends on the endflag passed to ._endex. | 
					
						
							|  |  |  |         * 'sel.first' or 'sel.last', where sel is a tag -- not implemented. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         if isinstance(index, (float, bytes)): | 
					
						
							|  |  |  |             index = str(index) | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             index=index.lower() | 
					
						
							|  |  |  |         except AttributeError: | 
					
						
							|  |  |  |             raise TclError('bad text index "%s"' % index) from None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         lastline =  len(self.data) - 1  # same as number of text lines | 
					
						
							|  |  |  |         if index == 'insert': | 
					
						
							|  |  |  |             return lastline, len(self.data[lastline]) - 1 | 
					
						
							|  |  |  |         elif index == 'end': | 
					
						
							|  |  |  |             return self._endex(endflag) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         line, char = index.split('.') | 
					
						
							|  |  |  |         line = int(line) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Out of bounds line becomes first or last ('end') index | 
					
						
							|  |  |  |         if line < 1: | 
					
						
							|  |  |  |             return 1, 0 | 
					
						
							|  |  |  |         elif line > lastline: | 
					
						
							|  |  |  |             return self._endex(endflag) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         linelength = len(self.data[line])  -1  # position before/at \n | 
					
						
							|  |  |  |         if char.endswith(' lineend') or char == 'end': | 
					
						
							|  |  |  |             return line, linelength | 
					
						
							|  |  |  |             # Tk requires that ignored chars before ' lineend' be valid int | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Out of bounds char becomes first or last index of line | 
					
						
							|  |  |  |         char = int(char) | 
					
						
							|  |  |  |         if char < 0: | 
					
						
							|  |  |  |             char = 0 | 
					
						
							|  |  |  |         elif char > linelength: | 
					
						
							|  |  |  |             char = linelength | 
					
						
							|  |  |  |         return line, char | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _endex(self, endflag): | 
					
						
							|  |  |  |         '''Return position for 'end' or line overflow corresponding to endflag.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |        -1: position before terminal \n; for .insert(), .delete | 
					
						
							|  |  |  |        0: position after terminal \n; for .get, .delete index 1 | 
					
						
							| 
									
										
										
										
											2013-08-17 16:11:40 +03:00
										 |  |  |        1: same viewed as beginning of non-existent next line (for .index) | 
					
						
							| 
									
										
										
										
											2013-07-12 20:10:23 -04:00
										 |  |  |        '''
 | 
					
						
							|  |  |  |         n = len(self.data) | 
					
						
							|  |  |  |         if endflag == 1: | 
					
						
							|  |  |  |             return n, 0 | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             n -= 1 | 
					
						
							|  |  |  |             return n, len(self.data[n]) + endflag | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def insert(self, index, chars): | 
					
						
							|  |  |  |         "Insert chars before the character at index." | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if not chars:  # ''.splitlines() is [], not [''] | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         chars = chars.splitlines(True) | 
					
						
							|  |  |  |         if chars[-1][-1] == '\n': | 
					
						
							|  |  |  |             chars.append('') | 
					
						
							|  |  |  |         line, char = self._decode(index, -1) | 
					
						
							|  |  |  |         before = self.data[line][:char] | 
					
						
							|  |  |  |         after = self.data[line][char:] | 
					
						
							|  |  |  |         self.data[line] = before + chars[0] | 
					
						
							|  |  |  |         self.data[line+1:line+1] = chars[1:] | 
					
						
							|  |  |  |         self.data[line+len(chars)-1] += after | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get(self, index1, index2=None): | 
					
						
							|  |  |  |         "Return slice from index1 to index2 (default is 'index1+1')." | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         startline, startchar = self._decode(index1) | 
					
						
							|  |  |  |         if index2 is None: | 
					
						
							|  |  |  |             endline, endchar = startline, startchar+1 | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             endline, endchar = self._decode(index2) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if startline == endline: | 
					
						
							|  |  |  |             return self.data[startline][startchar:endchar] | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             lines = [self.data[startline][startchar:]] | 
					
						
							|  |  |  |             for i in range(startline+1, endline): | 
					
						
							|  |  |  |                 lines.append(self.data[i]) | 
					
						
							|  |  |  |             lines.append(self.data[endline][:endchar]) | 
					
						
							|  |  |  |             return ''.join(lines) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def delete(self, index1, index2=None): | 
					
						
							|  |  |  |         '''Delete slice from index1 to index2 (default is 'index1+1').
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Adjust default index2 ('index+1) for line ends. | 
					
						
							|  |  |  |         Do not delete the terminal \n at the very end of self.data ([-1][-1]). | 
					
						
							|  |  |  |         '''
 | 
					
						
							|  |  |  |         startline, startchar = self._decode(index1, -1) | 
					
						
							|  |  |  |         if index2 is None: | 
					
						
							|  |  |  |             if startchar < len(self.data[startline])-1: | 
					
						
							|  |  |  |                 # not deleting \n | 
					
						
							|  |  |  |                 endline, endchar = startline, startchar+1 | 
					
						
							|  |  |  |             elif startline < len(self.data) - 1: | 
					
						
							|  |  |  |                 # deleting non-terminal \n, convert 'index1+1 to start of next line | 
					
						
							|  |  |  |                 endline, endchar = startline+1, 0 | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 # do not delete terminal \n if index1 == 'insert' | 
					
						
							| 
									
										
										
										
											2013-07-12 20:16:28 -04:00
										 |  |  |                 return | 
					
						
							| 
									
										
										
										
											2013-07-12 20:10:23 -04:00
										 |  |  |         else: | 
					
						
							|  |  |  |             endline, endchar = self._decode(index2, -1) | 
					
						
							|  |  |  |             # restricting end position to insert position excludes terminal \n | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if startline == endline and startchar < endchar: | 
					
						
							|  |  |  |             self.data[startline] = self.data[startline][:startchar] + \ | 
					
						
							|  |  |  |                                              self.data[startline][endchar:] | 
					
						
							|  |  |  |         elif startline < endline: | 
					
						
							| 
									
										
										
										
											2013-07-12 20:16:28 -04:00
										 |  |  |             self.data[startline] = self.data[startline][:startchar] + \ | 
					
						
							|  |  |  |                                    self.data[endline][endchar:] | 
					
						
							|  |  |  |             startline += 1 | 
					
						
							|  |  |  |             for i in range(startline, endline+1): | 
					
						
							|  |  |  |                 del self.data[startline] | 
					
						
							| 
									
										
										
										
											2013-07-12 20:10:23 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def compare(self, index1, op, index2): | 
					
						
							|  |  |  |         line1, char1 = self._decode(index1) | 
					
						
							|  |  |  |         line2, char2 = self._decode(index2) | 
					
						
							|  |  |  |         if op == '<': | 
					
						
							|  |  |  |             return line1 < line2 or line1 == line2 and char1 < char2 | 
					
						
							|  |  |  |         elif op == '<=': | 
					
						
							|  |  |  |             return line1 < line2 or line1 == line2 and char1 <= char2 | 
					
						
							|  |  |  |         elif op == '>': | 
					
						
							|  |  |  |             return line1 > line2 or line1 == line2 and char1 > char2 | 
					
						
							|  |  |  |         elif op == '>=': | 
					
						
							|  |  |  |             return line1 > line2 or line1 == line2 and char1 >= char2 | 
					
						
							|  |  |  |         elif op == '==': | 
					
						
							|  |  |  |             return line1 == line2 and char1 == char2 | 
					
						
							|  |  |  |         elif op == '!=': | 
					
						
							|  |  |  |             return line1 != line2 or  char1 != char2 | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             raise TclError('''bad comparison operator "%s":''' | 
					
						
							|  |  |  |                                   '''must be <, <=, ==, >=, >, or !=''' % op) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # The following Text methods normally do something and return None. | 
					
						
							|  |  |  |     # Whether doing nothing is sufficient for a test will depend on the test. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def mark_set(self, name, index): | 
					
						
							|  |  |  |         "Set mark *name* before the character at index." | 
					
						
							|  |  |  |         pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def mark_unset(self, *markNames): | 
					
						
							|  |  |  |         "Delete all marks in markNames." | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def tag_remove(self, tagName, index1, index2=None): | 
					
						
							|  |  |  |         "Remove tag tagName from all characters between index1 and index2." | 
					
						
							|  |  |  |         pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # The following Text methods affect the graphics screen and return None. | 
					
						
							|  |  |  |     # Doing nothing should always be sufficient for tests. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def scan_dragto(self, x, y): | 
					
						
							|  |  |  |         "Adjust the view of the text according to scan_mark" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def scan_mark(self, x, y): | 
					
						
							|  |  |  |         "Remember the current X, Y coordinates." | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def see(self, index): | 
					
						
							|  |  |  |         "Scroll screen to make the character at INDEX is visible." | 
					
						
							|  |  |  |         pass | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-08-17 16:11:40 +03:00
										 |  |  |     #  The following is a Misc method inherited by Text. | 
					
						
							| 
									
										
										
										
											2013-07-12 20:10:23 -04:00
										 |  |  |     # It should properly go in a Misc mock, but is included here for now. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def bind(sequence=None, func=None, add=None): | 
					
						
							|  |  |  |         "Bind to this widget at event sequence a call to function func." | 
					
						
							|  |  |  |         pass |