@@ -156,6 +156,9 @@ def wrapped(*args, **kwargs):
156
156
return wrap_patchfs
157
157
158
158
159
+ DOCTEST_PATCHER = None
160
+
161
+
159
162
def load_doctests (
160
163
loader : Any ,
161
164
tests : TestSuite ,
@@ -177,22 +180,26 @@ def load_doctests(
177
180
178
181
File `example_test.py` in the pyfakefs release provides a usage example.
179
182
"""
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 ))
190
197
tests .addTests (
191
198
doctest .DocTestSuite (
192
199
module ,
193
200
globs = globs ,
194
- setUp = _patcher .setUp ,
195
- tearDown = _patcher .tearDown ,
201
+ setUp = Patcher . DOC_PATCHER .setUp ,
202
+ tearDown = Patcher . DOC_PATCHER .tearDown ,
196
203
)
197
204
)
198
205
return tests
@@ -242,9 +249,15 @@ def __init__(self, methodName='runTest'):
242
249
modules_to_reload : Optional [List [ModuleType ]] = None
243
250
modules_to_patch : Optional [Dict [str , ModuleType ]] = None
244
251
252
+ @property
253
+ def patcher (self ):
254
+ if hasattr (self , "_patcher" ):
255
+ return self ._patcher or Patcher .PATCHER
256
+ return Patcher .PATCHER
257
+
245
258
@property
246
259
def fs (self ) -> FakeFilesystem :
247
- return cast (FakeFilesystem , self ._stubber .fs )
260
+ return cast (FakeFilesystem , self .patcher .fs )
248
261
249
262
def setUpPyfakefs (
250
263
self ,
@@ -268,13 +281,17 @@ def setUpPyfakefs(
268
281
the current test case. Settings the arguments here may be a more
269
282
convenient way to adapt the setting than overwriting `__init__()`.
270
283
"""
284
+ # if the class has already a patcher setup, we use this one
285
+ if Patcher .PATCHER is not None :
286
+ return
287
+
271
288
if additional_skip_names is None :
272
289
additional_skip_names = self .additional_skip_names
273
290
if modules_to_reload is None :
274
291
modules_to_reload = self .modules_to_reload
275
292
if modules_to_patch is None :
276
293
modules_to_patch = self .modules_to_patch
277
- self ._stubber = Patcher (
294
+ self ._patcher = Patcher (
278
295
additional_skip_names = additional_skip_names ,
279
296
modules_to_reload = modules_to_reload ,
280
297
modules_to_patch = modules_to_patch ,
@@ -285,8 +302,69 @@ def setUpPyfakefs(
285
302
use_cache = use_cache ,
286
303
)
287
304
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
290
368
291
369
def pause (self ) -> None :
292
370
"""Pause the patching of the file system modules until `resume` is
@@ -295,15 +373,15 @@ def pause(self) -> None:
295
373
Calling pause() twice is silently ignored.
296
374
297
375
"""
298
- self ._stubber .pause ()
376
+ self .patcher .pause ()
299
377
300
378
def resume (self ) -> None :
301
379
"""Resume the patching of the file system modules if `pause` has
302
380
been called before. After that call, all file system calls are
303
381
executed in the fake file system.
304
382
Does nothing if patching is not paused.
305
383
"""
306
- self ._stubber .resume ()
384
+ self .patcher .resume ()
307
385
308
386
309
387
class TestCase (unittest .TestCase , TestCaseMixin ):
@@ -408,10 +486,16 @@ class Patcher:
408
486
PATCHED_MODULE_NAMES : Set [str ] = set ()
409
487
ADDITIONAL_SKIP_NAMES : Set [str ] = set ()
410
488
PATCH_DEFAULT_ARGS = False
411
- PATCHER = None
489
+ PATCHER : Optional ["Patcher" ] = None
490
+ DOC_PATCHER : Optional ["Patcher" ] = None
412
491
REF_COUNT = 0
492
+ DOC_REF_COUNT = 0
413
493
414
494
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
415
499
if cls .PATCHER is None :
416
500
cls .PATCHER = super ().__new__ (cls )
417
501
return cls .PATCHER
@@ -426,6 +510,7 @@ def __init__(
426
510
patch_open_code : PatchMode = PatchMode .OFF ,
427
511
patch_default_args : bool = False ,
428
512
use_cache : bool = True ,
513
+ is_doc_test : bool = False ,
429
514
) -> None :
430
515
"""
431
516
Args:
@@ -458,7 +543,11 @@ def __init__(
458
543
feature, this argument allows to turn it off in case it
459
544
causes any problems.
460
545
"""
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 :
462
551
return
463
552
if not allow_root_user :
464
553
# set non-root IDs even if the real user is root
@@ -764,9 +853,14 @@ def setUp(self, doctester: Any = None) -> None:
764
853
"""Bind the file-related modules to the :py:mod:`pyfakefs` fake
765
854
modules real ones. Also bind the fake `file()` and `open()` functions.
766
855
"""
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
770
864
self .has_fcopy_file = (
771
865
sys .platform == "darwin"
772
866
and hasattr (shutil , "_HAS_FCOPYFILE" )
@@ -853,15 +947,23 @@ def replace_globs(self, globs_: Dict[str, Any]) -> Dict[str, Any]:
853
947
854
948
def tearDown (self , doctester : Any = None ):
855
949
"""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
859
958
self .stop_patching ()
860
959
if self .has_fcopy_file :
861
960
shutil ._HAS_FCOPYFILE = True # type: ignore[attr-defined]
862
961
863
962
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
865
967
866
968
def stop_patching (self ) -> None :
867
969
if self ._patching :
0 commit comments