Skip to content

Commit 5373ca4

Browse files
committed
Use cleanup hook to reload django view
- 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)
1 parent c4df67d commit 5373ca4

File tree

4 files changed

+63
-28
lines changed

4 files changed

+63
-28
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

+6-7
Original file line numberDiff line numberDiff line change
@@ -680,16 +680,15 @@ once instance while testing a django project.
680680

681681
module_cleanup_mode
682682
~~~~~~~~~~~~~~~~~~~
683-
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.
683+
This is a setting that worked around a potential problem with the cleanup of
684+
dynamically loaded modules (e.g. modules loaded after the test has started).
685+
As the original problem has now been resolved in another way, the setting may
686+
not be needed anymore, and is subject to removal in a future version.
688687

689688
The setting defines how the dynamically loaded modules are cleaned up after the test
690689
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.
690+
The default (`ModuleCleanupMode.AUTO`) is currently the same as `ModuleCleanupMode.DELETE`.
691+
`DELETE` will delete all dynamically loaded modules and `RELOAD` will reload them.
693692
Under some rare conditions, changing this setting may help to avoid problems related
694693
to incorrect test cleanup.
695694

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)