Skip to content

Commit 48d5fa8

Browse files
committed
tests: add more cache tests and add instance_caches fixture
1 parent 8d9eaa9 commit 48d5fa8

File tree

3 files changed

+154
-18
lines changed

3 files changed

+154
-18
lines changed

fsspec/conftest.py

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
import subprocess
44
import sys
55
import time
6+
from collections import deque
7+
from collections.abc import Generator, Sequence
68

79
import pytest
810

911
import fsspec
10-
from fsspec.implementations.cached import CachingFileSystem
1112

1213

1314
@pytest.fixture()
@@ -27,16 +28,142 @@ def m():
2728
m.pseudo_dirs.append("")
2829

2930

30-
@pytest.fixture
31+
class InstanceCacheInspector:
32+
"""
33+
Helper class to inspect instance caches of filesystem classes in tests.
34+
"""
35+
36+
@staticmethod
37+
def classes_from_refs(
38+
cls_reference: tuple[str | type[fsspec.AbstractFileSystem], ...],
39+
/,
40+
*,
41+
empty_is_all: bool = True,
42+
) -> deque[type[fsspec.AbstractFileSystem]]:
43+
"""
44+
Convert class references (strings or types) to a deque of filesystem classes.
45+
46+
Parameters
47+
----------
48+
cls_reference:
49+
Tuple of class references as strings or types.
50+
Supports fqns, protocol names, or the class types themselves.
51+
empty_is_all:
52+
If True and no classes are specified, include all imported filesystem classes.
53+
54+
Returns
55+
-------
56+
fs_classes:
57+
Deque of filesystem classes corresponding to the provided references.
58+
"""
59+
classes: deque[type[fsspec.AbstractFileSystem]] = deque()
60+
61+
for ref in cls_reference:
62+
if isinstance(ref, str):
63+
try:
64+
cls = fsspec.get_filesystem_class(ref)
65+
except ValueError:
66+
module_name, _, class_name = ref.rpartition(".")
67+
module = __import__(module_name, fromlist=[class_name])
68+
cls = getattr(module, class_name)
69+
classes.append(cls)
70+
else:
71+
classes.append(ref)
72+
if empty_is_all and not classes:
73+
classes.append(fsspec.spec.AbstractFileSystem)
74+
return classes
75+
76+
def clear(
77+
self,
78+
*cls_reference: str | type[fsspec.AbstractFileSystem],
79+
recursive: bool = True,
80+
) -> None:
81+
"""
82+
Clear instance caches of specified filesystem classes.
83+
"""
84+
classes = self.classes_from_refs(cls_reference)
85+
# Clear specified classes and optionally their subclasses
86+
while classes:
87+
cls = classes.popleft()
88+
cls.clear_instance_cache()
89+
if recursive:
90+
subclasses = cls.__subclasses__()
91+
classes.extend(subclasses)
92+
93+
def gather_counts(
94+
self,
95+
*cls_reference: str | type[fsspec.AbstractFileSystem],
96+
omit_zero: bool = True,
97+
recursive: bool = True,
98+
) -> dict[str, int]:
99+
"""
100+
Gather counts of filesystem instances in the instance caches of all loaded classes.
101+
102+
Parameters
103+
----------
104+
cls_reference:
105+
class references as strings or types.
106+
omit_zero:
107+
Whether to omit instance types with no cached instances.
108+
recursive:
109+
Whether to include subclasses of the specified classes.
110+
"""
111+
out: dict[str, int] = {}
112+
classes = self.classes_from_refs(cls_reference)
113+
while classes:
114+
cls = classes.popleft()
115+
count = len(cls._cache)
116+
# note: skip intermediate AbstractFileSystem subclasses
117+
# if they proxy the protocol attribute via a property.
118+
if isinstance(cls.protocol, (Sequence, str)):
119+
key = cls.protocol if isinstance(cls.protocol, str) else cls.protocol[0]
120+
if count or not omit_zero:
121+
out[key] = count
122+
if recursive:
123+
subclasses = cls.__subclasses__()
124+
classes.extend(subclasses)
125+
return out
126+
127+
128+
@pytest.fixture(scope="function", autouse=True)
129+
def instance_caches() -> Generator[InstanceCacheInspector, None, None]:
130+
"""
131+
Fixture to ensure empty filesystem instance caches before and after a test.
132+
133+
Used by default for all tests.
134+
Clears caches of all imported filesystem classes.
135+
Can be used to write test assertions about instance caches.
136+
137+
Usage:
138+
139+
def test_something(instance_caches):
140+
# Test code here
141+
fsspec.open("file://abc")
142+
fsspec.open("memory://foo/bar")
143+
144+
# Test assertion
145+
assert instance_caches.gather_counts() == {"file": 1, "memory": 1}
146+
147+
Returns
148+
-------
149+
instance_caches: An instance cache inspector for clearing and inspecting caches.
150+
"""
151+
ic = InstanceCacheInspector()
152+
153+
ic.clear()
154+
try:
155+
yield ic
156+
finally:
157+
ic.clear()
158+
159+
160+
@pytest.fixture(scope="function")
31161
def ftp_writable(tmpdir):
32162
"""
33163
Fixture providing a writable FTP filesystem.
34164
"""
35165
pytest.importorskip("pyftpdlib")
36-
from fsspec.implementations.ftp import FTPFileSystem
37166

38-
FTPFileSystem.clear_instance_cache() # remove lingering connections
39-
CachingFileSystem.clear_instance_cache()
40167
d = str(tmpdir)
41168
with open(os.path.join(d, "out"), "wb") as f:
42169
f.write(b"hello" * 10000)

fsspec/implementations/tests/test_cached.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1361,8 +1361,30 @@ def test_local_temp_file_put_by_list2(protocol, mocker, tmp_path) -> None:
13611361

13621362

13631363
def test_simplecache_tokenization_independent_of_path():
1364+
# check that the tokenization is independent of the path
13641365
of0 = fsspec.open("simplecache::memory://foo/bar.txt")
13651366
of1 = fsspec.open("simplecache::memory://baz/qux.txt")
13661367
assert of0.path != of1.path
13671368
assert of0.fs._fs_token_ == of1.fs._fs_token_
13681369
assert of0.fs is of1.fs
1370+
1371+
1372+
def test_simplecache_instance_cache(instance_caches):
1373+
# check that the simplecache instance cache does not grow with every unique path
1374+
1375+
assert instance_caches.gather_counts() == {}
1376+
1377+
# check that the cache does not grow with multiple paths
1378+
fsspec.open("simplecache::memory://foo/bar.txt")
1379+
fsspec.open("simplecache::memory://bar/baz.txt")
1380+
fsspec.open("simplecache::memory://baz/qux.txt")
1381+
fsspec.open("simplecache::file:///foo/bar.txt")
1382+
fsspec.open("simplecache::memory://bar/baz.txt")
1383+
fsspec.open("simplecache::https://example.com/")
1384+
1385+
assert instance_caches.gather_counts() == {
1386+
"simplecache": 3,
1387+
"memory": 1,
1388+
"file": 1,
1389+
"https": 1,
1390+
}

fsspec/tests/test_core.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,6 @@
2020
)
2121

2222

23-
@pytest.fixture(scope="function", autouse=True)
24-
def clear_cachingfilesystem_instance_caches():
25-
# prevent test cross-contamination due to cached fs instances
26-
from fsspec.implementations.cached import CachingFileSystem
27-
28-
classes = [CachingFileSystem]
29-
while classes:
30-
cls = classes.pop()
31-
cls.clear_instance_cache()
32-
classes.extend(cls.__subclasses__())
33-
yield
34-
35-
3623
@contextmanager
3724
def tempzip(data=None):
3825
data = data or {}

0 commit comments

Comments
 (0)