Skip to content

Commit 041aca1

Browse files
authored
Fix open kwarg passthrough (#204)
* upath.core: handle kwargs in UPath.open * upath: tests for open * upath.local: support fsspec options in open
1 parent 3646cd2 commit 041aca1

File tree

5 files changed

+149
-5
lines changed

5 files changed

+149
-5
lines changed

.flake8

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ ignore=
99
# unindexed parameters in the str.format, see:
1010
# https://pypi.org/project/flake8-string-format/
1111
P1
12+
# def statements on the same line with overload
13+
E704
1214
max_line_length = 88
1315
max-complexity = 15
1416
select = B,C,E,F,W,T4,B902,T,P

upath/core.py

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@
66
from copy import copy
77
from pathlib import Path
88
from types import MappingProxyType
9+
from typing import IO
910
from typing import TYPE_CHECKING
1011
from typing import Any
12+
from typing import BinaryIO
13+
from typing import Literal
1114
from typing import Mapping
15+
from typing import TextIO
1216
from typing import TypeVar
17+
from typing import overload
1318
from urllib.parse import urlsplit
1419

15-
from fsspec import AbstractFileSystem
16-
from fsspec import get_filesystem_class
20+
from fsspec.registry import get_filesystem_class
21+
from fsspec.spec import AbstractFileSystem
1722

1823
from upath._compat import FSSpecAccessorShim
1924
from upath._compat import PathlibPathShim
@@ -741,8 +746,64 @@ def is_socket(self):
741746
def samefile(self, other_path):
742747
raise NotImplementedError
743748

744-
def open(self, mode="r", buffering=-1, encoding=None, errors=None, newline=None):
745-
return self.fs.open(self.path, mode) # fixme
749+
@overload
750+
def open(
751+
self,
752+
mode: Literal["r", "w", "a"] = ...,
753+
buffering: int = ...,
754+
encoding: str = ...,
755+
errors: str = ...,
756+
newline: str = ...,
757+
**fsspec_kwargs: Any,
758+
) -> TextIO: ...
759+
760+
@overload
761+
def open(
762+
self,
763+
mode: Literal["rb", "wb", "ab"] = ...,
764+
buffering: int = ...,
765+
encoding: str = ...,
766+
errors: str = ...,
767+
newline: str = ...,
768+
**fsspec_kwargs: Any,
769+
) -> BinaryIO: ...
770+
771+
def open(
772+
self,
773+
mode: str = "r",
774+
*args: Any,
775+
**fsspec_kwargs: Any,
776+
) -> IO[Any]:
777+
"""
778+
Open the file pointed by this path and return a file object, as
779+
the built-in open() function does.
780+
781+
Parameters
782+
----------
783+
mode:
784+
Opening mode. Default is 'r'.
785+
buffering:
786+
Default is the block size of the underlying fsspec filesystem.
787+
encoding:
788+
Encoding is only used in text mode. Default is None.
789+
errors:
790+
Error handling for encoding. Only used in text mode. Default is None.
791+
newline:
792+
Newline handling. Only used in text mode. Default is None.
793+
**fsspec_kwargs:
794+
Additional options for the fsspec filesystem.
795+
"""
796+
# match the signature of pathlib.Path.open()
797+
for key, value in zip(["buffering", "encoding", "errors", "newline"], args):
798+
if key in fsspec_kwargs:
799+
raise TypeError(
800+
f"{type(self).__name__}.open() got multiple values for '{key}'"
801+
)
802+
fsspec_kwargs[key] = value
803+
# translate pathlib buffering to fs block_size
804+
if "buffering" in fsspec_kwargs:
805+
fsspec_kwargs.setdefault("block_size", fsspec_kwargs.pop("buffering"))
806+
return self.fs.open(self.path, mode=mode, **fsspec_kwargs)
746807

747808
def iterdir(self):
748809
for name in self.fs.listdir(self.path):

upath/implementations/local.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from pathlib import Path
77
from pathlib import PosixPath
88
from pathlib import WindowsPath
9+
from typing import IO
910
from typing import Any
1011
from typing import Collection
1112
from typing import MutableMapping
@@ -110,6 +111,27 @@ class PosixUPath(PosixPath, LocalPath):
110111
# assign all PosixPath methods/attrs to prevent multi inheritance issues
111112
_set_class_attributes(locals(), src=PosixPath)
112113

114+
def open(
115+
self,
116+
mode="r",
117+
buffering=-1,
118+
encoding=None,
119+
errors=None,
120+
newline=None,
121+
**fsspec_kwargs,
122+
) -> IO[Any]:
123+
if fsspec_kwargs:
124+
return super(LocalPath, self).open(
125+
mode=mode,
126+
buffering=buffering,
127+
encoding=encoding,
128+
errors=errors,
129+
newline=newline,
130+
**fsspec_kwargs,
131+
)
132+
else:
133+
return PosixPath.open(self, mode, buffering, encoding, errors, newline)
134+
113135
if sys.version_info < (3, 12):
114136

115137
def __new__(
@@ -153,6 +175,27 @@ class WindowsUPath(WindowsPath, LocalPath):
153175
# assign all WindowsPath methods/attrs to prevent multi inheritance issues
154176
_set_class_attributes(locals(), src=WindowsPath)
155177

178+
def open(
179+
self,
180+
mode="r",
181+
buffering=-1,
182+
encoding=None,
183+
errors=None,
184+
newline=None,
185+
**fsspec_kwargs,
186+
) -> IO[Any]:
187+
if fsspec_kwargs:
188+
return super(LocalPath, self).open(
189+
mode=mode,
190+
buffering=buffering,
191+
encoding=encoding,
192+
errors=errors,
193+
newline=newline,
194+
**fsspec_kwargs,
195+
)
196+
else:
197+
return WindowsPath.open(self, mode, buffering, encoding, errors, newline)
198+
156199
if sys.version_info < (3, 12):
157200

158201
def __new__(

upath/tests/cases.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,25 @@ def test_makedirs_exist_ok_false(self):
236236
new_dir._accessor.makedirs(new_dir, exist_ok=False)
237237

238238
def test_open(self):
239-
pass
239+
p = self.path.joinpath("file1.txt")
240+
with p.open(mode="r") as f:
241+
assert f.read() == "hello world"
242+
with p.open(mode="rb") as f:
243+
assert f.read() == b"hello world"
244+
245+
def test_open_buffering(self):
246+
p = self.path.joinpath("file1.txt")
247+
p.open(buffering=-1)
248+
249+
def test_open_block_size(self):
250+
p = self.path.joinpath("file1.txt")
251+
with p.open(mode="r", block_size=8192) as f:
252+
assert f.read() == "hello world"
253+
254+
def test_open_errors(self):
255+
p = self.path.joinpath("file1.txt")
256+
with p.open(mode="r", encoding="ascii", errors="strict") as f:
257+
assert f.read() == "hello world"
240258

241259
def test_owner(self):
242260
with pytest.raises(NotImplementedError):

upath/tests/implementations/test_data.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,26 @@ def test_mkdir_parents_true_exists_ok_true(self):
9292
def test_mkdir_parents_true_exists_ok_false(self):
9393
pass
9494

95+
def test_open(self):
96+
p = UPath("data:text/plain;base64,aGVsbG8gd29ybGQ=")
97+
with p.open(mode="r") as f:
98+
assert f.read() == "hello world"
99+
with p.open(mode="rb") as f:
100+
assert f.read() == b"hello world"
101+
102+
def test_open_buffering(self):
103+
self.path.open(buffering=-1)
104+
105+
def test_open_block_size(self):
106+
p = UPath("data:text/plain;base64,aGVsbG8gd29ybGQ=")
107+
with p.open(mode="r", block_size=8192) as f:
108+
assert f.read() == "hello world"
109+
110+
def test_open_errors(self):
111+
p = UPath("data:text/plain;base64,aGVsbG8gd29ybGQ=")
112+
with p.open(mode="r", encoding="ascii", errors="strict") as f:
113+
assert f.read() == "hello world"
114+
95115
def test_read_bytes(self, pathlib_base):
96116
assert len(self.path.read_bytes()) == 69
97117

0 commit comments

Comments
 (0)