Skip to content

Commit

Permalink
Merge pull request InsightSoftwareConsortium#1134 from thewtex/python…
Browse files Browse the repository at this point in the history
…-cache-dir

feat(python): support ITKWASM_CACHE_DIR for cache directory
  • Loading branch information
thewtex authored May 21, 2024
2 parents 642526d + dc4b8a7 commit e290a8b
Show file tree
Hide file tree
Showing 40 changed files with 662 additions and 390 deletions.
69 changes: 37 additions & 32 deletions packages/core/python/itkwasm/itkwasm/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""itkwasm: Python interface to itk-wasm WebAssembly modules."""

__version__ = "1.0b171"
__version__ = "1.0b175"

from .interface_types import InterfaceTypes
from .image import Image, ImageType
from .image import Image, ImageType, ImageRegion
from .pointset import PointSet, PointSetType
from .mesh import Mesh, MeshType
from .polydata import PolyData, PolyDataType
Expand All @@ -21,36 +21,41 @@
from .environment_dispatch import environment_dispatch, function_factory
from .cast_image import cast_image
from .image_from_array import image_from_array
from .to_numpy_array import array_like_to_numpy_array, array_like_to_bytes, buffer_to_numpy_array
from .to_numpy_array import (
array_like_to_numpy_array,
array_like_to_bytes,
buffer_to_numpy_array,
)
from .to_cupy_array import array_like_to_cupy_array

__all__ = [
"InterfaceTypes",
"Pipeline",
"PipelineInput",
"PipelineOutput",
"Image",
"ImageType",
"PointSet",
"PointSetType",
"Mesh",
"MeshType",
"PolyData",
"PolyDataType",
"BinaryFile",
"BinaryStream",
"TextFile",
"TextStream",
"JsonCompatible",
"FloatTypes",
"IntTypes",
"PixelTypes",
"environment_dispatch",
"function_factory",
"cast_image",
"image_from_array",
"array_like_to_numpy_array",
"array_like_to_bytes",
"array_like_to_cupy_array",
"buffer_to_numpy_array",
]
"InterfaceTypes",
"Pipeline",
"PipelineInput",
"PipelineOutput",
"Image",
"ImageType",
"ImageRegion",
"PointSet",
"PointSetType",
"Mesh",
"MeshType",
"PolyData",
"PolyDataType",
"BinaryFile",
"BinaryStream",
"TextFile",
"TextStream",
"JsonCompatible",
"FloatTypes",
"IntTypes",
"PixelTypes",
"environment_dispatch",
"function_factory",
"cast_image",
"image_from_array",
"array_like_to_numpy_array",
"array_like_to_bytes",
"array_like_to_cupy_array",
"buffer_to_numpy_array",
]
3 changes: 2 additions & 1 deletion packages/core/python/itkwasm/itkwasm/binary_file.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from dataclasses import dataclass
from pathlib import PurePosixPath


@dataclass
class BinaryFile:
path: PurePosixPath
path: PurePosixPath
3 changes: 2 additions & 1 deletion packages/core/python/itkwasm/itkwasm/binary_stream.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from dataclasses import dataclass


@dataclass
class BinaryStream:
data: bytes
data: bytes
11 changes: 7 additions & 4 deletions packages/core/python/itkwasm/itkwasm/cast_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@
from .int_types import IntTypes
from .float_types import FloatTypes

def cast_image(input_image: Image,
pixel_type: Optional[PixelTypes]=None,
component_type: Optional[Union[IntTypes, FloatTypes]]=None) -> Image:

def cast_image(
input_image: Image,
pixel_type: Optional[PixelTypes] = None,
component_type: Optional[Union[IntTypes, FloatTypes]] = None,
) -> Image:
"""Cast an image to another pixel type and / or component type.
:param input_image: Image to be cast.
Expand Down Expand Up @@ -72,6 +75,6 @@ def cast_image(input_image: Image,
elif component_type == FloatTypes.Float64:
output_image.data = input_image.data.astype(np.float64)
else:
raise ValueError('Unsupported component type')
raise ValueError("Unsupported component type")

return output_image
29 changes: 18 additions & 11 deletions packages/core/python/itkwasm/itkwasm/environment_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@
import sys
import importlib
import sys

if sys.version_info < (3, 10):
from importlib_metadata import entry_points
else:
from importlib.metadata import entry_points


class FunctionFactory:
def __init__(self):
self._registered: Dict[Tuple[str, str], Set[Callable]] = {}
self._priorities: Dict[Callable, int] = {}
self._has_entry_point_lookup: Set[Tuple[str, str]] = set()

def register(self, interface_package: str, func_name: str, func: Callable, priority: int=1)-> None:
def register(self, interface_package: str, func_name: str, func: Callable, priority: int = 1) -> None:
key = (interface_package, func_name)
registered = self._registered.get(key, set())
registered.add(func)
Expand All @@ -23,9 +25,9 @@ def register(self, interface_package: str, func_name: str, func: Callable, prior
def lookup(self, interface_package: str, func_name: str) -> Optional[Set[Callable]]:
key = (interface_package, func_name)
if not key in self._has_entry_point_lookup:
discovered_funcs = entry_points(group=f'{interface_package}.{func_name}')
discovered_funcs = entry_points(group=f"{interface_package}.{func_name}")
for ep in discovered_funcs:
priority = ep.name.partition('.priority.')[2]
priority = ep.name.partition(".priority.")[2]
if priority:
priority = int(priority)
else:
Expand All @@ -40,17 +42,20 @@ def highest_priority(self, interface_package: str, func_name: str) -> Optional[C
registered = self.lookup(interface_package, func_name)
if registered is None:
return None
highest = max(self._registered[(interface_package, func_name)], key=lambda x: self._priorities[x])
highest = max(
self._registered[(interface_package, func_name)],
key=lambda x: self._priorities[x],
)
if self._priorities[highest] < 1:
return None
return highest

def set_priority(self, func: Callable, priority: int)-> None:
def set_priority(self, func: Callable, priority: int) -> None:
if func not in self._priorities:
raise ValueError(f"Function {func} has not been registered")
self._priorities[func] = priority

def get_priority(self, func: Callable)-> int:
def get_priority(self, func: Callable) -> int:
if func not in self._priorities:
raise ValueError(f"Function {func} has not been registered")
return self._priorities[func]
Expand All @@ -61,22 +66,24 @@ def disable(self, interface_package: str, func_name: str):
for func in registered:
self._priorities[func] = -1


function_factory = FunctionFactory()


def environment_dispatch(interface_package: str, func_name: str) -> Callable:
factory_func = function_factory.highest_priority(interface_package, func_name)
if factory_func is not None:
return factory_func

if sys.platform != "emscripten":
if func_name.endswith('_async'):
raise ValueError('async function are only implemented for emscripten')
if func_name.endswith("_async"):
raise ValueError("async function are only implemented for emscripten")
package = f"{interface_package}_wasi"
else:
if not func_name.endswith('_async'):
raise ValueError('emscripten only implements the _async version of this function')
if not func_name.endswith("_async"):
raise ValueError("emscripten only implements the _async version of this function")
package = f"{interface_package}_emscripten"
mod = importlib.import_module(package)
func = getattr(mod, func_name)

return func
return func
9 changes: 5 additions & 4 deletions packages/core/python/itkwasm/itkwasm/float_types.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from enum import Enum


class FloatTypes(str, Enum):
Float32 = 'float32'
Float64 = 'float64'
SpacePrecisionType = 'float64'
Float32 = "float32"
Float64 = "float64"
SpacePrecisionType = "float64"

def __str__(self):
return str(self.value)

def __repr__(self):
return f'"{str(self.value)}"'
return f'"{str(self.value)}"'
33 changes: 29 additions & 4 deletions packages/core/python/itkwasm/itkwasm/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,40 +12,65 @@
from .float_types import FloatTypes
from .pixel_types import PixelTypes


@dataclass
class ImageType:
dimension: int = 2
componentType: Union[IntTypes, FloatTypes] = IntTypes.UInt8
pixelType: PixelTypes = PixelTypes.Scalar
components: int = 1


def _default_direction() -> ArrayLike:
return np.empty((0,), np.float64)


@dataclass
class ImageRegion:
index: Sequence[int] = field(default_factory=list)
size: Sequence[int] = field(default_factory=list)


@dataclass
class Image:
imageType: Union[ImageType, Dict] = field(default_factory=ImageType)
name: str = 'Image'
name: str = "Image"
origin: Sequence[float] = field(default_factory=list)
spacing: Sequence[float] = field(default_factory=list)
direction: ArrayLike = field(default_factory=_default_direction)
size: Sequence[int] = field(default_factory=list)
metadata: Dict = field(default_factory=dict)
data: Optional[ArrayLike] = None
bufferedRegion: Optional[ImageRegion] = None

def __post_init__(self):
if isinstance(self.imageType, dict):
self.imageType = ImageType(**self.imageType)

dimension = self.imageType.dimension
if len(self.origin) == 0:
self.origin += [0.0,] * dimension
self.origin += [
0.0,
] * dimension

if len(self.spacing) == 0:
self.spacing += [1.0,] * dimension
self.spacing += [
1.0,
] * dimension

if len(self.direction) == 0:
self.direction = np.eye(dimension).astype(np.float64)

if len(self.size) == 0:
self.size += [1,] * dimension
self.size += [
1,
] * dimension

if self.bufferedRegion is None:
self.bufferedRegion = ImageRegion(
index=[
0,
]
* dimension,
size=self.size,
)
4 changes: 3 additions & 1 deletion packages/core/python/itkwasm/itkwasm/image_from_array.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Optional

try:
from numpy.typing import ArrayLike
except ImportError:
Expand All @@ -9,6 +10,7 @@

from .pixel_types import PixelTypes


def image_from_array(arr, is_vector: bool = False, image_type: Optional[ImageType] = None) -> Image:
"""Convert a numpy array-like to an itkwasm Image.
Expand All @@ -33,7 +35,7 @@ def image_from_array(arr, is_vector: bool = False, image_type: Optional[ImageTyp
image_type = ImageType(
dimension=dimension,
componentType=_dtype_to_component_type(arr.dtype),
pixelType=PixelTypes.Scalar if not is_vector else PixelTypes.VariableLengthVector,
pixelType=(PixelTypes.Scalar if not is_vector else PixelTypes.VariableLengthVector),
components=arr.shape[-1] if is_vector else 1,
)

Expand Down
25 changes: 13 additions & 12 deletions packages/core/python/itkwasm/itkwasm/int_types.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
from enum import Enum


class IntTypes(str, Enum):
Int8 = 'int8'
UInt8 = 'uint8'
Int16 = 'int16'
UInt16 = 'uint16'
Int32 = 'int32'
UInt32 = 'uint32'
Int64 = 'int64'
UInt64 = 'uint64'
Int8 = "int8"
UInt8 = "uint8"
Int16 = "int16"
UInt16 = "uint16"
Int32 = "int32"
UInt32 = "uint32"
Int64 = "int64"
UInt64 = "uint64"

SizeValueType = 'uint64'
IdentifierType = 'uint64'
IndexValueType = 'int64'
OffsetValueType = 'int64'
SizeValueType = "uint64"
IdentifierType = "uint64"
IndexValueType = "int64"
OffsetValueType = "int64"

def __str__(self):
return str(self.value)
Expand Down
17 changes: 9 additions & 8 deletions packages/core/python/itkwasm/itkwasm/interface_types.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from enum import Enum


class InterfaceTypes(Enum):
TextFile = 'InterfaceTextFile'
BinaryFile = 'InterfaceBinaryFile'
TextStream = 'InterfaceTextStream'
BinaryStream = 'InterfaceBinaryStream'
Image = 'InterfaceImage'
Mesh = 'InterfaceMesh'
PolyData = 'InterfacePolyData'
JsonCompatible = 'InterfaceJsonCompatible'
TextFile = "InterfaceTextFile"
BinaryFile = "InterfaceBinaryFile"
TextStream = "InterfaceTextStream"
BinaryStream = "InterfaceBinaryStream"
Image = "InterfaceImage"
Mesh = "InterfaceMesh"
PolyData = "InterfacePolyData"
JsonCompatible = "InterfaceJsonCompatible"
3 changes: 1 addition & 2 deletions packages/core/python/itkwasm/itkwasm/json_compatible.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from typing import Dict, Union, List

JsonCompatible = Union[Dict[str, "JsonCompatible"], None, bool, str, int,
float, List["JsonCompatible"]]
JsonCompatible = Union[Dict[str, "JsonCompatible"], None, bool, str, int, float, List["JsonCompatible"]]
Loading

0 comments on commit e290a8b

Please sign in to comment.