polystore 0.2.2__tar.gz → 0.2.4__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 (91) hide show
  1. {polystore-0.2.2/src/polystore.egg-info → polystore-0.2.4}/PKG-INFO +1 -1
  2. {polystore-0.2.2 → polystore-0.2.4}/pyproject.toml +1 -1
  3. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/__init__.py +0 -2
  4. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/bioformats_java.py +30 -7
  5. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/imagej_runtime.py +20 -19
  6. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/omero_tables.py +9 -2
  7. {polystore-0.2.2 → polystore-0.2.4/src/polystore.egg-info}/PKG-INFO +1 -1
  8. {polystore-0.2.2 → polystore-0.2.4}/tests/test_bioformats_java.py +19 -2
  9. {polystore-0.2.2 → polystore-0.2.4}/tests/test_imagej_runtime.py +55 -7
  10. {polystore-0.2.2 → polystore-0.2.4}/tests/test_omero_tables.py +13 -0
  11. {polystore-0.2.2 → polystore-0.2.4}/LICENSE +0 -0
  12. {polystore-0.2.2 → polystore-0.2.4}/README.md +0 -0
  13. {polystore-0.2.2 → polystore-0.2.4}/setup.cfg +0 -0
  14. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/array_payload.py +0 -0
  15. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/async_init.py +0 -0
  16. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/atomic.py +0 -0
  17. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/backend_registry.py +0 -0
  18. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/base.py +0 -0
  19. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/bioformats_storage.py +0 -0
  20. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/config.py +0 -0
  21. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/constants.py +0 -0
  22. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/disk.py +0 -0
  23. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/exceptions.py +0 -0
  24. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/fiji_stream.py +0 -0
  25. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/filemanager.py +0 -0
  26. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/formats.py +0 -0
  27. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/lazy_imports.py +0 -0
  28. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/memory.py +0 -0
  29. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/metadata_migration.py +0 -0
  30. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/metadata_writer.py +0 -0
  31. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/napari_stream.py +0 -0
  32. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/ome_zarr_storage.py +0 -0
  33. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/omero_local.py +0 -0
  34. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/omero_text.py +0 -0
  35. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/roi.py +0 -0
  36. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/roi_converters.py +0 -0
  37. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/streaming/__init__.py +0 -0
  38. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/streaming/_streaming_backend.py +0 -0
  39. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/streaming/base.py +0 -0
  40. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/streaming/handlers/__init__.py +0 -0
  41. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/streaming/handlers/fiji_images.py +0 -0
  42. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/streaming/handlers/fiji_rois.py +0 -0
  43. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/streaming/identity.py +0 -0
  44. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/streaming/receivers/__init__.py +0 -0
  45. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/streaming/receivers/batching/__init__.py +0 -0
  46. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/streaming/receivers/core/__init__.py +0 -0
  47. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/streaming/receivers/core/batch_engine.py +0 -0
  48. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/streaming/receivers/core/contracts.py +0 -0
  49. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/streaming/receivers/core/window_projection.py +0 -0
  50. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/streaming/receivers/fiji/__init__.py +0 -0
  51. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/streaming/receivers/fiji/fiji_batch_processor.py +0 -0
  52. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/streaming/receivers/napari/__init__.py +0 -0
  53. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/streaming/receivers/napari/layer_key.py +0 -0
  54. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/streaming/receivers/napari/napari_batch_processor.py +0 -0
  55. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/streaming/viewer_transport.py +0 -0
  56. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/streaming_constants.py +0 -0
  57. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/utils.py +0 -0
  58. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/virtual_workspace.py +0 -0
  59. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/zarr.py +0 -0
  60. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/zarr_batch.py +0 -0
  61. {polystore-0.2.2 → polystore-0.2.4}/src/polystore/zmq_config.py +0 -0
  62. {polystore-0.2.2 → polystore-0.2.4}/src/polystore.egg-info/SOURCES.txt +0 -0
  63. {polystore-0.2.2 → polystore-0.2.4}/src/polystore.egg-info/dependency_links.txt +0 -0
  64. {polystore-0.2.2 → polystore-0.2.4}/src/polystore.egg-info/requires.txt +0 -0
  65. {polystore-0.2.2 → polystore-0.2.4}/src/polystore.egg-info/top_level.txt +0 -0
  66. {polystore-0.2.2 → polystore-0.2.4}/tests/test_array_payload.py +0 -0
  67. {polystore-0.2.2 → polystore-0.2.4}/tests/test_backend_registry.py +0 -0
  68. {polystore-0.2.2 → polystore-0.2.4}/tests/test_basic.py +0 -0
  69. {polystore-0.2.2 → polystore-0.2.4}/tests/test_bioformats_storage.py +0 -0
  70. {polystore-0.2.2 → polystore-0.2.4}/tests/test_disk_backend.py +0 -0
  71. {polystore-0.2.2 → polystore-0.2.4}/tests/test_disk_coverage.py +0 -0
  72. {polystore-0.2.2 → polystore-0.2.4}/tests/test_disk_more.py +0 -0
  73. {polystore-0.2.2 → polystore-0.2.4}/tests/test_filemanager_backends.py +0 -0
  74. {polystore-0.2.2 → polystore-0.2.4}/tests/test_filemanager_extended.py +0 -0
  75. {polystore-0.2.2 → polystore-0.2.4}/tests/test_lazy_package_exports.py +0 -0
  76. {polystore-0.2.2 → polystore-0.2.4}/tests/test_memory_backend.py +0 -0
  77. {polystore-0.2.2 → polystore-0.2.4}/tests/test_memory_coverage.py +0 -0
  78. {polystore-0.2.2 → polystore-0.2.4}/tests/test_memory_type_authority.py +0 -0
  79. {polystore-0.2.2 → polystore-0.2.4}/tests/test_napari_batch_processor.py +0 -0
  80. {polystore-0.2.2 → polystore-0.2.4}/tests/test_ome_zarr_storage.py +0 -0
  81. {polystore-0.2.2 → polystore-0.2.4}/tests/test_omero_image_plane_batch.py +0 -0
  82. {polystore-0.2.2 → polystore-0.2.4}/tests/test_omero_text.py +0 -0
  83. {polystore-0.2.2 → polystore-0.2.4}/tests/test_png_format.py +0 -0
  84. {polystore-0.2.2 → polystore-0.2.4}/tests/test_roi.py +0 -0
  85. {polystore-0.2.2 → polystore-0.2.4}/tests/test_streaming_identity.py +0 -0
  86. {polystore-0.2.2 → polystore-0.2.4}/tests/test_streaming_metadata.py +0 -0
  87. {polystore-0.2.2 → polystore-0.2.4}/tests/test_streaming_receiver_core.py +0 -0
  88. {polystore-0.2.2 → polystore-0.2.4}/tests/test_streaming_shared_memory.py +0 -0
  89. {polystore-0.2.2 → polystore-0.2.4}/tests/test_viewer_transport.py +0 -0
  90. {polystore-0.2.2 → polystore-0.2.4}/tests/test_zarr_backend.py +0 -0
  91. {polystore-0.2.2 → polystore-0.2.4}/tests/test_zarr_batch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polystore
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Framework-agnostic multi-backend storage abstraction for ML and scientific computing
5
5
  Author-email: Tristan Simas <tristan.simas@mail.mcgill.ca>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "polystore"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "Framework-agnostic multi-backend storage abstraction for ML and scientific computing"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -40,7 +40,6 @@ from .filemanager import FileManager
40
40
  from .formats import DEFAULT_IMAGE_EXTENSIONS, FileFormat
41
41
  from .imagej_runtime import (
42
42
  FIJI_IMAGEJ_RUNTIME,
43
- ImageJJvmTeardown,
44
43
  ImageJRuntimePolicy,
45
44
  ImageJRuntimeUnavailableError,
46
45
  )
@@ -97,7 +96,6 @@ __all__ = [
97
96
  "Backend",
98
97
  "MemoryType",
99
98
  "FileFormat",
100
- "ImageJJvmTeardown",
101
99
  "ImageJRuntimePolicy",
102
100
  "ImageJRuntimeUnavailableError",
103
101
  "FIJI_IMAGEJ_RUNTIME",
@@ -93,17 +93,30 @@ class BioFormatsJavaContext:
93
93
  self.FormatTools = format_tools
94
94
 
95
95
  def dispose(self) -> None:
96
- """Dispose the owned ImageJ gateway before interpreter shutdown."""
96
+ """Dispose the current ImageJ gateway while keeping the JVM reusable."""
97
97
 
98
98
  with self._lifecycle_lock:
99
- gateway = self.ij
100
- self.ij = None
101
- self.ImageReader = None
102
- self.MetadataTools = None
103
- self.FormatTools = None
99
+ gateway = self._detach_gateway()
104
100
  if gateway is not None:
105
101
  gateway.dispose()
106
102
 
103
+ def shutdown(self) -> None:
104
+ """Stop the process-owned ImageJ context and JVM exactly once."""
105
+
106
+ with self._lifecycle_lock:
107
+ gateway = self._detach_gateway()
108
+ FIJI_IMAGEJ_RUNTIME.shutdown(gateway, self.scyjava)
109
+
110
+ def _detach_gateway(self) -> Any | None:
111
+ """Clear and return the currently owned ImageJ gateway."""
112
+
113
+ gateway = self.ij
114
+ self.ij = None
115
+ self.ImageReader = None
116
+ self.MetadataTools = None
117
+ self.FormatTools = None
118
+ return gateway
119
+
107
120
  @classmethod
108
121
  def dispose_instance(cls) -> None:
109
122
  """Dispose the initialized process-wide context without creating it."""
@@ -113,6 +126,16 @@ class BioFormatsJavaContext:
113
126
  if context is not None:
114
127
  context.dispose()
115
128
 
129
+ @classmethod
130
+ def shutdown_instance(cls) -> None:
131
+ """Stop and release the initialized process-wide context without creating it."""
132
+
133
+ with cls._lock:
134
+ context = cls._instance
135
+ cls._instance = None
136
+ if context is not None:
137
+ context.shutdown()
138
+
116
139
  def open_reader(self, source_path: str | Path) -> BioFormatsOpenedReader:
117
140
  self.ensure_initialized()
118
141
  metadata = self.MetadataTools.createOMEXMLMetadata()
@@ -136,7 +159,7 @@ class BioFormatsJavaContext:
136
159
  reader.close()
137
160
 
138
161
 
139
- register_cleanup_callback(BioFormatsJavaContext.dispose_instance)
162
+ register_cleanup_callback(BioFormatsJavaContext.shutdown_instance)
140
163
 
141
164
 
142
165
  def java_int(value: Any) -> int | None:
@@ -5,7 +5,6 @@ from __future__ import annotations
5
5
  import logging
6
6
  import time
7
7
  from dataclasses import dataclass
8
- from enum import Enum
9
8
  from typing import Any
10
9
 
11
10
  logger = logging.getLogger(__name__)
@@ -15,29 +14,14 @@ class ImageJRuntimeUnavailableError(RuntimeError):
15
14
  """Raised when an ImageJ distribution cannot use its declared Java runtime."""
16
15
 
17
16
 
18
- class ImageJJvmTeardown(Enum):
19
- """JPype teardown behavior owned by an ImageJ process runtime."""
20
-
21
- PROCESS_EXIT = False
22
- DESTROY_JVM = True
23
-
24
- def configure(self) -> None:
25
- """Apply this teardown behavior before the process starts its JVM."""
26
-
27
- import jpype.config
28
-
29
- jpype.config.destroy_jvm = self.value
30
-
31
-
32
17
  @dataclass(frozen=True, slots=True)
33
18
  class ImageJRuntimePolicy:
34
- """Own the endpoint and managed Java requirement for an ImageJ runtime."""
19
+ """Own an ImageJ endpoint, managed Java requirement, and process lifecycle."""
35
20
 
36
21
  endpoint: str
37
22
  java_fetch: str
38
23
  java_vendor: str
39
24
  java_version: str
40
- jvm_teardown: ImageJJvmTeardown
41
25
  initialization_retry_delays_seconds: tuple[float, ...] = ()
42
26
 
43
27
  @property
@@ -82,7 +66,7 @@ class ImageJRuntimePolicy:
82
66
  def configure_java(self, scyjava_module: Any) -> None:
83
67
  """Select managed Java before startup or validate the active JVM."""
84
68
 
85
- self.jvm_teardown.configure()
69
+ self._configure_controlled_jvm_shutdown()
86
70
  if scyjava_module.jvm_started():
87
71
  self.require_compatible_active_java(scyjava_module)
88
72
  return
@@ -92,6 +76,24 @@ class ImageJRuntimePolicy:
92
76
  version=self.java_version,
93
77
  )
94
78
 
79
+ def shutdown(self, gateway: Any | None, scyjava_module: Any) -> None:
80
+ """Dispose the ImageJ gateway and stop its JVM before Python finalization."""
81
+
82
+ self._configure_controlled_jvm_shutdown()
83
+ try:
84
+ if gateway is not None:
85
+ gateway.dispose()
86
+ finally:
87
+ scyjava_module.shutdown_jvm()
88
+
89
+ @staticmethod
90
+ def _configure_controlled_jvm_shutdown() -> None:
91
+ """Require explicit JVM destruction while Python runtime state is valid."""
92
+
93
+ import jpype.config
94
+
95
+ jpype.config.destroy_jvm = True
96
+
95
97
  def require_compatible_active_java(self, scyjava_module: Any) -> None:
96
98
  """Reject a JVM whose feature version differs from this declaration."""
97
99
 
@@ -122,6 +124,5 @@ FIJI_IMAGEJ_RUNTIME = ImageJRuntimePolicy(
122
124
  java_fetch="always",
123
125
  java_vendor="zulu-jre",
124
126
  java_version="21",
125
- jvm_teardown=ImageJJvmTeardown.PROCESS_EXIT,
126
127
  initialization_retry_delays_seconds=(1.0, 2.0),
127
128
  )
@@ -122,8 +122,8 @@ class OMEROTableService:
122
122
 
123
123
  return bool(connection.c.sf.sharedResources().areTablesEnabled())
124
124
 
125
- def create_table(self, connection: Any, path: str) -> Any:
126
- """Create one table through a ready service and repository declaration."""
125
+ def wait_until_available(self, connection: Any) -> Any:
126
+ """Return shared resources after the declared table capability is ready."""
127
127
 
128
128
  resources = connection.c.sf.sharedResources()
129
129
  retry_delays = iter(self.readiness_retry_delays_seconds)
@@ -139,6 +139,13 @@ class OMEROTableService:
139
139
  )
140
140
  time.sleep(retry_delay)
141
141
 
142
+ return resources
143
+
144
+ def create_table(self, connection: Any, path: str) -> Any:
145
+ """Create one table through a ready service and repository declaration."""
146
+
147
+ resources = self.wait_until_available(connection)
148
+
142
149
  repository_map = resources.repositories()
143
150
  descriptions = repository_map.descriptions
144
151
  if not descriptions:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polystore
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Framework-agnostic multi-backend storage abstraction for ML and scientific computing
5
5
  Author-email: Tristan Simas <tristan.simas@mail.mcgill.ca>
6
6
  License-Expression: MIT
@@ -23,6 +23,7 @@ class _Gateway:
23
23
  class _Runtime:
24
24
  def __init__(self) -> None:
25
25
  self.gateways: list[_Gateway] = []
26
+ self.shutdown_gateways: list[_Gateway | None] = []
26
27
 
27
28
  def initialize(self, imagej_module, scyjava_module, *, mode: str) -> _Gateway:
28
29
  assert imagej_module == "imagej"
@@ -32,11 +33,18 @@ class _Runtime:
32
33
  self.gateways.append(gateway)
33
34
  return gateway
34
35
 
36
+ def shutdown(self, gateway: _Gateway | None, scyjava_module) -> None:
37
+ self.shutdown_gateways.append(gateway)
38
+ if gateway is not None:
39
+ gateway.dispose()
40
+ scyjava_module.shutdown_jvm()
41
+
35
42
 
36
43
  class _ScyJava:
37
44
  def __init__(self, *, failing_import: str | None = None) -> None:
38
45
  self.failing_import = failing_import
39
46
  self.imports: list[str] = []
47
+ self.shutdown_count = 0
40
48
 
41
49
  def jimport(self, name: str) -> object:
42
50
  self.imports.append(name)
@@ -44,6 +52,9 @@ class _ScyJava:
44
52
  raise RuntimeError(f"could not import {name}")
45
53
  return object()
46
54
 
55
+ def shutdown_jvm(self) -> None:
56
+ self.shutdown_count += 1
57
+
47
58
 
48
59
  def test_bioformats_context_disposes_and_can_reinitialize(monkeypatch) -> None:
49
60
  runtime = _Runtime()
@@ -98,8 +109,11 @@ def test_dispose_instance_does_not_create_process_context(monkeypatch) -> None:
98
109
  assert BioFormatsJavaContext._instance is None
99
110
 
100
111
 
101
- def test_test_runtime_cleanup_disposes_process_context(monkeypatch) -> None:
102
- context = BioFormatsJavaContext("imagej", _ScyJava())
112
+ def test_test_runtime_cleanup_stops_process_context(monkeypatch) -> None:
113
+ runtime = _Runtime()
114
+ scyjava = _ScyJava()
115
+ monkeypatch.setattr(bioformats_java, "FIJI_IMAGEJ_RUNTIME", runtime)
116
+ context = BioFormatsJavaContext("imagej", scyjava)
103
117
  gateway = _Gateway()
104
118
  context.ij = gateway
105
119
  monkeypatch.setattr(BioFormatsJavaContext, "_instance", context)
@@ -108,3 +122,6 @@ def test_test_runtime_cleanup_disposes_process_context(monkeypatch) -> None:
108
122
 
109
123
  assert gateway.dispose_count == 1
110
124
  assert context.ij is None
125
+ assert runtime.shutdown_gateways == [gateway]
126
+ assert scyjava.shutdown_count == 1
127
+ assert BioFormatsJavaContext._instance is None
@@ -6,7 +6,6 @@ import pytest
6
6
 
7
7
  from polystore.imagej_runtime import (
8
8
  FIJI_IMAGEJ_RUNTIME,
9
- ImageJJvmTeardown,
10
9
  ImageJRuntimePolicy,
11
10
  ImageJRuntimeUnavailableError,
12
11
  )
@@ -25,6 +24,7 @@ class _ScyJava:
25
24
  def __init__(self, *, started: bool, java_version: str) -> None:
26
25
  self._started = started
27
26
  self.constraints: list[dict[str, str]] = []
27
+ self.shutdown_count = 0
28
28
  self.config = SimpleNamespace(
29
29
  set_java_constraints=lambda **constraints: self.constraints.append(constraints)
30
30
  )
@@ -37,12 +37,27 @@ class _ScyJava:
37
37
  assert name == "java.lang.System"
38
38
  return self._system
39
39
 
40
+ def shutdown_jvm(self) -> None:
41
+ self.shutdown_count += 1
42
+ self._started = False
43
+
44
+
45
+ class _Gateway:
46
+ def __init__(self, events: list[str] | None = None) -> None:
47
+ self.dispose_count = 0
48
+ self.events = events
49
+
50
+ def dispose(self) -> None:
51
+ self.dispose_count += 1
52
+ if self.events is not None:
53
+ self.events.append("gateway_disposed")
54
+
40
55
 
41
56
  class _ImageJ:
42
57
  def __init__(self, scyjava: _ScyJava) -> None:
43
58
  self._scyjava = scyjava
44
59
  self.calls: list[tuple[str, str]] = []
45
- self.gateway = object()
60
+ self.gateway = _Gateway()
46
61
 
47
62
  def init(self, endpoint: str, *, mode: str) -> object:
48
63
  self.calls.append((endpoint, mode))
@@ -53,7 +68,7 @@ class _ImageJ:
53
68
  def test_fiji_runtime_selects_managed_java_before_initialization(monkeypatch) -> None:
54
69
  import jpype.config
55
70
 
56
- monkeypatch.setattr(jpype.config, "destroy_jvm", True)
71
+ monkeypatch.setattr(jpype.config, "destroy_jvm", False)
57
72
  scyjava = _ScyJava(started=False, java_version="21.0.8")
58
73
  imagej = _ImageJ(scyjava)
59
74
 
@@ -62,7 +77,43 @@ def test_fiji_runtime_selects_managed_java_before_initialization(monkeypatch) ->
62
77
  assert gateway is imagej.gateway
63
78
  assert scyjava.constraints == [{"fetch": "always", "vendor": "zulu-jre", "version": "21"}]
64
79
  assert imagej.calls == [("sc.fiji:fiji", "headless")]
65
- assert jpype.config.destroy_jvm is False
80
+ assert jpype.config.destroy_jvm is True
81
+
82
+
83
+ def test_fiji_runtime_shuts_down_gateway_before_jvm(monkeypatch) -> None:
84
+ import jpype.config
85
+
86
+ events: list[str] = []
87
+ gateway = _Gateway(events)
88
+ scyjava = _ScyJava(started=True, java_version="21")
89
+ original_shutdown = scyjava.shutdown_jvm
90
+
91
+ def record_shutdown() -> None:
92
+ events.append("jvm_shutdown")
93
+ original_shutdown()
94
+
95
+ scyjava.shutdown_jvm = record_shutdown
96
+ monkeypatch.setattr(jpype.config, "destroy_jvm", False)
97
+
98
+ FIJI_IMAGEJ_RUNTIME.shutdown(gateway, scyjava)
99
+
100
+ assert events == ["gateway_disposed", "jvm_shutdown"]
101
+ assert gateway.dispose_count == 1
102
+ assert scyjava.shutdown_count == 1
103
+ assert jpype.config.destroy_jvm is True
104
+
105
+
106
+ def test_fiji_runtime_stops_jvm_after_gateway_disposal_failure() -> None:
107
+ class _FailingGateway:
108
+ def dispose(self) -> None:
109
+ raise RuntimeError("gateway disposal failed")
110
+
111
+ scyjava = _ScyJava(started=True, java_version="21")
112
+
113
+ with pytest.raises(RuntimeError, match="gateway disposal failed"):
114
+ FIJI_IMAGEJ_RUNTIME.shutdown(_FailingGateway(), scyjava)
115
+
116
+ assert scyjava.shutdown_count == 1
66
117
 
67
118
 
68
119
  def test_runtime_accepts_compatible_active_java_without_reconfiguration() -> None:
@@ -95,7 +146,6 @@ def test_runtime_reports_an_unparseable_active_java_version() -> None:
95
146
  java_fetch="always",
96
147
  java_vendor="example-jre",
97
148
  java_version="21",
98
- jvm_teardown=ImageJJvmTeardown.PROCESS_EXIT,
99
149
  )
100
150
  scyjava = _ScyJava(started=True, java_version="unknown")
101
151
 
@@ -138,7 +188,6 @@ def test_runtime_retries_initialization_only_before_jvm_start(monkeypatch) -> No
138
188
  java_fetch="always",
139
189
  java_vendor="example-jre",
140
190
  java_version="21",
141
- jvm_teardown=ImageJJvmTeardown.PROCESS_EXIT,
142
191
  initialization_retry_delays_seconds=(0.1, 0.2),
143
192
  )
144
193
  scyjava = _ScyJava(started=False, java_version="21")
@@ -169,7 +218,6 @@ def test_runtime_preserves_final_failure_after_retry_schedule() -> None:
169
218
  java_fetch="always",
170
219
  java_vendor="example-jre",
171
220
  java_version="21",
172
- jvm_teardown=ImageJJvmTeardown.PROCESS_EXIT,
173
221
  initialization_retry_delays_seconds=(0.0, 0.0),
174
222
  )
175
223
  scyjava = _ScyJava(started=False, java_version="21")
@@ -142,6 +142,19 @@ def test_table_service_waits_on_capability_instead_of_exception_text(
142
142
  assert resources.created_tables == [(7, "measurements.h5")]
143
143
 
144
144
 
145
+ def test_table_service_exposes_declared_readiness_wait(monkeypatch) -> None:
146
+ observed_delays: list[float] = []
147
+ monkeypatch.setattr("polystore.omero_tables.time.sleep", observed_delays.append)
148
+ resources = _Resources([False, True])
149
+
150
+ ready_resources = OMEROTableService(
151
+ readiness_retry_delays_seconds=(0.1,),
152
+ ).wait_until_available(_connection(resources))
153
+
154
+ assert ready_resources is resources
155
+ assert observed_delays == [0.1]
156
+
157
+
145
158
  def test_table_service_fails_before_create_when_capability_stays_unavailable() -> None:
146
159
  resources = _Resources([False])
147
160
 
File without changes
File without changes
File without changes
File without changes
File without changes