Skip to content

Commit 41dbe99

Browse files
committed
Added some support for io.open_code in Python 3.8
- added new argument 'patch_open_code' to allow patching the function - see pytest-dev#554
1 parent e34eab6 commit 41dbe99

File tree

5 files changed

+146
-40
lines changed

5 files changed

+146
-40
lines changed

CHANGES.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ The released versions correspond to PyPi releases.
66
#### New Features
77
* add support for the `buffering` parameter in `open`
88
(see [#549](../../issues/549))
9-
9+
* add possibility to patch `io.open_code` using the new argument
10+
`patch_open_code` (since Python 3.8)
11+
(see [#554](../../issues/554))
12+
1013
#### Fixes
1114
* do not call fake `open` if called from skipped module
1215
(see [#552](../../issues/552))

docs/usage.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,22 @@ the default value of ``use_known_patches`` should be used, but it is present
386386
to allow users to disable this patching in case it causes any problems. It
387387
may be removed or replaced by more fine-grained arguments in future releases.
388388

389+
patch_open_code
390+
~~~~~~~~~~~~~~~
391+
Since Python 3.8, the ``io`` module has the function ``open_code``, which
392+
opens a file read-only and is used to open Python code files. By default, this
393+
function is not patched, because the files it opens usually belong to the
394+
executed library code and are not present in the fake file system.
395+
Under some circumstances, this may not be the case, and the opened file
396+
lives in the fake filesystem. For these cases, you can set ``patch_open_code``
397+
to ``True``.
398+
399+
.. note:: There is no possibility to change this setting based on affected
400+
files. Depending on the upcoming use cases, this may be changed in future
401+
versions of ``pyfakefs``, and this argument may be changed or removed in a
402+
later version.
403+
404+
389405
Using convenience methods
390406
-------------------------
391407
While ``pyfakefs`` can be used just with the standard Python file system

pyfakefs/fake_filesystem.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,8 @@ def __init__(self, path_separator=os.path.sep, total_size=None,
888888
self.add_mount_point(self.root.name, total_size)
889889
self._add_standard_streams()
890890
self.dev_null = FakeNullFile(self)
891+
# set from outside if needed
892+
self.patch_open_code = False
891893

892894
@property
893895
def is_linux(self):
@@ -3438,21 +3440,21 @@ def dir():
34383440
"""Return the list of patched function names. Used for patching
34393441
functions imported from the module.
34403442
"""
3441-
dir = [
3443+
_dir = [
34423444
'access', 'chdir', 'chmod', 'chown', 'close', 'fstat', 'fsync',
34433445
'getcwd', 'lchmod', 'link', 'listdir', 'lstat', 'makedirs',
34443446
'mkdir', 'mknod', 'open', 'read', 'readlink', 'remove',
34453447
'removedirs', 'rename', 'rmdir', 'stat', 'symlink', 'umask',
34463448
'unlink', 'utime', 'walk', 'write', 'getcwdb', 'replace'
34473449
]
34483450
if sys.platform.startswith('linux'):
3449-
dir += [
3451+
_dir += [
34503452
'fdatasync', 'getxattr', 'listxattr',
34513453
'removexattr', 'setxattr'
34523454
]
34533455
if use_scandir:
3454-
dir += ['scandir']
3455-
return dir
3456+
_dir += ['scandir']
3457+
return _dir
34563458

34573459
def __init__(self, filesystem):
34583460
"""Also exposes self.path (to fake os.path).
@@ -4468,7 +4470,10 @@ def dir():
44684470
"""Return the list of patched function names. Used for patching
44694471
functions imported from the module.
44704472
"""
4471-
return 'open',
4473+
_dir = ['open']
4474+
if sys.version_info >= (3, 8):
4475+
_dir.append('open_code')
4476+
return _dir
44724477

44734478
def __init__(self, filesystem):
44744479
"""
@@ -4498,6 +4503,21 @@ def open(self, file, mode='r', buffering=-1, encoding=None,
44984503
return fake_open(file, mode, buffering, encoding, errors,
44994504
newline, closefd, opener)
45004505

4506+
if sys.version_info >= (3, 8):
4507+
def open_code(self, path):
4508+
"""Redirect the call to open. Note that the behavior of the real
4509+
function may be overridden by an earlier call to the
4510+
PyFile_SetOpenCodeHook(). This behavior is not reproduced here.
4511+
"""
4512+
if not isinstance(path, str):
4513+
raise TypeError(
4514+
"open_code() argument 'path' must be str, not int")
4515+
if not self.filesystem.patch_open_code:
4516+
# mostly this is used for compiled code -
4517+
# don't patch these, as the files are probably in the real fs
4518+
return self._io_module.open_code(path)
4519+
return self.open(path, mode='rb')
4520+
45014521
def __getattr__(self, name):
45024522
"""Forwards any unfaked calls to the standard io module."""
45034523
return getattr(self._io_module, name)

pyfakefs/fake_filesystem_unittest.py

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ def patchfs(_func=None, *,
7878
modules_to_reload=None,
7979
modules_to_patch=None,
8080
allow_root_user=True,
81-
use_known_patches=True):
81+
use_known_patches=True,
82+
patch_open_code=False):
8283
"""Convenience decorator to use patcher with additional parameters in a
8384
test function.
8485
@@ -101,7 +102,8 @@ def wrapped(*args, **kwargs):
101102
modules_to_reload=modules_to_reload,
102103
modules_to_patch=modules_to_patch,
103104
allow_root_user=allow_root_user,
104-
use_known_patches=use_known_patches) as p:
105+
use_known_patches=use_known_patches,
106+
patch_open_code=patch_open_code) as p:
105107
kwargs['fs'] = p.fs
106108
return f(*args, **kwargs)
107109

@@ -123,7 +125,8 @@ def load_doctests(loader, tests, ignore, module,
123125
modules_to_reload=None,
124126
modules_to_patch=None,
125127
allow_root_user=True,
126-
use_known_patches=True): # pylint: disable=unused-argument
128+
use_known_patches=True,
129+
patch_open_code=False): # pylint: disable=unused-argument
127130
"""Load the doctest tests for the specified module into unittest.
128131
Args:
129132
loader, tests, ignore : arguments passed in from `load_tests()`
@@ -136,7 +139,8 @@ def load_doctests(loader, tests, ignore, module,
136139
modules_to_reload=modules_to_reload,
137140
modules_to_patch=modules_to_patch,
138141
allow_root_user=allow_root_user,
139-
use_known_patches=use_known_patches)
142+
use_known_patches=use_known_patches,
143+
patch_open_code=patch_open_code)
140144
globs = _patcher.replace_globs(vars(module))
141145
tests.addTests(doctest.DocTestSuite(module,
142146
globs=globs,
@@ -162,8 +166,6 @@ class TestCaseMixin:
162166
modules_to_patch: A dictionary of fake modules mapped to the
163167
fully qualified patched module names. Can be used to add patching
164168
of modules not provided by `pyfakefs`.
165-
use_known_patches: If True (the default), some patches for commonly
166-
used packges are applied which make them usable with pyfakes.
167169
168170
If you specify some of these attributes here and you have DocTests,
169171
consider also specifying the same arguments to :py:func:`load_doctests`.
@@ -200,7 +202,8 @@ def setUpPyfakefs(self,
200202
modules_to_reload=None,
201203
modules_to_patch=None,
202204
allow_root_user=True,
203-
use_known_patches=True):
205+
use_known_patches=True,
206+
patch_open_code=False):
204207
"""Bind the file-related modules to the :py:class:`pyfakefs` fake file
205208
system instead of the real file system. Also bind the fake `open()`
206209
function.
@@ -223,7 +226,8 @@ def setUpPyfakefs(self,
223226
modules_to_reload=modules_to_reload,
224227
modules_to_patch=modules_to_patch,
225228
allow_root_user=allow_root_user,
226-
use_known_patches=use_known_patches
229+
use_known_patches=use_known_patches,
230+
patch_open_code=patch_open_code
227231
)
228232

229233
self._stubber.setUp()
@@ -257,9 +261,7 @@ class TestCase(unittest.TestCase, TestCaseMixin):
257261
def __init__(self, methodName='runTest',
258262
additional_skip_names=None,
259263
modules_to_reload=None,
260-
modules_to_patch=None,
261-
allow_root_user=True,
262-
use_known_patches=True):
264+
modules_to_patch=None):
263265
"""Creates the test class instance and the patcher used to stub out
264266
file system related modules.
265267
@@ -272,8 +274,6 @@ def __init__(self, methodName='runTest',
272274
self.additional_skip_names = additional_skip_names
273275
self.modules_to_reload = modules_to_reload
274276
self.modules_to_patch = modules_to_patch
275-
self.allow_root_user = allow_root_user
276-
self.use_known_patches = use_known_patches
277277

278278
@Deprecator('add_real_file')
279279
def copyRealFile(self, real_file_path, fake_file_path=None,
@@ -358,8 +358,32 @@ class Patcher:
358358

359359
def __init__(self, additional_skip_names=None,
360360
modules_to_reload=None, modules_to_patch=None,
361-
allow_root_user=True, use_known_patches=True):
362-
"""For a description of the arguments, see TestCase.__init__"""
361+
allow_root_user=True, use_known_patches=True,
362+
patch_open_code=False):
363+
"""
364+
Args:
365+
additional_skip_names: names of modules inside of which no module
366+
replacement shall be performed, in addition to the names in
367+
:py:attr:`fake_filesystem_unittest.Patcher.SKIPNAMES`.
368+
Instead of the module names, the modules themselves
369+
may be used.
370+
modules_to_reload: A list of modules that need to be reloaded
371+
to be patched dynamically; may be needed if the module
372+
imports file system modules under an alias
373+
374+
.. caution:: Reloading modules may have unwanted side effects.
375+
modules_to_patch: A dictionary of fake modules mapped to the
376+
fully qualified patched module names. Can be used to add
377+
patching of modules not provided by `pyfakefs`.
378+
allow_root_user: If True (default), if the test is run as root
379+
user, the user in the fake file system is also considered a
380+
root user, otherwise it is always considered a regular user.
381+
use_known_patches: If True (the default), some patches for commonly
382+
used packages are applied which make them usable with pyfakefs.
383+
patch_open_code: If True, `io.open_code` is patched. The default
384+
is not to patch it, as it mostly is used to load compiled
385+
modules that are not in the fake file system.
386+
"""
363387

364388
if not allow_root_user:
365389
# set non-root IDs even if the real user is root
@@ -370,6 +394,7 @@ def __init__(self, additional_skip_names=None,
370394
# save the original open function for use in pytest plugin
371395
self.original_open = open
372396
self.fake_open = None
397+
self.patch_open_code = patch_open_code
373398

374399
if additional_skip_names is not None:
375400
skip_names = [m.__name__ if inspect.ismodule(m) else m
@@ -589,6 +614,7 @@ def _refresh(self):
589614
self._stubs = mox3_stubout.StubOutForTesting()
590615

591616
self.fs = fake_filesystem.FakeFilesystem(patcher=self)
617+
self.fs.patch_open_code = self.patch_open_code
592618
for name in self._fake_module_classes:
593619
self.fake_modules[name] = self._fake_module_classes[name](self.fs)
594620
if hasattr(self.fake_modules[name], 'skip_names'):

pyfakefs/tests/fake_open_test.py

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,24 @@
2121
import locale
2222
import os
2323
import stat
24+
import sys
2425
import time
2526
import unittest
2627

2728
from pyfakefs import fake_filesystem
28-
from pyfakefs.fake_filesystem import is_root, PERM_READ
29+
from pyfakefs.fake_filesystem import is_root, PERM_READ, FakeIoModule
2930
from pyfakefs.tests.test_utils import RealFsTestCase
3031

3132

3233
class FakeFileOpenTestBase(RealFsTestCase):
34+
def setUp(self):
35+
super(FakeFileOpenTestBase, self).setUp()
36+
if self.use_real_fs():
37+
self.open = io.open
38+
else:
39+
self.fake_io_module = FakeIoModule(self.filesystem)
40+
self.open = self.fake_io_module.open
41+
3342
def path_separator(self):
3443
return '!'
3544

@@ -89,7 +98,7 @@ def test_unicode_contents(self):
8998
contents = f.read()
9099
self.assertEqual(contents, text_fractions)
91100

92-
def test_byte_contents_py3(self):
101+
def test_byte_contents(self):
93102
file_path = self.make_path('foo')
94103
byte_fractions = b'\xe2\x85\x93 \xe2\x85\x94 \xe2\x85\x95 \xe2\x85\x96'
95104
with self.open(file_path, 'wb') as f:
@@ -110,15 +119,6 @@ def test_write_str_read_bytes(self):
110119
self.assertEqual(str_contents, contents.decode(
111120
locale.getpreferredencoding(False)))
112121

113-
def test_byte_contents(self):
114-
file_path = self.make_path('foo')
115-
byte_fractions = b'\xe2\x85\x93 \xe2\x85\x94 \xe2\x85\x95 \xe2\x85\x96'
116-
with self.open(file_path, 'wb') as f:
117-
f.write(byte_fractions)
118-
with self.open(file_path, 'rb') as f:
119-
contents = f.read()
120-
self.assertEqual(contents, byte_fractions)
121-
122122
def test_open_valid_file(self):
123123
contents = [
124124
'I am he as\n',
@@ -926,6 +926,55 @@ def use_real_fs(self):
926926
return True
927927

928928

929+
@unittest.skipIf(sys.version_info < (3, 8),
930+
'open_code only present since Python 3.8')
931+
class FakeFileOpenCodeTest(FakeFileOpenTestBase):
932+
933+
def setUp(self):
934+
super(FakeFileOpenCodeTest, self).setUp()
935+
if self.use_real_fs():
936+
self.open_code = io.open_code
937+
else:
938+
self.open_code = self.fake_io_module.open_code
939+
940+
def tearDown(self):
941+
self.filesystem.patch_open_code = False
942+
super(FakeFileOpenCodeTest, self).tearDown()
943+
944+
def test_invalid_path(self):
945+
self.filesystem.patch_open_code = True
946+
with self.assertRaises(TypeError):
947+
self.open_code(4)
948+
949+
def test_byte_contents_open_code(self):
950+
self.filesystem.patch_open_code = True
951+
byte_fractions = b'\xe2\x85\x93 \xe2\x85\x94 \xe2\x85\x95 \xe2\x85\x96'
952+
file_path = self.make_path('foo')
953+
self.create_file(file_path, contents=byte_fractions)
954+
with self.open_code(file_path) as f:
955+
contents = f.read()
956+
self.assertEqual(contents, byte_fractions)
957+
958+
def test_not_patched_open_code_in_fake_fs(self):
959+
file_path = self.make_path('foo')
960+
self.create_file(file_path)
961+
962+
with self.assertRaises(OSError):
963+
self.open_code(file_path)
964+
965+
def test_not_patched_open_code_in_real_fs(self):
966+
file_path = __file__
967+
968+
with self.open_code(file_path) as f:
969+
contents = f.read()
970+
self.assertTrue(len(contents) > 100)
971+
972+
973+
class RealFileOpenCodeTest(FakeFileOpenCodeTest):
974+
def use_real_fs(self):
975+
return True
976+
977+
929978
class BufferingModeTest(FakeFileOpenTestBase):
930979
def test_no_buffering(self):
931980
file_path = self.make_path("buffertest.bin")
@@ -1168,10 +1217,6 @@ class OpenFileWithEncodingTest(FakeFileOpenTestBase):
11681217

11691218
def setUp(self):
11701219
super(OpenFileWithEncodingTest, self).setUp()
1171-
if self.use_real_fs():
1172-
self.open = io.open
1173-
else:
1174-
self.open = fake_filesystem.FakeFileOpen(self.filesystem)
11751220
self.file_path = self.make_path('foo')
11761221

11771222
def test_write_str_read_bytes(self):
@@ -1451,10 +1496,6 @@ def use_real_fs(self):
14511496
class FakeFileOpenLineEndingWithEncodingTest(FakeFileOpenTestBase):
14521497
def setUp(self):
14531498
super(FakeFileOpenLineEndingWithEncodingTest, self).setUp()
1454-
if self.use_real_fs():
1455-
self.open = io.open
1456-
else:
1457-
self.open = fake_filesystem.FakeFileOpen(self.filesystem)
14581499

14591500
def test_read_standard_newline_mode(self):
14601501
file_path = self.make_path('some_file')

0 commit comments

Comments
 (0)