예제 #1
0
 def test_context_base_url_homepage_use_directory_urls(self):
     nav_cfg = [
         {'Home': 'index.md'}
     ]
     cfg = load_config(nav=nav_cfg)
     files = Files([
         File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
     ])
     nav = get_navigation(files, cfg)
     context = build.get_context(nav, files, cfg, nav.pages[0])
     self.assertEqual(context['base_url'], '.')
예제 #2
0
 def test_build_page_dirty_not_modified(self, site_dir, mock_write_file):
     cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
     files = Files([File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
     nav = get_navigation(files, cfg)
     page = files.documentation_pages()[0].page
     # Fake populate page
     page.title = 'Title'
     page.markdown = 'page content'
     page.content = '<p>page content</p>'
     build._build_page(page, cfg, files, nav, cfg['theme'].get_env(), dirty=True)
     mock_write_file.assert_called_once()
예제 #3
0
 def test_build_page(self, site_dir):
     cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
     files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
     nav = get_navigation(files, cfg)
     page = files.documentation_pages()[0].page
     # Fake populate page
     page.title = 'Title'
     page.markdown = 'page content'
     page.content = '<p>page content</p>'
     build._build_page(page, cfg, files, nav, cfg['theme'].get_env())
     self.assertPathIsFile(site_dir, 'index.html')
예제 #4
0
 def test_context_base_url_nested_page(self):
     nav_cfg = [
         {'Home': 'index.md'},
         {'Nested': 'foo/bar.md'}
     ]
     cfg = load_config(nav=nav_cfg, use_directory_urls=False)
     files = Files([
         File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
         File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
     ])
     nav = get_navigation(files, cfg)
     context = build.get_context(nav, files, cfg, nav.pages[1])
     self.assertEqual(context['base_url'], '..')
예제 #5
0
 def test_build_page_error(self, site_dir, mock_write_file):
     cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
     files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
     nav = get_navigation(files, cfg)
     page = files.documentation_pages()[0].page
     # Fake populate page
     page.title = 'Title'
     page.markdown = 'page content'
     page.content = '<p>page content</p>'
     with self.assertLogs('mkdocs', level='ERROR') as cm:
         self.assertRaises(IOError, build._build_page, page, cfg, files, nav, cfg['theme'].get_env())
     self.assertEqual(
         cm.output,
         ["ERROR:mkdocs.commands.build:Error building page 'index.md': Error message."]
     )
     mock_write_file.assert_called_once()
예제 #6
0
 def test_context_extra_css_js_from_homepage(self):
     nav_cfg = [
         {'Home': 'index.md'}
     ]
     cfg = load_config(
         nav=nav_cfg,
         extra_css=['style.css'],
         extra_javascript=['script.js'],
         use_directory_urls=False
     )
     files = Files([
         File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
     ])
     nav = get_navigation(files, cfg)
     context = build.get_context(nav, files, cfg, nav.pages[0])
     self.assertEqual(context['extra_css'], ['style.css'])
     self.assertEqual(context['extra_javascript'], ['script.js'])
예제 #7
0
 def test_nav_no_title(self):
     nav_cfg = [
         'test.md',
         {'About': 'about.md'}
     ]
     expected = dedent("""
     Page(title=[blank], url='/')
     Page(title='About', url='/about/')
     """)
     cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
     files = Files([
         File(nav_cfg[0], cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
         File(nav_cfg[1]['About'], cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
     ])
     site_navigation = get_navigation(files, cfg)
     self.assertEqual(str(site_navigation).strip(), expected)
     self.assertEqual(len(site_navigation.items), 2)
     self.assertEqual(len(site_navigation.pages), 2)
예제 #8
0
 def test_nav_no_directory_urls(self):
     nav_cfg = [{'Home': 'index.md'}, {'About': 'about.md'}]
     expected = dedent("""
     Page(title='Home', url='/index.html')
     Page(title='About', url='/about.html')
     """)
     cfg = load_config(nav=nav_cfg,
                       use_directory_urls=False,
                       site_url='http://example.com/')
     files = Files([
         File(
             list(item.values())[0], cfg['docs_dir'], cfg['site_dir'],
             cfg['use_directory_urls']) for item in nav_cfg
     ])
     site_navigation = get_navigation(files, cfg)
     self.assertEqual(str(site_navigation).strip(), expected)
     self.assertEqual(len(site_navigation.items), 2)
     self.assertEqual(len(site_navigation.pages), 2)
예제 #9
0
 def test_nav_missing_page(self):
     nav_cfg = [{'Home': 'index.md'}]
     expected = dedent("""
     Page(title='Home', url='/')
     """)
     cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
     files = Files([
         File('index.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls']),
         File('page_not_in_nav.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls'])
     ])
     site_navigation = get_navigation(files, cfg)
     self.assertEqual(str(site_navigation).strip(), expected)
     self.assertEqual(len(site_navigation.items), 1)
     self.assertEqual(len(site_navigation.pages), 1)
     for file in files:
         self.assertIsInstance(file.page, Page)
예제 #10
0
 def test_context_extra_css_js_from_nested_page_use_directory_urls(self):
     nav_cfg = [
         {'Home': 'index.md'},
         {'Nested': 'foo/bar.md'}
     ]
     cfg = load_config(
         nav=nav_cfg,
         extra_css=['style.css'],
         extra_javascript=['script.js']
     )
     files = Files([
         File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
         File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
     ])
     nav = get_navigation(files, cfg)
     context = build.get_context(nav, files, cfg, nav.pages[1])
     self.assertEqual(context['extra_css'], ['../../style.css'])
     self.assertEqual(context['extra_javascript'], ['../../script.js'])
예제 #11
0
 def test_nav_from_files(self):
     expected = dedent("""
     Page(title=[blank], url='/')
     Page(title=[blank], url='/about/')
     """)
     cfg = load_config(site_url='http://example.com/')
     files = Files([
         File('index.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls']),
         File('about.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls'])
     ])
     site_navigation = get_navigation(files, cfg)
     self.assertEqual(str(site_navigation).strip(), expected)
     self.assertEqual(len(site_navigation.items), 2)
     self.assertEqual(len(site_navigation.pages), 2)
     self.assertEqual(repr(site_navigation.homepage),
                      "Page(title=[blank], url='/')")
예제 #12
0
 def test_build_page_error(self, site_dir, mock_write_file):
     cfg = load_config(site_dir=site_dir, nav=['test.md'], plugins=[])
     files = Files([
         File('test.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls'])
     ])
     nav = get_navigation(files, cfg)
     page = files.documentation_pages()[0].page
     # Fake populate page
     page.title = 'Title'
     page.markdown = 'page content'
     page.content = '<p>page content</p>'
     with self.assertLogs('mkdocs', level='ERROR') as cm:
         self.assertRaises(IOError, build._build_page, page, cfg, files,
                           nav, cfg['theme'].get_env())
     self.assertEqual(cm.output, [
         "ERROR:mkdocs.commands.build:Error building page 'test.md': Error message."
     ])
     mock_write_file.assert_called_once()
예제 #13
0
 def test_build_page_dirty_not_modified(self, site_dir, mock_write_file):
     cfg = load_config(site_dir=site_dir, nav=['test.md'], plugins=[])
     files = Files([
         File('testing.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls'])
     ])
     nav = get_navigation(files, cfg)
     page = files.documentation_pages()[0].page
     # Fake populate page
     page.title = 'Title'
     page.markdown = 'page content'
     page.content = '<p>page content</p>'
     build._build_page(page,
                       cfg,
                       files,
                       nav,
                       cfg['theme'].get_env(),
                       dirty=True)
     mock_write_file.assert_called_once()
예제 #14
0
    def test_nested_ungrouped_nav_no_titles(self):
        nav_cfg = ['index.md', 'about/contact.md', 'about/sub/license.md']
        expected = dedent("""
        Page(title=[blank], url='/')
        Page(title=[blank], url='/about/contact/')
        Page(title=[blank], url='/about/sub/license/')
        """)

        cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
        files = Files([
            File(item, cfg['docs_dir'], cfg['site_dir'],
                 cfg['use_directory_urls']) for item in nav_cfg
        ])
        site_navigation = get_navigation(files, cfg)
        self.assertEqual(str(site_navigation).strip(), expected)
        self.assertEqual(len(site_navigation.items), 3)
        self.assertEqual(len(site_navigation.pages), 3)
        self.assertEqual(repr(site_navigation.homepage),
                         "Page(title=[blank], url='/')")
예제 #15
0
 def test_nested_ungrouped_nav(self):
     nav_cfg = [
         {'Home': 'test.md'},
         {'Contact': 'about/contact.md'},
         {'License Title': 'about/sub/license.md'},
     ]
     expected = dedent("""
     Page(title='Home', url='/')
     Page(title='Contact', url='/about/contact/')
     Page(title='License Title', url='/about/sub/license/')
     """)
     cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
     files = Files(
         [File(list(item.values())[0], cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
          for item in nav_cfg]
     )
     site_navigation = get_navigation(files, cfg)
     self.assertEqual(str(site_navigation).strip(), expected)
     self.assertEqual(len(site_navigation.items), 3)
     self.assertEqual(len(site_navigation.pages), 3)
예제 #16
0
    def on_nav(self, nav: Navigation, config: Config, files: Files) -> Navigation:
        """
        Handler for nav event.

        Builds navigation for translated pages.
        For each language creates it's own navigation.

        :param nav: current navigation.
        :param config: project config.
        :param files: all project files.
        :return: new Navigation.
        """
        if self.i18navs:
            return nav
        for language, lang_files in self.i18pages.items():
            logger.debug(f"Building navigation for {language}")
            self.i18navs[language] = get_navigation(lang_files, config)
            self.i18navs[language].homepage = deepcopy(
                self._get_lang_homepage(language),
            )
        return nav
예제 #17
0
 def test_nav_external_links(self):
     nav_cfg = [{
         'Home': 'index.md'
     }, {
         'Local': '/local.html'
     }, {
         'External': 'http://example.com/external.html'
     }]
     expected = dedent("""
     Page(title='Home', url='/')
     Link(title='Local', url='/local.html')
     Link(title='External', url='http://example.com/external.html')
     """)
     cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
     files = Files([
         File('index.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls'])
     ])
     site_navigation = get_navigation(files, cfg)
     self.assertEqual(str(site_navigation).strip(), expected)
     self.assertEqual(len(site_navigation.items), 3)
     self.assertEqual(len(site_navigation.pages), 1)
 def test_build_page_dirty_modified(self, site_dir, docs_dir,
                                    mock_write_file):
     cfg = load_config(docs_dir=docs_dir,
                       site_dir=site_dir,
                       nav=['index.md'],
                       plugins=[])
     files = Files([
         File('index.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls'])
     ])
     nav = get_navigation(files, cfg)
     page = files.documentation_pages()[0].page
     # Fake populate page
     page.title = 'Title'
     page.markdown = 'new page content'
     page.content = '<p>new page content</p>'
     build._build_page(page,
                       cfg,
                       files,
                       nav,
                       self._get_env_with_null_translations(cfg),
                       dirty=True)
     mock_write_file.assert_not_called()
예제 #19
0
 def test_nav_from_nested_files(self):
     expected = dedent("""
     Page(title=[blank], url='/')
     Section(title='About')
         Page(title=[blank], url='/about/license/')
         Page(title=[blank], url='/about/release-notes/')
     Section(title='Api guide')
         Page(title=[blank], url='/api-guide/debugging/')
         Page(title=[blank], url='/api-guide/running/')
         Page(title=[blank], url='/api-guide/testing/')
         Section(title='Advanced')
             Page(title=[blank], url='/api-guide/advanced/part-1/')
     """)
     cfg = load_config(site_url='http://example.com/')
     files = Files([
         File('index.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls']),
         File('about/license.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls']),
         File('about/release-notes.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls']),
         File('api-guide/debugging.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls']),
         File('api-guide/running.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls']),
         File('api-guide/testing.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls']),
         File('api-guide/advanced/part-1.md', cfg['docs_dir'],
              cfg['site_dir'], cfg['use_directory_urls']),
     ])
     site_navigation = get_navigation(files, cfg)
     self.assertEqual(str(site_navigation).strip(), expected)
     self.assertEqual(len(site_navigation.items), 3)
     self.assertEqual(len(site_navigation.pages), 7)
     self.assertEqual(repr(site_navigation.homepage),
                      "Page(title=[blank], url='/')")
예제 #20
0
def nav(config, plugin, files):
    nav = get_navigation(files, config)
    nav = plugin.on_nav(nav, config)
    return nav
예제 #21
0
 def test_active(self):
     nav_cfg = [{
         'Home': 'index.md'
     }, {
         'API Guide': [
             {
                 'Running': 'api-guide/running.md'
             },
             {
                 'Testing': 'api-guide/testing.md'
             },
             {
                 'Debugging': 'api-guide/debugging.md'
             },
             {
                 'Advanced': [
                     {
                         'Part 1': 'api-guide/advanced/part-1.md'
                     },
                 ]
             },
         ]
     }, {
         'About': [{
             'Release notes': 'about/release-notes.md'
         }, {
             'License': 'about/license.md'
         }]
     }]
     cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
     files = Files([
         File('index.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls']),
         File('api-guide/running.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls']),
         File('api-guide/testing.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls']),
         File('api-guide/debugging.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls']),
         File('api-guide/advanced/part-1.md', cfg['docs_dir'],
              cfg['site_dir'], cfg['use_directory_urls']),
         File('about/release-notes.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls']),
         File('about/license.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls']),
     ])
     site_navigation = get_navigation(files, cfg)
     # Confirm nothing is active
     self.assertTrue(
         all(page.active is False for page in site_navigation.pages))
     self.assertTrue(
         all(item.active is False for item in site_navigation.items))
     # Activate
     site_navigation.items[1].children[3].children[0].active = True
     # Confirm ancestors are activated
     self.assertTrue(
         site_navigation.items[1].children[3].children[0].active)
     self.assertTrue(site_navigation.items[1].children[3].active)
     self.assertTrue(site_navigation.items[1].active)
     # Confirm non-ancestors are not activated
     self.assertFalse(site_navigation.items[0].active)
     self.assertFalse(site_navigation.items[1].children[0].active)
     self.assertFalse(site_navigation.items[1].children[1].active)
     self.assertFalse(site_navigation.items[1].children[2].active)
     self.assertFalse(site_navigation.items[2].active)
     self.assertFalse(site_navigation.items[2].children[0].active)
     self.assertFalse(site_navigation.items[2].children[1].active)
     # Deactivate
     site_navigation.items[1].children[3].children[0].active = False
     # Confirm ancestors are deactivated
     self.assertFalse(
         site_navigation.items[1].children[3].children[0].active)
     self.assertFalse(site_navigation.items[1].children[3].active)
     self.assertFalse(site_navigation.items[1].active)
예제 #22
0
파일: build.py 프로젝트: yang-lile/mkdocs
def build(config, live_server=False, dirty=False):
    """ Perform a full site build. """
    try:

        from time import time
        start = time()

        # Run `config` plugin events.
        config = config['plugins'].run_event('config', config)

        # Run `pre_build` plugin events.
        config['plugins'].run_event('pre_build', config=config)

        if not dirty:
            log.info("Cleaning site directory")
            utils.clean_directory(config['site_dir'])
        else:  # pragma: no cover
            # Warn user about problems that may occur with --dirty option
            log.warning(
                "A 'dirty' build is being performed, this will likely lead to inaccurate navigation and other"
                " links within your site. This option is designed for site development purposes only."
            )

        if not live_server:  # pragma: no cover
            log.info("Building documentation to directory: %s",
                     config['site_dir'])
            if dirty and site_directory_contains_stale_files(
                    config['site_dir']):
                log.info(
                    "The directory contains stale files. Use --clean to remove them."
                )

        # First gather all data from all files/pages to ensure all data is consistent across all pages.

        files = get_files(config)
        env = config['theme'].get_env()
        files.add_files_from_theme(env, config)

        # Run `files` plugin events.
        files = config['plugins'].run_event('files', files, config=config)

        nav = get_navigation(files, config)

        # Run `nav` plugin events.
        nav = config['plugins'].run_event('nav',
                                          nav,
                                          config=config,
                                          files=files)

        log.debug("Reading markdown pages.")
        for file in files.documentation_pages():
            log.debug("Reading: " + file.src_path)
            _populate_page(file.page, config, files, dirty)

        # Run `env` plugin events.
        env = config['plugins'].run_event('env',
                                          env,
                                          config=config,
                                          files=files)

        # Start writing files to site_dir now that all data is gathered. Note that order matters. Files
        # with lower precedence get written first so that files with higher precedence can overwrite them.

        log.debug("Copying static assets.")
        files.copy_static_files(dirty=dirty)

        for template in config['theme'].static_templates:
            _build_theme_template(template, env, files, config, nav)

        for template in config['extra_templates']:
            _build_extra_template(template, files, config, nav)

        log.debug("Building markdown pages.")
        doc_files = files.documentation_pages()
        for file in doc_files:
            _build_page(file.page, config, doc_files, nav, env, dirty)

        # Run `post_build` plugin events.
        config['plugins'].run_event('post_build', config=config)

        if config['strict'] and utils.warning_filter.count:
            raise SystemExit(
                '\nExited with {} warnings in strict mode.'.format(
                    utils.warning_filter.count))

        log.info('Documentation built in %.2f seconds', time() - start)

    except Exception as e:
        # Run `build_error` plugin events.
        config['plugins'].run_event('build_error', error=e)
        if isinstance(e, BuildError):
            raise SystemExit('\n' + str(e))
        raise
예제 #23
0
 def test_indented_nav(self):
     nav_cfg = [{
         'Home': 'index.md'
     }, {
         'API Guide': [
             {
                 'Running': 'api-guide/running.md'
             },
             {
                 'Testing': 'api-guide/testing.md'
             },
             {
                 'Debugging': 'api-guide/debugging.md'
             },
             {
                 'Advanced': [
                     {
                         'Part 1': 'api-guide/advanced/part-1.md'
                     },
                 ]
             },
         ]
     }, {
         'About': [{
             'Release notes': 'about/release-notes.md'
         }, {
             'License': '/license.html'
         }]
     }, {
         'External': 'https://example.com/'
     }]
     expected = dedent("""
     Page(title='Home', url='/')
     Section(title='API Guide')
         Page(title='Running', url='/api-guide/running/')
         Page(title='Testing', url='/api-guide/testing/')
         Page(title='Debugging', url='/api-guide/debugging/')
         Section(title='Advanced')
             Page(title='Part 1', url='/api-guide/advanced/part-1/')
     Section(title='About')
         Page(title='Release notes', url='/about/release-notes/')
         Link(title='License', url='/license.html')
     Link(title='External', url='https://example.com/')
     """)
     cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
     files = Files([
         File('index.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls']),
         File('api-guide/running.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls']),
         File('api-guide/testing.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls']),
         File('api-guide/debugging.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls']),
         File('api-guide/advanced/part-1.md', cfg['docs_dir'],
              cfg['site_dir'], cfg['use_directory_urls']),
         File('about/release-notes.md', cfg['docs_dir'], cfg['site_dir'],
              cfg['use_directory_urls']),
     ])
     site_navigation = get_navigation(files, cfg)
     self.assertEqual(str(site_navigation).strip(), expected)
     self.assertEqual(len(site_navigation.items), 4)
     self.assertEqual(len(site_navigation.pages), 6)
     self.assertEqual(repr(site_navigation.homepage),
                      "Page(title='Home', url='/')")
     self.assertIsNone(site_navigation.items[0].parent)
     self.assertEqual(site_navigation.items[0].ancestors, [])
     self.assertIsNone(site_navigation.items[1].parent)
     self.assertEqual(site_navigation.items[1].ancestors, [])
     self.assertEqual(len(site_navigation.items[1].children), 4)
     self.assertEqual(repr(site_navigation.items[1].children[0].parent),
                      "Section(title='API Guide')")
     self.assertEqual(site_navigation.items[1].children[0].ancestors,
                      [site_navigation.items[1]])
     self.assertEqual(repr(site_navigation.items[1].children[1].parent),
                      "Section(title='API Guide')")
     self.assertEqual(site_navigation.items[1].children[1].ancestors,
                      [site_navigation.items[1]])
     self.assertEqual(repr(site_navigation.items[1].children[2].parent),
                      "Section(title='API Guide')")
     self.assertEqual(site_navigation.items[1].children[2].ancestors,
                      [site_navigation.items[1]])
     self.assertEqual(repr(site_navigation.items[1].children[3].parent),
                      "Section(title='API Guide')")
     self.assertEqual(site_navigation.items[1].children[3].ancestors,
                      [site_navigation.items[1]])
     self.assertEqual(len(site_navigation.items[1].children[3].children), 1)
     self.assertEqual(
         repr(site_navigation.items[1].children[3].children[0].parent),
         "Section(title='Advanced')")
     self.assertEqual(
         site_navigation.items[1].children[3].children[0].ancestors,
         [site_navigation.items[1].children[3], site_navigation.items[1]])
     self.assertIsNone(site_navigation.items[2].parent)
     self.assertEqual(len(site_navigation.items[2].children), 2)
     self.assertEqual(repr(site_navigation.items[2].children[0].parent),
                      "Section(title='About')")
     self.assertEqual(site_navigation.items[2].children[0].ancestors,
                      [site_navigation.items[2]])
     self.assertEqual(repr(site_navigation.items[2].children[1].parent),
                      "Section(title='About')")
     self.assertEqual(site_navigation.items[2].children[1].ancestors,
                      [site_navigation.items[2]])
     self.assertIsNone(site_navigation.items[3].parent)
     self.assertEqual(site_navigation.items[3].ancestors, [])
     self.assertIsNone(site_navigation.items[3].children)