base.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import logging
  2. import os
  3. import sys
  4. from yaml import YAMLError
  5. from collections import UserDict
  6. from mkdocs import exceptions
  7. from mkdocs import utils
  8. log = logging.getLogger('mkdocs.config')
  9. class ValidationError(Exception):
  10. """Raised during the validation process of the config on errors."""
  11. class Config(UserDict):
  12. """
  13. MkDocs Configuration dict
  14. This is a fairly simple extension of a standard dictionary. It adds methods
  15. for running validation on the structure and contents.
  16. """
  17. def __init__(self, schema, config_file_path=None):
  18. """
  19. The schema is a Python dict which maps the config name to a validator.
  20. """
  21. self._schema = schema
  22. self._schema_keys = set(dict(schema).keys())
  23. # Ensure config_file_path is a Unicode string
  24. if config_file_path is not None and not isinstance(config_file_path, str):
  25. try:
  26. # Assume config_file_path is encoded with the file system encoding.
  27. config_file_path = config_file_path.decode(encoding=sys.getfilesystemencoding())
  28. except UnicodeDecodeError:
  29. raise ValidationError("config_file_path is not a Unicode string.")
  30. self.config_file_path = config_file_path
  31. self.data = {}
  32. self.user_configs = []
  33. self.set_defaults()
  34. def set_defaults(self):
  35. """
  36. Set the base config by going through each validator and getting the
  37. default if it has one.
  38. """
  39. for key, config_option in self._schema:
  40. self[key] = config_option.default
  41. def _validate(self):
  42. failed, warnings = [], []
  43. for key, config_option in self._schema:
  44. try:
  45. value = self.get(key)
  46. self[key] = config_option.validate(value)
  47. warnings.extend([(key, w) for w in config_option.warnings])
  48. config_option.reset_warnings()
  49. except ValidationError as e:
  50. failed.append((key, e))
  51. for key in (set(self.keys()) - self._schema_keys):
  52. warnings.append((
  53. key, "Unrecognised configuration name: {}".format(key)
  54. ))
  55. return failed, warnings
  56. def _pre_validate(self):
  57. failed, warnings = [], []
  58. for key, config_option in self._schema:
  59. try:
  60. config_option.pre_validation(self, key_name=key)
  61. warnings.extend([(key, w) for w in config_option.warnings])
  62. config_option.reset_warnings()
  63. except ValidationError as e:
  64. failed.append((key, e))
  65. return failed, warnings
  66. def _post_validate(self):
  67. failed, warnings = [], []
  68. for key, config_option in self._schema:
  69. try:
  70. config_option.post_validation(self, key_name=key)
  71. warnings.extend([(key, w) for w in config_option.warnings])
  72. config_option.reset_warnings()
  73. except ValidationError as e:
  74. failed.append((key, e))
  75. return failed, warnings
  76. def validate(self):
  77. failed, warnings = self._pre_validate()
  78. run_failed, run_warnings = self._validate()
  79. failed.extend(run_failed)
  80. warnings.extend(run_warnings)
  81. # Only run the post validation steps if there are no failures, warnings
  82. # are okay.
  83. if len(failed) == 0:
  84. post_failed, post_warnings = self._post_validate()
  85. failed.extend(post_failed)
  86. warnings.extend(post_warnings)
  87. return failed, warnings
  88. def load_dict(self, patch):
  89. if not isinstance(patch, dict):
  90. raise exceptions.ConfigurationError(
  91. "The configuration is invalid. The expected type was a key "
  92. "value mapping (a python dict) but we got an object of type: "
  93. "{}".format(type(patch)))
  94. self.user_configs.append(patch)
  95. self.data.update(patch)
  96. def load_file(self, config_file):
  97. try:
  98. return self.load_dict(utils.yaml_load(config_file))
  99. except YAMLError as e:
  100. # MkDocs knows and understands ConfigurationErrors
  101. raise exceptions.ConfigurationError(
  102. "MkDocs encountered as error parsing the configuration file: {}".format(e)
  103. )
  104. def _open_config_file(config_file):
  105. # Default to the standard config filename.
  106. if config_file is None:
  107. config_file = os.path.abspath('mkdocs.yml')
  108. # If closed file descriptor, get file path to reopen later.
  109. if hasattr(config_file, 'closed') and config_file.closed:
  110. config_file = config_file.name
  111. log.debug("Loading configuration file: {}".format(config_file))
  112. # If it is a string, we can assume it is a path and attempt to open it.
  113. if isinstance(config_file, str):
  114. if os.path.exists(config_file):
  115. config_file = open(config_file, 'rb')
  116. else:
  117. raise exceptions.ConfigurationError(
  118. "Config file '{}' does not exist.".format(config_file))
  119. # Ensure file descriptor is at begining
  120. config_file.seek(0)
  121. return config_file
  122. def load_config(config_file=None, **kwargs):
  123. """
  124. Load the configuration for a given file object or name
  125. The config_file can either be a file object, string or None. If it is None
  126. the default `mkdocs.yml` filename will loaded.
  127. Extra kwargs are passed to the configuration to replace any default values
  128. unless they themselves are None.
  129. """
  130. options = kwargs.copy()
  131. # Filter None values from the options. This usually happens with optional
  132. # parameters from Click.
  133. for key, value in options.copy().items():
  134. if value is None:
  135. options.pop(key)
  136. config_file = _open_config_file(config_file)
  137. options['config_file_path'] = getattr(config_file, 'name', '')
  138. # Initialise the config with the default schema .
  139. from mkdocs import config
  140. cfg = Config(schema=config.DEFAULT_SCHEMA, config_file_path=options['config_file_path'])
  141. # First load the config file
  142. cfg.load_file(config_file)
  143. # Then load the options to overwrite anything in the config.
  144. cfg.load_dict(options)
  145. errors, warnings = cfg.validate()
  146. for config_name, warning in warnings:
  147. log.warning("Config value: '%s'. Warning: %s", config_name, warning)
  148. for config_name, error in errors:
  149. log.error("Config value: '%s'. Error: %s", config_name, error)
  150. for key, value in cfg.items():
  151. log.debug("Config value: '%s' = %r", key, value)
  152. if len(errors) > 0:
  153. raise exceptions.ConfigurationError(
  154. "Aborted with {} Configuration Errors!".format(len(errors))
  155. )
  156. elif cfg['strict'] and len(warnings) > 0:
  157. raise exceptions.ConfigurationError(
  158. "Aborted with {} Configuration Warnings in 'strict' mode!".format(len(warnings))
  159. )
  160. return cfg