Skip to content

Commit 1d502d5

Browse files
authored
Merge pull request #3614 from Flamefire/downloadFileChunked
Add option to write file from file-like object and use in download_file
2 parents 4038b80 + 401cdd3 commit 1d502d5

File tree

2 files changed

+34
-4
lines changed

2 files changed

+34
-4
lines changed

easybuild/tools/filetools.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ def write_file(path, data, append=False, forced=False, backup=False, always_over
217217
overwrites current file contents without backup by default!
218218
219219
:param path: location of file
220-
:param data: contents to write to file
220+
:param data: contents to write to file. Can be a file-like object of binary data
221221
:param append: append to existing file rather than overwrite
222222
:param forced: force actually writing file in (extended) dry run mode
223223
:param backup: back up existing file before overwriting or modifying it
@@ -246,15 +246,21 @@ 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+
data_is_file_obj = hasattr(data, 'read')
250+
249251
# special care must be taken with binary data in Python 3
250-
if sys.version_info[0] >= 3 and isinstance(data, bytes):
252+
if sys.version_info[0] >= 3 and (isinstance(data, bytes) or data_is_file_obj):
251253
mode += 'b'
252254

253255
# note: we can't use try-except-finally, because Python 2.4 doesn't support it as a single block
254256
try:
255257
mkdir(os.path.dirname(path), parents=True)
256258
with open_file(path, mode) as fh:
257-
fh.write(data)
259+
if data_is_file_obj:
260+
# if a file-like object was provided, use copyfileobj (which reads the file in chunks)
261+
shutil.copyfileobj(data, fh)
262+
else:
263+
fh.write(data)
258264
except IOError as err:
259265
raise EasyBuildError("Failed to write to %s: %s", path, err)
260266

@@ -710,7 +716,11 @@ def download_file(filename, url, path, forced=False):
710716
url_fd = response.raw
711717
url_fd.decode_content = True
712718
_log.debug('response code for given url %s: %s' % (url, status_code))
713-
write_file(path, url_fd.read(), forced=forced, backup=True)
719+
# note: we pass the file object to write_file rather than reading the file first,
720+
# to ensure the data is read in chunks (which prevents problems in Python 3.9+);
721+
# cfr. https://github.com/easybuilders/easybuild-framework/issues/3455
722+
# and https://bugs.python.org/issue42853
723+
write_file(path, url_fd, forced=forced, backup=True)
714724
_log.info("Downloaded file %s from url %s to %s" % (filename, url, path))
715725
downloaded = True
716726
url_fd.close()

test/framework/filetools.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,26 @@ 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_obj(self):
709+
"""Test writing from a file-like object directly"""
710+
# Write a text file
711+
fp = os.path.join(self.test_prefix, 'test.txt')
712+
fp_out = os.path.join(self.test_prefix, 'test_out.txt')
713+
ft.write_file(fp, b'Hyphen: \xe2\x80\x93\nEuro sign: \xe2\x82\xac\na with dots: \xc3\xa4')
714+
715+
with ft.open_file(fp, 'rb') as fh:
716+
ft.write_file(fp_out, fh)
717+
self.assertEqual(ft.read_file(fp_out), ft.read_file(fp))
718+
719+
# Write a binary file
720+
fp = os.path.join(self.test_prefix, 'test.bin')
721+
fp_out = os.path.join(self.test_prefix, 'test_out.bin')
722+
ft.write_file(fp, b'\x00\x01'+os.urandom(42)+b'\x02\x03')
723+
724+
with ft.open_file(fp, 'rb') as fh:
725+
ft.write_file(fp_out, fh)
726+
self.assertEqual(ft.read_file(fp_out, mode='rb'), ft.read_file(fp, mode='rb'))
727+
708728
def test_is_binary(self):
709729
"""Test is_binary function."""
710730

0 commit comments

Comments
 (0)