Skip to content

Commit bdaca74

Browse files
committed
Introduce and use new argument "module_cleanup_mode"
- fixes a regression due to the changed behavior of the dynamic patcher cleanup The modules are now reloaded only if the django module is loaded, or the reload mode set to RELOAD.
1 parent 01e6dee commit bdaca74

File tree

3 files changed

+63
-9
lines changed

3 files changed

+63
-9
lines changed

CHANGES.md

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ The released versions correspond to PyPI releases.
33

44
## Unreleased
55

6+
### Fixes
7+
* Fixes a regression due to the changed behavior of the dynamic patcher cleanup (see [#939](../../issues/939)).
8+
The change is now by default only made if the `django` module is loaded, and the behavior can
9+
be changed using the new argument `module_cleanup_mode`.
10+
611
### Packaging
712
* include `tox.ini` and a few more files into the source distribution (see [#937](../../issues/937))
813

docs/usage.rst

+14
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,20 @@ for modules loaded locally inside of functions).
664664
Can be switched off if it causes unwanted side effects, which happened at least in
665665
once instance while testing a django project.
666666

667+
module_cleanup_mode
668+
~~~~~~~~~~~~~~~~~~~
669+
This is a setting that works around a potential problem with the cleanup of
670+
dynamically loaded modules (e.g. modules loaded after the test has started),
671+
known to occur with `django` applications.
672+
The setting is subject to change or removal in future versions, provided a better
673+
solution for the problem is found.
674+
675+
The setting defines how the dynamically loaded modules are cleaned up after the test
676+
to ensure that no patched modules can be used after the test has finished.
677+
The default (AUTO) currently depends on the availability of the `django` module,
678+
DELETE will delete all dynamically loaded modules and RELOAD will reload them.
679+
Under some rare conditions, changing this setting may help to avoid problems related
680+
to incorrect test cleanup.
667681

668682
.. _convenience_methods:
669683

pyfakefs/fake_filesystem_unittest.py

+44-9
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import sys
4848
import tempfile
4949
import tokenize
50+
from enum import Enum
5051
from importlib.abc import Loader, MetaPathFinder
5152
from types import ModuleType, TracebackType, FunctionType
5253
from typing import (
@@ -94,6 +95,14 @@
9495
PATH_MODULE = "ntpath" if sys.platform == "win32" else "posixpath"
9596

9697

98+
class ModuleCleanupMode(Enum):
99+
"""Defines the behavior of module cleanup on dynamic patcher shutdown."""
100+
101+
AUTO = 1
102+
DELETE = 2
103+
RELOAD = 3
104+
105+
97106
def patchfs(
98107
_func: Optional[Callable] = None,
99108
*,
@@ -106,6 +115,7 @@ def patchfs(
106115
patch_default_args: bool = False,
107116
use_cache: bool = True,
108117
use_dynamic_patch: bool = True,
118+
module_cleanup_mode: ModuleCleanupMode = ModuleCleanupMode.AUTO,
109119
) -> Callable:
110120
"""Convenience decorator to use patcher with additional parameters in a
111121
test function.
@@ -134,6 +144,7 @@ def wrapped(*args, **kwargs):
134144
patch_default_args=patch_default_args,
135145
use_cache=use_cache,
136146
use_dynamic_patch=use_dynamic_patch,
147+
module_cleanup_mode=module_cleanup_mode,
137148
) as p:
138149
args = list(args)
139150
args.append(p.fs)
@@ -170,6 +181,7 @@ def load_doctests(
170181
patch_open_code: PatchMode = PatchMode.OFF,
171182
patch_default_args: bool = False,
172183
use_dynamic_patch: bool = True,
184+
module_cleanup_mode: ModuleCleanupMode = ModuleCleanupMode.AUTO,
173185
) -> TestSuite: # pylint:disable=unused-argument
174186
"""Load the doctest tests for the specified module into unittest.
175187
Args:
@@ -190,6 +202,7 @@ def load_doctests(
190202
patch_open_code=patch_open_code,
191203
patch_default_args=patch_default_args,
192204
use_dynamic_patch=use_dynamic_patch,
205+
module_cleanup_mode=module_cleanup_mode,
193206
is_doc_test=True,
194207
)
195208
assert Patcher.DOC_PATCHER is not None
@@ -270,6 +283,7 @@ def setUpPyfakefs(
270283
patch_default_args: bool = False,
271284
use_cache: bool = True,
272285
use_dynamic_patch: bool = True,
286+
module_cleanup_mode: ModuleCleanupMode = ModuleCleanupMode.AUTO,
273287
) -> None:
274288
"""Bind the file-related modules to the :py:class:`pyfakefs` fake file
275289
system instead of the real file system. Also bind the fake `open()`
@@ -302,6 +316,7 @@ def setUpPyfakefs(
302316
patch_default_args=patch_default_args,
303317
use_cache=use_cache,
304318
use_dynamic_patch=use_dynamic_patch,
319+
module_cleanup_mode=module_cleanup_mode,
305320
)
306321

307322
self._patcher.setUp()
@@ -319,6 +334,7 @@ def setUpClassPyfakefs(
319334
patch_default_args: bool = False,
320335
use_cache: bool = True,
321336
use_dynamic_patch: bool = True,
337+
module_cleanup_mode: ModuleCleanupMode = ModuleCleanupMode.AUTO,
322338
) -> None:
323339
"""Similar to :py:func:`setUpPyfakefs`, but as a class method that
324340
can be used in `setUpClass` instead of in `setUp`.
@@ -357,6 +373,7 @@ def setUpClassPyfakefs(
357373
patch_default_args=patch_default_args,
358374
use_cache=use_cache,
359375
use_dynamic_patch=use_dynamic_patch,
376+
module_cleanup_mode=module_cleanup_mode,
360377
)
361378

362379
Patcher.PATCHER.setUp()
@@ -522,6 +539,7 @@ def __init__(
522539
patch_default_args: bool = False,
523540
use_cache: bool = True,
524541
use_dynamic_patch: bool = True,
542+
module_cleanup_mode: ModuleCleanupMode = ModuleCleanupMode.AUTO,
525543
is_doc_test: bool = False,
526544
) -> None:
527545
"""
@@ -554,9 +572,14 @@ def __init__(
554572
cached between tests for performance reasons. As this is a new
555573
feature, this argument allows to turn it off in case it
556574
causes any problems.
557-
use_dynamic_patch: If `True`, dynamic patching after setup is used
575+
use_dynamic_patch: If `True`, dynamic patching after setup is used
558576
(for example for modules loaded locally inside of functions).
559577
Can be switched off if it causes unwanted side effects.
578+
module_cleanup_mode: Defines how the modules in the dynamic patcher are
579+
cleaned up after the test. The default (AUTO) currently depends
580+
on the availability of the `django` module, DELETE will delete
581+
all dynamically loaded modules, RELOAD will reload them.
582+
This option is subject to change in later versions.
560583
"""
561584
self.is_doc_test = is_doc_test
562585
if is_doc_test:
@@ -595,6 +618,7 @@ def __init__(
595618
self.patch_default_args = patch_default_args
596619
self.use_cache = use_cache
597620
self.use_dynamic_patch = use_dynamic_patch
621+
self.module_cleanup_mode = module_cleanup_mode
598622

599623
if use_known_patches:
600624
from pyfakefs.patched_packages import (
@@ -928,7 +952,7 @@ def start_patching(self) -> None:
928952
if sys.modules.get(module.__name__) is module:
929953
reload(module)
930954
if not self.use_dynamic_patch:
931-
self._dyn_patcher.cleanup()
955+
self._dyn_patcher.cleanup(ModuleCleanupMode.DELETE)
932956
sys.meta_path.pop(0)
933957

934958
def patch_functions(self) -> None:
@@ -1006,7 +1030,7 @@ def stop_patching(self, temporary=False) -> None:
10061030
self._stubs.smart_unset_all()
10071031
self.unset_defaults()
10081032
if self.use_dynamic_patch and self._dyn_patcher:
1009-
self._dyn_patcher.cleanup()
1033+
self._dyn_patcher.cleanup(self.module_cleanup_mode)
10101034
sys.meta_path.pop(0)
10111035

10121036
@property
@@ -1098,7 +1122,7 @@ def __init__(self, patcher: Patcher) -> None:
10981122
for name, module in self.modules.items():
10991123
sys.modules[name] = module
11001124

1101-
def cleanup(self) -> None:
1125+
def cleanup(self, cleanup_mode: ModuleCleanupMode) -> None:
11021126
for module_name in self.sysmodules:
11031127
sys.modules[module_name] = self.sysmodules[module_name]
11041128
for module in self._patcher.modules_to_reload:
@@ -1107,13 +1131,24 @@ def cleanup(self) -> None:
11071131
reloaded_module_names = [
11081132
module.__name__ for module in self._patcher.modules_to_reload
11091133
]
1110-
# Reload all modules loaded during the test, ensuring that
1111-
# no faked modules are referenced after the test.
1134+
# Delete all modules loaded during the test, ensuring that
1135+
# they are reloaded after the test.
1136+
# If cleanup_mode is set to RELOAD, or it is AUTO and django is imported,
1137+
# reload the modules instead - this is a workaround related to some internal
1138+
# module caching by django, that will likely change in the future.
1139+
if cleanup_mode == ModuleCleanupMode.AUTO:
1140+
if "django" in sys.modules:
1141+
cleanup_mode = ModuleCleanupMode.RELOAD
1142+
else:
1143+
cleanup_mode = ModuleCleanupMode.DELETE
11121144
for name in self._loaded_module_names:
11131145
if name in sys.modules and name not in reloaded_module_names:
1114-
try:
1115-
reload(sys.modules[name])
1116-
except Exception:
1146+
if cleanup_mode == ModuleCleanupMode.RELOAD:
1147+
try:
1148+
reload(sys.modules[name])
1149+
except Exception:
1150+
del sys.modules[name]
1151+
else:
11171152
del sys.modules[name]
11181153

11191154
def needs_patch(self, name: str) -> bool:

0 commit comments

Comments
 (0)