Repository: mkdocs/mkdocs
Pull request #3443 (2023-10-28 15:43:21)

Apply planned changes to `File.page` and `not_in_nav`
- Error when setting File.page to a type other than Page
  As per the prior plan https://github.com/mkdocs/mkdocs/pull/3381#issuecomment-1712750515

- When `nav` config is not specified, apply file inclusion
  to decide whether a file in included when generating the implicit nav.
  Fixes https://github.com/mkdocs/mkdocs/issues/3336



Patch:
diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md
index 9c6eff4e..eaccea55 100644
--- a/docs/user-guide/configuration.md
+++ b/docs/user-guide/configuration.md
@@ -333,7 +333,9 @@ exclude_docs: |
 
 NEW: **New in version 1.5.**
 
-NOTE: This option does *not* actually exclude anything from the nav.
+> NEW: **New in version 1.6:**
+>
+> If the [`nav`](#nav) config is not specified at all, pages specified in this config will now be excluded from the inferred navigation.
 
 If you want to include some docs into the site but intentionally exclude them from the nav, normally MkDocs warns about this.
 
diff --git a/mkdocs/commands/build.py b/mkdocs/commands/build.py
index e8af54fb..4a21a3da 100644
--- a/mkdocs/commands/build.py
+++ b/mkdocs/commands/build.py
@@ -13,7 +13,7 @@ from jinja2.exceptions import TemplateNotFound
 import mkdocs
 from mkdocs import utils
 from mkdocs.exceptions import Abort, BuildError
-from mkdocs.structure.files import File, Files, InclusionLevel, _set_exclusions, get_files
+from mkdocs.structure.files import File, Files, InclusionLevel, get_files, set_exclusions
 from mkdocs.structure.nav import Navigation, get_navigation
 from mkdocs.structure.pages import Page
 from mkdocs.utils import DuplicateFilter  # noqa: F401 - legacy re-export
@@ -294,7 +294,7 @@ def build(
         # Run `files` plugin events.
         files = config.plugins.on_files(files, config=config)
         # If plugins have added files but haven't set their inclusion level, calculate it again.
-        _set_exclusions(files._files, config)
+        set_exclusions(files._files, config)
 
         nav = get_navigation(files, config)
 
@@ -305,8 +305,8 @@ def build(
         excluded = []
         for file in files.documentation_pages(inclusion=inclusion):
             log.debug(f"Reading: {file.src_uri}")
-            if file.page is None and file.inclusion.is_excluded():
-                if live_server:
+            if file.page is None and file.inclusion.is_not_in_nav():
+                if live_server and file.inclusion.is_excluded():
                     excluded.append(urljoin(live_server.url, file.url))
                 Page(None, file, config)
             assert file.page is not None
diff --git a/mkdocs/structure/files.py b/mkdocs/structure/files.py
index b1c4df5a..01d2f639 100644
--- a/mkdocs/structure/files.py
+++ b/mkdocs/structure/files.py
@@ -322,7 +322,7 @@ class File:
 _default_exclude = pathspec.gitignore.GitIgnoreSpec.from_lines(['.*', '/templates/'])
 
 
-def _set_exclusions(files: Iterable[File], config: MkDocsConfig) -> None:
+def set_exclusions(files: Iterable[File], config: MkDocsConfig) -> None:
     """Re-calculate which files are excluded, based on the patterns in the config."""
     exclude: pathspec.gitignore.GitIgnoreSpec | None = config.get('exclude_docs')
     exclude = _default_exclude + exclude if exclude else _default_exclude
@@ -362,7 +362,7 @@ def get_files(config: MkDocsConfig) -> Files:
             files.append(file)
             prev_file = file
 
-    _set_exclusions(files, config)
+    set_exclusions(files, config)
     # Skip README.md if an index file also exists in dir (part 2)
     for a, b in conflicting_files:
         if b.inclusion.is_included():
diff --git a/mkdocs/structure/nav.py b/mkdocs/structure/nav.py
index 25d3e97b..d122c821 100644
--- a/mkdocs/structure/nav.py
+++ b/mkdocs/structure/nav.py
@@ -1,10 +1,10 @@
 from __future__ import annotations
 
 import logging
-import warnings
 from typing import TYPE_CHECKING, Iterator, TypeVar
 from urllib.parse import urlsplit
 
+from mkdocs.exceptions import BuildError
 from mkdocs.structure import StructureItem
 from mkdocs.structure.pages import Page
 from mkdocs.utils import nest_paths
@@ -129,7 +129,9 @@ class Link(StructureItem):
 def get_navigation(files: Files, config: MkDocsConfig) -> Navigation:
     """Build site navigation from config and files."""
     documentation_pages = files.documentation_pages()
-    nav_config = config['nav'] or nest_paths(f.src_uri for f in documentation_pages)
+    nav_config = config['nav'] or nest_paths(
+        f.src_uri for f in documentation_pages if f.inclusion.is_in_nav()
+    )
     items = _data_to_navigation(nav_config, files, config)
     if not isinstance(items, list):
         items = [items]
@@ -202,20 +204,9 @@ def _data_to_navigation(data, files: Files, config: MkDocsConfig):
             )
         page = file.page
         if page is not None:
-            if isinstance(page, Page):
-                if type(page) is not Page:  # Strict subclass
-                    return page
-                warnings.warn(
-                    "A plugin has set File.page to an instance of Page and it got overwritten. "
-                    "The behavior of this will change in MkDocs 1.6.",
-                    DeprecationWarning,
-                )
-            else:
-                warnings.warn(  # type: ignore[unreachable]
-                    "A plugin has set File.page to a type other than Page. "
-                    "This will be an error in MkDocs 1.6.",
-                    DeprecationWarning,
-                )
+            if not isinstance(page, Page):
+                raise BuildError("A plugin has set File.page to a type other than Page.")
+            return page
         return Page(title, file, config)
     return Link(title, path)
 


Test patch:
diff --git a/mkdocs/tests/structure/nav_tests.py b/mkdocs/tests/structure/nav_tests.py
index 44ae2adc..04846111 100644
--- a/mkdocs/tests/structure/nav_tests.py
+++ b/mkdocs/tests/structure/nav_tests.py
@@ -3,7 +3,7 @@
 import sys
 import unittest
 
-from mkdocs.structure.files import File, Files
+from mkdocs.structure.files import File, Files, set_exclusions
 from mkdocs.structure.nav import Section, _get_by_type, get_navigation
 from mkdocs.structure.pages import Page
 from mkdocs.tests.base import dedent, load_config
@@ -378,6 +378,35 @@ class SiteNavigationTests(unittest.TestCase):
         self.assertEqual(len(site_navigation.pages), 7)
         self.assertEqual(repr(site_navigation.homepage), "Page(title=[blank], url='/')")
 
+    def test_nav_with_exclusion(self):
+        expected = dedent(
+            """
+            Page(title=[blank], url='index.html')
+            Section(title='About')
+                Page(title=[blank], url='about/license.html')
+                Page(title=[blank], url='about/release-notes.html')
+            Section(title='Api guide')
+                Page(title=[blank], url='api-guide/running.html')
+                Page(title=[blank], url='api-guide/testing.html')
+            """
+        )
+        cfg = load_config(use_directory_urls=False, not_in_nav='*ging.md\n/foo.md\n')
+        fs = [
+            'index.md',
+            'foo.md',
+            'about/license.md',
+            'about/release-notes.md',
+            'api-guide/debugging.md',
+            'api-guide/running.md',
+            'api-guide/testing.md',
+        ]
+        files = Files([File(s, cfg.docs_dir, cfg.site_dir, cfg.use_directory_urls) for s in fs])
+        set_exclusions(files, 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), 5)
+
     def test_nav_page_subclass(self):
         class PageSubclass(Page):
             pass