| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- import logging
- import os
- import sys
- from yaml import YAMLError
- from collections import UserDict
- from mkdocs import exceptions
- from mkdocs import utils
- log = logging.getLogger('mkdocs.config')
- class ValidationError(Exception):
- """Raised during the validation process of the config on errors."""
- class Config(UserDict):
- """
- MkDocs Configuration dict
- This is a fairly simple extension of a standard dictionary. It adds methods
- for running validation on the structure and contents.
- """
- def __init__(self, schema, config_file_path=None):
- """
- The schema is a Python dict which maps the config name to a validator.
- """
- self._schema = schema
- self._schema_keys = set(dict(schema).keys())
- # Ensure config_file_path is a Unicode string
- if config_file_path is not None and not isinstance(config_file_path, str):
- try:
- # Assume config_file_path is encoded with the file system encoding.
- config_file_path = config_file_path.decode(encoding=sys.getfilesystemencoding())
- except UnicodeDecodeError:
- raise ValidationError("config_file_path is not a Unicode string.")
- self.config_file_path = config_file_path
- self.data = {}
- self.user_configs = []
- self.set_defaults()
- def set_defaults(self):
- """
- Set the base config by going through each validator and getting the
- default if it has one.
- """
- for key, config_option in self._schema:
- self[key] = config_option.default
- def _validate(self):
- failed, warnings = [], []
- for key, config_option in self._schema:
- try:
- value = self.get(key)
- self[key] = config_option.validate(value)
- warnings.extend([(key, w) for w in config_option.warnings])
- config_option.reset_warnings()
- except ValidationError as e:
- failed.append((key, e))
- for key in (set(self.keys()) - self._schema_keys):
- warnings.append((
- key, "Unrecognised configuration name: {}".format(key)
- ))
- return failed, warnings
- def _pre_validate(self):
- failed, warnings = [], []
- for key, config_option in self._schema:
- try:
- config_option.pre_validation(self, key_name=key)
- warnings.extend([(key, w) for w in config_option.warnings])
- config_option.reset_warnings()
- except ValidationError as e:
- failed.append((key, e))
- return failed, warnings
- def _post_validate(self):
- failed, warnings = [], []
- for key, config_option in self._schema:
- try:
- config_option.post_validation(self, key_name=key)
- warnings.extend([(key, w) for w in config_option.warnings])
- config_option.reset_warnings()
- except ValidationError as e:
- failed.append((key, e))
- return failed, warnings
- def validate(self):
- failed, warnings = self._pre_validate()
- run_failed, run_warnings = self._validate()
- failed.extend(run_failed)
- warnings.extend(run_warnings)
- # Only run the post validation steps if there are no failures, warnings
- # are okay.
- if len(failed) == 0:
- post_failed, post_warnings = self._post_validate()
- failed.extend(post_failed)
- warnings.extend(post_warnings)
- return failed, warnings
- def load_dict(self, patch):
- if not isinstance(patch, dict):
- raise exceptions.ConfigurationError(
- "The configuration is invalid. The expected type was a key "
- "value mapping (a python dict) but we got an object of type: "
- "{}".format(type(patch)))
- self.user_configs.append(patch)
- self.data.update(patch)
- def load_file(self, config_file):
- try:
- return self.load_dict(utils.yaml_load(config_file))
- except YAMLError as e:
- # MkDocs knows and understands ConfigurationErrors
- raise exceptions.ConfigurationError(
- "MkDocs encountered as error parsing the configuration file: {}".format(e)
- )
- def _open_config_file(config_file):
- # Default to the standard config filename.
- if config_file is None:
- config_file = os.path.abspath('mkdocs.yml')
- # If closed file descriptor, get file path to reopen later.
- if hasattr(config_file, 'closed') and config_file.closed:
- config_file = config_file.name
- log.debug("Loading configuration file: {}".format(config_file))
- # If it is a string, we can assume it is a path and attempt to open it.
- if isinstance(config_file, str):
- if os.path.exists(config_file):
- config_file = open(config_file, 'rb')
- else:
- raise exceptions.ConfigurationError(
- "Config file '{}' does not exist.".format(config_file))
- # Ensure file descriptor is at begining
- config_file.seek(0)
- return config_file
- def load_config(config_file=None, **kwargs):
- """
- Load the configuration for a given file object or name
- The config_file can either be a file object, string or None. If it is None
- the default `mkdocs.yml` filename will loaded.
- Extra kwargs are passed to the configuration to replace any default values
- unless they themselves are None.
- """
- options = kwargs.copy()
- # Filter None values from the options. This usually happens with optional
- # parameters from Click.
- for key, value in options.copy().items():
- if value is None:
- options.pop(key)
- config_file = _open_config_file(config_file)
- options['config_file_path'] = getattr(config_file, 'name', '')
- # Initialise the config with the default schema .
- from mkdocs import config
- cfg = Config(schema=config.DEFAULT_SCHEMA, config_file_path=options['config_file_path'])
- # First load the config file
- cfg.load_file(config_file)
- # Then load the options to overwrite anything in the config.
- cfg.load_dict(options)
- errors, warnings = cfg.validate()
- for config_name, warning in warnings:
- log.warning("Config value: '%s'. Warning: %s", config_name, warning)
- for config_name, error in errors:
- log.error("Config value: '%s'. Error: %s", config_name, error)
- for key, value in cfg.items():
- log.debug("Config value: '%s' = %r", key, value)
- if len(errors) > 0:
- raise exceptions.ConfigurationError(
- "Aborted with {} Configuration Errors!".format(len(errors))
- )
- elif cfg['strict'] and len(warnings) > 0:
- raise exceptions.ConfigurationError(
- "Aborted with {} Configuration Warnings in 'strict' mode!".format(len(warnings))
- )
- return cfg
|