Skip to content

Commit

Permalink
Use cleanup hook to reload django view
Browse files Browse the repository at this point in the history
- reload only the django view modules instead of all modules
  • Loading branch information
mrbean-bremen committed Feb 25, 2024
1 parent c4df67d commit 991adb5
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 18 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The released versions correspond to PyPI releases.
### Fixes
* Fixed a specific problem on reloading a pandas-related module (see [#947](../../issues/947)),
added possibility for unload hooks for specific modules
* Use this also to reload django views (see [#932](../../issues/932))

## [Version 5.3.5](https://pypi.python.org/pypi/pyfakefs/5.3.5) (2024-01-30)
Fixes a regression.
Expand Down
26 changes: 10 additions & 16 deletions pyfakefs/fake_filesystem_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,7 @@ def __init__(
self.use_cache = use_cache
self.use_dynamic_patch = use_dynamic_patch
self.module_cleanup_mode = module_cleanup_mode
self.cleanup_handlers: Dict[str, Callable[[], bool]] = {}
self.cleanup_handlers: Dict[str, Callable[[str], bool]] = {}

if use_known_patches:
from pyfakefs.patched_packages import (
Expand Down Expand Up @@ -683,7 +683,7 @@ def clear_cache(self) -> None:
"""Clear the module cache (convenience instance method)."""
self.__class__.clear_fs_cache()

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

self._dyn_patcher = DynamicPatcher(
self, cleanup_handlers=self.cleanup_handlers
)
self._dyn_patcher = DynamicPatcher(self)
sys.meta_path.insert(0, self._dyn_patcher)
for module in self.modules_to_reload:
if sys.modules.get(module.__name__) is module:
Expand Down Expand Up @@ -1127,16 +1125,12 @@ class DynamicPatcher(MetaPathFinder, Loader):
Implements the protocol needed for import hooks.
"""

def __init__(
self,
patcher: Patcher,
cleanup_handlers: Optional[Dict[str, Callable[[], bool]]] = None,
) -> None:
def __init__(self, patcher: Patcher) -> None:
self._patcher = patcher
self.sysmodules = {}
self.modules = self._patcher.fake_modules
self._loaded_module_names: Set[str] = set()
self.cleanup_handlers = cleanup_handlers or {}
self.cleanup_handlers = patcher.cleanup_handlers

# remove all modules that have to be patched from `sys.modules`,
# otherwise the find_... methods will not be called
Expand All @@ -1163,13 +1157,13 @@ def cleanup(self, cleanup_mode: ModuleCleanupMode) -> None:
# reload the modules instead - this is a workaround related to some internal
# module caching by django, that will likely change in the future.
if cleanup_mode == ModuleCleanupMode.AUTO:
if "django" in sys.modules:
cleanup_mode = ModuleCleanupMode.RELOAD
else:
cleanup_mode = ModuleCleanupMode.DELETE
# if "django" in sys.modules:
# cleanup_mode = ModuleCleanupMode.RELOAD
# else:
cleanup_mode = ModuleCleanupMode.DELETE
for name in self._loaded_module_names:
if name in sys.modules and name not in reloaded_module_names:
if name in self.cleanup_handlers and self.cleanup_handlers[name]():
if name in self.cleanup_handlers and self.cleanup_handlers[name](name):
continue
if cleanup_mode == ModuleCleanupMode.RELOAD:
try:
Expand Down
49 changes: 47 additions & 2 deletions pyfakefs/patched_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
with pyfakefs.
"""
import sys
from importlib import reload

try:
import pandas as pd
Expand All @@ -33,9 +34,16 @@
except ImportError:
xlrd = None


try:
from django.core.files import locks
import django

try:
from django.core.files import locks
except ImportError:
locks = None
except ImportError:
django = None
locks = None

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


def reload_handler(name):
if name in sys.modules:
reload(sys.modules[name])
return True


def get_cleanup_handlers():
handlers = {}
if pd is not None:
handlers["pandas.core.arrays.arrow.extension_types"] = (
handle_extension_type_cleanup
)
if django is not None:
for module_name in django_view_modules():
handlers[module_name] = lambda name=module_name: reload_handler(name)
return handlers


Expand Down Expand Up @@ -151,7 +168,7 @@ def __getattr__(self, name):

if pd is not None:

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

def __getattr__(self, name):
return getattr(self._locks_module, name)


if django is not None:

def get_all_view_modules(urlpatterns, modules=None):
if modules is None:
modules = set()
for pattern in urlpatterns:
if hasattr(pattern, "url_patterns"):
get_all_view_modules(pattern.url_patterns, modules=modules)
else:
if hasattr(pattern.callback, "cls"):
view = pattern.callback.cls
elif hasattr(pattern.callback, "view_class"):
view = pattern.callback.view_class
else:
view = pattern.callback
modules.add(view.__module__)
return modules

def django_view_modules():
try:
all_urlpatterns = __import__(
django.conf.settings.ROOT_URLCONF
).urls.urlpatterns
return get_all_view_modules(all_urlpatterns)
except Exception:
return set()

0 comments on commit 991adb5

Please sign in to comment.