Skip to content

Commit 53b6c0d

Browse files
authored
Merge pull request #1 from 1kbgz/tkp/init
Add support for union of python filesystems
2 parents 57021f0 + 4fd26ea commit 53b6c0d

File tree

8 files changed

+125
-1
lines changed

8 files changed

+125
-1
lines changed

fsspec_union/fs.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from __future__ import annotations
2+
3+
from fsspec import filesystem
4+
from fsspec.implementations.chained import ChainedFileSystem
5+
6+
__all__ = ("UnionFileSystem",)
7+
8+
9+
class UnionFileSystem(ChainedFileSystem):
10+
"""Union filesystem"""
11+
12+
def __init__(self, target_protocol=None, target_options=None, fs=None, **kwargs):
13+
"""
14+
Args:
15+
target_protocol: str (optional) Target filesystem protocol. Provide either this or ``fs``.
16+
target_options: dict or None Passed to the instantiation of the FS, if fs is None.
17+
fs: filesystem instance The target filesystem to run against. Provide this or ``protocol``.
18+
"""
19+
super().__init__(**kwargs)
20+
if fs is None and target_protocol is None:
21+
raise ValueError("Please provide filesystem instance(fs) or target_protocol")
22+
if not (fs is None) ^ (target_protocol is None):
23+
raise ValueError("Both filesystems (fs) and target_protocol may not be both given.")
24+
25+
if target_protocol:
26+
# unpack the targets and then instantiate in reverse order
27+
fs_options = [{"target_protocol": target_protocol, "target_options": kwargs}]
28+
fss = []
29+
30+
while "target_options" in target_options:
31+
target_protocol = target_options.pop("target_protocol")
32+
new_target_options = target_options.pop("target_options")
33+
kwargs = target_options
34+
fs_options.append({"target_protocol": target_protocol, "target_options": kwargs})
35+
target_options = new_target_options
36+
37+
# instantiate in reverse order
38+
for fspec in reversed(fs_options):
39+
target_protocol = fspec["target_protocol"]
40+
target_options = fspec["target_options"]
41+
fss.append(filesystem(target_protocol, fs=fss[-1] if fss else None, **target_options))
42+
fss.reverse()
43+
self.fss = fss
44+
self.fs = fss[0]
45+
else:
46+
self.fss = [fs]
47+
self.fs = fs
48+
49+
def exit(self):
50+
for fs in self.fss:
51+
if hasattr(fs, "exit"):
52+
fs.exit()

fsspec_union/tests/conftest.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import importlib
2+
import sys
3+
from pathlib import Path
4+
5+
import pytest
6+
from fsspec import url_to_fs
7+
8+
9+
@pytest.fixture(scope="function")
10+
def fs_union_importer():
11+
sys_meta_path_length = len(sys.meta_path)
12+
fs, _ = url_to_fs(f"union::python::file://{Path(__file__).parent}/local1::python::file://{Path(__file__).parent}/local2")
13+
import masked
14+
15+
importlib.reload(masked)
16+
yield fs
17+
fs.exit()
18+
sys.modules.pop("masked", None)
19+
assert len(sys.meta_path) == sys_meta_path_length
20+
21+
22+
@pytest.fixture(scope="function")
23+
def fs_union_importer_inverse():
24+
sys_meta_path_length = len(sys.meta_path)
25+
fs, _ = url_to_fs(f"union::python::file://{Path(__file__).parent}/local2::python::file://{Path(__file__).parent}/local1")
26+
import masked
27+
28+
importlib.reload(masked)
29+
yield fs
30+
fs.exit()
31+
sys.modules.pop("masked", None)
32+
assert len(sys.meta_path) == sys_meta_path_length
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def masked():
2+
return 1
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def foo1():
2+
return "This is a local file 1."
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def masked():
2+
return 2
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def foo2():
2+
return "This is a local file 2."

fsspec_union/tests/test_fs.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
class TestFs:
2+
def test_fs_union(self, fs_union_importer):
3+
import my_local_file1
4+
import my_local_file2
5+
6+
assert my_local_file1.foo1() == "This is a local file 1."
7+
assert my_local_file2.foo2() == "This is a local file 2."
8+
9+
import masked
10+
11+
assert masked.masked() == 1
12+
13+
def test_fs_union_inverse(self, fs_union_importer_inverse):
14+
import my_local_file1
15+
import my_local_file2
16+
17+
assert my_local_file1.foo1() == "This is a local file 1."
18+
assert my_local_file2.foo2() == "This is a local file 2."
19+
20+
import masked
21+
22+
assert masked.masked() == 2

pyproject.toml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,13 @@ classifiers = [
2626
"Programming Language :: Python :: 3.14",
2727
]
2828

29-
dependencies = []
29+
dependencies = [
30+
"fsspec@git+https://github.com/fsspec/filesystem_spec#egg=c23674c4c7bd60f8c3b41d0c4bbcefe4d18d6c70",
31+
"fsspec-python@git+https://github.com/1kbgz/fsspec-python#egg=9dfe6ecc5410ef871083f2b0d0cbb008acdb7f4d",
32+
]
33+
34+
[project.entry-points."fsspec.specs"]
35+
union = "fsspec_union.fs.UnionFileSystem"
3036

3137
[project.optional-dependencies]
3238
develop = [
@@ -88,6 +94,10 @@ exclude_also = [
8894
ignore_errors = true
8995
fail_under = 50
9096

97+
# TODO remove
98+
[tool.hatch.metadata]
99+
allow-direct-references = true
100+
91101
[tool.hatch.build]
92102
artifacts = []
93103

0 commit comments

Comments
 (0)