Skip to content

Commit 5c44c64

Browse files
committed
Use cleanup hook to reload django views
- reload only the django view modules instead of all modules - change the module cleanup mode default to always delete modules (avoiding problems with reloading other modules) - closes #932
1 parent c4df67d commit 5c44c64

File tree

4 files changed

+74
-32
lines changed

4 files changed

+74
-32
lines changed

CHANGES.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The released versions correspond to PyPI releases.
66
### Fixes
77
* Fixed a specific problem on reloading a pandas-related module (see [#947](../../issues/947)),
88
added possibility for unload hooks for specific modules
9+
* Use this also to reload django views (see [#932](../../issues/932))
910

1011
## [Version 5.3.5](https://pypi.python.org/pypi/pyfakefs/5.3.5) (2024-01-30)
1112
Fixes a regression.

docs/usage.rst

+17-11
Original file line numberDiff line numberDiff line change
@@ -675,24 +675,30 @@ use_dynamic_patch
675675
~~~~~~~~~~~~~~~~~
676676
If ``True`` (the default), dynamic patching after setup is used (for example
677677
for modules loaded locally inside of functions).
678-
Can be switched off if it causes unwanted side effects, which happened at least in
679-
once instance while testing a django project.
678+
Can be switched off if it causes unwanted side effects, though that would mean that
679+
dynamically loaded modules are no longer patched, if they use file system functions.
680680

681681
module_cleanup_mode
682682
~~~~~~~~~~~~~~~~~~~
683683
This is a setting that works around a potential problem with the cleanup of
684-
dynamically loaded modules (e.g. modules loaded after the test has started),
685-
known to occur with `django` applications.
686-
The setting is subject to change or removal in future versions, provided a better
687-
solution for the problem is found.
688-
689-
The setting defines how the dynamically loaded modules are cleaned up after the test
690-
to ensure that no patched modules can be used after the test has finished.
691-
The default (ModuleCleanupMode.AUTO) currently depends on the availability of the `django` module,
692-
DELETE will delete all dynamically loaded modules and RELOAD will reload them.
684+
dynamically loaded modules (e.g. modules loaded after the test has started).
685+
As the original problem related to `django` has now been resolved in another way (see below),
686+
the setting may not be needed anymore, and is subject to removal in a future version.
687+
688+
The setting defines how the dynamically loaded modules are cleaned up after the test.
689+
Any dynamically loaded module is cleaned up after the test to ensure that no patched modules
690+
can be used after that.
691+
The default (`ModuleCleanupMode.AUTO`) is currently the same as `ModuleCleanupMode.DELETE`.
692+
`DELETE` will delete all dynamically loaded modules (so that they are reloaded the next time
693+
they are needed), while `RELOAD` will reload them immediately.
693694
Under some rare conditions, changing this setting may help to avoid problems related
694695
to incorrect test cleanup.
695696

697+
Problems with unloading dynamically loaded modules may also be solved more
698+
specifically by registering a handler for specific modules (using `Patcher.register_cleanup_handler`),
699+
that will be called during the cleanup process. This is used internally
700+
to handle known problems with the `django` and `pandas` packages.
701+
696702
.. _convenience_methods:
697703

698704
Using convenience methods

pyfakefs/fake_filesystem_unittest.py

+9-19
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,7 @@ def __init__(
619619
self.use_cache = use_cache
620620
self.use_dynamic_patch = use_dynamic_patch
621621
self.module_cleanup_mode = module_cleanup_mode
622-
self.cleanup_handlers: Dict[str, Callable[[], bool]] = {}
622+
self.cleanup_handlers: Dict[str, Callable[[str], bool]] = {}
623623

624624
if use_known_patches:
625625
from pyfakefs.patched_packages import (
@@ -683,7 +683,7 @@ def clear_cache(self) -> None:
683683
"""Clear the module cache (convenience instance method)."""
684684
self.__class__.clear_fs_cache()
685685

686-
def register_cleanup_handler(self, name: str, handler: Callable[[], bool]):
686+
def register_cleanup_handler(self, name: str, handler: Callable[[str], bool]):
687687
"""Register a handler for cleaning up a module after it had been loaded by
688688
the dynamic patcher. This allows to handle modules that cannot be reloaded
689689
without unwanted side effects.
@@ -965,9 +965,7 @@ def start_patching(self) -> None:
965965
self.patch_functions()
966966
self.patch_defaults()
967967

968-
self._dyn_patcher = DynamicPatcher(
969-
self, cleanup_handlers=self.cleanup_handlers
970-
)
968+
self._dyn_patcher = DynamicPatcher(self)
971969
sys.meta_path.insert(0, self._dyn_patcher)
972970
for module in self.modules_to_reload:
973971
if sys.modules.get(module.__name__) is module:
@@ -1127,16 +1125,12 @@ class DynamicPatcher(MetaPathFinder, Loader):
11271125
Implements the protocol needed for import hooks.
11281126
"""
11291127

1130-
def __init__(
1131-
self,
1132-
patcher: Patcher,
1133-
cleanup_handlers: Optional[Dict[str, Callable[[], bool]]] = None,
1134-
) -> None:
1128+
def __init__(self, patcher: Patcher) -> None:
11351129
self._patcher = patcher
11361130
self.sysmodules = {}
11371131
self.modules = self._patcher.fake_modules
11381132
self._loaded_module_names: Set[str] = set()
1139-
self.cleanup_handlers = cleanup_handlers or {}
1133+
self.cleanup_handlers = patcher.cleanup_handlers
11401134

11411135
# remove all modules that have to be patched from `sys.modules`,
11421136
# otherwise the find_... methods will not be called
@@ -1159,17 +1153,13 @@ def cleanup(self, cleanup_mode: ModuleCleanupMode) -> None:
11591153
]
11601154
# Delete all modules loaded during the test, ensuring that
11611155
# they are reloaded after the test.
1162-
# If cleanup_mode is set to RELOAD, or it is AUTO and django is imported,
1163-
# reload the modules instead - this is a workaround related to some internal
1164-
# module caching by django, that will likely change in the future.
1156+
# If cleanup_mode is set to RELOAD, reload the modules instead.
1157+
# This is probably not needed anymore with the cleanup handlers in place.
11651158
if cleanup_mode == ModuleCleanupMode.AUTO:
1166-
if "django" in sys.modules:
1167-
cleanup_mode = ModuleCleanupMode.RELOAD
1168-
else:
1169-
cleanup_mode = ModuleCleanupMode.DELETE
1159+
cleanup_mode = ModuleCleanupMode.DELETE
11701160
for name in self._loaded_module_names:
11711161
if name in sys.modules and name not in reloaded_module_names:
1172-
if name in self.cleanup_handlers and self.cleanup_handlers[name]():
1162+
if name in self.cleanup_handlers and self.cleanup_handlers[name](name):
11731163
continue
11741164
if cleanup_mode == ModuleCleanupMode.RELOAD:
11751165
try:

pyfakefs/patched_packages.py

+47-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
with pyfakefs.
1616
"""
1717
import sys
18+
from importlib import reload
1819

1920
try:
2021
import pandas as pd
@@ -33,9 +34,16 @@
3334
except ImportError:
3435
xlrd = None
3536

37+
3638
try:
37-
from django.core.files import locks
39+
import django
40+
41+
try:
42+
from django.core.files import locks
43+
except ImportError:
44+
locks = None
3845
except ImportError:
46+
django = None
3947
locks = None
4048

4149
# From pandas v 1.2 onwards the python fs functions are used even when the engine
@@ -63,12 +71,21 @@ def get_classes_to_patch():
6371
return classes_to_patch
6472

6573

74+
def reload_handler(name):
75+
if name in sys.modules:
76+
reload(sys.modules[name])
77+
return True
78+
79+
6680
def get_cleanup_handlers():
6781
handlers = {}
6882
if pd is not None:
6983
handlers["pandas.core.arrays.arrow.extension_types"] = (
7084
handle_extension_type_cleanup
7185
)
86+
if django is not None:
87+
for module_name in django_view_modules():
88+
handlers[module_name] = lambda name=module_name: reload_handler(name)
7289
return handlers
7390

7491

@@ -151,7 +168,7 @@ def __getattr__(self, name):
151168

152169
if pd is not None:
153170

154-
def handle_extension_type_cleanup():
171+
def handle_extension_type_cleanup(_name):
155172
# the module registers two extension types on load
156173
# on reload it raises if the extensions have not been unregistered before
157174
try:
@@ -186,3 +203,31 @@ def unlock(f):
186203

187204
def __getattr__(self, name):
188205
return getattr(self._locks_module, name)
206+
207+
208+
if django is not None:
209+
210+
def get_all_view_modules(urlpatterns, modules=None):
211+
if modules is None:
212+
modules = set()
213+
for pattern in urlpatterns:
214+
if hasattr(pattern, "url_patterns"):
215+
get_all_view_modules(pattern.url_patterns, modules=modules)
216+
else:
217+
if hasattr(pattern.callback, "cls"):
218+
view = pattern.callback.cls
219+
elif hasattr(pattern.callback, "view_class"):
220+
view = pattern.callback.view_class
221+
else:
222+
view = pattern.callback
223+
modules.add(view.__module__)
224+
return modules
225+
226+
def django_view_modules():
227+
try:
228+
all_urlpatterns = __import__(
229+
django.conf.settings.ROOT_URLCONF
230+
).urls.urlpatterns
231+
return get_all_view_modules(all_urlpatterns)
232+
except Exception:
233+
return set()

0 commit comments

Comments
 (0)