| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633 |
- import os
- from collections import Sequence, namedtuple
- from urllib.parse import urlparse
- import ipaddress
- import markdown
- from mkdocs import utils, theme, plugins
- from mkdocs.config.base import Config, ValidationError
- class BaseConfigOption:
- def __init__(self):
- self.warnings = []
- self.default = None
- def is_required(self):
- return False
- def validate(self, value):
- return self.run_validation(value)
- def reset_warnings(self):
- self.warnings = []
- def pre_validation(self, config, key_name):
- """
- Before all options are validated, perform a pre-validation process.
- The pre-validation process method should be implemented by subclasses.
- """
- def run_validation(self, value):
- """
- Perform validation for a value.
- The run_validation method should be implemented by subclasses.
- """
- return value
- def post_validation(self, config, key_name):
- """
- After all options have passed validation, perform a post-validation
- process to do any additional changes dependant on other config values.
- The post-validation process method should be implemented by subclasses.
- """
- class SubConfig(BaseConfigOption, Config):
- def __init__(self, *config_options):
- BaseConfigOption.__init__(self)
- Config.__init__(self, config_options)
- self.default = {}
- def validate(self, value):
- self.load_dict(value)
- return self.run_validation(value)
- def run_validation(self, value):
- Config.validate(self)
- return self
- class ConfigItems(BaseConfigOption):
- """
- Config Items Option
- Validates a list of mappings that all must match the same set of
- options.
- """
- def __init__(self, *config_options, **kwargs):
- BaseConfigOption.__init__(self)
- self.item_config = SubConfig(*config_options)
- self.required = kwargs.get('required', False)
- def __repr__(self):
- return '{}: {}'.format(self.__class__.__name__, self.item_config)
- def run_validation(self, value):
- if value is None:
- if self.required:
- raise ValidationError("Required configuration not provided.")
- else:
- return ()
- if not isinstance(value, Sequence):
- raise ValidationError('Expected a sequence of mappings, but a %s '
- 'was given.' % type(value))
- result = []
- for item in value:
- result.append(self.item_config.validate(item))
- return result
- class OptionallyRequired(BaseConfigOption):
- """
- A subclass of BaseConfigOption that adds support for default values and
- required values. It is a base class for config options.
- """
- def __init__(self, default=None, required=False):
- super().__init__()
- self.default = default
- self.required = required
- def is_required(self):
- return self.required
- def validate(self, value):
- """
- Perform some initial validation.
- If the option is empty (None) and isn't required, leave it as such. If
- it is empty but has a default, use that. Finally, call the
- run_validation method on the subclass unless.
- """
- if value is None:
- if self.default is not None:
- if hasattr(self.default, 'copy'):
- # ensure no mutable values are assigned
- value = self.default.copy()
- else:
- value = self.default
- elif not self.required:
- return
- elif self.required:
- raise ValidationError("Required configuration not provided.")
- return self.run_validation(value)
- class Type(OptionallyRequired):
- """
- Type Config Option
- Validate the type of a config option against a given Python type.
- """
- def __init__(self, type_, length=None, **kwargs):
- super().__init__(**kwargs)
- self._type = type_
- self.length = length
- def run_validation(self, value):
- if not isinstance(value, self._type):
- msg = ("Expected type: {} but received: {}"
- .format(self._type, type(value)))
- elif self.length is not None and len(value) != self.length:
- msg = ("Expected type: {0} with length {2} but received: {1} with "
- "length {3}").format(self._type, value, self.length,
- len(value))
- else:
- return value
- raise ValidationError(msg)
- class Choice(OptionallyRequired):
- """
- Choice Config Option
- Validate the config option against a strict set of values.
- """
- def __init__(self, choices, **kwargs):
- super().__init__(**kwargs)
- try:
- length = len(choices)
- except TypeError:
- length = 0
- if not length or isinstance(choices, str):
- raise ValueError('Expected iterable of choices, got {}', choices)
- self.choices = choices
- def run_validation(self, value):
- if value not in self.choices:
- msg = ("Expected one of: {} but received: {}"
- .format(self.choices, value))
- else:
- return value
- raise ValidationError(msg)
- class Deprecated(BaseConfigOption):
- def __init__(self, moved_to=None):
- super().__init__()
- self.default = None
- self.moved_to = moved_to
- def pre_validation(self, config, key_name):
- if config.get(key_name) is None or self.moved_to is None:
- return
- warning = ('The configuration option {} has been deprecated and '
- 'will be removed in a future release of MkDocs.'
- ''.format(key_name))
- self.warnings.append(warning)
- if '.' not in self.moved_to:
- target = config
- target_key = self.moved_to
- else:
- move_to, target_key = self.moved_to.rsplit('.', 1)
- target = config
- for key in move_to.split('.'):
- target = target.setdefault(key, {})
- if not isinstance(target, dict):
- # We can't move it for the user
- return
- target[target_key] = config.pop(key_name)
- class IpAddress(OptionallyRequired):
- """
- IpAddress Config Option
- Validate that an IP address is in an apprioriate format
- """
- def run_validation(self, value):
- try:
- host, port = value.rsplit(':', 1)
- except Exception:
- raise ValidationError("Must be a string of format 'IP:PORT'")
- if host != 'localhost':
- try:
- # Validate and normalize IP Address
- host = str(ipaddress.ip_address(host))
- except ValueError as e:
- raise ValidationError(e)
- try:
- port = int(port)
- except Exception:
- raise ValidationError("'{}' is not a valid port".format(port))
- class Address(namedtuple('Address', 'host port')):
- def __str__(self):
- return '{}:{}'.format(self.host, self.port)
- return Address(host, port)
- def post_validation(self, config, key_name):
- host = config[key_name].host
- if key_name == 'dev_addr' and host in ['0.0.0.0', '::']:
- self.warnings.append(
- ("The use of the IP address '{}' suggests a production environment "
- "or the use of a proxy to connect to the MkDocs server. However, "
- "the MkDocs' server is intended for local development purposes only. "
- "Please use a third party production-ready server instead.").format(host)
- )
- class URL(OptionallyRequired):
- """
- URL Config Option
- Validate a URL by requiring a scheme is present.
- """
- def __init__(self, default='', required=False):
- super().__init__(default, required)
- def run_validation(self, value):
- if value == '':
- return value
- try:
- parsed_url = urlparse(value)
- except (AttributeError, TypeError):
- raise ValidationError("Unable to parse the URL.")
- if parsed_url.scheme:
- return value
- raise ValidationError(
- "The URL isn't valid, it should include the http:// (scheme)")
- class RepoURL(URL):
- """
- Repo URL Config Option
- A small extension to the URL config that sets the repo_name and edit_uri,
- based on the url if they haven't already been provided.
- """
- def post_validation(self, config, key_name):
- repo_host = urlparse(config['repo_url']).netloc.lower()
- edit_uri = config.get('edit_uri')
- # derive repo_name from repo_url if unset
- if config['repo_url'] is not None and config.get('repo_name') is None:
- if repo_host == 'github.com':
- config['repo_name'] = 'GitHub'
- elif repo_host == 'bitbucket.org':
- config['repo_name'] = 'Bitbucket'
- elif repo_host == 'gitlab.com':
- config['repo_name'] = 'GitLab'
- else:
- config['repo_name'] = repo_host.split('.')[0].title()
- # derive edit_uri from repo_name if unset
- if config['repo_url'] is not None and edit_uri is None:
- if repo_host == 'github.com' or repo_host == 'gitlab.com':
- edit_uri = 'edit/master/docs/'
- elif repo_host == 'bitbucket.org':
- edit_uri = 'src/default/docs/'
- else:
- edit_uri = ''
- # ensure a well-formed edit_uri
- if edit_uri:
- if not edit_uri.startswith(('?', '#')) \
- and not config['repo_url'].endswith('/'):
- config['repo_url'] += '/'
- if not edit_uri.endswith('/'):
- edit_uri += '/'
- config['edit_uri'] = edit_uri
- class FilesystemObject(Type):
- """
- Base class for options that point to filesystem objects.
- """
- def __init__(self, exists=False, **kwargs):
- super().__init__(type_=str, **kwargs)
- self.exists = exists
- self.config_dir = None
- def pre_validation(self, config, key_name):
- self.config_dir = os.path.dirname(config.config_file_path) if config.config_file_path else None
- def run_validation(self, value):
- value = super().run_validation(value)
- if self.config_dir and not os.path.isabs(value):
- value = os.path.join(self.config_dir, value)
- if self.exists and not self.existence_test(value):
- raise ValidationError("The path {path} isn't an existing {name}.".
- format(path=value, name=self.name))
- value = os.path.abspath(value)
- assert isinstance(value, str)
- return value
- class Dir(FilesystemObject):
- """
- Dir Config Option
- Validate a path to a directory, optionally verifying that it exists.
- """
- existence_test = staticmethod(os.path.isdir)
- name = 'directory'
- def post_validation(self, config, key_name):
- if config.config_file_path is None:
- return
- # Validate that the dir is not the parent dir of the config file.
- if os.path.dirname(config.config_file_path) == config[key_name]:
- raise ValidationError(
- ("The '{0}' should not be the parent directory of the config "
- "file. Use a child directory instead so that the '{0}' "
- "is a sibling of the config file.").format(key_name))
- class File(FilesystemObject):
- """
- File Config Option
- Validate a path to a file, optionally verifying that it exists.
- """
- existence_test = staticmethod(os.path.isfile)
- name = 'file'
- class SiteDir(Dir):
- """
- SiteDir Config Option
- Validates the site_dir and docs_dir directories do not contain each other.
- """
- def post_validation(self, config, key_name):
- super().post_validation(config, key_name)
- # Validate that the docs_dir and site_dir don't contain the
- # other as this will lead to copying back and forth on each
- # and eventually make a deep nested mess.
- if (config['docs_dir'] + os.sep).startswith(config['site_dir'].rstrip(os.sep) + os.sep):
- raise ValidationError(
- ("The 'docs_dir' should not be within the 'site_dir' as this "
- "can mean the source files are overwritten by the output or "
- "it will be deleted if --clean is passed to mkdocs build."
- "(site_dir: '{}', docs_dir: '{}')"
- ).format(config['site_dir'], config['docs_dir']))
- elif (config['site_dir'] + os.sep).startswith(config['docs_dir'].rstrip(os.sep) + os.sep):
- raise ValidationError(
- ("The 'site_dir' should not be within the 'docs_dir' as this "
- "leads to the build directory being copied into itself and "
- "duplicate nested files in the 'site_dir'."
- "(site_dir: '{}', docs_dir: '{}')"
- ).format(config['site_dir'], config['docs_dir']))
- class Theme(BaseConfigOption):
- """
- Theme Config Option
- Validate that the theme exists and build Theme instance.
- """
- def __init__(self, default=None):
- super().__init__()
- self.default = default
- def validate(self, value):
- if value is None and self.default is not None:
- value = {'name': self.default}
- if isinstance(value, str):
- value = {'name': value}
- themes = utils.get_theme_names()
- if isinstance(value, dict):
- if 'name' in value:
- if value['name'] is None or value['name'] in themes:
- return value
- raise ValidationError(
- "Unrecognised theme name: '{}'. The available installed themes "
- "are: {}".format(value['name'], ', '.join(themes))
- )
- raise ValidationError("No theme name set.")
- raise ValidationError('Invalid type "{}". Expected a string or key/value pairs.'.format(type(value)))
- def post_validation(self, config, key_name):
- theme_config = config[key_name]
- if not theme_config['name'] and 'custom_dir' not in theme_config:
- raise ValidationError("At least one of 'theme.name' or 'theme.custom_dir' must be defined.")
- # Ensure custom_dir is an absolute path
- if 'custom_dir' in theme_config and not os.path.isabs(theme_config['custom_dir']):
- config_dir = os.path.dirname(config.config_file_path)
- theme_config['custom_dir'] = os.path.join(config_dir, theme_config['custom_dir'])
- if 'custom_dir' in theme_config and not os.path.isdir(theme_config['custom_dir']):
- raise ValidationError("The path set in {name}.custom_dir ('{path}') does not exist.".
- format(path=theme_config['custom_dir'], name=key_name))
- config[key_name] = theme.Theme(**theme_config)
- class Nav(OptionallyRequired):
- """
- Nav Config Option
- Validate the Nav config. Automatically add all markdown files if empty.
- """
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self.file_match = utils.is_markdown_file
- def run_validation(self, value):
- if not isinstance(value, list):
- raise ValidationError(
- "Expected a list, got {}".format(type(value)))
- if len(value) == 0:
- return
- config_types = {type(item) for item in value}
- if config_types.issubset({str, dict}):
- return value
- raise ValidationError("Invalid pages config. {} {}".format(
- config_types, {str, dict}
- ))
- def post_validation(self, config, key_name):
- # TODO: remove this when `pages` config setting is fully deprecated.
- if key_name == 'pages' and config['pages'] is not None:
- if config['nav'] is None:
- # copy `pages` config to new 'nav' config setting
- config['nav'] = config['pages']
- warning = ("The 'pages' configuration option has been deprecated and will "
- "be removed in a future release of MkDocs. Use 'nav' instead.")
- self.warnings.append(warning)
- class Private(OptionallyRequired):
- """
- Private Config Option
- A config option only for internal use. Raises an error if set by the user.
- """
- def run_validation(self, value):
- raise ValidationError('For internal use only.')
- class MarkdownExtensions(OptionallyRequired):
- """
- Markdown Extensions Config Option
- A list of extensions. If a list item contains extension configs,
- those are set on the private setting passed to `configkey`. The
- `builtins` keyword accepts a list of extensions which cannot be
- overriden by the user. However, builtins can be duplicated to define
- config options for them if desired.
- """
- def __init__(self, builtins=None, configkey='mdx_configs', **kwargs):
- super().__init__(**kwargs)
- self.builtins = builtins or []
- self.configkey = configkey
- self.configdata = {}
- def run_validation(self, value):
- if not isinstance(value, (list, tuple)):
- raise ValidationError('Invalid Markdown Extensions configuration')
- extensions = []
- for item in value:
- if isinstance(item, dict):
- if len(item) > 1:
- raise ValidationError('Invalid Markdown Extensions configuration')
- ext, cfg = item.popitem()
- extensions.append(ext)
- if cfg is None:
- continue
- if not isinstance(cfg, dict):
- raise ValidationError('Invalid config options for Markdown '
- "Extension '{}'.".format(ext))
- self.configdata[ext] = cfg
- elif isinstance(item, str):
- extensions.append(item)
- else:
- raise ValidationError('Invalid Markdown Extensions configuration')
- extensions = utils.reduce_list(self.builtins + extensions)
- # Confirm that Markdown considers extensions to be valid
- try:
- markdown.Markdown(extensions=extensions, extension_configs=self.configdata)
- except Exception as e:
- raise ValidationError(e.args[0])
- return extensions
- def post_validation(self, config, key_name):
- config[self.configkey] = self.configdata
- class Plugins(OptionallyRequired):
- """
- Plugins config option.
- A list of plugins. If a plugin defines config options those are used when
- initializing the plugin class.
- """
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self.installed_plugins = plugins.get_plugins()
- self.config_file_path = None
- def pre_validation(self, config, key_name):
- self.config_file_path = config.config_file_path
- def run_validation(self, value):
- if not isinstance(value, (list, tuple)):
- raise ValidationError('Invalid Plugins configuration. Expected a list of plugins')
- plgins = plugins.PluginCollection()
- for item in value:
- if isinstance(item, dict):
- if len(item) > 1:
- raise ValidationError('Invalid Plugins configuration')
- name, cfg = item.popitem()
- cfg = cfg or {} # Users may define a null (None) config
- if not isinstance(cfg, dict):
- raise ValidationError('Invalid config options for '
- 'the "{}" plugin.'.format(name))
- item = name
- else:
- cfg = {}
- if not isinstance(item, str):
- raise ValidationError('Invalid Plugins configuration')
- plgins[item] = self.load_plugin(item, cfg)
- return plgins
- def load_plugin(self, name, config):
- if name not in self.installed_plugins:
- raise ValidationError('The "{}" plugin is not installed'.format(name))
- Plugin = self.installed_plugins[name].load()
- if not issubclass(Plugin, plugins.BasePlugin):
- raise ValidationError('{}.{} must be a subclass of {}.{}'.format(
- Plugin.__module__, Plugin.__name__, plugins.BasePlugin.__module__,
- plugins.BasePlugin.__name__))
- plugin = Plugin()
- errors, warnings = plugin.load_config(config, self.config_file_path)
- self.warnings.extend(warnings)
- errors_message = '\n'.join(
- "Plugin value: '{}'. Error: {}".format(x, y)
- for x, y in errors
- )
- if errors_message:
- raise ValidationError(errors_message)
- return plugin
|