Skip to content

Commit df97e1e

Browse files
Merge pull request #47 from scalableminds/fix-pickle
Fix serialization of UPath
2 parents 747cc18 + 2cfc277 commit df97e1e

File tree

4 files changed

+121
-12
lines changed

4 files changed

+121
-12
lines changed

noxfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def install(session):
2828
@nox.session(python=False)
2929
def smoke(session):
3030
session.install(*"pytest aiohttp requests gcsfs".split())
31-
session.run(*"pytest --skiphdfs upath".split())
31+
session.run(*"pytest --skiphdfs -vv upath".split())
3232

3333

3434
@nox.session(python=False)

upath/core.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,3 +325,42 @@ def _from_parsed_parts(self, drv, root, parts, init=True):
325325
if init:
326326
obj._init(**self._kwargs)
327327
return obj
328+
329+
def __truediv__(self, key):
330+
# Add `/` root if not present
331+
if len(self._parts) == 0:
332+
key = f"{self._root}{key}"
333+
334+
# Adapted from `PurePath._make_child`
335+
drv, root, parts = self._parse_args((key,))
336+
drv, root, parts = self._flavour.join_parsed_parts(
337+
self._drv, self._root, self._parts, drv, root, parts
338+
)
339+
340+
kwargs = self._kwargs.copy()
341+
kwargs.pop("_url")
342+
343+
# Create a new object
344+
out = self.__class__(
345+
self._format_parsed_parts(drv, root, parts),
346+
**kwargs,
347+
)
348+
return out
349+
350+
def __setstate__(self, state):
351+
kwargs = state["_kwargs"].copy()
352+
kwargs["_url"] = self._url
353+
self._kwargs = kwargs
354+
# _init needs to be called again, because when __new__ called _init,
355+
# the _kwargs were not yet set
356+
self._init()
357+
358+
def __reduce__(self):
359+
kwargs = self._kwargs.copy()
360+
kwargs.pop("_url", None)
361+
362+
return (
363+
self.__class__,
364+
(self._format_parsed_parts(self._drv, self._root, self._parts),),
365+
{"_kwargs": kwargs},
366+
)

upath/tests/cases.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import pickle
2+
import sys
13
from pathlib import Path
24

35
import pytest
4-
56
from upath import UPath
67

78

@@ -37,15 +38,14 @@ def test_glob(self, pathlib_base):
3738
mock_glob = list(self.path.glob("**.txt"))
3839
path_glob = list(pathlib_base.glob("**/*.txt"))
3940

40-
assert len(mock_glob) == len(path_glob)
41-
assert all(
42-
map(
43-
lambda m: m.path
44-
in [str(p).replace("\\", "/") for p in path_glob],
45-
mock_glob,
46-
)
41+
root = "/" if sys.platform.startswith("win") else ""
42+
mock_glob_normalized = sorted([a.path for a in mock_glob])
43+
path_glob_normalized = sorted(
44+
[f"{root}{a}".replace("\\", "/") for a in path_glob]
4745
)
4846

47+
assert mock_glob_normalized == path_glob_normalized
48+
4949
def test_group(self):
5050
with pytest.raises(NotImplementedError):
5151
self.path.group()
@@ -228,3 +228,34 @@ def test_fsspec_compat(self):
228228
upath2 = UPath(p2)
229229
assert upath2.read_bytes() == content
230230
upath2.unlink()
231+
232+
def test_pickling(self):
233+
path = self.path
234+
pickled_path = pickle.dumps(path)
235+
recovered_path = pickle.loads(pickled_path)
236+
237+
assert type(path) == type(recovered_path)
238+
assert str(path) == str(recovered_path)
239+
assert path.fs.storage_options == recovered_path.fs.storage_options
240+
241+
def test_pickling_child_path(self):
242+
path = (self.path) / "subfolder" / "subsubfolder"
243+
pickled_path = pickle.dumps(path)
244+
recovered_path = pickle.loads(pickled_path)
245+
246+
assert type(path) == type(recovered_path)
247+
assert str(path) == str(recovered_path)
248+
assert path._drv == recovered_path._drv
249+
assert path._root == recovered_path._root
250+
assert path._parts == recovered_path._parts
251+
assert path.fs.storage_options == recovered_path.fs.storage_options
252+
253+
def test_child_path(self):
254+
path_a = UPath(f"{self.path}/folder")
255+
path_b = self.path / "folder"
256+
257+
assert str(path_a) == str(path_b)
258+
assert path_a._root == path_b._root
259+
assert path_a._drv == path_b._drv
260+
assert path_a._parts == path_b._parts
261+
assert path_a._url == path_b._url

upath/tests/test_core.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import sys
21
import pathlib
2+
import pickle
3+
import sys
34
import warnings
45

56
import pytest
6-
77
from upath import UPath
88
from upath.implementations.s3 import S3Path
99
from upath.tests.cases import BaseTests
@@ -38,7 +38,12 @@ class TestUpath(BaseTests):
3838
def path(self, local_testdir):
3939
with warnings.catch_warnings():
4040
warnings.simplefilter("ignore")
41-
self.path = UPath(f"mock:{local_testdir}")
41+
42+
# On Windows the path needs to be prefixed with `/`, becaue
43+
# `UPath` implements `_posix_flavour`, which requires a `/` root
44+
# in order to correctly deserialize pickled objects
45+
root = "/" if sys.platform.startswith("win") else ""
46+
self.path = UPath(f"mock:{root}{local_testdir}")
4247

4348
def test_fsspec_compat(self):
4449
pass
@@ -137,3 +142,37 @@ def test_create_from_type(path, storage_options, module, object_type):
137142
except (ImportError, ModuleNotFoundError):
138143
# fs failed to import
139144
pass
145+
146+
147+
def test_child_path():
148+
path_a = UPath("gcs://bucket/folder")
149+
path_b = UPath("gcs://bucket") / "folder"
150+
151+
assert str(path_a) == str(path_b)
152+
assert path_a._root == path_b._root
153+
assert path_a._drv == path_b._drv
154+
assert path_a._parts == path_b._parts
155+
assert path_a._url == path_b._url
156+
157+
158+
def test_pickling():
159+
path = UPath("gcs://bucket/folder", storage_options={"anon": True})
160+
pickled_path = pickle.dumps(path)
161+
recovered_path = pickle.loads(pickled_path)
162+
163+
assert type(path) == type(recovered_path)
164+
assert str(path) == str(recovered_path)
165+
assert path.fs.storage_options == recovered_path.fs.storage_options
166+
167+
168+
def test_pickling_child_path():
169+
path = UPath("gcs://bucket", anon=True) / "subfolder" / "subsubfolder"
170+
pickled_path = pickle.dumps(path)
171+
recovered_path = pickle.loads(pickled_path)
172+
173+
assert type(path) == type(recovered_path)
174+
assert str(path) == str(recovered_path)
175+
assert path._drv == recovered_path._drv
176+
assert path._root == recovered_path._root
177+
assert path._parts == recovered_path._parts
178+
assert path.fs.storage_options == recovered_path.fs.storage_options

0 commit comments

Comments
 (0)