Skip to content

Commit 0c840dc

Browse files
authored
Merge pull request #1 from 1kbgz/tkp/init
Add initial support for loading from remote
2 parents 9b36326 + b8d2249 commit 0c840dc

File tree

8 files changed

+156
-0
lines changed

8 files changed

+156
-0
lines changed

.github/workflows/build.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ jobs:
5353

5454
- name: Test
5555
run: make coverage
56+
env:
57+
FSSPEC_S3_ENDPOINT_URL: ${{ secrets.FSSPEC_S3_ENDPOINT_URL }}
5658

5759
- name: Upload test results (Python)
5860
uses: actions/upload-artifact@v4

fsspec_python/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1+
from .importer import *
2+
from .open import *
3+
14
__version__ = "0.1.0"

fsspec_python/importer.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from __future__ import annotations
2+
3+
import sys
4+
from importlib.abc import MetaPathFinder, SourceLoader
5+
from importlib.machinery import SOURCE_SUFFIXES, ModuleSpec
6+
from os.path import join
7+
from types import ModuleType
8+
from typing import TYPE_CHECKING
9+
10+
from fsspec import url_to_fs
11+
from fsspec.implementations.local import AbstractFileSystem
12+
13+
if TYPE_CHECKING:
14+
from collections.abc import Sequence
15+
16+
17+
__all__ = (
18+
"FSSpecImportFinder",
19+
"FSSpecImportLoader",
20+
"install_importer",
21+
"uninstall_importer",
22+
)
23+
24+
25+
class FSSpecImportFinder(MetaPathFinder):
26+
def __init__(self, fsspec: str, **fsspec_args: str) -> None:
27+
self.fsspec_fs: AbstractFileSystem
28+
self.root: str
29+
self.fsspec_fs, self.root = url_to_fs(fsspec, **fsspec_args)
30+
self.remote_modules: dict[str, str] = {}
31+
32+
def find_spec(self, fullname: str, path: Sequence[str | bytes] | None, target: ModuleType | None = None) -> ModuleSpec | None:
33+
for suffix in SOURCE_SUFFIXES:
34+
filename = join(self.root, fullname.split(".")[-1] + suffix)
35+
if not self.fsspec_fs.exists(filename):
36+
continue
37+
self.remote_modules[fullname] = ModuleSpec(
38+
name=fullname, loader=FSSpecImportLoader(fullname, filename, self.fsspec_fs), origin=filename, is_package=False
39+
)
40+
return self.remote_modules[fullname]
41+
return None
42+
43+
def unload(self) -> None:
44+
# unimport all remote modules from sys.modules
45+
for mod in self.remote_modules:
46+
if mod in sys.modules:
47+
del sys.modules[mod]
48+
self.remote_modules = {}
49+
50+
51+
# Singleton for use elsewhere
52+
_finder: FSSpecImportFinder = None
53+
54+
55+
class FSSpecImportLoader(SourceLoader):
56+
def __init__(self, fullname: str, path: str, fsspec_fs: AbstractFileSystem):
57+
self.fullname = fullname
58+
self.path = path
59+
self.fsspec_fs = fsspec_fs
60+
61+
def get_filename(self, fullname: str) -> str: # noqa: ARG002
62+
return self.path
63+
64+
def get_data(self, path: str | bytes) -> bytes:
65+
with self.fsspec_fs.open(path, "rb") as f:
66+
return f.read()
67+
68+
# def exec_module(self, module: ModuleType) -> None:
69+
# source_bytes = self.get_data(self.get_filename(self.fullname))
70+
# source = source_bytes.decode("utf-8")
71+
72+
73+
def install_importer(fsspec: str, **fsspec_args: str) -> FSSpecImportFinder:
74+
"""Install the fsspec importer.
75+
76+
Args:
77+
fsspec: fsspec filesystem string
78+
Returns: The finder instance that was installed.
79+
"""
80+
global _finder
81+
if _finder is None:
82+
_finder = FSSpecImportFinder(fsspec, **fsspec_args)
83+
84+
sys.meta_path.insert(0, _finder)
85+
return _finder
86+
87+
88+
def uninstall_importer() -> None:
89+
"""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

fsspec_python/open.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import builtins
2+
3+
_original_open = builtins.open
4+
5+
__all__ = ("open_from_fsspec", "install_open_hook")
6+
7+
8+
def open_from_fsspec(file, mode="r", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None):
9+
print(f"Intercepted open call for: '{file}' with mode: '{mode}'")
10+
return _original_open(file, mode, buffering, encoding, errors, newline, closefd, opener)
11+
12+
13+
def install_open_hook():
14+
builtins.open = open_from_fsspec
15+
globals()["open"] = open_from_fsspec

fsspec_python/tests/conftest.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import os
2+
from pathlib import Path
3+
4+
import pytest
5+
6+
from fsspec_python import install_importer, uninstall_importer
7+
8+
9+
@pytest.fixture()
10+
def s3_importer():
11+
if not os.environ.get("FSSPEC_S3_ENDPOINT_URL"):
12+
pytest.skip("S3 not configured")
13+
install_importer("s3://timkpaine-public/projects/fsspec-python")
14+
yield
15+
uninstall_importer()
16+
17+
18+
@pytest.fixture()
19+
def local_importer():
20+
install_importer(f"file://{Path(__file__).parent}/local")
21+
yield
22+
uninstall_importer()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def bar():
2+
return "This is a local file."
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import os
2+
3+
import pytest
4+
5+
6+
class TestRemoteImport:
7+
@pytest.mark.skipif(not os.environ.get("FSSPEC_S3_ENDPOINT_URL"), reason="S3 not configured")
8+
def test_importer_s3(self, s3_importer):
9+
import my_remote_file
10+
11+
assert my_remote_file.foo() == "This is a remote file."
12+
13+
def test_importer_local(self, local_importer):
14+
import my_local_file
15+
16+
assert my_local_file.bar() == "This is a local file."

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ develop = [
4545
"twine",
4646
"uv",
4747
"wheel",
48+
# Tests
49+
"s3fs",
4850
]
4951

5052
[project.scripts]

0 commit comments

Comments
 (0)