Skip to content

Commit 6cdc73a

Browse files
authored
Merge pull request #5 from 1kbgz/tkp/reg
Add python protocol handler
2 parents c4c924c + e594453 commit 6cdc73a

File tree

8 files changed

+156
-18
lines changed

8 files changed

+156
-18
lines changed

fsspec_python/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from .fs import *
12
from .importer import *
23
from .open import *
34

fsspec_python/fs.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from __future__ import annotations
2+
3+
import inspect
4+
5+
from fsspec import AbstractFileSystem, filesystem
6+
7+
from .importer import install_importer, uninstall_importer
8+
9+
__all__ = ("PythonFileSystem",)
10+
11+
12+
class PythonFileSystem(AbstractFileSystem):
13+
"""Python import filesystem"""
14+
15+
def __init__(self, target_protocol=None, target_options=None, fs=None, **kwargs):
16+
"""
17+
Args:
18+
target_protocol: str (optional) Target filesystem protocol. Provide either this or ``fs``.
19+
target_options: dict or None Passed to the instantiation of the FS, if fs is None.
20+
fs: filesystem instance The target filesystem to run against. Provide this or ``protocol``.
21+
"""
22+
super().__init__(**kwargs)
23+
if fs is None and target_protocol is None:
24+
raise ValueError("Please provide filesystem instance(fs) or target_protocol")
25+
if not (fs is None) ^ (target_protocol is None):
26+
raise ValueError("Both filesystems (fs) and target_protocol may not be both given.")
27+
28+
target_options = target_options or {}
29+
self.target_protocol = (
30+
target_protocol if isinstance(target_protocol, str) else (fs.protocol if isinstance(fs.protocol, str) else fs.protocol[0])
31+
)
32+
self.fs = fs if fs is not None else filesystem(target_protocol, **target_options)
33+
34+
if target_protocol and kwargs.get("fo"):
35+
install_importer(f"{self.target_protocol}://{kwargs['fo']}", **target_options)
36+
else:
37+
install_importer(self.fs, **target_options, **kwargs)
38+
39+
def close(self):
40+
uninstall_importer(self.target_protocol)
41+
self.fs.close()
42+
super().close()
43+
44+
def __getattribute__(self, item):
45+
if item in {
46+
"__init__",
47+
"__getattribute__",
48+
"__reduce__",
49+
"_make_local_details",
50+
"open",
51+
"cat",
52+
"cat_file",
53+
"_cat_file",
54+
"cat_ranges",
55+
"_cat_ranges",
56+
"get",
57+
"read_block",
58+
"tail",
59+
"head",
60+
"info",
61+
"ls",
62+
"exists",
63+
"isfile",
64+
"isdir",
65+
"_check_file",
66+
"_check_cache",
67+
"_mkcache",
68+
"clear_cache",
69+
"clear_expired_cache",
70+
"pop_from_cache",
71+
"local_file",
72+
"_paths_from_path",
73+
"get_mapper",
74+
"open_many",
75+
"commit_many",
76+
"hash_name",
77+
"__hash__",
78+
"__eq__",
79+
"to_json",
80+
"to_dict",
81+
"cache_size",
82+
"pipe_file",
83+
"pipe",
84+
"start_transaction",
85+
"end_transaction",
86+
}:
87+
return object.__getattribute__(self, item)
88+
d = object.__getattribute__(self, "__dict__")
89+
fs = d.get("fs", None) # fs is not immediately defined
90+
if item in d:
91+
return d[item]
92+
if fs is not None:
93+
if item in fs.__dict__:
94+
# attribute of instance
95+
return fs.__dict__[item]
96+
# attributed belonging to the target filesystem
97+
cls = type(fs)
98+
m = getattr(cls, item)
99+
if (inspect.isfunction(m) or inspect.isdatadescriptor(m)) and (not hasattr(m, "__self__") or m.__self__ is None):
100+
# instance method
101+
return m.__get__(fs, cls)
102+
return m # class method or attribute
103+
# attributes of the superclass, while target is being set up
104+
return super().__getattribute__(item)

fsspec_python/importer.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from importlib.machinery import SOURCE_SUFFIXES, ModuleSpec
66
from os.path import join
77
from types import ModuleType
8-
from typing import TYPE_CHECKING
8+
from typing import TYPE_CHECKING, Dict
99

1010
from fsspec import url_to_fs
1111
from fsspec.implementations.local import AbstractFileSystem
@@ -26,7 +26,11 @@ class FSSpecImportFinder(MetaPathFinder):
2626
def __init__(self, fsspec: str, **fsspec_args: str) -> None:
2727
self.fsspec_fs: AbstractFileSystem
2828
self.root: str
29-
self.fsspec_fs, self.root = url_to_fs(fsspec, **fsspec_args)
29+
if isinstance(fsspec, AbstractFileSystem):
30+
self.fsspec_fs = fsspec
31+
self.root = fsspec_args.get("fo", fsspec.root_marker)
32+
else:
33+
self.fsspec_fs, self.root = url_to_fs(fsspec, **fsspec_args)
3034
self.remote_modules: dict[str, str] = {}
3135

3236
def find_spec(self, fullname: str, path: Sequence[str | bytes] | None, target: ModuleType | None = None) -> ModuleSpec | None:
@@ -49,7 +53,7 @@ def unload(self) -> None:
4953

5054

5155
# Singleton for use elsewhere
52-
_finder: FSSpecImportFinder = None
56+
_finders: Dict[str, FSSpecImportFinder] = {}
5357

5458

5559
class FSSpecImportLoader(SourceLoader):
@@ -77,18 +81,30 @@ def install_importer(fsspec: str, **fsspec_args: str) -> FSSpecImportFinder:
7781
fsspec: fsspec filesystem string
7882
Returns: The finder instance that was installed.
7983
"""
80-
global _finder
81-
if _finder is None:
82-
_finder = FSSpecImportFinder(fsspec, **fsspec_args)
84+
if isinstance(fsspec, AbstractFileSystem):
85+
# Reassemble fsspec and args
86+
fsspec = f"{fsspec.protocol if isinstance(fsspec.protocol, str) else fsspec.protocol[0]}://{fsspec.root_marker}"
87+
fsspec_args = fsspec_args or {}
8388

84-
sys.meta_path.insert(0, _finder)
85-
return _finder
89+
global _finders
90+
if fsspec in _finders:
91+
return _finders[fsspec]
92+
_finders[fsspec] = FSSpecImportFinder(fsspec, **fsspec_args)
93+
sys.meta_path.insert(0, _finders[fsspec])
94+
return _finders[fsspec]
8695

8796

88-
def uninstall_importer() -> None:
97+
def uninstall_importer(fsspec: str = "") -> None:
8998
"""Uninstall the fsspec importer."""
90-
global _finder
91-
if _finder is not None and _finder in sys.meta_path:
92-
_finder.unload()
93-
sys.meta_path.remove(_finder)
94-
_finder = None
99+
global _finders
100+
if not fsspec:
101+
# clear last
102+
if not _finders:
103+
return
104+
fsspec = list(_finders.keys())[-1]
105+
if fsspec in _finders:
106+
finder = _finders[fsspec]
107+
del _finders[fsspec]
108+
if finder in sys.meta_path:
109+
finder.unload()
110+
sys.meta_path.remove(finder)

fsspec_python/open.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,8 @@ def install_open_hook(fsspec: str, **fsspec_args):
1818
root: str
1919
fsspec_fs, root = url_to_fs(fsspec, **fsspec_args)
2020

21-
print(root)
22-
2321
def open_from_fsspec(file, mode="r", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None):
2422
filename = join(root, file)
25-
print(filename)
26-
2723
if ("w" in mode or "a" in mode or "x" in mode) or fsspec_fs.exists(filename):
2824
return fsspec_fs.open(filename, mode=mode, encoding=encoding, errors=errors, newline=newline)
2925
return _original_open(file, mode, buffering, encoding, errors, newline, closefd, opener)

fsspec_python/tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from pathlib import Path
33

44
import pytest
5+
from fsspec import open
56

67
from fsspec_python import install_importer, uninstall_importer
78

@@ -29,3 +30,10 @@ def open_hook():
2930
install_open_hook(f"file://{Path(__file__).parent}/dump/")
3031
yield
3132
uninstall_open_hook()
33+
34+
35+
@pytest.fixture()
36+
def fs_importer():
37+
fs = open(f"python::file://{Path(__file__).parent}/local2")
38+
yield fs
39+
fs.close()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def baz():
2+
return "This is a local file."

fsspec_python/tests/test_fs.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class TestFs:
2+
def test_fs(self, fs_importer):
3+
fs = fs_importer
4+
import my_local_file2
5+
6+
assert my_local_file2.baz() == "This is a local file."
7+
8+
fs.close()

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ dependencies = [
3030
"fsspec",
3131
]
3232

33+
[project.entry-points."fsspec.specs"]
34+
python = "fsspec_python.fs.PythonFileSystem"
35+
3336
[project.optional-dependencies]
3437
develop = [
3538
"build",

0 commit comments

Comments
 (0)