@@ -2368,6 +2368,26 @@ class CustomListener(logging.handlers.QueueListener):
23682368class CustomQueue (queue .Queue ):
23692369 pass
23702370
2371+ class CustomQueueProtocol :
2372+ def __init__ (self , maxsize = 0 ):
2373+ self .queue = queue .Queue (maxsize )
2374+
2375+ def __getattr__ (self , attribute ):
2376+ queue = object .__getattribute__ (self , 'queue' )
2377+ return getattr (queue , attribute )
2378+
2379+ class CustomQueueFakeProtocol (CustomQueueProtocol ):
2380+ # An object implementing the Queue API (incorrect signatures).
2381+ # The object will be considered a valid queue class since we
2382+ # do not check the signatures (only callability of methods)
2383+ # but will NOT be usable in production since a TypeError will
2384+ # be raised due to a missing argument.
2385+ def empty (self , x ):
2386+ pass
2387+
2388+ class CustomQueueWrongProtocol (CustomQueueProtocol ):
2389+ empty = None
2390+
23712391def queueMaker ():
23722392 return queue .Queue ()
23732393
@@ -3901,18 +3921,16 @@ def do_queuehandler_configuration(self, qspec, lspec):
39013921 @threading_helper .requires_working_threading ()
39023922 @support .requires_subprocess ()
39033923 def test_config_queue_handler (self ):
3904- q = CustomQueue ()
3905- dq = {
3906- '()' : __name__ + '.CustomQueue' ,
3907- 'maxsize' : 10
3908- }
3924+ qs = [CustomQueue (), CustomQueueProtocol ()]
3925+ dqs = [{'()' : f'{ __name__ } .{ cls } ' , 'maxsize' : 10 }
3926+ for cls in ['CustomQueue' , 'CustomQueueProtocol' ]]
39093927 dl = {
39103928 '()' : __name__ + '.listenerMaker' ,
39113929 'arg1' : None ,
39123930 'arg2' : None ,
39133931 'respect_handler_level' : True
39143932 }
3915- qvalues = (None , __name__ + '.queueMaker' , __name__ + '.CustomQueue' , dq , q )
3933+ qvalues = (None , __name__ + '.queueMaker' , __name__ + '.CustomQueue' , * dqs , * qs )
39163934 lvalues = (None , __name__ + '.CustomListener' , dl , CustomListener )
39173935 for qspec , lspec in itertools .product (qvalues , lvalues ):
39183936 self .do_queuehandler_configuration (qspec , lspec )
@@ -3932,15 +3950,21 @@ def test_config_queue_handler(self):
39323950 @support .requires_subprocess ()
39333951 @patch ("multiprocessing.Manager" )
39343952 def test_config_queue_handler_does_not_create_multiprocessing_manager (self , manager ):
3935- # gh-120868
3953+ # gh-120868, gh-121723
39363954
39373955 from multiprocessing import Queue as MQ
39383956
39393957 q1 = {"()" : "queue.Queue" , "maxsize" : - 1 }
39403958 q2 = MQ ()
39413959 q3 = queue .Queue ()
3942-
3943- for qspec in (q1 , q2 , q3 ):
3960+ # CustomQueueFakeProtocol passes the checks but will not be usable
3961+ # since the signatures are incompatible. Checking the Queue API
3962+ # without testing the type of the actual queue is a trade-off
3963+ # between usability and the work we need to do in order to safely
3964+ # check that the queue object correctly implements the API.
3965+ q4 = CustomQueueFakeProtocol ()
3966+
3967+ for qspec in (q1 , q2 , q3 , q4 ):
39443968 self .apply_config (
39453969 {
39463970 "version" : 1 ,
@@ -3956,21 +3980,62 @@ def test_config_queue_handler_does_not_create_multiprocessing_manager(self, mana
39563980
39573981 @patch ("multiprocessing.Manager" )
39583982 def test_config_queue_handler_invalid_config_does_not_create_multiprocessing_manager (self , manager ):
3959- # gh-120868
3983+ # gh-120868, gh-121723
39603984
3961- with self .assertRaises (ValueError ):
3962- self .apply_config (
3963- {
3964- "version" : 1 ,
3965- "handlers" : {
3966- "queue_listener" : {
3967- "class" : "logging.handlers.QueueHandler" ,
3968- "queue" : object (),
3985+ for qspec in [object (), CustomQueueWrongProtocol ()]:
3986+ with self .assertRaises (ValueError ):
3987+ self .apply_config (
3988+ {
3989+ "version" : 1 ,
3990+ "handlers" : {
3991+ "queue_listener" : {
3992+ "class" : "logging.handlers.QueueHandler" ,
3993+ "queue" : qspec ,
3994+ },
39693995 },
3970- },
3996+ }
3997+ )
3998+ manager .assert_not_called ()
3999+
4000+ @skip_if_tsan_fork
4001+ @support .requires_subprocess ()
4002+ @unittest .skipUnless (support .Py_DEBUG , "requires a debug build for testing"
4003+ "assertions in multiprocessing" )
4004+ def test_config_queue_handler_multiprocessing_context (self ):
4005+ # regression test for gh-121723
4006+ if support .MS_WINDOWS :
4007+ start_methods = ['spawn' ]
4008+ else :
4009+ start_methods = ['spawn' , 'fork' , 'forkserver' ]
4010+ for start_method in start_methods :
4011+ with self .subTest (start_method = start_method ):
4012+ ctx = multiprocessing .get_context (start_method )
4013+ with ctx .Manager () as manager :
4014+ q = manager .Queue ()
4015+ records = []
4016+ # use 1 process and 1 task per child to put 1 record
4017+ with ctx .Pool (1 , initializer = self ._mpinit_issue121723 ,
4018+ initargs = (q , "text" ), maxtasksperchild = 1 ):
4019+ records .append (q .get (timeout = 60 ))
4020+ self .assertTrue (q .empty ())
4021+ self .assertEqual (len (records ), 1 )
4022+
4023+ @staticmethod
4024+ def _mpinit_issue121723 (qspec , message_to_log ):
4025+ # static method for pickling support
4026+ logging .config .dictConfig ({
4027+ 'version' : 1 ,
4028+ 'disable_existing_loggers' : True ,
4029+ 'handlers' : {
4030+ 'log_to_parent' : {
4031+ 'class' : 'logging.handlers.QueueHandler' ,
4032+ 'queue' : qspec
39714033 }
3972- )
3973- manager .assert_not_called ()
4034+ },
4035+ 'root' : {'handlers' : ['log_to_parent' ], 'level' : 'DEBUG' }
4036+ })
4037+ # log a message (this creates a record put in the queue)
4038+ logging .getLogger ().info (message_to_log )
39744039
39754040 @skip_if_tsan_fork
39764041 @support .requires_subprocess ()
0 commit comments