1+ import logging
2+ import sys
13import unittest
24import tkinter
3- from tkinter .systray import SysTrayIcon , notify
5+ from tkinter .systray import SysTrayIcon , notify , NotificationHandler
46from test .support import requires
57from test .test_tkinter .support import (AbstractTkTest ,
68 AbstractDefaultRootTest ,
@@ -110,6 +112,95 @@ def test_notify(self):
110112 # Sends a real desktop notification.
111113 icon .notify ('Python test' , 'tkinter.systray test notification' )
112114
115+ @requires_tk (8 , 7 )
116+ def test_notification_handler (self ):
117+ # All real notifications are sent from the same Tcl interpreter.
118+ # Sending a notification after the interpreter which sent
119+ # the previous one was deleted crashes Tk on X11 with libnotify.
120+ if self .root ._windowingsystem != 'x11' :
121+ self .skipTest ('cannot safely send a native notification' )
122+ self .create ()
123+ handler = NotificationHandler ('Python test' , master = self .root )
124+ record = logging .LogRecord ('test' , logging .INFO , __file__ , 0 ,
125+ 'tkinter.systray test notification' ,
126+ None , None )
127+ # Sends a real desktop notification.
128+ handler .emit (record )
129+
130+
131+ class FakeTk :
132+ # Records the calls of "tk sysnotify" instead of sending
133+ # real notifications.
134+
135+ def __init__ (self ):
136+ self .calls = []
137+
138+ def call (self , * args ):
139+ if args [:2 ] != ('tk' , 'sysnotify' ):
140+ raise tkinter .TclError (f'unexpected call: { args } ' )
141+ self .calls .append (args [2 :])
142+
143+
144+ class FakeMaster :
145+ def __init__ (self ):
146+ self .tk = FakeTk ()
147+
148+
149+ class NotificationHandlerTest (unittest .TestCase ):
150+
151+ def setUp (self ):
152+ self .master = FakeMaster ()
153+ self .calls = self .master .tk .calls
154+ self .logger = logging .getLogger ('test.tkinter.systray' )
155+ self .logger .propagate = False
156+ self .logger .setLevel (logging .DEBUG )
157+ self .addCleanup (self .logger .setLevel , logging .NOTSET )
158+ self .addCleanup (setattr , self .logger , 'propagate' , True )
159+
160+ def add_handler (self , * args , ** kwargs ):
161+ handler = NotificationHandler (* args , master = self .master , ** kwargs )
162+ self .logger .addHandler (handler )
163+ self .addCleanup (self .logger .removeHandler , handler )
164+ return handler
165+
166+ def test_emit (self ):
167+ self .add_handler ()
168+ self .logger .warning ('spam %s' , 'eggs' )
169+ self .logger .error ('ham' )
170+ self .assertEqual (self .calls , [('WARNING' , 'spam eggs' ),
171+ ('ERROR' , 'ham' )])
172+
173+ def test_title (self ):
174+ self .add_handler ('Python' )
175+ self .logger .warning ('spam' )
176+ self .assertEqual (self .calls , [('Python' , 'spam' )])
177+
178+ def test_formatter (self ):
179+ handler = self .add_handler ()
180+ handler .setFormatter (logging .Formatter ('%(name)s: %(message)s' ))
181+ self .logger .info ('spam' )
182+ self .assertEqual (self .calls ,
183+ [('INFO' , 'test.tkinter.systray: spam' )])
184+
185+ def test_level (self ):
186+ handler = self .add_handler ()
187+ handler .setLevel (logging .WARNING )
188+ self .logger .info ('spam' )
189+ self .logger .warning ('eggs' )
190+ self .assertEqual (self .calls , [('WARNING' , 'eggs' )])
191+
192+ def test_error (self ):
193+ # Errors in sending a notification are handled by handleError().
194+ handler = self .add_handler ()
195+ errors = []
196+ handler .handleError = errors .append
197+ def call (* args ):
198+ raise tkinter .TclError ('no notifications' )
199+ self .master .tk .call = call
200+ self .logger .warning ('spam' )
201+ self .assertEqual (len (errors ), 1 )
202+ self .assertEqual (errors [0 ].getMessage (), 'spam' )
203+
113204
114205class DefaultRootTest (AbstractDefaultRootTest , unittest .TestCase ):
115206
@@ -129,6 +220,19 @@ def test_systray(self):
129220 self .assertRaises (RuntimeError , SysTrayIcon , image = 'none' )
130221 self .assertRaises (RuntimeError , notify , 'title' , 'message' )
131222
223+ def test_notification_handler (self ):
224+ # The default root window is looked up when a record is emitted.
225+ handler = NotificationHandler ()
226+ self .assertIsNone (handler .master )
227+ errors = []
228+ handler .handleError = lambda record : errors .append (sys .exception ())
229+ record = logging .LogRecord ('test' , logging .INFO , __file__ , 0 ,
230+ 'message' , None , None )
231+ tkinter .NoDefaultRoot ()
232+ handler .emit (record )
233+ self .assertEqual (len (errors ), 1 )
234+ self .assertIsInstance (errors [0 ], RuntimeError )
235+
132236
133237if __name__ == "__main__" :
134238 unittest .main ()
0 commit comments