module_loader.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. # Copyright 2017 New Vector Ltd
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import importlib
  15. import importlib.util
  16. import itertools
  17. from types import ModuleType
  18. from typing import Any, Iterable, Tuple, Type
  19. import jsonschema
  20. from synapse.config._base import ConfigError
  21. from synapse.config._util import json_error_to_config_error
  22. def load_module(provider: dict, config_path: Iterable[str]) -> Tuple[Type, Any]:
  23. """Loads a synapse module with its config
  24. Args:
  25. provider: a dict with keys 'module' (the module name) and 'config'
  26. (the config dict).
  27. config_path: the path within the config file. This will be used as a basis
  28. for any error message.
  29. Returns
  30. Tuple of (provider class, parsed config object)
  31. """
  32. modulename = provider.get("module")
  33. if not isinstance(modulename, str):
  34. raise ConfigError(
  35. "expected a string", path=itertools.chain(config_path, ("module",))
  36. )
  37. # We need to import the module, and then pick the class out of
  38. # that, so we split based on the last dot.
  39. module_name, clz = modulename.rsplit(".", 1)
  40. module = importlib.import_module(module_name)
  41. provider_class = getattr(module, clz)
  42. # Load the module config. If None, pass an empty dictionary instead
  43. module_config = provider.get("config") or {}
  44. if hasattr(provider_class, "parse_config"):
  45. try:
  46. provider_config = provider_class.parse_config(module_config)
  47. except jsonschema.ValidationError as e:
  48. raise json_error_to_config_error(
  49. e, itertools.chain(config_path, ("config",))
  50. )
  51. except ConfigError as e:
  52. raise _wrap_config_error(
  53. "Failed to parse config for module %r" % (modulename,),
  54. prefix=itertools.chain(config_path, ("config",)),
  55. e=e,
  56. )
  57. except Exception as e:
  58. raise ConfigError(
  59. "Failed to parse config for module %r" % (modulename,),
  60. path=itertools.chain(config_path, ("config",)),
  61. ) from e
  62. else:
  63. provider_config = module_config
  64. return provider_class, provider_config
  65. def load_python_module(location: str) -> ModuleType:
  66. """Load a python module, and return a reference to its global namespace
  67. Args:
  68. location: path to the module
  69. Returns:
  70. python module object
  71. """
  72. spec = importlib.util.spec_from_file_location(location, location)
  73. if spec is None:
  74. raise Exception("Unable to load module at %s" % (location,))
  75. mod = importlib.util.module_from_spec(spec)
  76. spec.loader.exec_module(mod) # type: ignore
  77. return mod
  78. def _wrap_config_error(
  79. msg: str, prefix: Iterable[str], e: ConfigError
  80. ) -> "ConfigError":
  81. """Wrap a relative ConfigError with a new path
  82. This is useful when we have a ConfigError with a relative path due to a problem
  83. parsing part of the config, and we now need to set it in context.
  84. """
  85. path = prefix
  86. if e.path:
  87. path = itertools.chain(prefix, e.path)
  88. e1 = ConfigError(msg, path)
  89. # ideally we would set the 'cause' of the new exception to the original exception;
  90. # however now that we have merged the path into our own, the stringification of
  91. # e will be incorrect, so instead we create a new exception with just the "msg"
  92. # part.
  93. e1.__cause__ = Exception(e.msg)
  94. e1.__cause__.__cause__ = e.__cause__
  95. return e1