12
12
)
13
13
14
14
__all__ = [
15
+ 'UNBOUND' , 'UNBOUND_ERROR' , 'UNBOUND_REMOVE' ,
15
16
'create' , 'list_all' ,
16
17
'Queue' ,
17
18
'QueueError' , 'QueueNotFoundError' , 'QueueEmpty' , 'QueueFull' ,
19
+ 'ItemInterpreterDestroyed' ,
18
20
]
19
21
20
22
@@ -32,47 +34,119 @@ class QueueFull(QueueError, queue.Full):
32
34
"""
33
35
34
36
37
+ class ItemInterpreterDestroyed (QueueError ):
38
+ """Raised from get() and get_nowait()."""
39
+
40
+
35
41
_SHARED_ONLY = 0
36
42
_PICKLED = 1
37
43
38
- def create (maxsize = 0 , * , syncobj = False ):
44
+
45
+ class UnboundItem :
46
+ """Represents a Queue item no longer bound to an interpreter.
47
+
48
+ An item is unbound when the interpreter that added it to the queue
49
+ is destroyed.
50
+ """
51
+
52
+ __slots__ = ()
53
+
54
+ def __new__ (cls ):
55
+ return UNBOUND
56
+
57
+ def __repr__ (self ):
58
+ return f'interpreters.queues.UNBOUND'
59
+
60
+
61
+ UNBOUND = object .__new__ (UnboundItem )
62
+ UNBOUND_ERROR = object ()
63
+ UNBOUND_REMOVE = object ()
64
+
65
+ _UNBOUND_CONSTANT_TO_FLAG = {
66
+ UNBOUND_REMOVE : 1 ,
67
+ UNBOUND_ERROR : 2 ,
68
+ UNBOUND : 3 ,
69
+ }
70
+ _UNBOUND_FLAG_TO_CONSTANT = {v : k
71
+ for k , v in _UNBOUND_CONSTANT_TO_FLAG .items ()}
72
+
73
+ def _serialize_unbound (unbound ):
74
+ op = unbound
75
+ try :
76
+ flag = _UNBOUND_CONSTANT_TO_FLAG [op ]
77
+ except KeyError :
78
+ raise NotImplementedError (f'unsupported unbound replacement op { op !r} ' )
79
+ return flag ,
80
+
81
+
82
+ def _resolve_unbound (flag ):
83
+ try :
84
+ op = _UNBOUND_FLAG_TO_CONSTANT [flag ]
85
+ except KeyError :
86
+ raise NotImplementedError (f'unsupported unbound replacement op { flag !r} ' )
87
+ if op is UNBOUND_REMOVE :
88
+ # "remove" not possible here
89
+ raise NotImplementedError
90
+ elif op is UNBOUND_ERROR :
91
+ raise ItemInterpreterDestroyed ("item's original interpreter destroyed" )
92
+ elif op is UNBOUND :
93
+ return UNBOUND
94
+ else :
95
+ raise NotImplementedError (repr (op ))
96
+
97
+
98
+ def create (maxsize = 0 , * , syncobj = False , unbounditems = UNBOUND ):
39
99
"""Return a new cross-interpreter queue.
40
100
41
101
The queue may be used to pass data safely between interpreters.
42
102
43
103
"syncobj" sets the default for Queue.put()
44
104
and Queue.put_nowait().
105
+
106
+ "unbounditems" likewise sets the default. See Queue.put() for
107
+ supported values. The default value is UNBOUND, which replaces
108
+ the unbound item.
45
109
"""
46
110
fmt = _SHARED_ONLY if syncobj else _PICKLED
47
- qid = _queues .create (maxsize , fmt )
48
- return Queue (qid , _fmt = fmt )
111
+ unbound = _serialize_unbound (unbounditems )
112
+ unboundop , = unbound
113
+ qid = _queues .create (maxsize , fmt , unboundop )
114
+ return Queue (qid , _fmt = fmt , _unbound = unbound )
49
115
50
116
51
117
def list_all ():
52
118
"""Return a list of all open queues."""
53
- return [Queue (qid , _fmt = fmt )
54
- for qid , fmt in _queues .list_all ()]
119
+ return [Queue (qid , _fmt = fmt , _unbound = ( unboundop ,) )
120
+ for qid , fmt , unboundop in _queues .list_all ()]
55
121
56
122
57
123
_known_queues = weakref .WeakValueDictionary ()
58
124
59
125
class Queue :
60
126
"""A cross-interpreter queue."""
61
127
62
- def __new__ (cls , id , / , * , _fmt = None ):
128
+ def __new__ (cls , id , / , * , _fmt = None , _unbound = None ):
63
129
# There is only one instance for any given ID.
64
130
if isinstance (id , int ):
65
131
id = int (id )
66
132
else :
67
133
raise TypeError (f'id must be an int, got { id !r} ' )
68
134
if _fmt is None :
69
- _fmt , = _queues .get_queue_defaults (id )
135
+ if _unbound is None :
136
+ _fmt , op = _queues .get_queue_defaults (id )
137
+ _unbound = (op ,)
138
+ else :
139
+ _fmt , _ = _queues .get_queue_defaults (id )
140
+ elif _unbound is None :
141
+ _ , op = _queues .get_queue_defaults (id )
142
+ _unbound = (op ,)
70
143
try :
71
144
self = _known_queues [id ]
72
145
except KeyError :
73
146
self = super ().__new__ (cls )
74
147
self ._id = id
75
148
self ._fmt = _fmt
149
+ self ._unbound = _unbound
76
150
_known_queues [id ] = self
77
151
_queues .bind (id )
78
152
return self
@@ -124,14 +198,15 @@ def qsize(self):
124
198
125
199
def put (self , obj , timeout = None , * ,
126
200
syncobj = None ,
201
+ unbound = None ,
127
202
_delay = 10 / 1000 , # 10 milliseconds
128
203
):
129
204
"""Add the object to the queue.
130
205
131
206
This blocks while the queue is full.
132
207
133
208
If "syncobj" is None (the default) then it uses the
134
- queue's default, set with create_queue()..
209
+ queue's default, set with create_queue().
135
210
136
211
If "syncobj" is false then all objects are supported,
137
212
at the expense of worse performance.
@@ -152,11 +227,37 @@ def put(self, obj, timeout=None, *,
152
227
actually is. That's a slightly different and stronger promise
153
228
than just (initial) equality, which is all "syncobj=False"
154
229
can promise.
230
+
231
+ "unbound" controls the behavior of Queue.get() for the given
232
+ object if the current interpreter (calling put()) is later
233
+ destroyed.
234
+
235
+ If "unbound" is None (the default) then it uses the
236
+ queue's default, set with create_queue(),
237
+ which is usually UNBOUND.
238
+
239
+ If "unbound" is UNBOUND_ERROR then get() will raise an
240
+ ItemInterpreterDestroyed exception if the original interpreter
241
+ has been destroyed. This does not otherwise affect the queue;
242
+ the next call to put() will work like normal, returning the next
243
+ item in the queue.
244
+
245
+ If "unbound" is UNBOUND_REMOVE then the item will be removed
246
+ from the queue as soon as the original interpreter is destroyed.
247
+ Be aware that this will introduce an imbalance between put()
248
+ and get() calls.
249
+
250
+ If "unbound" is UNBOUND then it is returned by get() in place
251
+ of the unbound item.
155
252
"""
156
253
if syncobj is None :
157
254
fmt = self ._fmt
158
255
else :
159
256
fmt = _SHARED_ONLY if syncobj else _PICKLED
257
+ if unbound is None :
258
+ unboundop , = self ._unbound
259
+ else :
260
+ unboundop , = _serialize_unbound (unbound )
160
261
if timeout is not None :
161
262
timeout = int (timeout )
162
263
if timeout < 0 :
@@ -166,29 +267,37 @@ def put(self, obj, timeout=None, *,
166
267
obj = pickle .dumps (obj )
167
268
while True :
168
269
try :
169
- _queues .put (self ._id , obj , fmt )
270
+ _queues .put (self ._id , obj , fmt , unboundop )
170
271
except QueueFull as exc :
171
272
if timeout is not None and time .time () >= end :
172
273
raise # re-raise
173
274
time .sleep (_delay )
174
275
else :
175
276
break
176
277
177
- def put_nowait (self , obj , * , syncobj = None ):
278
+ def put_nowait (self , obj , * , syncobj = None , unbound = None ):
178
279
if syncobj is None :
179
280
fmt = self ._fmt
180
281
else :
181
282
fmt = _SHARED_ONLY if syncobj else _PICKLED
283
+ if unbound is None :
284
+ unboundop , = self ._unbound
285
+ else :
286
+ unboundop , = _serialize_unbound (unbound )
182
287
if fmt is _PICKLED :
183
288
obj = pickle .dumps (obj )
184
- _queues .put (self ._id , obj , fmt )
289
+ _queues .put (self ._id , obj , fmt , unboundop )
185
290
186
291
def get (self , timeout = None , * ,
187
292
_delay = 10 / 1000 , # 10 milliseconds
188
293
):
189
294
"""Return the next object from the queue.
190
295
191
296
This blocks while the queue is empty.
297
+
298
+ If the next item's original interpreter has been destroyed
299
+ then the "next object" is determined by the value of the
300
+ "unbound" argument to put().
192
301
"""
193
302
if timeout is not None :
194
303
timeout = int (timeout )
@@ -197,13 +306,16 @@ def get(self, timeout=None, *,
197
306
end = time .time () + timeout
198
307
while True :
199
308
try :
200
- obj , fmt = _queues .get (self ._id )
309
+ obj , fmt , unboundop = _queues .get (self ._id )
201
310
except QueueEmpty as exc :
202
311
if timeout is not None and time .time () >= end :
203
312
raise # re-raise
204
313
time .sleep (_delay )
205
314
else :
206
315
break
316
+ if unboundop is not None :
317
+ assert obj is None , repr (obj )
318
+ return _resolve_unbound (unboundop )
207
319
if fmt == _PICKLED :
208
320
obj = pickle .loads (obj )
209
321
else :
@@ -217,9 +329,12 @@ def get_nowait(self):
217
329
is the same as get().
218
330
"""
219
331
try :
220
- obj , fmt = _queues .get (self ._id )
332
+ obj , fmt , unboundop = _queues .get (self ._id )
221
333
except QueueEmpty as exc :
222
334
raise # re-raise
335
+ if unboundop is not None :
336
+ assert obj is None , repr (obj )
337
+ return _resolve_unbound (unboundop )
223
338
if fmt == _PICKLED :
224
339
obj = pickle .loads (obj )
225
340
else :
0 commit comments