Skip to content

Commit e94b9bb

Browse files
committed
Repair fileio across python versions.
1 parent 043cd9a commit e94b9bb

File tree

2 files changed

+72
-58
lines changed

2 files changed

+72
-58
lines changed

mig/shared/fileio.py

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,13 @@
3939
import time
4040
import zipfile
4141

42+
from mig.shared.compat import PY2
43+
4244
# NOTE: We expose optimized walk function directly for ease and efficiency.
4345
# Requires stand-alone scandir module on python 2 whereas the native os
4446
# functions are built-in and optimized similarly on python 3+
4547
slow_walk, slow_listdir = False, False
46-
if sys.version_info[0] > 2:
48+
if not PY2:
4749
from os import walk, listdir
4850
else:
4951
try:
@@ -70,19 +72,8 @@
7072
exit(1)
7173

7274

73-
def _auto_adjust_mode(data, mode):
74-
"""Select suitable file open mode based on string type of data. I.e. whether
75-
to use binary or text mode depending on data in bytes or unicode format.
76-
"""
77-
78-
# NOTE: detect byte/unicode writes and handle explicitly in a portable way
79-
if isinstance(data, bytes):
80-
if 'b' not in mode:
81-
mode = "%sb" % mode # appended to avoid mode ordering error on PY2
82-
else:
83-
if 'b' in mode:
84-
mode = mode.strip('b')
85-
return mode
75+
def _is_string(value):
76+
return isinstance(value, unicode) if PY2 else isinstance(value, str)
8677

8778

8879
def _write_chunk(path, chunk, offset, logger=None, mode='r+b',
@@ -100,6 +91,12 @@ def _write_chunk(path, chunk, offset, logger=None, mode='r+b',
10091
(offset, path))
10192
return False
10293

94+
if _is_string(chunk):
95+
chunk = bytearray(chunk, 'utf8')
96+
# detect byte writes and handle explicitly in a portable way
97+
if isinstance(chunk, (bytes, bytearray)) and 'b' not in mode:
98+
mode = "%sb" % mode # appended to avoid mode ordering error on PY2
99+
103100
# create dir and file if it does not exists
104101

105102
(head, _) = os.path.split(path)
@@ -152,9 +149,6 @@ def write_chunk(path, chunk, offset, logger, mode='r+b', force_string=False):
152149
if not logger:
153150
logger = null_logger("dummy")
154151

155-
# TODO: enable this again once throuroughly tested and assured py2+3 safe
156-
# mode = _auto_adjust_mode(chunk, mode)
157-
158152
return _write_chunk(path, chunk, offset, logger, mode,
159153
force_string=force_string)
160154

@@ -169,9 +163,6 @@ def write_file(content, path, logger, mode='w', make_parent=True, umask=None,
169163
if umask is not None:
170164
old_umask = os.umask(umask)
171165

172-
# TODO: enable this again once throuroughly tested and assured py2+3 safe
173-
#mode = _auto_adjust_mode(content, mode)
174-
175166
retval = _write_chunk(path, content, offset=0, logger=logger, mode=mode,
176167
make_parent=make_parent, create_file=False,
177168
force_string=force_string)

tests/test_mig_shared_fileio.py

Lines changed: 61 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,14 @@
2828
"""Unit test fileio functions"""
2929

3030
import binascii
31+
import codecs
3132
import os
3233
import sys
3334
import unittest
3435

3536
# NOTE: wrap next imports in try except to prevent autopep8 shuffling up
3637
try:
37-
from tests.support import MigTestCase, cleanpath, temppath, testmain
38+
from tests.support import PY2, MigTestCase, cleanpath, temppath, testmain
3839
import mig.shared.fileio as fileio
3940
except ImportError as ioe:
4041
print("Failed to import mig core modules: %s" % ioe)
@@ -43,15 +44,47 @@
4344
DUMMY_BYTES = binascii.unhexlify('DEADBEEF') # 4 bytes
4445
DUMMY_BYTES_LENGTH = 4
4546
DUMMY_UNICODE = u'UniCode123½¾µßðþđŋħĸþł@ª€£$¥©®'
46-
DUMMY_UNICODE_LENGTH = len(DUMMY_UNICODE)
47+
DUMMY_UNICODE_BYTES = bytearray(DUMMY_UNICODE, 'utf8')
48+
DUMMY_UNICODE_BYTES_LENGTH = len(DUMMY_UNICODE_BYTES)
4749
DUMMY_FILE_WRITECHUNK = 'fileio/write_chunk'
4850
DUMMY_FILE_WRITEFILE = 'fileio/write_file'
4951

5052
assert isinstance(DUMMY_BYTES, bytes)
5153

5254

55+
def _as_unicode_string(value):
56+
assert isinstance(value, bytearray)
57+
return unicode(codecs.decode(value, 'utf8')) if PY2 else str(value, 'utf8')
58+
59+
60+
class _ByteText:
61+
"""File-like object that allows interacting with a text file as a bytearray.
62+
63+
Supports reading and directly usable as a context manager."""
64+
65+
def __init__(self, path, mode='r'):
66+
self._file = None
67+
self._path = path
68+
self._mode = "%s%s" % (mode, 'b') if 'b' not in mode else mode
69+
70+
def read(self, size):
71+
content = self._file.read(size)
72+
# always read back the content as though it was raw bytes
73+
return bytearray(content)
74+
75+
def __enter__(self):
76+
self._file = open(self._path, mode=self._mode)
77+
return self
78+
79+
def __exit__(self, *args):
80+
self._file.close()
81+
if args[1] is not None:
82+
raise args[1]
83+
84+
5385
class MigSharedFileio__write_chunk(MigTestCase):
54-
# TODO: Add docstrings to this class and its methods
86+
"""Coverage of the write_chunk() function."""
87+
5588
def setUp(self):
5689
super(MigSharedFileio__write_chunk, self).setUp()
5790
self.tmp_path = temppath(DUMMY_FILE_WRITECHUNK, self, skip_clean=True)
@@ -60,9 +93,7 @@ def setUp(self):
6093
def test_return_false_on_invalid_data(self):
6194
self.logger.forgive_errors()
6295

63-
# NOTE: we make sure to disable any forced stringification here
64-
did_succeed = fileio.write_chunk(self.tmp_path, 1234, 0, self.logger,
65-
force_string=False)
96+
did_succeed = fileio.write_chunk(self.tmp_path, 1234, 0, self.logger)
6697
self.assertFalse(did_succeed)
6798

6899
def test_return_false_on_invalid_offset(self):
@@ -106,7 +137,6 @@ def test_store_bytes_at_offset(self):
106137
"expected a hole was left")
107138
self.assertEqual(content[3:], DUMMY_BYTES)
108139

109-
@unittest.skip("TODO: enable again - requires the temporarily disabled auto mode select")
110140
def test_store_bytes_in_text_mode(self):
111141
fileio.write_chunk(self.tmp_path, DUMMY_BYTES, 0, self.logger,
112142
mode="r+")
@@ -116,28 +146,29 @@ def test_store_bytes_in_text_mode(self):
116146
self.assertEqual(len(content), DUMMY_BYTES_LENGTH)
117147
self.assertEqual(content[:], DUMMY_BYTES)
118148

119-
@unittest.skip("TODO: enable again - requires the temporarily disabled auto mode select")
120149
def test_store_unicode(self):
121-
fileio.write_chunk(self.tmp_path, DUMMY_UNICODE, 0, self.logger,
122-
mode='r+')
150+
did_succeed = fileio.write_chunk(self.tmp_path, DUMMY_UNICODE, 0, self.logger,
151+
mode='r+')
152+
self.assertTrue(did_succeed)
123153

124-
with open(self.tmp_path, 'r') as file:
154+
with _ByteText(self.tmp_path) as file:
125155
content = file.read(1024)
126-
self.assertEqual(len(content), DUMMY_UNICODE_LENGTH)
127-
self.assertEqual(content[:], DUMMY_UNICODE)
156+
self.assertEqual(len(content), DUMMY_UNICODE_BYTES_LENGTH)
157+
self.assertEqual(content[:], DUMMY_UNICODE_BYTES)
128158

129-
@unittest.skip("TODO: enable again - requires the temporarily disabled auto mode select")
130159
def test_store_unicode_in_binary_mode(self):
131160
fileio.write_chunk(self.tmp_path, DUMMY_UNICODE, 0, self.logger,
132161
mode='r+b')
133162

134-
with open(self.tmp_path, 'r') as file:
163+
with _ByteText(self.tmp_path) as file:
135164
content = file.read(1024)
136-
self.assertEqual(len(content), DUMMY_UNICODE_LENGTH)
137-
self.assertEqual(content[:], DUMMY_UNICODE)
165+
self.assertEqual(len(content), DUMMY_UNICODE_BYTES_LENGTH)
166+
self.assertEqual(_as_unicode_string(content[:]), DUMMY_UNICODE)
138167

139168

140169
class MigSharedFileio__write_file(MigTestCase):
170+
"""Coverage of the write_file() function."""
171+
141172
def setUp(self):
142173
super(MigSharedFileio__write_file, self).setUp()
143174
self.tmp_path = temppath(DUMMY_FILE_WRITEFILE, self, skip_clean=True)
@@ -146,17 +177,16 @@ def setUp(self):
146177
def test_return_false_on_invalid_data(self):
147178
self.logger.forgive_errors()
148179

149-
# NOTE: we make sure to disable any forced stringification here
150-
did_succeed = fileio.write_file(1234, self.tmp_path, self.logger,
151-
force_string=False)
180+
did_succeed = fileio.write_file(1234, self.tmp_path, self.logger)
152181
self.assertFalse(did_succeed)
153182

154183
def test_return_false_on_invalid_dir(self):
155184
self.logger.forgive_errors()
156185

157186
os.makedirs(self.tmp_path)
158187

159-
did_succeed = fileio.write_file(DUMMY_BYTES, self.tmp_path, self.logger)
188+
did_succeed = fileio.write_file(
189+
DUMMY_BYTES, self.tmp_path, self.logger)
160190
self.assertFalse(did_succeed)
161191

162192
def test_return_false_on_missing_dir(self):
@@ -167,28 +197,23 @@ def test_return_false_on_missing_dir(self):
167197
self.assertFalse(did_succeed)
168198

169199
def test_creates_directory(self):
170-
# TODO: temporarily use empty string to avoid any byte/unicode issues
171-
# did_succeed = fileio.write_file(DUMMY_BYTES, self.tmp_path, self.logger)
172-
did_succeed = fileio.write_file('', self.tmp_path, self.logger)
200+
did_succeed = fileio.write_file(
201+
DUMMY_BYTES, self.tmp_path, self.logger)
173202
self.assertTrue(did_succeed)
174203

175204
path_kind = self.assertPathExists(DUMMY_FILE_WRITEFILE)
176205
self.assertEqual(path_kind, "file")
177206

178207
def test_store_bytes(self):
179-
mode = 'w'
180-
# TODO: remove next once we have auto adjust mode in write helper
181-
mode = fileio._auto_adjust_mode(DUMMY_BYTES, mode)
182-
did_succeed = fileio.write_file(DUMMY_BYTES, self.tmp_path, self.logger,
183-
mode=mode)
208+
did_succeed = fileio.write_file(
209+
DUMMY_BYTES, self.tmp_path, self.logger)
184210
self.assertTrue(did_succeed)
185211

186212
with open(self.tmp_path, 'rb') as file:
187213
content = file.read(1024)
188214
self.assertEqual(len(content), DUMMY_BYTES_LENGTH)
189215
self.assertEqual(content[:], DUMMY_BYTES)
190216

191-
@unittest.skip("TODO: enable again - requires the temporarily disabled auto mode select")
192217
def test_store_bytes_in_text_mode(self):
193218
did_succeed = fileio.write_file(DUMMY_BYTES, self.tmp_path, self.logger,
194219
mode="w")
@@ -199,27 +224,25 @@ def test_store_bytes_in_text_mode(self):
199224
self.assertEqual(len(content), DUMMY_BYTES_LENGTH)
200225
self.assertEqual(content[:], DUMMY_BYTES)
201226

202-
@unittest.skip("TODO: enable again - requires the temporarily disabled auto mode select")
203227
def test_store_unicode(self):
204228
did_succeed = fileio.write_file(DUMMY_UNICODE, self.tmp_path,
205229
self.logger, mode='w')
206230
self.assertTrue(did_succeed)
207231

208-
with open(self.tmp_path, 'r') as file:
232+
with _ByteText(self.tmp_path, 'r') as file:
209233
content = file.read(1024)
210-
self.assertEqual(len(content), DUMMY_UNICODE_LENGTH)
211-
self.assertEqual(content[:], DUMMY_UNICODE)
234+
self.assertEqual(len(content), DUMMY_UNICODE_BYTES_LENGTH)
235+
self.assertEqual(content[:], DUMMY_UNICODE_BYTES)
212236

213-
@unittest.skip("TODO: enable again - requires the temporarily disabled auto mode select")
214237
def test_store_unicode_in_binary_mode(self):
215238
did_succeed = fileio.write_file(DUMMY_UNICODE, self.tmp_path,
216239
self.logger, mode='wb')
217240
self.assertTrue(did_succeed)
218241

219-
with open(self.tmp_path, 'r') as file:
242+
with _ByteText(self.tmp_path, 'r') as file:
220243
content = file.read(1024)
221-
self.assertEqual(len(content), DUMMY_UNICODE_LENGTH)
222-
self.assertEqual(content[:], DUMMY_UNICODE)
244+
self.assertEqual(len(content), DUMMY_UNICODE_BYTES_LENGTH)
245+
self.assertEqual(content[:], DUMMY_UNICODE_BYTES)
223246

224247

225248
if __name__ == '__main__':

0 commit comments

Comments
 (0)