Skip to content

Commit ffed2cc

Browse files
committed
Automatically patch imported functions
- allows imports like 'from os import stat' to be patched
1 parent 0b89238 commit ffed2cc

9 files changed

+178
-12
lines changed

CHANGES.md

+11-7
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,19 @@ The release versions are PyPi releases.
1414
```
1515

1616
#### New Features
17-
* a module imported as another name (`import os as _os`) is now correctly
18-
patched without the need of additional parameters
17+
* improved automatic patching:
18+
* automatically patch methods of a patched file system module imported like
19+
`from os.path import exists`
20+
* a module imported as another name (`import os as _os`) is now correctly
21+
patched without the need of additional parameters
22+
([#434](../../pull/434))
23+
* automatically patch `Path` if imported like `from pathlib import Path`
24+
([#440](../../issues/440))
25+
* parameter `patch_path` has been removed from `UnitTest` and `Patcher`,
26+
the correct patching of `path` imports is now done automatically
27+
* `UnitTest` /`Patcher` arguments can now also be set in `setUpPyfakefs()`
1928
* added possibility to set root user ([#431](../../issues/431))
20-
* automatically patch `Path` if imported like `from pathlib import Path`
21-
([#440](../../issues/440))
2229
* added side_effect option to fake files ([#433](../../pull/433))
23-
* parameter `patch_path` has been removed from `UnitTest` and `Patcher`,
24-
the correct patching of `path` imports is now done automatically
25-
* `UnitTest` /`Patcher` arguments can now also be set in `setUpPyfakefs()`
2630
* added pathlib2 support ([#408](../../issues/408)) ([#422](../../issues/422))
2731
* added some support for extended filesystem attributes under Linux
2832
([#423](../../issues/423))

docs/usage.rst

+16-1
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,14 @@ Pyfakefs automatically patches file system related modules that are:
111111
from os import path
112112
from pathlib import Path
113113
114+
Additionally, functions from file system related modules are patched
115+
automatically if imported like:
116+
117+
.. code:: python
118+
119+
from os.path import exists
120+
from os import stat
121+
114122
There are other cases where automatic patching does not work.
115123
Both ``fake_filesystem_unittest.Patcher`` and ``fake_filesystem_unittest.TestCase``
116124
provide a few additional arguments to handle such cases.
@@ -132,8 +140,15 @@ modules_to_reload
132140
This allows to pass a list of modules that shall be reloaded, thus allowing
133141
to patch modules not patched automatically.
134142

143+
Here is a simple example for a default argument that is not patched
144+
automatically:
145+
146+
.. code:: python
147+
148+
def check_if_exists(filepath, file_exists=os.path.exists):
149+
return file_exists(filepath)
135150
136-
If adding the module containing these imports to ``modules_to_reload``, they
151+
If adding the module containing this code to ``modules_to_reload``, it
137152
will be correctly patched.
138153

139154
modules_to_patch

pyfakefs/fake_filesystem.py

+51-1
Original file line numberDiff line numberDiff line change
@@ -3026,6 +3026,23 @@ class FakePathModule(object):
30263026
"""
30273027
_OS_PATH_COPY = _copy_module(os.path)
30283028

3029+
@staticmethod
3030+
def dir():
3031+
"""Return the list of patched function names. Used for patching
3032+
functions imported from the module.
3033+
"""
3034+
dir = [
3035+
'abspath', 'dirname', 'exists', 'expanduser', 'getatime',
3036+
'getctime', 'getmtime', 'getsize', 'isabs', 'isdir', 'isfile',
3037+
'islink', 'ismount', 'join', 'lexists', 'normcase', 'normpath',
3038+
'realpath', 'relpath', 'split', 'splitdrive'
3039+
]
3040+
if IS_PY2:
3041+
dir.append('walk')
3042+
if sys.platform != 'win32' or not IS_PY2:
3043+
dir.append('samefile')
3044+
return dir
3045+
30293046
def __init__(self, filesystem, os_module=None):
30303047
"""Init.
30313048
@@ -3422,6 +3439,31 @@ class FakeOsModule(object):
34223439

34233440
devnull = None
34243441

3442+
@staticmethod
3443+
def dir():
3444+
"""Return the list of patched function names. Used for patching
3445+
functions imported from the module.
3446+
"""
3447+
dir = [
3448+
'access', 'chdir', 'chmod', 'chown', 'close', 'fstat', 'fsync',
3449+
'getcwd', 'lchmod', 'link', 'listdir', 'lstat', 'makedirs',
3450+
'mkdir', 'mknod', 'open', 'read', 'readlink', 'remove',
3451+
'removedirs', 'rename', 'rmdir', 'stat', 'symlink', 'umask',
3452+
'unlink', 'utime', 'walk', 'write'
3453+
]
3454+
if IS_PY2:
3455+
dir += ['getcwdu']
3456+
else:
3457+
dir += ['getcwdb', 'replace']
3458+
if sys.platform.startswith('linux'):
3459+
dir += [
3460+
'fdatasync','getxattr', 'listxattr',
3461+
'removexattr', 'setxattr'
3462+
]
3463+
if use_scandir:
3464+
dir += ['scandir']
3465+
return dir
3466+
34253467
def __init__(self, filesystem, os_path_module=None):
34263468
"""Also exposes self.path (to fake os.path).
34273469
@@ -3843,7 +3885,6 @@ def setxattr(self, path, attribute, value,
38433885
self.filesystem.raise_os_error(errno.EEXIST, file_obj.path)
38443886
file_obj.xattr[attribute] = value
38453887

3846-
38473888
if use_scandir:
38483889
def scandir(self, path=''):
38493890
"""Return an iterator of DirEntry objects corresponding to the
@@ -4420,6 +4461,15 @@ class FakeIoModule(object):
44204461
my_io_module = fake_filesystem.FakeIoModule(filesystem)
44214462
"""
44224463

4464+
@staticmethod
4465+
def dir():
4466+
"""Return the list of patched function names. Used for patching
4467+
functions imported from the module.
4468+
"""
4469+
# `open` would clash with build-in `open`, so don't patch it
4470+
# if imported like `from io import open`
4471+
return ()
4472+
44234473
def __init__(self, filesystem):
44244474
"""
44254475
Args:

pyfakefs/fake_filesystem_shutil.py

+11
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,22 @@
3030
import shutil
3131
import sys
3232

33+
from pyfakefs.helpers import IS_PY2
34+
3335

3436
class FakeShutilModule(object):
3537
"""Uses a FakeFilesystem to provide a fake replacement for shutil module.
3638
"""
3739

40+
@staticmethod
41+
def dir():
42+
"""Return the list of patched function names. Used for patching
43+
functions imported from the module.
44+
"""
45+
if not IS_PY2:
46+
return 'disk_usage',
47+
return ()
48+
3849
def __init__(self, filesystem):
3950
"""Construct fake shutil module using the fake filesystem.
4051

pyfakefs/fake_filesystem_unittest.py

+47-3
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
else:
7878
import builtins
7979

80+
OS_MODULE = 'nt' if sys.platform == 'win32' else 'posix'
8081
PATH_MODULE = 'ntpath' if sys.platform == 'win32' else 'posixpath'
8182

8283
def load_doctests(loader, tests, ignore, module,
@@ -284,7 +285,7 @@ class Patcher(object):
284285

285286
IS_WINDOWS = sys.platform in ('win32', 'cygwin')
286287

287-
SKIPNAMES = {'os', 'path', 'io', 'genericpath'}
288+
SKIPNAMES = {'os', 'path', 'io', 'genericpath', OS_MODULE, PATH_MODULE}
288289
if pathlib:
289290
SKIPNAMES.add('pathlib')
290291

@@ -338,8 +339,32 @@ def __init__(self, additional_skip_names=None,
338339
module_name)
339340
self._fake_module_classes[name] = fake_module
340341

342+
# handle patching function imported separately like
343+
# `from os import stat`
344+
# each patched function name has to be looked up separately
345+
self._fake_module_functions = {}
346+
for mod_name, fake_module in self._fake_module_classes.items():
347+
modnames = (
348+
(mod_name, OS_MODULE) if mod_name == 'os' else (mod_name,)
349+
)
350+
for fct_name in fake_module.dir():
351+
self._fake_module_functions[fct_name] = (
352+
modnames,
353+
getattr(fake_module, fct_name),
354+
mod_name
355+
)
356+
# special handling for functions in os.path
357+
fake_module = fake_filesystem.FakePathModule
358+
for fct_name in fake_module.dir():
359+
self._fake_module_functions[fct_name] = (
360+
('genericpath', PATH_MODULE),
361+
getattr(fake_module, fct_name),
362+
PATH_MODULE
363+
)
364+
341365
# Attributes set by _refresh()
342366
self._modules = {}
367+
self._fct_modules = {}
343368
self._stubs = None
344369
self.fs = None
345370
self.fake_open = None
@@ -366,6 +391,12 @@ def _find_modules(self):
366391
Later, `setUp()` will stub these with the fake file system
367392
modules.
368393
"""
394+
def is_fct(module, name):
395+
fct = module.__dict__.get(name)
396+
return (fct is not None and
397+
(inspect.isfunction(fct) or inspect.isbuiltin(fct)) and
398+
fct.__module__ in self._fake_module_functions[name][0])
399+
369400
module_names = list(self._fake_module_classes.keys()) + [PATH_MODULE]
370401
for name, module in set(sys.modules.items()):
371402
try:
@@ -378,6 +409,7 @@ def _find_modules(self):
378409
# where py.error has no __name__ attribute
379410
# see https://github.com/pytest-dev/py/issues/73
380411
continue
412+
381413
modules = {name: mod for name, mod in module.__dict__.items()
382414
if inspect.ismodule(mod) and
383415
mod.__name__ in module_names
@@ -387,6 +419,11 @@ def _find_modules(self):
387419
self._modules.setdefault(name, set()).add((module,
388420
mod.__name__))
389421

422+
functions = [name for name in self._fake_module_functions
423+
if is_fct(module, name)]
424+
for name in functions:
425+
self._fct_modules.setdefault(name, set()).add(module)
426+
390427
def _refresh(self):
391428
"""Renew the fake file system and set the _isStale flag to `False`."""
392429
if self._stubs is not None:
@@ -416,10 +453,17 @@ def setUp(self, doctester=None):
416453
# file() was eliminated in Python3
417454
self._stubs.smart_set(builtins, 'file', self.fake_open)
418455
self._stubs.smart_set(builtins, 'open', self.fake_open)
419-
for name in self._modules:
420-
for module, attr in self._modules[name]:
456+
for name, modules in self._modules.items():
457+
for module, attr in modules:
421458
self._stubs.smart_set(module, name, self.fake_modules[attr])
422459

460+
for name, modules in self._fct_modules.items():
461+
_, method, mod_name = self._fake_module_functions[name]
462+
fake_module = self.fake_modules[mod_name]
463+
attr = method.__get__(fake_module, fake_module.__class__)
464+
for module in modules:
465+
self._stubs.smart_set(module, name, attr)
466+
423467
self._dyn_patcher = DynamicPatcher(self)
424468
sys.meta_path.insert(0, self._dyn_patcher)
425469

pyfakefs/fake_pathlib.py

+11
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,13 @@ class FakePathlibModule(object):
633633
`fake_pathlib_module = fake_filesystem.FakePathlibModule(filesystem)`
634634
"""
635635

636+
@staticmethod
637+
def dir():
638+
"""Return an empty list as `Path` methods will always be called
639+
on the instances.
640+
"""
641+
return ()
642+
636643
def __init__(self, filesystem):
637644
"""
638645
Initializes the module with the given filesystem.
@@ -676,6 +683,10 @@ class FakePathlibPathModule(object):
676683
"""Patches `pathlib.Path` by passing all calls to FakePathlibModule."""
677684
fake_pathlib = None
678685

686+
@staticmethod
687+
def dir():
688+
return ()
689+
679690
def __init__(self, filesystem):
680691
if self.fake_pathlib is None:
681692
self.__class__.fake_pathlib = FakePathlibModule(filesystem)

pyfakefs/fake_scandir.py

+7
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,13 @@ class FakeScanDirModule(object):
258258
`fake_scandir_module = fake_filesystem.FakeScanDirModule(filesystem)`
259259
"""
260260

261+
@staticmethod
262+
def dir():
263+
"""Return the list of patched function names. Used for patching
264+
functions imported from the module.
265+
"""
266+
return 'scandir', 'walk'
267+
261268
def __init__(self, filesystem):
262269
self.filesystem = filesystem
263270

pyfakefs/tests/fake_filesystem_unittest_test.py

+12
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,18 @@ def test_import_path_from_pathlib(self):
160160
self.assertTrue(
161161
pyfakefs.tests.import_as_example.check_if_exists3(file_path))
162162

163+
def test_import_function_from_os_path(self):
164+
file_path = '/foo/bar'
165+
self.fs.create_dir(file_path)
166+
self.assertTrue(
167+
pyfakefs.tests.import_as_example.check_if_exists5(file_path))
168+
169+
def test_import_function_from_os(self):
170+
file_path = '/foo/bar'
171+
self.fs.create_file(file_path, contents=b'abc')
172+
stat_result = pyfakefs.tests.import_as_example.file_stat(file_path)
173+
self.assertEqual(3, stat_result.st_size)
174+
163175

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

pyfakefs/tests/import_as_example.py

+12
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
to be patched under another name.
1616
"""
1717
from os import path
18+
from os.path import exists
1819
import os as my_os
20+
from os import stat
1921

2022
try:
2123
from pathlib import Path
@@ -45,3 +47,13 @@ def check_if_exists3(filepath):
4547
def check_if_exists4(filepath, exists=my_os.path.exists):
4648
# this is a similar case as in the tempfile implementation under Posix
4749
return exists(filepath)
50+
51+
52+
def check_if_exists5(filepath):
53+
# tests patching `exists` imported from os.path
54+
return exists(filepath)
55+
56+
57+
def file_stat(filepath):
58+
# tests patching `stat` imported from os
59+
return stat(filepath)

0 commit comments

Comments
 (0)