Skip to content

Commit eb187cd

Browse files
committed
Added automatic patching of builtin and io open as other name
- in Python 2, builtin open is now handled by FakeBuiltinModule - in Python 3, builtin open is handled via io.open - adapted pytest plugin to ensure that open is not patched for linecache and dependent tokenize modules - importing builtin open as other name does not work with PyPy2
1 parent 2c606c0 commit eb187cd

File tree

8 files changed

+112
-36
lines changed

8 files changed

+112
-36
lines changed

CHANGES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ The release versions are PyPi releases.
55

66
#### New Features
77
* automatically patch file system methods imported as another name like
8-
`from os.path import exists as my_exists`
8+
`from os.path import exists as my_exists`, including builtin `open`
99

1010
#### Fixes
1111

pyfakefs/fake_filesystem.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4528,6 +4528,13 @@ class FakeIoModule(object):
45284528
my_io_module = fake_filesystem.FakeIoModule(filesystem)
45294529
"""
45304530

4531+
@staticmethod
4532+
def dir():
4533+
"""Return the list of patched function names. Used for patching
4534+
functions imported from the module.
4535+
"""
4536+
return 'open',
4537+
45314538
def __init__(self, filesystem):
45324539
"""
45334540
Args:
@@ -4553,6 +4560,24 @@ def __getattr__(self, name):
45534560
return getattr(self._io_module, name)
45544561

45554562

4563+
class FakeBuiltinModule(object):
4564+
"""Uses FakeFilesystem to provide a fake built-in open replacement.
4565+
"""
4566+
def __init__(self, filesystem):
4567+
"""
4568+
Args:
4569+
filesystem: FakeFilesystem used to provide
4570+
file system information.
4571+
"""
4572+
self.filesystem = filesystem
4573+
4574+
def open(self, *args, **kwargs):
4575+
"""Redirect the call to FakeFileOpen.
4576+
See FakeFileOpen.call() for description.
4577+
"""
4578+
return FakeFileOpen(self.filesystem)(*args, **kwargs)
4579+
4580+
45564581
class FakeFileWrapper(object):
45574582
"""Wrapper for a stream object for use by a FakeFile object.
45584583

pyfakefs/fake_filesystem_unittest.py

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
import unittest
4444
import zipfile # noqa: F401 make sure it gets correctly stubbed, see #427
4545

46+
from pyfakefs.helpers import IS_PY2, IS_PYPY
47+
4648
from pyfakefs.deprecator import Deprecator
4749

4850
try:
@@ -79,6 +81,7 @@
7981

8082
OS_MODULE = 'nt' if sys.platform == 'win32' else 'posix'
8183
PATH_MODULE = 'ntpath' if sys.platform == 'win32' else 'posixpath'
84+
BUILTIN_MODULE = '__builtin__' if IS_PY2 else 'buildins'
8285

8386

8487
def load_doctests(loader, tests, ignore, module,
@@ -319,6 +322,8 @@ def __init__(self, additional_skip_names=None,
319322
"""For a description of the arguments, see TestCase.__init__"""
320323

321324
self._skipNames = self.SKIPNAMES.copy()
325+
# save the original open function for use in pytest plugin
326+
self.original_open = open
322327

323328
if additional_skip_names is not None:
324329
self._skipNames.update(additional_skip_names)
@@ -339,6 +344,15 @@ def __init__(self, additional_skip_names=None,
339344
'shutil': fake_filesystem_shutil.FakeShutilModule,
340345
'io': fake_filesystem.FakeIoModule,
341346
}
347+
if IS_PY2 or IS_PYPY:
348+
# in Python 2 io.open, the module is referenced as _io
349+
self._fake_module_classes['_io'] = fake_filesystem.FakeIoModule
350+
351+
# No need to patch builtins in Python 3 - builtin open
352+
# is an alias for io.open
353+
if IS_PY2 or IS_PYPY:
354+
self._fake_module_classes[
355+
BUILTIN_MODULE] = fake_filesystem.FakeBuiltinModule
342356

343357
# class modules maps class names against a list of modules they can
344358
# be contained in - this allows for alternative modules like
@@ -364,32 +378,37 @@ def __init__(self, additional_skip_names=None,
364378
# each patched function name has to be looked up separately
365379
self._fake_module_functions = {}
366380
for mod_name, fake_module in self._fake_module_classes.items():
367-
modnames = (
368-
(mod_name, OS_MODULE) if mod_name == 'os' else (mod_name,)
369-
)
370381
if (hasattr(fake_module, 'dir') and
371382
inspect.isfunction(fake_module.dir)):
372383
for fct_name in fake_module.dir():
373-
self._fake_module_functions[fct_name] = (
374-
modnames,
375-
getattr(fake_module, fct_name),
376-
mod_name
377-
)
384+
module_attr = (getattr(fake_module, fct_name), mod_name)
385+
self._fake_module_functions.setdefault(
386+
fct_name, {})[mod_name] = module_attr
387+
if mod_name == 'os':
388+
self._fake_module_functions.setdefault(
389+
fct_name, {})[OS_MODULE] = module_attr
390+
378391
# special handling for functions in os.path
379392
fake_module = fake_filesystem.FakePathModule
380393
for fct_name in fake_module.dir():
381-
self._fake_module_functions[fct_name] = (
382-
('genericpath', PATH_MODULE),
383-
getattr(fake_module, fct_name),
384-
PATH_MODULE
385-
)
394+
module_attr = (getattr(fake_module, fct_name), PATH_MODULE)
395+
self._fake_module_functions.setdefault(
396+
fct_name, {})['genericpath'] = module_attr
397+
self._fake_module_functions.setdefault(
398+
fct_name, {})[PATH_MODULE] = module_attr
399+
400+
# special handling for built-in open
401+
self._fake_module_functions.setdefault(
402+
'open', {})[BUILTIN_MODULE] = (
403+
fake_filesystem.FakeBuiltinModule.open,
404+
BUILTIN_MODULE
405+
)
386406

387407
# Attributes set by _refresh()
388408
self._modules = {}
389409
self._fct_modules = {}
390410
self._stubs = None
391411
self.fs = None
392-
self.fake_open = None
393412
self.fake_modules = {}
394413
self._dyn_patcher = None
395414

@@ -441,10 +460,10 @@ def _find_modules(self):
441460
inspect.isbuiltin(fct)) and
442461
fct.__name__ in self._fake_module_functions and
443462
fct.__module__ in self._fake_module_functions[
444-
fct.__name__][0]}
463+
fct.__name__]}
445464
for name, fct in functions.items():
446465
self._fct_modules.setdefault(
447-
(name, fct.__name__), set()).add(module)
466+
(name, fct.__name__, fct.__module__), set()).add(module)
448467

449468
def _refresh(self):
450469
"""Renew the fake file system and set the _isStale flag to `False`."""
@@ -456,7 +475,6 @@ def _refresh(self):
456475
for name in self._fake_module_classes:
457476
self.fake_modules[name] = self._fake_module_classes[name](self.fs)
458477
self.fake_modules[PATH_MODULE] = self.fake_modules['os'].path
459-
self.fake_open = fake_filesystem.FakeFileOpen(self.fs)
460478

461479
self._isStale = False
462480

@@ -480,16 +498,21 @@ def setUp(self, doctester=None):
480498
def start_patching(self):
481499
if not self._patching:
482500
self._patching = True
483-
if sys.version_info < (3,):
484-
# file() was eliminated in Python3
485-
self._stubs.smart_set(builtins, 'file', self.fake_open)
486-
self._stubs.smart_set(builtins, 'open', self.fake_open)
501+
if IS_PYPY:
502+
# patching open via _fct_modules does not work in PyPy
503+
self._stubs.smart_set(builtins, 'open',
504+
self.fake_modules[BUILTIN_MODULE].open)
505+
if IS_PY2:
506+
# file is not a function and has to be handled separately
507+
self._stubs.smart_set(builtins, 'file',
508+
self.fake_modules[BUILTIN_MODULE].open)
509+
487510
for name, modules in self._modules.items():
488511
for module, attr in modules:
489512
self._stubs.smart_set(
490513
module, name, self.fake_modules[attr])
491-
for (name, fct_name), modules in self._fct_modules.items():
492-
_, method, mod_name = self._fake_module_functions[fct_name]
514+
for (name, ft_name, ft_mod), modules in self._fct_modules.items():
515+
method, mod_name = self._fake_module_functions[ft_name][ft_mod]
493516
fake_module = self.fake_modules[mod_name]
494517
attr = method.__get__(fake_module, fake_module.__class__)
495518
for module in modules:

pyfakefs/helpers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313
"""Helper classes use for fake file system implementation."""
1414
import io
1515
import locale
16+
import platform
1617
import sys
1718
from copy import copy
1819
from stat import S_IFLNK
1920

2021
import os
2122

2223
IS_PY2 = sys.version_info[0] < 3
23-
24+
IS_PYPY = platform.python_implementation() == 'PyPy'
2425

2526
try:
2627
text_type = unicode # Python 2

pyfakefs/pytest_plugin.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,30 @@ def my_fakefs_test(fs):
1010
"""
1111

1212
import linecache
13-
import sys
13+
import tokenize
1414

1515
import py
1616
import pytest
1717

1818
from pyfakefs.fake_filesystem_unittest import Patcher
1919

20-
if sys.version_info >= (3,):
21-
import builtins
22-
else:
23-
import __builtin__ as builtins
24-
2520
Patcher.SKIPMODULES.add(py) # Ignore pytest components when faking filesystem
2621

2722
# The "linecache" module is used to read the test file in case of test failure
2823
# to get traceback information before test tear down.
2924
# In order to make sure that reading the test file is not faked,
3025
# we both skip faking the module, and add the build-in open() function
31-
# as a local function in the module
26+
# as a local function in the module (used in Python 2).
27+
# In Python 3, we also have to set back the cached open function in tokenize.
3228
Patcher.SKIPMODULES.add(linecache)
33-
linecache.open = builtins.open
3429

3530

3631
@pytest.fixture
3732
def fs(request):
3833
""" Fake filesystem. """
3934
patcher = Patcher()
4035
patcher.setUp()
36+
linecache.open = patcher.original_open
37+
tokenize._builtin_open = patcher.original_open
4138
request.addfinalizer(patcher.tearDown)
4239
return patcher.fs

pyfakefs/tests/example.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
3030
The `open()` built-in is bound to the fake `open()`:
3131
32-
>>> open #doctest: +ELLIPSIS
33-
<pyfakefs.fake_filesystem.FakeFileOpen object...>
32+
>>> open #doctest: +ELLIPSIS
33+
<bound method Fake...Module.open of <pyfakefs.fake_filesystem.Fake...>
3434
3535
In Python 2 the `file()` built-in is also bound to the fake `open()`. `file()`
3636
was eliminated in Python 3.

pyfakefs/tests/fake_filesystem_unittest_test.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from pyfakefs.extra_packages import pathlib
3434
from pyfakefs.fake_filesystem_unittest import Patcher
3535
import pyfakefs.tests.import_as_example
36+
from pyfakefs.helpers import IS_PYPY, IS_PY2
3637
from pyfakefs.tests.fixtures import module_with_attributes
3738

3839

@@ -54,7 +55,7 @@ def setUp(self):
5455
class TestPyfakefsUnittest(TestPyfakefsUnittestBase): # pylint: disable=R0904
5556
"""Test the `pyfakefs.fake_filesystem_unittest.TestCase` base class."""
5657

57-
@unittest.skipIf(sys.version_info > (2,),
58+
@unittest.skipIf(sys.version_info[0] > 2,
5859
"file() was removed in Python 3")
5960
def test_file(self):
6061
"""Fake `file()` function is bound"""
@@ -185,6 +186,19 @@ def test_import_function_from_os_as_other_name(self):
185186
stat_result = pyfakefs.tests.import_as_example.file_stat2(file_path)
186187
self.assertEqual(3, stat_result.st_size)
187188

189+
@unittest.skipIf(IS_PYPY and IS_PY2, 'Not working for PyPy2')
190+
def test_import_open_as_other_name(self):
191+
file_path = '/foo/bar'
192+
self.fs.create_file(file_path, contents=b'abc')
193+
contents = pyfakefs.tests.import_as_example.file_contents1(file_path)
194+
self.assertEqual('abc', contents)
195+
196+
def test_import_io_open_as_other_name(self):
197+
file_path = '/foo/bar'
198+
self.fs.create_file(file_path, contents=b'abc')
199+
contents = pyfakefs.tests.import_as_example.file_contents2(file_path)
200+
self.assertEqual('abc', contents)
201+
188202

189203
class TestAttributesWithFakeModuleNames(TestPyfakefsUnittestBase):
190204
"""Test that module attributes with names like `path` or `io` are not

pyfakefs/tests/import_as_example.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020
from os import stat as my_stat
2121
from os.path import exists
2222
from os.path import exists as my_exists
23+
from io import open as io_open
24+
25+
try:
26+
from builtins import open as bltn_open
27+
except ImportError:
28+
from __builtin__ import open as bltn_open
2329

2430
import sys
2531

@@ -79,3 +85,13 @@ def system_stat(filepath):
7985
else:
8086
from posix import stat as system_stat
8187
return system_stat(filepath)
88+
89+
90+
def file_contents1(filepath):
91+
with bltn_open(filepath) as f:
92+
return f.read()
93+
94+
95+
def file_contents2(filepath):
96+
with io_open(filepath) as f:
97+
return f.read()

0 commit comments

Comments
 (0)