@@ -47,7 +47,7 @@ class IOPubThread:
4747 whose IO is always run in a thread.
4848 """
4949
50- def __init__ (self , socket , pipe = False ):
50+ def __init__ (self , socket , session , pipe = False ):
5151 """Create IOPub thread
5252
5353 Parameters
@@ -59,6 +59,7 @@ def __init__(self, socket, pipe=False):
5959 piped from subprocesses.
6060 """
6161 self .socket = socket
62+ self .session = session
6263 self ._stopped = False
6364 self .background_socket = BackgroundSocket (self )
6465 self ._master_pid = os .getpid ()
@@ -73,12 +74,73 @@ def __init__(self, socket, pipe=False):
7374 self ._event_pipe_gc_seconds : float = 10
7475 self ._event_pipe_gc_task : asyncio .Task [Any ] | None = None
7576 self ._setup_event_pipe ()
77+ self ._setup_xpub_listener ()
7678 self .thread = threading .Thread (target = self ._thread_main , name = "IOPub" )
7779 self .thread .daemon = True
7880 self .thread .pydev_do_not_trace = True # type:ignore[attr-defined]
7981 self .thread .is_pydev_daemon_thread = True # type:ignore[attr-defined]
8082 self .thread .name = "IOPub"
8183
84+ def _setup_xpub_listener (self ):
85+ """Setup listener for XPUB subscription events"""
86+
87+ # Checks the socket is not a DummySocket
88+ if not hasattr (self .socket , "getsockopt" ):
89+ return
90+
91+ socket_type = self .socket .getsockopt (zmq .TYPE )
92+ if socket_type == zmq .XPUB :
93+ self ._xpub_stream = ZMQStream (self .socket , self .io_loop )
94+ self ._xpub_stream .on_recv (self ._handle_subscription )
95+
96+ def _handle_subscription (self , frames ):
97+ """Handle subscription/unsubscription events from XPUB socket
98+
99+ XPUB sockets receive:
100+ - subscribe: single frame with b'\\ x01' + topic
101+ - unsubscribe: single frame with b'\\ x00' + topic
102+ """
103+
104+ for frame in frames :
105+ event_type = frame [0 ]
106+ if event_type == 1 :
107+ subscription = frame [1 :] if len (frame ) > 1 else b""
108+ try :
109+ subscription_str = subscription .decode ("utf-8" )
110+ except UnicodeDecodeError :
111+ continue
112+ self ._send_welcome_message (subscription_str )
113+
114+ def _send_welcome_message (self , subscription ):
115+ """Send iopub_welcome message for new subscription
116+
117+ Parameters
118+ ----------
119+ subscription : str
120+ The subscription topic (UTF-8 decoded)
121+ """
122+
123+ content = {"subscription" : subscription }
124+
125+ header = self .session .msg_header ("iopub_welcome" )
126+ msg = {
127+ "header" : header ,
128+ "parent_header" : {},
129+ "metadata" : {},
130+ "content" : content ,
131+ "buffers" : [],
132+ }
133+
134+ msg_list = self .session .serialize (msg )
135+
136+ if subscription :
137+ identity = subscription .encode ("utf-8" )
138+ full_msg = [identity , * msg_list ]
139+ else :
140+ full_msg = msg_list
141+ # Send directly on socket (we're already in IO thread context)
142+ self .socket .send_multipart (full_msg )
143+
82144 def _thread_main (self ):
83145 """The inner loop that's actually run in a thread"""
84146
@@ -447,7 +509,7 @@ def __init__(
447509 DeprecationWarning ,
448510 stacklevel = 2 ,
449511 )
450- pub_thread = IOPubThread (pub_thread )
512+ pub_thread = IOPubThread (pub_thread , self . session )
451513 pub_thread .start ()
452514 self .pub_thread = pub_thread
453515 self .name = name
0 commit comments