From 21b3d1d431f8a9bef0450c21ee5dc60c8242c8cd Mon Sep 17 00:00:00 2001 From: Waylan Limberg Date: Mon, 5 Apr 2021 18:48:59 -0400 Subject: [PATCH] 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. --- docs/about/release-notes.md | 4 + mkdocs/config/__init__.py | 4 +- mkdocs/config/base.py | 4 +- mkdocs/config/defaults.py | 162 ++++++++++++++-------------- mkdocs/tests/base.py | 2 +- mkdocs/tests/config/base_tests.py | 4 +- mkdocs/tests/config/config_tests.py | 25 ++++- 7 files changed, 113 insertions(+), 92 deletions(-) diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md index cf40ab99..e6c373a3 100644 --- a/docs/about/release-notes.md +++ b/docs/about/release-notes.md @@ -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 diff --git a/mkdocs/config/__init__.py b/mkdocs/config/__init__.py index 3f8314f3..b9780de6 100644 --- a/mkdocs/config/__init__.py +++ b/mkdocs/config/__init__.py @@ -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__] diff --git a/mkdocs/config/base.py b/mkdocs/config/base.py index 6ef3c39e..1b68cdd9 100644 --- a/mkdocs/config/base.py +++ b/mkdocs/config/base.py @@ -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. diff --git a/mkdocs/config/defaults.py b/mkdocs/config/defaults.py index 103e5fa1..7ba8acb6 100644 --- a/mkdocs/config/defaults.py +++ b/mkdocs/config/defaults.py @@ -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 `/index.hmtl` style files with hyperlinks to - # the directory.If `False`, use `.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 `/index.hmtl` style files with hyperlinks to + # the directory.If `False`, use `.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'])), + ) diff --git a/mkdocs/tests/base.py b/mkdocs/tests/base.py index 1ea373af..c335ad68 100644 --- a/mkdocs/tests/base.py +++ b/mkdocs/tests/base.py @@ -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() diff --git a/mkdocs/tests/config/base_tests.py b/mkdocs/tests/config/base_tests.py index 6bfc8bc6..d5265309 100644 --- a/mkdocs/tests/config/base_tests.py +++ b/mkdocs/tests/config/base_tests.py @@ -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() diff --git a/mkdocs/tests/config/config_tests.py b/mkdocs/tests/config/config_tests.py index 8d562dc5..19429f1d 100644 --- a/mkdocs/tests/config/config_tests.py +++ b/mkdocs/tests/config/config_tests.py @@ -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'))