Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 26 additions & 15 deletions django/utils/module_loading.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,45 @@
from importlib.util import find_spec as importlib_find


def cached_import(module_path, class_name):
def cached_import(module_path):
# Check whether module is loaded and fully initialized.
if not (
(module := sys.modules.get(module_path))
and (spec := getattr(module, "__spec__", None))
and getattr(spec, "_initializing", False) is False
):
module = import_module(module_path)
return getattr(module, class_name)
return module


def import_string(dotted_path):
"""
Import a dotted module path and return the attribute/class designated by
the last name in the path. Raise ImportError if the import failed.
Import a dotted module path and return the module or attribute/class
designated by the path. Raise ImportError if the import failed.
"""
try:
module_path, class_name = dotted_path.rsplit(".", 1)
except ValueError as err:
raise ImportError("%s doesn't look like a module path" % dotted_path) from err

try:
return cached_import(module_path, class_name)
except AttributeError as err:
raise ImportError(
'Module "%s" does not define a "%s" attribute/class'
% (module_path, class_name)
) from err
module = cached_import(dotted_path)
except ImportError as err:
# Attempt to load an attribute from a module
try:
module_path, class_name = dotted_path.rsplit(".", 1)
except ValueError:
# The supplied path contained no dots, so must be a module,
# and we've already failed to load.
raise err

module = cached_import(module_path)

try:
class_or_attr = getattr(module, class_name)
except AttributeError as err:
raise ImportError(
'Module "%s" does not define a "%s" attribute/class'
% (module_path, class_name)
) from err

return class_or_attr
return module


def autodiscover_modules(*args, **kwargs):
Expand Down
4 changes: 2 additions & 2 deletions docs/ref/utils.txt
Original file line number Diff line number Diff line change
Expand Up @@ -824,8 +824,8 @@ Functions for working with Python modules.

.. function:: import_string(dotted_path)

Imports a dotted module path and returns the attribute/class designated by
the last name in the path. Raises ``ImportError`` if the import failed. For
Imports a dotted module path and returns the module or attribute/class
designated by the path. Raises ``ImportError`` if the import failed. For
example::

from django.utils.module_loading import import_string
Expand Down
2 changes: 1 addition & 1 deletion tests/auth_tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ def test_nonexistent_backend(self):
)

def test_invalid_backend_submodule(self):
with self.assertRaises(ImportError):
with self.assertRaises(TypeError):
User.objects.with_perm(
"auth.test",
backend="json.tool",
Expand Down
9 changes: 3 additions & 6 deletions tests/auth_tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
password_validators_help_texts,
validate_password,
)
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.core.exceptions import ValidationError
from django.db import models
from django.test import SimpleTestCase, TestCase, override_settings
from django.test.utils import isolate_apps
Expand Down Expand Up @@ -53,11 +53,8 @@ def test_get_password_validators_custom(self):

def test_get_password_validators_custom_invalid(self):
validator_config = [{"NAME": "json.tool"}]
msg = (
"The module in NAME could not be imported: json.tool. "
"Check your AUTH_PASSWORD_VALIDATORS setting."
)
with self.assertRaisesMessage(ImproperlyConfigured, msg):
msg = "'module' object is not callable"
with self.assertRaisesMessage(TypeError, msg):
get_password_validators(validator_config)

def test_validate_password(self):
Expand Down
16 changes: 14 additions & 2 deletions tests/utils_tests/test_module_loading.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,24 @@ def test_import_string(self):
self.assertEqual(cls, import_string)

# Test exceptions raised
with self.assertRaises(ImportError):
import_string("no_dots_in_path")
msg = 'Module "utils_tests" does not define a "unexistent" attribute'
with self.assertRaisesMessage(ImportError, msg):
import_string("utils_tests.unexistent")

def test_import_string_unloaded_modules(self):
"""Regression test for issue #36864"""
module_dotpath = "utils_tests"
submodule_dotpath = "utils_tests.test_module.good_module"

import_string(module_dotpath)
import_string(submodule_dotpath)

with self.assertRaisesMessage(ValueError, "Empty module name"):
import_string("")

self.addCleanup(sys.modules.pop, submodule_dotpath, None)
self.addCleanup(sys.modules.pop, module_dotpath, None)


@modify_settings(INSTALLED_APPS={"append": "utils_tests.test_module"})
class AutodiscoverModulesTestCase(SimpleTestCase):
Expand Down