portal 3.2.0__tar.gz → 3.2.3__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 (35) hide show
  1. {portal-3.2.0/portal.egg-info → portal-3.2.3}/PKG-INFO +1 -1
  2. {portal-3.2.0 → portal-3.2.3}/portal/__init__.py +1 -1
  3. {portal-3.2.0 → portal-3.2.3}/portal/client_socket.py +13 -12
  4. {portal-3.2.0 → portal-3.2.3}/portal/contextlib.py +7 -1
  5. {portal-3.2.0 → portal-3.2.3}/portal/process.py +6 -5
  6. {portal-3.2.0 → portal-3.2.3}/portal/server.py +3 -0
  7. {portal-3.2.0 → portal-3.2.3}/portal/thread.py +0 -1
  8. {portal-3.2.0 → portal-3.2.3}/portal/utils.py +8 -2
  9. {portal-3.2.0 → portal-3.2.3/portal.egg-info}/PKG-INFO +1 -1
  10. {portal-3.2.0 → portal-3.2.3}/tests/test_client.py +17 -0
  11. {portal-3.2.0 → portal-3.2.3}/tests/test_process.py +22 -5
  12. {portal-3.2.0 → portal-3.2.3}/tests/test_server.py +25 -4
  13. {portal-3.2.0 → portal-3.2.3}/tests/test_thread.py +1 -1
  14. {portal-3.2.0 → portal-3.2.3}/LICENSE +0 -0
  15. {portal-3.2.0 → portal-3.2.3}/MANIFEST.in +0 -0
  16. {portal-3.2.0 → portal-3.2.3}/README.md +0 -0
  17. {portal-3.2.0 → portal-3.2.3}/portal/batching.py +0 -0
  18. {portal-3.2.0 → portal-3.2.3}/portal/buffers.py +0 -0
  19. {portal-3.2.0 → portal-3.2.3}/portal/client.py +0 -0
  20. {portal-3.2.0 → portal-3.2.3}/portal/packlib.py +0 -0
  21. {portal-3.2.0 → portal-3.2.3}/portal/poollib.py +0 -0
  22. {portal-3.2.0 → portal-3.2.3}/portal/server_socket.py +0 -0
  23. {portal-3.2.0 → portal-3.2.3}/portal/sharray.py +0 -0
  24. {portal-3.2.0 → portal-3.2.3}/portal.egg-info/SOURCES.txt +0 -0
  25. {portal-3.2.0 → portal-3.2.3}/portal.egg-info/dependency_links.txt +0 -0
  26. {portal-3.2.0 → portal-3.2.3}/portal.egg-info/requires.txt +0 -0
  27. {portal-3.2.0 → portal-3.2.3}/portal.egg-info/top_level.txt +0 -0
  28. {portal-3.2.0 → portal-3.2.3}/pyproject.toml +0 -0
  29. {portal-3.2.0 → portal-3.2.3}/requirements.txt +0 -0
  30. {portal-3.2.0 → portal-3.2.3}/setup.cfg +0 -0
  31. {portal-3.2.0 → portal-3.2.3}/setup.py +0 -0
  32. {portal-3.2.0 → portal-3.2.3}/tests/test_batching.py +0 -0
  33. {portal-3.2.0 → portal-3.2.3}/tests/test_errfile.py +0 -0
  34. {portal-3.2.0 → portal-3.2.3}/tests/test_pack.py +0 -0
  35. {portal-3.2.0 → portal-3.2.3}/tests/test_socket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portal
3
- Version: 3.2.0
3
+ Version: 3.2.3
4
4
  Summary: Fast and reliable distributed systems in Python
5
5
  Home-page: http://github.com/danijar/portal
6
6
  Author: Danijar Hafner
@@ -1,4 +1,4 @@
1
- __version__ = '3.2.0'
1
+ __version__ = '3.2.3'
2
2
 
3
3
  import multiprocessing as mp
4
4
  try:
@@ -34,11 +34,11 @@ class Options:
34
34
  class ClientSocket:
35
35
 
36
36
  def __init__(self, host, port=None, name='Client', start=True, **kwargs):
37
- assert (port or ':' in host) and '://' not in host, host
37
+ assert port or ':' in host, (host, port)
38
+ assert '://' not in host, (host, port)
38
39
  if port is None:
39
40
  host, port = host.rsplit(':', 1)
40
- port = int(port)
41
- assert host, host
41
+ assert host and port, (host, port)
42
42
  self.addr = (host, port)
43
43
  self.name = name
44
44
  self.options = Options(**{**contextlib.context.clientkw, **kwargs})
@@ -179,16 +179,19 @@ class ClientSocket:
179
179
  sock.close()
180
180
 
181
181
  def _connect(self):
182
- host, port = self.addr
183
- self._log(f'Connecting to {host}:{port}')
182
+ self._log(f'Connecting to {self.addr[0]}:{self.addr[1]}')
184
183
  once = True
185
184
  while self.running:
186
- sock, addr = self._create()
185
+ # We need to resolve the address regularly.
186
+ host, port = self.addr
187
+ if contextlib.context.resolver:
188
+ host, port = contextlib.context.resolver((host, port))
189
+ assert isinstance(host, str), (host, port)
190
+ assert isinstance(port, int), (host, port)
191
+ addr = (host, port, 0, 0) if self.options.ipv6 else (host, port)
192
+ sock = self._create()
187
193
  error = None
188
194
  try:
189
- # We need to resolve the address regularly.
190
- if contextlib.context.resolver:
191
- addr = contextlib.context.resolver(addr)
192
195
  sock.settimeout(10)
193
196
  sock.connect(addr)
194
197
  sock.settimeout(0)
@@ -210,10 +213,8 @@ class ClientSocket:
210
213
  def _create(self):
211
214
  if self.options.ipv6:
212
215
  sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
213
- addr = (*self.addr, 0, 0)
214
216
  else:
215
217
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
216
- addr = self.addr
217
218
  # sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
218
219
  after = self.options.keepalive_after
219
220
  every = self.options.keepalive_every
@@ -231,7 +232,7 @@ class ClientSocket:
231
232
  sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPALIVE, every)
232
233
  if sys.platform == 'win32':
233
234
  sock.ioctl(socket.SIO_KEEPALIVE_VALS, (1, after * 1000, every * 1000))
234
- return sock, addr
235
+ return sock
235
236
 
236
237
  def _log(self, *args):
237
238
  if not self.options.logging:
@@ -4,6 +4,7 @@ import pathlib
4
4
  import sys
5
5
  import threading
6
6
  import traceback
7
+ import warnings
7
8
 
8
9
  import cloudpickle
9
10
  import psutil
@@ -143,7 +144,12 @@ class Context:
143
144
  return
144
145
  if hasattr(worker, 'thread'):
145
146
  assert current != worker.thread
146
- current.children.append(worker)
147
+ try:
148
+ current.children.append(worker)
149
+ except AttributeError:
150
+ warnings.warn(
151
+ 'Using Portal from plain Python threads is discouraged because ' +
152
+ 'they can cause hangs during shutdown.')
147
153
 
148
154
  def children(self, thread):
149
155
  current = thread or threading.current_thread()
@@ -1,6 +1,5 @@
1
1
  import atexit
2
2
  import errno
3
- import os
4
3
  import traceback
5
4
 
6
5
  import cloudpickle
@@ -37,8 +36,10 @@ class Process:
37
36
  name = name or getattr(fn, '__name__', 'process')
38
37
  fn = cloudpickle.dumps(fn)
39
38
  options = contextlib.context.options()
39
+ self.ready = contextlib.context.mp.Barrier(2)
40
40
  self.process = contextlib.context.mp.Process(
41
- target=self._wrapper, name=name, args=(options, name, fn, args))
41
+ target=self._wrapper, name=name,
42
+ args=(options, self.ready, name, fn, args))
42
43
  self.started = False
43
44
  self.killed = False
44
45
  self.thepid = None
@@ -73,6 +74,7 @@ class Process:
73
74
  assert not self.started
74
75
  self.started = True
75
76
  self.process.start()
77
+ self.ready.wait()
76
78
  self.thepid = self.process.pid
77
79
  assert self.thepid is not None
78
80
  return self
@@ -83,8 +85,6 @@ class Process:
83
85
  return self
84
86
 
85
87
  def kill(self, timeout=1):
86
- # Cannot early exit if process is not running, because it may just be
87
- # starting up.
88
88
  assert self.started
89
89
  try:
90
90
  children = list(psutil.Process(self.pid).children(recursive=True))
@@ -108,9 +108,10 @@ class Process:
108
108
  return 'Process(' + ', '.join(attrs) + ')'
109
109
 
110
110
  @staticmethod
111
- def _wrapper(options, name, fn, args):
111
+ def _wrapper(options, ready, name, fn, args):
112
112
  exitcode = 0
113
113
  try:
114
+ ready.wait()
114
115
  contextlib.setup(**options)
115
116
  fn = cloudpickle.loads(fn)
116
117
  exitcode = fn(*args)
@@ -148,4 +148,7 @@ class Server:
148
148
  data = message.encode('utf-8')
149
149
  self.socket.send(addr, reqnum, status, data)
150
150
  if self.errors:
151
+ # Wait until the error is delivered to the client and then raise.
152
+ self.socket.shutdown()
153
+ self.socket.close()
151
154
  raise RuntimeError(message)
@@ -21,7 +21,6 @@ class Thread:
21
21
  """
22
22
 
23
23
  def __init__(self, fn, *args, name=None, start=False):
24
- global TIDS
25
24
  self.fn = fn
26
25
  self.excode = None
27
26
  name = name or getattr(fn, '__name__', 'thread')
@@ -8,6 +8,8 @@ import time
8
8
 
9
9
  import psutil
10
10
 
11
+ from . import contextlib
12
+
11
13
 
12
14
  def run(workers, duration=None):
13
15
  [None if x.started else x.start() for x in workers]
@@ -96,9 +98,13 @@ def free_port():
96
98
  # Return a port that is currently free. This function is not thread or
97
99
  # process safe, because there is no way to guarantee that the port will still
98
100
  # be free at the time it will be used.
99
- sock = socket.socket()
101
+ if contextlib.context.serverkw.get('ipv6', False):
102
+ family, addr = socket.AF_INET6, ('localhost', 0, 0, 0)
103
+ else:
104
+ family, addr = socket.AF_INET, ('localhost', 0)
105
+ sock = socket.socket(family, socket.SOCK_STREAM)
100
106
  sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
101
- sock.bind(('localhost', 0))
107
+ sock.bind(addr)
102
108
  port = sock.getsockname()[1]
103
109
  sock.close()
104
110
  return port
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portal
3
- Version: 3.2.0
3
+ Version: 3.2.3
4
4
  Summary: Fast and reliable distributed systems in Python
5
5
  Home-page: http://github.com/danijar/portal
6
6
  Author: Danijar Hafner
@@ -321,3 +321,20 @@ class TestClient:
321
321
  portal.Thread(server),
322
322
  portal.Thread(client),
323
323
  ])
324
+
325
+ def test_resolver(self):
326
+ portnum = portal.free_port()
327
+
328
+ def client(portnum):
329
+ def resolver(host, portstr):
330
+ assert portstr == 'name'
331
+ return host, portnum
332
+ portal.setup(resolver=resolver)
333
+ client = portal.Client('localhost:name')
334
+ assert client.fn(42).result() == 42
335
+
336
+ server = portal.Server(portnum)
337
+ server.bind('fn', lambda x: x)
338
+ server.start(block=False)
339
+ portal.Process(client, portnum, start=True).join()
340
+ server.close()
@@ -16,35 +16,43 @@ class TestProcess:
16
16
  assert worker.exitcode == 42
17
17
 
18
18
  def test_error(self):
19
+
19
20
  def fn():
20
21
  raise KeyError('foo')
22
+
21
23
  worker = portal.Process(fn, start=True)
22
24
  worker.join()
23
25
  assert not worker.running
24
26
  assert worker.exitcode == 1
25
27
 
26
28
  def test_error_with_children(self):
29
+
27
30
  def hang():
28
31
  while True:
29
32
  time.sleep(0.1)
33
+
30
34
  def fn():
31
35
  portal.Process(hang, start=True)
32
36
  portal.Thread(hang, start=True)
33
37
  time.sleep(0.1)
34
38
  raise KeyError('foo')
39
+
35
40
  worker = portal.Process(fn, start=True)
36
41
  worker.join()
37
42
  assert not worker.running
38
43
  assert worker.exitcode == 1
39
44
 
40
- def test_kill(self):
45
+ @pytest.mark.parametrize('repeat', range(5))
46
+ def test_kill_basic(self, repeat):
47
+
41
48
  def fn():
42
49
  while True:
43
50
  time.sleep(0.1)
51
+
44
52
  worker = portal.Process(fn, start=True)
45
53
  worker.kill()
46
54
  assert not worker.running
47
- assert worker.exitcode < 0
55
+ assert abs(worker.exitcode) >= 1
48
56
 
49
57
  @pytest.mark.parametrize('repeat', range(5))
50
58
  def test_kill_with_subproc(self, repeat):
@@ -52,14 +60,18 @@ class TestProcess:
52
60
  queue = portal.context.mp.Queue()
53
61
 
54
62
  def outer(ready, queue):
55
- queue.put(os.getpid())
56
63
  portal.Process(inner, ready, queue, start=True)
64
+ queue.put(os.getpid())
65
+ queue.close()
66
+ queue.join_thread()
57
67
  ready.wait()
58
68
  while True:
59
69
  time.sleep(0.1)
60
70
 
61
71
  def inner(ready, queue):
62
72
  queue.put(os.getpid())
73
+ queue.close()
74
+ queue.join_thread()
63
75
  ready.wait()
64
76
  while True:
65
77
  time.sleep(0.1)
@@ -68,7 +80,7 @@ class TestProcess:
68
80
  ready.wait()
69
81
  worker.kill()
70
82
  assert not worker.running
71
- assert worker.exitcode < 0
83
+ assert abs(worker.exitcode) >= 1
72
84
  assert not portal.proc_alive(queue.get())
73
85
  assert not portal.proc_alive(queue.get())
74
86
 
@@ -87,20 +99,25 @@ class TestProcess:
87
99
  ready.wait()
88
100
  worker.kill()
89
101
  assert not worker.running
90
- assert worker.exitcode < 0
102
+ assert abs(worker.exitcode) >= 1
91
103
 
92
104
  def test_initfn(self):
105
+
93
106
  def init():
94
107
  portal.foo = 42
108
+
95
109
  portal.initfn(init)
96
110
  ready = portal.context.mp.Event()
97
111
  assert portal.foo == 42
112
+
98
113
  def outer(ready):
99
114
  assert portal.foo == 42
100
115
  portal.Process(inner, ready, start=True).join()
116
+
101
117
  def inner(ready):
102
118
  assert portal.foo == 42
103
119
  ready.set()
120
+
104
121
  portal.Process(outer, ready, start=True).join()
105
122
  ready.wait()
106
123
  assert ready.is_set()
@@ -91,6 +91,27 @@ class TestServer:
91
91
  def test_server_errors(self, Server):
92
92
  port = portal.free_port()
93
93
 
94
+ server = Server(port, errors=False)
95
+ def fn(x):
96
+ if x == 2:
97
+ raise ValueError(x)
98
+ return x
99
+ server.bind('fn', fn)
100
+ server.start(block=False)
101
+
102
+ client = portal.Client('localhost', port)
103
+ assert client.fn(1).result() == 1
104
+ with pytest.raises(RuntimeError):
105
+ client.fn(2).result()
106
+ assert client.fn(3).result() == 3
107
+
108
+ client.close()
109
+ server.close()
110
+
111
+ @pytest.mark.parametrize('Server', SERVERS)
112
+ def test_server_errors_raise(self, Server):
113
+ port = portal.free_port()
114
+
94
115
  def server(port):
95
116
  server = Server(port, errors=True)
96
117
  def fn(x):
@@ -108,8 +129,8 @@ class TestServer:
108
129
  client = portal.Client('localhost', port)
109
130
  assert client.fn(1).result() == 1
110
131
  assert server.running
111
- with pytest.raises(RuntimeError):
112
- client.fn(2).result()
132
+ with pytest.raises((RuntimeError, TimeoutError)):
133
+ client.fn(2).result(timeout=3)
113
134
 
114
135
  client.close()
115
136
  server.join()
@@ -178,7 +199,7 @@ class TestServer:
178
199
  def test_shared_pool(self, repeat, Server):
179
200
 
180
201
  def slow(x):
181
- time.sleep(0.2)
202
+ time.sleep(0.5)
182
203
  return x
183
204
 
184
205
  def fast(x):
@@ -205,7 +226,7 @@ class TestServer:
205
226
  def test_separate_pools(self, repeat, Server):
206
227
 
207
228
  def slow(x):
208
- time.sleep(0.1)
229
+ time.sleep(0.5)
209
230
  return x
210
231
 
211
232
  def fast(x):
@@ -91,4 +91,4 @@ class TestThread:
91
91
  assert not worker.running
92
92
  assert not proc[0].running
93
93
  assert worker.exitcode == 2
94
- assert proc[0].exitcode < 0
94
+ assert abs(proc[0].exitcode) >= 1
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes