Repository: fsspec/filesystem_spec

Issue:
Inconsistent return type in open_local for string and pathlib.Path
If `open_local` is given a string URL for a local file without globs a string is returned. If the same URL is given as a `pathlib.Path` a list is returned.

```python
import fsspec
from pathlib import Path

url = "/path/to/file"

fsspec.open_local(url)  # -> returns string

p = Path(url)
fsspec.open_local(p)  # -> returns list
```

Granted the `open_local` documentation says that arguments to `url` should be a string or a list of strings but since `open_files` can also handle `pathlib.Path` which is a useful feature I think this should be adapted (also documentation of return value could be better because even while it is implied and maybe even logical it is more transparent if the documentation makes it clear that a string or a list of strings can be returned).

Anyway I think the relevant line is: https://github.com/fsspec/filesystem_spec/blob/e20f626b87b5bb87d223495a56aefd768272a7ca/fsspec/core.py#L492

Discussion:
martindurant: I think you may need
```
from __future__ import annotations
```
to get those type things to import. Or just remove them.

Patch:
diff --git a/fsspec/asyn.py b/fsspec/asyn.py
index 8dbdc70..ee71c35 100644
--- a/fsspec/asyn.py
+++ b/fsspec/asyn.py
@@ -106,7 +106,7 @@ def sync(loop, func, *args, timeout=None, **kwargs):
 
 
 def sync_wrapper(func, obj=None):
-    """Given a function, make so can be called in async or blocking contexts
+    """Given a function, make so can be called in blocking contexts
 
     Leave obj=None if defining within a class. Pass the instance if attaching
     as an attribute of the instance.
diff --git a/fsspec/core.py b/fsspec/core.py
index c5bce58..a1e15b2 100644
--- a/fsspec/core.py
+++ b/fsspec/core.py
@@ -1,8 +1,11 @@
+from __future__ import annotations
+
 import io
 import logging
 import os
 import re
 from glob import has_magic
+from pathlib import Path
 
 # for backwards compat, we export cache things from here too
 from .caching import (  # noqa: F401
@@ -469,7 +472,11 @@ def open(
     return out[0]
 
 
-def open_local(url, mode="rb", **storage_options):
+def open_local(
+    url: str | list[str] | Path | list[Path],
+    mode: str = "rb",
+    **storage_options: dict,
+) -> str | list[str]:
     """Open file(s) which can be resolved to local
 
     For files which either are local, or get downloaded upon open
@@ -493,7 +500,7 @@ def open_local(url, mode="rb", **storage_options):
         )
     with of as files:
         paths = [f.name for f in files]
-    if isinstance(url, str) and not has_magic(url):
+    if (isinstance(url, str) and not has_magic(url)) or isinstance(url, Path):
         return paths[0]
     return paths
 
diff --git a/fsspec/implementations/reference.py b/fsspec/implementations/reference.py
index 674654d..e061cb9 100644
--- a/fsspec/implementations/reference.py
+++ b/fsspec/implementations/reference.py
@@ -150,8 +150,28 @@ class LazyReferenceMapper(collections.abc.MutableMapping):
         self.open_refs = open_refs
 
     @staticmethod
-    def create(record_size, root, fs, **kwargs):
+    def create(root, storage_options=None, fs=None, record_size=10000, **kwargs):
+        """Make empty parquet reference set
+
+        Parameters
+        ----------
+        root: str
+            Directory to contain the output; will be created
+        storage_options: dict | None
+            For making the filesystem to use for writing is fs is None
+        fs: FileSystem | None
+            Filesystem for writing
+        record_size: int
+            Number of references per parquet file
+        kwargs: passed to __init__
+
+        Returns
+        -------
+        LazyReferenceMapper instance
+        """
         met = {"metadata": {}, "record_size": record_size}
+        if fs is None:
+            fs, root = fsspec.core.url_to_fs(root, **(storage_options or {}))
         fs.makedirs(root, exist_ok=True)
         fs.pipe("/".join([root, ".zmetadata"]), json.dumps(met).encode())
         return LazyReferenceMapper(root, fs, **kwargs)
diff --git a/fsspec/registry.py b/fsspec/registry.py
index e20f693..db9d3d5 100644
--- a/fsspec/registry.py
+++ b/fsspec/registry.py
@@ -57,7 +57,7 @@ def register_implementation(name, cls, clobber=False, errtxt=None):
             _registry[name] = cls
 
 
-# protocols mapped to the class which implements them. This dict can
+# protocols mapped to the class which implements them. This dict can be
 # updated with register_implementation
 known_implementations = {
     "data": {"class": "fsspec.implementations.data.DataFileSystem"},
diff --git a/fsspec/utils.py b/fsspec/utils.py
index 34f1ad8..543bd89 100644
--- a/fsspec/utils.py
+++ b/fsspec/utils.py
@@ -436,6 +436,7 @@ def isfilelike(f: Any) -> TypeGuard[IO[bytes]]:
 
 
 def get_protocol(url: str) -> str:
+    url = stringify_path(url)
     parts = re.split(r"(\:\:|\://)", url, 1)
     if len(parts) > 1:
         return parts[0]


Test patch:
diff --git a/fsspec/tests/test_core.py b/fsspec/tests/test_core.py
index 6e8e1c7..a14f688 100644
--- a/fsspec/tests/test_core.py
+++ b/fsspec/tests/test_core.py
@@ -3,6 +3,7 @@ import pickle
 import tempfile
 import zipfile
 from contextlib import contextmanager
+from pathlib import Path
 
 import pytest
 
@@ -101,7 +102,7 @@ def test_openfile_open(m):
     assert m.size("somepath") == 5
 
 
-def test_open_local():
+def test_open_local_w_cache():
     d1 = str(tempfile.mkdtemp())
     f1 = os.path.join(d1, "f1")
     open(f1, "w").write("test1")
@@ -112,6 +113,45 @@ def test_open_local():
     assert d2 in fn
 
 
+def test_open_local_w_magic():
+    d1 = str(tempfile.mkdtemp())
+    f1 = os.path.join(d1, "f1")
+    open(f1, "w").write("test1")
+    fn = open_local(os.path.join(d1, "f*"))
+    assert len(fn) == 1
+    assert isinstance(fn, list)
+
+
+def test_open_local_w_list_of_str():
+    d1 = str(tempfile.mkdtemp())
+    f1 = os.path.join(d1, "f1")
+    open(f1, "w").write("test1")
+    fn = open_local([f1, f1])
+    assert len(fn) == 2
+    assert isinstance(fn, list)
+    assert all(isinstance(elem, str) for elem in fn)
+
+
+def test_open_local_w_path():
+    d1 = str(tempfile.mkdtemp())
+    f1 = os.path.join(d1, "f1")
+    open(f1, "w").write("test1")
+    p = Path(f1)
+    fn = open_local(p)
+    assert isinstance(fn, str)
+
+
+def test_open_local_w_list_of_path():
+    d1 = str(tempfile.mkdtemp())
+    f1 = os.path.join(d1, "f1")
+    open(f1, "w").write("test1")
+    p = Path(f1)
+    fn = open_local([p, p])
+    assert len(fn) == 2
+    assert isinstance(fn, list)
+    assert all(isinstance(elem, str) for elem in fn)
+
+
 def test_xz_lzma_compressions():
     pytest.importorskip("lzma")
     # Ensure that both 'xz' and 'lzma' compression names can be parsed
diff --git a/fsspec/tests/test_utils.py b/fsspec/tests/test_utils.py
index b5732e9..c7bad93 100644
--- a/fsspec/tests/test_utils.py
+++ b/fsspec/tests/test_utils.py
@@ -1,5 +1,6 @@
 import io
 import sys
+from pathlib import Path
 from unittest.mock import Mock
 
 import pytest
@@ -8,6 +9,7 @@ import fsspec.utils
 from fsspec.utils import (
     can_be_local,
     common_prefix,
+    get_protocol,
     infer_storage_options,
     merge_offset_ranges,
     mirror_from,
@@ -333,6 +335,24 @@ def test_log():
     assert logger.level == logging.DEBUG
 
 
+@pytest.mark.parametrize(
+    "par",
+    [
+        ("afile", "file"),
+        ("file://afile", "file"),
+        ("noproto://afile", "noproto"),
+        ("noproto::stuff", "noproto"),
+        ("simplecache::stuff", "simplecache"),
+        ("simplecache://stuff", "simplecache"),
+        ("s3://afile", "s3"),
+        (Path("afile"), "file"),
+    ],
+)
+def test_get_protocol(par):
+    url, outcome = par
+    assert get_protocol(url) == outcome
+
+
 @pytest.mark.parametrize(
     "par",
     [
@@ -342,6 +362,7 @@ def test_log():
         ("noproto::stuff", False),
         ("simplecache::stuff", True),
         ("simplecache://stuff", True),
+        (Path("afile"), True),
     ],
 )
 def test_can_local(par):