Repository: fsspec/filesystem_spec
Pull request #1645 (2024-07-08 17:16:53)

Correct ls() for parquet backed nested reference sets
Fixes https://github.com/fsspec/kerchunk/issues/474

@sreesanjeevkg , can you test, please?

sreesanjeevkg: @martindurant , how do I install the changes from your branch for testing ? 

[martindurant:ref_nested_ls_parq](https://github.com/martindurant/filesystem_spec/tree/ref_nested_ls_parq)
martindurant: I think
```bash
> pip install git+https://github.com/martindurant/filesystem_spec.git@ref_nested_ls_parq
```
sreesanjeevkg: Hey Martin, I am able to access the file from parquet. Thank you for your help and quick turnaround.

<img width="1328" alt="Screenshot 2024-07-08 at 12 37 30 PM" src="https://github.com/fsspec/filesystem_spec/assets/32449066/a6cb54c4-1cd5-4e0f-944c-158e7b2c5b86">

martindurant: A simpler line to open the data:
```
ds = xr.open_dataset("combined.parq", engine="kerchunk", chunks="auto", group="Solar_Zenith")
```

Patch:
diff --git a/fsspec/implementations/reference.py b/fsspec/implementations/reference.py
index e05ef40..58acd31 100644
--- a/fsspec/implementations/reference.py
+++ b/fsspec/implementations/reference.py
@@ -5,6 +5,7 @@ import itertools
 import logging
 import math
 import os
+from itertools import chain
 from functools import lru_cache
 from typing import TYPE_CHECKING
 
@@ -16,10 +17,10 @@ except ImportError:
     if not TYPE_CHECKING:
         import json
 
-from ..asyn import AsyncFileSystem
-from ..callbacks import DEFAULT_CALLBACK
-from ..core import filesystem, open, split_protocol
-from ..utils import isfilelike, merge_offset_ranges, other_paths
+from fsspec.asyn import AsyncFileSystem
+from fsspec.callbacks import DEFAULT_CALLBACK
+from fsspec.core import filesystem, open, split_protocol
+from fsspec.utils import isfilelike, merge_offset_ranges, other_paths
 
 logger = logging.getLogger("fsspec.reference")
 
@@ -131,7 +132,6 @@ class LazyReferenceMapper(collections.abc.MutableMapping):
         self.out_root = out_root or self.root
         self.cat_thresh = categorical_threshold
         self.cache_size = cache_size
-        self.dirs = None
         self.url = self.root + "/{field}/refs.{record}.parq"
         # TODO: derive fs from `root`
         self.fs = fsspec.filesystem("file") if fs is None else fs
@@ -195,32 +195,36 @@ class LazyReferenceMapper(collections.abc.MutableMapping):
         fs.pipe("/".join([root, ".zmetadata"]), json.dumps(met).encode())
         return LazyReferenceMapper(root, fs, **kwargs)
 
-    def listdir(self, basename=True):
+    @lru_cache()
+    def listdir(self):
         """List top-level directories"""
-        # cache me?
-        if self.dirs is None:
-            dirs = [p.split("/", 1)[0] for p in self.zmetadata]
-            self.dirs = {p for p in dirs if p and not p.startswith(".")}
-        listing = self.dirs
-        if basename:
-            listing = [os.path.basename(path) for path in listing]
-        return listing
+        dirs = (p.rsplit("/", 1)[0] for p in self.zmetadata if not p.startswith(".z"))
+        return set(dirs)
 
     def ls(self, path="", detail=True):
         """Shortcut file listings"""
-        if not path:
-            dirnames = self.listdir()
-            others = set(
-                [".zmetadata"]
-                + [name for name in self.zmetadata if "/" not in name]
-                + [name for name in self._items if "/" not in name]
-            )
+        path = path.rstrip("/")
+        pathdash = path + "/" if path else ""
+        dirnames = self.listdir()
+        dirs = [
+            d
+            for d in dirnames
+            if d.startswith(pathdash) and "/" not in d.lstrip(pathdash)
+        ]
+        if dirs:
+            others = {
+                f
+                for f in chain(
+                    [".zmetadata"],
+                    (name for name in self.zmetadata),
+                    (name for name in self._items),
+                )
+                if f.startswith(pathdash) and "/" not in f.lstrip(pathdash)
+            }
             if detail is False:
-                others.update(dirnames)
+                others.update(dirs)
                 return sorted(others)
-            dirinfo = [
-                {"name": name, "type": "directory", "size": 0} for name in dirnames
-            ]
+            dirinfo = [{"name": name, "type": "directory", "size": 0} for name in dirs]
             fileinfo = [
                 {
                     "name": name,
@@ -234,10 +238,7 @@ class LazyReferenceMapper(collections.abc.MutableMapping):
                 for name in others
             ]
             return sorted(dirinfo + fileinfo, key=lambda s: s["name"])
-        parts = path.split("/", 1)
-        if len(parts) > 1:
-            raise FileNotFoundError("Cannot list within directories right now")
-        field = parts[0]
+        field = path
         others = set(
             [name for name in self.zmetadata if name.startswith(f"{path}/")]
             + [name for name in self._items if name.startswith(f"{path}/")]


Test patch:
diff --git a/fsspec/implementations/tests/test_reference.py b/fsspec/implementations/tests/test_reference.py
index 762d831..d980cd1 100644
--- a/fsspec/implementations/tests/test_reference.py
+++ b/fsspec/implementations/tests/test_reference.py
@@ -759,3 +759,27 @@ def test_append_parquet(lazy_refs, m):
     with pytest.raises(KeyError):
         lazy2["data/0"]
     assert lazy2["data/1"] == b"Adata"
+
+
+def test_deep_parq(m):
+    zarr = pytest.importorskip("zarr")
+    lz = fsspec.implementations.reference.LazyReferenceMapper.create(
+        "memory://out.parq", fs=m
+    )
+    g = zarr.open_group(lz, mode="w")
+    g2 = g.create_group("instant")
+    g2.create_dataset(name="one", data=[1, 2, 3])
+    lz.flush()
+
+    lz = fsspec.implementations.reference.LazyReferenceMapper("memory://out.parq", fs=m)
+    g = zarr.open_group(lz)
+    assert g.instant.one[:].tolist() == [1, 2, 3]
+    assert sorted(_["name"] for _ in lz.ls("")) == [".zgroup", ".zmetadata", "instant"]
+    assert sorted(_["name"] for _ in lz.ls("instant")) == [
+        "instant/.zgroup",
+        "instant/one",
+    ]
+    assert sorted(_["name"] for _ in lz.ls("instant/one")) == [
+        "instant/one/.zarray",
+        "instant/one/0",
+    ]