22
33import json
44from asyncio import gather
5- from dataclasses import dataclass , field , replace
5+ from dataclasses import dataclass , field
66from itertools import starmap
77from logging import getLogger
88from typing import TYPE_CHECKING , Any , Generic , Literal , cast , overload
@@ -1104,15 +1104,15 @@ async def setitem(
11041104 )
11051105 return await self ._set_selection (indexer , value , prototype = prototype )
11061106
1107- async def resize (self , new_shape : ChunkCoords , delete_outside_chunks : bool = True ) -> Self :
1107+ async def resize (self , new_shape : ShapeLike , delete_outside_chunks : bool = True ) -> None :
1108+ new_shape = parse_shapelike (new_shape )
11081109 assert len (new_shape ) == len (self .metadata .shape )
11091110 new_metadata = self .metadata .update_shape (new_shape )
11101111
1111- # Remove all chunks outside of the new shape
1112- old_chunk_coords = set (self .metadata .chunk_grid .all_chunk_coords (self .metadata .shape ))
1113- new_chunk_coords = set (self .metadata .chunk_grid .all_chunk_coords (new_shape ))
1114-
11151112 if delete_outside_chunks :
1113+ # Remove all chunks outside of the new shape
1114+ old_chunk_coords = set (self .metadata .chunk_grid .all_chunk_coords (self .metadata .shape ))
1115+ new_chunk_coords = set (self .metadata .chunk_grid .all_chunk_coords (new_shape ))
11161116
11171117 async def _delete_key (key : str ) -> None :
11181118 await (self .store_path / key ).delete ()
@@ -1128,7 +1128,63 @@ async def _delete_key(key: str) -> None:
11281128
11291129 # Write new metadata
11301130 await self ._save_metadata (new_metadata )
1131- return replace (self , metadata = new_metadata )
1131+
1132+ # Update metadata (in place)
1133+ object .__setattr__ (self , "metadata" , new_metadata )
1134+
1135+ async def append (self , data : npt .ArrayLike , axis : int = 0 ) -> ChunkCoords :
1136+ """Append `data` to `axis`.
1137+
1138+ Parameters
1139+ ----------
1140+ data : array-like
1141+ Data to be appended.
1142+ axis : int
1143+ Axis along which to append.
1144+
1145+ Returns
1146+ -------
1147+ new_shape : tuple
1148+
1149+ Notes
1150+ -----
1151+ The size of all dimensions other than `axis` must match between this
1152+ array and `data`.
1153+ """
1154+ # ensure data is array-like
1155+ if not hasattr (data , "shape" ):
1156+ data = np .asanyarray (data )
1157+
1158+ self_shape_preserved = tuple (s for i , s in enumerate (self .shape ) if i != axis )
1159+ data_shape_preserved = tuple (s for i , s in enumerate (data .shape ) if i != axis )
1160+ if self_shape_preserved != data_shape_preserved :
1161+ raise ValueError (
1162+ f"shape of data to append is not compatible with the array. "
1163+ f"The shape of the data is ({ data_shape_preserved } )"
1164+ f"and the shape of the array is ({ self_shape_preserved } )."
1165+ "All dimensions must match except for the dimension being "
1166+ "appended."
1167+ )
1168+ # remember old shape
1169+ old_shape = self .shape
1170+
1171+ # determine new shape
1172+ new_shape = tuple (
1173+ self .shape [i ] if i != axis else self .shape [i ] + data .shape [i ]
1174+ for i in range (len (self .shape ))
1175+ )
1176+
1177+ # resize
1178+ await self .resize (new_shape )
1179+
1180+ # store data
1181+ append_selection = tuple (
1182+ slice (None ) if i != axis else slice (old_shape [i ], new_shape [i ])
1183+ for i in range (len (self .shape ))
1184+ )
1185+ await self .setitem (append_selection , data )
1186+
1187+ return new_shape
11321188
11331189 async def update_attributes (self , new_attributes : dict [str , JSON ]) -> Self :
11341190 # metadata.attributes is "frozen" so we simply clear and update the dict
@@ -1147,7 +1203,8 @@ async def info(self) -> None:
11471203 raise NotImplementedError
11481204
11491205
1150- @dataclass (frozen = True )
1206+ # TODO: Array can be a frozen data class again once property setters (e.g. shape) are removed
1207+ @dataclass (frozen = False )
11511208class Array :
11521209 """Instantiate an array from an initialized store."""
11531210
@@ -1297,6 +1354,11 @@ def shape(self) -> ChunkCoords:
12971354 """
12981355 return self ._async_array .shape
12991356
1357+ @shape .setter
1358+ def shape (self , value : ChunkCoords ) -> None :
1359+ """Sets the shape of the array by calling resize."""
1360+ self .resize (value )
1361+
13001362 @property
13011363 def chunks (self ) -> ChunkCoords :
13021364 """Returns a tuple of integers describing the length of each dimension of a chunk of the array.
@@ -2754,18 +2816,18 @@ def blocks(self) -> BlockIndex:
27542816 :func:`set_block_selection` for documentation and examples."""
27552817 return BlockIndex (self )
27562818
2757- def resize (self , new_shape : ChunkCoords ) -> Array :
2819+ def resize (self , new_shape : ShapeLike ) -> None :
27582820 """
27592821 Change the shape of the array by growing or shrinking one or more
27602822 dimensions.
27612823
2762- This method does not modify the original Array object. Instead, it returns a new Array
2763- with the specified shape.
2824+ Parameters
2825+ ----------
2826+ new_shape : tuple
2827+ New shape of the array.
27642828
27652829 Notes
27662830 -----
2767- When resizing an array, the data are not rearranged in any way.
2768-
27692831 If one or more dimensions are shrunk, any chunks falling outside the
27702832 new array shape will be deleted from the underlying store.
27712833 However, it is noteworthy that the chunks partially falling inside the new array
@@ -2778,7 +2840,6 @@ def resize(self, new_shape: ChunkCoords) -> Array:
27782840 >>> import zarr
27792841 >>> z = zarr.zeros(shape=(10000, 10000),
27802842 >>> chunk_shape=(1000, 1000),
2781- >>> store=StorePath(MemoryStore(mode="w")),
27822843 >>> dtype="i4",)
27832844 >>> z.shape
27842845 (10000, 10000)
@@ -2791,10 +2852,43 @@ def resize(self, new_shape: ChunkCoords) -> Array:
27912852 >>> z2.shape
27922853 (50, 50)
27932854 """
2794- resized = sync (self ._async_array .resize (new_shape ))
2795- # TODO: remove this cast when type inference improves
2796- _resized = cast (AsyncArray [ArrayV2Metadata ] | AsyncArray [ArrayV3Metadata ], resized )
2797- return type (self )(_resized )
2855+ sync (self ._async_array .resize (new_shape ))
2856+
2857+ def append (self , data : npt .ArrayLike , axis : int = 0 ) -> ChunkCoords :
2858+ """Append `data` to `axis`.
2859+
2860+ Parameters
2861+ ----------
2862+ data : array-like
2863+ Data to be appended.
2864+ axis : int
2865+ Axis along which to append.
2866+
2867+ Returns
2868+ -------
2869+ new_shape : tuple
2870+
2871+ Notes
2872+ -----
2873+ The size of all dimensions other than `axis` must match between this
2874+ array and `data`.
2875+
2876+ Examples
2877+ --------
2878+ >>> import numpy as np
2879+ >>> import zarr
2880+ >>> a = np.arange(10000000, dtype='i4').reshape(10000, 1000)
2881+ >>> z = zarr.array(a, chunks=(1000, 100))
2882+ >>> z.shape
2883+ (10000, 1000)
2884+ >>> z.append(a)
2885+ (20000, 1000)
2886+ >>> z.append(np.vstack([a, a]), axis=1)
2887+ (20000, 2000)
2888+ >>> z.shape
2889+ (20000, 2000)
2890+ """
2891+ return sync (self ._async_array .append (data , axis = axis ))
27982892
27992893 def update_attributes (self , new_attributes : dict [str , JSON ]) -> Array :
28002894 # TODO: remove this cast when type inference improves
0 commit comments