123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- """Classes that replace tkinter gui objects used by an object being tested.
- A gui object is anything with a master or parent parameter, which is
- typically required in spite of what the doc strings say.
- """
- import re
- from _tkinter import TclError
- 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)
- 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:
- """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 examine.
- The test can set the result returned to ask function
- """
- def __init__(self, result=None):
- self.result = result # Return None for all show funcs
- 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.
- Example usage in test_module.py for testing functions in module.py:
- ---
- from idlelib.idle_test.mock_tk import Mbox
- import module
- orig_mbox = module.messagebox
- showerror = Mbox.showerror # example, for attribute access in test methods
- class Test(unittest.TestCase):
- @classmethod
- def setUpClass(cls):
- module.messagebox = Mbox
- @classmethod
- def tearDownClass(cls):
- module.messagebox = orig_mbox
- ---
- For 'ask' functions, set func.result return value before calling the method
- that uses the message function. When messagebox functions are the
- only GUI calls in a method, this replacement makes the method GUI-free,
- """
- 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
- 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.
- The result is constrained by the number of lines and linelengths of
- self.data. For many indexes, the result is initially (1, 0).
- 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
- if m := re.fullmatch(r'end-(\d*)c', char, re.A): # Used by hyperparser.
- return line, linelength - int(m.group(1))
- # 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
- 1: same viewed as beginning of non-existent next line (for .index)
- '''
- 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'
- return
- 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:
- self.data[startline] = self.data[startline][:startchar] + \
- self.data[endline][endchar:]
- startline += 1
- for i in range(startline, endline+1):
- del self.data[startline]
- 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
- # The following is a Misc method inherited by Text.
- # 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
- class Entry:
- "Mock for tkinter.Entry."
- def focus_set(self):
- pass
|