polystore 0.2.0__tar.gz → 0.2.2__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.0/src/polystore.egg-info → polystore-0.2.2}/PKG-INFO +2 -1
  2. {polystore-0.2.0 → polystore-0.2.2}/pyproject.toml +2 -1
  3. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/__init__.py +2 -0
  4. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/disk.py +11 -8
  5. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/imagej_runtime.py +18 -0
  6. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/omero_local.py +4 -26
  7. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/omero_tables.py +96 -0
  8. {polystore-0.2.0 → polystore-0.2.2/src/polystore.egg-info}/PKG-INFO +2 -1
  9. {polystore-0.2.0 → polystore-0.2.2}/src/polystore.egg-info/requires.txt +1 -0
  10. {polystore-0.2.0 → polystore-0.2.2}/tests/test_disk_backend.py +18 -0
  11. {polystore-0.2.0 → polystore-0.2.2}/tests/test_imagej_runtime.py +9 -1
  12. {polystore-0.2.0 → polystore-0.2.2}/tests/test_omero_tables.py +62 -1
  13. {polystore-0.2.0 → polystore-0.2.2}/LICENSE +0 -0
  14. {polystore-0.2.0 → polystore-0.2.2}/README.md +0 -0
  15. {polystore-0.2.0 → polystore-0.2.2}/setup.cfg +0 -0
  16. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/array_payload.py +0 -0
  17. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/async_init.py +0 -0
  18. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/atomic.py +0 -0
  19. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/backend_registry.py +0 -0
  20. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/base.py +0 -0
  21. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/bioformats_java.py +0 -0
  22. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/bioformats_storage.py +0 -0
  23. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/config.py +0 -0
  24. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/constants.py +0 -0
  25. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/exceptions.py +0 -0
  26. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/fiji_stream.py +0 -0
  27. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/filemanager.py +0 -0
  28. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/formats.py +0 -0
  29. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/lazy_imports.py +0 -0
  30. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/memory.py +0 -0
  31. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/metadata_migration.py +0 -0
  32. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/metadata_writer.py +0 -0
  33. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/napari_stream.py +0 -0
  34. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/ome_zarr_storage.py +0 -0
  35. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/omero_text.py +0 -0
  36. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/roi.py +0 -0
  37. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/roi_converters.py +0 -0
  38. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/streaming/__init__.py +0 -0
  39. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/streaming/_streaming_backend.py +0 -0
  40. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/streaming/base.py +0 -0
  41. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/streaming/handlers/__init__.py +0 -0
  42. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/streaming/handlers/fiji_images.py +0 -0
  43. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/streaming/handlers/fiji_rois.py +0 -0
  44. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/streaming/identity.py +0 -0
  45. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/streaming/receivers/__init__.py +0 -0
  46. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/streaming/receivers/batching/__init__.py +0 -0
  47. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/streaming/receivers/core/__init__.py +0 -0
  48. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/streaming/receivers/core/batch_engine.py +0 -0
  49. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/streaming/receivers/core/contracts.py +0 -0
  50. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/streaming/receivers/core/window_projection.py +0 -0
  51. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/streaming/receivers/fiji/__init__.py +0 -0
  52. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/streaming/receivers/fiji/fiji_batch_processor.py +0 -0
  53. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/streaming/receivers/napari/__init__.py +0 -0
  54. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/streaming/receivers/napari/layer_key.py +0 -0
  55. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/streaming/receivers/napari/napari_batch_processor.py +0 -0
  56. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/streaming/viewer_transport.py +0 -0
  57. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/streaming_constants.py +0 -0
  58. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/utils.py +0 -0
  59. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/virtual_workspace.py +0 -0
  60. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/zarr.py +0 -0
  61. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/zarr_batch.py +0 -0
  62. {polystore-0.2.0 → polystore-0.2.2}/src/polystore/zmq_config.py +0 -0
  63. {polystore-0.2.0 → polystore-0.2.2}/src/polystore.egg-info/SOURCES.txt +0 -0
  64. {polystore-0.2.0 → polystore-0.2.2}/src/polystore.egg-info/dependency_links.txt +0 -0
  65. {polystore-0.2.0 → polystore-0.2.2}/src/polystore.egg-info/top_level.txt +0 -0
  66. {polystore-0.2.0 → polystore-0.2.2}/tests/test_array_payload.py +0 -0
  67. {polystore-0.2.0 → polystore-0.2.2}/tests/test_backend_registry.py +0 -0
  68. {polystore-0.2.0 → polystore-0.2.2}/tests/test_basic.py +0 -0
  69. {polystore-0.2.0 → polystore-0.2.2}/tests/test_bioformats_java.py +0 -0
  70. {polystore-0.2.0 → polystore-0.2.2}/tests/test_bioformats_storage.py +0 -0
  71. {polystore-0.2.0 → polystore-0.2.2}/tests/test_disk_coverage.py +0 -0
  72. {polystore-0.2.0 → polystore-0.2.2}/tests/test_disk_more.py +0 -0
  73. {polystore-0.2.0 → polystore-0.2.2}/tests/test_filemanager_backends.py +0 -0
  74. {polystore-0.2.0 → polystore-0.2.2}/tests/test_filemanager_extended.py +0 -0
  75. {polystore-0.2.0 → polystore-0.2.2}/tests/test_lazy_package_exports.py +0 -0
  76. {polystore-0.2.0 → polystore-0.2.2}/tests/test_memory_backend.py +0 -0
  77. {polystore-0.2.0 → polystore-0.2.2}/tests/test_memory_coverage.py +0 -0
  78. {polystore-0.2.0 → polystore-0.2.2}/tests/test_memory_type_authority.py +0 -0
  79. {polystore-0.2.0 → polystore-0.2.2}/tests/test_napari_batch_processor.py +0 -0
  80. {polystore-0.2.0 → polystore-0.2.2}/tests/test_ome_zarr_storage.py +0 -0
  81. {polystore-0.2.0 → polystore-0.2.2}/tests/test_omero_image_plane_batch.py +0 -0
  82. {polystore-0.2.0 → polystore-0.2.2}/tests/test_omero_text.py +0 -0
  83. {polystore-0.2.0 → polystore-0.2.2}/tests/test_png_format.py +0 -0
  84. {polystore-0.2.0 → polystore-0.2.2}/tests/test_roi.py +0 -0
  85. {polystore-0.2.0 → polystore-0.2.2}/tests/test_streaming_identity.py +0 -0
  86. {polystore-0.2.0 → polystore-0.2.2}/tests/test_streaming_metadata.py +0 -0
  87. {polystore-0.2.0 → polystore-0.2.2}/tests/test_streaming_receiver_core.py +0 -0
  88. {polystore-0.2.0 → polystore-0.2.2}/tests/test_streaming_shared_memory.py +0 -0
  89. {polystore-0.2.0 → polystore-0.2.2}/tests/test_viewer_transport.py +0 -0
  90. {polystore-0.2.0 → polystore-0.2.2}/tests/test_zarr_backend.py +0 -0
  91. {polystore-0.2.0 → polystore-0.2.2}/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.0
3
+ Version: 0.2.2
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
@@ -58,6 +58,7 @@ Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
58
58
  Requires-Dist: coverage>=7.3.2; extra == "dev"
59
59
  Requires-Dist: genbadge[coverage]; extra == "dev"
60
60
  Requires-Dist: pandas>=2.3.1; extra == "dev"
61
+ Requires-Dist: jpype1>=1.4.0; extra == "dev"
61
62
  Requires-Dist: black>=23.0.0; extra == "dev"
62
63
  Requires-Dist: ruff>=0.1.0; extra == "dev"
63
64
  Provides-Extra: docs
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "polystore"
7
- version = "0.2.0"
7
+ version = "0.2.2"
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"
@@ -94,6 +94,7 @@ dev = [
94
94
  "coverage>=7.3.2",
95
95
  "genbadge[coverage]",
96
96
  "pandas>=2.3.1",
97
+ "jpype1>=1.4.0",
97
98
  "black>=23.0.0",
98
99
  "ruff>=0.1.0",
99
100
  ]
@@ -40,6 +40,7 @@ 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,
43
44
  ImageJRuntimePolicy,
44
45
  ImageJRuntimeUnavailableError,
45
46
  )
@@ -96,6 +97,7 @@ __all__ = [
96
97
  "Backend",
97
98
  "MemoryType",
98
99
  "FileFormat",
100
+ "ImageJJvmTeardown",
99
101
  "ImageJRuntimePolicy",
100
102
  "ImageJRuntimeUnavailableError",
101
103
  "FIJI_IMAGEJ_RUNTIME",
@@ -24,6 +24,7 @@ from .exceptions import StorageResolutionError
24
24
  from .formats import FileFormat
25
25
 
26
26
  logger = logging.getLogger(__name__)
27
+ DISK_TEXT_ENCODING = "utf-8"
27
28
 
28
29
 
29
30
  def optional_import(module_name):
@@ -293,34 +294,36 @@ class DiskStorageBackend(StorageBackend):
293
294
  return numeric_arrays[0]
294
295
 
295
296
  def _text_writer(self, path, data, **kwargs):
296
- """Write text data to file. Accepts and ignores extra kwargs for compatibility."""
297
- path.write_text(str(data))
297
+ """Write UTF-8 text bytes without platform newline translation."""
298
+
299
+ del kwargs
300
+ path.write_bytes(str(data).encode(DISK_TEXT_ENCODING))
298
301
 
299
302
  def _text_reader(self, path):
300
- return path.read_text()
303
+ return path.read_text(encoding=DISK_TEXT_ENCODING)
301
304
 
302
305
  def _json_writer(self, path, data, **kwargs):
303
306
  import json
304
307
 
305
308
  # Ensure parent directory exists
306
309
  path.parent.mkdir(parents=True, exist_ok=True)
307
- path.write_text(json.dumps(data, indent=2))
310
+ self._text_writer(path, json.dumps(data, indent=2))
308
311
 
309
312
  def _json_reader(self, path):
310
313
  import json
311
314
 
312
- return json.loads(path.read_text())
315
+ return json.loads(self._text_reader(path))
313
316
 
314
317
  def _csv_writer(self, path, data, **kwargs):
315
318
  import csv
316
319
 
317
320
  # Handle pre-formatted CSV strings (from pandas to_csv)
318
321
  if isinstance(data, str):
319
- path.write_text(data)
322
+ self._text_writer(path, data)
320
323
  return
321
324
 
322
325
  # Assume data is a list of rows or a dict
323
- with path.open("w", newline="") as f:
326
+ with path.open("w", newline="", encoding=DISK_TEXT_ENCODING) as f:
324
327
  if isinstance(data, dict):
325
328
  # Write dict as CSV with headers
326
329
  writer = csv.DictWriter(f, fieldnames=data.keys())
@@ -355,7 +358,7 @@ class DiskStorageBackend(StorageBackend):
355
358
  def _csv_reader(self, path):
356
359
  import csv
357
360
 
358
- with path.open("r", newline="") as f:
361
+ with path.open("r", newline="", encoding=DISK_TEXT_ENCODING) as f:
359
362
  reader = csv.DictReader(f)
360
363
  return list(reader)
361
364
 
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import logging
6
6
  import time
7
7
  from dataclasses import dataclass
8
+ from enum import Enum
8
9
  from typing import Any
9
10
 
10
11
  logger = logging.getLogger(__name__)
@@ -14,6 +15,20 @@ class ImageJRuntimeUnavailableError(RuntimeError):
14
15
  """Raised when an ImageJ distribution cannot use its declared Java runtime."""
15
16
 
16
17
 
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
+
17
32
  @dataclass(frozen=True, slots=True)
18
33
  class ImageJRuntimePolicy:
19
34
  """Own the endpoint and managed Java requirement for an ImageJ runtime."""
@@ -22,6 +37,7 @@ class ImageJRuntimePolicy:
22
37
  java_fetch: str
23
38
  java_vendor: str
24
39
  java_version: str
40
+ jvm_teardown: ImageJJvmTeardown
25
41
  initialization_retry_delays_seconds: tuple[float, ...] = ()
26
42
 
27
43
  @property
@@ -66,6 +82,7 @@ class ImageJRuntimePolicy:
66
82
  def configure_java(self, scyjava_module: Any) -> None:
67
83
  """Select managed Java before startup or validate the active JVM."""
68
84
 
85
+ self.jvm_teardown.configure()
69
86
  if scyjava_module.jvm_started():
70
87
  self.require_compatible_active_java(scyjava_module)
71
88
  return
@@ -105,5 +122,6 @@ FIJI_IMAGEJ_RUNTIME = ImageJRuntimePolicy(
105
122
  java_fetch="always",
106
123
  java_vendor="zulu-jre",
107
124
  java_version="21",
125
+ jvm_teardown=ImageJJvmTeardown.PROCESS_EXIT,
108
126
  initialization_retry_delays_seconds=(1.0, 2.0),
109
127
  )
@@ -28,7 +28,7 @@ import numpy as np
28
28
 
29
29
  from .array_payload import storage_numpy_array
30
30
  from .base import PicklableBackend, VirtualBackend
31
- from .omero_tables import OMERO_TABLE_SERVICE
31
+ from .omero_tables import OMERO_TABLE_SERVICE, OMEROTableColumnType
32
32
  from .omero_text import OMEROTextFormat
33
33
 
34
34
  logger = logging.getLogger(__name__)
@@ -616,7 +616,6 @@ class OMEROLocalBackend(VirtualBackend, PicklableBackend):
616
616
  from io import StringIO
617
617
 
618
618
  import pandas as pd
619
- from omero.grid import BoolColumn, DoubleColumn, LongColumn, StringColumn
620
619
  from omero.model import FileAnnotationI
621
620
  from omero.rtypes import rstring
622
621
 
@@ -650,30 +649,9 @@ class OMEROLocalBackend(VirtualBackend, PicklableBackend):
650
649
  # OMERO table names cannot contain dots except for the .h5 extension
651
650
  table_name = output_path.name.split(".")[0]
652
651
 
653
- # Build column objects based on DataFrame dtypes
654
- columns = []
655
- for col_name in df.columns:
656
- col_data = df[col_name]
657
-
658
- # Determine column type from pandas dtype
659
- if pd.api.types.is_integer_dtype(col_data):
660
- col = LongColumn(col_name, "", [])
661
- col.values = col_data.astype(int).tolist()
662
- elif pd.api.types.is_float_dtype(col_data):
663
- col = DoubleColumn(col_name, "", [])
664
- col.values = col_data.astype(float).tolist()
665
- elif pd.api.types.is_bool_dtype(col_data):
666
- col = BoolColumn(col_name, "", [])
667
- col.values = col_data.astype(bool).tolist()
668
- else:
669
- # Default to string column
670
- # Calculate max string length (OMERO requires size > 0)
671
- str_values = col_data.astype(str).tolist()
672
- max_len = max(len(s) for s in str_values) if str_values else 1
673
- col = StringColumn(col_name, "", max_len, [])
674
- col.values = str_values
675
-
676
- columns.append(col)
652
+ columns = [
653
+ OMEROTableColumnType.column_for(col_name, df[col_name]) for col_name in df.columns
654
+ ]
677
655
 
678
656
  table = OMERO_TABLE_SERVICE.create_table(conn, f"{table_name}.h5")
679
657
 
@@ -4,11 +4,107 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  import time
7
+ from collections.abc import Callable
7
8
  from dataclasses import dataclass
9
+ from enum import Enum
8
10
  from typing import Any
9
11
 
10
12
  logger = logging.getLogger(__name__)
11
13
 
14
+ OMEROColumnMatcher = Callable[[Any], bool]
15
+ OMEROColumnBuilder = Callable[[str, Any], Any]
16
+
17
+
18
+ def _integer_column_matches(series: Any) -> bool:
19
+ import pandas as pd
20
+
21
+ return bool(pd.api.types.is_integer_dtype(series))
22
+
23
+
24
+ def _float_column_matches(series: Any) -> bool:
25
+ import pandas as pd
26
+
27
+ return bool(pd.api.types.is_float_dtype(series))
28
+
29
+
30
+ def _bool_column_matches(series: Any) -> bool:
31
+ import pandas as pd
32
+
33
+ return bool(pd.api.types.is_bool_dtype(series))
34
+
35
+
36
+ def _fallback_column_matches(series: Any) -> bool:
37
+ del series
38
+ return True
39
+
40
+
41
+ def _integer_column(name: str, series: Any) -> Any:
42
+ from omero.grid import LongColumn
43
+
44
+ column = LongColumn(name, "", [])
45
+ column.values = series.astype(int).tolist()
46
+ return column
47
+
48
+
49
+ def _float_column(name: str, series: Any) -> Any:
50
+ from omero.grid import DoubleColumn
51
+
52
+ column = DoubleColumn(name, "", [])
53
+ column.values = series.astype(float).tolist()
54
+ return column
55
+
56
+
57
+ def _bool_column(name: str, series: Any) -> Any:
58
+ from omero.grid import BoolColumn
59
+
60
+ column = BoolColumn(name, "", [])
61
+ column.values = series.astype(bool).tolist()
62
+ return column
63
+
64
+
65
+ def _string_column(name: str, series: Any) -> Any:
66
+ from omero.grid import StringColumn
67
+
68
+ values = [str(value) for value in series.tolist()]
69
+ column = StringColumn(
70
+ name,
71
+ "",
72
+ max((len(value) for value in values), default=1),
73
+ [],
74
+ )
75
+ column.values = values
76
+ return column
77
+
78
+
79
+ class OMEROTableColumnType(Enum):
80
+ """One pandas dtype family's complete OMERO column projection."""
81
+
82
+ def __new__(
83
+ cls,
84
+ identity: str,
85
+ matches: OMEROColumnMatcher,
86
+ build: OMEROColumnBuilder,
87
+ ):
88
+ member = object.__new__(cls)
89
+ member._value_ = identity
90
+ member._matches = matches
91
+ member._build = build
92
+ return member
93
+
94
+ INTEGER = ("integer", _integer_column_matches, _integer_column)
95
+ FLOAT = ("float", _float_column_matches, _float_column)
96
+ BOOLEAN = ("boolean", _bool_column_matches, _bool_column)
97
+ STRING = ("string", _fallback_column_matches, _string_column)
98
+
99
+ @classmethod
100
+ def column_for(cls, name: str, series: Any) -> Any:
101
+ """Build the first declared OMERO column matching one pandas series."""
102
+
103
+ return next(member for member in cls if member._matches(series))._build(
104
+ name,
105
+ series,
106
+ )
107
+
12
108
 
13
109
  class OMEROTableServiceUnavailableError(RuntimeError):
14
110
  """Raised when OMERO.tables cannot satisfy a table operation."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polystore
3
- Version: 0.2.0
3
+ Version: 0.2.2
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
@@ -58,6 +58,7 @@ Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
58
58
  Requires-Dist: coverage>=7.3.2; extra == "dev"
59
59
  Requires-Dist: genbadge[coverage]; extra == "dev"
60
60
  Requires-Dist: pandas>=2.3.1; extra == "dev"
61
+ Requires-Dist: jpype1>=1.4.0; extra == "dev"
61
62
  Requires-Dist: black>=23.0.0; extra == "dev"
62
63
  Requires-Dist: ruff>=0.1.0; extra == "dev"
63
64
  Provides-Extra: docs
@@ -25,6 +25,7 @@ pytest-cov>=4.1.0
25
25
  coverage>=7.3.2
26
26
  genbadge[coverage]
27
27
  pandas>=2.3.1
28
+ jpype1>=1.4.0
28
29
  black>=23.0.0
29
30
  ruff>=0.1.0
30
31
 
@@ -3,6 +3,7 @@
3
3
  Keep tests small and focused — these hit CSV/JSON/TEXT handlers, listing,
4
4
  ensure_directory idempotence, and symlink creation.
5
5
  """
6
+
6
7
  from pathlib import Path
7
8
 
8
9
  import numpy as np
@@ -38,6 +39,23 @@ def test_text_json_csv_save_load(tmp_path: Path):
38
39
  assert loaded[0]["a"] == "1"
39
40
 
40
41
 
42
+ def test_preformatted_text_is_written_as_exact_utf8_bytes(
43
+ tmp_path: Path,
44
+ monkeypatch,
45
+ ) -> None:
46
+ content = "well,count\r\nA01,2\r\n"
47
+ output_path = tmp_path / "summary.csv"
48
+
49
+ def reject_platform_text_translation(*args, **kwargs):
50
+ del args, kwargs
51
+ raise AssertionError("text persistence must bypass platform newline translation")
52
+
53
+ monkeypatch.setattr(Path, "write_text", reject_platform_text_translation)
54
+ DiskBackend().save(content, output_path)
55
+
56
+ assert output_path.read_bytes() == content.encode(disk_module.DISK_TEXT_ENCODING)
57
+
58
+
41
59
  def test_tiff_save_normalizes_external_array_payload(tmp_path, monkeypatch) -> None:
42
60
  class ExternalArray:
43
61
  pass
@@ -6,6 +6,7 @@ import pytest
6
6
 
7
7
  from polystore.imagej_runtime import (
8
8
  FIJI_IMAGEJ_RUNTIME,
9
+ ImageJJvmTeardown,
9
10
  ImageJRuntimePolicy,
10
11
  ImageJRuntimeUnavailableError,
11
12
  )
@@ -49,7 +50,10 @@ class _ImageJ:
49
50
  return self.gateway
50
51
 
51
52
 
52
- def test_fiji_runtime_selects_managed_java_before_initialization() -> None:
53
+ def test_fiji_runtime_selects_managed_java_before_initialization(monkeypatch) -> None:
54
+ import jpype.config
55
+
56
+ monkeypatch.setattr(jpype.config, "destroy_jvm", True)
53
57
  scyjava = _ScyJava(started=False, java_version="21.0.8")
54
58
  imagej = _ImageJ(scyjava)
55
59
 
@@ -58,6 +62,7 @@ def test_fiji_runtime_selects_managed_java_before_initialization() -> None:
58
62
  assert gateway is imagej.gateway
59
63
  assert scyjava.constraints == [{"fetch": "always", "vendor": "zulu-jre", "version": "21"}]
60
64
  assert imagej.calls == [("sc.fiji:fiji", "headless")]
65
+ assert jpype.config.destroy_jvm is False
61
66
 
62
67
 
63
68
  def test_runtime_accepts_compatible_active_java_without_reconfiguration() -> None:
@@ -90,6 +95,7 @@ def test_runtime_reports_an_unparseable_active_java_version() -> None:
90
95
  java_fetch="always",
91
96
  java_vendor="example-jre",
92
97
  java_version="21",
98
+ jvm_teardown=ImageJJvmTeardown.PROCESS_EXIT,
93
99
  )
94
100
  scyjava = _ScyJava(started=True, java_version="unknown")
95
101
 
@@ -132,6 +138,7 @@ def test_runtime_retries_initialization_only_before_jvm_start(monkeypatch) -> No
132
138
  java_fetch="always",
133
139
  java_vendor="example-jre",
134
140
  java_version="21",
141
+ jvm_teardown=ImageJJvmTeardown.PROCESS_EXIT,
135
142
  initialization_retry_delays_seconds=(0.1, 0.2),
136
143
  )
137
144
  scyjava = _ScyJava(started=False, java_version="21")
@@ -162,6 +169,7 @@ def test_runtime_preserves_final_failure_after_retry_schedule() -> None:
162
169
  java_fetch="always",
163
170
  java_vendor="example-jre",
164
171
  java_version="21",
172
+ jvm_teardown=ImageJJvmTeardown.PROCESS_EXIT,
165
173
  initialization_retry_delays_seconds=(0.0, 0.0),
166
174
  )
167
175
  scyjava = _ScyJava(started=False, java_version="21")
@@ -1,10 +1,13 @@
1
1
  """Tests for the declaration-owned OMERO.tables capability boundary."""
2
2
 
3
- from types import SimpleNamespace
3
+ import sys
4
+ from types import ModuleType, SimpleNamespace
4
5
 
6
+ import pandas as pd
5
7
  import pytest
6
8
 
7
9
  from polystore.omero_tables import (
10
+ OMEROTableColumnType,
8
11
  OMEROTableService,
9
12
  OMEROTableServiceUnavailableError,
10
13
  )
@@ -12,6 +15,64 @@ from polystore.omero_tables import (
12
15
  _DEFAULT_TABLE = object()
13
16
 
14
17
 
18
+ class _Column:
19
+ def __init__(self, *declaration) -> None:
20
+ self.declaration = declaration
21
+ self.values = []
22
+
23
+
24
+ class _LongColumn(_Column):
25
+ pass
26
+
27
+
28
+ class _DoubleColumn(_Column):
29
+ pass
30
+
31
+
32
+ class _BoolColumn(_Column):
33
+ pass
34
+
35
+
36
+ class _StringColumn(_Column):
37
+ pass
38
+
39
+
40
+ @pytest.fixture
41
+ def omero_grid_columns(monkeypatch):
42
+ grid = ModuleType("omero.grid")
43
+ grid.LongColumn = _LongColumn
44
+ grid.DoubleColumn = _DoubleColumn
45
+ grid.BoolColumn = _BoolColumn
46
+ grid.StringColumn = _StringColumn
47
+ omero = ModuleType("omero")
48
+ omero.__path__ = []
49
+ monkeypatch.setitem(sys.modules, "omero", omero)
50
+ monkeypatch.setitem(sys.modules, "omero.grid", grid)
51
+
52
+
53
+ @pytest.mark.parametrize(
54
+ ("series", "column_type", "values"),
55
+ (
56
+ (pd.Series([1, 2]), _LongColumn, [1, 2]),
57
+ (pd.Series([1.5, 2.5]), _DoubleColumn, [1.5, 2.5]),
58
+ (pd.Series([True, False]), _BoolColumn, [True, False]),
59
+ (pd.Series(["A01", float("nan")], dtype=object), _StringColumn, ["A01", "nan"]),
60
+ ),
61
+ )
62
+ def test_table_column_type_owns_dtype_projection(
63
+ omero_grid_columns,
64
+ series,
65
+ column_type,
66
+ values,
67
+ ) -> None:
68
+ column = OMEROTableColumnType.column_for("value", series)
69
+
70
+ assert isinstance(column, column_type)
71
+ assert column.values == values
72
+ if isinstance(column, _StringColumn):
73
+ assert column.declaration == ("value", "", 3, [])
74
+
75
+
15
76
  class _Resources:
16
77
  def __init__(
17
78
  self,
File without changes
File without changes
File without changes
File without changes
File without changes