Skip to content

Commit 548fdf3

Browse files
committed
Add setUpClassPyfakefs convenience method
- only available from Python 3.8 onwards - also add class-scoped 'fs_class' fixture - handle patcher for doc tests separately
1 parent 58c6325 commit 548fdf3

File tree

4 files changed

+200
-32
lines changed

4 files changed

+200
-32
lines changed

Diff for: docs/usage.rst

+40-5
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,17 @@ tests:
3737
"""
3838
yield fs
3939
40-
Module- and session scoped fixtures
41-
...................................
42-
For convenience, module- and session-scoped fixtures with the same
43-
functionality are provided, named ``fs_module`` and ``fs_session``,
40+
Class-, module- and session-scoped fixtures
41+
...........................................
42+
For convenience, class-, module- and session-scoped fixtures with the same
43+
functionality are provided, named ``fake_fs``, ```fs_module`` and ``fs_session``,
4444
respectively.
4545

4646
.. caution:: If any of these fixtures is active, any other ``fs`` fixture will
4747
not setup / tear down the fake filesystem in the current scope; instead, it
48-
will just serve as a reference to the active fake filesystem.
48+
will just serve as a reference to the active fake filesystem. That means that changes
49+
done in the fake filesystem inside a test will remain there until the respective scope
50+
ends.
4951

5052
Patch using fake_filesystem_unittest
5153
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -74,6 +76,39 @@ with the fake file system functions and modules:
7476
The usage is explained in more detail in :ref:`auto_patch` and
7577
demonstrated in the files `example.py`_ and `example_test.py`_.
7678

79+
If your setup is the same for all tests in a class, you can use the class setup
80+
method ``setUpClassPyfakefs`` instead:
81+
82+
.. code:: python
83+
84+
from pyfakefs.fake_filesystem_unittest import TestCase
85+
86+
87+
class ExampleTestCase(TestCase):
88+
def setUpClass(cls):
89+
self.setUpClassPyfakefs()
90+
cls.file_path = "/test/file.txt"
91+
# you can access the fake fs via fake_fs() here
92+
cls.fake_fs().create_file(file_path)
93+
94+
def test1(self):
95+
self.assertTrue(os.path.exists(self.file_path))
96+
97+
def test2(self):
98+
self.assertTrue(os.path.exists(self.file_path))
99+
file_path = "/test/file2.txt"
100+
# self.fs is the same instance as cls.fake_fs() above
101+
self.fs.create_file(file_path)
102+
self.assertTrue(os.path.exists(file_path))
103+
104+
.. note:: This feature cannot be used with a Python version before Python 3.8 due to
105+
a missing feature in ``unittest``.
106+
107+
.. caution:: If this is used, any changes made in the fake filesystem inside a test
108+
will remain there for all following tests, if they are not reverted in the test
109+
itself.
110+
111+
77112
Patch using fake_filesystem_unittest.Patcher
78113
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
79114
If you are using other means of testing like `nose`_,

Diff for: pyfakefs/fake_filesystem_unittest.py

+129-27
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ def wrapped(*args, **kwargs):
156156
return wrap_patchfs
157157

158158

159+
DOCTEST_PATCHER = None
160+
161+
159162
def load_doctests(
160163
loader: Any,
161164
tests: TestSuite,
@@ -177,22 +180,26 @@ def load_doctests(
177180
178181
File `example_test.py` in the pyfakefs release provides a usage example.
179182
"""
180-
_patcher = Patcher(
181-
additional_skip_names=additional_skip_names,
182-
modules_to_reload=modules_to_reload,
183-
modules_to_patch=modules_to_patch,
184-
allow_root_user=allow_root_user,
185-
use_known_patches=use_known_patches,
186-
patch_open_code=patch_open_code,
187-
patch_default_args=patch_default_args,
188-
)
189-
globs = _patcher.replace_globs(vars(module))
183+
has_patcher = Patcher.DOC_PATCHER is not None
184+
if not has_patcher:
185+
Patcher.DOC_PATCHER = Patcher(
186+
additional_skip_names=additional_skip_names,
187+
modules_to_reload=modules_to_reload,
188+
modules_to_patch=modules_to_patch,
189+
allow_root_user=allow_root_user,
190+
use_known_patches=use_known_patches,
191+
patch_open_code=patch_open_code,
192+
patch_default_args=patch_default_args,
193+
is_doc_test=True,
194+
)
195+
assert Patcher.DOC_PATCHER is not None
196+
globs = Patcher.DOC_PATCHER.replace_globs(vars(module))
190197
tests.addTests(
191198
doctest.DocTestSuite(
192199
module,
193200
globs=globs,
194-
setUp=_patcher.setUp,
195-
tearDown=_patcher.tearDown,
201+
setUp=Patcher.DOC_PATCHER.setUp,
202+
tearDown=Patcher.DOC_PATCHER.tearDown,
196203
)
197204
)
198205
return tests
@@ -242,9 +249,15 @@ def __init__(self, methodName='runTest'):
242249
modules_to_reload: Optional[List[ModuleType]] = None
243250
modules_to_patch: Optional[Dict[str, ModuleType]] = None
244251

252+
@property
253+
def patcher(self):
254+
if hasattr(self, "_patcher"):
255+
return self._patcher or Patcher.PATCHER
256+
return Patcher.PATCHER
257+
245258
@property
246259
def fs(self) -> FakeFilesystem:
247-
return cast(FakeFilesystem, self._stubber.fs)
260+
return cast(FakeFilesystem, self.patcher.fs)
248261

249262
def setUpPyfakefs(
250263
self,
@@ -268,13 +281,17 @@ def setUpPyfakefs(
268281
the current test case. Settings the arguments here may be a more
269282
convenient way to adapt the setting than overwriting `__init__()`.
270283
"""
284+
# if the class has already a patcher setup, we use this one
285+
if Patcher.PATCHER is not None:
286+
return
287+
271288
if additional_skip_names is None:
272289
additional_skip_names = self.additional_skip_names
273290
if modules_to_reload is None:
274291
modules_to_reload = self.modules_to_reload
275292
if modules_to_patch is None:
276293
modules_to_patch = self.modules_to_patch
277-
self._stubber = Patcher(
294+
self._patcher = Patcher(
278295
additional_skip_names=additional_skip_names,
279296
modules_to_reload=modules_to_reload,
280297
modules_to_patch=modules_to_patch,
@@ -285,8 +302,69 @@ def setUpPyfakefs(
285302
use_cache=use_cache,
286303
)
287304

288-
self._stubber.setUp()
289-
cast(TestCase, self).addCleanup(self._stubber.tearDown)
305+
self._patcher.setUp()
306+
cast(TestCase, self).addCleanup(self._patcher.tearDown)
307+
308+
@classmethod
309+
def setUpClassPyfakefs(
310+
cls,
311+
additional_skip_names: Optional[List[Union[str, ModuleType]]] = None,
312+
modules_to_reload: Optional[List[ModuleType]] = None,
313+
modules_to_patch: Optional[Dict[str, ModuleType]] = None,
314+
allow_root_user: bool = True,
315+
use_known_patches: bool = True,
316+
patch_open_code: PatchMode = PatchMode.OFF,
317+
patch_default_args: bool = False,
318+
use_cache: bool = True,
319+
) -> None:
320+
"""Similar to :py:func:`setUpPyfakefs`, but as a class method that
321+
can be used in `setUpClass` instead of in `setUp`.
322+
The fake filesystem will live in all test methods in the test class
323+
and can be used in the usual way.
324+
Note that using both :py:func:`setUpClassPyfakefs` and
325+
:py:func:`setUpPyfakefs` in the same class will not work correctly.
326+
327+
.. note:: This method is only available from Python 3.8 onwards.
328+
"""
329+
if sys.version_info < (3, 8):
330+
raise NotImplementedError(
331+
"setUpClassPyfakefs is only available in "
332+
"Python versions starting from 3.8"
333+
)
334+
335+
# if the class has already a patcher setup, we use this one
336+
if Patcher.PATCHER is not None:
337+
return
338+
339+
if additional_skip_names is None:
340+
additional_skip_names = cls.additional_skip_names
341+
if modules_to_reload is None:
342+
modules_to_reload = cls.modules_to_reload
343+
if modules_to_patch is None:
344+
modules_to_patch = cls.modules_to_patch
345+
Patcher.PATCHER = Patcher(
346+
additional_skip_names=additional_skip_names,
347+
modules_to_reload=modules_to_reload,
348+
modules_to_patch=modules_to_patch,
349+
allow_root_user=allow_root_user,
350+
use_known_patches=use_known_patches,
351+
patch_open_code=patch_open_code,
352+
patch_default_args=patch_default_args,
353+
use_cache=use_cache,
354+
)
355+
356+
Patcher.PATCHER.setUp()
357+
cast(TestCase, cls).addClassCleanup(Patcher.PATCHER.tearDown)
358+
359+
@classmethod
360+
def fake_fs(cls):
361+
"""Convenience class method for accessing the fake filesystem.
362+
For use inside `setUpClass`, after :py:func:`setUpClassPyfakefs`
363+
has been called.
364+
"""
365+
if Patcher.PATCHER:
366+
return Patcher.PATCHER.fs
367+
return None
290368

291369
def pause(self) -> None:
292370
"""Pause the patching of the file system modules until `resume` is
@@ -295,15 +373,15 @@ def pause(self) -> None:
295373
Calling pause() twice is silently ignored.
296374
297375
"""
298-
self._stubber.pause()
376+
self.patcher.pause()
299377

300378
def resume(self) -> None:
301379
"""Resume the patching of the file system modules if `pause` has
302380
been called before. After that call, all file system calls are
303381
executed in the fake file system.
304382
Does nothing if patching is not paused.
305383
"""
306-
self._stubber.resume()
384+
self.patcher.resume()
307385

308386

309387
class TestCase(unittest.TestCase, TestCaseMixin):
@@ -408,10 +486,16 @@ class Patcher:
408486
PATCHED_MODULE_NAMES: Set[str] = set()
409487
ADDITIONAL_SKIP_NAMES: Set[str] = set()
410488
PATCH_DEFAULT_ARGS = False
411-
PATCHER = None
489+
PATCHER: Optional["Patcher"] = None
490+
DOC_PATCHER: Optional["Patcher"] = None
412491
REF_COUNT = 0
492+
DOC_REF_COUNT = 0
413493

414494
def __new__(cls, *args, **kwargs):
495+
if kwargs.get("is_doc_test", False):
496+
if cls.DOC_PATCHER is None:
497+
cls.DOC_PATCHER = super().__new__(cls)
498+
return cls.DOC_PATCHER
415499
if cls.PATCHER is None:
416500
cls.PATCHER = super().__new__(cls)
417501
return cls.PATCHER
@@ -426,6 +510,7 @@ def __init__(
426510
patch_open_code: PatchMode = PatchMode.OFF,
427511
patch_default_args: bool = False,
428512
use_cache: bool = True,
513+
is_doc_test: bool = False,
429514
) -> None:
430515
"""
431516
Args:
@@ -458,7 +543,11 @@ def __init__(
458543
feature, this argument allows to turn it off in case it
459544
causes any problems.
460545
"""
461-
if self.REF_COUNT > 0:
546+
self.is_doc_test = is_doc_test
547+
if is_doc_test:
548+
if self.DOC_REF_COUNT > 0:
549+
return
550+
elif self.REF_COUNT > 0:
462551
return
463552
if not allow_root_user:
464553
# set non-root IDs even if the real user is root
@@ -764,9 +853,14 @@ def setUp(self, doctester: Any = None) -> None:
764853
"""Bind the file-related modules to the :py:mod:`pyfakefs` fake
765854
modules real ones. Also bind the fake `file()` and `open()` functions.
766855
"""
767-
self.__class__.REF_COUNT += 1
768-
if self.__class__.REF_COUNT > 1:
769-
return
856+
if self.is_doc_test:
857+
self.__class__.DOC_REF_COUNT += 1
858+
if self.__class__.DOC_REF_COUNT > 1:
859+
return
860+
else:
861+
self.__class__.REF_COUNT += 1
862+
if self.__class__.REF_COUNT > 1:
863+
return
770864
self.has_fcopy_file = (
771865
sys.platform == "darwin"
772866
and hasattr(shutil, "_HAS_FCOPYFILE")
@@ -853,15 +947,23 @@ def replace_globs(self, globs_: Dict[str, Any]) -> Dict[str, Any]:
853947

854948
def tearDown(self, doctester: Any = None):
855949
"""Clear the fake filesystem bindings created by `setUp()`."""
856-
self.__class__.REF_COUNT -= 1
857-
if self.__class__.REF_COUNT > 0:
858-
return
950+
if self.is_doc_test:
951+
self.__class__.DOC_REF_COUNT -= 1
952+
if self.__class__.DOC_REF_COUNT > 0:
953+
return
954+
else:
955+
self.__class__.REF_COUNT -= 1
956+
if self.__class__.REF_COUNT > 0:
957+
return
859958
self.stop_patching()
860959
if self.has_fcopy_file:
861960
shutil._HAS_FCOPYFILE = True # type: ignore[attr-defined]
862961

863962
reset_ids()
864-
self.__class__.PATCHER = None
963+
if self.is_doc_test:
964+
self.__class__.DOC_PATCHER = None
965+
else:
966+
self.__class__.PATCHER = None
865967

866968
def stop_patching(self) -> None:
867969
if self._patching:

Diff for: pyfakefs/pytest_plugin.py

+12
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ def fs(request):
3030
patcher.tearDown()
3131

3232

33+
@pytest.fixture(scope="class")
34+
def fs_class(request):
35+
"""Class-scoped fake filesystem fixture."""
36+
if hasattr(request, "param"):
37+
patcher = Patcher(*request.param)
38+
else:
39+
patcher = Patcher()
40+
patcher.setUp()
41+
yield patcher.fs
42+
patcher.tearDown()
43+
44+
3345
@pytest.fixture(scope="module")
3446
def fs_module(request):
3547
"""Module-scoped fake filesystem fixture."""

Diff for: pyfakefs/tests/fake_filesystem_unittest_test.py

+19
Original file line numberDiff line numberDiff line change
@@ -850,5 +850,24 @@ def test_is_absolute(self, fs):
850850
self.assertTrue(pathlib.Path(".").absolute().is_absolute())
851851

852852

853+
@unittest.skipIf(sys.version_info < (3, 8), "Not available before Python 3.8")
854+
class TestClassSetup(fake_filesystem_unittest.TestCase):
855+
@classmethod
856+
def setUpClass(cls):
857+
cls.setUpClassPyfakefs()
858+
cls.fake_fs().create_file("foo/bar", contents="test")
859+
860+
def test_using_fs_functions(self):
861+
self.assertTrue(os.path.exists("foo/bar"))
862+
with open("foo/bar") as f:
863+
contents = f.read()
864+
self.assertEqual("test", contents)
865+
866+
def test_using_fakefs(self):
867+
self.assertTrue(self.fs.exists("foo/bar"))
868+
f = self.fs.get_object("foo/bar")
869+
self.assertEqual("test", f.contents)
870+
871+
853872
if __name__ == "__main__":
854873
unittest.main()

0 commit comments

Comments
 (0)