Skip to content

Commit 2a08f77

Browse files
committed
Add missing mode property to fake file wrapper
- conforms to the IO protocol - fixes #1162
1 parent 50e7ef4 commit 2a08f77

File tree

4 files changed

+40
-24
lines changed

4 files changed

+40
-24
lines changed

CHANGES.md

+1
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

+28-20
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,16 @@ 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+
return "a+" if self.open_modes.can_read else "a"
895+
if self.open_modes.truncate:
896+
return "w+" if self.open_modes.can_read else "w"
897+
if self.open_modes.must_not_exist:
898+
return "x+" if self.open_modes.can_read else "x"
899+
return "r+" if self.open_modes.can_write else "r"
900+
893901
def _try_flush(self, old_pos: int) -> None:
894902
"""Try to flush and reset the position if it fails."""
895903
flush_pos = self._flush_pos
@@ -910,7 +918,7 @@ def flush(self) -> None:
910918
self._check_open_file()
911919

912920
if self.allow_update:
913-
if self._append:
921+
if self.open_modes.append:
914922
contents = self._io.getvalue()
915923
self._sync_io()
916924
old_contents = self.file_object.byte_contents
@@ -951,14 +959,14 @@ def _flush_related_files(self) -> None:
951959
open_file is not self
952960
and isinstance(open_file, FakeFileWrapper)
953961
and self.file_object == open_file.file_object
954-
and not open_file._append
962+
and not open_file.open_modes.append
955963
):
956964
open_file._sync_io()
957965

958966
def seek(self, offset: int, whence: int = 0) -> None:
959967
"""Move read/write pointer in 'file'."""
960968
self._check_open_file()
961-
if not self._append:
969+
if not self.open_modes.append:
962970
self._io.seek(offset, whence)
963971
else:
964972
self._read_seek = offset
@@ -976,7 +984,7 @@ def tell(self) -> int:
976984
if not self.is_stream:
977985
self.flush()
978986

979-
if not self._append:
987+
if not self.open_modes.append:
980988
return self._io.tell()
981989
if self._read_whence:
982990
write_seek = self._io.tell()
@@ -1001,7 +1009,7 @@ def _set_stream_contents(self, contents: bytes) -> None:
10011009
self._io.seek(0)
10021010
self._io.truncate()
10031011
self._io.putvalue(contents)
1004-
if not self._append:
1012+
if not self.open_modes.append:
10051013
self._io.seek(whence)
10061014

10071015
def _read_wrappers(self, name: str) -> Callable:
@@ -1117,7 +1125,7 @@ def write_wrapper(*args, **kwargs):
11171125
self._try_flush(old_pos)
11181126
if not flush_all:
11191127
ret_value = io_attr(*args, **kwargs)
1120-
if self._append:
1128+
if self.open_modes.append:
11211129
self._read_seek = self._io.tell()
11221130
self._read_whence = 0
11231131
return ret_value
@@ -1132,7 +1140,7 @@ def _adapt_size_for_related_files(self, size: int) -> None:
11321140
open_file is not self
11331141
and isinstance(open_file, FakeFileWrapper)
11341142
and self.file_object == open_file.file_object
1135-
and cast(FakeFileWrapper, open_file)._append
1143+
and cast(FakeFileWrapper, open_file).open_modes.append
11361144
):
11371145
open_file._read_seek += size
11381146

@@ -1146,7 +1154,7 @@ def _truncate_wrapper(self) -> Callable:
11461154

11471155
def truncate_wrapper(*args, **kwargs):
11481156
"""Wrap truncate call to call flush after truncate."""
1149-
if self._append:
1157+
if self.open_modes.append:
11501158
self._io.seek(self._read_seek, self._read_whence)
11511159
size = io_attr(*args, **kwargs)
11521160
self.flush()
@@ -1179,7 +1187,7 @@ def __getattr__(self, name: str) -> Any:
11791187

11801188
if reading or writing:
11811189
self._check_open_file()
1182-
if not self._read and reading:
1190+
if not self.open_modes.can_read and reading:
11831191
return self._read_error()
11841192
if not self.opened_as_fd and not self.allow_update and writing:
11851193
return self._write_error()
@@ -1192,7 +1200,7 @@ def __getattr__(self, name: str) -> Any:
11921200
self.file_object.st_atime = helpers.now()
11931201
if truncate:
11941202
return self._truncate_wrapper()
1195-
if self._append:
1203+
if self.open_modes.append:
11961204
if reading:
11971205
return self._read_wrappers(name)
11981206
elif not writing:
@@ -1234,12 +1242,12 @@ def _check_open_file(self) -> None:
12341242
raise ValueError("I/O operation on closed file")
12351243

12361244
def __iter__(self) -> Union[Iterator[str], Iterator[bytes]]:
1237-
if not self._read:
1245+
if not self.open_modes.can_read:
12381246
self._raise("File is not open for reading")
12391247
return self._io.__iter__()
12401248

12411249
def __next__(self):
1242-
if not self._read:
1250+
if not self.open_modes.can_read:
12431251
self._raise("File is not open for reading")
12441252
return next(self._io)
12451253

pyfakefs/fake_open.py

+3-4
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

+8
Original file line numberDiff line numberDiff line change
@@ -302,11 +302,13 @@ def test_open_with_wplus(self):
302302
self.assertTrue(self.os.path.exists(file_path))
303303
with self.open(file_path, "r", encoding="utf8") as fake_file:
304304
self.assertEqual("old contents", fake_file.read())
305+
self.assertEqual("r", fake_file.mode)
305306
# actual tests
306307
with self.open(file_path, "w+", encoding="utf8") as fake_file:
307308
fake_file.write("new contents")
308309
fake_file.seek(0)
309310
self.assertTrue("new contents", fake_file.read())
311+
self.assertEqual("w+", fake_file.mode)
310312

311313
def test_open_with_wplus_truncation(self):
312314
# set up
@@ -319,6 +321,7 @@ def test_open_with_wplus_truncation(self):
319321
with self.open(file_path, "w+", encoding="utf8") as fake_file:
320322
fake_file.seek(0)
321323
self.assertEqual("", fake_file.read())
324+
self.assertEqual("w+", fake_file.mode)
322325

323326
def test_open_with_append_flag(self):
324327
contents = [
@@ -335,6 +338,7 @@ def test_open_with_append_flag(self):
335338
fake_file.read(0)
336339
with self.assertRaises(io.UnsupportedOperation):
337340
fake_file.readline()
341+
self.assertEqual("a", fake_file.mode)
338342
expected_len = len("".join(contents))
339343
expected_len += len(contents) * (len(self.os.linesep) - 1)
340344
self.assertEqual(expected_len, fake_file.tell())
@@ -357,6 +361,7 @@ def check_append_with_aplus(self):
357361
self.filesystem, delete_on_close=True
358362
)
359363
with self.open(file_path, "a+", encoding="utf8") as fake_file:
364+
self.assertEqual("a+", fake_file.mode)
360365
self.assertEqual(12, fake_file.tell())
361366
fake_file.write("new contents")
362367
self.assertEqual(24, fake_file.tell())
@@ -401,6 +406,7 @@ def test_read_with_rplus(self):
401406
# actual tests
402407
with self.open(file_path, "r+", encoding="utf8") as fake_file:
403408
self.assertEqual("old contents here", fake_file.read())
409+
self.assertEqual("r+", fake_file.mode)
404410
fake_file.seek(0)
405411
fake_file.write("new contents")
406412
fake_file.seek(0)
@@ -1028,6 +1034,7 @@ def test_use_opener_with_exclusive_write(self):
10281034

10291035
file_path = self.make_path("bar")
10301036
with self.open(file_path, "x", encoding="utf8", opener=self.opener) as f:
1037+
self.assertEqual("x", f.mode)
10311038
assert f.write("bar") == 3
10321039
with self.assertRaises(OSError):
10331040
f.read()
@@ -1042,6 +1049,7 @@ def test_use_opener_with_exclusive_plus(self):
10421049

10431050
file_path = self.make_path("bar")
10441051
with self.open(file_path, "x+", encoding="utf8", opener=self.opener) as f:
1052+
self.assertEqual("x+", f.mode)
10451053
assert f.write("bar") == 3
10461054
assert f.read() == ""
10471055
with self.open(file_path, encoding="utf8") as f:

0 commit comments

Comments
 (0)