zprocess 2.26.0__tar.gz → 2.26.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {zprocess-2.26.0/zprocess.egg-info → zprocess-2.26.1}/PKG-INFO +2 -2
  2. {zprocess-2.26.0 → zprocess-2.26.1}/pyproject.toml +1 -1
  3. {zprocess-2.26.0 → zprocess-2.26.1}/tests/test_zprocess.py +513 -510
  4. {zprocess-2.26.0 → zprocess-2.26.1/zprocess.egg-info}/PKG-INFO +2 -2
  5. {zprocess-2.26.0 → zprocess-2.26.1}/.github/workflows/release.yml +0 -0
  6. {zprocess-2.26.0 → zprocess-2.26.1}/.gitignore +0 -0
  7. {zprocess-2.26.0 → zprocess-2.26.1}/LICENSE.txt +0 -0
  8. {zprocess-2.26.0 → zprocess-2.26.1}/README.md +0 -0
  9. {zprocess-2.26.0 → zprocess-2.26.1}/doc/Makefile +0 -0
  10. {zprocess-2.26.0 → zprocess-2.26.1}/doc/conf.py +0 -0
  11. {zprocess-2.26.0 → zprocess-2.26.1}/doc/index.rst +0 -0
  12. {zprocess-2.26.0 → zprocess-2.26.1}/doc/requirements.txt +0 -0
  13. {zprocess-2.26.0 → zprocess-2.26.1}/pytest.ini +0 -0
  14. {zprocess-2.26.0 → zprocess-2.26.1}/requirements.txt +0 -0
  15. {zprocess-2.26.0 → zprocess-2.26.1}/run_tests.py +0 -0
  16. {zprocess-2.26.0 → zprocess-2.26.1}/setup.cfg +0 -0
  17. {zprocess-2.26.0 → zprocess-2.26.1}/tests/GUI_output_redirection.py +0 -0
  18. {zprocess-2.26.0 → zprocess-2.26.1}/tests/output_redirection.py +0 -0
  19. {zprocess-2.26.0 → zprocess-2.26.1}/tests/test_remote.py +0 -0
  20. {zprocess-2.26.0 → zprocess-2.26.1}/tests/test_zlock_server.py +0 -0
  21. {zprocess-2.26.0 → zprocess-2.26.1}/tests/test_zlog_server.py +0 -0
  22. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/__init__.py +0 -0
  23. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/__version__.py +0 -0
  24. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/clientserver.py +0 -0
  25. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/cryptoproxy.py +0 -0
  26. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/examples/example_child.py +0 -0
  27. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/examples/example_parent.py +0 -0
  28. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/examples/processclass_example.py +0 -0
  29. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/examples/processclass_remote_example.py +0 -0
  30. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/locking/__init__.py +0 -0
  31. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/locking/__main__.py +0 -0
  32. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/makesecret.py +0 -0
  33. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/process_class_wrapper.py +0 -0
  34. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/process_tree.py +0 -0
  35. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/remote/__init__.py +0 -0
  36. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/remote/__main__.py +0 -0
  37. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/remote/curses_server.py +0 -0
  38. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/remote/server.py +0 -0
  39. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/security.py +0 -0
  40. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/socks/__init__.py +0 -0
  41. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/socks/server.py +0 -0
  42. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/tasks.py +0 -0
  43. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/utils.py +0 -0
  44. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/zlock/__init__.py +0 -0
  45. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/zlock/__main__.py +0 -0
  46. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/zlock/server.py +0 -0
  47. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/zlog/__init__.py +0 -0
  48. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/zlog/__main__.py +0 -0
  49. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess/zlog/server.py +0 -0
  50. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess.egg-info/SOURCES.txt +0 -0
  51. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess.egg-info/dependency_links.txt +0 -0
  52. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess.egg-info/requires.txt +0 -0
  53. {zprocess-2.26.0 → zprocess-2.26.1}/zprocess.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: zprocess
3
- Version: 2.26.0
3
+ Version: 2.26.1
4
4
  Summary: A set of utilities for multiprocessing using zeromq
5
5
  Author-email: Christopher Billington <chrisjbillington@gmail.com>
6
6
  License: Copyright (c) 2013, Christopher Billington
@@ -34,7 +34,7 @@ Project-URL: Tracker, https://github.com/chrisjbillington/zprocess/issues
34
34
  Keywords: multiprocessing,IPC,zeromq
35
35
  Classifier: License :: OSI Approved :: BSD License
36
36
  Classifier: Programming Language :: Python :: 3 :: Only
37
- Requires-Python: >=3.9
37
+ Requires-Python: >=3.8
38
38
  Description-Content-Type: text/markdown
39
39
  License-File: LICENSE.txt
40
40
  Requires-Dist: setuptools_scm
@@ -20,7 +20,7 @@ classifiers = [
20
20
  "License :: OSI Approved :: BSD License",
21
21
  "Programming Language :: Python :: 3 :: Only",
22
22
  ]
23
- requires-python = ">=3.9"
23
+ requires-python = ">=3.8"
24
24
  dependencies = [
25
25
  "setuptools_scm",
26
26
  "pyzmq",
@@ -1,510 +1,513 @@
1
- import unittest
2
- from pathlib import Path
3
- import sys
4
- import os
5
- import time
6
- import threading
7
-
8
- import zmq
9
- import pytest
10
-
11
- THIS_DIR = Path(__file__).absolute().parent
12
-
13
- # Add project root to import path
14
- PROJECT_ROOT = THIS_DIR.parent
15
- if PROJECT_ROOT not in [Path(s).absolute() for s in sys.path]:
16
- sys.path.insert(0, str(PROJECT_ROOT))
17
-
18
- import zprocess
19
- zprocess._silent = True
20
- from zprocess import (ZMQServer, Process, TimeoutError, RichStreamHandler, rich_print,
21
- raise_exception_in_thread, zmq_get, zmq_push, zmq_get_raw,
22
- ExternalBroker)
23
- import zprocess.clientserver as clientserver
24
- from zprocess.clientserver import _typecheck_or_convert_data
25
- from zprocess.process_tree import _default_process_tree, EventBroker
26
- shared_secret = _default_process_tree.shared_secret
27
- from zprocess.security import SecureContext
28
- from zprocess.tasks import Task, TaskQueue
29
-
30
-
31
- class TestError(Exception):
32
- pass
33
-
34
-
35
- class RaiseExceptionInThreadTest(unittest.TestCase):
36
-
37
- def setUp(self):
38
- # Mock threading.Thread to just run a function in the main thread:
39
- class MockThread(object):
40
- used = False
41
- def __init__(self, target, args):
42
- self.target = target
43
- self.args = args
44
- def start(self):
45
- MockThread.used = True
46
- self.target(*self.args)
47
- self.mock_thread = MockThread
48
- self.orig_thread = threading.Thread
49
- threading.Thread = MockThread
50
-
51
- def test_can_raise_exception_in_thread(self):
52
- try:
53
- raise TestError('test')
54
- except Exception:
55
- exc_info = sys.exc_info()
56
- with self.assertRaises(TestError):
57
- raise_exception_in_thread(exc_info)
58
- self.assertTrue(self.mock_thread.used)
59
-
60
- def tearDown(self):
61
- # Restore threading.Thread to what it should be
62
- threading.Thread = self.orig_thread
63
-
64
-
65
- class TypeCheckConvertTests(unittest.TestCase):
66
- """test the _typecheck_or_convert_data function"""
67
-
68
- def test_turns_None_into_empty_bytestring_raw(self):
69
- result = _typecheck_or_convert_data(None, 'raw')
70
- self.assertEqual(result, b'')
71
-
72
- def test_turns_None_into_empty_bytestring_multipart(self):
73
- result = _typecheck_or_convert_data(None, 'multipart')
74
- self.assertEqual(result, [b''])
75
-
76
- def test_wraps_bytestring_into_list_multipart(self):
77
- data = b'spam'
78
- result = _typecheck_or_convert_data(data, 'multipart')
79
- self.assertEqual(result, [data])
80
-
81
- def test_accepts_bytes_raw(self):
82
- data = b'spam'
83
- result = _typecheck_or_convert_data(data, 'raw')
84
- self.assertEqual(result, data)
85
-
86
- def test_accepts_list_of_bytes_multipart(self):
87
- data = [b'spam', b'ham']
88
- result = _typecheck_or_convert_data(data, 'multipart')
89
- self.assertEqual(result, data)
90
-
91
- def test_accepts_string_string(self):
92
- data = 'spam'
93
- result = _typecheck_or_convert_data(data, 'string')
94
- self.assertEqual(result, data)
95
-
96
- def test_accepts_pyobj_pyobj(self):
97
- data = {'spam': ['ham'], 'eggs': True}
98
- result = _typecheck_or_convert_data(data, 'pyobj')
99
- self.assertEqual(result, data)
100
-
101
- def test_rejects_string_raw(self):
102
- data = 'spam'
103
- with self.assertRaises(TypeError):
104
- _typecheck_or_convert_data(data, 'raw')
105
-
106
- def test_rejects_string_multipart(self):
107
- data = [b'spam', 'ham']
108
- with self.assertRaises(TypeError):
109
- _typecheck_or_convert_data(data, 'multipart')
110
-
111
- def test_rejects_pyobj_string(self):
112
- data = {'spam': ['ham'], 'eggs': True}
113
- with self.assertRaises(TypeError):
114
- _typecheck_or_convert_data(data, 'string')
115
-
116
- def test_rejects_invalid_send_type(self):
117
- data = {'spam': ['ham'], 'eggs': True}
118
- with self.assertRaises(ValueError):
119
- _typecheck_or_convert_data(data, 'invalid_send_type')
120
-
121
-
122
- class TestProcess(Process):
123
- def run(self):
124
- item = self.from_parent.get()
125
- x, y = item
126
- sys.stdout.write(repr(x))
127
- sys.stderr.write(y)
128
- self.to_parent.put(item)
129
- os.system('echo hello from echo')
130
- # Wait some time here to reduce the chance of output arriving out of order,
131
- # which would break the tests even though we don't actually guarantee that the
132
- # order will come out right.
133
- time.sleep(0.2)
134
-
135
- # And now test logging:
136
- import logging
137
- logger = logging.Logger('test')
138
- logger.setLevel(logging.DEBUG)
139
- logger.addHandler(RichStreamHandler())
140
- logger.info('this is a log message')
141
-
142
- # And now test rich printing
143
- rich_print('some test text', color='#123456', bold=True, italic=False)
144
-
145
-
146
-
147
- class ProcessClassTests(unittest.TestCase):
148
-
149
- def setUp(self):
150
- """Create a subprocess with output redirection to a zmq port"""
151
- context = SecureContext.instance(shared_secret=shared_secret)
152
- self.redirection_sock = context.socket(zmq.PULL)
153
- redirection_port = self.redirection_sock.bind_to_random_port(
154
- 'tcp://127.0.0.1')
155
- self.process = TestProcess(output_redirection_port=redirection_port)
156
-
157
- def test_process(self):
158
- to_child, from_child = self.process.start()
159
- # Check the child process is running:
160
- self.assertIs(self.process.child.poll(), None)
161
-
162
- # Send some data:
163
- x = [('spam', ['ham']), ('eggs', True)]
164
- y = 'über'
165
- data = (x, y)
166
- to_child.put(data)
167
-
168
- # Subprocess should send back the data unmodified:
169
- recv_data = from_child.get(timeout=1)
170
- self.assertEqual(recv_data, data)
171
-
172
- # Subprocess should not send any more data, expect TimeoutError:
173
- with self.assertRaises(TimeoutError):
174
- from_child.get(timeout=0.1)
175
-
176
- # Check we recieved its stdout and stderr:
177
- self.assertEqual(self.redirection_sock.poll(1000), zmq.POLLIN)
178
- self.assertEqual(self.redirection_sock.recv_multipart(),
179
- [b'stdout', repr(x).encode('utf8')])
180
- self.assertEqual(self.redirection_sock.poll(1000), zmq.POLLIN)
181
- self.assertEqual(self.redirection_sock.recv_multipart(),
182
- [b'stderr', y.encode('utf8')])
183
- # And the shell output:
184
- self.assertEqual(self.redirection_sock.poll(1000), zmq.POLLIN)
185
- self.assertEqual(self.redirection_sock.recv_multipart(),
186
- [b'stdout', b'hello from echo\n'])
187
-
188
- # And the formatted logging:
189
- self.assertEqual(self.redirection_sock.recv_multipart(),
190
- [b'INFO', b'this is a log message\n'])
191
-
192
- # And the formatted printing:
193
- import ast
194
- charfmt_repr, text = self.redirection_sock.recv_multipart()
195
- self.assertEqual(ast.literal_eval(charfmt_repr.decode('utf8')), ('#123456', True, False))
196
- self.assertEqual(text, b'some test text\n')
197
-
198
- # And no more...
199
- self.assertEqual(self.redirection_sock.poll(100), 0)
200
-
201
- def tearDown(self):
202
- self.process.terminate()
203
- self.redirection_sock.close()
204
-
205
-
206
- class HeartbeatClientTestProcess(Process):
207
- """For testing that subprocesses are behaving correcly re. heartbeats"""
208
- def run(self):
209
- self.from_parent.get()
210
- # If the parent sends a message, acquire the kill lock for 3 seconds:
211
- with self.kill_lock:
212
- time.sleep(3)
213
- time.sleep(10)
214
-
215
-
216
- class HeartbeatServerTestProcess(Process):
217
- """For testing that parent processes are behaving correcly re. heartbeats"""
218
- def run(self):
219
- # We'll send heartbeats of our own, independent of the HeartbeatClient
220
- # already running in this process:
221
- shared_secret = self.process_tree.shared_secret
222
- context = SecureContext.instance(shared_secret=shared_secret)
223
- sock = context.socket(zmq.REQ)
224
- sock.setsockopt(zmq.LINGER, 0)
225
- server_port = self.from_parent.get()
226
- sock.connect('tcp://127.0.0.1:%s' % server_port)
227
- # Send a heartbeat with whatever data:
228
- data = b'heartbeat_data'
229
- sock.send(data)
230
- if sock.poll(1000):
231
- response = sock.recv()
232
- # Tell the parent whether things were as expected:
233
- if response == data:
234
- self.to_parent.put(True)
235
- time.sleep(1) # Ensure it sends before we return
236
- return
237
- self.to_parent.put(False)
238
- time.sleep(1) # Ensure it sends before we return
239
-
240
-
241
- class HeartbeatTests(unittest.TestCase):
242
- def setUp(self):
243
- """Create a sock for output redirection and a zmq port to mock a heartbeat
244
- server"""
245
- import zmq
246
- context = SecureContext.instance(shared_secret=shared_secret)
247
- self.heartbeat_sock = context.socket(zmq.REP)
248
- heartbeat_port = self.heartbeat_sock.bind_to_random_port('tcp://127.0.0.1')
249
-
250
- class mock_heartbeat_server(object):
251
- port = heartbeat_port
252
-
253
- self.mock_heartbeat_server = mock_heartbeat_server
254
-
255
- def test_subproc_lives_with_heartbeats(self):
256
- _default_process_tree.heartbeat_server = self.mock_heartbeat_server
257
- self.process = HeartbeatClientTestProcess()
258
- self.process.start()
259
- for i in range(3):
260
- # Wait for a heartbeat request:
261
- self.assertTrue(self.heartbeat_sock.poll(3000))
262
- # Echo it back:
263
- self.heartbeat_sock.send(self.heartbeat_sock.recv())
264
-
265
- def test_subproc_dies_without_heartbeats(self):
266
- _default_process_tree.heartbeat_server = self.mock_heartbeat_server
267
- self.process = HeartbeatClientTestProcess()
268
- self.process.start()
269
- # Wait for a heartbeat request:
270
- self.assertTrue(self.heartbeat_sock.poll(3000))
271
- # Don't respond to it
272
- time.sleep(2)
273
- # Process should be dead:
274
- self.assertIsNot(self.process.child.poll(), None)
275
-
276
- def test_subproc_dies_on_incorrect_response(self):
277
- _default_process_tree.heartbeat_server = self.mock_heartbeat_server
278
- self.process = HeartbeatClientTestProcess()
279
- self.process.start()
280
- # Wait for a heartbeat request:
281
- self.assertTrue(self.heartbeat_sock.poll(3000))
282
- # Echo it back wrongly:
283
- self.heartbeat_sock.send(self.heartbeat_sock.recv() + b'wrong')
284
- time.sleep(1)
285
- # Process should be dead:
286
- self.assertIsNot(self.process.child.poll(), None)
287
-
288
- def test_subproc_survives_until_kill_lock_released(self):
289
- _default_process_tree.heartbeat_server = self.mock_heartbeat_server
290
- self.process = HeartbeatClientTestProcess()
291
- to_child, from_child = self.process.start()
292
- # Wait for a heartbeat request:
293
- self.assertTrue(self.heartbeat_sock.poll(3000))
294
- # Tell child to acquire kill lock for 3 sec:
295
- to_child.put(None)
296
- # Don't respond to the heartbeat, process should still be alive 2 sec later
297
- time.sleep(2)
298
- # Process should be alive:
299
- self.assertIs(self.process.child.poll(), None)
300
- # After kill lock released, child should be terminated:
301
- time.sleep(2)
302
- # Process should be dead:
303
- self.assertIsNot(self.process.child.poll(), None)
304
-
305
- def test_parent_correctly_responds_to_heartbeats(self):
306
- # No mock server this time, we're testing the real one:
307
- _default_process_tree.heartbeat_server = None
308
- self.process = HeartbeatServerTestProcess()
309
- to_child, from_child = self.process.start()
310
- to_child.put(_default_process_tree.heartbeat_server.port)
311
- self.assertTrue(from_child.sock.poll(1000))
312
- self.assertTrue(from_child.get())
313
-
314
- def tearDown(self):
315
- self.heartbeat_sock.close()
316
- try:
317
- self.process.terminate()
318
- except Exception:
319
- pass # already dead
320
-
321
- class TestEventProcess(Process):
322
- def run(self):
323
- event = self.process_tree.event('hello', role='post')
324
- event.post('1', data=u'boo')
325
- time.sleep(0.5)
326
-
327
- class TestExternalEventProcess(Process):
328
- def run(self, broker_details):
329
- event = self.process_tree.event('hello', role='post', external_broker=broker_details)
330
- event.post('1', data=u'boo')
331
- time.sleep(0.5)
332
-
333
-
334
-
335
- class EventTests(unittest.TestCase):
336
- def test_events(self):
337
- proc = TestEventProcess()
338
- event = _default_process_tree.event('hello', role='wait')
339
- proc.start()
340
- try:
341
- data = event.wait('1', timeout=1)
342
- self.assertEqual(data, u'boo')
343
- finally:
344
- proc.terminate()
345
-
346
- def test_external_broker(self):
347
- broker = EventBroker(bind_address='tcp://127.0.0.1')
348
- broker_details = ExternalBroker('localhost', broker.in_port, broker.out_port)
349
- proc = TestExternalEventProcess()
350
- event = _default_process_tree.event('hello', role='wait', external_broker=broker_details)
351
- proc.start(broker_details)
352
- try:
353
- data = event.wait('1', timeout=1)
354
- self.assertEqual(data, u'boo')
355
- finally:
356
- proc.terminate()
357
-
358
-
359
- class TaskTests(unittest.TestCase):
360
- def test_cant_call_task_twice(self):
361
- task = Task(1, lambda: None)
362
- task()
363
- with self.assertRaises(RuntimeError):
364
- task()
365
-
366
- def test_queue(self):
367
- # Test insert order:
368
- queue = TaskQueue()
369
- task1 = Task(1, lambda: None)
370
- task2 = Task(2, lambda: None)
371
- task3 = Task(3, lambda: None)
372
-
373
- queue.add(task1)
374
- queue.add(task3)
375
- queue.add(task2)
376
-
377
- self.assertIs(queue[0], task3)
378
- self.assertIs(queue[1], task2)
379
- self.assertIs(queue[2], task1)
380
-
381
- # Test correct task pops:
382
- self.assertIs(queue.pop(), task1)
383
-
384
- # test cancel:
385
- queue.cancel(task2)
386
- self.assertEqual(queue, [task3])
387
-
388
-
389
- class ClientServerTests(unittest.TestCase):
390
-
391
- def test_rep_server(self):
392
- class MyServer(ZMQServer):
393
- def handler(self, data):
394
- if data == 'error':
395
- raise TestError
396
- return data
397
-
398
- server = MyServer(port=None, bind_address='tcp://127.0.0.1')
399
- try:
400
- self.assertIsInstance(server.context, SecureContext)
401
- response = zmq_get(server.port, data='hello!')
402
- self.assertEqual(response, 'hello!')
403
-
404
- # Ignore the exception in the other thread:
405
- clientserver.raise_exception_in_thread = lambda *args: None
406
- try:
407
- with self.assertRaises(TestError):
408
- zmq_get(server.port, data='error')
409
- finally:
410
- clientserver.raise_exception_in_thread = raise_exception_in_thread
411
- finally:
412
- server.shutdown()
413
-
414
- def test_raw_server(self):
415
- class MyServer(ZMQServer):
416
- def handler(self, data):
417
- if data == b'error':
418
- raise TestError
419
- return data
420
-
421
- for argname in ["dtype", "type", "positional"]:
422
- if argname == 'dtype':
423
- server = MyServer(port=None, dtype='raw',
424
- bind_address='tcp://127.0.0.1')
425
- elif argname == 'type':
426
- server = MyServer(port=None, type='raw',
427
- bind_address='tcp://127.0.0.1')
428
- elif argname == 'positional':
429
- server = MyServer(None, 'raw',
430
- bind_address='tcp://127.0.0.1')
431
- try:
432
- self.assertIsInstance(server.context, SecureContext)
433
- response = zmq_get_raw(server.port, data=b'hello!')
434
- self.assertEqual(response, b'hello!')
435
-
436
- # Ignore the exception in the other thread:
437
- clientserver.raise_exception_in_thread = lambda *args: None
438
- try:
439
- self.assertIn(b'TestError', zmq_get_raw(server.port, data=b'error'))
440
- finally:
441
- clientserver.raise_exception_in_thread = \
442
- raise_exception_in_thread
443
- finally:
444
- server.shutdown()
445
-
446
- def test_pull_server(self):
447
-
448
- testcase = self
449
- got_data = threading.Event()
450
-
451
- class MyPullServer(ZMQServer):
452
- def handler(self, data):
453
- if data == 'error!':
454
- return "not None!"
455
- testcase.assertEqual(data, 'hello!')
456
- got_data.set()
457
-
458
- server = MyPullServer(port=None, bind_address='tcp://127.0.0.1',
459
- pull_only=True)
460
-
461
- # So we can catch errors raised by raise_exception_in_thread
462
-
463
- try:
464
- self.assertIsInstance(server.context, SecureContext)
465
- response = zmq_push(server.port, data='hello!')
466
- self.assertEqual(response, None)
467
- self.assertEqual(got_data.wait(timeout=1), True)
468
- got_data.clear()
469
-
470
- # Confirm you get an error when the handler returns something:
471
- got_error = threading.Event()
472
- class MockThread(object):
473
- def __init__(self, target, args):
474
- self.target = target
475
- self.args = args
476
- def start(self):
477
- try:
478
- self.target(*self.args)
479
- except ValueError:
480
- got_error.set()
481
-
482
- orig_thread = threading.Thread
483
- try:
484
- threading.Thread = MockThread
485
- response = zmq_push(server.port, data='error!')
486
- self.assertEqual(got_error.wait(timeout=1), True)
487
- finally:
488
- threading.Thread = orig_thread
489
-
490
- # Confirm the server still works:
491
- response = zmq_push(server.port, data='hello!')
492
- self.assertEqual(response, None)
493
- self.assertEqual(got_data.wait(timeout=1), True)
494
- got_data.clear()
495
- finally:
496
- server.shutdown()
497
-
498
- def test_customauth_backcompat(self):
499
- class MyCustomAuthServer(ZMQServer):
500
- def setup_auth(self, context):
501
- pass
502
-
503
- server = MyCustomAuthServer(port=None, bind_address='tcp://127.0.0.1')
504
- try:
505
- self.assertNotIsInstance(server.context, SecureContext)
506
- finally:
507
- server.shutdown()
508
-
509
- if __name__ == '__main__':
510
- pytest.main([__file__, '-v'])
1
+ import unittest
2
+ from pathlib import Path
3
+ import sys
4
+ import os
5
+ import time
6
+ import threading
7
+
8
+ import zmq
9
+ import pytest
10
+
11
+ THIS_DIR = Path(__file__).absolute().parent
12
+
13
+ # Add project root to import path
14
+ PROJECT_ROOT = THIS_DIR.parent
15
+ if PROJECT_ROOT not in [Path(s).absolute() for s in sys.path]:
16
+ sys.path.insert(0, str(PROJECT_ROOT))
17
+
18
+ import zprocess
19
+ zprocess._silent = True
20
+ from zprocess import (ZMQServer, Process, TimeoutError, RichStreamHandler, rich_print,
21
+ raise_exception_in_thread, zmq_get, zmq_push, zmq_get_raw,
22
+ ExternalBroker)
23
+ import zprocess.clientserver as clientserver
24
+ from zprocess.clientserver import _typecheck_or_convert_data
25
+ from zprocess.process_tree import _default_process_tree, EventBroker
26
+ shared_secret = _default_process_tree.shared_secret
27
+ from zprocess.security import SecureContext
28
+ from zprocess.tasks import Task, TaskQueue
29
+
30
+
31
+ class TestError(Exception):
32
+ pass
33
+
34
+
35
+ class RaiseExceptionInThreadTest(unittest.TestCase):
36
+
37
+ def setUp(self):
38
+ # Mock threading.Thread to just run a function in the main thread:
39
+ class MockThread(object):
40
+ used = False
41
+ def __init__(self, target, args):
42
+ self.target = target
43
+ self.args = args
44
+ def start(self):
45
+ MockThread.used = True
46
+ self.target(*self.args)
47
+ self.mock_thread = MockThread
48
+ self.orig_thread = threading.Thread
49
+ threading.Thread = MockThread
50
+
51
+ def test_can_raise_exception_in_thread(self):
52
+ try:
53
+ raise TestError('test')
54
+ except Exception:
55
+ exc_info = sys.exc_info()
56
+ with self.assertRaises(TestError):
57
+ raise_exception_in_thread(exc_info)
58
+ self.assertTrue(self.mock_thread.used)
59
+
60
+ def tearDown(self):
61
+ # Restore threading.Thread to what it should be
62
+ threading.Thread = self.orig_thread
63
+
64
+
65
+ class TypeCheckConvertTests(unittest.TestCase):
66
+ """test the _typecheck_or_convert_data function"""
67
+
68
+ def test_turns_None_into_empty_bytestring_raw(self):
69
+ result = _typecheck_or_convert_data(None, 'raw')
70
+ self.assertEqual(result, b'')
71
+
72
+ def test_turns_None_into_empty_bytestring_multipart(self):
73
+ result = _typecheck_or_convert_data(None, 'multipart')
74
+ self.assertEqual(result, [b''])
75
+
76
+ def test_wraps_bytestring_into_list_multipart(self):
77
+ data = b'spam'
78
+ result = _typecheck_or_convert_data(data, 'multipart')
79
+ self.assertEqual(result, [data])
80
+
81
+ def test_accepts_bytes_raw(self):
82
+ data = b'spam'
83
+ result = _typecheck_or_convert_data(data, 'raw')
84
+ self.assertEqual(result, data)
85
+
86
+ def test_accepts_list_of_bytes_multipart(self):
87
+ data = [b'spam', b'ham']
88
+ result = _typecheck_or_convert_data(data, 'multipart')
89
+ self.assertEqual(result, data)
90
+
91
+ def test_accepts_string_string(self):
92
+ data = 'spam'
93
+ result = _typecheck_or_convert_data(data, 'string')
94
+ self.assertEqual(result, data)
95
+
96
+ def test_accepts_pyobj_pyobj(self):
97
+ data = {'spam': ['ham'], 'eggs': True}
98
+ result = _typecheck_or_convert_data(data, 'pyobj')
99
+ self.assertEqual(result, data)
100
+
101
+ def test_rejects_string_raw(self):
102
+ data = 'spam'
103
+ with self.assertRaises(TypeError):
104
+ _typecheck_or_convert_data(data, 'raw')
105
+
106
+ def test_rejects_string_multipart(self):
107
+ data = [b'spam', 'ham']
108
+ with self.assertRaises(TypeError):
109
+ _typecheck_or_convert_data(data, 'multipart')
110
+
111
+ def test_rejects_pyobj_string(self):
112
+ data = {'spam': ['ham'], 'eggs': True}
113
+ with self.assertRaises(TypeError):
114
+ _typecheck_or_convert_data(data, 'string')
115
+
116
+ def test_rejects_invalid_send_type(self):
117
+ data = {'spam': ['ham'], 'eggs': True}
118
+ with self.assertRaises(ValueError):
119
+ _typecheck_or_convert_data(data, 'invalid_send_type')
120
+
121
+
122
+ class TestProcess(Process):
123
+ def run(self):
124
+ item = self.from_parent.get()
125
+ x, y = item
126
+ sys.stdout.write(repr(x))
127
+ sys.stderr.write(y)
128
+ self.to_parent.put(item)
129
+ os.system('echo hello from echo')
130
+ # Wait some time here to reduce the chance of output arriving out of order,
131
+ # which would break the tests even though we don't actually guarantee that the
132
+ # order will come out right.
133
+ time.sleep(0.2)
134
+
135
+ # And now test logging:
136
+ import logging
137
+ logger = logging.Logger('test')
138
+ logger.setLevel(logging.DEBUG)
139
+ logger.addHandler(RichStreamHandler())
140
+ logger.info('this is a log message')
141
+
142
+ # And now test rich printing
143
+ rich_print('some test text', color='#123456', bold=True, italic=False)
144
+
145
+
146
+
147
+ class ProcessClassTests(unittest.TestCase):
148
+
149
+ def setUp(self):
150
+ """Create a subprocess with output redirection to a zmq port"""
151
+ context = SecureContext.instance(shared_secret=shared_secret)
152
+ self.redirection_sock = context.socket(zmq.PULL)
153
+ redirection_port = self.redirection_sock.bind_to_random_port(
154
+ 'tcp://127.0.0.1')
155
+ self.process = TestProcess(output_redirection_port=redirection_port)
156
+
157
+ def test_process(self):
158
+ to_child, from_child = self.process.start()
159
+ # Check the child process is running:
160
+ self.assertIs(self.process.child.poll(), None)
161
+
162
+ # Send some data:
163
+ x = [('spam', ['ham']), ('eggs', True)]
164
+ y = 'über'
165
+ data = (x, y)
166
+ to_child.put(data)
167
+
168
+ # Subprocess should send back the data unmodified:
169
+ recv_data = from_child.get(timeout=1)
170
+ self.assertEqual(recv_data, data)
171
+
172
+ # Subprocess should not send any more data, expect TimeoutError:
173
+ with self.assertRaises(TimeoutError):
174
+ from_child.get(timeout=0.1)
175
+
176
+ # Check we recieved its stdout and stderr:
177
+ self.assertEqual(self.redirection_sock.poll(1000), zmq.POLLIN)
178
+ self.assertEqual(self.redirection_sock.recv_multipart(),
179
+ [b'stdout', repr(x).encode('utf8')])
180
+ self.assertEqual(self.redirection_sock.poll(1000), zmq.POLLIN)
181
+ self.assertEqual(self.redirection_sock.recv_multipart(),
182
+ [b'stderr', y.encode('utf8')])
183
+ # And the shell output:
184
+ self.assertEqual(self.redirection_sock.poll(1000), zmq.POLLIN)
185
+
186
+ self.assertEqual(
187
+ [p.strip() for p in self.redirection_sock.recv_multipart()],
188
+ [b'stdout', b'hello from echo']
189
+ )
190
+
191
+ # And the formatted logging:
192
+ self.assertEqual(self.redirection_sock.recv_multipart(),
193
+ [b'INFO', b'this is a log message\n'])
194
+
195
+ # And the formatted printing:
196
+ import ast
197
+ charfmt_repr, text = self.redirection_sock.recv_multipart()
198
+ self.assertEqual(ast.literal_eval(charfmt_repr.decode('utf8')), ('#123456', True, False))
199
+ self.assertEqual(text, b'some test text\n')
200
+
201
+ # And no more...
202
+ self.assertEqual(self.redirection_sock.poll(100), 0)
203
+
204
+ def tearDown(self):
205
+ self.process.terminate()
206
+ self.redirection_sock.close()
207
+
208
+
209
+ class HeartbeatClientTestProcess(Process):
210
+ """For testing that subprocesses are behaving correcly re. heartbeats"""
211
+ def run(self):
212
+ self.from_parent.get()
213
+ # If the parent sends a message, acquire the kill lock for 3 seconds:
214
+ with self.kill_lock:
215
+ time.sleep(3)
216
+ time.sleep(10)
217
+
218
+
219
+ class HeartbeatServerTestProcess(Process):
220
+ """For testing that parent processes are behaving correcly re. heartbeats"""
221
+ def run(self):
222
+ # We'll send heartbeats of our own, independent of the HeartbeatClient
223
+ # already running in this process:
224
+ shared_secret = self.process_tree.shared_secret
225
+ context = SecureContext.instance(shared_secret=shared_secret)
226
+ sock = context.socket(zmq.REQ)
227
+ sock.setsockopt(zmq.LINGER, 0)
228
+ server_port = self.from_parent.get()
229
+ sock.connect('tcp://127.0.0.1:%s' % server_port)
230
+ # Send a heartbeat with whatever data:
231
+ data = b'heartbeat_data'
232
+ sock.send(data)
233
+ if sock.poll(1000):
234
+ response = sock.recv()
235
+ # Tell the parent whether things were as expected:
236
+ if response == data:
237
+ self.to_parent.put(True)
238
+ time.sleep(1) # Ensure it sends before we return
239
+ return
240
+ self.to_parent.put(False)
241
+ time.sleep(1) # Ensure it sends before we return
242
+
243
+
244
+ class HeartbeatTests(unittest.TestCase):
245
+ def setUp(self):
246
+ """Create a sock for output redirection and a zmq port to mock a heartbeat
247
+ server"""
248
+ import zmq
249
+ context = SecureContext.instance(shared_secret=shared_secret)
250
+ self.heartbeat_sock = context.socket(zmq.REP)
251
+ heartbeat_port = self.heartbeat_sock.bind_to_random_port('tcp://127.0.0.1')
252
+
253
+ class mock_heartbeat_server(object):
254
+ port = heartbeat_port
255
+
256
+ self.mock_heartbeat_server = mock_heartbeat_server
257
+
258
+ def test_subproc_lives_with_heartbeats(self):
259
+ _default_process_tree.heartbeat_server = self.mock_heartbeat_server
260
+ self.process = HeartbeatClientTestProcess()
261
+ self.process.start()
262
+ for i in range(3):
263
+ # Wait for a heartbeat request:
264
+ self.assertTrue(self.heartbeat_sock.poll(3000))
265
+ # Echo it back:
266
+ self.heartbeat_sock.send(self.heartbeat_sock.recv())
267
+
268
+ def test_subproc_dies_without_heartbeats(self):
269
+ _default_process_tree.heartbeat_server = self.mock_heartbeat_server
270
+ self.process = HeartbeatClientTestProcess()
271
+ self.process.start()
272
+ # Wait for a heartbeat request:
273
+ self.assertTrue(self.heartbeat_sock.poll(3000))
274
+ # Don't respond to it
275
+ time.sleep(2)
276
+ # Process should be dead:
277
+ self.assertIsNot(self.process.child.poll(), None)
278
+
279
+ def test_subproc_dies_on_incorrect_response(self):
280
+ _default_process_tree.heartbeat_server = self.mock_heartbeat_server
281
+ self.process = HeartbeatClientTestProcess()
282
+ self.process.start()
283
+ # Wait for a heartbeat request:
284
+ self.assertTrue(self.heartbeat_sock.poll(3000))
285
+ # Echo it back wrongly:
286
+ self.heartbeat_sock.send(self.heartbeat_sock.recv() + b'wrong')
287
+ time.sleep(1)
288
+ # Process should be dead:
289
+ self.assertIsNot(self.process.child.poll(), None)
290
+
291
+ def test_subproc_survives_until_kill_lock_released(self):
292
+ _default_process_tree.heartbeat_server = self.mock_heartbeat_server
293
+ self.process = HeartbeatClientTestProcess()
294
+ to_child, from_child = self.process.start()
295
+ # Wait for a heartbeat request:
296
+ self.assertTrue(self.heartbeat_sock.poll(3000))
297
+ # Tell child to acquire kill lock for 3 sec:
298
+ to_child.put(None)
299
+ # Don't respond to the heartbeat, process should still be alive 2 sec later
300
+ time.sleep(2)
301
+ # Process should be alive:
302
+ self.assertIs(self.process.child.poll(), None)
303
+ # After kill lock released, child should be terminated:
304
+ time.sleep(2)
305
+ # Process should be dead:
306
+ self.assertIsNot(self.process.child.poll(), None)
307
+
308
+ def test_parent_correctly_responds_to_heartbeats(self):
309
+ # No mock server this time, we're testing the real one:
310
+ _default_process_tree.heartbeat_server = None
311
+ self.process = HeartbeatServerTestProcess()
312
+ to_child, from_child = self.process.start()
313
+ to_child.put(_default_process_tree.heartbeat_server.port)
314
+ self.assertTrue(from_child.sock.poll(1000))
315
+ self.assertTrue(from_child.get())
316
+
317
+ def tearDown(self):
318
+ self.heartbeat_sock.close()
319
+ try:
320
+ self.process.terminate()
321
+ except Exception:
322
+ pass # already dead
323
+
324
+ class TestEventProcess(Process):
325
+ def run(self):
326
+ event = self.process_tree.event('hello', role='post')
327
+ event.post('1', data=u'boo')
328
+ time.sleep(0.5)
329
+
330
+ class TestExternalEventProcess(Process):
331
+ def run(self, broker_details):
332
+ event = self.process_tree.event('hello', role='post', external_broker=broker_details)
333
+ event.post('1', data=u'boo')
334
+ time.sleep(0.5)
335
+
336
+
337
+
338
+ class EventTests(unittest.TestCase):
339
+ def test_events(self):
340
+ proc = TestEventProcess()
341
+ event = _default_process_tree.event('hello', role='wait')
342
+ proc.start()
343
+ try:
344
+ data = event.wait('1', timeout=1)
345
+ self.assertEqual(data, u'boo')
346
+ finally:
347
+ proc.terminate()
348
+
349
+ def test_external_broker(self):
350
+ broker = EventBroker(bind_address='tcp://127.0.0.1')
351
+ broker_details = ExternalBroker('localhost', broker.in_port, broker.out_port)
352
+ proc = TestExternalEventProcess()
353
+ event = _default_process_tree.event('hello', role='wait', external_broker=broker_details)
354
+ proc.start(broker_details)
355
+ try:
356
+ data = event.wait('1', timeout=1)
357
+ self.assertEqual(data, u'boo')
358
+ finally:
359
+ proc.terminate()
360
+
361
+
362
+ class TaskTests(unittest.TestCase):
363
+ def test_cant_call_task_twice(self):
364
+ task = Task(1, lambda: None)
365
+ task()
366
+ with self.assertRaises(RuntimeError):
367
+ task()
368
+
369
+ def test_queue(self):
370
+ # Test insert order:
371
+ queue = TaskQueue()
372
+ task1 = Task(1, lambda: None)
373
+ task2 = Task(2, lambda: None)
374
+ task3 = Task(3, lambda: None)
375
+
376
+ queue.add(task1)
377
+ queue.add(task3)
378
+ queue.add(task2)
379
+
380
+ self.assertIs(queue[0], task3)
381
+ self.assertIs(queue[1], task2)
382
+ self.assertIs(queue[2], task1)
383
+
384
+ # Test correct task pops:
385
+ self.assertIs(queue.pop(), task1)
386
+
387
+ # test cancel:
388
+ queue.cancel(task2)
389
+ self.assertEqual(queue, [task3])
390
+
391
+
392
+ class ClientServerTests(unittest.TestCase):
393
+
394
+ def test_rep_server(self):
395
+ class MyServer(ZMQServer):
396
+ def handler(self, data):
397
+ if data == 'error':
398
+ raise TestError
399
+ return data
400
+
401
+ server = MyServer(port=None, bind_address='tcp://127.0.0.1')
402
+ try:
403
+ self.assertIsInstance(server.context, SecureContext)
404
+ response = zmq_get(server.port, data='hello!')
405
+ self.assertEqual(response, 'hello!')
406
+
407
+ # Ignore the exception in the other thread:
408
+ clientserver.raise_exception_in_thread = lambda *args: None
409
+ try:
410
+ with self.assertRaises(TestError):
411
+ zmq_get(server.port, data='error')
412
+ finally:
413
+ clientserver.raise_exception_in_thread = raise_exception_in_thread
414
+ finally:
415
+ server.shutdown()
416
+
417
+ def test_raw_server(self):
418
+ class MyServer(ZMQServer):
419
+ def handler(self, data):
420
+ if data == b'error':
421
+ raise TestError
422
+ return data
423
+
424
+ for argname in ["dtype", "type", "positional"]:
425
+ if argname == 'dtype':
426
+ server = MyServer(port=None, dtype='raw',
427
+ bind_address='tcp://127.0.0.1')
428
+ elif argname == 'type':
429
+ server = MyServer(port=None, type='raw',
430
+ bind_address='tcp://127.0.0.1')
431
+ elif argname == 'positional':
432
+ server = MyServer(None, 'raw',
433
+ bind_address='tcp://127.0.0.1')
434
+ try:
435
+ self.assertIsInstance(server.context, SecureContext)
436
+ response = zmq_get_raw(server.port, data=b'hello!')
437
+ self.assertEqual(response, b'hello!')
438
+
439
+ # Ignore the exception in the other thread:
440
+ clientserver.raise_exception_in_thread = lambda *args: None
441
+ try:
442
+ self.assertIn(b'TestError', zmq_get_raw(server.port, data=b'error'))
443
+ finally:
444
+ clientserver.raise_exception_in_thread = \
445
+ raise_exception_in_thread
446
+ finally:
447
+ server.shutdown()
448
+
449
+ def test_pull_server(self):
450
+
451
+ testcase = self
452
+ got_data = threading.Event()
453
+
454
+ class MyPullServer(ZMQServer):
455
+ def handler(self, data):
456
+ if data == 'error!':
457
+ return "not None!"
458
+ testcase.assertEqual(data, 'hello!')
459
+ got_data.set()
460
+
461
+ server = MyPullServer(port=None, bind_address='tcp://127.0.0.1',
462
+ pull_only=True)
463
+
464
+ # So we can catch errors raised by raise_exception_in_thread
465
+
466
+ try:
467
+ self.assertIsInstance(server.context, SecureContext)
468
+ response = zmq_push(server.port, data='hello!')
469
+ self.assertEqual(response, None)
470
+ self.assertEqual(got_data.wait(timeout=1), True)
471
+ got_data.clear()
472
+
473
+ # Confirm you get an error when the handler returns something:
474
+ got_error = threading.Event()
475
+ class MockThread(object):
476
+ def __init__(self, target, args):
477
+ self.target = target
478
+ self.args = args
479
+ def start(self):
480
+ try:
481
+ self.target(*self.args)
482
+ except ValueError:
483
+ got_error.set()
484
+
485
+ orig_thread = threading.Thread
486
+ try:
487
+ threading.Thread = MockThread
488
+ response = zmq_push(server.port, data='error!')
489
+ self.assertEqual(got_error.wait(timeout=1), True)
490
+ finally:
491
+ threading.Thread = orig_thread
492
+
493
+ # Confirm the server still works:
494
+ response = zmq_push(server.port, data='hello!')
495
+ self.assertEqual(response, None)
496
+ self.assertEqual(got_data.wait(timeout=1), True)
497
+ got_data.clear()
498
+ finally:
499
+ server.shutdown()
500
+
501
+ def test_customauth_backcompat(self):
502
+ class MyCustomAuthServer(ZMQServer):
503
+ def setup_auth(self, context):
504
+ pass
505
+
506
+ server = MyCustomAuthServer(port=None, bind_address='tcp://127.0.0.1')
507
+ try:
508
+ self.assertNotIsInstance(server.context, SecureContext)
509
+ finally:
510
+ server.shutdown()
511
+
512
+ if __name__ == '__main__':
513
+ pytest.main([__file__, '-v'])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: zprocess
3
- Version: 2.26.0
3
+ Version: 2.26.1
4
4
  Summary: A set of utilities for multiprocessing using zeromq
5
5
  Author-email: Christopher Billington <chrisjbillington@gmail.com>
6
6
  License: Copyright (c) 2013, Christopher Billington
@@ -34,7 +34,7 @@ Project-URL: Tracker, https://github.com/chrisjbillington/zprocess/issues
34
34
  Keywords: multiprocessing,IPC,zeromq
35
35
  Classifier: License :: OSI Approved :: BSD License
36
36
  Classifier: Programming Language :: Python :: 3 :: Only
37
- Requires-Python: >=3.9
37
+ Requires-Python: >=3.8
38
38
  Description-Content-Type: text/markdown
39
39
  License-File: LICENSE.txt
40
40
  Requires-Dist: setuptools_scm
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes