1#!/usr/bin/env python
2
3import os
4import tempfile
5import unittest
6from tempfile import TemporaryDirectory
7
8import mkdocs
9from mkdocs import config
10from mkdocs.config import config_options
11from mkdocs.config import defaults
12from mkdocs.exceptions import ConfigurationError
13from mkdocs.localization import parse_locale
14from mkdocs.tests.base import dedent
15
16
17class ConfigTests(unittest.TestCase):
18    def test_missing_config_file(self):
19
20        def load_missing_config():
21            config.load_config(config_file='bad_filename.yaml')
22        self.assertRaises(ConfigurationError, load_missing_config)
23
24    def test_missing_site_name(self):
25        c = config.Config(schema=defaults.get_schema())
26        c.load_dict({})
27        errors, warnings = c.validate()
28        self.assertEqual(len(errors), 1)
29        self.assertEqual(errors[0][0], 'site_name')
30        self.assertEqual(str(errors[0][1]), 'Required configuration not provided.')
31
32        self.assertEqual(len(warnings), 0)
33
34    def test_empty_config(self):
35        def load_empty_config():
36            config.load_config(config_file='/dev/null')
37        self.assertRaises(ConfigurationError, load_empty_config)
38
39    def test_nonexistant_config(self):
40        def load_empty_config():
41            config.load_config(config_file='/path/that/is/not/real')
42        self.assertRaises(ConfigurationError, load_empty_config)
43
44    def test_invalid_config(self):
45        file_contents = dedent("""
46        - ['index.md', 'Introduction']
47        - ['index.md', 'Introduction']
48        - ['index.md', 'Introduction']
49        """)
50        config_file = tempfile.NamedTemporaryFile('w', delete=False)
51        try:
52            config_file.write(file_contents)
53            config_file.flush()
54            config_file.close()
55
56            self.assertRaises(
57                ConfigurationError,
58                config.load_config, config_file=open(config_file.name, 'rb')
59            )
60        finally:
61            os.remove(config_file.name)
62
63    def test_config_option(self):
64        """
65        Users can explicitly set the config file using the '--config' option.
66        Allows users to specify a config other than the default `mkdocs.yml`.
67        """
68        expected_result = {
69            'site_name': 'Example',
70            'pages': [
71                {'Introduction': 'index.md'}
72            ],
73        }
74        file_contents = dedent("""
75        site_name: Example
76        pages:
77        - 'Introduction': 'index.md'
78        """)
79        with TemporaryDirectory() as temp_path:
80            os.mkdir(os.path.join(temp_path, 'docs'))
81            config_path = os.path.join(temp_path, 'mkdocs.yml')
82            config_file = open(config_path, 'w')
83
84            config_file.write(file_contents)
85            config_file.flush()
86            config_file.close()
87
88            result = config.load_config(config_file=config_file.name)
89            self.assertEqual(result['site_name'], expected_result['site_name'])
90            self.assertEqual(result['pages'], expected_result['pages'])
91
92    def test_theme(self):
93        with TemporaryDirectory() as mytheme, TemporaryDirectory() as custom:
94            configs = [
95                dict(),  # default theme
96                {"theme": "readthedocs"},  # builtin theme
97                {"theme": {'name': 'readthedocs'}},  # builtin as complex
98                {"theme": {'name': None, 'custom_dir': mytheme}},  # custom only as complex
99                {"theme": {'name': 'readthedocs', 'custom_dir': custom}},  # builtin and custom as complex
100                {  # user defined variables
101                    'theme': {
102                        'name': 'mkdocs',
103                        'locale': 'fr',
104                        'static_templates': ['foo.html'],
105                        'show_sidebar': False,
106                        'some_var': 'bar'
107                    }
108                }
109            ]
110
111            mkdocs_dir = os.path.abspath(os.path.dirname(mkdocs.__file__))
112            mkdocs_templates_dir = os.path.join(mkdocs_dir, 'templates')
113            theme_dir = os.path.abspath(os.path.join(mkdocs_dir, 'themes'))
114
115            results = (
116                {
117                    'dirs': [os.path.join(theme_dir, 'mkdocs'), mkdocs_templates_dir],
118                    'static_templates': ['404.html', 'sitemap.xml'],
119                    'vars': {
120                        'locale': parse_locale('en'),
121                        'include_search_page': False,
122                        'search_index_only': False,
123                        'analytics': {'gtag': None},
124                        'highlightjs': True,
125                        'hljs_style': 'github',
126                        'hljs_languages': [],
127                        'navigation_depth': 2,
128                        'nav_style': 'primary',
129                        'shortcuts': {'help': 191, 'next': 78, 'previous': 80, 'search': 83}
130                    }
131                }, {
132                    'dirs': [os.path.join(theme_dir, 'readthedocs'), mkdocs_templates_dir],
133                    'static_templates': ['404.html', 'sitemap.xml'],
134                    'vars': {
135                        'locale': parse_locale('en'),
136                        'include_search_page': True,
137                        'search_index_only': False,
138                        'analytics': {'gtag': None},
139                        'highlightjs': True,
140                        'hljs_languages': [],
141                        'include_homepage_in_sidebar': True,
142                        'prev_next_buttons_location': 'bottom',
143                        'navigation_depth': 4,
144                        'sticky_navigation': True,
145                        'titles_only': False,
146                        'collapse_navigation': True
147                    }
148                }, {
149                    'dirs': [os.path.join(theme_dir, 'readthedocs'), mkdocs_templates_dir],
150                    'static_templates': ['404.html', 'sitemap.xml'],
151                    'vars': {
152                        'locale': parse_locale('en'),
153                        'include_search_page': True,
154                        'search_index_only': False,
155                        'analytics': {'gtag': None},
156                        'highlightjs': True,
157                        'hljs_languages': [],
158                        'include_homepage_in_sidebar': True,
159                        'prev_next_buttons_location': 'bottom',
160                        'navigation_depth': 4,
161                        'sticky_navigation': True,
162                        'titles_only': False,
163                        'collapse_navigation': True
164                    }
165                }, {
166                    'dirs': [mytheme, mkdocs_templates_dir],
167                    'static_templates': ['sitemap.xml'],
168                    'vars': {'locale': parse_locale('en')}
169                }, {
170                    'dirs': [custom, os.path.join(theme_dir, 'readthedocs'), mkdocs_templates_dir],
171                    'static_templates': ['404.html', 'sitemap.xml'],
172                    'vars': {
173                        'locale': parse_locale('en'),
174                        'include_search_page': True,
175                        'search_index_only': False,
176                        'analytics': {'gtag': None},
177                        'highlightjs': True,
178                        'hljs_languages': [],
179                        'include_homepage_in_sidebar': True,
180                        'prev_next_buttons_location': 'bottom',
181                        'navigation_depth': 4,
182                        'sticky_navigation': True,
183                        'titles_only': False,
184                        'collapse_navigation': True
185                    }
186                }, {
187                    'dirs': [os.path.join(theme_dir, 'mkdocs'), mkdocs_templates_dir],
188                    'static_templates': ['404.html', 'sitemap.xml', 'foo.html'],
189                    'vars': {
190                        'locale': parse_locale('fr'),
191                        'show_sidebar': False,
192                        'some_var': 'bar',
193                        'include_search_page': False,
194                        'search_index_only': False,
195                        'analytics': {'gtag': None},
196                        'highlightjs': True,
197                        'hljs_style': 'github',
198                        'hljs_languages': [],
199                        'navigation_depth': 2,
200                        'nav_style': 'primary',
201                        'shortcuts': {'help': 191, 'next': 78, 'previous': 80, 'search': 83}
202                    }
203                }
204            )
205
206            for config_contents, result in zip(configs, results):
207
208                c = config.Config(schema=(('theme', config_options.Theme(default='mkdocs')),))
209                c.load_dict(config_contents)
210                errors, warnings = c.validate()
211                self.assertEqual(len(errors), 0)
212                self.assertEqual(c['theme'].dirs, result['dirs'])
213                self.assertEqual(c['theme'].static_templates, set(result['static_templates']))
214                self.assertEqual({k: c['theme'][k] for k in iter(c['theme'])}, result['vars'])
215
216    def test_empty_nav(self):
217        conf = config.Config(schema=defaults.get_schema())
218        conf.load_dict({
219            'site_name': 'Example',
220            'config_file_path': os.path.join(os.path.abspath('.'), 'mkdocs.yml')
221        })
222        conf.validate()
223        self.assertEqual(conf['nav'], None)
224
225    def test_copy_pages_to_nav(self):
226        # TODO: remove this when pages config setting is fully deprecated.
227        conf = config.Config(schema=defaults.get_schema())
228        conf.load_dict({
229            'site_name': 'Example',
230            'pages': ['index.md', 'about.md'],
231            'config_file_path': os.path.join(os.path.abspath('.'), 'mkdocs.yml')
232        })
233        conf.validate()
234        self.assertEqual(conf['nav'], ['index.md', 'about.md'])
235
236    def test_dont_overwrite_nav_with_pages(self):
237        # TODO: remove this when pages config setting is fully deprecated.
238        conf = config.Config(schema=defaults.get_schema())
239        conf.load_dict({
240            'site_name': 'Example',
241            'pages': ['index.md', 'about.md'],
242            'nav': ['foo.md', 'bar.md'],
243            'config_file_path': os.path.join(os.path.abspath('.'), 'mkdocs.yml')
244        })
245        conf.validate()
246        self.assertEqual(conf['nav'], ['foo.md', 'bar.md'])
247
248    def test_doc_dir_in_site_dir(self):
249
250        j = os.path.join
251
252        test_configs = (
253            {'docs_dir': j('site', 'docs'), 'site_dir': 'site'},
254            {'docs_dir': 'docs', 'site_dir': '.'},
255            {'docs_dir': '.', 'site_dir': '.'},
256            {'docs_dir': 'docs', 'site_dir': ''},
257            {'docs_dir': '', 'site_dir': ''},
258            {'docs_dir': 'docs', 'site_dir': 'docs'},
259        )
260
261        conf = {
262            'config_file_path': j(os.path.abspath('..'), 'mkdocs.yml')
263        }
264
265        for test_config in test_configs:
266
267            patch = conf.copy()
268            patch.update(test_config)
269
270            # Same as the default schema, but don't verify the docs_dir exists.
271            c = config.Config(schema=(
272                ('docs_dir', config_options.Dir(default='docs')),
273                ('site_dir', config_options.SiteDir(default='site')),
274                ('config_file_path', config_options.Type(str))
275            ))
276            c.load_dict(patch)
277
278            errors, warnings = c.validate()
279
280            self.assertEqual(len(errors), 1)
281            self.assertEqual(warnings, [])
282
283    def testConfigInstancesUnique(self):
284        conf = mkdocs.config.Config(mkdocs.config.defaults.get_schema())
285        conf.load_dict({'site_name': 'foo'})
286        conf.validate()
287        self.assertIsNone(conf['mdx_configs'].get('toc'))
288
289        conf = mkdocs.config.Config(mkdocs.config.defaults.get_schema())
290        conf.load_dict({'site_name': 'foo', 'markdown_extensions': [{"toc": {"permalink": "aaa"}}]})
291        conf.validate()
292        self.assertEqual(conf['mdx_configs'].get('toc'), {'permalink': 'aaa'})
293
294        conf = mkdocs.config.Config(mkdocs.config.defaults.get_schema())
295        conf.load_dict({'site_name': 'foo'})
296        conf.validate()
297        self.assertIsNone(conf['mdx_configs'].get('toc'))
298