Skip to content

Commit a4da98c

Browse files
committedApr 18, 2025
Add missing mode property to fake file wrapper
- conforms to the IO protocol - fixes #1162
1 parent 50e7ef4 commit a4da98c

File tree

4 files changed

+99
-24
lines changed

4 files changed

+99
-24
lines changed
 

‎CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ The released versions correspond to PyPI releases.
2727
(see [#1121](../../issues/1121))
2828
* fixed workaround for recursion with pytest under Windows to ignore capitalization
2929
of pytest executable (see [#1096](../../issues/1096))
30+
* added missing `mode` property to fake file wrapper (see [#1162](../../issues/1096))
3031

3132
### Infrastructure
3233
* adapt test for increased default buffer size in Python 3.14a6

‎pyfakefs/fake_file.py

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -751,9 +751,8 @@ def __init__(
751751
self,
752752
file_object: FakeFile,
753753
file_path: AnyStr,
754-
update: bool,
755-
read: bool,
756-
append: bool,
754+
open_modes: _OpenModes,
755+
allow_update: bool,
757756
delete_on_close: bool,
758757
filesystem: "FakeFilesystem",
759758
newline: Optional[str],
@@ -768,9 +767,8 @@ def __init__(
768767
):
769768
self.file_object = file_object
770769
self.file_path = file_path # type: ignore[var-annotated]
771-
self._append = append
772-
self._read = read
773-
self.allow_update = update
770+
self.open_modes = open_modes
771+
self.allow_update = allow_update
774772
self._closefd = closefd
775773
self._file_epoch = file_object.epoch
776774
self.raw_io = raw_io
@@ -801,8 +799,8 @@ def __init__(
801799
self._flush_pos = 0
802800
if contents:
803801
self._flush_pos = len(contents)
804-
if update:
805-
if not append:
802+
if self.allow_update:
803+
if not self.open_modes.append:
806804
self._io.seek(0)
807805
else:
808806
self._io.seek(self._flush_pos)
@@ -890,6 +888,21 @@ def closed(self) -> bool:
890888
"""Simulate the `closed` attribute on file."""
891889
return not self._is_open()
892890

891+
@property
892+
def mode(self) -> str:
893+
if self.open_modes.append:
894+
m = "ab" if self._binary else "a"
895+
return m + "+" if self.open_modes.can_read else m
896+
if self.open_modes.truncate:
897+
if self._binary:
898+
return "rb+" if self.open_modes.can_read else "wb"
899+
return "w+" if self.open_modes.can_read else "w"
900+
if self.open_modes.must_not_exist:
901+
m = "xb" if self._binary else "x"
902+
return m + "+" if self.open_modes.can_read else m
903+
m = "rb" if self._binary else "r"
904+
return m + "+" if self.open_modes.can_write else m
905+
893906
def _try_flush(self, old_pos: int) -> None:
894907
"""Try to flush and reset the position if it fails."""
895908
flush_pos = self._flush_pos
@@ -910,7 +923,7 @@ def flush(self) -> None:
910923
self._check_open_file()
911924

912925
if self.allow_update:
913-
if self._append:
926+
if self.open_modes.append:
914927
contents = self._io.getvalue()
915928
self._sync_io()
916929
old_contents = self.file_object.byte_contents
@@ -951,14 +964,14 @@ def _flush_related_files(self) -> None:
951964
open_file is not self
952965
and isinstance(open_file, FakeFileWrapper)
953966
and self.file_object == open_file.file_object
954-
and not open_file._append
967+
and not open_file.open_modes.append
955968
):
956969
open_file._sync_io()
957970

958971
def seek(self, offset: int, whence: int = 0) -> None:
959972
"""Move read/write pointer in 'file'."""
960973
self._check_open_file()
961-
if not self._append:
974+
if not self.open_modes.append:
962975
self._io.seek(offset, whence)
963976
else:
964977
self._read_seek = offset
@@ -976,7 +989,7 @@ def tell(self) -> int:
976989
if not self.is_stream:
977990
self.flush()
978991

979-
if not self._append:
992+
if not self.open_modes.append:
980993
return self._io.tell()
981994
if self._read_whence:
982995
write_seek = self._io.tell()
@@ -1001,7 +1014,7 @@ def _set_stream_contents(self, contents: bytes) -> None:
10011014
self._io.seek(0)
10021015
self._io.truncate()
10031016
self._io.putvalue(contents)
1004-
if not self._append:
1017+
if not self.open_modes.append:
10051018
self._io.seek(whence)
10061019

10071020
def _read_wrappers(self, name: str) -> Callable:
@@ -1117,7 +1130,7 @@ def write_wrapper(*args, **kwargs):
11171130
self._try_flush(old_pos)
11181131
if not flush_all:
11191132
ret_value = io_attr(*args, **kwargs)
1120-
if self._append:
1133+
if self.open_modes.append:
11211134
self._read_seek = self._io.tell()
11221135
self._read_whence = 0
11231136
return ret_value
@@ -1132,7 +1145,7 @@ def _adapt_size_for_related_files(self, size: int) -> None:
11321145
open_file is not self
11331146
and isinstance(open_file, FakeFileWrapper)
11341147
and self.file_object == open_file.file_object
1135-
and cast(FakeFileWrapper, open_file)._append
1148+
and cast(FakeFileWrapper, open_file).open_modes.append
11361149
):
11371150
open_file._read_seek += size
11381151

@@ -1146,7 +1159,7 @@ def _truncate_wrapper(self) -> Callable:
11461159

11471160
def truncate_wrapper(*args, **kwargs):
11481161
"""Wrap truncate call to call flush after truncate."""
1149-
if self._append:
1162+
if self.open_modes.append:
11501163
self._io.seek(self._read_seek, self._read_whence)
11511164
size = io_attr(*args, **kwargs)
11521165
self.flush()
@@ -1179,7 +1192,7 @@ def __getattr__(self, name: str) -> Any:
11791192

11801193
if reading or writing:
11811194
self._check_open_file()
1182-
if not self._read and reading:
1195+
if not self.open_modes.can_read and reading:
11831196
return self._read_error()
11841197
if not self.opened_as_fd and not self.allow_update and writing:
11851198
return self._write_error()
@@ -1192,7 +1205,7 @@ def __getattr__(self, name: str) -> Any:
11921205
self.file_object.st_atime = helpers.now()
11931206
if truncate:
11941207
return self._truncate_wrapper()
1195-
if self._append:
1208+
if self.open_modes.append:
11961209
if reading:
11971210
return self._read_wrappers(name)
11981211
elif not writing:
@@ -1234,12 +1247,12 @@ def _check_open_file(self) -> None:
12341247
raise ValueError("I/O operation on closed file")
12351248

12361249
def __iter__(self) -> Union[Iterator[str], Iterator[bytes]]:
1237-
if not self._read:
1250+
if not self.open_modes.can_read:
12381251
self._raise("File is not open for reading")
12391252
return self._io.__iter__()
12401253

12411254
def __next__(self):
1242-
if not self._read:
1255+
if not self.open_modes.can_read:
12431256
self._raise("File is not open for reading")
12441257
return next(self._io)
12451258

‎pyfakefs/fake_open.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def call(
160160
flushed if buffer size is exceeded. The default (-1) uses a
161161
system specific default buffer size. Text line mode (e.g.
162162
buffering=1 in text mode) is not supported.
163-
encoding: The encoding used to encode unicode strings / decode
163+
encoding: The encoding used to encode Unicode strings / decode
164164
bytes.
165165
errors: (str) Defines how encoding errors are handled.
166166
newline: Controls universal newlines, passed to stream object.
@@ -270,9 +270,8 @@ def call(
270270
fakefile = FakeFileWrapper(
271271
file_object,
272272
file_path,
273-
update=open_modes.can_write and can_write,
274-
read=open_modes.can_read,
275-
append=open_modes.append,
273+
open_modes=open_modes,
274+
allow_update=open_modes.can_write and can_write,
276275
delete_on_close=self._delete_on_close,
277276
filesystem=self.filesystem,
278277
newline=newline,

‎pyfakefs/tests/fake_open_test.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,10 @@ def test_exclusive_create_binary_file(self):
263263
self.os.mkdir(file_dir)
264264
contents = b"Binary contents"
265265
with self.open(file_path, "xb") as fake_file:
266+
self.assertEqual("xb", fake_file.mode)
266267
fake_file.write(contents)
267268
with self.open(file_path, "rb") as fake_file:
269+
self.assertEqual("rb", fake_file.mode)
268270
self.assertEqual(contents, fake_file.read())
269271

270272
def test_overwrite_existing_file(self):
@@ -302,11 +304,27 @@ def test_open_with_wplus(self):
302304
self.assertTrue(self.os.path.exists(file_path))
303305
with self.open(file_path, "r", encoding="utf8") as fake_file:
304306
self.assertEqual("old contents", fake_file.read())
307+
self.assertEqual("r", fake_file.mode)
305308
# actual tests
306309
with self.open(file_path, "w+", encoding="utf8") as fake_file:
307310
fake_file.write("new contents")
308311
fake_file.seek(0)
309312
self.assertTrue("new contents", fake_file.read())
313+
self.assertEqual("w+", fake_file.mode)
314+
315+
def test_open_with_wplus_binary(self):
316+
file_path = self.make_path("wplus_file_b")
317+
self.create_file(file_path, contents=b"old contents")
318+
self.assertTrue(self.os.path.exists(file_path))
319+
with self.open(file_path, "rb") as fake_file:
320+
self.assertEqual(b"old contents", fake_file.read())
321+
self.assertEqual("rb", fake_file.mode)
322+
# actual tests
323+
with self.open(file_path, "wb+") as fake_file:
324+
fake_file.write(b"new contents")
325+
fake_file.seek(0)
326+
self.assertTrue(b"new contents", fake_file.read())
327+
self.assertEqual("rb+", fake_file.mode)
310328

311329
def test_open_with_wplus_truncation(self):
312330
# set up
@@ -319,6 +337,7 @@ def test_open_with_wplus_truncation(self):
319337
with self.open(file_path, "w+", encoding="utf8") as fake_file:
320338
fake_file.seek(0)
321339
self.assertEqual("", fake_file.read())
340+
self.assertEqual("w+", fake_file.mode)
322341

323342
def test_open_with_append_flag(self):
324343
contents = [
@@ -335,6 +354,7 @@ def test_open_with_append_flag(self):
335354
fake_file.read(0)
336355
with self.assertRaises(io.UnsupportedOperation):
337356
fake_file.readline()
357+
self.assertEqual("a", fake_file.mode)
338358
expected_len = len("".join(contents))
339359
expected_len += len(contents) * (len(self.os.linesep) - 1)
340360
self.assertEqual(expected_len, fake_file.tell())
@@ -344,6 +364,22 @@ def test_open_with_append_flag(self):
344364
with self.open(file_path, encoding="utf8") as fake_file:
345365
self.assertEqual(contents + additional_contents, fake_file.readlines())
346366

367+
def test_open_with_append_flag_binary(self):
368+
contents = b"Just some boring stuff... "
369+
additional_contents = b"some excitement added"
370+
file_path = self.make_path("append-binary")
371+
self.create_file(file_path, contents=contents)
372+
with self.open(file_path, "ab") as fake_file:
373+
with self.assertRaises(io.UnsupportedOperation):
374+
fake_file.read(0)
375+
self.assertEqual("ab", fake_file.mode)
376+
self.assertEqual(len(contents), fake_file.tell())
377+
fake_file.seek(0)
378+
self.assertEqual(0, fake_file.tell())
379+
fake_file.write(additional_contents)
380+
with self.open(file_path, "rb") as fake_file:
381+
self.assertEqual(contents + additional_contents, fake_file.read())
382+
347383
def check_append_with_aplus(self):
348384
file_path = self.make_path("aplus_file")
349385
self.create_file(file_path, contents="old contents")
@@ -357,6 +393,7 @@ def check_append_with_aplus(self):
357393
self.filesystem, delete_on_close=True
358394
)
359395
with self.open(file_path, "a+", encoding="utf8") as fake_file:
396+
self.assertEqual("a+", fake_file.mode)
360397
self.assertEqual(12, fake_file.tell())
361398
fake_file.write("new contents")
362399
self.assertEqual(24, fake_file.tell())
@@ -391,6 +428,12 @@ def test_read_empty_file_with_aplus(self):
391428
with self.open(file_path, "a+", encoding="utf8") as fake_file:
392429
self.assertEqual("", fake_file.read())
393430

431+
def test_read_empty_file_with_aplus_binary(self):
432+
file_path = self.make_path("aplus_file")
433+
with self.open(file_path, "ab+") as fake_file:
434+
self.assertEqual(b"", fake_file.read())
435+
self.assertEqual("ab+", fake_file.mode)
436+
394437
def test_read_with_rplus(self):
395438
# set up
396439
file_path = self.make_path("rplus_file")
@@ -401,11 +444,27 @@ def test_read_with_rplus(self):
401444
# actual tests
402445
with self.open(file_path, "r+", encoding="utf8") as fake_file:
403446
self.assertEqual("old contents here", fake_file.read())
447+
self.assertEqual("r+", fake_file.mode)
404448
fake_file.seek(0)
405449
fake_file.write("new contents")
406450
fake_file.seek(0)
407451
self.assertEqual("new contents here", fake_file.read())
408452

453+
def test_read_with_rplus_binary(self):
454+
file_path = self.make_path("rplus_binary")
455+
self.create_file(file_path, contents=b"old contents here")
456+
self.assertTrue(self.os.path.exists(file_path))
457+
with self.open(file_path, "rb") as fake_file:
458+
self.assertEqual(b"old contents here", fake_file.read())
459+
460+
with self.open(file_path, "rb+") as fake_file:
461+
self.assertEqual(b"old contents here", fake_file.read())
462+
self.assertEqual("rb+", fake_file.mode)
463+
fake_file.seek(0)
464+
fake_file.write(b"new contents")
465+
fake_file.seek(0)
466+
self.assertEqual(b"new contents here", fake_file.read())
467+
409468
def create_with_permission(self, file_path, perm_bits):
410469
self.create_file(file_path)
411470
self.os.chmod(file_path, perm_bits)
@@ -900,6 +959,7 @@ def test_closing_file_with_different_close_mode(self):
900959
file_obj = self.filesystem.get_object(filename)
901960
with self.open(fd, "wb", closefd=False) as fp:
902961
fp.write(b"test")
962+
self.assertEqual("wb", fp.mode)
903963
self.assertTrue(self.filesystem.has_open_file(file_obj))
904964
self.os.close(fd)
905965
self.assertFalse(self.filesystem.has_open_file(file_obj))
@@ -1028,6 +1088,7 @@ def test_use_opener_with_exclusive_write(self):
10281088

10291089
file_path = self.make_path("bar")
10301090
with self.open(file_path, "x", encoding="utf8", opener=self.opener) as f:
1091+
self.assertEqual("x", f.mode)
10311092
assert f.write("bar") == 3
10321093
with self.assertRaises(OSError):
10331094
f.read()
@@ -1042,6 +1103,7 @@ def test_use_opener_with_exclusive_plus(self):
10421103

10431104
file_path = self.make_path("bar")
10441105
with self.open(file_path, "x+", encoding="utf8", opener=self.opener) as f:
1106+
self.assertEqual("x+", f.mode)
10451107
assert f.write("bar") == 3
10461108
assert f.read() == ""
10471109
with self.open(file_path, encoding="utf8") as f:

0 commit comments

Comments
 (0)
Failed to load comments.