Skip to content

Commit b0ce401

Browse files
authored
Merge pull request #764 from isidentical/http-put-file-obj
http: allow put_file()ing file-like objects
2 parents 04636c2 + a79fc88 commit b0ce401

File tree

3 files changed

+35
-7
lines changed

3 files changed

+35
-7
lines changed

Diff for: fsspec/implementations/http.py

+20-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import absolute_import, division, print_function
22

33
import asyncio
4+
import io
45
import logging
56
import re
67
import weakref
@@ -14,7 +15,7 @@
1415
from fsspec.callbacks import _DEFAULT_CALLBACK
1516
from fsspec.exceptions import FSTimeoutError
1617
from fsspec.spec import AbstractBufferedFile
17-
from fsspec.utils import DEFAULT_BLOCK_SIZE, tokenize
18+
from fsspec.utils import DEFAULT_BLOCK_SIZE, nullcontext, tokenize
1819

1920
from ..caching import AllBytes
2021

@@ -254,17 +255,29 @@ async def _get_file(
254255

255256
async def _put_file(
256257
self,
257-
rpath,
258258
lpath,
259+
rpath,
259260
chunk_size=5 * 2 ** 20,
260261
callback=_DEFAULT_CALLBACK,
261262
method="post",
262263
**kwargs,
263264
):
264265
async def gen_chunks():
265-
with open(rpath, "rb") as f:
266-
callback.set_size(f.seek(0, 2))
267-
f.seek(0)
266+
# Support passing arbitrary file-like objects
267+
# and use them instead of streams.
268+
if isinstance(lpath, io.IOBase):
269+
context = nullcontext(lpath)
270+
use_seek = False # might not support seeking
271+
else:
272+
context = open(lpath, "rb")
273+
use_seek = True
274+
275+
with context as f:
276+
if use_seek:
277+
callback.set_size(f.seek(0, 2))
278+
f.seek(0)
279+
else:
280+
callback.set_size(getattr(f, "size", None))
268281

269282
chunk = f.read(64 * 1024)
270283
while chunk:
@@ -283,8 +296,8 @@ async def gen_chunks():
283296
)
284297

285298
meth = getattr(session, method)
286-
async with meth(lpath, data=gen_chunks(), **kw) as resp:
287-
self._raise_not_found_for_status(resp, lpath)
299+
async with meth(rpath, data=gen_chunks(), **kw) as resp:
300+
self._raise_not_found_for_status(resp, rpath)
288301

289302
async def _exists(self, path, **kwargs):
290303
kw = self.kwargs.copy()

Diff for: fsspec/implementations/tests/test_http.py

+9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import contextlib
3+
import io
34
import os
45
import sys
56
import threading
@@ -465,6 +466,14 @@ def test_put_file(server, tmp_path, method, reset_files):
465466
fs.get_file(server + "/hey", dwl_file)
466467
assert dwl_file.read_bytes() == data
467468

469+
src_file.write_bytes(b"xxx")
470+
with open(src_file, "rb") as stream:
471+
fs.put_file(stream, server + "/hey_2", method=method)
472+
assert fs.cat(server + "/hey_2") == b"xxx"
473+
474+
fs.put_file(io.BytesIO(b"yyy"), server + "/hey_3", method=method)
475+
assert fs.cat(server + "/hey_3") == b"yyy"
476+
468477

469478
@pytest.mark.xfail(
470479
condition=sys.flags.optimize > 1, reason="no docstrings when optimised"

Diff for: fsspec/utils.py

+6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pathlib
55
import re
66
import sys
7+
from contextlib import contextmanager
78
from functools import partial
89
from hashlib import md5
910
from urllib.parse import urlsplit
@@ -477,3 +478,8 @@ def wrapper(cls):
477478
return cls
478479

479480
return wrapper
481+
482+
483+
@contextmanager
484+
def nullcontext(obj):
485+
yield obj

0 commit comments

Comments
 (0)