appose 0.4.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

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.
appose/python_worker.py CHANGED
@@ -43,7 +43,7 @@ import sys
43
43
  import traceback
44
44
  from threading import Thread
45
45
  from time import sleep
46
- from typing import Any, Dict, Optional
46
+ from typing import Any
47
47
 
48
48
  # NB: Avoid relative imports so that this script can be run standalone.
49
49
  from appose.service import RequestType, ResponseType
@@ -51,19 +51,23 @@ from appose.types import Args, _set_worker, decode, encode
51
51
 
52
52
 
53
53
  class Task:
54
- def __init__(self, uuid: str) -> None:
55
- self.uuid = uuid
54
+ def __init__(self, uuid: str, script: str, inputs: Args | None = None) -> None:
55
+ self._uuid = uuid
56
+ self._script = script
57
+ self._inputs = inputs
58
+ self._finished = False
59
+ self._thread = None
60
+
61
+ # Public-facing fields for use within the task script.
56
62
  self.outputs = {}
57
- self.finished = False
58
63
  self.cancel_requested = False
59
- self.thread = None # Initialize thread attribute
60
64
 
61
65
  def update(
62
66
  self,
63
- message: Optional[str] = None,
64
- current: Optional[int] = None,
65
- maximum: Optional[int] = None,
66
- info: Optional[Dict[str, Any]] = None,
67
+ message: str | None = None,
68
+ current: int | None = None,
69
+ maximum: int | None = None,
70
+ info: dict[str, Any] | None = None,
67
71
  ) -> None:
68
72
  args = {}
69
73
  if message is not None:
@@ -84,16 +88,16 @@ class Task:
84
88
  def cancel(self) -> None:
85
89
  self._respond(ResponseType.CANCELATION, None)
86
90
 
87
- def fail(self, error: Optional[str] = None) -> None:
91
+ def fail(self, error: str | None = None) -> None:
88
92
  args = None if error is None else {"error": error}
89
93
  self._respond(ResponseType.FAILURE, args)
90
94
 
91
- def _start(self, script: str, inputs: Optional[Args]) -> None:
92
- def execute_script():
95
+ def _run(self) -> None:
96
+ try:
93
97
  # Populate script bindings.
94
98
  binding = {"task": self}
95
- if inputs is not None:
96
- binding.update(inputs)
99
+ if self._inputs is not None:
100
+ binding.update(self._inputs)
97
101
 
98
102
  # Inform the calling process that the script is launching.
99
103
  self._report_launch()
@@ -101,37 +105,32 @@ class Task:
101
105
  # Execute the script.
102
106
  # result = exec(script, locals=binding)
103
107
  result = None
104
- try:
105
- # NB: Execute the block, except for the last statement,
106
- # which we evaluate instead to get its return value.
107
- # Credit: https://stackoverflow.com/a/39381428/1207769
108
-
109
- block = ast.parse(script, mode="exec")
110
- last = None
111
- if (
112
- len(block.body) > 0
113
- and hasattr(block.body[-1], "value")
114
- and not isinstance(block.body[-1], ast.Assign)
115
- ):
116
- # Last statement of the script looks like an expression. Evaluate!
117
- last = ast.Expression(block.body.pop().value)
118
-
119
- # NB: When `exec` gets two separate objects as *globals* and
120
- # *locals*, the code will be executed as if it were embedded in
121
- # a class definition. This means functions and classes defined
122
- # in the executed code will not be able to access variables
123
- # assigned at the top level, because the "top level" variables
124
- # are treated as class variables in a class definition.
125
- # See: https://docs.python.org/3/library/functions.html#exec
126
- _globals = binding
127
- exec(compile(block, "<string>", mode="exec"), _globals, binding)
128
- if last is not None:
129
- result = eval(
130
- compile(last, "<string>", mode="eval"), _globals, binding
131
- )
132
- except Exception:
133
- self.fail(traceback.format_exc())
134
- return
108
+
109
+ # NB: Execute the block, except for the last statement,
110
+ # which we evaluate instead to get its return value.
111
+ # Credit: https://stackoverflow.com/a/39381428/1207769
112
+
113
+ block = ast.parse(self._script, mode="exec")
114
+ last = None
115
+ if (
116
+ len(block.body) > 0
117
+ and hasattr(block.body[-1], "value")
118
+ and not isinstance(block.body[-1], ast.Assign)
119
+ ):
120
+ # Last statement of the script looks like an expression. Evaluate!
121
+ last = ast.Expression(block.body.pop().value)
122
+
123
+ # NB: When `exec` gets two separate objects as *globals* and
124
+ # *locals*, the code will be executed as if it were embedded in
125
+ # a class definition. This means functions and classes defined
126
+ # in the executed code will not be able to access variables
127
+ # assigned at the top level, because the "top level" variables
128
+ # are treated as class variables in a class definition.
129
+ # See: https://docs.python.org/3/library/functions.html#exec
130
+ _globals = binding
131
+ exec(compile(block, "<string>", mode="exec"), _globals, binding)
132
+ if last is not None:
133
+ result = eval(compile(last, "<string>", mode="eval"), _globals, binding)
135
134
 
136
135
  # Report the results to the Appose calling process.
137
136
  if isinstance(result, dict):
@@ -140,27 +139,9 @@ class Task:
140
139
  elif result is not None:
141
140
  # Script produced a non-dict; add it alone to the outputs.
142
141
  self.outputs["result"] = result
143
-
144
142
  self._report_completion()
145
-
146
- # HACK: Pre-load toplevel import statements before running the script
147
- # as a whole on its own Thread. Why? Because on Windows, some imports
148
- # (e.g. numpy) may lead to hangs if loaded from a separate thread.
149
- # See https://github.com/apposed/appose/issues/13.
150
- block = ast.parse(script, mode="exec")
151
- import_nodes = [
152
- node
153
- for node in block.body
154
- if isinstance(node, (ast.Import, ast.ImportFrom))
155
- ]
156
- import_block = ast.Module(body=import_nodes, type_ignores=[])
157
- compiled_imports = compile(import_block, filename="<imports>", mode="exec")
158
- exec(compiled_imports, globals())
159
-
160
- # Create a thread and save a reference to it, in case its script
161
- # ends up killing the thread. This happens e.g. if it calls sys.exit.
162
- self.thread = Thread(target=execute_script, name=f"Appose-{self.uuid}")
163
- self.thread.start()
143
+ except Exception:
144
+ self.fail(traceback.format_exc())
164
145
 
165
146
  def _report_launch(self) -> None:
166
147
  self._respond(ResponseType.LAUNCH, None)
@@ -169,20 +150,20 @@ class Task:
169
150
  args = None if self.outputs is None else {"outputs": self.outputs}
170
151
  self._respond(ResponseType.COMPLETION, args)
171
152
 
172
- def _respond(self, response_type: ResponseType, args: Optional[Args]) -> None:
153
+ def _respond(self, response_type: ResponseType, args: Args | None) -> None:
173
154
  already_terminated = False
174
155
  if response_type.is_terminal():
175
- if self.finished:
156
+ if self._finished:
176
157
  # This is not the first terminal response. Let's
177
158
  # remember, in case an exception is generated below,
178
159
  # so that we can avoid infinite recursion loops.
179
160
  already_terminated = True
180
- self.finished = True
161
+ self._finished = True
181
162
 
182
163
  response = {}
183
164
  if args is not None:
184
165
  response.update(args)
185
- response.update({"task": self.uuid, "responseType": response_type.value})
166
+ response.update({"task": self._uuid, "responseType": response_type.value})
186
167
  # NB: Flush is necessary to ensure service receives the data!
187
168
  try:
188
169
  print(encode(response), flush=True)
@@ -198,57 +179,86 @@ class Task:
198
179
  self.fail(traceback.format_exc())
199
180
 
200
181
 
201
- def main() -> None:
202
- _set_worker(True)
182
+ class Worker:
203
183
 
204
- tasks = {}
205
- running = True
184
+ def __init__(self):
185
+ self.tasks = {}
186
+ self.queue: list[Task] = []
187
+ self.running = True
206
188
 
207
- def cleanup_threads():
208
- while running:
189
+ # Flag this process as a worker, not a service.
190
+ _set_worker(True)
191
+
192
+ Thread(target=self._process_input, name="Appose-Receiver").start()
193
+ Thread(target=self._cleanup_threads, name="Appose-Janitor").start()
194
+
195
+ def run(self) -> None:
196
+ """
197
+ Processes tasks from the task queue.
198
+ """
199
+ while self.running:
200
+ if len(self.queue) == 0:
201
+ # Nothing queued, so wait a bit.
202
+ sleep(0.05)
203
+ continue
204
+ task = self.queue.pop()
205
+ task._run()
206
+
207
+ def _process_input(self) -> None:
208
+ while True:
209
+ try:
210
+ line = input().strip()
211
+ except EOFError:
212
+ line = None
213
+ if not line:
214
+ self.running = False
215
+ return
216
+
217
+ request = decode(line)
218
+ uuid = request.get("task")
219
+ request_type = request.get("requestType")
220
+
221
+ match RequestType(request_type):
222
+ case RequestType.EXECUTE:
223
+ script = request.get("script")
224
+ inputs = request.get("inputs")
225
+ queue = request.get("queue")
226
+ task = Task(uuid, script, inputs)
227
+ self.tasks[uuid] = task
228
+ if queue == "main":
229
+ # Add the task to the main thread queue.
230
+ self.queue.append(task)
231
+ else:
232
+ # Create a thread and save a reference to it, in case its script
233
+ # kills the thread. This happens e.g. if it calls sys.exit.
234
+ task._thread = Thread(target=task._run, name=f"Appose-{uuid}")
235
+ task._thread.start()
236
+
237
+ case RequestType.CANCEL:
238
+ task = self.tasks.get(uuid)
239
+ if task is None:
240
+ print(f"No such task: {uuid}", file=sys.stderr)
241
+ continue
242
+ task.cancel_requested = True
243
+
244
+ def _cleanup_threads(self) -> None:
245
+ while self.running:
209
246
  sleep(0.05)
210
247
  dead = {
211
248
  uuid: task
212
- for uuid, task in tasks.items()
213
- if task.thread is not None and not task.thread.is_alive()
249
+ for uuid, task in self.tasks.items()
250
+ if task._thread is not None and not task._thread.is_alive()
214
251
  }
215
252
  for uuid, task in dead.items():
216
- tasks.pop(uuid)
217
- if not task.finished:
253
+ self.tasks.pop(uuid)
254
+ if not task._finished:
218
255
  # The task died before reporting a terminal status.
219
256
  # We report this situation as failure by thread death.
220
257
  task.fail("thread death")
221
258
 
222
- Thread(target=cleanup_threads, name="Appose-Janitor").start()
223
259
 
224
- while True:
225
- try:
226
- line = input().strip()
227
- except EOFError:
228
- break
229
- if not line:
230
- break
231
-
232
- request = decode(line)
233
- uuid = request.get("task")
234
- request_type = request.get("requestType")
235
-
236
- match RequestType(request_type):
237
- case RequestType.EXECUTE:
238
- script = request.get("script")
239
- inputs = request.get("inputs")
240
- task = Task(uuid)
241
- tasks[uuid] = task
242
- task._start(script, inputs)
243
-
244
- case RequestType.CANCEL:
245
- task = tasks.get(uuid)
246
- if task is None:
247
- print(f"No such task: {uuid}", file=sys.stderr)
248
- continue
249
- task.cancel_requested = True
250
-
251
- running = False
260
+ def main() -> None:
261
+ Worker().run()
252
262
 
253
263
 
254
264
  if __name__ == "__main__":
appose/service.py CHANGED
@@ -36,7 +36,7 @@ import threading
36
36
  from enum import Enum
37
37
  from pathlib import Path
38
38
  from traceback import format_exc
39
- from typing import Any, Callable, Dict, List, Optional, Sequence, Union
39
+ from typing import Any, Callable
40
40
  from uuid import uuid4
41
41
 
42
42
  from .types import Args, decode, encode
@@ -52,17 +52,17 @@ class Service:
52
52
 
53
53
  _service_count = 0
54
54
 
55
- def __init__(self, cwd: Union[str, Path], args: Sequence[str]) -> None:
55
+ def __init__(self, cwd: str | Path, args: list[str]) -> None:
56
56
  self._cwd = cwd
57
57
  self._args = args[:]
58
- self._tasks: Dict[str, "Task"] = {}
58
+ self._tasks: dict[str, "Task"] = {}
59
59
  self._service_id = Service._service_count
60
60
  Service._service_count += 1
61
- self._process: Optional[subprocess.Popen] = None
62
- self._stdout_thread: Optional[threading.Thread] = None
63
- self._stderr_thread: Optional[threading.Thread] = None
64
- self._monitor_thread: Optional[threading.Thread] = None
65
- self._debug_callback: Optional[Callable[[Any], Any]] = None
61
+ self._process: subprocess.Popen | None = None
62
+ self._stdout_thread: threading.Thread | None = None
63
+ self._stderr_thread: threading.Thread | None = None
64
+ self._monitor_thread: threading.Thread | None = None
65
+ self._debug_callback: Callable[[Any], Any] | None = None
66
66
 
67
67
  def debug(self, debug_callback: Callable[[Any], Any]) -> None:
68
68
  """
@@ -109,16 +109,20 @@ class Service:
109
109
  self._stderr_thread.start()
110
110
  self._monitor_thread.start()
111
111
 
112
- def task(self, script: str, inputs: Optional[Args] = None) -> "Task":
112
+ def task(
113
+ self, script: str, inputs: Args | None = None, queue: str | None = None
114
+ ) -> "Task":
113
115
  """
114
116
  Create a new task, passing the given script to the worker for execution.
115
117
  :param script:
116
118
  The script for the worker to execute in its environment.
117
119
  :param inputs:
118
120
  Optional list of key/value pairs to feed into the script as inputs.
121
+ :param queue:
122
+ Optional queue target. Pass "main" to queue to worker's main thread.
119
123
  """
120
124
  self.start()
121
- return Task(self, script, inputs)
125
+ return Task(self, script, inputs, queue)
122
126
 
123
127
  def close(self) -> None:
124
128
  """
@@ -274,17 +278,17 @@ class TaskEvent:
274
278
  self,
275
279
  task: "Task",
276
280
  response_type: ResponseType,
277
- message: Optional[str] = None,
278
- current: Optional[int] = None,
279
- maximum: Optional[int] = None,
280
- info: Optional[Dict[str, Any]] = None,
281
+ message: str | None = None,
282
+ current: int | None = None,
283
+ maximum: int | None = None,
284
+ info: dict[str, Any] | None = None,
281
285
  ) -> None:
282
286
  self.task: "Task" = task
283
287
  self.response_type: ResponseType = response_type
284
- self.message: Optional[str] = message
285
- self.current: Optional[int] = current
286
- self.maximum: Optional[int] = maximum
287
- self.info: Optional[Dict[str, Any]] = info
288
+ self.message: str | None = message
289
+ self.current: int | None = current
290
+ self.maximum: int | None = maximum
291
+ self.info: dict[str, Any] | None = info
288
292
 
289
293
  def __str__(self):
290
294
  return f"[{self.response_type}] {self.task}"
@@ -298,21 +302,26 @@ class Task:
298
302
  """
299
303
 
300
304
  def __init__(
301
- self, service: Service, script: str, inputs: Optional[Args] = None
305
+ self,
306
+ service: Service,
307
+ script: str,
308
+ inputs: Args | None = None,
309
+ queue: str | None = None,
302
310
  ) -> None:
303
311
  self.uuid = uuid4().hex
304
312
  self.service = service
305
313
  self.script = script
306
314
  self.inputs: Args = {}
315
+ self.queue: str | None = queue
307
316
  if inputs is not None:
308
317
  self.inputs.update(inputs)
309
318
  self.outputs: Args = {}
310
319
  self.status: TaskStatus = TaskStatus.INITIAL
311
- self.message: Optional[str] = None
320
+ self.message: str | None = None
312
321
  self.current: int = 0
313
322
  self.maximum: int = 1
314
- self.error: Optional[str] = None
315
- self.listeners: List[Callable[["TaskEvent"], None]] = []
323
+ self.error: str | None = None
324
+ self.listeners: list[Callable[["TaskEvent"], None]] = []
316
325
  self.cv = threading.Condition()
317
326
  self.service._tasks[self.uuid] = self
318
327
 
@@ -323,7 +332,7 @@ class Task:
323
332
 
324
333
  self.status = TaskStatus.QUEUED
325
334
 
326
- args = {"script": self.script, "inputs": self.inputs}
335
+ args = {"script": self.script, "inputs": self.inputs, "queue": self.queue}
327
336
  self._request(RequestType.EXECUTE, args)
328
337
 
329
338
  return self
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: appose
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: Appose: multi-language interprocess cooperation with shared memory.
5
5
  Author: Appose developers
6
6
  License-Expression: BSD-2-Clause
@@ -0,0 +1,11 @@
1
+ appose/__init__.py,sha256=SRXmNEqFIAajVlwVCSAdhHQytRKZJiWI5Y5-xwioW0w,7544
2
+ appose/environment.py,sha256=Kg-MfAcmVPDltwqvmrcvus-pP01SNUdn4bE2kdkIVuk,7597
3
+ appose/paths.py,sha256=WgBHwnZdS3Ot8_hssFqUiTGnunolYRrHLf9rAfEU5r4,2182
4
+ appose/python_worker.py,sha256=rkssHh3YTBueGTQ9qAuRVqq7bXncYulscG3kbM0D2TQ,10061
5
+ appose/service.py,sha256=jdtGOOoWjut9eZj441fL8t_P_ln2mfgilBvJdH0bDus,14750
6
+ appose/types.py,sha256=uidbt2JwWJ2D3Mbtc4Vl2sRP6WNgKjmfcHjJ9QLWkcs,10557
7
+ appose-0.6.0.dist-info/licenses/LICENSE.txt,sha256=ZedidA6NyZclNlsx-vl9IVBWRAQSqXdNsCWgtFcwzIE,1313
8
+ appose-0.6.0.dist-info/METADATA,sha256=F900U1qP-pPiPgklzrnjg7og3LuY5h8pXYDdadVtfdY,5598
9
+ appose-0.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ appose-0.6.0.dist-info/top_level.txt,sha256=oqHhw2QGlaFfH3jMR9H5Cd6XYHdEv5DvK1am0iEFPW0,7
11
+ appose-0.6.0.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- appose/__init__.py,sha256=SRXmNEqFIAajVlwVCSAdhHQytRKZJiWI5Y5-xwioW0w,7544
2
- appose/environment.py,sha256=Kg-MfAcmVPDltwqvmrcvus-pP01SNUdn4bE2kdkIVuk,7597
3
- appose/paths.py,sha256=WgBHwnZdS3Ot8_hssFqUiTGnunolYRrHLf9rAfEU5r4,2182
4
- appose/python_worker.py,sha256=iQoz2t0glbXlR1lgynN-yXPIgtlAbCFtMYYpev-LZyU,9844
5
- appose/service.py,sha256=UQn1aoL3uQ8gPKS6nf5oYg0Ea1ZRsaQ3TfLCKeCZLPQ,14581
6
- appose/types.py,sha256=uidbt2JwWJ2D3Mbtc4Vl2sRP6WNgKjmfcHjJ9QLWkcs,10557
7
- appose-0.4.0.dist-info/licenses/LICENSE.txt,sha256=ZedidA6NyZclNlsx-vl9IVBWRAQSqXdNsCWgtFcwzIE,1313
8
- appose-0.4.0.dist-info/METADATA,sha256=cI_E4xetwcHlusk2UzSPOk6SWrNr_vBrGjOC_0H4CFo,5598
9
- appose-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
- appose-0.4.0.dist-info/top_level.txt,sha256=oqHhw2QGlaFfH3jMR9H5Cd6XYHdEv5DvK1am0iEFPW0,7
11
- appose-0.4.0.dist-info/RECORD,,
File without changes