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 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import sys
  2. import traceback
  3. import warnings
  4. from importlib import import_module
  5. from django.apps import apps
  6. from django.apps.config import MODELS_MODULE_NAME
  7. from django.conf import settings
  8. from django.core.exceptions import AppRegistryNotReady
  9. from django.utils.lru_cache import lru_cache
  10. from django.utils.module_loading import import_string
  11. from oscar.core.exceptions import (
  12. AppNotFoundError, ClassNotFoundError, ModuleNotFoundError)
  13. from oscar.utils.deprecation import RemovedInOscar22Warning
  14. # To preserve backwards compatibility of loading classes which moved
  15. # from one Oscar module to another, we look into the dictionary below
  16. # for the moved items during loading.
  17. MOVED_MODELS = {
  18. 'customer': (
  19. 'communication', ('communicationeventtype', 'email', 'notification')
  20. )
  21. }
  22. def get_class(module_label, classname, module_prefix='oscar.apps'):
  23. """
  24. Dynamically import a single class from the given module.
  25. This is a simple wrapper around `get_classes` for the case of loading a
  26. single class.
  27. Args:
  28. module_label (str): Module label comprising the app label and the
  29. module name, separated by a dot. For example, 'catalogue.forms'.
  30. classname (str): Name of the class to be imported.
  31. Returns:
  32. The requested class object or `None` if it can't be found
  33. """
  34. return get_classes(module_label, [classname], module_prefix)[0]
  35. @lru_cache(maxsize=100)
  36. def get_class_loader():
  37. return import_string(settings.OSCAR_DYNAMIC_CLASS_LOADER)
  38. def get_classes(module_label, classnames, module_prefix='oscar.apps'):
  39. class_loader = get_class_loader()
  40. return class_loader(module_label, classnames, module_prefix)
  41. def default_class_loader(module_label, classnames, module_prefix):
  42. """
  43. Dynamically import a list of classes from the given module.
  44. This works by looking up a matching app from the app registry,
  45. against the passed module label. If the requested class can't be found in
  46. the matching module, then we attempt to import it from the corresponding
  47. core app.
  48. This is very similar to ``django.db.models.get_model`` function for
  49. dynamically loading models. This function is more general though as it can
  50. load any class from the matching app, not just a model.
  51. Args:
  52. module_label (str): Module label comprising the app label and the
  53. module name, separated by a dot. For example, 'catalogue.forms'.
  54. classname (str): Name of the class to be imported.
  55. Returns:
  56. The requested class object or ``None`` if it can't be found
  57. Examples:
  58. Load a single class:
  59. >>> get_class('dashboard.catalogue.forms', 'ProductForm')
  60. oscar.apps.dashboard.catalogue.forms.ProductForm
  61. Load a list of classes:
  62. >>> get_classes('dashboard.catalogue.forms',
  63. ... ['ProductForm', 'StockRecordForm'])
  64. [oscar.apps.dashboard.catalogue.forms.ProductForm,
  65. oscar.apps.dashboard.catalogue.forms.StockRecordForm]
  66. Raises:
  67. AppNotFoundError: If no app is found in ``INSTALLED_APPS`` that matches
  68. the passed module label.
  69. ImportError: If the attempted import of a class raises an
  70. ``ImportError``, it is re-raised
  71. """
  72. if '.' not in module_label:
  73. # Importing from top-level modules is not supported, e.g.
  74. # get_class('shipping', 'Scale'). That should be easy to fix,
  75. # but @maikhoepfel had a stab and could not get it working reliably.
  76. # Overridable classes in a __init__.py might not be a good idea anyway.
  77. raise ValueError(
  78. "Importing from top-level modules is not supported")
  79. # import from Oscar package (should succeed in most cases)
  80. # e.g. 'oscar.apps.dashboard.catalogue.forms'
  81. oscar_module_label = "%s.%s" % (module_prefix, module_label)
  82. oscar_module = _import_module(oscar_module_label, classnames)
  83. # returns e.g. 'oscar.apps.dashboard.catalogue',
  84. # 'yourproject.apps.dashboard.catalogue' or 'dashboard.catalogue',
  85. # depending on what is set in INSTALLED_APPS
  86. app_name = _find_registered_app_name(module_label)
  87. if app_name.startswith('%s.' % module_prefix):
  88. # The entry is obviously an Oscar one, we don't import again
  89. local_module = None
  90. else:
  91. # Attempt to import the classes from the local module
  92. # e.g. 'yourproject.dashboard.catalogue.forms'
  93. local_module_label = '.'.join(app_name.split('.') + module_label.split('.')[1:])
  94. local_module = _import_module(local_module_label, classnames)
  95. if oscar_module is local_module is None:
  96. # This intentionally doesn't raise an ImportError, because ImportError
  97. # can get masked in complex circular import scenarios.
  98. raise ModuleNotFoundError(
  99. "The module with label '%s' could not be imported. This either"
  100. "means that it indeed does not exist, or you might have a problem"
  101. " with a circular import." % module_label
  102. )
  103. # return imported classes, giving preference to ones from the local package
  104. return _pluck_classes([local_module, oscar_module], classnames)
  105. def _import_module(module_label, classnames):
  106. """
  107. Imports the module with the given name.
  108. Returns None if the module doesn't exist, but propagates any import errors.
  109. """
  110. try:
  111. return __import__(module_label, fromlist=classnames)
  112. except ImportError:
  113. # There are 2 reasons why there could be an ImportError:
  114. #
  115. # 1. Module does not exist. In that case, we ignore the import and
  116. # return None
  117. # 2. Module exists but another ImportError occurred when trying to
  118. # import the module. In that case, it is important to propagate the
  119. # error.
  120. #
  121. # ImportError does not provide easy way to distinguish those two cases.
  122. # Fortunately, the traceback of the ImportError starts at __import__
  123. # statement. If the traceback has more than one frame, it means that
  124. # application was found and ImportError originates within the local app
  125. __, __, exc_traceback = sys.exc_info()
  126. frames = traceback.extract_tb(exc_traceback)
  127. if len(frames) > 1:
  128. raise
  129. def _pluck_classes(modules, classnames):
  130. """
  131. Gets a list of class names and a list of modules to pick from.
  132. For each class name, will return the class from the first module that has a
  133. matching class.
  134. """
  135. klasses = []
  136. for classname in classnames:
  137. klass = None
  138. for module in modules:
  139. if hasattr(module, classname):
  140. klass = getattr(module, classname)
  141. break
  142. if not klass:
  143. packages = [m.__name__ for m in modules if m is not None]
  144. raise ClassNotFoundError("No class '%s' found in %s" % (
  145. classname, ", ".join(packages)))
  146. klasses.append(klass)
  147. return klasses
  148. def _find_registered_app_name(module_label):
  149. """
  150. Given a module label, finds the name of the matching Oscar app from the
  151. Django app registry.
  152. """
  153. from oscar.core.application import OscarConfig
  154. app_label = module_label.split('.')[0]
  155. try:
  156. app_config = apps.get_app_config(app_label)
  157. except LookupError:
  158. raise AppNotFoundError(
  159. "Couldn't find an app to import %s from" % module_label)
  160. if not isinstance(app_config, OscarConfig):
  161. raise AppNotFoundError(
  162. "Couldn't find an Oscar app to import %s from" % module_label)
  163. return app_config.name
  164. def get_profile_class():
  165. """
  166. Return the profile model class
  167. """
  168. # The AUTH_PROFILE_MODULE setting was deprecated in Django 1.5, but it
  169. # makes sense for Oscar to continue to use it. Projects built on Django
  170. # 1.4 are likely to have used a profile class and it's very difficult to
  171. # upgrade to a single user model. Hence, we should continue to support
  172. # having a separate profile class even if Django doesn't.
  173. setting = getattr(settings, 'AUTH_PROFILE_MODULE', None)
  174. if setting is None:
  175. return None
  176. app_label, model_name = settings.AUTH_PROFILE_MODULE.split('.')
  177. return get_model(app_label, model_name)
  178. def feature_hidden(feature_name):
  179. """
  180. Test if a certain Oscar feature is disabled.
  181. """
  182. return (feature_name is not None
  183. and feature_name in settings.OSCAR_HIDDEN_FEATURES)
  184. def get_model(app_label, model_name):
  185. """
  186. Fetches a Django model using the app registry.
  187. This doesn't require that an app with the given app label exists,
  188. which makes it safe to call when the registry is being populated.
  189. All other methods to access models might raise an exception about the
  190. registry not being ready yet.
  191. Raises LookupError if model isn't found.
  192. """
  193. oscar_moved_model = MOVED_MODELS.get(app_label, None)
  194. if oscar_moved_model:
  195. if model_name.lower() in oscar_moved_model[1]:
  196. original_app_label = app_label
  197. app_label = oscar_moved_model[0]
  198. warnings.warn(
  199. 'Model %s has recently moved from %s to the application %s, '
  200. 'please update your imports.' % (model_name, original_app_label, app_label),
  201. RemovedInOscar22Warning, stacklevel=2)
  202. try:
  203. return apps.get_model(app_label, model_name)
  204. except AppRegistryNotReady:
  205. if apps.apps_ready and not apps.models_ready:
  206. # If this function is called while `apps.populate()` is
  207. # loading models, ensure that the module that defines the
  208. # target model has been imported and try looking the model up
  209. # in the app registry. This effectively emulates
  210. # `from path.to.app.models import Model` where we use
  211. # `Model = get_model('app', 'Model')` instead.
  212. app_config = apps.get_app_config(app_label)
  213. # `app_config.import_models()` cannot be used here because it
  214. # would interfere with `apps.populate()`.
  215. import_module('%s.%s' % (app_config.name, MODELS_MODULE_NAME))
  216. # In order to account for case-insensitivity of model_name,
  217. # look up the model through a private API of the app registry.
  218. return apps.get_registered_model(app_label, model_name)
  219. else:
  220. # This must be a different case (e.g. the model really doesn't
  221. # exist). We just re-raise the exception.
  222. raise
  223. def is_model_registered(app_label, model_name):
  224. """
  225. Checks whether a given model is registered. This is used to only
  226. register Oscar models if they aren't overridden by a forked app.
  227. """
  228. try:
  229. apps.get_registered_model(app_label, model_name)
  230. except LookupError:
  231. return False
  232. else:
  233. return True
  234. @lru_cache(maxsize=128)
  235. def cached_import_string(path):
  236. return import_string(path)