You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

loading.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. import sys
  2. import traceback
  3. from importlib import import_module
  4. import django
  5. from django.conf import settings
  6. from django.utils import six
  7. from oscar.core.exceptions import (ModuleNotFoundError, ClassNotFoundError,
  8. AppNotFoundError)
  9. def import_string(dotted_path):
  10. """
  11. Import a dotted module path and return the attribute/class designated by
  12. the last name in the path. Raise ImportError if the import failed.
  13. This is backported from unreleased Django 1.7 at
  14. 47927eb786f432cb069f0b00fd810c465a78fd71. Can be removed once we don't
  15. support Django versions below 1.7.
  16. """
  17. try:
  18. module_path, class_name = dotted_path.rsplit('.', 1)
  19. except ValueError:
  20. msg = "%s doesn't look like a module path" % dotted_path
  21. six.reraise(ImportError, ImportError(msg), sys.exc_info()[2])
  22. module = import_module(module_path)
  23. try:
  24. return getattr(module, class_name)
  25. except AttributeError:
  26. msg = 'Module "%s" does not define a "%s" attribute/class' % (
  27. dotted_path, class_name)
  28. six.reraise(ImportError, ImportError(msg), sys.exc_info()[2])
  29. try:
  30. module_path, class_name = dotted_path.rsplit('.', 1)
  31. except ValueError:
  32. msg = "%s doesn't look like a module path" % dotted_path
  33. six.reraise(ImportError, ImportError(msg), sys.exc_info()[2])
  34. module = __import__(module_path, fromlist=[class_name])
  35. try:
  36. return getattr(module, class_name)
  37. except AttributeError:
  38. msg = 'Module "%s" does not define a "%s" attribute/class' % (
  39. dotted_path, class_name)
  40. six.reraise(ImportError, ImportError(msg), sys.exc_info()[2])
  41. def get_class(module_label, classname):
  42. """
  43. Dynamically import a single class from the given module.
  44. This is a simple wrapper around `get_classes` for the case of loading a
  45. single class.
  46. Args:
  47. module_label (str): Module label comprising the app label and the
  48. module name, separated by a dot. For example, 'catalogue.forms'.
  49. classname (str): Name of the class to be imported.
  50. Returns:
  51. The requested class object or `None` if it can't be found
  52. """
  53. return get_classes(module_label, [classname])[0]
  54. def get_classes(module_label, classnames):
  55. """
  56. Dynamically import a list of classes from the given module.
  57. This works by looping over ``INSTALLED_APPS`` and looking for a match
  58. against the passed module label. If the requested class can't be found in
  59. the matching module, then we attempt to import it from the corresponding
  60. core Oscar app (assuming the matched module isn't in Oscar).
  61. This is very similar to ``django.db.models.get_model`` function for
  62. dynamically loading models. This function is more general though as it can
  63. load any class from the matching app, not just a model.
  64. Args:
  65. module_label (str): Module label comprising the app label and the
  66. module name, separated by a dot. For example, 'catalogue.forms'.
  67. classname (str): Name of the class to be imported.
  68. Returns:
  69. The requested class object or ``None`` if it can't be found
  70. Examples:
  71. Load a single class:
  72. >>> get_class('dashboard.catalogue.forms', 'ProductForm')
  73. oscar.apps.dashboard.catalogue.forms.ProductForm
  74. Load a list of classes:
  75. >>> get_classes('dashboard.catalogue.forms',
  76. ... ['ProductForm', 'StockRecordForm'])
  77. [oscar.apps.dashboard.catalogue.forms.ProductForm,
  78. oscar.apps.dashboard.catalogue.forms.StockRecordForm]
  79. Raises:
  80. AppNotFoundError: If no app is found in ``INSTALLED_APPS`` that matches
  81. the passed module label.
  82. ImportError: If the attempted import of a class raises an
  83. ``ImportError``, it is re-raised
  84. """
  85. if '.' not in module_label:
  86. # Importing from top-level modules is not supported, e.g.
  87. # get_class('shipping', 'Scale'). That should be easy to fix,
  88. # but @maikhoepfel had a stab and could not get it working reliably.
  89. # Overridable classes in a __init__.py might not be a good idea anyway.
  90. raise ValueError(
  91. "Importing from top-level modules is not supported")
  92. # import from Oscar package (should succeed in most cases)
  93. # e.g. 'oscar.apps.dashboard.catalogue.forms'
  94. oscar_module_label = "oscar.apps.%s" % module_label
  95. oscar_module = _import_module(oscar_module_label, classnames)
  96. # returns e.g. 'oscar.apps.dashboard.catalogue',
  97. # 'yourproject.apps.dashboard.catalogue' or 'dashboard.catalogue',
  98. # depending on what is set in INSTALLED_APPS
  99. installed_apps_entry, app_name = _find_installed_apps_entry(module_label)
  100. if installed_apps_entry.startswith('oscar.apps.'):
  101. # The entry is obviously an Oscar one, we don't import again
  102. local_module = None
  103. else:
  104. # Attempt to import the classes from the local module
  105. # e.g. 'yourproject.dashboard.catalogue.forms'
  106. sub_module = module_label.replace(app_name, '')
  107. local_module_label = installed_apps_entry + sub_module
  108. local_module = _import_module(local_module_label, classnames)
  109. if oscar_module is local_module is None:
  110. # This intentionally doesn't raise an ImportError, because ImportError
  111. # can get masked in complex circular import scenarios.
  112. raise ModuleNotFoundError(
  113. "The module with label '%s' could not be imported. This either"
  114. "means that it indeed does not exist, or you might have a problem"
  115. " with a circular import." % module_label
  116. )
  117. # return imported classes, giving preference to ones from the local package
  118. return _pluck_classes([local_module, oscar_module], classnames)
  119. def _import_module(module_label, classnames):
  120. """
  121. Imports the module with the given name.
  122. Returns None if the module doesn't exist, but propagates any import errors.
  123. """
  124. try:
  125. return __import__(module_label, fromlist=classnames)
  126. except ImportError:
  127. # There are 2 reasons why there could be an ImportError:
  128. #
  129. # 1. Module does not exist. In that case, we ignore the import and
  130. # return None
  131. # 2. Module exists but another ImportError occurred when trying to
  132. # import the module. In that case, it is important to propagate the
  133. # error.
  134. #
  135. # ImportError does not provide easy way to distinguish those two cases.
  136. # Fortunately, the traceback of the ImportError starts at __import__
  137. # statement. If the traceback has more than one frame, it means that
  138. # application was found and ImportError originates within the local app
  139. __, __, exc_traceback = sys.exc_info()
  140. frames = traceback.extract_tb(exc_traceback)
  141. if len(frames) > 1:
  142. raise
  143. def _pluck_classes(modules, classnames):
  144. """
  145. Gets a list of class names and a list of modules to pick from.
  146. For each class name, will return the class from the first module that has a
  147. matching class.
  148. """
  149. klasses = []
  150. for classname in classnames:
  151. klass = None
  152. for module in modules:
  153. if hasattr(module, classname):
  154. klass = getattr(module, classname)
  155. break
  156. if not klass:
  157. packages = [m.__name__ for m in modules if m is not None]
  158. raise ClassNotFoundError("No class '%s' found in %s" % (
  159. classname, ", ".join(packages)))
  160. klasses.append(klass)
  161. return klasses
  162. def _get_installed_apps_entry(app_name):
  163. """
  164. Given an app name (e.g. 'catalogue'), walk through INSTALLED_APPS
  165. and return the first match, or None.
  166. This does depend on the order of INSTALLED_APPS and will break if
  167. e.g. 'dashboard.catalogue' comes before 'catalogue' in INSTALLED_APPS.
  168. """
  169. for installed_app in settings.INSTALLED_APPS:
  170. # match root-level apps ('catalogue') or apps with same name at end
  171. # ('shop.catalogue'), but don't match 'fancy_catalogue'
  172. if installed_app == app_name or installed_app.endswith('.' + app_name):
  173. return installed_app
  174. return None
  175. def _find_installed_apps_entry(module_label):
  176. """
  177. Given a module label, finds the best matching INSTALLED_APPS entry.
  178. This is made trickier by the fact that we don't know what part of the
  179. module_label is part of the INSTALLED_APPS entry. So we try all possible
  180. combinations, trying the longer versions first. E.g. for
  181. 'dashboard.catalogue.forms', 'dashboard.catalogue' is attempted before
  182. 'dashboard'
  183. """
  184. modules = module_label.split('.')
  185. # if module_label is 'dashboard.catalogue.forms.widgets', combinations
  186. # will be ['dashboard.catalogue.forms', 'dashboard.catalogue', 'dashboard']
  187. combinations = [
  188. '.'.join(modules[:-count]) for count in range(1, len(modules))]
  189. for app_name in combinations:
  190. entry = _get_installed_apps_entry(app_name)
  191. if entry:
  192. return entry, app_name
  193. raise AppNotFoundError(
  194. "Couldn't find an app to import %s from" % module_label)
  195. def get_profile_class():
  196. """
  197. Return the profile model class
  198. """
  199. # The AUTH_PROFILE_MODULE setting was deprecated in Django 1.5, but it
  200. # makes sense for Oscar to continue to use it. Projects built on Django
  201. # 1.4 are likely to have used a profile class and it's very difficult to
  202. # upgrade to a single user model. Hence, we should continue to support
  203. # having a separate profile class even if Django doesn't.
  204. setting = getattr(settings, 'AUTH_PROFILE_MODULE', None)
  205. if setting is None:
  206. return None
  207. app_label, model_name = settings.AUTH_PROFILE_MODULE.split('.')
  208. return get_model(app_label, model_name)
  209. def feature_hidden(feature_name):
  210. """
  211. Test if a certain Oscar feature is disabled.
  212. """
  213. return (feature_name is not None and
  214. feature_name in settings.OSCAR_HIDDEN_FEATURES)
  215. # The following section is concerned with offering both the
  216. # get_model(app_label, model_name) and
  217. # is_model_registered(app_label, model_name) methods. Because the Django
  218. # internals dramatically changed in the Django 1.7 app refactor, we distinguish
  219. # based on the Django version and declare a total of four methods that
  220. # hopefully do mostly the same
  221. if django.VERSION < (1, 7):
  222. from django.db.models import get_model as django_get_model
  223. def get_model(app_label, model_name, *args, **kwargs):
  224. """
  225. Gets a model class by it's app label and model name. Fails loudly if
  226. the model class can't be imported.
  227. This is merely a thin wrapper around Django's get_model function.
  228. Raises LookupError if model isn't found.
  229. """
  230. # The snippet below is not useful in production, but helpful to
  231. # investigate circular import issues
  232. # from django.db.models.loading import app_cache_ready
  233. # if not app_cache_ready():
  234. # print(
  235. # "%s.%s accessed before app cache is fully populated!" %
  236. # (app_label, model_name))
  237. model = django_get_model(app_label, model_name, *args, **kwargs)
  238. if model is None:
  239. raise LookupError(
  240. "{app_label}.{model_name} could not be imported.".format(
  241. app_label=app_label, model_name=model_name))
  242. return model
  243. def is_model_registered(app_label, model_name):
  244. """
  245. Checks whether a given model is registered. This is used to only
  246. register Oscar models if they aren't overridden by a forked app.
  247. """
  248. return bool(django_get_model(app_label, model_name, seed_cache=False))
  249. else:
  250. from django.apps import apps
  251. def get_model(app_label, model_name):
  252. """
  253. Fetches a Django model using the app registry.
  254. This doesn't require that an app with the given app label exists,
  255. which makes it safe to call when the registry is being populated.
  256. All other methods to access models might raise an exception about the
  257. registry not being ready yet.
  258. Raises LookupError if model isn't found.
  259. """
  260. return apps.get_registered_model(app_label, model_name)
  261. def is_model_registered(app_label, model_name):
  262. """
  263. Checks whether a given model is registered. This is used to only
  264. register Oscar models if they aren't overridden by a forked app.
  265. """
  266. try:
  267. apps.get_registered_model(app_label, model_name)
  268. except LookupError:
  269. return False
  270. else:
  271. return True