arraybridge 0.3.2__tar.gz → 0.3.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 (88) hide show
  1. {arraybridge-0.3.2 → arraybridge-0.3.4}/.github/workflows/publish.yml +1 -1
  2. {arraybridge-0.3.2 → arraybridge-0.3.4}/PKG-INFO +1 -1
  3. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/source/conf.py +3 -1
  4. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/source/gpu_features.rst +5 -0
  5. {arraybridge-0.3.2 → arraybridge-0.3.4}/pyproject.toml +4 -1
  6. arraybridge-0.3.4/scripts/update_version.py +60 -0
  7. {arraybridge-0.3.2 → arraybridge-0.3.4}/scripts/verify_release_ready.py +2 -17
  8. arraybridge-0.3.4/src/arraybridge/__init__.py +109 -0
  9. {arraybridge-0.3.2 → arraybridge-0.3.4}/src/arraybridge/types.py +91 -21
  10. {arraybridge-0.3.2 → arraybridge-0.3.4}/tests/test_types.py +52 -0
  11. arraybridge-0.3.2/scripts/update_version.py +0 -79
  12. arraybridge-0.3.2/src/arraybridge/__init__.py +0 -75
  13. {arraybridge-0.3.2 → arraybridge-0.3.4}/.github/workflows/ci.yml +0 -0
  14. {arraybridge-0.3.2 → arraybridge-0.3.4}/.github/workflows/coverage-pages.yml +0 -0
  15. {arraybridge-0.3.2 → arraybridge-0.3.4}/.github/workflows/gpu-tests.yml +0 -0
  16. {arraybridge-0.3.2 → arraybridge-0.3.4}/.gitignore +0 -0
  17. {arraybridge-0.3.2 → arraybridge-0.3.4}/.readthedocs.yml +0 -0
  18. {arraybridge-0.3.2 → arraybridge-0.3.4}/ADDING_NEW_FRAMEWORKS.md +0 -0
  19. {arraybridge-0.3.2 → arraybridge-0.3.4}/CONTRIBUTING.md +0 -0
  20. {arraybridge-0.3.2 → arraybridge-0.3.4}/LICENSE +0 -0
  21. {arraybridge-0.3.2 → arraybridge-0.3.4}/README.md +0 -0
  22. {arraybridge-0.3.2 → arraybridge-0.3.4}/TESTING.md +0 -0
  23. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/Makefile +0 -0
  24. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/archive/CI_ARTIFACT_UPDATE.md +0 -0
  25. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/archive/CI_CUPY_FIX.md +0 -0
  26. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/archive/CI_IMPROVEMENTS.md +0 -0
  27. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/archive/CONFLICT_VISUALIZATION.md +0 -0
  28. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/archive/COVERAGE_AUDIT_PLAN.md +0 -0
  29. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/archive/DEPLOYMENT_SUMMARY.md +0 -0
  30. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/archive/GPU_TESTING_SETUP.md +0 -0
  31. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/archive/MERGE_CONFLICT_RESOLUTION.md +0 -0
  32. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/archive/MIGRATION_SUMMARY.md +0 -0
  33. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/archive/QUICK_FIX_GUIDE.txt +0 -0
  34. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/archive/README.md +0 -0
  35. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/archive/RESOLVED_FILES.md +0 -0
  36. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/ci-cd.md +0 -0
  37. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/source/.gitignore +0 -0
  38. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/source/Makefile +0 -0
  39. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/source/README.md +0 -0
  40. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/source/advanced_topics.rst +0 -0
  41. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/source/api_reference.rst +0 -0
  42. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/source/ci-cd.rst +0 -0
  43. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/source/contributing.rst +0 -0
  44. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/source/converters.rst +0 -0
  45. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/source/decorators.rst +0 -0
  46. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/source/examples/basic_conversion.rst +0 -0
  47. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/source/examples/decorators.rst +0 -0
  48. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/source/examples/index.rst +0 -0
  49. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/source/examples/multi_framework.rst +0 -0
  50. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/source/index.rst +0 -0
  51. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/source/installation.rst +0 -0
  52. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/source/quickstart.rst +0 -0
  53. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/source/stack_utils.rst +0 -0
  54. {arraybridge-0.3.2 → arraybridge-0.3.4}/docs/source/user_guide.rst +0 -0
  55. {arraybridge-0.3.2 → arraybridge-0.3.4}/paper.bib +0 -0
  56. {arraybridge-0.3.2 → arraybridge-0.3.4}/paper.md +0 -0
  57. {arraybridge-0.3.2 → arraybridge-0.3.4}/scripts/release.py +0 -0
  58. {arraybridge-0.3.2 → arraybridge-0.3.4}/src/arraybridge/array_geometry.py +0 -0
  59. {arraybridge-0.3.2 → arraybridge-0.3.4}/src/arraybridge/array_operations.py +0 -0
  60. {arraybridge-0.3.2 → arraybridge-0.3.4}/src/arraybridge/array_payload.py +0 -0
  61. {arraybridge-0.3.2 → arraybridge-0.3.4}/src/arraybridge/converters.py +0 -0
  62. {arraybridge-0.3.2 → arraybridge-0.3.4}/src/arraybridge/converters_registry.py +0 -0
  63. {arraybridge-0.3.2 → arraybridge-0.3.4}/src/arraybridge/decorators.py +0 -0
  64. {arraybridge-0.3.2 → arraybridge-0.3.4}/src/arraybridge/dtype_scaling.py +0 -0
  65. {arraybridge-0.3.2 → arraybridge-0.3.4}/src/arraybridge/exceptions.py +0 -0
  66. {arraybridge-0.3.2 → arraybridge-0.3.4}/src/arraybridge/framework_config.py +0 -0
  67. {arraybridge-0.3.2 → arraybridge-0.3.4}/src/arraybridge/framework_ops.py +0 -0
  68. {arraybridge-0.3.2 → arraybridge-0.3.4}/src/arraybridge/gpu_cleanup.py +0 -0
  69. {arraybridge-0.3.2 → arraybridge-0.3.4}/src/arraybridge/oom_recovery.py +0 -0
  70. {arraybridge-0.3.2 → arraybridge-0.3.4}/src/arraybridge/slice_processing.py +0 -0
  71. {arraybridge-0.3.2 → arraybridge-0.3.4}/src/arraybridge/stack_utils.py +0 -0
  72. {arraybridge-0.3.2 → arraybridge-0.3.4}/src/arraybridge/utils.py +0 -0
  73. {arraybridge-0.3.2 → arraybridge-0.3.4}/tests/__init__.py +0 -0
  74. {arraybridge-0.3.2 → arraybridge-0.3.4}/tests/conftest.py +0 -0
  75. {arraybridge-0.3.2 → arraybridge-0.3.4}/tests/test_array_geometry.py +0 -0
  76. {arraybridge-0.3.2 → arraybridge-0.3.4}/tests/test_converters.py +0 -0
  77. {arraybridge-0.3.2 → arraybridge-0.3.4}/tests/test_converters_registry.py +0 -0
  78. {arraybridge-0.3.2 → arraybridge-0.3.4}/tests/test_decorators.py +0 -0
  79. {arraybridge-0.3.2 → arraybridge-0.3.4}/tests/test_dtype_scaling.py +0 -0
  80. {arraybridge-0.3.2 → arraybridge-0.3.4}/tests/test_exceptions.py +0 -0
  81. {arraybridge-0.3.2 → arraybridge-0.3.4}/tests/test_framework_config.py +0 -0
  82. {arraybridge-0.3.2 → arraybridge-0.3.4}/tests/test_gpu_cleanup.py +0 -0
  83. {arraybridge-0.3.2 → arraybridge-0.3.4}/tests/test_integration.py +0 -0
  84. {arraybridge-0.3.2 → arraybridge-0.3.4}/tests/test_oom_recovery.py +0 -0
  85. {arraybridge-0.3.2 → arraybridge-0.3.4}/tests/test_registry_integration.py +0 -0
  86. {arraybridge-0.3.2 → arraybridge-0.3.4}/tests/test_slice_processing.py +0 -0
  87. {arraybridge-0.3.2 → arraybridge-0.3.4}/tests/test_stack_utils.py +0 -0
  88. {arraybridge-0.3.2 → arraybridge-0.3.4}/tests/test_utils.py +0 -0
@@ -42,7 +42,7 @@ jobs:
42
42
 
43
43
  - name: Verify release tag and main ancestry
44
44
  run: |
45
- declared_version=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
45
+ declared_version=$(python -c 'import arraybridge; print(arraybridge.__version__)')
46
46
  test "$GITHUB_REF_NAME" = "v$declared_version"
47
47
  git fetch origin main
48
48
  git merge-base --is-ancestor "$GITHUB_SHA" origin/main
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arraybridge
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Unified API for NumPy, CuPy, PyTorch, TensorFlow, JAX, and pyclesperanto with automatic memory type conversion
5
5
  Project-URL: Homepage, https://github.com/OpenHCSDev/arraybridge
6
6
  Project-URL: Documentation, https://arraybridge.readthedocs.io
@@ -16,13 +16,15 @@ EXTERNAL_ROOT = PACKAGE_ROOT.parent
16
16
  sys.path.insert(0, str(EXTERNAL_ROOT / "metaclass-registry" / "src"))
17
17
  sys.path.insert(0, str(PACKAGE_ROOT / "src"))
18
18
 
19
+ from arraybridge import __version__
20
+
19
21
  # -- Project information -----------------------------------------------------
20
22
  # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
21
23
 
22
24
  project = 'arraybridge'
23
25
  copyright = '2025, Tristan Simas'
24
26
  author = 'Tristan Simas'
25
- release = '0.3.1'
27
+ release = __version__
26
28
 
27
29
  # -- General configuration ---------------------------------------------------
28
30
  # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
@@ -15,6 +15,11 @@ overwritten. Hosts that import optional frameworks themselves should call
15
15
  ``MemoryType.prepare_import()`` first. ArrayBridge warns when it encounters an
16
16
  already-loaded framework whose import-time defaults were absent.
17
17
 
18
+ ``MemoryType.subprocess_environment()`` projects the same member-owned import
19
+ defaults into a child-process environment. The CuPy member also derives native
20
+ library search paths from installed NVIDIA wheels, so a fresh child interpreter
21
+ does not depend on another framework having loaded CUDA libraries first.
22
+
18
23
  JAX float64 output requires x64 mode to be enabled before import. ArrayBridge
19
24
  raises instead of silently returning float32 when a caller requests float64
20
25
  while that capability is disabled.
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
5
5
 
6
6
  [project]
7
7
  name = "arraybridge"
8
- version = "0.3.2"
8
+ dynamic = ["version"]
9
9
  description = "Unified API for NumPy, CuPy, PyTorch, TensorFlow, JAX, and pyclesperanto with automatic memory type conversion"
10
10
  authors = [{name = "Tristan Simas", email = "tristan.simas@mail.mcgill.ca"}]
11
11
  license = {text = "MIT"}
@@ -98,6 +98,9 @@ Issues = "https://github.com/OpenHCSDev/arraybridge/issues"
98
98
  [tool.hatch.build.targets.wheel]
99
99
  packages = ["src/arraybridge"]
100
100
 
101
+ [tool.hatch.version]
102
+ path = "src/arraybridge/__init__.py"
103
+
101
104
  [tool.pytest.ini_options]
102
105
  testpaths = ["tests"]
103
106
  python_files = ["test_*.py"]
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env python3
2
+ """Update ArrayBridge's canonical package version without Git side effects."""
3
+
4
+ import argparse
5
+ import re
6
+ from pathlib import Path
7
+
8
+ from packaging.version import InvalidVersion, Version
9
+
10
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
11
+ VERSION_FILE = PROJECT_ROOT / "src/arraybridge/__init__.py"
12
+ VERSION_PATTERN = r"(__version__\s*=\s*['\"])([^'\"]+)(['\"])"
13
+
14
+
15
+ def current_version() -> Version:
16
+ """Return the package-owned canonical version."""
17
+
18
+ match = re.search(
19
+ VERSION_PATTERN,
20
+ VERSION_FILE.read_text(encoding="utf-8"),
21
+ flags=re.MULTILINE,
22
+ )
23
+ if match is None:
24
+ raise RuntimeError(f"Version declaration is missing from {VERSION_FILE}")
25
+ return Version(match.group(2))
26
+
27
+
28
+ def update_version(version_text: str) -> None:
29
+ try:
30
+ requested = Version(version_text)
31
+ except InvalidVersion as error:
32
+ raise ValueError(f"Invalid version: {version_text}") from error
33
+ current = current_version()
34
+ if requested <= current:
35
+ raise ValueError(f"New version {requested} must be greater than {current}")
36
+
37
+ content = VERSION_FILE.read_text(encoding="utf-8")
38
+ updated, count = re.subn(
39
+ VERSION_PATTERN,
40
+ rf"\g<1>{requested}\g<3>",
41
+ content,
42
+ flags=re.MULTILINE,
43
+ )
44
+ if count != 1:
45
+ raise RuntimeError(f"Expected one version declaration in {VERSION_FILE}, found {count}")
46
+ VERSION_FILE.write_text(updated, encoding="utf-8")
47
+
48
+ print(f"Updated ArrayBridge version to {requested}")
49
+ print("Run scripts/verify_release_ready.py --allow-dirty, then review and commit.")
50
+
51
+
52
+ def main() -> None:
53
+ parser = argparse.ArgumentParser(description=__doc__)
54
+ parser.add_argument("version", help="New release version, for example 0.3.0")
55
+ args = parser.parse_args()
56
+ update_version(args.version)
57
+
58
+
59
+ if __name__ == "__main__":
60
+ main()
@@ -16,8 +16,6 @@ import sys
16
16
  import tempfile
17
17
  from pathlib import Path
18
18
 
19
- import tomllib
20
-
21
19
  PROJECT_ROOT = Path(__file__).resolve().parents[1]
22
20
 
23
21
 
@@ -41,20 +39,6 @@ def check_version():
41
39
  print(f" ❌ Version '{version}' doesn't follow semantic versioning (MAJOR.MINOR.PATCH)")
42
40
  return False
43
41
 
44
- pyproject_version = tomllib.loads(
45
- (PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8")
46
- )["project"]["version"]
47
- docs_match = re.search(
48
- r"release\s*=\s*['\"]([^'\"]+)",
49
- (PROJECT_ROOT / "docs/source/conf.py").read_text(encoding="utf-8"),
50
- )
51
- docs_version = docs_match.group(1) if docs_match else None
52
- if len({version, pyproject_version, docs_version}) != 1:
53
- print(
54
- " ❌ Version authorities disagree: "
55
- f"package={version}, pyproject={pyproject_version}, docs={docs_version}"
56
- )
57
- return False
58
42
  print(f" ✅ Version: {version}")
59
43
  return True
60
44
 
@@ -70,7 +54,8 @@ def check_pyproject_toml():
70
54
  content = pyproject_file.read_text(encoding="utf-8")
71
55
  required_fields = {
72
56
  "name": r'name\s*=\s*["\']arraybridge["\']',
73
- "version": r"version\s*=",
57
+ "dynamic version": r'dynamic\s*=\s*\[["\']version["\']\]',
58
+ "version authority": r'path\s*=\s*["\']src/arraybridge/__init__\.py["\']',
74
59
  "description": r"description\s*=",
75
60
  "authors": r"authors\s*=",
76
61
  "build-backend": r'build-backend\s*=\s*["\']hatchling\.build["\']',
@@ -0,0 +1,109 @@
1
+ """
2
+ arraybridge: Unified API for NumPy, CuPy, PyTorch, TensorFlow, JAX, and pyclesperanto.
3
+
4
+ This package provides automatic memory type conversion, declarative decorators,
5
+ and unified utilities for working with multiple array/tensor frameworks.
6
+
7
+ Package-root imports stay lightweight: submodule exports load on first
8
+ attribute access instead of at import time. Declaration-only consumers
9
+ (memory-type tables, configuration modules) therefore do not pay the
10
+ NumPy/numcodecs import cost.
11
+ """
12
+
13
+ __version__ = "0.3.4"
14
+
15
+ _LAZY_EXPORTS: dict[str, str] = {
16
+ "MemoryType": ".types",
17
+ "MemoryContractAttribute": ".types",
18
+ "ArrayPayload": ".array_payload",
19
+ "ArrayGeometry": ".array_geometry",
20
+ "CPU_MEMORY_TYPES": ".types",
21
+ "GPU_MEMORY_TYPES": ".types",
22
+ "SUPPORTED_MEMORY_TYPES": ".types",
23
+ "convert_memory": ".converters",
24
+ "detect_memory_type": ".converters",
25
+ "memory_types": ".decorators",
26
+ "DtypeConversion": ".decorators",
27
+ "SliceBySliceRuntimeParameter": ".decorators",
28
+ "wrap_dtype_preserving_callable": ".decorators",
29
+ "stack_slices": ".stack_utils",
30
+ "unstack_slices": ".stack_utils",
31
+ "process_slices": ".slice_processing",
32
+ "cleanup_all_gpu_frameworks": ".gpu_cleanup",
33
+ "MemoryConversionError": ".exceptions",
34
+ "SCALING_FUNCTIONS": ".dtype_scaling",
35
+ "_FRAMEWORK_CONFIG": ".framework_config",
36
+ "_FRAMEWORK_OPS": ".framework_ops",
37
+ "_execute_with_oom_recovery": ".oom_recovery",
38
+ "_ensure_module": ".utils",
39
+ "_supports_dlpack": ".utils",
40
+ "_get_device_id": ".utils",
41
+ # Decorator exports named after each memory type (numpy, cupy, torch, ...).
42
+ "numpy": ".decorators",
43
+ "cupy": ".decorators",
44
+ "torch": ".decorators",
45
+ "tensorflow": ".decorators",
46
+ "jax": ".decorators",
47
+ "pyclesperanto": ".decorators",
48
+ }
49
+
50
+ __all__ = [
51
+ # Types
52
+ "MemoryType",
53
+ "MemoryContractAttribute",
54
+ "ArrayPayload",
55
+ "ArrayGeometry",
56
+ "CPU_MEMORY_TYPES",
57
+ "GPU_MEMORY_TYPES",
58
+ "SUPPORTED_MEMORY_TYPES",
59
+ # Converters
60
+ "convert_memory",
61
+ "detect_memory_type",
62
+ # Decorators
63
+ "memory_types",
64
+ "DtypeConversion",
65
+ "SliceBySliceRuntimeParameter",
66
+ "wrap_dtype_preserving_callable",
67
+ # Stack utilities
68
+ "stack_slices",
69
+ "unstack_slices",
70
+ # Slice processing
71
+ "process_slices",
72
+ # GPU cleanup
73
+ "cleanup_all_gpu_frameworks",
74
+ # Exceptions
75
+ "MemoryConversionError",
76
+ # Scaling
77
+ "SCALING_FUNCTIONS",
78
+ # Framework config (internal but needed by some consumers)
79
+ "_FRAMEWORK_CONFIG",
80
+ "_FRAMEWORK_OPS",
81
+ # OOM recovery
82
+ "_execute_with_oom_recovery",
83
+ # Utils
84
+ "_ensure_module",
85
+ "_supports_dlpack",
86
+ "_get_device_id",
87
+ "numpy",
88
+ "cupy",
89
+ "torch",
90
+ "tensorflow",
91
+ "jax",
92
+ "pyclesperanto",
93
+ ]
94
+
95
+
96
+ def __getattr__(name: str):
97
+ module_name = _LAZY_EXPORTS.get(name)
98
+ if module_name is None:
99
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
100
+
101
+ import importlib
102
+
103
+ value = getattr(importlib.import_module(module_name, __name__), name)
104
+ globals()[name] = value
105
+ return value
106
+
107
+
108
+ def __dir__() -> list[str]:
109
+ return sorted(set(globals()) | set(_LAZY_EXPORTS))
@@ -5,6 +5,8 @@ This module defines the MemoryType enum and related constants for managing
5
5
  different array/tensor frameworks.
6
6
  """
7
7
 
8
+ from __future__ import annotations
9
+
8
10
  import importlib
9
11
  import importlib.util
10
12
  import logging
@@ -14,17 +16,11 @@ from collections.abc import Callable, Iterator, Mapping, MutableMapping
14
16
  from contextlib import AbstractContextManager, contextmanager, nullcontext
15
17
  from dataclasses import dataclass
16
18
  from enum import Enum
17
- from typing import Any, TypeVar, cast
18
-
19
- from arraybridge.array_operations import (
20
- CUPY_OPERATIONS,
21
- JAX_OPERATIONS,
22
- NUMPY_OPERATIONS,
23
- PYCLESPERANTO_OPERATIONS,
24
- TENSORFLOW_OPERATIONS,
25
- TORCH_OPERATIONS,
26
- ArrayOperations,
27
- )
19
+ from pathlib import Path
20
+ from typing import TYPE_CHECKING, Any, TypeVar, cast
21
+
22
+ if TYPE_CHECKING:
23
+ from arraybridge.array_operations import ArrayOperations
28
24
 
29
25
  T = TypeVar("T")
30
26
  logger = logging.getLogger(__name__)
@@ -41,6 +37,55 @@ StreamScopeFactory = Callable[[Any, Any], AbstractContextManager[None]]
41
37
  DLPackExporter = Callable[[Any, Any], Any | None]
42
38
  DLPackValidator = Callable[[Any, Any], bool]
43
39
  OOMMatcher = Callable[[BaseException, Any | None], bool]
40
+ SubprocessEnvironmentResolver = Callable[[Mapping[str, str]], dict[str, str]]
41
+
42
+
43
+ def _identity_subprocess_environment(
44
+ environment: Mapping[str, str],
45
+ ) -> dict[str, str]:
46
+ return dict(environment)
47
+
48
+
49
+ def _nvidia_wheel_library_paths() -> tuple[str, ...]:
50
+ """Return native-library directories declared by installed NVIDIA wheels."""
51
+
52
+ try:
53
+ spec = importlib.util.find_spec("nvidia")
54
+ except (ImportError, ModuleNotFoundError, ValueError):
55
+ return ()
56
+ if spec is None or spec.submodule_search_locations is None:
57
+ return ()
58
+
59
+ paths: set[str] = set()
60
+ for package_root in map(Path, spec.submodule_search_locations):
61
+ try:
62
+ components = tuple(package_root.iterdir())
63
+ except OSError:
64
+ continue
65
+ for component in components:
66
+ for library_directory_name in ("lib", "bin"):
67
+ candidate = component / library_directory_name
68
+ if candidate.is_dir():
69
+ paths.add(str(candidate))
70
+ return tuple(sorted(paths))
71
+
72
+
73
+ def _nvidia_wheel_subprocess_environment(
74
+ environment: Mapping[str, str],
75
+ ) -> dict[str, str]:
76
+ """Prepend installed NVIDIA wheel libraries to a child environment."""
77
+
78
+ prepared = dict(environment)
79
+ library_paths = _nvidia_wheel_library_paths()
80
+ if not library_paths:
81
+ return prepared
82
+
83
+ search_variable = "PATH" if os.name == "nt" else "LD_LIBRARY_PATH"
84
+ existing_paths = tuple(
85
+ path for path in prepared.get(search_variable, "").split(os.pathsep) if path
86
+ )
87
+ prepared[search_variable] = os.pathsep.join(dict.fromkeys((*library_paths, *existing_paths)))
88
+ return prepared
44
89
 
45
90
 
46
91
  class MemoryContractAttribute(str, Enum):
@@ -501,6 +546,7 @@ class FrameworkRuntime:
501
546
  dlpack_exporter: DLPackExporter | None = None
502
547
  dlpack_validator: DLPackValidator = _protocol_dlpack
503
548
  oom_matcher: OOMMatcher = _never_oom
549
+ subprocess_environment: SubprocessEnvironmentResolver = _identity_subprocess_environment
504
550
 
505
551
 
506
552
  class _MemoryTypeFields:
@@ -510,7 +556,7 @@ class _MemoryTypeFields:
510
556
  module_aliases: tuple[str, ...]
511
557
  import_environment: tuple[tuple[str, str], ...]
512
558
  _runtime: FrameworkRuntime
513
- _operations: ArrayOperations
559
+ _operations_name: str
514
560
 
515
561
 
516
562
  class MemoryType(_MemoryTypeFields, Enum):
@@ -520,7 +566,7 @@ class MemoryType(_MemoryTypeFields, Enum):
520
566
  cls,
521
567
  value: str,
522
568
  *declaration: Any,
523
- ) -> "MemoryType":
569
+ ) -> MemoryType:
524
570
  (
525
571
  import_name,
526
572
  display_name,
@@ -538,9 +584,18 @@ class MemoryType(_MemoryTypeFields, Enum):
538
584
  member.module_aliases = cast(tuple[str, ...], module_aliases)
539
585
  member.import_environment = cast(tuple[tuple[str, str], ...], import_environment)
540
586
  member._runtime = cast(FrameworkRuntime, runtime)
541
- member._operations = cast(ArrayOperations, operations)
587
+ member._operations_name = cast(str, operations)
542
588
  return member
543
589
 
590
+ @property
591
+ def _operations(self) -> ArrayOperations:
592
+ """Resolve this declaration's operations registry on first access."""
593
+
594
+ from arraybridge import array_operations
595
+
596
+ operations: ArrayOperations = getattr(array_operations, self._operations_name)
597
+ return operations
598
+
544
599
  NUMPY = (
545
600
  "numpy",
546
601
  "numpy",
@@ -549,7 +604,7 @@ class MemoryType(_MemoryTypeFields, Enum):
549
604
  (),
550
605
  (),
551
606
  FrameworkRuntime(oom_matcher=_numpy_oom),
552
- NUMPY_OPERATIONS,
607
+ "NUMPY_OPERATIONS",
553
608
  )
554
609
  CUPY = (
555
610
  "cupy",
@@ -571,8 +626,9 @@ class MemoryType(_MemoryTypeFields, Enum):
571
626
  dlpack_importer=_cupy_from_dlpack,
572
627
  dlpack_exporter=_protocol_dlpack_export,
573
628
  oom_matcher=_cupy_oom,
629
+ subprocess_environment=_nvidia_wheel_subprocess_environment,
574
630
  ),
575
- CUPY_OPERATIONS,
631
+ "CUPY_OPERATIONS",
576
632
  )
577
633
  TORCH = (
578
634
  "torch",
@@ -595,7 +651,7 @@ class MemoryType(_MemoryTypeFields, Enum):
595
651
  dlpack_exporter=_protocol_dlpack_export,
596
652
  oom_matcher=_torch_oom,
597
653
  ),
598
- TORCH_OPERATIONS,
654
+ "TORCH_OPERATIONS",
599
655
  )
600
656
  TENSORFLOW = (
601
657
  "tensorflow",
@@ -614,7 +670,7 @@ class MemoryType(_MemoryTypeFields, Enum):
614
670
  dlpack_validator=_tensorflow_dlpack,
615
671
  oom_matcher=_tensorflow_oom,
616
672
  ),
617
- TENSORFLOW_OPERATIONS,
673
+ "TENSORFLOW_OPERATIONS",
618
674
  )
619
675
  JAX = (
620
676
  "jax",
@@ -632,7 +688,7 @@ class MemoryType(_MemoryTypeFields, Enum):
632
688
  dlpack_exporter=_protocol_dlpack_export,
633
689
  oom_matcher=_jax_oom,
634
690
  ),
635
- JAX_OPERATIONS,
691
+ "JAX_OPERATIONS",
636
692
  )
637
693
  PYCLESPERANTO = (
638
694
  "pyclesperanto",
@@ -649,7 +705,7 @@ class MemoryType(_MemoryTypeFields, Enum):
649
705
  move_to_active_device=_pyclesperanto_device_move,
650
706
  oom_matcher=_pyclesperanto_oom,
651
707
  ),
652
- PYCLESPERANTO_OPERATIONS,
708
+ "PYCLESPERANTO_OPERATIONS",
653
709
  )
654
710
 
655
711
  @property
@@ -682,6 +738,20 @@ class MemoryType(_MemoryTypeFields, Enum):
682
738
  for name, value in self.import_environment:
683
739
  os.environ.setdefault(name, value)
684
740
 
741
+ @classmethod
742
+ def subprocess_environment(
743
+ cls,
744
+ environment: Mapping[str, str] | None = None,
745
+ ) -> dict[str, str]:
746
+ """Project framework import requirements into a child environment."""
747
+
748
+ prepared = dict(os.environ if environment is None else environment)
749
+ for memory_type in cls:
750
+ for name, value in memory_type.import_environment:
751
+ prepared.setdefault(name, value)
752
+ prepared = memory_type._runtime.subprocess_environment(prepared)
753
+ return prepared
754
+
685
755
  def loaded_module(self) -> Any | None:
686
756
  """Return an already-loaded framework without causing an import."""
687
757
 
@@ -887,7 +957,7 @@ class MemoryType(_MemoryTypeFields, Enum):
887
957
  payload = data if isinstance(data, DLPackPayload) else DLPackPayload(data, data)
888
958
  return importer(payload, framework)
889
959
 
890
- def convert_to(self, data: Any, target: "MemoryType", device_id: int) -> Any:
960
+ def convert_to(self, data: Any, target: MemoryType, device_id: int) -> Any:
891
961
  """Convert one array through the source and target declarations."""
892
962
 
893
963
  if self is target:
@@ -145,6 +145,58 @@ class TestMemoryTypeOwnership:
145
145
  assert os.environ["TF_FORCE_GPU_ALLOW_GROWTH"] == "true"
146
146
  assert os.environ["XLA_PYTHON_CLIENT_PREALLOCATE"] == "custom"
147
147
 
148
+ def test_subprocess_environment_derives_nvidia_wheel_paths_from_cupy_owner(
149
+ self,
150
+ monkeypatch,
151
+ tmp_path,
152
+ ):
153
+ from arraybridge import types
154
+
155
+ nvidia_root = tmp_path / "nvidia"
156
+ cublas_lib = nvidia_root / "cublas" / "lib"
157
+ runtime_bin = nvidia_root / "cuda_runtime" / "bin"
158
+ cublas_lib.mkdir(parents=True)
159
+ runtime_bin.mkdir(parents=True)
160
+ monkeypatch.setattr(
161
+ types.importlib.util,
162
+ "find_spec",
163
+ lambda name: (
164
+ SimpleNamespace(submodule_search_locations=(str(nvidia_root),))
165
+ if name == "nvidia"
166
+ else None
167
+ ),
168
+ )
169
+ search_variable = "PATH" if os.name == "nt" else "LD_LIBRARY_PATH"
170
+ original = {search_variable: "/existing", "CUSTOM": "value"}
171
+
172
+ prepared = MemoryType.subprocess_environment(original)
173
+
174
+ assert original == {search_variable: "/existing", "CUSTOM": "value"}
175
+ assert prepared[search_variable].split(os.pathsep) == [
176
+ *sorted((str(cublas_lib), str(runtime_bin))),
177
+ "/existing",
178
+ ]
179
+ assert prepared["CUSTOM"] == "value"
180
+ assert prepared["TF_FORCE_GPU_ALLOW_GROWTH"] == "true"
181
+ assert prepared["XLA_PYTHON_CLIENT_PREALLOCATE"] == "false"
182
+
183
+ def test_subprocess_environment_preserves_host_values_without_nvidia_wheels(
184
+ self,
185
+ monkeypatch,
186
+ ):
187
+ from arraybridge import types
188
+
189
+ monkeypatch.setattr(types.importlib.util, "find_spec", lambda name: None)
190
+ original = {
191
+ "TF_FORCE_GPU_ALLOW_GROWTH": "host",
192
+ "XLA_PYTHON_CLIENT_PREALLOCATE": "host",
193
+ }
194
+
195
+ prepared = MemoryType.subprocess_environment(original)
196
+
197
+ assert prepared == original
198
+ assert prepared is not original
199
+
148
200
  def test_preloaded_framework_reports_late_import_preparation(
149
201
  self,
150
202
  monkeypatch,
@@ -1,79 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Update ArrayBridge's three version projections without Git side effects."""
3
-
4
- import argparse
5
- import re
6
- from pathlib import Path
7
-
8
- from packaging.version import InvalidVersion, Version
9
-
10
- PROJECT_ROOT = Path(__file__).resolve().parents[1]
11
- VERSION_FILES = {
12
- PROJECT_ROOT
13
- / "src/arraybridge/__init__.py": (
14
- r"(__version__\s*=\s*['\"])([^'\"]+)(['\"])",
15
- "package",
16
- ),
17
- PROJECT_ROOT
18
- / "pyproject.toml": (
19
- r"(^version\s*=\s*['\"])([^'\"]+)(['\"])",
20
- "project",
21
- ),
22
- PROJECT_ROOT
23
- / "docs/source/conf.py": (
24
- r"(^release\s*=\s*['\"])([^'\"]+)(['\"])",
25
- "documentation",
26
- ),
27
- }
28
-
29
-
30
- def current_version() -> Version:
31
- """Return the agreed version, failing when a projection has drifted."""
32
- declared_versions: dict[str, Version] = {}
33
- for path, (pattern, projection_name) in VERSION_FILES.items():
34
- match = re.search(pattern, path.read_text(encoding="utf-8"), flags=re.MULTILINE)
35
- if match is None:
36
- raise RuntimeError(f"Version declaration is missing from {path}")
37
- declared_versions[projection_name] = Version(match.group(2))
38
-
39
- unique_versions = set(declared_versions.values())
40
- if len(unique_versions) != 1:
41
- declarations = ", ".join(f"{name}={version}" for name, version in declared_versions.items())
42
- raise RuntimeError(f"Version projections have drifted: {declarations}")
43
- return unique_versions.pop()
44
-
45
-
46
- def update_version(version_text: str) -> None:
47
- try:
48
- requested = Version(version_text)
49
- except InvalidVersion as error:
50
- raise ValueError(f"Invalid version: {version_text}") from error
51
- current = current_version()
52
- if requested <= current:
53
- raise ValueError(f"New version {requested} must be greater than {current}")
54
-
55
- for path, (pattern, _projection_name) in VERSION_FILES.items():
56
- content = path.read_text(encoding="utf-8")
57
- updated, count = re.subn(
58
- pattern,
59
- rf"\g<1>{requested}\g<3>",
60
- content,
61
- flags=re.MULTILINE,
62
- )
63
- if count != 1:
64
- raise RuntimeError(f"Expected one version declaration in {path}, found {count}")
65
- path.write_text(updated, encoding="utf-8")
66
-
67
- print(f"Updated ArrayBridge version projections to {requested}")
68
- print("Run scripts/verify_release_ready.py --allow-dirty, then review and commit.")
69
-
70
-
71
- def main() -> None:
72
- parser = argparse.ArgumentParser(description=__doc__)
73
- parser.add_argument("version", help="New release version, for example 0.3.0")
74
- args = parser.parse_args()
75
- update_version(args.version)
76
-
77
-
78
- if __name__ == "__main__":
79
- main()
@@ -1,75 +0,0 @@
1
- """
2
- arraybridge: Unified API for NumPy, CuPy, PyTorch, TensorFlow, JAX, and pyclesperanto.
3
-
4
- This package provides automatic memory type conversion, declarative decorators,
5
- and unified utilities for working with multiple array/tensor frameworks.
6
- """
7
-
8
- __version__ = "0.3.2"
9
-
10
- from . import decorators as _decorators
11
- from .array_geometry import ArrayGeometry
12
- from .array_payload import ArrayPayload
13
- from .converters import convert_memory, detect_memory_type
14
- from .dtype_scaling import SCALING_FUNCTIONS
15
- from .exceptions import MemoryConversionError
16
- from .framework_config import _FRAMEWORK_CONFIG
17
- from .framework_ops import _FRAMEWORK_OPS
18
- from .gpu_cleanup import cleanup_all_gpu_frameworks
19
- from .oom_recovery import _execute_with_oom_recovery
20
- from .slice_processing import process_slices
21
- from .stack_utils import stack_slices, unstack_slices
22
- from .types import (
23
- CPU_MEMORY_TYPES,
24
- GPU_MEMORY_TYPES,
25
- SUPPORTED_MEMORY_TYPES,
26
- MemoryContractAttribute,
27
- MemoryType,
28
- )
29
- from .utils import _ensure_module, _get_device_id, _supports_dlpack
30
-
31
- DtypeConversion = _decorators.DtypeConversion
32
- SliceBySliceRuntimeParameter = _decorators.SliceBySliceRuntimeParameter
33
- memory_types = _decorators.memory_types
34
- wrap_dtype_preserving_callable = _decorators.wrap_dtype_preserving_callable
35
- for _memory_type in MemoryType:
36
- globals()[_memory_type.value] = getattr(_decorators, _memory_type.value)
37
-
38
- __all__ = [
39
- # Types
40
- "MemoryType",
41
- "MemoryContractAttribute",
42
- "ArrayPayload",
43
- "ArrayGeometry",
44
- "CPU_MEMORY_TYPES",
45
- "GPU_MEMORY_TYPES",
46
- "SUPPORTED_MEMORY_TYPES",
47
- # Converters
48
- "convert_memory",
49
- "detect_memory_type",
50
- # Decorators
51
- "memory_types",
52
- "DtypeConversion",
53
- "SliceBySliceRuntimeParameter",
54
- "wrap_dtype_preserving_callable",
55
- # Stack utilities
56
- "stack_slices",
57
- "unstack_slices",
58
- # Slice processing
59
- "process_slices",
60
- # GPU cleanup
61
- "cleanup_all_gpu_frameworks",
62
- # Exceptions
63
- "MemoryConversionError",
64
- # Scaling
65
- "SCALING_FUNCTIONS",
66
- # Framework config (internal but needed by some consumers)
67
- "_FRAMEWORK_CONFIG",
68
- "_FRAMEWORK_OPS",
69
- # OOM recovery
70
- "_execute_with_oom_recovery",
71
- # Utils
72
- "_ensure_module",
73
- "_supports_dlpack",
74
- "_get_device_id",
75
- ] + [memory_type.value for memory_type in MemoryType]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes