Repository: zarr-developers/zarr-python

Issue:
v3 arrays have no `.name` attribute
V2 arrays have a `.name`: https://zarr.readthedocs.io/en/stable/_autoapi/zarr.core.Array.html#zarr.core.Array.name

V3 arrays don't:

```python
import zarr
arr = zarr.Array.create(store=zarr.store.MemoryStore(), shape=(10,), chunks=(5,), dtype="i4")
arr.name
# attribute error
```

It would be a pretty disruptive API change to remove this.

Discussion:

Patch:
diff --git a/src/zarr/array.py b/src/zarr/array.py
index a45e7c6b..3e6cf553 100644
--- a/src/zarr/array.py
+++ b/src/zarr/array.py
@@ -378,6 +378,29 @@ class AsyncArray:
     def attrs(self) -> dict[str, JSON]:
         return self.metadata.attributes
 
+    @property
+    def path(self) -> str:
+        """Storage path."""
+        return self.store_path.path
+
+    @property
+    def name(self) -> str | None:
+        """Array name following h5py convention."""
+        if self.path:
+            # follow h5py convention: add leading slash
+            name = self.path
+            if name[0] != "/":
+                name = "/" + name
+            return name
+        return None
+
+    @property
+    def basename(self) -> str | None:
+        """Final component of name."""
+        if self.name is not None:
+            return self.name.split("/")[-1]
+        return None
+
     async def _get_selection(
         self,
         indexer: Indexer,
@@ -630,6 +653,21 @@ class Array:
     def attrs(self) -> Attributes:
         return Attributes(self)
 
+    @property
+    def path(self) -> str:
+        """Storage path."""
+        return self._async_array.path
+
+    @property
+    def name(self) -> str | None:
+        """Array name following h5py convention."""
+        return self._async_array.name
+
+    @property
+    def basename(self) -> str | None:
+        """Final component of name."""
+        return self._async_array.basename
+
     @property
     def metadata(self) -> ArrayMetadata:
         return self._async_array.metadata
diff --git a/src/zarr/group.py b/src/zarr/group.py
index 88e7fd09..ccad0e56 100644
--- a/src/zarr/group.py
+++ b/src/zarr/group.py
@@ -270,6 +270,27 @@ class AsyncGroup:
         awaitables = [set_or_delete(self.store_path / key, value) for key, value in to_save.items()]
         await asyncio.gather(*awaitables)
 
+    @property
+    def path(self) -> str:
+        """Storage path."""
+        return self.store_path.path
+
+    @property
+    def name(self) -> str:
+        """Group name following h5py convention."""
+        if self.path:
+            # follow h5py convention: add leading slash
+            name = self.path
+            if name[0] != "/":
+                name = "/" + name
+            return name
+        return "/"
+
+    @property
+    def basename(self) -> str:
+        """Final component of name."""
+        return self.name.split("/")[-1]
+
     @property
     def attrs(self) -> dict[str, Any]:
         return self.metadata.attributes
@@ -462,6 +483,7 @@ class Group(SyncMixin):
         store: StoreLike,
         *,
         attributes: dict[str, Any] = {},  # noqa: B006, FIXME
+        zarr_format: ZarrFormat = 3,
         exists_ok: bool = False,
     ) -> Group:
         obj = sync(
@@ -469,6 +491,7 @@ class Group(SyncMixin):
                 store,
                 attributes=attributes,
                 exists_ok=exists_ok,
+                zarr_format=zarr_format,
             ),
         )
 
@@ -521,6 +544,21 @@ class Group(SyncMixin):
     def metadata(self) -> GroupMetadata:
         return self._async_group.metadata
 
+    @property
+    def path(self) -> str:
+        """Storage path."""
+        return self._async_group.path
+
+    @property
+    def name(self) -> str:
+        """Group name following h5py convention."""
+        return self._async_group.name
+
+    @property
+    def basename(self) -> str:
+        """Final component of name."""
+        return self._async_group.basename
+
     @property
     def attrs(self) -> Attributes:
         return Attributes(self)


Test patch:
diff --git a/tests/v3/test_array.py b/tests/v3/test_array.py
new file mode 100644
index 00000000..203cfbf8
--- /dev/null
+++ b/tests/v3/test_array.py
@@ -0,0 +1,36 @@
+import pytest
+
+from zarr.array import Array
+from zarr.common import ZarrFormat
+from zarr.group import Group
+from zarr.store import LocalStore, MemoryStore
+
+
+@pytest.mark.parametrize("store", ("local", "memory"), indirect=["store"])
+@pytest.mark.parametrize("zarr_format", (2, 3))
+def test_array_name_properties_no_group(
+    store: LocalStore | MemoryStore, zarr_format: ZarrFormat
+) -> None:
+    arr = Array.create(store=store, shape=(100,), chunks=(10,), zarr_format=zarr_format, dtype="i4")
+    assert arr.path == ""
+    assert arr.name is None
+    assert arr.basename is None
+
+
+@pytest.mark.parametrize("store", ("local", "memory"), indirect=["store"])
+@pytest.mark.parametrize("zarr_format", (2, 3))
+def test_array_name_properties_with_group(
+    store: LocalStore | MemoryStore, zarr_format: ZarrFormat
+) -> None:
+    root = Group.create(store=store, zarr_format=zarr_format)
+    foo = root.create_array("foo", shape=(100,), chunks=(10,), dtype="i4")
+    assert foo.path == "foo"
+    assert foo.name == "/foo"
+    assert foo.basename == "foo"
+
+    bar = root.create_group("bar")
+    spam = bar.create_array("spam", shape=(100,), chunks=(10,), dtype="i4")
+
+    assert spam.path == "bar/spam"
+    assert spam.name == "/bar/spam"
+    assert spam.basename == "spam"
diff --git a/tests/v3/test_group.py b/tests/v3/test_group.py
index 4d55d722..9ce9b07a 100644
--- a/tests/v3/test_group.py
+++ b/tests/v3/test_group.py
@@ -372,3 +372,22 @@ def test_group_init(store: LocalStore | MemoryStore, zarr_format: ZarrFormat) ->
     agroup = sync(AsyncGroup.create(store=store, zarr_format=zarr_format))
     group = Group(agroup)
     assert group._async_group == agroup
+
+
+@pytest.mark.parametrize("store", ("local", "memory"), indirect=["store"])
+@pytest.mark.parametrize("zarr_format", (2, 3))
+def test_group_name_properties(store: LocalStore | MemoryStore, zarr_format: ZarrFormat) -> None:
+    root = Group.create(store=store, zarr_format=zarr_format)
+    assert root.path == ""
+    assert root.name == "/"
+    assert root.basename == ""
+
+    foo = root.create_group("foo")
+    assert foo.path == "foo"
+    assert foo.name == "/foo"
+    assert foo.basename == "foo"
+
+    bar = root.create_group("foo/bar")
+    assert bar.path == "foo/bar"
+    assert bar.name == "/foo/bar"
+    assert bar.basename == "bar"