Repository: mkdocs/mkdocs

Issue:
Setting `InclusionLevel.NOT_IN_NAV` still adds pages to `nav`
[As discussed on Gitter](https://app.gitter.im/#/room/#mkdocs_community:gitter.im/$MBc6guuJOmcU1MUR2U_H3arjzqK4tXClo_ncOpCo-pI), we should consider excluding pages from auto-populated navigation if they are tagged as `NOT_IN_NAV` within a plugin as part of `on_files`. Currently, only pages tagged with `EXCLUDED` are not considered. The solution that @oprypin suggested was to simply change:

https://github.com/mkdocs/mkdocs/blob/79f17b4b71c73460c304e3281f6ff209788a76bf/mkdocs/structure/nav.py#L128

... to:

``` python
documentation_pages = files.documentation_pages(inclusion = InclusionLevel.is_in_nav)
```

[Current workaround](https://app.gitter.im/#/room/#mkdocs_community:gitter.im/$GaQ_vIed9_hW4OBuqtTdHg4SWhNYMXpDHtRUI11QrNU):

1. Set pages that should not be auto-populated to `InclusionLevel.EXCLUDED` in `on_files`
2. Set pages that were previously excluded to `InclusionLevel.NOT_IN_NAV` in `on_nav` (after population)


Discussion:

Resolving patch:
diff --git a/mkdocs/commands/build.py b/mkdocs/commands/build.py
--- a/mkdocs/commands/build.py
+++ b/mkdocs/commands/build.py
@@ -13,7 +13,7 @@
 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
--- a/mkdocs/structure/files.py
+++ b/mkdocs/structure/files.py
@@ -322,7 +322,7 @@ def is_css(self) -> bool:
 _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
--- 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 @@ def __repr__(self):
 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
--- 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 @@ def test_nav_from_nested_files(self):
         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