Skip to content

Commit e121f46

Browse files
committed
Fake file wrappers now derive from io.Base classes
- text file wrappers derive from io.TextIOBase - binary file wrappers derive from io.BufferedIOBase
1 parent 3fe24c5 commit e121f46

5 files changed

Lines changed: 54 additions & 6 deletions

File tree

CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ The released versions correspond to PyPI releases.
44

55
## Unreleased
66

7+
### Changes
8+
* fake file wrappers now derive from `io.TextIOBase` or `io.BufferedIOBase`,
9+
so that `isinstance`-checks for these classes succeed
10+
(see [#1307](https://github.com/pytest-dev/pyfakefs/issues/1307)
11+
and [#484](https://github.com/pytest-dev/pyfakefs/issues/484))
12+
713
### Fixes
814
* route some pseudo-devices to the system instead of patching them; this ensures
915
that `os.urandom` and related functions work correctly with PyPy

docs/intro.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,10 @@ Limitations
7272
See :ref:`customizing_patcher` for more information and ways to work around
7373
this.
7474

75-
- ``pyfakefs`` does not retain the MRO for file objects, so you cannot rely on
76-
checks using `isinstance` for these objects (for example, to differentiate
77-
between binary and textual file objects).
75+
- ``pyfakefs`` does not retain the MRO for file objects, so you can mostly not rely on
76+
checks using `isinstance` for these objects (note though that binary and textual file
77+
objects are derived from `io.BufferedIOBase` and `io.TextIOBase`, respectively,
78+
and can thus be differentiated via `isinstance` checks).
7879

7980
- ``pyfakefs`` is only tested with CPython and the newest PyPy versions, other
8081
Python implementations will probably not work

pyfakefs/fake_file.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1286,14 +1286,40 @@ def _check_open_file(self) -> None:
12861286
def __iter__(self) -> Iterator[str] | Iterator[bytes]:
12871287
if not self.readable():
12881288
self._raise("File is not open for reading")
1289-
return self._io.__iter__()
1289+
return self._io.__iter__() # type: ignore[return-value]
12901290

12911291
def __next__(self):
12921292
if not self.readable():
12931293
self._raise("File is not open for reading")
12941294
return next(self._io)
12951295

12961296

1297+
class FakeTextFileWrapper(FakeFileWrapper, io.TextIOBase): # type: ignore[misc]
1298+
"""Represents a text file wrapper.
1299+
Derives from io.TextIOBase so it can be used as a file object
1300+
if the type is checked by a library user, while it delegates all calls
1301+
to `FakeFileWrapper`.
1302+
"""
1303+
1304+
def __getattribute__(self, name): # type: ignore[override]
1305+
if hasattr(FakeFileWrapper, name) or name in self.__dict__:
1306+
return FakeFileWrapper.__getattribute__(self, name)
1307+
return self.__getattr__(name)
1308+
1309+
1310+
class FakeBinaryFileWrapper(FakeFileWrapper, io.BufferedIOBase): # type: ignore[misc]
1311+
"""Represents a buffered binary file wrapper.
1312+
Derives from io.BufferedIOBase so it can be used as a file object
1313+
if the type is checked by a library user, while it delegates all calls
1314+
to `FakeFileWrapper`.
1315+
"""
1316+
1317+
def __getattribute__(self, name): # type: ignore[override]
1318+
if hasattr(FakeFileWrapper, name) or name in self.__dict__:
1319+
return FakeFileWrapper.__getattribute__(self, name)
1320+
return self.__getattr__(name)
1321+
1322+
12971323
class StandardStreamWrapper:
12981324
"""Wrapper for a system standard stream to be used in open files list."""
12991325

pyfakefs/fake_open.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@
3232
IO,
3333
)
3434

35-
from pyfakefs import helpers
3635
from pyfakefs.fake_file import (
36+
FakeBinaryFileWrapper,
37+
FakeTextFileWrapper,
3738
FakePipeWrapper,
3839
FakeFileWrapper,
3940
FakeFile,
4041
AnyFileWrapper,
4142
)
43+
from pyfakefs import helpers
4244
from pyfakefs.helpers import (
4345
AnyString,
4446
is_called_from_skipped_module,
@@ -270,7 +272,8 @@ def call(
270272
if not self.filesystem.is_windows_fs:
271273
file_object.st_ctime = current_time
272274

273-
fakefile = FakeFileWrapper(
275+
wrapper_class = FakeBinaryFileWrapper if binary else FakeTextFileWrapper
276+
fakefile = wrapper_class(
274277
file_object,
275278
file_path,
276279
open_modes=open_modes,

pyfakefs/tests/fake_open_test.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,6 +1051,18 @@ def test_readable_writable(self):
10511051
self.assertTrue(f.readable())
10521052
self.assertTrue(f.writable())
10531053

1054+
def test_file_class(self):
1055+
file_path = self.make_path("foo")
1056+
with self.open(file_path, "w") as f:
1057+
assert isinstance(f, io.TextIOBase)
1058+
f.write("test")
1059+
with self.open(file_path) as f:
1060+
assert isinstance(f, io.TextIOBase)
1061+
assert f.read() == "test"
1062+
with self.open(file_path, "rb") as f:
1063+
assert isinstance(f, io.BufferedIOBase)
1064+
assert f.read() == b"test"
1065+
10541066

10551067
class RealFileOpenTest(FakeFileOpenTest):
10561068
def use_real_fs(self):

0 commit comments

Comments
 (0)