appose 0.1.0__tar.gz → 0.2.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.
@@ -1,4 +1,4 @@
1
- Copyright (c) 2023, Appose developers.
1
+ Copyright (c) 2023 - 2024, Appose developers.
2
2
  All rights reserved.
3
3
 
4
4
  Redistribution and use in source and binary forms, with or without modification,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: appose
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Appose: multi-language interprocess cooperation with shared memory.
5
5
  Author: Appose developers
6
6
  License: Simplified BSD License
@@ -8,7 +8,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-python/issues
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
@@ -27,8 +27,19 @@ 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"
32
43
 
33
44
  # Appose Python
34
45
 
@@ -60,16 +71,6 @@ This is the **Python implementation of Appose**.
60
71
 
61
72
  The name of the package is `appose`.
62
73
 
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
74
  ### PyPI/Pip
74
75
 
75
76
  To use [the PyPI package](https://pypi.org/project/appose),
@@ -87,6 +88,16 @@ dependencies = [
87
88
  ]
88
89
  ```
89
90
 
91
+ ### Conda/Mamba
92
+
93
+ To use [the conda-forge package](https://anaconda.org/conda-forge/appose),
94
+ add `appose` to your `environment.yml`'s `dependencies` section:
95
+
96
+ ```yaml
97
+ dependencies:
98
+ - appose
99
+ ```
100
+
90
101
  ## Examples
91
102
 
92
103
  Here is a minimal example for calling into Java from Python:
@@ -96,12 +107,12 @@ import appose
96
107
  env = appose.java(vendor="zulu", version="17").build()
97
108
  with env.groovy() as groovy:
98
109
  task = groovy.task("5 + 6")
99
- task.waitFor()
100
- result = task.outputs.get("result")
110
+ task.wait_for()
111
+ result = task.outputs["result"]
101
112
  assert 11 == result
102
113
  ```
103
114
 
104
- *Note: The `Appose.java` builder is planned, but not yet implemented.*
115
+ *Note: The `appose.java` builder is planned, but not yet implemented.*
105
116
 
106
117
  Here is an example using a few more of Appose's features:
107
118
 
@@ -158,3 +169,9 @@ with env.groovy() as groovy:
158
169
 
159
170
  Of course, the above examples could have been done all in one language. But
160
171
  hopefully they hint at the possibilities of easy cross-language integration.
172
+
173
+ ## Issue tracker
174
+
175
+ All implementations of Appose use the same issue tracker:
176
+
177
+ 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.waitFor()
68
- result = task.outputs.get("result")
67
+ task.wait_for()
68
+ result = task.outputs["result"]
69
69
  assert 11 == result
70
70
  ```
71
71
 
72
- *Note: The `Appose.java` builder is planned, but not yet implemented.*
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,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "appose"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Appose: multi-language interprocess cooperation with shared memory."
9
9
  license = {text = "Simplified BSD License"}
10
10
  authors = [{name = "Appose developers"}]
@@ -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-python/issues"
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 - 2024 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:
@@ -52,7 +52,7 @@ Here is a very simple example written in Python:
52
52
  Task task = groovy.task("""
53
53
  5 + 6
54
54
  """)
55
- task.waitFor()
55
+ task.wait_for()
56
56
  result = task.outputs.get("result")
57
57
  assert 11 == result
58
58
 
@@ -103,7 +103,7 @@ And here is an example using a few more of Appose's features:
103
103
  # Task is taking too long; request a cancelation.
104
104
  task.cancel()
105
105
 
106
- task.waitFor()
106
+ task.wait_for()
107
107
 
108
108
  Of course, the above examples could have been done all in Python. But
109
109
  hopefully they hint at the possibilities of easy cross-language integration.
@@ -134,6 +134,7 @@ JSON, one line per request/response.
134
134
  from pathlib import Path
135
135
 
136
136
  from .environment import Builder, Environment
137
+ from .types import NDArray, SharedMemory # noqa: F401
137
138
 
138
139
 
139
140
  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 - 2024 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
- for element in class_path:
113
- cp[element] = None
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 - 2024 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 - 2024 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,7 +28,14 @@
28
28
  ###
29
29
 
30
30
  """
31
- TODO
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
@@ -39,7 +46,7 @@ from typing import Optional
39
46
 
40
47
  # NB: Avoid relative imports so that this script can be run standalone.
41
48
  from appose.service import RequestType, ResponseType
42
- from appose.types import Args, decode, encode
49
+ from appose.types import Args, _set_worker, decode, encode
43
50
 
44
51
 
45
52
  class Task:
@@ -56,11 +63,17 @@ class Task:
56
63
  ) -> None:
57
64
  args = {}
58
65
  if message is not None:
59
- args["message"] = message
66
+ args["message"] = str(message)
60
67
  if current is not None:
61
- args["current"] = current
68
+ try:
69
+ args["current"] = int(current)
70
+ except ValueError:
71
+ pass
62
72
  if maximum is not None:
63
- args["maximum"] = maximum
73
+ try:
74
+ args["maximum"] = int(maximum)
75
+ except ValueError:
76
+ pass
64
77
  self._respond(ResponseType.UPDATE, args)
65
78
 
66
79
  def cancel(self) -> None:
@@ -74,7 +87,6 @@ class Task:
74
87
  def execute_script():
75
88
  # Populate script bindings.
76
89
  binding = {"task": self}
77
- # TODO: Magically convert shared memory image inputs.
78
90
  if inputs is not None:
79
91
  binding.update(inputs)
80
92
 
@@ -136,10 +148,22 @@ class Task:
136
148
  if args is not None:
137
149
  response.update(args)
138
150
  # NB: Flush is necessary to ensure service receives the data!
139
- print(encode(response), flush=True)
151
+ try:
152
+ print(encode(response), flush=True)
153
+ except Exception:
154
+ # Encoding can fail due to unsupported types, when the response
155
+ # or its elements are not supported by JSON encoding.
156
+ # No matter what goes wrong, we want to tell the caller.
157
+ if response_type is ResponseType.FAILURE:
158
+ # TODO: How to address this hypothetical case
159
+ # of a failure message triggering another failure?
160
+ raise
161
+ self.fail(traceback.format_exc())
140
162
 
141
163
 
142
164
  def main() -> None:
165
+ _set_worker(True)
166
+
143
167
  tasks = {}
144
168
 
145
169
  while True:
@@ -165,8 +189,6 @@ def main() -> None:
165
189
  case RequestType.CANCEL:
166
190
  task = tasks.get(uuid)
167
191
  if task is None:
168
- # TODO: proper logging
169
- # Maybe should stdout the error back to Appose calling process.
170
192
  print(f"No such task: {uuid}", file=sys.stderr)
171
193
  continue
172
194
  task.cancel_requested = True
@@ -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 - 2024 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:
@@ -61,6 +61,7 @@ class Service:
61
61
  self._process: Optional[subprocess.Popen] = None
62
62
  self._stdout_thread: Optional[threading.Thread] = None
63
63
  self._stderr_thread: Optional[threading.Thread] = None
64
+ self._monitor_thread: Optional[threading.Thread] = None
64
65
  self._debug_callback: Optional[Callable[[Any], Any]] = None
65
66
 
66
67
  def debug(self, debug_callback: Callable[[Any], Any]) -> None:
@@ -101,8 +102,12 @@ class Service:
101
102
  self._stderr_thread = threading.Thread(
102
103
  target=self._stderr_loop, name=f"{prefix}-Stderr"
103
104
  )
105
+ self._monitor_thread = threading.Thread(
106
+ target=self._monitor_loop, name=f"{prefix}-Monitor"
107
+ )
104
108
  self._stdout_thread.start()
105
109
  self._stderr_thread.start()
110
+ self._monitor_thread.start()
106
111
 
107
112
  def task(self, script: str, inputs: Optional[Args] = None) -> "Task":
108
113
  """
@@ -131,15 +136,24 @@ class Service:
131
136
  """
132
137
  Input loop processing lines from the worker's stdout stream.
133
138
  """
134
- # noinspection PyBroadException
135
- try:
136
- while True:
137
- line = self._process.stdout.readline()
138
- self._debug_service("<worker stdout closed>" if line is None else line)
139
+ while True:
140
+ stdout = self._process.stdout
141
+ # noinspection PyBroadException
142
+ try:
143
+ line = None if stdout is None else stdout.readline()
144
+ except Exception:
145
+ # Something went wrong reading the line. Panic!
146
+ self._debug_service(format_exc())
147
+ break
148
+
149
+ if not line: # readline returns empty string upon EOF
150
+ self._debug_service("<worker stdout closed>")
151
+ return
139
152
 
140
- if line is None:
141
- return # pipe closed
153
+ # noinspection PyBroadException
154
+ try:
142
155
  response = decode(line)
156
+ self._debug_service(line) # Echo the line to the debug listener.
143
157
  uuid = response.get("task")
144
158
  if uuid is None:
145
159
  self._debug_service("Invalid service message: {line}")
@@ -150,8 +164,10 @@ class Service:
150
164
  continue
151
165
  # noinspection PyProtectedMember
152
166
  task._handle(response)
153
- except Exception:
154
- self._debug_service(format_exc())
167
+ except Exception:
168
+ # Something went wrong decoding the line of JSON.
169
+ # Skip it and keep going, but log it first.
170
+ self._debug_service(f"<INVALID> {line}")
155
171
 
156
172
  def _stderr_loop(self) -> None:
157
173
  """
@@ -160,14 +176,37 @@ class Service:
160
176
  # noinspection PyBroadException
161
177
  try:
162
178
  while True:
163
- line = self._process.stderr.readline()
164
- if line is None:
179
+ stderr = self._process.stderr
180
+ line = None if stderr is None else stderr.readline()
181
+ if not line: # readline returns empty string upon EOF
165
182
  self._debug_service("<worker stderr closed>")
166
183
  return
167
184
  self._debug_worker(line)
168
185
  except Exception:
169
186
  self._debug_service(format_exc())
170
187
 
188
+ def _monitor_loop(self) -> None:
189
+ # Wait until the worker process terminates.
190
+ self._process.wait()
191
+
192
+ # Do some sanity checks.
193
+ exit_code = self._process.returncode
194
+ if exit_code != 0:
195
+ self._debug_service(
196
+ f"<worker process terminated with exit code {exit_code}>"
197
+ )
198
+ task_count = len(self._tasks)
199
+ if task_count > 0:
200
+ self._debug_service(
201
+ f"<worker process terminated with {task_count} pending tasks>"
202
+ )
203
+
204
+ # Notify any remaining tasks about the process crash.
205
+ for task in self._tasks.values():
206
+ task._crash()
207
+
208
+ self._tasks.clear()
209
+
171
210
  def _debug_service(self, message: str) -> None:
172
211
  self._debug("SERVICE", message)
173
212
 
@@ -190,15 +229,17 @@ class TaskStatus(Enum):
190
229
  COMPLETE = "COMPLETE"
191
230
  CANCELED = "CANCELED"
192
231
  FAILED = "FAILED"
232
+ CRASHED = "CRASHED"
193
233
 
194
234
  def is_finished(self):
195
235
  """
196
- True iff status is COMPLETE, CANCELED, or FAILED.
236
+ True iff status is COMPLETE, CANCELED, FAILED, or CRASHED.
197
237
  """
198
238
  return self in (
199
239
  TaskStatus.COMPLETE,
200
240
  TaskStatus.CANCELED,
201
241
  TaskStatus.FAILED,
242
+ TaskStatus.CRASHED,
202
243
  )
203
244
 
204
245
 
@@ -213,6 +254,7 @@ class ResponseType(Enum):
213
254
  COMPLETION = "COMPLETION"
214
255
  CANCELATION = "CANCELATION"
215
256
  FAILURE = "FAILURE"
257
+ CRASH = "CRASH"
216
258
 
217
259
 
218
260
  class TaskEvent:
@@ -220,6 +262,9 @@ class TaskEvent:
220
262
  self.task: "Task" = task
221
263
  self.response_type: ResponseType = response_type
222
264
 
265
+ def __str__(self):
266
+ return f"[{self.response_type}] {self.task}"
267
+
223
268
 
224
269
  # noinspection PyProtectedMember
225
270
  class Task:
@@ -342,3 +387,17 @@ class Task:
342
387
  if self.status.is_finished():
343
388
  with self.cv:
344
389
  self.cv.notify_all()
390
+
391
+ def _crash(self):
392
+ event = TaskEvent(self, ResponseType.CRASH)
393
+ self.status = TaskStatus.CRASHED
394
+ for listener in self.listeners:
395
+ listener(event)
396
+ with self.cv:
397
+ self.cv.notify_all()
398
+
399
+ def __str__(self):
400
+ return (
401
+ f"{self.uuid=}, {self.status=}, {self.message=}, "
402
+ f"{self.current=}, {self.maximum=}, {self.error=}"
403
+ )
@@ -0,0 +1,197 @@
1
+ ###
2
+ # #%L
3
+ # Appose: multi-language interprocess cooperation with shared memory.
4
+ # %%
5
+ # Copyright (C) 2023 - 2024 Appose developers.
6
+ # %%
7
+ # Redistribution and use in source and binary forms, with or without
8
+ # modification, are permitted provided that the following conditions are met:
9
+ #
10
+ # 1. Redistributions of source code must retain the above copyright notice,
11
+ # this list of conditions and the following disclaimer.
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26
+ # POSSIBILITY OF SUCH DAMAGE.
27
+ # #L%
28
+ ###
29
+
30
+ import json
31
+ import re
32
+ from math import ceil, prod
33
+ from multiprocessing import resource_tracker, shared_memory
34
+ from typing import Any, Dict, Sequence, Union
35
+
36
+ Args = Dict[str, Any]
37
+
38
+
39
+ class SharedMemory(shared_memory.SharedMemory):
40
+ """
41
+ An enhanced version of Python's multiprocessing.shared_memory.SharedMemory
42
+ class which can be used with a `with` statement. When the program flow
43
+ exits the `with` block, this class's `dispose()` method will be invoked,
44
+ which might call `close()` or `unlink()` depending on the value of its
45
+ `unlink_on_dispose` flag.
46
+ """
47
+
48
+ def __init__(self, name: str = None, create: bool = False, size: int = 0):
49
+ super().__init__(name=name, create=create, size=size)
50
+ self._unlink_on_dispose = create
51
+ if _is_worker:
52
+ # HACK: Remove this shared memory block from the resource_tracker,
53
+ # which wants to clean up shared memory blocks after all known
54
+ # references are done using them.
55
+ #
56
+ # There is one resource_tracker per Python process, and they will
57
+ # each try to delete shared memory blocks known to them when they
58
+ # are shutting down, even when other processes still need them.
59
+ #
60
+ # As such, the rule Appose follows is: let the service process
61
+ # always handle cleanup of shared memory blocks, regardless of
62
+ # which process initially allocated it.
63
+ resource_tracker.unregister(self._name, "shared_memory")
64
+
65
+ def unlink_on_dispose(self, value: bool) -> None:
66
+ """
67
+ Set whether the `unlink()` method should be invoked to destroy
68
+ the shared memory block when the `dispose()` method is called.
69
+
70
+ Note: dispose() is the method called when exiting a `with` block.
71
+
72
+ By default, shared memory objects constructed with `create=True`
73
+ will behave this way, whereas shared memory objects constructed
74
+ with `create=False` will not. But this method allows to override
75
+ the behavior.
76
+ """
77
+ self._unlink_on_dispose = value
78
+
79
+ def dispose(self) -> None:
80
+ if self._unlink_on_dispose:
81
+ self.unlink()
82
+ else:
83
+ self.close()
84
+
85
+ def __enter__(self) -> "SharedMemory":
86
+ return self
87
+
88
+ def __exit__(self, exc_type, exc_value, exc_tb) -> None:
89
+ self.dispose()
90
+
91
+
92
+ def encode(data: Args) -> str:
93
+ return json.dumps(data, cls=_ApposeJSONEncoder, separators=(",", ":"))
94
+
95
+
96
+ def decode(the_json: str) -> Args:
97
+ return json.loads(the_json, object_hook=_appose_object_hook)
98
+
99
+
100
+ class NDArray:
101
+ """
102
+ Data structure for a multi-dimensional array.
103
+ The array contains elements of a data type, arranged in
104
+ a particular shape, and flattened into SharedMemory.
105
+ """
106
+
107
+ def __init__(self, dtype: str, shape: Sequence[int], shm: SharedMemory = None):
108
+ """
109
+ Create an NDArray.
110
+ :param dtype: The type of the data elements; e.g. int8, uint8, float32, float64.
111
+ :param shape: The dimensional extents; e.g. a stack of 7 image planes
112
+ with resolution 512x512 would have shape [7, 512, 512].
113
+ :param shm: The SharedMemory containing the array data, or None to create it.
114
+ """
115
+ self.dtype = dtype
116
+ self.shape = shape
117
+ self.shm = (
118
+ SharedMemory(
119
+ create=True, size=ceil(prod(shape) * _bytes_per_element(dtype))
120
+ )
121
+ if shm is None
122
+ else shm
123
+ )
124
+
125
+ def __str__(self):
126
+ return (
127
+ f"NDArray("
128
+ f"dtype='{self.dtype}', "
129
+ f"shape={self.shape}, "
130
+ f"shm='{self.shm.name}' ({self.shm.size}))"
131
+ )
132
+
133
+ def ndarray(self):
134
+ """
135
+ Create a NumPy ndarray object for working with the array data.
136
+ No array data is copied; the NumPy array wraps the same SharedMemory.
137
+ Requires the numpy package to be installed.
138
+ """
139
+ try:
140
+ import numpy
141
+
142
+ return numpy.ndarray(
143
+ prod(self.shape), dtype=self.dtype, buffer=self.shm.buf
144
+ ).reshape(self.shape)
145
+ except ModuleNotFoundError:
146
+ raise ImportError("NumPy is not available.")
147
+
148
+ def __enter__(self) -> "NDArray":
149
+ return self
150
+
151
+ def __exit__(self, exc_type, exc_value, exc_tb) -> None:
152
+ self.shm.dispose()
153
+
154
+
155
+ class _ApposeJSONEncoder(json.JSONEncoder):
156
+ def default(self, obj):
157
+ if isinstance(obj, SharedMemory):
158
+ return {
159
+ "appose_type": "shm",
160
+ "name": obj.name,
161
+ "size": obj.size,
162
+ }
163
+ if isinstance(obj, NDArray):
164
+ return {
165
+ "appose_type": "ndarray",
166
+ "dtype": obj.dtype,
167
+ "shape": obj.shape,
168
+ "shm": obj.shm,
169
+ }
170
+ return super().default(obj)
171
+
172
+
173
+ def _appose_object_hook(obj: Dict):
174
+ atype = obj.get("appose_type")
175
+ if atype == "shm":
176
+ # Attach to existing shared memory block.
177
+ return SharedMemory(name=(obj["name"]), size=(obj["size"]))
178
+ elif atype == "ndarray":
179
+ return NDArray(obj["dtype"], obj["shape"], obj["shm"])
180
+ else:
181
+ return obj
182
+
183
+
184
+ def _bytes_per_element(dtype: str) -> Union[int, float]:
185
+ try:
186
+ bits = int(re.sub("[^0-9]", "", dtype))
187
+ except ValueError:
188
+ raise ValueError(f"Invalid dtype: {dtype}")
189
+ return bits / 8
190
+
191
+
192
+ _is_worker = False
193
+
194
+
195
+ def _set_worker(value: bool) -> None:
196
+ global _is_worker
197
+ _is_worker = value
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: appose
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Appose: multi-language interprocess cooperation with shared memory.
5
5
  Author: Appose developers
6
6
  License: Simplified BSD License
@@ -8,7 +8,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-python/issues
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
@@ -27,8 +27,19 @@ 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"
32
43
 
33
44
  # Appose Python
34
45
 
@@ -60,16 +71,6 @@ This is the **Python implementation of Appose**.
60
71
 
61
72
  The name of the package is `appose`.
62
73
 
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
74
  ### PyPI/Pip
74
75
 
75
76
  To use [the PyPI package](https://pypi.org/project/appose),
@@ -87,6 +88,16 @@ dependencies = [
87
88
  ]
88
89
  ```
89
90
 
91
+ ### Conda/Mamba
92
+
93
+ To use [the conda-forge package](https://anaconda.org/conda-forge/appose),
94
+ add `appose` to your `environment.yml`'s `dependencies` section:
95
+
96
+ ```yaml
97
+ dependencies:
98
+ - appose
99
+ ```
100
+
90
101
  ## Examples
91
102
 
92
103
  Here is a minimal example for calling into Java from Python:
@@ -96,12 +107,12 @@ import appose
96
107
  env = appose.java(vendor="zulu", version="17").build()
97
108
  with env.groovy() as groovy:
98
109
  task = groovy.task("5 + 6")
99
- task.waitFor()
100
- result = task.outputs.get("result")
110
+ task.wait_for()
111
+ result = task.outputs["result"]
101
112
  assert 11 == result
102
113
  ```
103
114
 
104
- *Note: The `Appose.java` builder is planned, but not yet implemented.*
115
+ *Note: The `appose.java` builder is planned, but not yet implemented.*
105
116
 
106
117
  Here is an example using a few more of Appose's features:
107
118
 
@@ -158,3 +169,9 @@ with env.groovy() as groovy:
158
169
 
159
170
  Of course, the above examples could have been done all in one language. But
160
171
  hopefully they hint at the possibilities of easy cross-language integration.
172
+
173
+ ## Issue tracker
174
+
175
+ All implementations of Appose use the same issue tracker:
176
+
177
+ https://github.com/apposed/appose/issues
@@ -12,4 +12,6 @@ src/appose.egg-info/SOURCES.txt
12
12
  src/appose.egg-info/dependency_links.txt
13
13
  src/appose.egg-info/requires.txt
14
14
  src/appose.egg-info/top_level.txt
15
- tests/test_appose.py
15
+ tests/test_appose.py
16
+ tests/test_shm.py
17
+ tests/test_types.py
@@ -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 - 2024 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:
@@ -70,6 +70,19 @@ def test_python():
70
70
  execute_and_assert(service, collatz_python)
71
71
 
72
72
 
73
+ def test_service_startup_failure():
74
+ env = appose.base("no-java-to-be-found-here").build()
75
+ try:
76
+ with env.groovy():
77
+ raise AssertionError("Groovy worker process started successfully!?")
78
+ except ValueError as e:
79
+ assert (
80
+ "No executables found amongst candidates: "
81
+ "['java', 'java.exe', 'bin/java', 'bin/java.exe', "
82
+ "'jre/bin/java', 'jre/bin/java.exe']"
83
+ ) == str(e)
84
+
85
+
73
86
  def execute_and_assert(service: Service, script: str):
74
87
  task = service.task(script)
75
88
 
@@ -91,6 +104,7 @@ def execute_and_assert(service: Service, script: str):
91
104
  task.wait_for()
92
105
 
93
106
  # Validate the execution result.
107
+ assert TaskStatus.COMPLETE == task.status
94
108
  result = task.outputs["result"]
95
109
  assert 91 == result
96
110
 
@@ -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 - 2024 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:
@@ -27,15 +27,34 @@
27
27
  # #L%
28
28
  ###
29
29
 
30
- import json
31
- from typing import Any, Dict
30
+ import appose
31
+ from appose.service import TaskStatus
32
32
 
33
- Args = Dict[str, Any]
33
+ ndarray_inspect = """
34
+ task.outputs["size"] = data.shm.size
35
+ task.outputs["dtype"] = data.dtype
36
+ task.outputs["shape"] = data.shape
37
+ task.outputs["sum"] = sum(v for v in data.shm.buf)
38
+ """
34
39
 
35
40
 
36
- def encode(data: Args) -> str:
37
- return json.dumps(data)
41
+ def test_ndarray():
42
+ env = appose.system()
43
+ with env.python() as service:
44
+ with appose.SharedMemory(create=True, size=2 * 2 * 20 * 25) as shm:
45
+ # Construct the data.
46
+ shm.buf[0] = 123
47
+ shm.buf[456] = 78
48
+ shm.buf[1999] = 210
49
+ data = appose.NDArray("uint16", [2, 20, 25], shm)
38
50
 
51
+ # Run the task.
52
+ task = service.task(ndarray_inspect, {"data": data})
53
+ task.wait_for()
39
54
 
40
- def decode(the_json: str) -> Args:
41
- return json.loads(the_json)
55
+ # Validate the execution result.
56
+ assert TaskStatus.COMPLETE == task.status
57
+ assert 2 * 20 * 25 * 2 == task.outputs["size"]
58
+ assert "uint16" == task.outputs["dtype"]
59
+ assert [2, 20, 25] == task.outputs["shape"]
60
+ assert 123 + 78 + 210 == task.outputs["sum"]
@@ -0,0 +1,131 @@
1
+ ###
2
+ # #%L
3
+ # Appose: multi-language interprocess cooperation with shared memory.
4
+ # %%
5
+ # Copyright (C) 2023 - 2024 Appose developers.
6
+ # %%
7
+ # Redistribution and use in source and binary forms, with or without
8
+ # modification, are permitted provided that the following conditions are met:
9
+ #
10
+ # 1. Redistributions of source code must retain the above copyright notice,
11
+ # this list of conditions and the following disclaimer.
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26
+ # POSSIBILITY OF SUCH DAMAGE.
27
+ # #L%
28
+ ###
29
+
30
+ import unittest
31
+
32
+ import appose
33
+
34
+
35
+ class TypesTest(unittest.TestCase):
36
+ JSON = (
37
+ "{"
38
+ '"posByte":123,"negByte":-98,'
39
+ '"posDouble":9.876543210123456,"negDouble":-1.234567890987654e+302,'
40
+ '"posFloat":9.876543,"negFloat":-1.2345678,'
41
+ '"posInt":1234567890,"negInt":-987654321,'
42
+ '"posLong":12345678987654321,"negLong":-98765432123456789,'
43
+ '"posShort":32109,"negShort":-23456,'
44
+ '"trueBoolean":true,"falseBoolean":false,'
45
+ '"nullChar":"\\u0000",'
46
+ '"aString":"-=[]\\\\;\',./_+{}|:\\"<>?'
47
+ "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz"
48
+ '~!@#$%^&*()",'
49
+ '"numbers":[1,1,2,3,5,8],'
50
+ '"words":["quick","brown","fox"],'
51
+ # fmt: off
52
+ '"ndArray":{'
53
+ '"appose_type":"ndarray",' # noqa: E131
54
+ '"dtype":"float32",' # noqa: E131
55
+ '"shape":[2,20,25],' # noqa: E131
56
+ '"shm":{' # noqa: E131
57
+ '"appose_type":"shm",' # noqa: E131
58
+ '"name":"SHM_NAME",' # noqa: E131
59
+ '"size":4000' # noqa: E131
60
+ "}" # noqa: E131
61
+ "}"
62
+ # fmt: on
63
+ "}"
64
+ )
65
+
66
+ STRING = (
67
+ "-=[]\\;',./_+{}|:\"<>?"
68
+ "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz"
69
+ "~!@#$%^&*()"
70
+ )
71
+
72
+ NUMBERS = [1, 1, 2, 3, 5, 8]
73
+
74
+ WORDS = ["quick", "brown", "fox"]
75
+
76
+ def test_encode(self):
77
+ data = {
78
+ "posByte": 123,
79
+ "negByte": -98,
80
+ "posDouble": 9.876543210123456,
81
+ "negDouble": -1.234567890987654e302,
82
+ "posFloat": 9.876543,
83
+ "negFloat": -1.2345678,
84
+ "posInt": 1234567890,
85
+ "negInt": -987654321,
86
+ "posLong": 12345678987654321,
87
+ "negLong": -98765432123456789,
88
+ "posShort": 32109,
89
+ "negShort": -23456,
90
+ "trueBoolean": True,
91
+ "falseBoolean": False,
92
+ "nullChar": "\0",
93
+ "aString": self.STRING,
94
+ "numbers": self.NUMBERS,
95
+ "words": self.WORDS,
96
+ }
97
+ with appose.NDArray("float32", [2, 20, 25]) as ndarray:
98
+ shm_name = ndarray.shm.name
99
+ data["ndArray"] = ndarray
100
+ json_str = appose.types.encode(data)
101
+ self.assertIsNotNone(json_str)
102
+ expected = self.JSON.replace("SHM_NAME", shm_name)
103
+ self.assertEqual(expected, json_str)
104
+
105
+ def test_decode(self):
106
+ with appose.SharedMemory(create=True, size=4000) as shm:
107
+ shm_name = shm.name
108
+ data = appose.types.decode(self.JSON.replace("SHM_NAME", shm_name))
109
+ self.assertIsNotNone(data)
110
+ self.assertEqual(19, len(data))
111
+ self.assertEqual(123, data["posByte"])
112
+ self.assertEqual(-98, data["negByte"])
113
+ self.assertEqual(9.876543210123456, data["posDouble"])
114
+ self.assertEqual(-1.234567890987654e302, data["negDouble"])
115
+ self.assertEqual(9.876543, data["posFloat"])
116
+ self.assertEqual(-1.2345678, data["negFloat"])
117
+ self.assertEqual(1234567890, data["posInt"])
118
+ self.assertEqual(-987654321, data["negInt"])
119
+ self.assertEqual(12345678987654321, data["posLong"])
120
+ self.assertEqual(-98765432123456789, data["negLong"])
121
+ self.assertEqual(32109, data["posShort"])
122
+ self.assertEqual(-23456, data["negShort"])
123
+ self.assertTrue(data["trueBoolean"])
124
+ self.assertFalse(data["falseBoolean"])
125
+ self.assertEqual("\0", data["nullChar"])
126
+ self.assertEqual(self.STRING, data["aString"])
127
+ self.assertEqual(self.NUMBERS, data["numbers"])
128
+ self.assertEqual(self.WORDS, data["words"])
129
+ ndArray = data["ndArray"]
130
+ self.assertEqual("float32", ndArray.dtype)
131
+ self.assertEqual([2, 20, 25], ndArray.shape)
File without changes