From 5d263dee304fdaf95e18d2f0619d6925984a7f02 Mon Sep 17 00:00:00 2001
From: Berker Peksag <berker.peksag@gmail.com>
Date: Mon, 20 Jan 2014 22:15:14 +0200
Subject: [PATCH] Fixed #21674 -- Deprecated the import_by_path() function in
 favor of import_string().

Thanks Aymeric Augustin for the suggestion and review.
---
 AUTHORS                                       |  1 +
 django/contrib/admin/tests.py                 |  4 +-
 django/contrib/auth/__init__.py               |  4 +-
 django/contrib/auth/hashers.py                |  4 +-
 django/contrib/auth/middleware.py             |  2 +-
 .../formtools/wizard/storage/__init__.py      |  7 ++--
 django/contrib/messages/storage/__init__.py   |  6 +--
 django/contrib/sessions/backends/base.py      |  4 +-
 django/contrib/staticfiles/finders.py         |  4 +-
 django/core/cache/__init__.py                 | 10 ++---
 django/core/cache/backends/base.py            |  4 +-
 django/core/files/storage.py                  |  4 +-
 django/core/files/uploadhandler.py            |  4 +-
 django/core/handlers/base.py                  |  4 +-
 django/core/mail/__init__.py                  |  4 +-
 django/core/servers/basehttp.py               | 20 +++++++---
 django/core/signing.py                        |  4 +-
 django/db/migrations/state.py                 |  8 ++--
 django/db/utils.py                            |  4 +-
 django/template/context.py                    |  4 +-
 django/template/loader.py                     |  4 +-
 django/utils/log.py                           |  4 +-
 django/utils/module_loading.py                | 39 +++++++++++++------
 django/views/debug.py                         |  4 +-
 docs/internals/deprecation.txt                |  3 ++
 docs/ref/utils.txt                            | 23 +++++++----
 docs/releases/1.7.txt                         | 11 ++++++
 tests/file_storage/tests.py                   | 18 +++------
 tests/staticfiles_tests/tests.py              |  4 +-
 tests/utils_tests/test_module_loading.py      | 32 +++++++++++----
 tests/wsgi/tests.py                           |  2 +-
 31 files changed, 155 insertions(+), 95 deletions(-)

diff --git a/AUTHORS b/AUTHORS
index f3f300889a..fe1ede2f54 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -484,6 +484,7 @@ answer newbie questions, and generally made Django that much better:
     John Paulett <john@paulett.org>
     pavithran s <pavithran.s@gmail.com>
     Barry Pederson <bp@barryp.org>
+    Berker Peksag <berker.peksag@gmail.com>
     Andreas Pelme <andreas@pelme.se>
     permonik@mesias.brnonet.cz
     peter@mymart.com
diff --git a/django/contrib/admin/tests.py b/django/contrib/admin/tests.py
index 40fd10089b..56b8b66023 100644
--- a/django/contrib/admin/tests.py
+++ b/django/contrib/admin/tests.py
@@ -2,7 +2,7 @@ import os
 from unittest import SkipTest
 
 from django.contrib.staticfiles.testing import StaticLiveServerCase
-from django.utils.module_loading import import_by_path
+from django.utils.module_loading import import_string
 from django.utils.translation import ugettext as _
 
 
@@ -22,7 +22,7 @@ class AdminSeleniumWebDriverTestCase(StaticLiveServerCase):
         if not os.environ.get('DJANGO_SELENIUM_TESTS', False):
             raise SkipTest('Selenium tests not requested')
         try:
-            cls.selenium = import_by_path(cls.webdriver_class)()
+            cls.selenium = import_string(cls.webdriver_class)()
         except Exception as e:
             raise SkipTest('Selenium webdriver "%s" not installed or not '
                            'operational: %s' % (cls.webdriver_class, str(e)))
diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py
index 9fd46814b2..ed01da94cf 100644
--- a/django/contrib/auth/__init__.py
+++ b/django/contrib/auth/__init__.py
@@ -4,7 +4,7 @@ import re
 from django.apps import apps as django_apps
 from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured, PermissionDenied
-from django.utils.module_loading import import_by_path
+from django.utils.module_loading import import_string
 from django.middleware.csrf import rotate_token
 
 from .signals import user_logged_in, user_logged_out, user_login_failed
@@ -15,7 +15,7 @@ REDIRECT_FIELD_NAME = 'next'
 
 
 def load_backend(path):
-    return import_by_path(path)()
+    return import_string(path)()
 
 
 def get_backends():
diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py
index 713240b502..992e19e2a4 100644
--- a/django/contrib/auth/hashers.py
+++ b/django/contrib/auth/hashers.py
@@ -13,7 +13,7 @@ from django.utils.encoding import force_bytes, force_str, force_text
 from django.core.exceptions import ImproperlyConfigured
 from django.utils.crypto import (
     pbkdf2, constant_time_compare, get_random_string)
-from django.utils.module_loading import import_by_path
+from django.utils.module_loading import import_string
 from django.utils.translation import ugettext_noop as _
 
 
@@ -92,7 +92,7 @@ def load_hashers(password_hashers=None):
     if not password_hashers:
         password_hashers = settings.PASSWORD_HASHERS
     for backend in password_hashers:
-        hasher = import_by_path(backend)()
+        hasher = import_string(backend)()
         if not getattr(hasher, 'algorithm'):
             raise ImproperlyConfigured("hasher doesn't specify an "
                                        "algorithm name: %s" % backend)
diff --git a/django/contrib/auth/middleware.py b/django/contrib/auth/middleware.py
index a107055e34..3ee742446d 100644
--- a/django/contrib/auth/middleware.py
+++ b/django/contrib/auth/middleware.py
@@ -58,7 +58,7 @@ class RemoteUserMiddleware(object):
                         auth.BACKEND_SESSION_KEY, ''))
                     if isinstance(stored_backend, RemoteUserBackend):
                         auth.logout(request)
-                except ImproperlyConfigured:
+                except ImportError:
                     # backend failed to load
                     auth.logout(request)
             return
diff --git a/django/contrib/formtools/wizard/storage/__init__.py b/django/contrib/formtools/wizard/storage/__init__.py
index 4dbc15a298..71a21f89a3 100644
--- a/django/contrib/formtools/wizard/storage/__init__.py
+++ b/django/contrib/formtools/wizard/storage/__init__.py
@@ -1,5 +1,4 @@
-from django.core.exceptions import ImproperlyConfigured
-from django.utils.module_loading import import_by_path
+from django.utils.module_loading import import_string
 
 from django.contrib.formtools.wizard.storage.base import BaseStorage
 from django.contrib.formtools.wizard.storage.exceptions import (
@@ -12,7 +11,7 @@ __all__ = [
 
 def get_storage(path, *args, **kwargs):
     try:
-        storage_class = import_by_path(path)
-    except ImproperlyConfigured as e:
+        storage_class = import_string(path)
+    except ImportError as e:
         raise MissingStorage('Error loading storage: %s' % e)
     return storage_class(*args, **kwargs)
diff --git a/django/contrib/messages/storage/__init__.py b/django/contrib/messages/storage/__init__.py
index 591ae14d7c..373dbbaada 100644
--- a/django/contrib/messages/storage/__init__.py
+++ b/django/contrib/messages/storage/__init__.py
@@ -1,12 +1,12 @@
 from django.conf import settings
-from django.utils.module_loading import import_by_path
+from django.utils.module_loading import import_string
 
 
 def default_storage(request):
     """
     Callable with the same interface as the storage classes.
 
-    This isn't just default_storage = import_by_path(settings.MESSAGE_STORAGE)
+    This isn't just default_storage = import_string(settings.MESSAGE_STORAGE)
     to avoid accessing the settings at the module level.
     """
-    return import_by_path(settings.MESSAGE_STORAGE)(request)
+    return import_string(settings.MESSAGE_STORAGE)(request)
diff --git a/django/contrib/sessions/backends/base.py b/django/contrib/sessions/backends/base.py
index 2a497fe9a1..a77a25bb31 100644
--- a/django/contrib/sessions/backends/base.py
+++ b/django/contrib/sessions/backends/base.py
@@ -12,7 +12,7 @@ from django.utils.crypto import get_random_string
 from django.utils.crypto import salted_hmac
 from django.utils import timezone
 from django.utils.encoding import force_bytes, force_text
-from django.utils.module_loading import import_by_path
+from django.utils.module_loading import import_string
 
 from django.contrib.sessions.exceptions import SuspiciousSession
 
@@ -40,7 +40,7 @@ class SessionBase(object):
         self._session_key = session_key
         self.accessed = False
         self.modified = False
-        self.serializer = import_by_path(settings.SESSION_SERIALIZER)
+        self.serializer = import_string(settings.SESSION_SERIALIZER)
 
     def __contains__(self, key):
         return key in self._session
diff --git a/django/contrib/staticfiles/finders.py b/django/contrib/staticfiles/finders.py
index 30767405d3..86b620696f 100644
--- a/django/contrib/staticfiles/finders.py
+++ b/django/contrib/staticfiles/finders.py
@@ -6,7 +6,7 @@ from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured
 from django.core.files.storage import default_storage, Storage, FileSystemStorage
 from django.utils.functional import empty, LazyObject
-from django.utils.module_loading import import_by_path
+from django.utils.module_loading import import_string
 from django.utils._os import safe_join
 from django.utils import six, lru_cache
 
@@ -257,7 +257,7 @@ def get_finder(import_path):
     Imports the staticfiles finder class described by import_path, where
     import_path is the full Python path to the class.
     """
-    Finder = import_by_path(import_path)
+    Finder = import_string(import_path)
     if not issubclass(Finder, BaseFinder):
         raise ImproperlyConfigured('Finder "%s" is not a subclass of "%s"' %
                                    (Finder, BaseFinder))
diff --git a/django/core/cache/__init__.py b/django/core/cache/__init__.py
index c362800dcc..603086a39c 100644
--- a/django/core/cache/__init__.py
+++ b/django/core/cache/__init__.py
@@ -20,7 +20,7 @@ from django.core import signals
 from django.core.cache.backends.base import (
     InvalidCacheBackendError, CacheKeyWarning, BaseCache)
 from django.core.exceptions import ImproperlyConfigured
-from django.utils.module_loading import import_by_path
+from django.utils.module_loading import import_string
 
 
 __all__ = [
@@ -69,8 +69,8 @@ def _create_cache(backend, **kwargs):
         except KeyError:
             try:
                 # Trying to import the given backend, in case it's a dotted path
-                import_by_path(backend)
-            except ImproperlyConfigured as e:
+                import_string(backend)
+            except ImportError as e:
                 raise InvalidCacheBackendError("Could not find backend '%s': %s" % (
                     backend, e))
             location = kwargs.pop('LOCATION', '')
@@ -80,8 +80,8 @@ def _create_cache(backend, **kwargs):
             params.update(kwargs)
             backend = params.pop('BACKEND')
             location = params.pop('LOCATION', '')
-        backend_cls = import_by_path(backend)
-    except (AttributeError, ImportError, ImproperlyConfigured) as e:
+        backend_cls = import_string(backend)
+    except ImportError as e:
         raise InvalidCacheBackendError(
             "Could not find backend '%s': %s" % (backend, e))
     return backend_cls(location, params)
diff --git a/django/core/cache/backends/base.py b/django/core/cache/backends/base.py
index 2c3151369a..ae83865db8 100644
--- a/django/core/cache/backends/base.py
+++ b/django/core/cache/backends/base.py
@@ -5,7 +5,7 @@ import time
 import warnings
 
 from django.core.exceptions import ImproperlyConfigured, DjangoRuntimeWarning
-from django.utils.module_loading import import_by_path
+from django.utils.module_loading import import_string
 
 
 class InvalidCacheBackendError(ImproperlyConfigured):
@@ -45,7 +45,7 @@ def get_key_func(key_func):
         if callable(key_func):
             return key_func
         else:
-            return import_by_path(key_func)
+            return import_string(key_func)
     return default_key_func
 
 
diff --git a/django/core/files/storage.py b/django/core/files/storage.py
index c8b4c7185d..ade1817dad 100644
--- a/django/core/files/storage.py
+++ b/django/core/files/storage.py
@@ -9,7 +9,7 @@ from django.core.files import locks, File
 from django.core.files.move import file_move_safe
 from django.utils.encoding import force_text, filepath_to_uri
 from django.utils.functional import LazyObject
-from django.utils.module_loading import import_by_path
+from django.utils.module_loading import import_string
 from django.utils.six.moves.urllib.parse import urljoin
 from django.utils.text import get_valid_filename
 from django.utils._os import safe_join, abspathu
@@ -301,7 +301,7 @@ class FileSystemStorage(Storage):
 
 
 def get_storage_class(import_path=None):
-    return import_by_path(import_path or settings.DEFAULT_FILE_STORAGE)
+    return import_string(import_path or settings.DEFAULT_FILE_STORAGE)
 
 
 class DefaultStorage(LazyObject):
diff --git a/django/core/files/uploadhandler.py b/django/core/files/uploadhandler.py
index 995e97a2f6..2fa1d7a934 100644
--- a/django/core/files/uploadhandler.py
+++ b/django/core/files/uploadhandler.py
@@ -9,7 +9,7 @@ from io import BytesIO
 from django.conf import settings
 from django.core.files.uploadedfile import TemporaryUploadedFile, InMemoryUploadedFile
 from django.utils.encoding import python_2_unicode_compatible
-from django.utils.module_loading import import_by_path
+from django.utils.module_loading import import_string
 
 __all__ = [
     'UploadFileException', 'StopUpload', 'SkipFile', 'FileUploadHandler',
@@ -214,4 +214,4 @@ def load_handler(path, *args, **kwargs):
         <TemporaryFileUploadHandler object at 0x...>
 
     """
-    return import_by_path(path)(*args, **kwargs)
+    return import_string(path)(*args, **kwargs)
diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py
index 587cbae2c3..3986257660 100644
--- a/django/core/handlers/base.py
+++ b/django/core/handlers/base.py
@@ -11,7 +11,7 @@ from django.core import signals
 from django.core.exceptions import MiddlewareNotUsed, PermissionDenied, SuspiciousOperation
 from django.db import connections, transaction
 from django.utils.encoding import force_text
-from django.utils.module_loading import import_by_path
+from django.utils.module_loading import import_string
 from django.utils import six
 from django.views import debug
 
@@ -43,7 +43,7 @@ class BaseHandler(object):
 
         request_middleware = []
         for middleware_path in settings.MIDDLEWARE_CLASSES:
-            mw_class = import_by_path(middleware_path)
+            mw_class = import_string(middleware_path)
             try:
                 mw_instance = mw_class()
             except MiddlewareNotUsed:
diff --git a/django/core/mail/__init__.py b/django/core/mail/__init__.py
index 5a01adc4ea..cef9d1b87b 100644
--- a/django/core/mail/__init__.py
+++ b/django/core/mail/__init__.py
@@ -4,7 +4,7 @@ Tools for sending email.
 from __future__ import unicode_literals
 
 from django.conf import settings
-from django.utils.module_loading import import_by_path
+from django.utils.module_loading import import_string
 
 # Imported for backwards compatibility, and for the sake
 # of a cleaner namespace. These symbols used to be in
@@ -34,7 +34,7 @@ def get_connection(backend=None, fail_silently=False, **kwds):
     Both fail_silently and other keyword arguments are used in the
     constructor of the backend.
     """
-    klass = import_by_path(backend or settings.EMAIL_BACKEND)
+    klass = import_string(backend or settings.EMAIL_BACKEND)
     return klass(fail_silently=fail_silently, **kwds)
 
 
diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py
index d34c246d51..090a694c3e 100644
--- a/django/core/servers/basehttp.py
+++ b/django/core/servers/basehttp.py
@@ -16,9 +16,11 @@ import traceback
 from wsgiref import simple_server
 from wsgiref.util import FileWrapper   # NOQA: for backwards compatibility
 
+from django.core.exceptions import ImproperlyConfigured
 from django.core.management.color import color_style
 from django.core.wsgi import get_wsgi_application
-from django.utils.module_loading import import_by_path
+from django.utils import six
+from django.utils.module_loading import import_string
 from django.utils.six.moves import socketserver
 
 __all__ = ('WSGIServer', 'WSGIRequestHandler', 'MAX_SOCKET_CHUNK_SIZE')
@@ -50,10 +52,18 @@ def get_internal_wsgi_application():
     if app_path is None:
         return get_wsgi_application()
 
-    return import_by_path(
-        app_path,
-        error_prefix="WSGI application '%s' could not be loaded; " % app_path
-    )
+    try:
+        return import_string(app_path)
+    except ImportError as e:
+        msg = (
+            "WSGI application '%(app_path)s' could not be loaded; "
+            "Error importing module: '%(exception)s'" % ({
+                'app_path': app_path,
+                'exception': e,
+            })
+        )
+        six.reraise(ImproperlyConfigured, ImproperlyConfigured(msg),
+                    sys.exc_info()[2])
 
 
 class ServerHandler(simple_server.ServerHandler, object):
diff --git a/django/core/signing.py b/django/core/signing.py
index 15b9eaec6e..17953af151 100644
--- a/django/core/signing.py
+++ b/django/core/signing.py
@@ -44,7 +44,7 @@ from django.conf import settings
 from django.utils import baseconv
 from django.utils.crypto import constant_time_compare, salted_hmac
 from django.utils.encoding import force_bytes, force_str, force_text
-from django.utils.module_loading import import_by_path
+from django.utils.module_loading import import_string
 
 
 class BadSignature(Exception):
@@ -75,7 +75,7 @@ def base64_hmac(salt, value, key):
 
 
 def get_cookie_signer(salt='django.core.signing.get_cookie_signer'):
-    Signer = import_by_path(settings.SIGNING_BACKEND)
+    Signer = import_string(settings.SIGNING_BACKEND)
     return Signer('django.http.cookies' + settings.SECRET_KEY, salt=salt)
 
 
diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py
index 949d2251e5..3e73744091 100644
--- a/django/db/migrations/state.py
+++ b/django/db/migrations/state.py
@@ -3,7 +3,7 @@ from django.apps.registry import Apps
 from django.db import models
 from django.db.models.options import DEFAULT_NAMES, normalize_unique_together
 from django.utils import six
-from django.utils.module_loading import import_by_path
+from django.utils.module_loading import import_string
 
 
 class InvalidBasesError(ValueError):
@@ -115,7 +115,7 @@ class ModelState(object):
         fields = []
         for field in model._meta.local_fields:
             name, path, args, kwargs = field.deconstruct()
-            field_class = import_by_path(path)
+            field_class = import_string(path)
             try:
                 fields.append((name, field_class(*args, **kwargs)))
             except TypeError as e:
@@ -127,7 +127,7 @@ class ModelState(object):
                 ))
         for field in model._meta.local_many_to_many:
             name, path, args, kwargs = field.deconstruct()
-            field_class = import_by_path(path)
+            field_class = import_string(path)
             try:
                 fields.append((name, field_class(*args, **kwargs)))
             except TypeError as e:
@@ -175,7 +175,7 @@ class ModelState(object):
         fields = []
         for name, field in self.fields:
             _, path, args, kwargs = field.deconstruct()
-            field_class = import_by_path(path)
+            field_class = import_string(path)
             fields.append((name, field_class(*args, **kwargs)))
         # Now make a copy
         return self.__class__(
diff --git a/django/db/utils.py b/django/db/utils.py
index 0bcf33f252..51d2ffcba0 100644
--- a/django/db/utils.py
+++ b/django/db/utils.py
@@ -7,7 +7,7 @@ import warnings
 from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured
 from django.utils.functional import cached_property
-from django.utils.module_loading import import_by_path
+from django.utils.module_loading import import_string
 from django.utils._os import upath
 from django.utils import six
 
@@ -221,7 +221,7 @@ class ConnectionRouter(object):
         routers = []
         for r in self._routers:
             if isinstance(r, six.string_types):
-                router = import_by_path(r)()
+                router = import_string(r)()
             else:
                 router = r
             routers.append(router)
diff --git a/django/template/context.py b/django/template/context.py
index 774f7af6b1..0f624ff8a6 100644
--- a/django/template/context.py
+++ b/django/template/context.py
@@ -1,5 +1,5 @@
 from copy import copy
-from django.utils.module_loading import import_by_path
+from django.utils.module_loading import import_string
 
 # Cache of actual callables.
 _standard_context_processors = None
@@ -162,7 +162,7 @@ def get_standard_processors():
         collect.extend(_builtin_context_processors)
         collect.extend(settings.TEMPLATE_CONTEXT_PROCESSORS)
         for path in collect:
-            func = import_by_path(path)
+            func = import_string(path)
             processors.append(func)
         _standard_context_processors = tuple(processors)
     return _standard_context_processors
diff --git a/django/template/loader.py b/django/template/loader.py
index 1e18eecc02..e9e2172e4b 100644
--- a/django/template/loader.py
+++ b/django/template/loader.py
@@ -28,7 +28,7 @@
 from django.core.exceptions import ImproperlyConfigured
 from django.template.base import Origin, Template, Context, TemplateDoesNotExist
 from django.conf import settings
-from django.utils.module_loading import import_by_path
+from django.utils.module_loading import import_string
 from django.utils import six
 
 template_source_loaders = None
@@ -95,7 +95,7 @@ def find_template_loader(loader):
     else:
         args = []
     if isinstance(loader, six.string_types):
-        TemplateLoader = import_by_path(loader)
+        TemplateLoader = import_string(loader)
 
         if hasattr(TemplateLoader, 'load_template_source'):
             func = TemplateLoader(*args)
diff --git a/django/utils/log.py b/django/utils/log.py
index cf778b619b..f319704116 100644
--- a/django/utils/log.py
+++ b/django/utils/log.py
@@ -5,7 +5,7 @@ import warnings
 from django.conf import settings
 from django.core import mail
 from django.core.mail import get_connection
-from django.utils.module_loading import import_by_path
+from django.utils.module_loading import import_string
 from django.views.debug import ExceptionReporter, get_exception_reporter_filter
 
 # Imports kept for backwards-compatibility in Django 1.7.
@@ -73,7 +73,7 @@ def configure_logging(logging_config, logging_settings):
 
     if logging_config:
          # First find the logging configuration function ...
-        logging_config_func = import_by_path(logging_config)
+        logging_config_func = import_string(logging_config)
 
         logging_config_func(DEFAULT_LOGGING)
 
diff --git a/django/utils/module_loading.py b/django/utils/module_loading.py
index a6fe7aebcb..7ba0381307 100644
--- a/django/utils/module_loading.py
+++ b/django/utils/module_loading.py
@@ -5,33 +5,48 @@ import imp
 from importlib import import_module
 import os
 import sys
+import warnings
 
 from django.core.exceptions import ImproperlyConfigured
 from django.utils import six
 
 
+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.
+    """
+    try:
+        module_path, class_name = dotted_path.rsplit('.', 1)
+    except ValueError:
+        msg = "%s doesn't look like a module path" % dotted_path
+        six.reraise(ImportError, ImportError(msg), sys.exc_info()[2])
+
+    module = import_module(module_path)
+
+    try:
+        return getattr(module, class_name)
+    except AttributeError:
+        msg = 'Module "%s" does not define a "%s" attribute/class' % (
+            dotted_path, class_name)
+        six.reraise(ImportError, ImportError(msg), sys.exc_info()[2])
+
+
 def import_by_path(dotted_path, error_prefix=''):
     """
     Import a dotted module path and return the attribute/class designated by the
     last name in the path. Raise ImproperlyConfigured if something goes wrong.
     """
+    warnings.warn(
+        'import_by_path() has been deprecated. Use import_string() instead.',
+        PendingDeprecationWarning, stacklevel=2)
     try:
-        module_path, class_name = dotted_path.rsplit('.', 1)
-    except ValueError:
-        raise ImproperlyConfigured("%s%s doesn't look like a module path" % (
-            error_prefix, dotted_path))
-    try:
-        module = import_module(module_path)
+        attr = import_string(dotted_path)
     except ImportError as e:
         msg = '%sError importing module %s: "%s"' % (
-            error_prefix, module_path, e)
+            error_prefix, dotted_path, e)
         six.reraise(ImproperlyConfigured, ImproperlyConfigured(msg),
                     sys.exc_info()[2])
-    try:
-        attr = getattr(module, class_name)
-    except AttributeError:
-        raise ImproperlyConfigured('%sModule "%s" does not define a "%s" attribute/class' % (
-            error_prefix, module_path, class_name))
     return attr
 
 
diff --git a/django/views/debug.py b/django/views/debug.py
index 4f2097600f..d665c4e36b 100644
--- a/django/views/debug.py
+++ b/django/views/debug.py
@@ -14,7 +14,7 @@ from django.template.defaultfilters import force_escape, pprint
 from django.utils.datastructures import MultiValueDict
 from django.utils.html import escape
 from django.utils.encoding import force_bytes, smart_text
-from django.utils.module_loading import import_by_path
+from django.utils.module_loading import import_string
 from django.utils import six
 
 HIDDEN_SETTINGS = re.compile('API|TOKEN|KEY|SECRET|PASS|PROFANITIES_LIST|SIGNATURE')
@@ -85,7 +85,7 @@ def get_exception_reporter_filter(request):
     global default_exception_reporter_filter
     if default_exception_reporter_filter is None:
         # Load the default filter for the first time and cache it.
-        default_exception_reporter_filter = import_by_path(
+        default_exception_reporter_filter = import_string(
             settings.DEFAULT_EXCEPTION_REPORTER_FILTER)()
     if request:
         return getattr(request, 'exception_reporter_filter', default_exception_reporter_filter)
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index 7b5a5d7468..4b79e4696d 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -116,6 +116,9 @@ details on these changes.
 * ``django.db.backends.DatabaseValidation.validate_field`` will be removed in
   favor of the ``check_field`` method.
 
+* ``django.utils.module_loading.import_by_path`` will be removed in favor of
+  ``django.utils.module_loading.import_string``.
+
 .. _deprecation-removed-in-1.8:
 
 1.8
diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt
index 6cac9fa058..5313a1de19 100644
--- a/docs/ref/utils.txt
+++ b/docs/ref/utils.txt
@@ -709,22 +709,31 @@ escaping HTML.
 
 Functions for working with Python modules.
 
-.. function:: import_by_path(dotted_path, error_prefix='')
+.. function:: import_string(dotted_path)
 
-    .. versionadded:: 1.6
+    .. versionadded:: 1.7
 
     Imports a dotted module path and returns the attribute/class designated by
-    the last name in the path. Raises
-    :exc:`~django.core.exceptions.ImproperlyConfigured` if something goes
-    wrong. For example::
+    the last name in the path. Raises ``ImportError`` if the import failed. For
+    example::
 
-        from django.utils.module_loading import import_by_path
-        ImproperlyConfigured = import_by_path('django.core.exceptions.ImproperlyConfigured')
+        from django.utils.module_loading import import_string
+        ImproperlyConfigured = import_string('django.core.exceptions.ImproperlyConfigured')
 
     is equivalent to::
 
         from django.core.exceptions import ImproperlyConfigured
 
+.. function:: import_by_path(dotted_path, error_prefix='')
+
+    .. versionadded:: 1.6
+    .. deprecated:: 1.7
+       Use :meth:`~django.utils.module_loading.import_string` instead.
+
+    Imports a dotted module path and returns the attribute/class designated by
+    the last name in the path. Raises :exc:`~django.core.exceptions.ImproperlyConfigured`
+    if something goes wrong.
+
 ``django.utils.safestring``
 ===========================
 
diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt
index f9d3a7e795..1fe2b25ecf 100644
--- a/docs/releases/1.7.txt
+++ b/docs/releases/1.7.txt
@@ -1042,6 +1042,17 @@ Features deprecated in 1.7
 respectively :mod:`logging.config` and :mod:`importlib` provided for Python
 versions prior to 2.7. They have been deprecated.
 
+``django.utils.module_loading.import_by_path``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The current :meth:`~django.utils.module_loading.import_by_path` function
+catches ``AttributeError``, ``ImportError`` and ``ValueError`` exceptions,
+and re-raises :exc:`~django.core.exceptions.ImproperlyConfigured`. Such
+exception masking makes it needlessly hard to diagnose circular import
+problems, because it makes it look like the problem comes from inside Django.
+It has been deprecated in favor of
+:meth:`~django.utils.module_loading.import_string`.
+
 ``django.utils.tzinfo``
 ~~~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/tests/file_storage/tests.py b/tests/file_storage/tests.py
index 2f047f049a..ae2076c5e4 100644
--- a/tests/file_storage/tests.py
+++ b/tests/file_storage/tests.py
@@ -16,7 +16,7 @@ except ImportError:
     import dummy_threading as threading
 
 from django.core.cache import cache
-from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured
+from django.core.exceptions import SuspiciousOperation
 from django.core.files.base import File, ContentFile
 from django.core.files.storage import FileSystemStorage, get_storage_class
 from django.core.files.uploadedfile import SimpleUploadedFile
@@ -43,29 +43,23 @@ class GetStorageClassTests(SimpleTestCase):
         """
         get_storage_class raises an error if the requested import don't exist.
         """
-        with six.assertRaisesRegex(self, ImproperlyConfigured,
-                "Error importing module storage: \"No module named '?storage'?\""):
+        with six.assertRaisesRegex(self, ImportError, "No module named '?storage'?"):
             get_storage_class('storage.NonExistingStorage')
 
     def test_get_nonexisting_storage_class(self):
         """
         get_storage_class raises an error if the requested class don't exist.
         """
-        self.assertRaisesMessage(
-            ImproperlyConfigured,
-            'Module "django.core.files.storage" does not define a '
-            '"NonExistingStorage" attribute/class',
-            get_storage_class,
-            'django.core.files.storage.NonExistingStorage')
+        self.assertRaises(ImportError, get_storage_class,
+                          'django.core.files.storage.NonExistingStorage')
 
     def test_get_nonexisting_storage_module(self):
         """
         get_storage_class raises an error if the requested module don't exist.
         """
         # Error message may or may not be the fully qualified path.
-        with six.assertRaisesRegex(self, ImproperlyConfigured,
-                "Error importing module django.core.files.non_existing_storage: "
-                "\"No module named '?(django.core.files.)?non_existing_storage'?\""):
+        with six.assertRaisesRegex(self, ImportError,
+                "No module named '?(django.core.files.)?non_existing_storage'?"):
             get_storage_class(
                 'django.core.files.non_existing_storage.NonExistingStorage')
 
diff --git a/tests/staticfiles_tests/tests.py b/tests/staticfiles_tests/tests.py
index 74e3782c83..1d3daff11e 100644
--- a/tests/staticfiles_tests/tests.py
+++ b/tests/staticfiles_tests/tests.py
@@ -788,11 +788,11 @@ class TestMiscFinder(TestCase):
             finders.FileSystemFinder)
 
     def test_get_finder_bad_classname(self):
-        self.assertRaises(ImproperlyConfigured, finders.get_finder,
+        self.assertRaises(ImportError, finders.get_finder,
                           'django.contrib.staticfiles.finders.FooBarFinder')
 
     def test_get_finder_bad_module(self):
-        self.assertRaises(ImproperlyConfigured,
+        self.assertRaises(ImportError,
             finders.get_finder, 'foo.bar.FooBarFinder')
 
     def test_cache(self):
diff --git a/tests/utils_tests/test_module_loading.py b/tests/utils_tests/test_module_loading.py
index 905163b1b9..2eb01ad770 100644
--- a/tests/utils_tests/test_module_loading.py
+++ b/tests/utils_tests/test_module_loading.py
@@ -3,13 +3,15 @@ from importlib import import_module
 import os
 import sys
 import unittest
+import warnings
 from zipimport import zipimporter
 
 from django.core.exceptions import ImproperlyConfigured
 from django.test import SimpleTestCase, modify_settings
-from django.test.utils import extend_sys_path
+from django.test.utils import IgnorePendingDeprecationWarningsMixin, extend_sys_path
 from django.utils import six
-from django.utils.module_loading import autodiscover_modules, import_by_path, module_has_submodule
+from django.utils.module_loading import (autodiscover_modules, import_by_path, import_string,
+                                         module_has_submodule)
 from django.utils._os import upath
 
 
@@ -107,15 +109,13 @@ class EggLoader(unittest.TestCase):
             self.assertRaises(ImportError, import_module, 'egg_module.sub1.sub2.no_such_module')
 
 
-class ModuleImportTestCase(unittest.TestCase):
+class ModuleImportTestCase(IgnorePendingDeprecationWarningsMixin, unittest.TestCase):
     def test_import_by_path(self):
-        cls = import_by_path(
-            'django.utils.module_loading.import_by_path')
+        cls = import_by_path('django.utils.module_loading.import_by_path')
         self.assertEqual(cls, import_by_path)
 
         # Test exceptions raised
-        for path in ('no_dots_in_path', 'unexistent.path',
-                'utils_tests.unexistent'):
+        for path in ('no_dots_in_path', 'unexistent.path', 'utils_tests.unexistent'):
             self.assertRaises(ImproperlyConfigured, import_by_path, path)
 
         with self.assertRaises(ImproperlyConfigured) as cm:
@@ -132,6 +132,24 @@ class ModuleImportTestCase(unittest.TestCase):
         self.assertIsNotNone(traceback.tb_next.tb_next,
             'Should have more than the calling frame in the traceback.')
 
+    def test_import_by_path_pending_deprecation_warning(self):
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter('always', category=PendingDeprecationWarning)
+            cls = import_by_path('django.utils.module_loading.import_by_path')
+            self.assertEqual(cls, import_by_path)
+            self.assertEqual(len(w), 1)
+            self.assertTrue(issubclass(w[-1].category, PendingDeprecationWarning))
+            self.assertIn('deprecated', str(w[-1].message))
+
+    def test_import_string(self):
+        cls = import_string('django.utils.module_loading.import_string')
+        self.assertEqual(cls, import_string)
+
+        # Test exceptions raised
+        self.assertRaises(ImportError, import_string, 'no_dots_in_path')
+        self.assertRaises(ImportError, import_string, 'utils_tests.unexistent')
+        self.assertRaises(ImportError, import_string, 'unexistent.path')
+
 
 @modify_settings(INSTALLED_APPS={'append': 'utils_tests.test_module'})
 class AutodiscoverModulesTestCase(SimpleTestCase):
diff --git a/tests/wsgi/tests.py b/tests/wsgi/tests.py
index 567ca94f98..299beacf46 100644
--- a/tests/wsgi/tests.py
+++ b/tests/wsgi/tests.py
@@ -101,6 +101,6 @@ class GetInternalWSGIApplicationTest(unittest.TestCase):
     def test_bad_name(self):
         with six.assertRaisesRegex(self,
                 ImproperlyConfigured,
-                r"^WSGI application 'wsgi.wsgi.noexist' could not be loaded; Module.*"):
+                r"^WSGI application 'wsgi.wsgi.noexist' could not be loaded; Error importing.*"):
 
             get_internal_wsgi_application()