#33107 closed Bug (fixed)
ImportError: partially initialized module '...' has no attribute '...'
| Reported by: | Collin Anderson | Owned by: | Mariusz Felisiak |
|---|---|---|---|
| Component: | Utilities | Version: | 4.0 |
| Severity: | Release blocker | Keywords: | |
| Cc: | Keryn Knight | Triage Stage: | Accepted |
| Has patch: | yes | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
Today I've been getting some occasional "ImportError"s when reloading the runserver. I have a feeling it's a race condition when importing my custom auth backend when runserver is reloading, likely caused by #33099 cached imports, committed on Friday. It doesn't happen every time which makes it harder to track down.
Maybe the cached_import logic needs to somehow check for a "partially initialized module" and use the slow-path in that case?
I'm not 100% it's not just an issue on my end, but I at least wanted to raise this here in case other people have issues and want to diagnose further.
Here's an example (ImportError when importing my custom auth backend):
Request Method: GET
Request URL: /cart/dropdown/
Django Version: 4.0.dev20210913065016
Python Version: 3.8.10
Template error:
In template templates/order/cart_dropdown.html, error at line 3
Module "authbackend" does not define a "Bknd" attribute/class
3 : {% if request.order.ops %}
Traceback (most recent call last):
File "django/template/base.py", line 862, in _resolve_lookup
current = current[bit]
During handling of the above exception ('WSGIRequest' object is not subscriptable), another exception occurred:
File "django/utils/module_loading.py", line 26, in import_string
return cached_import(module_path, class_name)
File "django/utils/module_loading.py", line 12, in cached_import
return getattr(modules[module_path], class_name)
The above exception (partially initialized module 'authbackend' has no attribute 'Bknd' (most likely due to a circular import)) was the direct cause of the following exception:
File "django/core/handlers/exception.py", line 47, in inner
response = get_response(request)
File "django/core/handlers/base.py", line 181, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "django/shortcuts.py", line 19, in render
content = loader.render_to_string(template_name, context, request, using=using)
File "django/template/loader.py", line 62, in render_to_string
return template.render(context, request)
File "django/template/backends/django.py", line 61, in render
return self.template.render(context)
File "django/template/base.py", line 176, in render
return self._render(context)
File "django/template/base.py", line 168, in _render
return self.nodelist.render(context)
File "django/template/base.py", line 977, in render
return SafeString(''.join([
File "django/template/base.py", line 978, in <listcomp>
node.render_annotated(context) for node in self
File "django/template/base.py", line 938, in render_annotated
return self.render(context)
File "django/template/defaulttags.py", line 386, in render
return strip_spaces_between_tags(self.nodelist.render(context).strip())
File "django/template/base.py", line 977, in render
return SafeString(''.join([
File "django/template/base.py", line 978, in <listcomp>
node.render_annotated(context) for node in self
File "django/template/base.py", line 938, in render_annotated
return self.render(context)
File "django/template/defaulttags.py", line 288, in render
match = condition.eval(context)
File "django/template/defaulttags.py", line 829, in eval
return self.value.resolve(context, ignore_failures=True)
File "django/template/base.py", line 701, in resolve
obj = self.var.resolve(context)
File "django/template/base.py", line 829, in resolve
value = self._resolve_lookup(context)
File "django/template/base.py", line 870, in _resolve_lookup
current = getattr(current, bit)
File "order/middleware.py", line 20, in __get__
if not request.user.pk: # if not logged in
File "django/utils/functional.py", line 248, in inner
self._setup()
File "django/utils/functional.py", line 384, in _setup
self._wrapped = self._setupfunc()
File "django/contrib/auth/middleware.py", line 25, in <lambda>
request.user = SimpleLazyObject(lambda: get_user(request))
File "django/contrib/auth/middleware.py", line 11, in get_user
request._cached_user = auth.get_user(request)
File "django/contrib/auth/__init__.py", line 183, in get_user
backend = load_backend(backend_path)
File "django/contrib/auth/__init__.py", line 21, in load_backend
return import_string(path)()
File "django/utils/module_loading.py", line 28, in import_string
raise ImportError('Module "%s" does not define a "%s" attribute/class' % (
Exception Type: ImportError at /cart/dropdown/
Exception Value: Module "authbackend" does not define a "Bknd" attribute/class
Change History (10)
comment:1 by , 4 years ago
| Summary: | ImportError: partially initialized module '...' has no attribute '...' (most likely due to a circular import) → ImportError: partially initialized module '...' has no attribute '...' |
|---|
comment:2 by , 4 years ago
| Cc: | added |
|---|---|
| Triage Stage: | Unreviewed → Accepted |
comment:3 by , 4 years ago
| Component: | Uncategorized → Utilities |
|---|---|
| Severity: | Normal → Release blocker |
| Version: | dev → 4.0 |
Regression in ecf87ad513fd8af6e4a6093ed918723a7d88d5ca.
comment:4 by , 4 years ago
The easiest (but hacky) way to fix it is to check Python's internals and reload the module again:
diff --git a/django/utils/module_loading.py b/django/utils/module_loading.py
index 1df82b1c32..39cb784f72 100644
--- a/django/utils/module_loading.py
+++ b/django/utils/module_loading.py
@@ -7,7 +7,11 @@ from importlib.util import find_spec as importlib_find
def cached_import(module_path, class_name):
modules = sys.modules
- if module_path not in modules:
+ if module_path not in modules or (
+ # Module it not fully initialize.
+ getattr(modules[module_path], '__spec__', None) is not None and
+ getattr(modules[module_path].__spec__, '_initializing', False) is True
+ ):
import_module(module_path)
return getattr(modules[module_path], class_name)
comment:5 by , 4 years ago
importlib now uses the same hook, see https://github.com/python/cpython/commit/03648a2a91f9f1091cd21bd4cd6ca092ddb25640.
comment:6 by , 4 years ago
Serendipitous find :) For historical reference, it looks like _initializing is set via importlib._bootstrap._load_unlocked:
# This must be done before putting the module in sys.modules # (otherwise an optimization shortcut in import.c becomes # wrong). spec._initializing = True
Possibly worth noting however is importlib.util._module_to_load which has equivalent but different (at least in 3.9.5):
# This must be done before putting the module in sys.modules # (otherwise an optimization shortcut in import.c becomes wrong) module.__initializing__ = True
If we're going to try and keep the cached_import, perhaps we need to accommodate both, or at least investigate the __initializing__ variation?
FWIW, the investigation/patch proposed above does change the performance profile (it's 5.64 µs for me now) but is still world's better than repeatedly doing import_module (at least until Python 3.11 which might improve things via the linked bpo-43392)
comment:7 by , 4 years ago
| Has patch: | set |
|---|---|
| Owner: | changed from to |
| Status: | new → assigned |
Possibly worth noting however is
importlib.util._module_to_loadwhich has equivalent but different (at least in3.9.5):
As far as I'm aware we shouldn't reach _module_to_load() using import_module(). Also Python uses the same strategy (checking __spec__._initialized) in few places, e.g. import_ensure_initialized().
comment:8 by , 4 years ago
By the way, I figured out I can just put time.sleep(5) at the top-level of my custom auth backend to reproduce the issue the patch does fix the issue. Thanks!
Seems like a legitimate problem, that either requires working around (fastpath vs slowpath as mentioned) or reverting the change made in #33099.
It'd be interesting to see the middlewares & modules in play, even if they're reduced for privacy. My guess is circular imports are occurring possibly, or dynamic class creation?