Skip to content

Commit af9c9dc

Browse files
authored
feat: add ignore_flush parameter to BlobWriter (#644)
* feat: add ignore_flush parameter to BlobWriter * address feedback
1 parent 49f78b0 commit af9c9dc

File tree

4 files changed

+74
-17
lines changed

4 files changed

+74
-17
lines changed

google/cloud/storage/blob.py

+31-2
Original file line numberDiff line numberDiff line change
@@ -3760,6 +3760,7 @@ def open(
37603760
self,
37613761
mode="r",
37623762
chunk_size=None,
3763+
ignore_flush=None,
37633764
encoding=None,
37643765
errors=None,
37653766
newline=None,
@@ -3801,6 +3802,19 @@ def open(
38013802
chunk_size for writes must be exactly a multiple of 256KiB as with
38023803
other resumable uploads. The default is 40 MiB.
38033804
3805+
:type ignore_flush: bool
3806+
:param ignore_flush:
3807+
(Optional) For non text-mode writes, makes flush() do nothing
3808+
instead of raising an error. flush() without closing is not
3809+
supported by the remote service and therefore calling it normally
3810+
results in io.UnsupportedOperation. However, that behavior is
3811+
incompatible with some consumers and wrappers of file objects in
3812+
Python, such as zipfile.ZipFile or io.TextIOWrapper. Setting
3813+
ignore_flush will cause flush() to successfully do nothing, for
3814+
compatibility with those contexts. The correct way to actually flush
3815+
data to the remote server is to close() (using a context manager,
3816+
such as in the example, will cause this to happen automatically).
3817+
38043818
:type encoding: str
38053819
:param encoding:
38063820
(Optional) For text mode only, the name of the encoding that the stream will
@@ -3873,23 +3887,38 @@ def open(
38733887
raise ValueError(
38743888
"encoding, errors and newline arguments are for text mode only"
38753889
)
3890+
if ignore_flush:
3891+
raise ValueError(
3892+
"ignore_flush argument is for non-text write mode only"
3893+
)
38763894
return BlobReader(self, chunk_size=chunk_size, **kwargs)
38773895
elif mode == "wb":
38783896
if encoding or errors or newline:
38793897
raise ValueError(
38803898
"encoding, errors and newline arguments are for text mode only"
38813899
)
3882-
return BlobWriter(self, chunk_size=chunk_size, **kwargs)
3900+
return BlobWriter(
3901+
self, chunk_size=chunk_size, ignore_flush=ignore_flush, **kwargs
3902+
)
38833903
elif mode in ("r", "rt"):
3904+
if ignore_flush:
3905+
raise ValueError(
3906+
"ignore_flush argument is for non-text write mode only"
3907+
)
38843908
return TextIOWrapper(
38853909
BlobReader(self, chunk_size=chunk_size, **kwargs),
38863910
encoding=encoding,
38873911
errors=errors,
38883912
newline=newline,
38893913
)
38903914
elif mode in ("w", "wt"):
3915+
if ignore_flush is False:
3916+
raise ValueError(
3917+
"ignore_flush is required for text mode writing and "
3918+
"cannot be set to False"
3919+
)
38913920
return TextIOWrapper(
3892-
BlobWriter(self, chunk_size=chunk_size, text_mode=True, **kwargs),
3921+
BlobWriter(self, chunk_size=chunk_size, ignore_flush=True, **kwargs),
38933922
encoding=encoding,
38943923
errors=errors,
38953924
newline=newline,

google/cloud/storage/fileio.py

+27-14
Original file line numberDiff line numberDiff line change
@@ -229,11 +229,23 @@ class BlobWriter(io.BufferedIOBase):
229229
writes must be exactly a multiple of 256KiB as with other resumable
230230
uploads. The default is the chunk_size of the blob, or 40 MiB.
231231
232-
:type text_mode: boolean
232+
:type text_mode: bool
233233
:param text_mode:
234-
Whether this class is wrapped in 'io.TextIOWrapper'. Toggling this
235-
changes the behavior of flush() to conform to TextIOWrapper's
236-
expectations.
234+
(Deprecated) A synonym for ignore_flush. For backwards-compatibility,
235+
if True, sets ignore_flush to True. Use ignore_flush instead. This
236+
parameter will be removed in a future release.
237+
238+
:type ignore_flush: bool
239+
:param ignore_flush:
240+
Makes flush() do nothing instead of raise an error. flush() without
241+
closing is not supported by the remote service and therefore calling it
242+
on this class normally results in io.UnsupportedOperation. However, that
243+
behavior is incompatible with some consumers and wrappers of file
244+
objects in Python, such as zipfile.ZipFile or io.TextIOWrapper. Setting
245+
ignore_flush will cause flush() to successfully do nothing, for
246+
compatibility with those contexts. The correct way to actually flush
247+
data to the remote server is to close() (using this object as a context
248+
manager is recommended).
237249
238250
:type retry: google.api_core.retry.Retry or google.cloud.storage.retry.ConditionalRetryPolicy
239251
:param retry:
@@ -278,6 +290,7 @@ def __init__(
278290
blob,
279291
chunk_size=None,
280292
text_mode=False,
293+
ignore_flush=False,
281294
retry=DEFAULT_RETRY_IF_GENERATION_SPECIFIED,
282295
**upload_kwargs
283296
):
@@ -292,9 +305,8 @@ def __init__(
292305
# Resumable uploads require a chunk size of a multiple of 256KiB.
293306
# self._chunk_size must not be changed after the upload is initiated.
294307
self._chunk_size = chunk_size or blob.chunk_size or DEFAULT_CHUNK_SIZE
295-
# In text mode this class will be wrapped and TextIOWrapper requires a
296-
# different behavior of flush().
297-
self._text_mode = text_mode
308+
# text_mode is a deprecated synonym for ignore_flush
309+
self._ignore_flush = ignore_flush or text_mode
298310
self._retry = retry
299311
self._upload_kwargs = upload_kwargs
300312

@@ -394,13 +406,14 @@ def tell(self):
394406
return self._buffer.tell() + len(self._buffer)
395407

396408
def flush(self):
397-
if self._text_mode:
398-
# TextIOWrapper expects this method to succeed before calling close().
399-
return
400-
401-
raise io.UnsupportedOperation(
402-
"Cannot flush without finalizing upload. Use close() instead."
403-
)
409+
# flush() is not fully supported by the remote service, so raise an
410+
# error here, unless self._ignore_flush is set.
411+
if not self._ignore_flush:
412+
raise io.UnsupportedOperation(
413+
"Cannot flush without finalizing upload. Use close() instead, "
414+
"or set ignore_flush=True when constructing this class (see "
415+
"docstring)."
416+
)
404417

405418
def close(self):
406419
self._checkClosed() # Raises ValueError if closed.

tests/unit/test_blob.py

+8
Original file line numberDiff line numberDiff line change
@@ -5585,13 +5585,21 @@ def test_open(self):
55855585
self.assertEqual(type(f.buffer), BlobWriter)
55865586
f = blob.open("wb")
55875587
self.assertEqual(type(f), BlobWriter)
5588+
f = blob.open("wb", ignore_flush=True)
5589+
self.assertTrue(f._ignore_flush)
55885590

55895591
with self.assertRaises(NotImplementedError):
55905592
blob.open("a")
55915593
with self.assertRaises(ValueError):
55925594
blob.open("rb", encoding="utf-8")
55935595
with self.assertRaises(ValueError):
55945596
blob.open("wb", encoding="utf-8")
5597+
with self.assertRaises(ValueError):
5598+
blob.open("r", ignore_flush=True)
5599+
with self.assertRaises(ValueError):
5600+
blob.open("rb", ignore_flush=True)
5601+
with self.assertRaises(ValueError):
5602+
blob.open("w", ignore_flush=False)
55955603

55965604

55975605
class Test__quote(unittest.TestCase):

tests/unit/test_fileio.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,13 @@ def test_attributes_explicit(self):
272272
self.assertEqual(writer._chunk_size, 512 * 1024)
273273
self.assertEqual(writer._retry, DEFAULT_RETRY)
274274

275+
def test_deprecated_text_mode_attribute(self):
276+
blob = mock.Mock()
277+
blob.chunk_size = 256 * 1024
278+
writer = self._make_blob_writer(blob, text_mode=True)
279+
self.assertTrue(writer._ignore_flush)
280+
writer.flush() # This should do nothing and not raise an error.
281+
275282
def test_reject_wrong_chunk_size(self):
276283
blob = mock.Mock()
277284
blob.chunk_size = 123
@@ -857,7 +864,7 @@ def test_write(self, mock_warn):
857864
unwrapped_writer = self._make_blob_writer(
858865
blob,
859866
chunk_size=chunk_size,
860-
text_mode=True,
867+
ignore_flush=True,
861868
num_retries=NUM_RETRIES,
862869
content_type=PLAIN_CONTENT_TYPE,
863870
)

0 commit comments

Comments
 (0)