• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Common tests for test_tkinter/test_widgets.py and test_ttk/test_widgets.py
2
3import re
4import tkinter
5from test.test_tkinter.support import (AbstractTkTest, requires_tk, tk_version,
6                                  pixels_conv, tcl_obj_eq)
7import test.support
8
9
10_sentinel = object()
11
12class AbstractWidgetTest(AbstractTkTest):
13    _default_pixels = '' if tk_version >= (9, 0) else -1 if tk_version >= (8, 7) else ''
14    _conv_pixels = round
15    _conv_pad_pixels = None
16    _stringify = False
17    _clip_highlightthickness = True
18    _clip_pad = False
19    _clip_borderwidth = False
20    _allow_empty_justify = False
21
22    @property
23    def scaling(self):
24        try:
25            return self._scaling
26        except AttributeError:
27            self._scaling = float(self.root.call('tk', 'scaling'))
28            return self._scaling
29
30    def _str(self, value):
31        if not self._stringify and self.wantobjects and tk_version >= (8, 6):
32            return value
33        if isinstance(value, tuple):
34            return ' '.join(map(self._str, value))
35        return str(value)
36
37    def assertEqual2(self, actual, expected, msg=None, eq=object.__eq__):
38        if eq(actual, expected):
39            return
40        self.assertEqual(actual, expected, msg)
41
42    def checkParam(self, widget, name, value, *, expected=_sentinel,
43                   conv=False, eq=None):
44        widget[name] = value
45        if expected is _sentinel:
46            expected = value
47        if conv:
48            expected = conv(expected)
49        if self._stringify or not self.wantobjects:
50            if isinstance(expected, tuple):
51                expected = tkinter._join(expected)
52            else:
53                expected = str(expected)
54        if eq is None:
55            eq = tcl_obj_eq
56        self.assertEqual2(widget[name], expected, eq=eq)
57        self.assertEqual2(widget.cget(name), expected, eq=eq)
58        t = widget.configure(name)
59        self.assertEqual(len(t), 5)
60        self.assertEqual2(t[4], expected, eq=eq)
61
62    def checkInvalidParam(self, widget, name, value, errmsg=None):
63        orig = widget[name]
64        if errmsg is not None:
65            errmsg = errmsg.format(re.escape(str(value)))
66            errmsg = fr'\A{errmsg}\Z'
67        with self.assertRaisesRegex(tkinter.TclError, errmsg or ''):
68            widget[name] = value
69        self.assertEqual(widget[name], orig)
70        with self.assertRaisesRegex(tkinter.TclError, errmsg or ''):
71            widget.configure({name: value})
72        self.assertEqual(widget[name], orig)
73
74    def checkParams(self, widget, name, *values, **kwargs):
75        for value in values:
76            self.checkParam(widget, name, value, **kwargs)
77
78    def checkIntegerParam(self, widget, name, *values, **kwargs):
79        self.checkParams(widget, name, *values, **kwargs)
80        errmsg = 'expected integer but got "{}"'
81        self.checkInvalidParam(widget, name, '', errmsg=errmsg)
82        self.checkInvalidParam(widget, name, '10p', errmsg=errmsg)
83        self.checkInvalidParam(widget, name, 3.2, errmsg=errmsg)
84
85    def checkFloatParam(self, widget, name, *values, conv=float, **kwargs):
86        for value in values:
87            self.checkParam(widget, name, value, conv=conv, **kwargs)
88        errmsg = 'expected floating-point number but got "{}"'
89        self.checkInvalidParam(widget, name, '', errmsg=errmsg)
90        self.checkInvalidParam(widget, name, 'spam', errmsg=errmsg)
91
92    def checkBooleanParam(self, widget, name):
93        for value in (False, 0, 'false', 'no', 'off'):
94            self.checkParam(widget, name, value, expected=0)
95        for value in (True, 1, 'true', 'yes', 'on'):
96            self.checkParam(widget, name, value, expected=1)
97        errmsg = 'expected boolean value but got "{}"'
98        self.checkInvalidParam(widget, name, '', errmsg=errmsg)
99        self.checkInvalidParam(widget, name, 'spam', errmsg=errmsg)
100
101    def checkColorParam(self, widget, name, *, allow_empty=None, **kwargs):
102        self.checkParams(widget, name,
103                         '#ff0000', '#00ff00', '#0000ff', '#123456',
104                         'red', 'green', 'blue', 'white', 'black', 'grey',
105                         **kwargs)
106        self.checkInvalidParam(widget, name, 'spam',
107                errmsg='unknown color name "spam"')
108
109    def checkCursorParam(self, widget, name, **kwargs):
110        self.checkParams(widget, name, 'arrow', 'watch', 'cross', '',**kwargs)
111        self.checkParam(widget, name, 'none')
112        self.checkInvalidParam(widget, name, 'spam',
113                errmsg='bad cursor spec "spam"')
114
115    def checkCommandParam(self, widget, name):
116        def command(*args):
117            pass
118        widget[name] = command
119        self.assertTrue(widget[name])
120        self.checkParams(widget, name, '')
121
122    def checkEnumParam(self, widget, name, *values,
123                       errmsg=None, allow_empty=False, fullname=None,
124                       sort=False, **kwargs):
125        self.checkParams(widget, name, *values, **kwargs)
126        if errmsg is None:
127            if sort:
128                if values[-1]:
129                    values = tuple(sorted(values))
130                else:
131                    values = tuple(sorted(values[:-1])) + ('',)
132            errmsg2 = ' %s "{}": must be %s%s or %s' % (
133                    fullname or name,
134                    ', '.join(values[:-1]),
135                    ',' if len(values) > 2 else '',
136                    values[-1] or '""')
137            if '' not in values and not allow_empty:
138                self.checkInvalidParam(widget, name, '',
139                                       errmsg='ambiguous' + errmsg2)
140            errmsg = 'bad' + errmsg2
141        self.checkInvalidParam(widget, name, 'spam', errmsg=errmsg)
142
143    def checkPixelsParam(self, widget, name, *values,
144                         conv=None, **kwargs):
145        if conv is None:
146            conv = self._conv_pixels
147        for value in values:
148            expected = _sentinel
149            conv1 = conv
150            if isinstance(value, str):
151                if conv1 and conv1 is not str:
152                    expected = pixels_conv(value) * self.scaling
153                    conv1 = round
154            self.checkParam(widget, name, value, expected=expected,
155                            conv=conv1, **kwargs)
156        errmsg = '(bad|expected) screen distance ((or "" )?but got )?"{}"'
157        self.checkInvalidParam(widget, name, '6x', errmsg=errmsg)
158        self.checkInvalidParam(widget, name, 'spam', errmsg=errmsg)
159
160    def checkReliefParam(self, widget, name, *, allow_empty=False):
161        values = ('flat', 'groove', 'raised', 'ridge', 'solid', 'sunken')
162        if allow_empty:
163            values += ('',)
164        self.checkParams(widget, name, *values)
165        errmsg = 'bad relief "{}": must be %s, or %s' % (
166                ', '.join(values[:-1]),
167                values[-1] or '""')
168        if tk_version < (8, 6):
169            errmsg = None
170        self.checkInvalidParam(widget, name, 'spam', errmsg=errmsg)
171
172    def checkImageParam(self, widget, name):
173        image = tkinter.PhotoImage(master=self.root, name='image1')
174        self.checkParam(widget, name, image, conv=str)
175        self.checkInvalidParam(widget, name, 'spam',
176                errmsg='image "spam" doesn\'t exist')
177        widget[name] = ''
178
179    def checkVariableParam(self, widget, name, var):
180        self.checkParam(widget, name, var, conv=str)
181
182    def assertIsBoundingBox(self, bbox):
183        self.assertIsNotNone(bbox)
184        self.assertIsInstance(bbox, tuple)
185        if len(bbox) != 4:
186            self.fail('Invalid bounding box: %r' % (bbox,))
187        for item in bbox:
188            if not isinstance(item, int):
189                self.fail('Invalid bounding box: %r' % (bbox,))
190                break
191
192
193    def test_keys(self):
194        widget = self.create()
195        keys = widget.keys()
196        self.assertEqual(sorted(keys), sorted(widget.configure()))
197        for k in keys:
198            widget[k]
199        # Test if OPTIONS contains all keys
200        if test.support.verbose:
201            aliases = {
202                'bd': 'borderwidth',
203                'bg': 'background',
204                'bgimg': 'backgroundimage',
205                'fg': 'foreground',
206                'invcmd': 'invalidcommand',
207                'vcmd': 'validatecommand',
208            }
209            keys = set(keys)
210            expected = set(self.OPTIONS)
211            for k in sorted(keys - expected):
212                if not (k in aliases and
213                        aliases[k] in keys and
214                        aliases[k] in expected):
215                    print('%s.OPTIONS doesn\'t contain "%s"' %
216                          (self.__class__.__name__, k))
217
218
219class StandardOptionsTests:
220    STANDARD_OPTIONS = (
221        'activebackground', 'activeborderwidth', 'activeforeground', 'anchor',
222        'background', 'bitmap', 'borderwidth', 'compound', 'cursor',
223        'disabledforeground', 'exportselection', 'font', 'foreground',
224        'highlightbackground', 'highlightcolor', 'highlightthickness',
225        'image', 'insertbackground', 'insertborderwidth',
226        'insertofftime', 'insertontime', 'insertwidth',
227        'jump', 'justify', 'orient', 'padx', 'pady', 'relief',
228        'repeatdelay', 'repeatinterval',
229        'selectbackground', 'selectborderwidth', 'selectforeground',
230        'setgrid', 'takefocus', 'text', 'textvariable', 'troughcolor',
231        'underline', 'wraplength', 'xscrollcommand', 'yscrollcommand',
232    )
233
234    def test_configure_activebackground(self):
235        widget = self.create()
236        self.checkColorParam(widget, 'activebackground')
237
238    def test_configure_activeborderwidth(self):
239        widget = self.create()
240        self.checkPixelsParam(widget, 'activeborderwidth',
241                              0, 1.3, 2.9, 6, -2, '10p')
242
243    def test_configure_activeforeground(self):
244        widget = self.create()
245        self.checkColorParam(widget, 'activeforeground')
246
247    def test_configure_activerelief(self):
248        widget = self.create()
249        self.checkReliefParam(widget, 'activerelief')
250
251    def test_configure_anchor(self):
252        widget = self.create()
253        self.checkEnumParam(widget, 'anchor',
254                'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'center')
255
256    def test_configure_background(self):
257        widget = self.create()
258        self.checkColorParam(widget, 'background')
259        if 'bg' in self.OPTIONS:
260            self.checkColorParam(widget, 'bg')
261
262    @requires_tk(8, 7)
263    def test_configure_backgroundimage(self):
264        widget = self.create()
265        self.checkImageParam(widget, 'backgroundimage')
266
267    def test_configure_bitmap(self):
268        widget = self.create()
269        self.checkParam(widget, 'bitmap', 'questhead')
270        self.checkParam(widget, 'bitmap', 'gray50')
271        filename = test.support.findfile('python.xbm', subdir='tkinterdata')
272        self.checkParam(widget, 'bitmap', '@' + filename)
273        # Cocoa Tk widgets don't detect invalid -bitmap values
274        # See https://core.tcl.tk/tk/info/31cd33dbf0
275        if not ('aqua' in self.root.tk.call('tk', 'windowingsystem') and
276                'AppKit' in self.root.winfo_server()):
277            self.checkInvalidParam(widget, 'bitmap', 'spam',
278                    errmsg='bitmap "spam" not defined')
279
280    def test_configure_borderwidth(self):
281        widget = self.create()
282        self.checkPixelsParam(widget, 'borderwidth',
283                              0, 1.3, 2.6, 6, '10p')
284        expected = 0 if self._clip_borderwidth else -2
285        self.checkParam(widget, 'borderwidth', -2, expected=expected,
286                        conv=self._conv_pixels)
287        if 'bd' in self.OPTIONS:
288            self.checkPixelsParam(widget, 'bd', 0, 1.3, 2.6, 6, '10p')
289            self.checkParam(widget, 'bd', -2, expected=expected,
290                            conv=self._conv_pixels)
291
292    def test_configure_compound(self):
293        widget = self.create()
294        self.checkEnumParam(widget, 'compound',
295                'bottom', 'center', 'left', 'none', 'right', 'top')
296
297    def test_configure_cursor(self):
298        widget = self.create()
299        self.checkCursorParam(widget, 'cursor')
300
301    def test_configure_disabledforeground(self):
302        widget = self.create()
303        self.checkColorParam(widget, 'disabledforeground')
304
305    def test_configure_exportselection(self):
306        widget = self.create()
307        self.checkBooleanParam(widget, 'exportselection')
308
309    def test_configure_font(self):
310        widget = self.create()
311        self.checkParam(widget, 'font',
312                        '-Adobe-Helvetica-Medium-R-Normal--*-120-*-*-*-*-*-*')
313        is_ttk = widget.__class__.__module__ == 'tkinter.ttk'
314        if not is_ttk:
315            self.checkInvalidParam(widget, 'font', '',
316                                   errmsg='font "" doesn\'t exist')
317
318    def test_configure_foreground(self):
319        widget = self.create()
320        self.checkColorParam(widget, 'foreground')
321        if 'fg' in self.OPTIONS:
322            self.checkColorParam(widget, 'fg')
323
324    def test_configure_highlightbackground(self):
325        widget = self.create()
326        self.checkColorParam(widget, 'highlightbackground')
327
328    def test_configure_highlightcolor(self):
329        widget = self.create()
330        self.checkColorParam(widget, 'highlightcolor')
331
332    def test_configure_highlightthickness(self):
333        widget = self.create()
334        self.checkPixelsParam(widget, 'highlightthickness',
335                              0, 1.3, 2.6, 6, '10p')
336        expected = 0 if self._clip_highlightthickness else -2
337        self.checkParam(widget, 'highlightthickness', -2, expected=expected,
338                        conv=self._conv_pixels)
339
340    def test_configure_image(self):
341        widget = self.create()
342        self.checkImageParam(widget, 'image')
343
344    def test_configure_insertbackground(self):
345        widget = self.create()
346        self.checkColorParam(widget, 'insertbackground')
347
348    def test_configure_insertborderwidth(self):
349        widget = self.create()
350        self.checkPixelsParam(widget, 'insertborderwidth',
351                              0, 1.3, 2.6, 6, -2, '10p')
352
353    def test_configure_insertofftime(self):
354        widget = self.create()
355        self.checkIntegerParam(widget, 'insertofftime', 100)
356
357    def test_configure_insertontime(self):
358        widget = self.create()
359        self.checkIntegerParam(widget, 'insertontime', 100)
360
361    def test_configure_insertwidth(self):
362        widget = self.create()
363        self.checkPixelsParam(widget, 'insertwidth', 1.3, 2.6, -2, '10p')
364
365    def test_configure_jump(self):
366        widget = self.create()
367        self.checkBooleanParam(widget, 'jump')
368
369    def test_configure_justify(self):
370        widget = self.create()
371        values = ('left', 'right', 'center')
372        if self._allow_empty_justify:
373            values += ('',)
374        self.checkEnumParam(widget, 'justify', *values,
375                            fullname='justification')
376
377    def test_configure_orient(self):
378        widget = self.create()
379        self.assertEqual(str(widget['orient']), self.default_orient)
380        self.checkEnumParam(widget, 'orient', 'horizontal', 'vertical')
381
382    def test_configure_padx(self):
383        widget = self.create()
384        self.checkPixelsParam(widget, 'padx', 3, 4.4, 5.6, '12m',
385                              conv=self._conv_pad_pixels)
386        expected = 0 if self._clip_pad else -2
387        self.checkParam(widget, 'padx', -2, expected=expected,
388                        conv=self._conv_pad_pixels)
389
390    def test_configure_pady(self):
391        widget = self.create()
392        self.checkPixelsParam(widget, 'pady', 3, 4.4, 5.6, '12m',
393                              conv=self._conv_pad_pixels)
394        expected = 0 if self._clip_pad else -2
395        self.checkParam(widget, 'pady', -2, expected=expected,
396                        conv=self._conv_pad_pixels)
397
398    @requires_tk(8, 7)
399    def test_configure_placeholder(self):
400        widget = self.create()
401        self.checkParam(widget, 'placeholder', 'xxx')
402
403    @requires_tk(8, 7)
404    def test_configure_placeholderforeground(self):
405        widget = self.create()
406        self.checkColorParam(widget, 'placeholderforeground')
407
408    def test_configure_relief(self):
409        widget = self.create()
410        self.checkReliefParam(widget, 'relief')
411
412    def test_configure_repeatdelay(self):
413        widget = self.create()
414        self.checkIntegerParam(widget, 'repeatdelay', -500, 500)
415
416    def test_configure_repeatinterval(self):
417        widget = self.create()
418        self.checkIntegerParam(widget, 'repeatinterval', -500, 500)
419
420    def test_configure_selectbackground(self):
421        widget = self.create()
422        self.checkColorParam(widget, 'selectbackground')
423
424    def test_configure_selectborderwidth(self):
425        widget = self.create()
426        self.checkPixelsParam(widget, 'selectborderwidth', 1.3, 2.6, -2, '10p')
427
428    def test_configure_selectforeground(self):
429        widget = self.create()
430        self.checkColorParam(widget, 'selectforeground')
431
432    def test_configure_setgrid(self):
433        widget = self.create()
434        self.checkBooleanParam(widget, 'setgrid')
435
436    def test_configure_state(self):
437        widget = self.create()
438        self.checkEnumParam(widget, 'state', 'active', 'disabled', 'normal')
439
440    def test_configure_takefocus(self):
441        widget = self.create()
442        self.checkParams(widget, 'takefocus', '0', '1', '')
443
444    def test_configure_text(self):
445        widget = self.create()
446        self.checkParams(widget, 'text', '', 'any string')
447
448    def test_configure_textvariable(self):
449        widget = self.create()
450        var = tkinter.StringVar(self.root)
451        self.checkVariableParam(widget, 'textvariable', var)
452
453    @requires_tk(8, 7)
454    def test_configure_tile(self):
455        widget = self.create()
456        self.checkBooleanParam(widget, 'tile')
457
458    def test_configure_troughcolor(self):
459        widget = self.create()
460        self.checkColorParam(widget, 'troughcolor')
461
462    def test_configure_underline(self):
463        widget = self.create()
464        self.checkParams(widget, 'underline', 0, 1, 10)
465        if tk_version >= (8, 7):
466            is_ttk = widget.__class__.__module__ == 'tkinter.ttk'
467            self.checkParam(widget, 'underline', '',
468                            expected='' if is_ttk else self._default_pixels)
469            self.checkParam(widget, 'underline', '5+2',
470                            expected='5+2' if is_ttk else 7)
471            self.checkParam(widget, 'underline', '5-2',
472                            expected='5-2' if is_ttk else 3)
473            self.checkParam(widget, 'underline', 'end', expected='end')
474            self.checkParam(widget, 'underline', 'end-2', expected='end-2')
475            errmsg = (r'bad index "{}": must be integer\?\[\+-\]integer\?, '
476                      r'end\?\[\+-\]integer\?, or ""')
477        else:
478            errmsg = 'expected integer but got "{}"'
479            self.checkInvalidParam(widget, 'underline', '', errmsg=errmsg)
480        self.checkInvalidParam(widget, 'underline', '10p', errmsg=errmsg)
481        self.checkInvalidParam(widget, 'underline', 3.2, errmsg=errmsg)
482
483    def test_configure_wraplength(self):
484        widget = self.create()
485        self.checkPixelsParam(widget, 'wraplength', 100)
486
487    def test_configure_xscrollcommand(self):
488        widget = self.create()
489        self.checkCommandParam(widget, 'xscrollcommand')
490
491    def test_configure_yscrollcommand(self):
492        widget = self.create()
493        self.checkCommandParam(widget, 'yscrollcommand')
494
495    # non-standard but common options
496
497    def test_configure_command(self):
498        widget = self.create()
499        self.checkCommandParam(widget, 'command')
500
501    def test_configure_indicatoron(self):
502        widget = self.create()
503        self.checkBooleanParam(widget, 'indicatoron')
504
505    def test_configure_offrelief(self):
506        widget = self.create()
507        self.checkReliefParam(widget, 'offrelief')
508
509    def test_configure_overrelief(self):
510        widget = self.create()
511        self.checkReliefParam(widget, 'overrelief',
512                              allow_empty=(tk_version >= (8, 7)))
513
514    def test_configure_selectcolor(self):
515        widget = self.create()
516        self.checkColorParam(widget, 'selectcolor')
517
518    def test_configure_selectimage(self):
519        widget = self.create()
520        self.checkImageParam(widget, 'selectimage')
521
522    def test_configure_tristateimage(self):
523        widget = self.create()
524        self.checkImageParam(widget, 'tristateimage')
525
526    def test_configure_tristatevalue(self):
527        widget = self.create()
528        self.checkParam(widget, 'tristatevalue', 'unknowable')
529
530    def test_configure_variable(self):
531        widget = self.create()
532        var = tkinter.DoubleVar(self.root)
533        self.checkVariableParam(widget, 'variable', var)
534
535
536class IntegerSizeTests:
537    def test_configure_height(self):
538        widget = self.create()
539        self.checkIntegerParam(widget, 'height', 100, -100, 0)
540
541    def test_configure_width(self):
542        widget = self.create()
543        self.checkIntegerParam(widget, 'width', 402, -402, 0)
544
545
546class PixelSizeTests:
547    def test_configure_height(self):
548        widget = self.create()
549        self.checkPixelsParam(widget, 'height', 100, 101.2, 102.6, -100, 0, '3c')
550
551    def test_configure_width(self):
552        widget = self.create()
553        self.checkPixelsParam(widget, 'width', 402, 403.4, 404.6, -402, 0, '5i')
554
555
556def add_standard_options(*source_classes):
557    # This decorator adds test_configure_xxx methods from source classes for
558    # every xxx option in the OPTIONS class attribute if they are not defined
559    # explicitly.
560    def decorator(cls):
561        for option in cls.OPTIONS:
562            methodname = 'test_configure_' + option
563            if not hasattr(cls, methodname):
564                for source_class in source_classes:
565                    if hasattr(source_class, methodname):
566                        setattr(cls, methodname,
567                                getattr(source_class, methodname))
568                        break
569                else:
570                    def test(self, option=option):
571                        widget = self.create()
572                        widget[option]
573                        raise AssertionError('Option "%s" is not tested in %s' %
574                                             (option, cls.__name__))
575                    test.__name__ = methodname
576                    setattr(cls, methodname, test)
577        return cls
578    return decorator
579
580def setUpModule():
581    if test.support.verbose:
582        tcl = tkinter.Tcl()
583        print('patchlevel =', tcl.call('info', 'patchlevel'), flush=True)
584