appose 0.1.0__tar.gz → 0.4.0__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.
- {appose-0.1.0 → appose-0.4.0}/LICENSE.txt +1 -1
- {appose-0.1.0/src/appose.egg-info → appose-0.4.0}/PKG-INFO +37 -19
- {appose-0.1.0 → appose-0.4.0}/README.md +19 -13
- {appose-0.1.0 → appose-0.4.0}/pyproject.toml +4 -4
- {appose-0.1.0 → appose-0.4.0}/src/appose/__init__.py +93 -8
- {appose-0.1.0 → appose-0.4.0}/src/appose/environment.py +5 -3
- {appose-0.1.0 → appose-0.4.0}/src/appose/paths.py +2 -2
- {appose-0.1.0 → appose-0.4.0}/src/appose/python_worker.py +96 -17
- {appose-0.1.0 → appose-0.4.0}/src/appose/service.py +101 -22
- appose-0.4.0/src/appose/types.py +243 -0
- {appose-0.1.0 → appose-0.4.0/src/appose.egg-info}/PKG-INFO +37 -19
- {appose-0.1.0 → appose-0.4.0}/src/appose.egg-info/SOURCES.txt +3 -1
- {appose-0.1.0 → appose-0.4.0}/tests/test_appose.py +41 -10
- appose-0.1.0/src/appose/types.py → appose-0.4.0/tests/test_shm.py +31 -8
- appose-0.4.0/tests/test_types.py +131 -0
- {appose-0.1.0 → appose-0.4.0}/setup.cfg +0 -0
- {appose-0.1.0 → appose-0.4.0}/src/appose.egg-info/dependency_links.txt +0 -0
- {appose-0.1.0 → appose-0.4.0}/src/appose.egg-info/requires.txt +0 -0
- {appose-0.1.0 → appose-0.4.0}/src/appose.egg-info/top_level.txt +0 -0
@@ -1,14 +1,14 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: appose
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.4.0
|
4
4
|
Summary: Appose: multi-language interprocess cooperation with shared memory.
|
5
5
|
Author: Appose developers
|
6
|
-
License:
|
6
|
+
License-Expression: BSD-2-Clause
|
7
7
|
Project-URL: homepage, https://github.com/apposed/appose-python
|
8
8
|
Project-URL: documentation, https://github.com/apposed/appose-python/blob/main/README.md
|
9
9
|
Project-URL: source, https://github.com/apposed/appose-python
|
10
10
|
Project-URL: download, https://pypi.org/project/appose-python
|
11
|
-
Project-URL: tracker, https://github.com/apposed/appose
|
11
|
+
Project-URL: tracker, https://github.com/apposed/appose/issues
|
12
12
|
Keywords: java,javascript,python,cross-language,interprocess
|
13
13
|
Classifier: Development Status :: 2 - Pre-Alpha
|
14
14
|
Classifier: Intended Audience :: Developers
|
@@ -17,7 +17,7 @@ Classifier: Intended Audience :: Science/Research
|
|
17
17
|
Classifier: Programming Language :: Python :: 3 :: Only
|
18
18
|
Classifier: Programming Language :: Python :: 3.10
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
20
|
-
Classifier:
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
21
21
|
Classifier: Operating System :: Microsoft :: Windows
|
22
22
|
Classifier: Operating System :: Unix
|
23
23
|
Classifier: Operating System :: MacOS
|
@@ -27,8 +27,20 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
27
|
Classifier: Topic :: Utilities
|
28
28
|
Requires-Python: >=3.10
|
29
29
|
Description-Content-Type: text/markdown
|
30
|
-
Provides-Extra: dev
|
31
30
|
License-File: LICENSE.txt
|
31
|
+
Provides-Extra: dev
|
32
|
+
Requires-Dist: autopep8; extra == "dev"
|
33
|
+
Requires-Dist: black; extra == "dev"
|
34
|
+
Requires-Dist: build; extra == "dev"
|
35
|
+
Requires-Dist: flake8; extra == "dev"
|
36
|
+
Requires-Dist: flake8-pyproject; extra == "dev"
|
37
|
+
Requires-Dist: flake8-typing-imports; extra == "dev"
|
38
|
+
Requires-Dist: isort; extra == "dev"
|
39
|
+
Requires-Dist: pytest; extra == "dev"
|
40
|
+
Requires-Dist: numpy; extra == "dev"
|
41
|
+
Requires-Dist: toml; extra == "dev"
|
42
|
+
Requires-Dist: validate-pyproject[all]; extra == "dev"
|
43
|
+
Dynamic: license-file
|
32
44
|
|
33
45
|
# Appose Python
|
34
46
|
|
@@ -60,16 +72,6 @@ This is the **Python implementation of Appose**.
|
|
60
72
|
|
61
73
|
The name of the package is `appose`.
|
62
74
|
|
63
|
-
### Conda/Mamba
|
64
|
-
|
65
|
-
To use [the conda-forge package](https://anaconda.org/conda-forge/appose),
|
66
|
-
add `appose` to your `environment.yml`'s `dependencies` section:
|
67
|
-
|
68
|
-
```yaml
|
69
|
-
dependencies:
|
70
|
-
- appose
|
71
|
-
```
|
72
|
-
|
73
75
|
### PyPI/Pip
|
74
76
|
|
75
77
|
To use [the PyPI package](https://pypi.org/project/appose),
|
@@ -87,6 +89,16 @@ dependencies = [
|
|
87
89
|
]
|
88
90
|
```
|
89
91
|
|
92
|
+
### Conda/Mamba
|
93
|
+
|
94
|
+
To use [the conda-forge package](https://anaconda.org/conda-forge/appose),
|
95
|
+
add `appose` to your `environment.yml`'s `dependencies` section:
|
96
|
+
|
97
|
+
```yaml
|
98
|
+
dependencies:
|
99
|
+
- appose
|
100
|
+
```
|
101
|
+
|
90
102
|
## Examples
|
91
103
|
|
92
104
|
Here is a minimal example for calling into Java from Python:
|
@@ -96,12 +108,12 @@ import appose
|
|
96
108
|
env = appose.java(vendor="zulu", version="17").build()
|
97
109
|
with env.groovy() as groovy:
|
98
110
|
task = groovy.task("5 + 6")
|
99
|
-
task.
|
100
|
-
result = task.outputs
|
111
|
+
task.wait_for()
|
112
|
+
result = task.outputs["result"]
|
101
113
|
assert 11 == result
|
102
114
|
```
|
103
115
|
|
104
|
-
*Note: The `
|
116
|
+
*Note: The `appose.java` builder is planned, but not yet implemented.*
|
105
117
|
|
106
118
|
Here is an example using a few more of Appose's features:
|
107
119
|
|
@@ -158,3 +170,9 @@ with env.groovy() as groovy:
|
|
158
170
|
|
159
171
|
Of course, the above examples could have been done all in one language. But
|
160
172
|
hopefully they hint at the possibilities of easy cross-language integration.
|
173
|
+
|
174
|
+
## Issue tracker
|
175
|
+
|
176
|
+
All implementations of Appose use the same issue tracker:
|
177
|
+
|
178
|
+
https://github.com/apposed/appose/issues
|
@@ -28,16 +28,6 @@ This is the **Python implementation of Appose**.
|
|
28
28
|
|
29
29
|
The name of the package is `appose`.
|
30
30
|
|
31
|
-
### Conda/Mamba
|
32
|
-
|
33
|
-
To use [the conda-forge package](https://anaconda.org/conda-forge/appose),
|
34
|
-
add `appose` to your `environment.yml`'s `dependencies` section:
|
35
|
-
|
36
|
-
```yaml
|
37
|
-
dependencies:
|
38
|
-
- appose
|
39
|
-
```
|
40
|
-
|
41
31
|
### PyPI/Pip
|
42
32
|
|
43
33
|
To use [the PyPI package](https://pypi.org/project/appose),
|
@@ -55,6 +45,16 @@ dependencies = [
|
|
55
45
|
]
|
56
46
|
```
|
57
47
|
|
48
|
+
### Conda/Mamba
|
49
|
+
|
50
|
+
To use [the conda-forge package](https://anaconda.org/conda-forge/appose),
|
51
|
+
add `appose` to your `environment.yml`'s `dependencies` section:
|
52
|
+
|
53
|
+
```yaml
|
54
|
+
dependencies:
|
55
|
+
- appose
|
56
|
+
```
|
57
|
+
|
58
58
|
## Examples
|
59
59
|
|
60
60
|
Here is a minimal example for calling into Java from Python:
|
@@ -64,12 +64,12 @@ import appose
|
|
64
64
|
env = appose.java(vendor="zulu", version="17").build()
|
65
65
|
with env.groovy() as groovy:
|
66
66
|
task = groovy.task("5 + 6")
|
67
|
-
task.
|
68
|
-
result = task.outputs
|
67
|
+
task.wait_for()
|
68
|
+
result = task.outputs["result"]
|
69
69
|
assert 11 == result
|
70
70
|
```
|
71
71
|
|
72
|
-
*Note: The `
|
72
|
+
*Note: The `appose.java` builder is planned, but not yet implemented.*
|
73
73
|
|
74
74
|
Here is an example using a few more of Appose's features:
|
75
75
|
|
@@ -126,3 +126,9 @@ with env.groovy() as groovy:
|
|
126
126
|
|
127
127
|
Of course, the above examples could have been done all in one language. But
|
128
128
|
hopefully they hint at the possibilities of easy cross-language integration.
|
129
|
+
|
130
|
+
## Issue tracker
|
131
|
+
|
132
|
+
All implementations of Appose use the same issue tracker:
|
133
|
+
|
134
|
+
https://github.com/apposed/appose/issues
|
@@ -4,9 +4,9 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "appose"
|
7
|
-
version = "0.
|
7
|
+
version = "0.4.0"
|
8
8
|
description = "Appose: multi-language interprocess cooperation with shared memory."
|
9
|
-
license =
|
9
|
+
license = "BSD-2-Clause"
|
10
10
|
authors = [{name = "Appose developers"}]
|
11
11
|
readme = "README.md"
|
12
12
|
keywords = ["java", "javascript", "python", "cross-language", "interprocess"]
|
@@ -18,7 +18,7 @@ classifiers = [
|
|
18
18
|
"Programming Language :: Python :: 3 :: Only",
|
19
19
|
"Programming Language :: Python :: 3.10",
|
20
20
|
"Programming Language :: Python :: 3.11",
|
21
|
-
"
|
21
|
+
"Programming Language :: Python :: 3.12",
|
22
22
|
"Operating System :: Microsoft :: Windows",
|
23
23
|
"Operating System :: Unix",
|
24
24
|
"Operating System :: MacOS",
|
@@ -54,7 +54,7 @@ homepage = "https://github.com/apposed/appose-python"
|
|
54
54
|
documentation = "https://github.com/apposed/appose-python/blob/main/README.md"
|
55
55
|
source = "https://github.com/apposed/appose-python"
|
56
56
|
download = "https://pypi.org/project/appose-python"
|
57
|
-
tracker = "https://github.com/apposed/appose
|
57
|
+
tracker = "https://github.com/apposed/appose/issues"
|
58
58
|
|
59
59
|
[tool.setuptools]
|
60
60
|
package-dir = {"" = "src"}
|
@@ -2,7 +2,7 @@
|
|
2
2
|
# #%L
|
3
3
|
# Appose: multi-language interprocess cooperation with shared memory.
|
4
4
|
# %%
|
5
|
-
# Copyright (C) 2023 Appose developers.
|
5
|
+
# Copyright (C) 2023 - 2025 Appose developers.
|
6
6
|
# %%
|
7
7
|
# Redistribution and use in source and binary forms, with or without
|
8
8
|
# modification, are permitted provided that the following conditions are met:
|
@@ -42,8 +42,6 @@ The steps for using Appose are:
|
|
42
42
|
|
43
43
|
## Examples
|
44
44
|
|
45
|
-
* TODO - move the below code somewhere linkable, for succinctness here.
|
46
|
-
|
47
45
|
Here is a very simple example written in Python:
|
48
46
|
|
49
47
|
import appose
|
@@ -52,7 +50,7 @@ Here is a very simple example written in Python:
|
|
52
50
|
Task task = groovy.task("""
|
53
51
|
5 + 6
|
54
52
|
""")
|
55
|
-
task.
|
53
|
+
task.wait_for()
|
56
54
|
result = task.outputs.get("result")
|
57
55
|
assert 11 == result
|
58
56
|
|
@@ -84,7 +82,7 @@ And here is an example using a few more of Appose's features:
|
|
84
82
|
def task_listener(event):
|
85
83
|
match event.responseType:
|
86
84
|
case UPDATE:
|
87
|
-
print(f"Progress {
|
85
|
+
print(f"Progress {event.current}/{event.maximum}")
|
88
86
|
case COMPLETION:
|
89
87
|
numer = task.outputs["numer"]
|
90
88
|
denom = task.outputs["denom"]
|
@@ -103,7 +101,7 @@ And here is an example using a few more of Appose's features:
|
|
103
101
|
# Task is taking too long; request a cancelation.
|
104
102
|
task.cancel()
|
105
103
|
|
106
|
-
task.
|
104
|
+
task.wait_for()
|
107
105
|
|
108
106
|
Of course, the above examples could have been done all in Python. But
|
109
107
|
hopefully they hint at the possibilities of easy cross-language integration.
|
@@ -127,13 +125,100 @@ But Appose is compatible with any program that abides by the
|
|
127
125
|
2. The worker must issue responses in Appose's response format on its
|
128
126
|
standard output (stdout) stream.
|
129
127
|
|
130
|
-
|
131
|
-
|
128
|
+
### Requests to worker from service
|
129
|
+
|
130
|
+
A *request* is a single line of JSON sent to the worker process via
|
131
|
+
its standard input stream. It has a `task` key taking the form of a
|
132
|
+
UUID, and a `requestType` key with one of the following values:
|
133
|
+
|
134
|
+
#### EXECUTE
|
135
|
+
|
136
|
+
Asynchronously execute a script within the worker process. E.g.:
|
137
|
+
|
138
|
+
{
|
139
|
+
"task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
|
140
|
+
"requestType" : "EXECUTE",
|
141
|
+
"script" : "task.outputs[\"result\"] = computeResult(gamma)\n",
|
142
|
+
"inputs" : {"gamma": 2.2}
|
143
|
+
}
|
144
|
+
|
145
|
+
#### CANCEL
|
146
|
+
|
147
|
+
Cancel a running script. E.g.:
|
148
|
+
|
149
|
+
{
|
150
|
+
"task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
|
151
|
+
"requestType" : "CANCEL"
|
152
|
+
}
|
153
|
+
|
154
|
+
### Responses from worker to service
|
155
|
+
|
156
|
+
A *response* is a single line of JSON with a `task` key taking the form
|
157
|
+
of a UUID, and a `responseType` key with one of the following values:
|
158
|
+
|
159
|
+
#### LAUNCH
|
160
|
+
|
161
|
+
A LAUNCH response is issued to confirm the success of an EXECUTE
|
162
|
+
request.
|
163
|
+
|
164
|
+
{
|
165
|
+
"task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
|
166
|
+
"responseType" : "LAUNCH"
|
167
|
+
}
|
168
|
+
|
169
|
+
#### UPDATE
|
170
|
+
|
171
|
+
An UPDATE response is issued to convey that a task has somehow made
|
172
|
+
progress. The UPDATE response typically comes bundled with a
|
173
|
+
`message` string indicating what has changed, `current` and/or
|
174
|
+
`maximum` progress indicators conveying the step the task has
|
175
|
+
reached, or both.
|
176
|
+
|
177
|
+
{
|
178
|
+
"task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
|
179
|
+
"responseType" : "UPDATE",
|
180
|
+
"message" : "Processing step 0 of 91",
|
181
|
+
"current" : 0,
|
182
|
+
"maximum" : 91
|
183
|
+
}
|
184
|
+
|
185
|
+
#### COMPLETION
|
186
|
+
|
187
|
+
A COMPLETION response is issued to convey that a task has successfully
|
188
|
+
completed execution, as well as report the values of any task outputs.
|
189
|
+
|
190
|
+
{
|
191
|
+
"task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
|
192
|
+
"responseType" : "COMPLETION",
|
193
|
+
"outputs" : {"result" : 91}
|
194
|
+
}
|
195
|
+
|
196
|
+
#### CANCELATION
|
197
|
+
|
198
|
+
A CANCELATION response is issued to confirm the success of a CANCEL
|
199
|
+
request.
|
200
|
+
|
201
|
+
{
|
202
|
+
"task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
|
203
|
+
"responseType" : "CANCELATION"
|
204
|
+
}
|
205
|
+
|
206
|
+
#### FAILURE
|
207
|
+
|
208
|
+
A FAILURE response is issued to convey that a task did not completely
|
209
|
+
and successfully execute, such as an exception being raised.
|
210
|
+
|
211
|
+
{
|
212
|
+
"task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
|
213
|
+
"responseType" : "FAILURE",
|
214
|
+
"error", "Invalid gamma value"
|
215
|
+
}
|
132
216
|
'''
|
133
217
|
|
134
218
|
from pathlib import Path
|
135
219
|
|
136
220
|
from .environment import Builder, Environment
|
221
|
+
from .types import NDArray, SharedMemory # noqa: F401
|
137
222
|
|
138
223
|
|
139
224
|
def base(directory: Path) -> Builder:
|
@@ -2,7 +2,7 @@
|
|
2
2
|
# #%L
|
3
3
|
# Appose: multi-language interprocess cooperation with shared memory.
|
4
4
|
# %%
|
5
|
-
# Copyright (C) 2023 Appose developers.
|
5
|
+
# Copyright (C) 2023 - 2025 Appose developers.
|
6
6
|
# %%
|
7
7
|
# Redistribution and use in source and binary forms, with or without
|
8
8
|
# modification, are permitted provided that the following conditions are met:
|
@@ -58,6 +58,7 @@ class Environment:
|
|
58
58
|
"""
|
59
59
|
python_exes = [
|
60
60
|
"python",
|
61
|
+
"python3",
|
61
62
|
"python.exe",
|
62
63
|
"bin/python",
|
63
64
|
"bin/python.exe",
|
@@ -109,8 +110,9 @@ class Environment:
|
|
109
110
|
# TODO: Ensure that the classpath includes Appose and its dependencies.
|
110
111
|
|
111
112
|
# Append any explicitly requested classpath elements.
|
112
|
-
|
113
|
-
|
113
|
+
if class_path is not None:
|
114
|
+
for element in class_path:
|
115
|
+
cp[element] = None
|
114
116
|
|
115
117
|
# Build up the service arguments.
|
116
118
|
args = [
|
@@ -2,7 +2,7 @@
|
|
2
2
|
# #%L
|
3
3
|
# Appose: multi-language interprocess cooperation with shared memory.
|
4
4
|
# %%
|
5
|
-
# Copyright (C) 2023 Appose developers.
|
5
|
+
# Copyright (C) 2023 - 2025 Appose developers.
|
6
6
|
# %%
|
7
7
|
# Redistribution and use in source and binary forms, with or without
|
8
8
|
# modification, are permitted provided that the following conditions are met:
|
@@ -51,6 +51,6 @@ def find_exe(dirs: Sequence[str], exes: Sequence[str]) -> Optional[Path]:
|
|
51
51
|
# Candidate is a relative path; check beneath each given directory.
|
52
52
|
for d in dirs:
|
53
53
|
f = Path(d) / exe
|
54
|
-
if can_execute(f):
|
54
|
+
if can_execute(f) and not f.is_dir():
|
55
55
|
return f
|
56
56
|
return None
|
@@ -2,7 +2,7 @@
|
|
2
2
|
# #%L
|
3
3
|
# Appose: multi-language interprocess cooperation with shared memory.
|
4
4
|
# %%
|
5
|
-
# Copyright (C) 2023 Appose developers.
|
5
|
+
# Copyright (C) 2023 - 2025 Appose developers.
|
6
6
|
# %%
|
7
7
|
# Redistribution and use in source and binary forms, with or without
|
8
8
|
# modification, are permitted provided that the following conditions are met:
|
@@ -28,39 +28,57 @@
|
|
28
28
|
###
|
29
29
|
|
30
30
|
"""
|
31
|
-
|
31
|
+
The Appose worker for running Python scripts.
|
32
|
+
|
33
|
+
Like all Appose workers, this program conforms to the Appose worker process
|
34
|
+
contract, meaning it accepts requests on stdin and produces responses on
|
35
|
+
stdout, both formatted according to Appose's assumptions.
|
36
|
+
|
37
|
+
For details, see the Appose README:
|
38
|
+
https://github.com/apposed/appose/blob/-/README.md#workers
|
32
39
|
"""
|
33
40
|
|
34
41
|
import ast
|
35
42
|
import sys
|
36
43
|
import traceback
|
37
44
|
from threading import Thread
|
38
|
-
from
|
45
|
+
from time import sleep
|
46
|
+
from typing import Any, Dict, Optional
|
39
47
|
|
40
48
|
# NB: Avoid relative imports so that this script can be run standalone.
|
41
49
|
from appose.service import RequestType, ResponseType
|
42
|
-
from appose.types import Args, decode, encode
|
50
|
+
from appose.types import Args, _set_worker, decode, encode
|
43
51
|
|
44
52
|
|
45
53
|
class Task:
|
46
54
|
def __init__(self, uuid: str) -> None:
|
47
55
|
self.uuid = uuid
|
48
56
|
self.outputs = {}
|
57
|
+
self.finished = False
|
49
58
|
self.cancel_requested = False
|
59
|
+
self.thread = None # Initialize thread attribute
|
50
60
|
|
51
61
|
def update(
|
52
62
|
self,
|
53
63
|
message: Optional[str] = None,
|
54
64
|
current: Optional[int] = None,
|
55
65
|
maximum: Optional[int] = None,
|
66
|
+
info: Optional[Dict[str, Any]] = None,
|
56
67
|
) -> None:
|
57
68
|
args = {}
|
58
69
|
if message is not None:
|
59
|
-
args["message"] = message
|
70
|
+
args["message"] = str(message)
|
60
71
|
if current is not None:
|
61
|
-
|
72
|
+
try:
|
73
|
+
args["current"] = int(current)
|
74
|
+
except ValueError:
|
75
|
+
pass
|
62
76
|
if maximum is not None:
|
63
|
-
|
77
|
+
try:
|
78
|
+
args["maximum"] = int(maximum)
|
79
|
+
except ValueError:
|
80
|
+
pass
|
81
|
+
args["info"] = info
|
64
82
|
self._respond(ResponseType.UPDATE, args)
|
65
83
|
|
66
84
|
def cancel(self) -> None:
|
@@ -74,7 +92,6 @@ class Task:
|
|
74
92
|
def execute_script():
|
75
93
|
# Populate script bindings.
|
76
94
|
binding = {"task": self}
|
77
|
-
# TODO: Magically convert shared memory image inputs.
|
78
95
|
if inputs is not None:
|
79
96
|
binding.update(inputs)
|
80
97
|
|
@@ -99,7 +116,14 @@ class Task:
|
|
99
116
|
# Last statement of the script looks like an expression. Evaluate!
|
100
117
|
last = ast.Expression(block.body.pop().value)
|
101
118
|
|
102
|
-
|
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
|
103
127
|
exec(compile(block, "<string>", mode="exec"), _globals, binding)
|
104
128
|
if last is not None:
|
105
129
|
result = eval(
|
@@ -119,10 +143,24 @@ class Task:
|
|
119
143
|
|
120
144
|
self._report_completion()
|
121
145
|
|
122
|
-
#
|
123
|
-
#
|
124
|
-
#
|
125
|
-
|
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()
|
126
164
|
|
127
165
|
def _report_launch(self) -> None:
|
128
166
|
self._respond(ResponseType.LAUNCH, None)
|
@@ -132,15 +170,56 @@ class Task:
|
|
132
170
|
self._respond(ResponseType.COMPLETION, args)
|
133
171
|
|
134
172
|
def _respond(self, response_type: ResponseType, args: Optional[Args]) -> None:
|
135
|
-
|
173
|
+
already_terminated = False
|
174
|
+
if response_type.is_terminal():
|
175
|
+
if self.finished:
|
176
|
+
# This is not the first terminal response. Let's
|
177
|
+
# remember, in case an exception is generated below,
|
178
|
+
# so that we can avoid infinite recursion loops.
|
179
|
+
already_terminated = True
|
180
|
+
self.finished = True
|
181
|
+
|
182
|
+
response = {}
|
136
183
|
if args is not None:
|
137
184
|
response.update(args)
|
185
|
+
response.update({"task": self.uuid, "responseType": response_type.value})
|
138
186
|
# NB: Flush is necessary to ensure service receives the data!
|
139
|
-
|
187
|
+
try:
|
188
|
+
print(encode(response), flush=True)
|
189
|
+
except Exception:
|
190
|
+
if already_terminated:
|
191
|
+
# An exception triggered a failure response which
|
192
|
+
# then triggered another exception. Let's stop here
|
193
|
+
# to avoid the risk of infinite recursion loops.
|
194
|
+
return
|
195
|
+
# Encoding can fail due to unsupported types, when the
|
196
|
+
# response or its elements are not supported by JSON encoding.
|
197
|
+
# No matter what goes wrong, we want to tell the caller.
|
198
|
+
self.fail(traceback.format_exc())
|
140
199
|
|
141
200
|
|
142
201
|
def main() -> None:
|
202
|
+
_set_worker(True)
|
203
|
+
|
143
204
|
tasks = {}
|
205
|
+
running = True
|
206
|
+
|
207
|
+
def cleanup_threads():
|
208
|
+
while running:
|
209
|
+
sleep(0.05)
|
210
|
+
dead = {
|
211
|
+
uuid: task
|
212
|
+
for uuid, task in tasks.items()
|
213
|
+
if task.thread is not None and not task.thread.is_alive()
|
214
|
+
}
|
215
|
+
for uuid, task in dead.items():
|
216
|
+
tasks.pop(uuid)
|
217
|
+
if not task.finished:
|
218
|
+
# The task died before reporting a terminal status.
|
219
|
+
# We report this situation as failure by thread death.
|
220
|
+
task.fail("thread death")
|
221
|
+
|
222
|
+
Thread(target=cleanup_threads, name="Appose-Janitor").start()
|
144
223
|
|
145
224
|
while True:
|
146
225
|
try:
|
@@ -165,12 +244,12 @@ def main() -> None:
|
|
165
244
|
case RequestType.CANCEL:
|
166
245
|
task = tasks.get(uuid)
|
167
246
|
if task is None:
|
168
|
-
# TODO: proper logging
|
169
|
-
# Maybe should stdout the error back to Appose calling process.
|
170
247
|
print(f"No such task: {uuid}", file=sys.stderr)
|
171
248
|
continue
|
172
249
|
task.cancel_requested = True
|
173
250
|
|
251
|
+
running = False
|
252
|
+
|
174
253
|
|
175
254
|
if __name__ == "__main__":
|
176
255
|
main()
|