Skip to content

Commit d32e203

Browse files
boegelFlamefire
authored andcommitted
make sure we copy the entire file when a file-like object is passed to write_file + add comments w.r.t. passing file handle in download_file
1 parent 16d19bc commit d32e203

File tree

2 files changed

+17
-5
lines changed

2 files changed

+17
-5
lines changed

easybuild/tools/filetools.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,17 +246,19 @@ def write_file(path, data, append=False, forced=False, backup=False, always_over
246246
# cfr. https://docs.python.org/3/library/functions.html#open
247247
mode = 'a' if append else 'w'
248248

249-
is_file_like = hasattr(data, 'read')
249+
data_is_file_obj = all(hasattr(data, x) for x in ('read', 'seek', 'tell', 'write'))
250250

251251
# special care must be taken with binary data in Python 3
252-
if sys.version_info[0] >= 3 and (isinstance(data, bytes) or is_file_like):
252+
if sys.version_info[0] >= 3 and (isinstance(data, bytes) or data_is_file_obj):
253253
mode += 'b'
254254

255255
# note: we can't use try-except-finally, because Python 2.4 doesn't support it as a single block
256256
try:
257257
mkdir(os.path.dirname(path), parents=True)
258258
with open_file(path, mode) as fh:
259-
if is_file_like:
259+
if data_is_file_obj:
260+
# if a file-like object was provided, use copyfileobj (which reads the file in chunks)
261+
data.seek(0)
260262
shutil.copyfileobj(data, fh)
261263
else:
262264
fh.write(data)
@@ -715,6 +717,10 @@ def download_file(filename, url, path, forced=False):
715717
url_fd = response.raw
716718
url_fd.decode_content = True
717719
_log.debug('response code for given url %s: %s' % (url, status_code))
720+
# note: we pass the file object to write_file rather than reading the file first,
721+
# to ensure the data is read in chunks (which prevents problems in Python 3.9+);
722+
# cfr. https://github.com/easybuilders/easybuild-framework/issues/3455
723+
# and https://bugs.python.org/issue42853
718724
write_file(path, url_fd, forced=forced, backup=True)
719725
_log.info("Downloaded file %s from url %s to %s" % (filename, url, path))
720726
downloaded = True

test/framework/filetools.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -705,8 +705,8 @@ def test_read_write_file(self):
705705
# test use of 'mode' in read_file
706706
self.assertEqual(ft.read_file(foo, mode='rb'), b'bar')
707707

708-
def test_write_file_like(self):
709-
"""Test writing from a file handle directly"""
708+
def test_write_file_obj(self):
709+
"""Test writing from a file-like object directly"""
710710
# Write a text file
711711
fp = os.path.join(self.test_prefix, 'test.txt')
712712
fp_out = os.path.join(self.test_prefix, 'test_out.txt')
@@ -716,6 +716,12 @@ def test_write_file_like(self):
716716
ft.write_file(fp_out, fh)
717717
self.assertEqual(ft.read_file(fp_out), ft.read_file(fp))
718718

719+
# works fine even if same data was already read through the provided file handle
720+
with ft.open_file(fp, 'rb') as fh:
721+
fh.read(4)
722+
ft.write_file(fp_out, fh)
723+
self.assertEqual(ft.read_file(fp_out), ft.read_file(fp))
724+
719725
# Write a binary file
720726
fp = os.path.join(self.test_prefix, 'test.bin')
721727
fp_out = os.path.join(self.test_prefix, 'test_out.bin')

0 commit comments

Comments
 (0)