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
def _mkdocs_config(config: dict) -> mkdocs_config.Config: config_instance = mkdocs_config.Config(schema=mkdocs_config.DEFAULT_SCHEMA) config_instance.load_dict(config) errors, warnings = config_instance.validate() if errors: raise _mkdocs_exceptions.ConfigurationError( "Aborted with {} Configuration Errors!".format(len(errors))) elif config.get("strict", False) and warnings: raise _mkdocs_exceptions.ConfigurationError( "Aborted with {} Configuration Warnings in 'strict' mode!".format( len(warnings))) config_instance.config_file_path = config["config_file_path"] return config_instance
def _generate_site_navigation(pages_config, url_context, use_directory_urls=True): """ Returns a list of Page and Header instances that represent the top level site navigation. """ nav_items = [] pages = [] previous = None for config_line in pages_config: if isinstance(config_line, str): path = config_line title, child_title = None, None elif len(config_line) in (1, 2, 3): # Pad any items that don't exist with 'None' padded_config = (list(config_line) + [None, None])[:3] path, title, child_title = padded_config else: msg = ( "Line in 'page' config contained %d items. " "Expected 1, 2 or 3 strings." % len(config_line) ) raise exceptions.ConfigurationError(msg) if title is None: filename = path.split(os.path.sep)[0] title = filename_to_title(filename) if child_title is None and os.path.sep in path: filename = path.split(os.path.sep)[-1] child_title = filename_to_title(filename) url = utils.get_url_path(path, use_directory_urls) if not child_title: # New top level page. page = Page(title=title, url=url, path=path, url_context=url_context) nav_items.append(page) elif not nav_items or (nav_items[-1].title != title): # New second level page. page = Page(title=child_title, url=url, path=path, url_context=url_context) header = Header(title=title, children=[page]) nav_items.append(header) page.ancestors = [header] else: # Additional second level page. page = Page(title=child_title, url=url, path=path, url_context=url_context) header = nav_items[-1] header.children.append(page) page.ancestors = [header] # Add in previous and next information. if previous: page.previous_page = previous previous.next_page = page previous = page pages.append(page) return (nav_items, pages)
def get_themes(): """Return a dict of theme names and their locations""" themes = {} builtins = pkg_resources.get_entry_map(dist='mkdocs', group='mkdocs.themes') for theme in pkg_resources.iter_entry_points(group='mkdocs.themes'): if theme.name in builtins and theme.dist.key != 'mkdocs': raise exceptions.ConfigurationError( "The theme {0} is a builtin theme but {1} provides a theme " "with the same name".format(theme.name, theme.dist.key)) elif theme.name in themes: multiple_packages = [themes[theme.name].dist.key, theme.dist.key] log.warning( "The theme %s is provided by the Python packages " "'%s'. The one in %s will be used.", theme.name, ','.join(multiple_packages), theme.dist.key) themes[theme.name] = theme themes = dict( (name, os.path.dirname(os.path.abspath(theme.load().__file__))) for name, theme in themes.items()) return themes
def _open_config_file(config_file): # Default to the standard config filename. if config_file is None: for possible_file_ending in ('yml', 'yaml'): config_file = os.path.abspath( 'mkdocs.{}'.format(possible_file_ending)) if os.path.exists(config_file): break else: 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_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 an error parsing the configuration file: {}".format(e) )
def _mkdocs_config(config: dict) -> mkdocs_config.Config: config_instance = mkdocs_config.Config(schema=mkdocs_schema()) config_instance.load_dict(config) errors, warnings = config_instance.validate() if errors: print(errors) raise _mkdocs_exceptions.ConfigurationError( f"Aborted with {len(errors)} Configuration Errors!") elif config.get("strict", False) and warnings: # pragma: no cover print(warnings) raise _mkdocs_exceptions.ConfigurationError( f"Aborted with {len(warnings)} Configuration Warnings in 'strict' mode!" ) config_instance.config_file_path = config["config_file_path"] return config_instance
def load_file(self, config_file): """ Load config options from the open file descriptor of a YAML file. """ try: return self.load_dict(utils.yaml_load(config_file)) except YAMLError as e: # MkDocs knows and understands ConfigurationErrors raise exceptions.ConfigurationError( f"MkDocs encountered an error parsing the configuration file: {e}" )
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_dict(self, patch): """ Load config options from a dictionary. """ 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: " f"{type(patch)}") self.user_configs.append(patch) self.data.update(patch)
def yaml_load(source, loader=None): """ Return dict of source YAML file using loader, recursively deep merging inherited parent. """ Loader = loader or get_yaml_loader() result = yaml.load(source, Loader=Loader) if result is not None and 'INHERIT' in result: relpath = result.pop('INHERIT') abspath = os.path.normpath(os.path.join(os.path.dirname(source.name), relpath)) if not os.path.exists(abspath): raise exceptions.ConfigurationError( f"Inherited config file '{relpath}' does not exist at '{abspath}'.") log.debug(f"Loading inherited configuration file: {abspath}") with open(abspath, 'rb') as fd: parent = yaml_load(fd, Loader) result = merge(parent, result) return result
def _open_config_file(config_file): """ A context manager which yields an open file descriptor ready to be read. Accepts a filename as a string, an open or closed file descriptor, or None. When None, it defaults to `mkdocs.yml` in the CWD. If a closed file descriptor is received, a new file descriptor is opened for the same file. The file descriptor is automatically closed when the context manager block is existed. """ # Default to the standard config filename. if config_file is None: paths_to_try = ['mkdocs.yml', 'mkdocs.yaml'] # If it is a string, we can assume it is a path and attempt to open it. elif isinstance(config_file, str): paths_to_try = [config_file] # If closed file descriptor, get file path to reopen later. elif getattr(config_file, 'closed', False): paths_to_try = [config_file.name] else: paths_to_try = None if paths_to_try: # config_file is not a file descriptor, so open it as a path. for path in paths_to_try: path = os.path.abspath(path) log.debug(f"Loading configuration file: {path}") try: config_file = open(path, 'rb') break except FileNotFoundError: continue else: raise exceptions.ConfigurationError( f"Config file '{paths_to_try[0]}' does not exist.") else: log.debug(f"Loading configuration file: {config_file}") # Ensure file descriptor is at beginning config_file.seek(0) try: yield config_file finally: if hasattr(config_file, 'close'): config_file.close()
def _open_config_file(config_file): # Default to the standard config filename. if config_file is None: config_file = os.path.abspath('mkdocs.yml') log.debug("Loading configuration file: %s", config_file) # If it is a string, we can assume it is a path and attempt to open it. if isinstance(config_file, utils.string_types): if os.path.exists(config_file): config_file = open(config_file, 'rb') else: raise exceptions.ConfigurationError( "Config file '{0}' does not exist.".format(config_file)) return config_file
def get_themes(): """ Return a dict of all installed themes as (name, entry point) pairs. """ themes = {} builtins = pkg_resources.get_entry_map(dist='mkdocs', group='mkdocs.themes') for theme in pkg_resources.iter_entry_points(group='mkdocs.themes'): if theme.name in builtins and theme.dist.key != 'mkdocs': raise exceptions.ConfigurationError( "The theme {0} is a builtin theme but {1} provides a theme " "with the same name".format(theme.name, theme.dist.key)) elif theme.name in themes: multiple_packages = [themes[theme.name].dist.key, theme.dist.key] log.warning("The theme %s is provided by the Python packages " "'%s'. The one in %s will be used.", theme.name, ','.join(multiple_packages), theme.dist.key) themes[theme.name] = theme return themes
def get_themes(): """ Return a dict of all installed themes as {name: EntryPoint}. """ themes = {} eps = set(importlib_metadata.entry_points(group='mkdocs.themes')) builtins = {ep.name for ep in eps if ep.dist.name == 'mkdocs'} for theme in eps: if theme.name in builtins and theme.dist.name != 'mkdocs': raise exceptions.ConfigurationError( f"The theme '{theme.name}' is a builtin theme but the package '{theme.dist.name}' " "attempts to provide a theme with the same name.") elif theme.name in themes: log.warning( f"A theme named '{theme.name}' is provided by the Python packages '{theme.dist.name}' " f"and '{themes[theme.name].dist.name}'. The one in '{theme.dist.name}' will be used." ) themes[theme.name] = theme return themes
def _generate_site_navigation(pages_config, url_context, use_dir_urls=True): """ Returns a list of Page and Header instances that represent the top level site navigation. """ nav_items = [] pages = [] previous = None for config_line in pages_config: for page_or_header in _follow( config_line, url_context, use_dir_urls): if isinstance(page_or_header, Header): if page_or_header.is_top_level: nav_items.append(page_or_header) elif isinstance(page_or_header, Page): if page_or_header.is_top_level: nav_items.append(page_or_header) pages.append(page_or_header) if previous: page_or_header.previous_page = previous previous.next_page = page_or_header previous = page_or_header if len(pages) == 0: raise exceptions.ConfigurationError( "No pages found in the pages config. " "Remove it entirely to enable automatic page discovery.") return (nav_items, pages)
def _open_config_file(config_file): """ A context manager which yields an open file descriptor ready to be read. Accepts a filename as a string, an open or closed file descriptor, or None. When None, it defaults to `mkdocs.yml` in the CWD. If a closed file descriptor is received, a new file descriptor is opened for the same file. The file descriptor is automaticaly closed when the context manager block is existed. """ # 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(f"Loading configuration file: {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( f"Config file '{config_file}' does not exist.") # Ensure file descriptor is at begining config_file.seek(0) try: yield config_file finally: if hasattr(config_file, 'close'): config_file.close()
def _generate_site_navigation(pages_config, url_context, use_directory_urls=True): """ Returns a list of Page and Header instances that represent the top level site navigation. """ nav_items = [] pages = [] previous = None for config_line in pages_config: if isinstance(config_line, str): path = os.path.normpath(config_line) title, child_title = None, None elif len(config_line) in (1, 2, 3): # Pad any items that don't exist with 'None' padded_config = (list(config_line) + [None, None])[:3] path, title, child_title = padded_config path = os.path.normpath(path) else: msg = ("Line in 'page' config contained %d items. " "Expected 1, 2 or 3 strings." % len(config_line)) raise exceptions.ConfigurationError(msg) # If both the title and child_title are None, then we # have just been given a path. If that path contains a / # then lets automatically nest it. if title is None and child_title is None and os.path.sep in path: filename = path.split(os.path.sep)[-1] child_title = filename_to_title(filename) if title is None: filename = path.split(os.path.sep)[0] title = filename_to_title(filename) # If we don't have a child title but the other title is the same, we # should be within a section and the child title needs to be inferred # from the filename. if len(nav_items) and title == nav_items[ -1].title == title and child_title is None: filename = path.split(os.path.sep)[-1] child_title = filename_to_title(filename) url = utils.get_url_path(path, use_directory_urls) if not child_title: # New top level page. page = Page(title=title, url=url, path=path, url_context=url_context) nav_items.append(page) elif not nav_items or (nav_items[-1].title != title): # New second level page. page = Page(title=child_title, url=url, path=path, url_context=url_context) header = Header(title=title, children=[page]) nav_items.append(header) page.ancestors = [header] else: # Additional second level page. page = Page(title=child_title, url=url, path=path, url_context=url_context) header = nav_items[-1] header.children.append(page) page.ancestors = [header] # Add in previous and next information. if previous: page.previous_page = previous previous.next_page = page previous = page pages.append(page) return (nav_items, pages)
def _follow(config_line, url_context, use_dir_urls, header=None, title=None): if isinstance(config_line, utils.string_types): path = os.path.normpath(config_line) page = _path_to_page(path, title, url_context, use_dir_urls) if header: page.ancestors = header.ancestors + [ header, ] header.children.append(page) yield page raise StopIteration elif not isinstance(config_line, dict): msg = ("Line in 'page' config is of type {0}, dict or string " "expected. Config: {1}").format(type(config_line), config_line) raise exceptions.ConfigurationError(msg) if len(config_line) > 1: raise exceptions.ConfigurationError( "Page configs should be in the format 'name: markdown.md'. The " "config contains an invalid entry: {0}".format(config_line)) elif len(config_line) == 0: log.warning("Ignoring empty line in the pages config.") raise StopIteration next_cat_or_title, subpages_or_path = next(iter(config_line.items())) if isinstance(subpages_or_path, utils.string_types): path = subpages_or_path for sub in _follow(path, url_context, use_dir_urls, header=header, title=next_cat_or_title): yield sub raise StopIteration elif not isinstance(subpages_or_path, list): msg = ("Line in 'page' config is of type {0}, list or string " "expected for sub pages. Config: {1}").format( type(config_line), config_line) raise exceptions.ConfigurationError(msg) subpages = subpages_or_path if len(subpages) and isinstance( subpages[0], utils.string_types) and subpages[0].endswith('index.md'): # The first child is an index page. Use it as the URL of the header. path = os.path.normpath(subpages.pop(0)) url = utils.get_url_path(path, use_dir_urls) next_header = HeaderPage(next_cat_or_title, url, path, url_context, children=[]) else: next_header = Header(title=next_cat_or_title, children=[]) if header: next_header.ancestors = [header] header.children.append(next_header) yield next_header for subpage in subpages: for sub in _follow(subpage, url_context, use_dir_urls, next_header): yield sub