Ensure each instance of Config is unique

Replace the global variable `mkdocs.config.DEFAULT_SCHEMA` with
the function `mkdocs.config.defaults.get_schema()`. An instance is no
longer created on import (eliminating circular imports under certain
circumstances) and each call to `get_schema()` builds a new instance
of each object. Fixes #2289.
This commit is contained in:
Waylan Limberg
2021-04-05 18:48:59 -04:00
committed by GitHub
parent a118183f5e
commit 21b3d1d431
7 changed files with 113 additions and 92 deletions

View File

@@ -108,6 +108,10 @@ option. (#2092).
The `mkdocs` theme now removes the sidebar when printing a page. This frees
up horizontal space for better rendering of content like tables (#2193).
The `mkdocs.config.DEFAULT_SCHEMA` global variable has been replaced with the
function `mkdocs.config.defaults.get_schema()`, which ensures that each
instance of the configuration is unique (#2289).
### Other Changes and Additions to Version 1.2
* Bugfix: Properly process navigation child items in `_get_by_type` when

View File

@@ -1,6 +1,4 @@
from mkdocs.config.base import load_config, Config
from mkdocs.config.defaults import DEFAULT_SCHEMA
__all__ = [load_config.__name__,
Config.__name__,
'DEFAULT_SCHEMA']
Config.__name__]

View File

@@ -187,8 +187,8 @@ def load_config(config_file=None, **kwargs):
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'])
from mkdocs.config.defaults import get_schema
cfg = Config(schema=get_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.

View File

@@ -8,107 +8,109 @@ from mkdocs.config import config_options
# isn't really needed either as we always sequentially process the schema other
# than at initialisation when we grab the full set of keys for convenience.
DEFAULT_SCHEMA = (
# Reserved for internal use, stores the mkdocs.yml config file.
('config_file_path', config_options.Type(str)),
def get_schema():
return (
# The title to use for the documentation
('site_name', config_options.Type(str, required=True)),
# Reserved for internal use, stores the mkdocs.yml config file.
('config_file_path', config_options.Type(str)),
# Defines the structure of the navigation.
('nav', config_options.Nav()),
# TODO: remove this when the `pages` config setting is fully deprecated.
('pages', config_options.Nav()),
# The title to use for the documentation
('site_name', config_options.Type(str, required=True)),
# The full URL to where the documentation will be hosted
('site_url', config_options.URL()),
# Defines the structure of the navigation.
('nav', config_options.Nav()),
# TODO: remove this when the `pages` config setting is fully deprecated.
('pages', config_options.Nav()),
# A description for the documentation project that will be added to the
# HTML meta tags.
('site_description', config_options.Type(str)),
# The name of the author to add to the HTML meta tags
('site_author', config_options.Type(str)),
# The full URL to where the documentation will be hosted
('site_url', config_options.URL()),
# The MkDocs theme for the documentation.
('theme', config_options.Theme(default='mkdocs')),
# A description for the documentation project that will be added to the
# HTML meta tags.
('site_description', config_options.Type(str)),
# The name of the author to add to the HTML meta tags
('site_author', config_options.Type(str)),
# The directory containing the documentation markdown.
('docs_dir', config_options.Dir(default='docs', exists=True)),
# The MkDocs theme for the documentation.
('theme', config_options.Theme(default='mkdocs')),
# The directory where the site will be built to
('site_dir', config_options.SiteDir(default='site')),
# The directory containing the documentation markdown.
('docs_dir', config_options.Dir(default='docs', exists=True)),
# A copyright notice to add to the footer of documentation.
('copyright', config_options.Type(str)),
# The directory where the site will be built to
('site_dir', config_options.SiteDir(default='site')),
# set of values for Google analytics containing the account IO and domain,
# this should look like, ['UA-27795084-5', 'mkdocs.org']
('google_analytics', config_options.Type(list, length=2)),
# A copyright notice to add to the footer of documentation.
('copyright', config_options.Type(str)),
# The address on which to serve the live reloading docs server.
('dev_addr', config_options.IpAddress(default='127.0.0.1:8000')),
# set of values for Google analytics containing the account IO and domain,
# this should look like, ['UA-27795084-5', 'mkdocs.org']
('google_analytics', config_options.Type(list, length=2)),
# If `True`, use `<page_name>/index.hmtl` style files with hyperlinks to
# the directory.If `False`, use `<page_name>.html style file with
# hyperlinks to the file.
# True generates nicer URLs, but False is useful if browsing the output on
# a filesystem.
('use_directory_urls', config_options.Type(bool, default=True)),
# The address on which to serve the live reloading docs server.
('dev_addr', config_options.IpAddress(default='127.0.0.1:8000')),
# Specify a link to the project source repo to be included
# in the documentation pages.
('repo_url', config_options.RepoURL()),
# If `True`, use `<page_name>/index.hmtl` style files with hyperlinks to
# the directory.If `False`, use `<page_name>.html style file with
# hyperlinks to the file.
# True generates nicer URLs, but False is useful if browsing the output on
# a filesystem.
('use_directory_urls', config_options.Type(bool, default=True)),
# A name to use for the link to the project source repo.
# Default, If repo_url is unset then None, otherwise
# "GitHub", "Bitbucket" or "GitLab" for known url or Hostname
# for unknown urls.
('repo_name', config_options.Type(str)),
# Specify a link to the project source repo to be included
# in the documentation pages.
('repo_url', config_options.RepoURL()),
# Specify a URI to the docs dir in the project source repo, relative to the
# repo_url. When set, a link directly to the page in the source repo will
# be added to the generated HTML. If repo_url is not set also, this option
# is ignored.
('edit_uri', config_options.Type(str)),
# A name to use for the link to the project source repo.
# Default, If repo_url is unset then None, otherwise
# "GitHub", "Bitbucket" or "GitLab" for known url or Hostname
# for unknown urls.
('repo_name', config_options.Type(str)),
# Specify which css or javascript files from the docs directory should be
# additionally included in the site.
('extra_css', config_options.Type(list, default=[])),
('extra_javascript', config_options.Type(list, default=[])),
# Specify a URI to the docs dir in the project source repo, relative to the
# repo_url. When set, a link directly to the page in the source repo will
# be added to the generated HTML. If repo_url is not set also, this option
# is ignored.
('edit_uri', config_options.Type(str)),
# Similar to the above, but each template (HTML or XML) will be build with
# Jinja2 and the global context.
('extra_templates', config_options.Type(list, default=[])),
# Specify which css or javascript files from the docs directory should be
# additionally included in the site.
('extra_css', config_options.Type(list, default=[])),
('extra_javascript', config_options.Type(list, default=[])),
# PyMarkdown extension names.
('markdown_extensions', config_options.MarkdownExtensions(
builtins=['toc', 'tables', 'fenced_code'],
configkey='mdx_configs', default=[])),
# Similar to the above, but each template (HTML or XML) will be build with
# Jinja2 and the global context.
('extra_templates', config_options.Type(list, default=[])),
# PyMarkdown Extension Configs. For internal use only.
('mdx_configs', config_options.Private()),
# PyMarkdown extension names.
('markdown_extensions', config_options.MarkdownExtensions(
builtins=['toc', 'tables', 'fenced_code'],
configkey='mdx_configs', default=[])),
# enabling strict mode causes MkDocs to stop the build when a problem is
# encountered rather than display an error.
('strict', config_options.Type(bool, default=False)),
# PyMarkdown Extension Configs. For internal use only.
('mdx_configs', config_options.Private()),
# the remote branch to commit to when using gh-deploy
('remote_branch', config_options.Type(
str, default='gh-pages')),
# enabling strict mode causes MkDocs to stop the build when a problem is
# encountered rather than display an error.
('strict', config_options.Type(bool, default=False)),
# the remote name to push to when using gh-deploy
('remote_name', config_options.Type(str, default='origin')),
# the remote branch to commit to when using gh-deploy
('remote_branch', config_options.Type(
str, default='gh-pages')),
# extra is a mapping/dictionary of data that is passed to the template.
# This allows template authors to require extra configuration that not
# relevant to all themes and doesn't need to be explicitly supported by
# MkDocs itself. A good example here would be including the current
# project version.
('extra', config_options.SubConfig()),
# the remote name to push to when using gh-deploy
('remote_name', config_options.Type(str, default='origin')),
# a list of plugins. Each item may contain a string name or a key value pair.
# A key value pair should be the string name (as the key) and a dict of config
# options (as the value).
('plugins', config_options.Plugins(default=['search'])),
)
# extra is a mapping/dictionary of data that is passed to the template.
# This allows template authors to require extra configuration that not
# relevant to all themes and doesn't need to be explicitly supported by
# MkDocs itself. A good example here would be including the current
# project version.
('extra', config_options.SubConfig()),
# a list of plugins. Each item may contain a string name or a key value pair.
# A key value pair should be the string name (as the key) and a dict of config
# options (as the value).
('plugins', config_options.Plugins(default=['search'])),
)

View File

@@ -32,7 +32,7 @@ def load_config(**cfg):
if 'docs_dir' not in cfg:
# Point to an actual dir to avoid a 'does not exist' error on validation.
cfg['docs_dir'] = os.path.join(path_base, 'docs')
conf = config.Config(schema=config.DEFAULT_SCHEMA, config_file_path=cfg['config_file_path'])
conf = config.Config(schema=config.defaults.get_schema(), config_file_path=cfg['config_file_path'])
conf.load_dict(cfg)
errors_warnings = conf.validate()

View File

@@ -12,7 +12,7 @@ class ConfigBaseTests(unittest.TestCase):
def test_unrecognised_keys(self):
c = base.Config(schema=defaults.DEFAULT_SCHEMA)
c = base.Config(schema=defaults.get_schema())
c.load_dict({
'not_a_valid_config_option': "test"
})
@@ -26,7 +26,7 @@ class ConfigBaseTests(unittest.TestCase):
def test_missing_required(self):
c = base.Config(schema=defaults.DEFAULT_SCHEMA)
c = base.Config(schema=defaults.get_schema())
errors, warnings = c.validate()

View File

@@ -8,6 +8,7 @@ from tempfile import TemporaryDirectory
import mkdocs
from mkdocs import config
from mkdocs.config import config_options
from mkdocs.config import defaults
from mkdocs.exceptions import ConfigurationError
from mkdocs.tests.base import dedent
@@ -20,7 +21,7 @@ class ConfigTests(unittest.TestCase):
self.assertRaises(ConfigurationError, load_missing_config)
def test_missing_site_name(self):
c = config.Config(schema=config.DEFAULT_SCHEMA)
c = config.Config(schema=defaults.get_schema())
c.load_dict({})
errors, warnings = c.validate()
self.assertEqual(len(errors), 1)
@@ -201,7 +202,7 @@ class ConfigTests(unittest.TestCase):
self.assertEqual({k: c['theme'][k] for k in iter(c['theme'])}, result['vars'])
def test_empty_nav(self):
conf = config.Config(schema=config.DEFAULT_SCHEMA)
conf = config.Config(schema=defaults.get_schema())
conf.load_dict({
'site_name': 'Example',
'config_file_path': os.path.join(os.path.abspath('.'), 'mkdocs.yml')
@@ -211,7 +212,7 @@ class ConfigTests(unittest.TestCase):
def test_copy_pages_to_nav(self):
# TODO: remove this when pages config setting is fully deprecated.
conf = config.Config(schema=config.DEFAULT_SCHEMA)
conf = config.Config(schema=defaults.get_schema())
conf.load_dict({
'site_name': 'Example',
'pages': ['index.md', 'about.md'],
@@ -222,7 +223,7 @@ class ConfigTests(unittest.TestCase):
def test_dont_overwrite_nav_with_pages(self):
# TODO: remove this when pages config setting is fully deprecated.
conf = config.Config(schema=config.DEFAULT_SCHEMA)
conf = config.Config(schema=defaults.get_schema())
conf.load_dict({
'site_name': 'Example',
'pages': ['index.md', 'about.md'],
@@ -266,3 +267,19 @@ class ConfigTests(unittest.TestCase):
self.assertEqual(len(errors), 1)
self.assertEqual(warnings, [])
def testConfigInstancesUnique(self):
conf = mkdocs.config.Config(mkdocs.config.defaults.get_schema())
conf.load_dict({'site_name': 'foo'})
conf.validate()
self.assertIsNone(conf['mdx_configs'].get('toc'))
conf = mkdocs.config.Config(mkdocs.config.defaults.get_schema())
conf.load_dict({'site_name': 'foo', 'markdown_extensions': [{"toc": {"permalink": "aaa"}}]})
conf.validate()
self.assertEqual(conf['mdx_configs'].get('toc'), {'permalink': 'aaa'})
conf = mkdocs.config.Config(mkdocs.config.defaults.get_schema())
conf.load_dict({'site_name': 'foo'})
conf.validate()
self.assertIsNone(conf['mdx_configs'].get('toc'))