Repository: mkdocs/mkdocs
Pull request #2934 (2022-08-15 21:56:12)

Move some documentation into code, add misc API docs page

pawamoy: Can't wait to see the live docs 🙂 
oprypin: @pawamoy Well...

I won't publish the docs for some time still, so anyway you'd have to see `mkdocs serve` for yourself for now.

And actually... at the moment it's a slight downgrade in terms of looks. (though mostly unchanged). I'd probably want a few features out of mkdocstrings to make it actually better. You'll hear from me soon. Or feel free to let me know when you want to discuss :D

But thanks to both for the enthusiasm 😊
oprypin: Ah, may as well publish it here, though:

https://oprypin.github.io/mkdocs/dev-guide/themes/#page
Before: https://www.mkdocs.org/dev-guide/themes/#page

And the new page:
https://oprypin.github.io/mkdocs/dev-guide/api/

Patch:
diff --git a/docs/css/extra.css b/docs/css/extra.css
index 3a015280..5b5f8290 100644
--- a/docs/css/extra.css
+++ b/docs/css/extra.css
@@ -30,10 +30,6 @@ dd {
     padding-left: 20px;
 }
 
-.doc-contents .field-body p:first-of-type {
-    display: inline;
-}
-
 .card-body svg {
     width: 100%;
     padding: 0 50px;
@@ -61,3 +57,30 @@ body.homepage>div.container>div.row>div.col-md-9 {
     flex: 0 0 100%;
     max-width: 100%;
 }
+
+/* mkdocstrings */
+
+.doc-contents .field-body p:first-of-type {
+    display: inline;
+}
+
+.doc-label-class-attribute {
+    display: none;
+}
+
+h2.doc-heading {
+    font-size: 1.5rem;
+}
+h3.doc-heading {
+    font-size: 1.4rem;
+}
+h4.doc-heading {
+    font-size: 1.3rem;
+}
+h5.doc-heading {
+    font-size: 1.2rem;
+}
+
+.doc-contents {
+    padding-left: 0;
+}
diff --git a/docs/dev-guide/api.md b/docs/dev-guide/api.md
new file mode 100644
index 00000000..0c1932dd
--- /dev/null
+++ b/docs/dev-guide/api.md
@@ -0,0 +1,19 @@
+# API reference
+
+NOTE: The main entry point to the API is through [Events](plugins.md#events) that are received by plugins. These events' descriptions link back to this page.
+
+::: mkdocs.structure.files.Files
+    options:
+      show_root_heading: true
+
+::: mkdocs.structure.files.File
+    options:
+      show_root_heading: true
+
+::: mkdocs.config.base.Config
+    options:
+      show_root_heading: true
+
+::: mkdocs.livereload.LiveReloadServer
+    options:
+      show_root_heading: true
diff --git a/docs/dev-guide/plugins.md b/docs/dev-guide/plugins.md
index e7952b32..b4598c2d 100644
--- a/docs/dev-guide/plugins.md
+++ b/docs/dev-guide/plugins.md
@@ -166,34 +166,50 @@ entire site.
 ##### on_serve
 
 ::: mkdocs.plugins.BasePlugin.on_serve
+    options:
+        show_root_heading: false
 
 ##### on_config
 
 ::: mkdocs.plugins.BasePlugin.on_config
+    options:
+        show_root_heading: false
 
 ##### on_pre_build
 
 ::: mkdocs.plugins.BasePlugin.on_pre_build
+    options:
+        show_root_heading: false
 
 ##### on_files
 
 ::: mkdocs.plugins.BasePlugin.on_files
+    options:
+        show_root_heading: false
 
 ##### on_nav
 
 ::: mkdocs.plugins.BasePlugin.on_nav
+    options:
+        show_root_heading: false
 
 ##### on_env
 
 ::: mkdocs.plugins.BasePlugin.on_env
+    options:
+        show_root_heading: false
 
 ##### on_post_build
 
 ::: mkdocs.plugins.BasePlugin.on_post_build
+    options:
+        show_root_heading: false
 
 ##### on_build_error
 
 ::: mkdocs.plugins.BasePlugin.on_build_error
+    options:
+        show_root_heading: false
 
 #### Template Events
 
@@ -205,14 +221,20 @@ called after the [env] event and before any [page events].
 ##### on_pre_template
 
 ::: mkdocs.plugins.BasePlugin.on_pre_template
+    options:
+        show_root_heading: false
 
 ##### on_template_context
 
 ::: mkdocs.plugins.BasePlugin.on_template_context
+    options:
+        show_root_heading: false
 
 ##### on_post_template
 
 ::: mkdocs.plugins.BasePlugin.on_post_template
+    options:
+        show_root_heading: false
 
 #### Page Events
 
@@ -223,26 +245,38 @@ page events are called after the [post_template] event and before the
 ##### on_pre_page
 
 ::: mkdocs.plugins.BasePlugin.on_pre_page
+    options:
+        show_root_heading: false
 
 ##### on_page_read_source
 
 ::: mkdocs.plugins.BasePlugin.on_page_read_source
+    options:
+        show_root_heading: false
 
 ##### on_page_markdown
 
 ::: mkdocs.plugins.BasePlugin.on_page_markdown
+    options:
+        show_root_heading: false
 
 ##### on_page_content
 
 ::: mkdocs.plugins.BasePlugin.on_page_content
+    options:
+        show_root_heading: false
 
 ##### on_page_context
 
 ::: mkdocs.plugins.BasePlugin.on_page_context
+    options:
+        show_root_heading: false
 
 ##### on_post_page
 
 ::: mkdocs.plugins.BasePlugin.on_post_page
+    options:
+        show_root_heading: false
 
 ### Handling Errors
 
diff --git a/docs/dev-guide/themes.md b/docs/dev-guide/themes.md
index 35ce220d..7c9c177f 100644
--- a/docs/dev-guide/themes.md
+++ b/docs/dev-guide/themes.md
@@ -195,17 +195,27 @@ defined by the [nav] configuration setting.
 
 [nav]: ../user-guide/configuration.md#nav
 
+::: mkdocs.structure.nav.Navigation
+    options:
+        show_root_heading: false
+        show_root_toc_entry: true
+        members: []
+        heading_level: 4
+
 In addition to the iterable of [navigation objects](#navigation-objects), the
 `nav` object contains the following attributes:
 
-##### nav.homepage
-
-The [page](#page) object for the homepage of the site.
+::: mkdocs.structure.nav.Navigation.homepage
+    options:
+        show_root_full_path: false
+        heading_level: 5
 
-##### nav.pages
+::: mkdocs.structure.nav.Navigation.pages
+    options:
+        show_root_full_path: false
+        heading_level: 5
 
-A flat list of all [page](#page) objects contained in the navigation. This list
-is not necessarily a complete list of all site pages as it does not contain
+This list is not necessarily a complete list of all site pages as it does not contain
 pages which are not included in the navigation. This list does match the list
 and order of pages used for all "next page" and "previous page" links. For a
 list of all pages, use the [pages](#pages) template variable.
@@ -271,25 +281,29 @@ the `page` variable contains a `page` object. The same `page` objects are used
 as `page` [navigation objects](#navigation-objects) in the global
 [navigation](#nav) and in the [pages](#pages) template variable.
 
-All `page` objects contain the following attributes:
-
-##### page.title
+::: mkdocs.structure.pages.Page
+    options:
+        show_root_heading: false
+        show_root_toc_entry: true
+        members: []
+        heading_level: 4
 
-Contains the Title for the current page.
-
-##### page.content
-
-The rendered Markdown as HTML, this is the contents of the documentation.
+All `page` objects contain the following attributes:
 
-##### page.toc
+::: mkdocs.structure.pages.Page.title
+    options:
+        show_root_full_path: false
+        heading_level: 5
 
-An iterable object representing the Table of contents for a page. Each item in
-the `toc` is an `AnchorLink` which contains the following attributes:
+::: mkdocs.structure.pages.Page.content
+    options:
+        show_root_full_path: false
+        heading_level: 5
 
-* `AnchorLink.title`: The text of the item.
-* `AnchorLink.url`: The hash fragment of a URL pointing to the item.
-* `AnchorLink.level`: The zero-based level of the item.
-* `AnchorLink.children`: An iterable of any child items.
+::: mkdocs.structure.pages.Page.toc
+    options:
+        show_root_full_path: false
+        heading_level: 5
 
 The following example would display the top two levels of the Table of Contents
 for a page.
@@ -305,10 +319,12 @@ for a page.
 </ul>
 ```
 
-##### page.meta
+::: mkdocs.structure.pages.Page.meta
+    options:
+        show_root_full_path: false
+        heading_level: 5
 
-A mapping of the metadata included at the top of the markdown page. In this
-example we define a `source` property above the page title.
+In this example we define a `source` property above the page title:
 
 ```no-highlight
 source: generics.py
@@ -331,53 +347,49 @@ documentation page.
 {% endfor %}
 ```
 
-##### page.url
+::: mkdocs.structure.pages.Page.url
+    options:
+        show_root_full_path: false
+        heading_level: 5
 
-The URL of the page relative to the MkDocs `site_dir`. It is expected that this
-be used with the [url](#url) filter to ensure the URL is relative to the current
+It is expected that this be used with the [url](#url) filter to ensure the URL is relative to the current
 page.
 
 ```django
 <a href="{{ page.url|url }}">{{ page.title }}</a>
 ```
 
-[base_url]: #base_url
-
-##### page.file
+::: mkdocs.structure.pages.Page.file
+    options:
+        show_root_full_path: false
+        heading_level: 5
 
-The documentation `File` that the page is being rendered from.
-
-##### page.abs_url
-
-The absolute URL of the page from the server root as determined by the value
-assigned to the [site_url] configuration setting. The value includes any
-subdirectory included in the `site_url`, but not the domain. [base_url] should
-not be used with this variable.
+::: mkdocs.structure.pages.Page.abs_url
+    options:
+        show_root_full_path: false
+        heading_level: 5
 
 For example, if `site_url: https://example.com/`, then the value of
 `page.abs_url` for the page `foo.md` would be `/foo/`. However, if
 `site_url: https://example.com/bar/`, then the value of `page.abs_url` for the
 page `foo.md` would be `/bar/foo/`.
 
-[site_url]: ../user-guide/configuration.md#site_url
-
-##### page.canonical_url
-
-The full, canonical URL to the current page as determined by the value assigned
-to the [site_url] configuration setting. The value includes the domain and any
-subdirectory included in the `site_url`. [base_url] should not be used with this
-variable.
+::: mkdocs.structure.pages.Page.canonical_url
+    options:
+        show_root_full_path: false
+        heading_level: 5
 
-##### page.edit_url
+::: mkdocs.structure.pages.Page.edit_url
+    options:
+        show_root_full_path: false
+        heading_level: 5
 
-The full URL to the source page in the source repository. Typically used to
-provide a link to edit the source page. [base_url] should not be used with this
-variable.
+::: mkdocs.structure.pages.Page.is_homepage
+    options:
+        show_root_full_path: false
+        heading_level: 5
 
-##### page.is_homepage
-
-Evaluates to `True` for the homepage of the site and `False` for all other
-pages. This can be used in conjunction with other attributes of the `page`
+This can be used in conjunction with other attributes of the `page`
 object to alter the behavior. For example, to display a different title
 on the homepage:
 
@@ -385,48 +397,53 @@ on the homepage:
 {% if not page.is_homepage %}{{ page.title }} - {% endif %}{{ site_name }}
 ```
 
-##### page.previous_page
-
-The page object for the previous page or `None`. The value will be `None` if the
-current page is the first item in the site navigation or if the current page is
-not included in the navigation at all. When the value is a page object, the
-usage is the same as for `page`.
-
-##### page.next_page
-
-The page object for the next page or `None`. The value will be `None` if the
-current page is the last item in the site navigation or if the current page is
-not included in the navigation at all. When the value is a page object, the
-usage is the same as for `page`.
-
-##### page.parent
-
-The immediate parent of the page in the [site navigation](#nav). `None` if the
-page is at the top level.
-
-##### page.children
-
-Pages do not contain children and the attribute is always `None`.
-
-##### page.active
-
-When `True`, indicates that this page is the currently viewed page. Defaults
-to `False`.
-
-##### page.is_section
-
-Indicates that the navigation object is a "section" object. Always `False` for
-page objects.
-
-##### page.is_page
-
-Indicates that the navigation object is a "page" object. Always `True` for
-page objects.
-
-##### page.is_link
-
-Indicates that the navigation object is a "link" object. Always `False` for
-page objects.
+::: mkdocs.structure.pages.Page.previous_page
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+::: mkdocs.structure.pages.Page.next_page
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+::: mkdocs.structure.pages.Page.parent
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+::: mkdocs.structure.pages.Page.children
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+::: mkdocs.structure.pages.Page.active
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+::: mkdocs.structure.pages.Page.is_section
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+::: mkdocs.structure.pages.Page.is_page
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+::: mkdocs.structure.pages.Page.is_link
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+#### AnchorLink
+
+::: mkdocs.structure.toc.AnchorLink
+    options:
+        show_root_heading: false
+        show_root_toc_entry: true
+        heading_level: 5
 
 ### Navigation Objects
 
@@ -447,83 +464,103 @@ URLs and are not links of any kind. However, by default, MkDocs sorts index
 pages to the top and the first child might be used as the URL for a section if a
 theme chooses to do so.
 
- The following attributes are available on `section` objects:
-
-##### section.title
-
-The title of the section.
-
-##### section.parent
-
-The immediate parent of the section or `None` if the section is at the top
-level.
-
-##### section.children
-
-An iterable of all child navigation objects. Children may include nested
-sections, pages and links.
-
-##### section.active
-
-When `True`, indicates that a child page of this section is the current page and
-can be used to highlight the section as the currently viewed section. Defaults
-to `False`.
-
-##### section.is_section
-
-Indicates that the navigation object is a "section" object. Always `True` for
-section objects.
-
-##### section.is_page
-
-Indicates that the navigation object is a "page" object. Always `False` for
-section objects.
-
-##### section.is_link
-
-Indicates that the navigation object is a "link" object. Always `False` for
-section objects.
+::: mkdocs.structure.nav.Section
+    options:
+        show_root_heading: false
+        show_root_toc_entry: true
+        members: []
+        heading_level: 4
+
+The following attributes are available on `section` objects:
+
+::: mkdocs.structure.nav.Section.title
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+::: mkdocs.structure.nav.Section.parent
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+::: mkdocs.structure.nav.Section.children
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+::: mkdocs.structure.nav.Section.active
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+::: mkdocs.structure.nav.Section.is_section
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+::: mkdocs.structure.nav.Section.is_page
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+::: mkdocs.structure.nav.Section.is_link
+    options:
+        show_root_full_path: false
+        heading_level: 5
 
 #### Link
 
 A `link` navigation object contains a link which does not point to an internal
-MkDocs page. The following attributes are available on `link` objects:
-
-##### link.title
-
-The title of the link. This would generally be used as the label of the link.
-
-##### link.url
-
-The URL that the link points to. The URL should always be an absolute URLs and
-should not need to have `base_url` prepended.
-
-##### link.parent
-
-The immediate parent of the link. `None` if the link is at the top level.
-
-##### link.children
-
-Links do not contain children and the attribute is always `None`.
-
-##### link.active
-
-External links cannot be "active" and the attribute is always `False`.
-
-##### link.is_section
-
-Indicates that the navigation object is a "section" object. Always `False` for
-link objects.
-
-##### link.is_page
-
-Indicates that the navigation object is a "page" object. Always `False` for
-link objects.
-
-##### link.is_link
-
-Indicates that the navigation object is a "link" object. Always `True` for
-link objects.
+MkDocs page.
+
+::: mkdocs.structure.nav.Link
+    options:
+        show_root_heading: false
+        show_root_toc_entry: true
+        members: []
+        heading_level: 4
+
+The following attributes are available on `link` objects:
+
+::: mkdocs.structure.nav.Link.title
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+::: mkdocs.structure.nav.Link.url
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+::: mkdocs.structure.nav.Link.parent
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+::: mkdocs.structure.nav.Link.children
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+::: mkdocs.structure.nav.Link.active
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+::: mkdocs.structure.nav.Link.is_section
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+::: mkdocs.structure.nav.Link.is_page
+    options:
+        show_root_full_path: false
+        heading_level: 5
+
+::: mkdocs.structure.nav.Link.is_link
+    options:
+        show_root_full_path: false
+        heading_level: 5
 
 ### Extra Context
 
diff --git a/mkdocs.yml b/mkdocs.yml
index 7b5e587c..4b505f4f 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -32,6 +32,7 @@ nav:
         - Themes: dev-guide/themes.md
         - Translations: dev-guide/translations.md
         - Plugins: dev-guide/plugins.md
+        - API Reference: dev-guide/api.md
     - About:
         - Release Notes: about/release-notes.md
         - Contributing: about/contributing.md
@@ -60,13 +61,16 @@ plugins:
             user-guide/plugins.md: dev-guide/plugins.md
             user-guide/custom-themes.md: dev-guide/themes.md
             user-guide/styling-your-docs.md: user-guide/choosing-your-theme.md
+    - autorefs
     - mkdocstrings:
         handlers:
           python:
             options:
               docstring_section_style: list
-              show_root_toc_entry: false
+              members_order: source
+              show_root_heading: true
               show_source: false
+              show_signature_annotations: true
 
 watch:
     - mkdocs
diff --git a/mkdocs/config/base.py b/mkdocs/config/base.py
index f01c107e..c15c69b0 100644
--- a/mkdocs/config/base.py
+++ b/mkdocs/config/base.py
@@ -3,6 +3,7 @@ import os
 import sys
 from collections import UserDict
 from contextlib import contextmanager
+from typing import IO, Optional, Sequence, Tuple
 
 from yaml import YAMLError
 
@@ -11,6 +12,44 @@ from mkdocs import exceptions, utils
 log = logging.getLogger('mkdocs.config')
 
 
+class BaseConfigOption:
+    def __init__(self):
+        self.warnings = []
+        self.default = None
+
+    def is_required(self):
+        return False
+
+    def validate(self, value):
+        return self.run_validation(value)
+
+    def reset_warnings(self):
+        self.warnings = []
+
+    def pre_validation(self, config, key_name):
+        """
+        Before all options are validated, perform a pre-validation process.
+
+        The pre-validation process method should be implemented by subclasses.
+        """
+
+    def run_validation(self, value):
+        """
+        Perform validation for a value.
+
+        The run_validation method should be implemented by subclasses.
+        """
+        return value
+
+    def post_validation(self, config, key_name):
+        """
+        After all options have passed validation, perform a post-validation
+        process to do any additional changes dependent on other config values.
+
+        The post-validation process method should be implemented by subclasses.
+        """
+
+
 class ValidationError(Exception):
     """Raised during the validation process of the config on errors."""
 
@@ -23,7 +62,9 @@ class Config(UserDict):
     for running validation on the structure and contents.
     """
 
-    def __init__(self, schema, config_file_path=None):
+    def __init__(
+        self, schema: Sequence[Tuple[str, BaseConfigOption]], config_file_path: Optional[str] = None
+    ):
         """
         The schema is a Python dict which maps the config name to a validator.
         """
@@ -43,7 +84,7 @@ class Config(UserDict):
         self.user_configs = []
         self.set_defaults()
 
-    def set_defaults(self):
+    def set_defaults(self) -> None:
         """
         Set the base config by going through each validator and getting the
         default if it has one.
@@ -116,7 +157,7 @@ class Config(UserDict):
 
         return failed, warnings
 
-    def load_dict(self, patch):
+    def load_dict(self, patch: dict) -> None:
         """Load config options from a dictionary."""
 
         if not isinstance(patch, dict):
@@ -129,7 +170,7 @@ class Config(UserDict):
         self.user_configs.append(patch)
         self.data.update(patch)
 
-    def load_file(self, config_file):
+    def load_file(self, config_file: IO) -> None:
         """Load config options from the open file descriptor of a YAML file."""
         try:
             return self.load_dict(utils.yaml_load(config_file))
diff --git a/mkdocs/config/config_options.py b/mkdocs/config/config_options.py
index 9f0108f5..7e38b647 100644
--- a/mkdocs/config/config_options.py
+++ b/mkdocs/config/config_options.py
@@ -9,57 +9,38 @@ from urllib.parse import urlsplit, urlunsplit
 import markdown
 
 from mkdocs import plugins, theme, utils
-from mkdocs.config.base import Config, ValidationError
+from mkdocs.config.base import BaseConfigOption, Config, ValidationError
 
 
-class BaseConfigOption:
-    def __init__(self):
-        self.warnings = []
-        self.default = None
-
-    def is_required(self):
-        return False
-
-    def validate(self, value):
-        return self.run_validation(value)
-
-    def reset_warnings(self):
-        self.warnings = []
-
-    def pre_validation(self, config, key_name):
-        """
-        Before all options are validated, perform a pre-validation process.
-
-        The pre-validation process method should be implemented by subclasses.
-        """
-
-    def run_validation(self, value):
-        """
-        Perform validation for a value.
-
-        The run_validation method should be implemented by subclasses.
-        """
-        return value
-
-    def post_validation(self, config, key_name):
-        """
-        After all options have passed validation, perform a post-validation
-        process to do any additional changes dependent on other config values.
-
-        The post-validation process method should be implemented by subclasses.
-        """
+class SubConfig(BaseConfigOption):
+    """
+    Subconfig Config Option
 
+    A set of `config_options` grouped under a single config option.
+    By default, validation errors and warnings resulting from validating
+    `config_options` are ignored (`validate=False`). Users should typically
+    enable validation with `validate=True`.
+    """
 
-class SubConfig(BaseConfigOption):
-    def __init__(self, *config_options):
+    def __init__(self, *config_options, validate=False):
         super().__init__()
         self.default = {}
         self.config_options = config_options
+        self._do_validation = validate
 
     def run_validation(self, value):
         config = Config(self.config_options)
         config.load_dict(value)
-        config.validate()
+        failed, warnings = config.validate()
+
+        if self._do_validation:
+            # Capture errors and warnings
+            self.warnings = warnings
+            if failed:
+                # Get the first failing one
+                key, err = failed[0]
+                raise ValidationError(f"Sub-option {key!r} configuration error: {err}")
+
         return config
 
 
diff --git a/mkdocs/livereload/__init__.py b/mkdocs/livereload/__init__.py
index 8f9b7bff..287cc147 100644
--- a/mkdocs/livereload/__init__.py
+++ b/mkdocs/livereload/__init__.py
@@ -13,6 +13,7 @@ import threading
 import time
 import warnings
 import wsgiref.simple_server
+from typing import Callable, Optional
 
 import watchdog.events
 import watchdog.observers.polling
@@ -92,7 +93,7 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe
 
         self._watched_paths = {}  # Used as an ordered set.
 
-    def watch(self, path, func=None, recursive=True):
+    def watch(self, path: str, func: Optional[Callable] = None, recursive: bool = True):
         """Add the 'path' to watched paths, call the function and reload when any file changes under it."""
         path = os.path.abspath(path)
         if func in (None, self.builder):
diff --git a/mkdocs/plugins.py b/mkdocs/plugins.py
index f66c6021..0f45236d 100644
--- a/mkdocs/plugins.py
+++ b/mkdocs/plugins.py
@@ -6,7 +6,7 @@ Implements the plugin API for MkDocs.
 
 import logging
 from collections import OrderedDict
-from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar
+from typing import Any, Callable, Dict, Optional, Tuple, TypeVar
 
 import importlib_metadata
 import jinja2.environment
@@ -20,27 +20,6 @@ from mkdocs.structure.pages import Page
 log = logging.getLogger('mkdocs.plugins')
 
 
-EVENTS = (
-    'config',
-    'pre_build',
-    'files',
-    'nav',
-    'env',
-    'pre_template',
-    'template_context',
-    'post_template',
-    'pre_page',
-    'page_read_source',
-    'page_markdown',
-    'page_content',
-    'page_context',
-    'post_page',
-    'post_build',
-    'serve',
-    'build_error',
-)
-
-
 def get_plugins():
     """Return a dict of all installed Plugins as {name: EntryPoint}."""
 
@@ -69,7 +48,7 @@ class BasePlugin:
 
     def load_config(
         self, options: Dict[str, Any], config_file_path: Optional[str] = None
-    ) -> Tuple[List[str], List[str]]:
+    ) -> Tuple[list, list]:
         """Load config from a dict of options. Returns a tuple of (errors, warnings)."""
 
         self.config = Config(schema=self.config_scheme, config_file_path=config_file_path)
@@ -77,10 +56,13 @@ class BasePlugin:
 
         return self.config.validate()
 
+    # (Note that event implementations shouldn't actually be static methods in subclasses)
+
     # Global events
 
+    @staticmethod
     def on_serve(
-        self, server: LiveReloadServer, config: Config, builder: Callable
+        server: LiveReloadServer, config: Config, builder: Callable
     ) -> Optional[LiveReloadServer]:
         """
         The `serve` event is only called when the `serve` command is used during
@@ -98,7 +80,8 @@ class BasePlugin:
         """
         return server
 
-    def on_config(self, config: Config) -> Optional[Config]:
+    @staticmethod
+    def on_config(config: Config) -> Optional[Config]:
         """
         The `config` event is the first event called on build and is run immediately
         after the user configuration is loaded and validated. Any alterations to the
@@ -112,7 +95,8 @@ class BasePlugin:
         """
         return config
 
-    def on_pre_build(self, config: Config) -> None:
+    @staticmethod
+    def on_pre_build(config: Config) -> None:
         """
         The `pre_build` event does not alter any variables. Use this event to call
         pre-build scripts.
@@ -121,7 +105,8 @@ class BasePlugin:
             config: global configuration object
         """
 
-    def on_files(self, files: Files, config: Config) -> Optional[Files]:
+    @staticmethod
+    def on_files(files: Files, config: Config) -> Optional[Files]:
         """
         The `files` event is called after the files collection is populated from the
         `docs_dir`. Use this event to add, remove, or alter files in the
@@ -138,7 +123,8 @@ class BasePlugin:
         """
         return files
 
-    def on_nav(self, nav: Navigation, config: Config, files: Files) -> Optional[Navigation]:
+    @staticmethod
+    def on_nav(nav: Navigation, config: Config, files: Files) -> Optional[Navigation]:
         """
         The `nav` event is called after the site navigation is created and can
         be used to alter the site navigation.
@@ -153,8 +139,9 @@ class BasePlugin:
         """
         return nav
 
+    @staticmethod
     def on_env(
-        self, env: jinja2.Environment, config: Config, files: Files
+        env: jinja2.Environment, config: Config, files: Files
     ) -> Optional[jinja2.Environment]:
         """
         The `env` event is called after the Jinja template environment is created
@@ -171,7 +158,8 @@ class BasePlugin:
         """
         return env
 
-    def on_post_build(self, config: Config) -> None:
+    @staticmethod
+    def on_post_build(config: Config) -> None:
         """
         The `post_build` event does not alter any variables. Use this event to call
         post-build scripts.
@@ -180,7 +168,8 @@ class BasePlugin:
             config: global configuration object
         """
 
-    def on_build_error(self, error: Exception) -> None:
+    @staticmethod
+    def on_build_error(error: Exception) -> None:
         """
         The `build_error` event is called after an exception of any kind
         is caught by MkDocs during the build process.
@@ -194,8 +183,9 @@ class BasePlugin:
 
     # Template events
 
+    @staticmethod
     def on_pre_template(
-        self, template: jinja2.Template, template_name: str, config: Config
+        template: jinja2.Template, template_name: str, config: Config
     ) -> Optional[jinja2.Template]:
         """
         The `pre_template` event is called immediately after the subject template is
@@ -211,8 +201,9 @@ class BasePlugin:
         """
         return template
 
+    @staticmethod
     def on_template_context(
-        self, context: Dict[str, Any], template_name: str, config: Config
+        context: Dict[str, Any], template_name: str, config: Config
     ) -> Optional[Dict[str, Any]]:
         """
         The `template_context` event is called immediately after the context is created
@@ -229,7 +220,8 @@ class BasePlugin:
         """
         return context
 
-    def on_post_template(self, output_content: str, template_name: str, config: Config) -> str:
+    @staticmethod
+    def on_post_template(output_content: str, template_name: str, config: Config) -> str:
         """
         The `post_template` event is called after the template is rendered, but before
         it is written to disc and can be used to alter the output of the template.
@@ -248,7 +240,8 @@ class BasePlugin:
 
     # Page events
 
-    def on_pre_page(self, page: Page, config: Config, files: Files) -> Optional[Page]:
+    @staticmethod
+    def on_pre_page(page: Page, config: Config, files: Files) -> Optional[Page]:
         """
         The `pre_page` event is called before any actions are taken on the subject
         page and can be used to alter the `Page` instance.
@@ -263,7 +256,8 @@ class BasePlugin:
         """
         return page
 
-    def on_page_read_source(self, page: Page, config: Config) -> Optional[str]:
+    @staticmethod
+    def on_page_read_source(page: Page, config: Config) -> Optional[str]:
         """
         The `on_page_read_source` event can replace the default mechanism to read
         the contents of a page's source from the filesystem.
@@ -278,9 +272,8 @@ class BasePlugin:
         """
         return None
 
-    def on_page_markdown(
-        self, markdown: str, page: Page, config: Config, files: Files
-    ) -> Optional[str]:
+    @staticmethod
+    def on_page_markdown(markdown: str, page: Page, config: Config, files: Files) -> Optional[str]:
         """
         The `page_markdown` event is called after the page's markdown is loaded
         from file and can be used to alter the Markdown source text. The meta-
@@ -297,7 +290,8 @@ class BasePlugin:
         """
         return markdown
 
-    def on_page_content(self, html: str, page: Page, config: Config, files: Files) -> Optional[str]:
+    @staticmethod
+    def on_page_content(html: str, page: Page, config: Config, files: Files) -> Optional[str]:
         """
         The `page_content` event is called after the Markdown text is rendered to
         HTML (but before being passed to a template) and can be used to alter the
@@ -314,8 +308,9 @@ class BasePlugin:
         """
         return html
 
+    @staticmethod
     def on_page_context(
-        self, context: Dict[str, Any], page: Page, config: Config, nav: Navigation
+        context: Dict[str, Any], page: Page, config: Config, nav: Navigation
     ) -> Optional[Dict[str, Any]]:
         """
         The `page_context` event is called after the context for a page is created
@@ -332,7 +327,8 @@ class BasePlugin:
         """
         return context
 
-    def on_post_page(self, output: str, page: Page, config: Config) -> str:
+    @staticmethod
+    def on_post_page(output: str, page: Page, config: Config) -> str:
         """
         The `post_page` event is called after the template is rendered, but
         before it is written to disc and can be used to alter the output of the
@@ -350,6 +346,8 @@ class BasePlugin:
         return output
 
 
+EVENTS = tuple(k[3:] for k in dir(BasePlugin) if k.startswith('on_'))
+
 T = TypeVar('T')
 
 
@@ -364,6 +362,11 @@ class PluginCollection(OrderedDict):
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
+        self.events = {k: [] for k in EVENTS}
+
+    def _register_event(self, event_name, method):
+        """Register a method for an event."""
+        self.events[event_name].append(method)
 
     def __setitem__(self, key: str, value: BasePlugin, **kwargs):
         if not isinstance(value, BasePlugin):
@@ -373,6 +376,11 @@ class PluginCollection(OrderedDict):
                 ' subclasses'
             )
         super().__setitem__(key, value, **kwargs)
+        # Register all of the event methods defined for this Plugin.
+        for event_name in (x for x in dir(value) if x.startswith('on_')):
+            method = getattr(value, event_name)
+            if callable(method) and method is not getattr(BasePlugin, event_name):
+                self._register_event(event_name[3:], method)
 
     def run_event(self, name: str, item: T = None, **kwargs) -> T:
         """
@@ -384,11 +392,8 @@ class PluginCollection(OrderedDict):
         be modified by the event method.
         """
 
-        name = 'on_' + name
         pass_item = item is not None
-        getattr(BasePlugin, name)  # just to produce AttributeError
-        for plugin in list(self.values()):  # can change size during iteration
-            method = getattr(plugin, name)
+        for method in self.events[name]:
             if pass_item:
                 result = method(item, **kwargs)
             else:
diff --git a/mkdocs/structure/files.py b/mkdocs/structure/files.py
index 0de55605..c2d3f3b5 100644
--- a/mkdocs/structure/files.py
+++ b/mkdocs/structure/files.py
@@ -3,81 +3,90 @@ import logging
 import os
 import posixpath
 from pathlib import PurePath
+from typing import Dict, Iterable, Iterator, List, Optional
 from urllib.parse import quote as urlquote
 
+import jinja2.environment
+
 from mkdocs import utils
+from mkdocs.config.base import Config
 
 log = logging.getLogger(__name__)
 
 
 class Files:
-    """A collection of File objects."""
+    """A collection of [File][mkdocs.structure.files.File] objects."""
 
-    def __init__(self, files):
+    def __init__(self, files: List['File']):
         self._files = files
         self._src_uris = None
 
-    def __iter__(self):
+    def __iter__(self) -> Iterator['File']:
+        """Iterate over the files within."""
         return iter(self._files)
 
-    def __len__(self):
+    def __len__(self) -> int:
+        """The number of files within."""
         return len(self._files)
 
-    def __contains__(self, path):
+    def __contains__(self, path: str) -> bool:
+        """Whether the file with this `src_uri` is in the collection."""
         return PurePath(path).as_posix() in self.src_uris
 
     @property
-    def src_paths(self):
-        """Soft-deprecated, prefer `.src_uris`."""
+    def src_paths(self) -> Dict[str, 'File']:
+        """Soft-deprecated, prefer `src_uris`."""
         return {file.src_path: file for file in self._files}
 
     @property
-    def src_uris(self):
+    def src_uris(self) -> Dict[str, 'File']:
+        """A mapping containing every file, with the keys being their
+        [`src_uri`][mkdocs.structure.files.File.src_uri]."""
         if self._src_uris is None:
             self._src_uris = {file.src_uri: file for file in self._files}
         return self._src_uris
 
-    def get_file_from_path(self, path):
+    def get_file_from_path(self, path: str) -> Optional['File']:
         """Return a File instance with File.src_uri equal to path."""
         return self.src_uris.get(PurePath(path).as_posix())
 
-    def append(self, file):
+    def append(self, file: 'File') -> None:
         """Append file to Files collection."""
         self._src_uris = None
         self._files.append(file)
 
-    def remove(self, file):
+    def remove(self, file: 'File') -> None:
         """Remove file from Files collection."""
         self._src_uris = None
         self._files.remove(file)
 
-    def copy_static_files(self, dirty=False):
+    def copy_static_files(self, dirty: bool = False) -> None:
         """Copy static files from source to destination."""
         for file in self:
             if not file.is_documentation_page():
                 file.copy_file(dirty)
 
-    def documentation_pages(self):
+    def documentation_pages(self) -> Iterable['File']:
         """Return iterable of all Markdown page file objects."""
         return [file for file in self if file.is_documentation_page()]
 
-    def static_pages(self):
+    def static_pages(self) -> Iterable['File']:
         """Return iterable of all static page file objects."""
         return [file for file in self if file.is_static_page()]
 
-    def media_files(self):
+    def media_files(self) -> Iterable['File']:
         """Return iterable of all file objects which are not documentation or static pages."""
         return [file for file in self if file.is_media_file()]
 
-    def javascript_files(self):
+    def javascript_files(self) -> Iterable['File']:
         """Return iterable of all javascript file objects."""
         return [file for file in self if file.is_javascript()]
 
-    def css_files(self):
+    def css_files(self) -> Iterable['File']:
         """Return iterable of all CSS file objects."""
         return [file for file in self if file.is_css()]
 
-    def add_files_from_theme(self, env, config):
+    def add_files_from_theme(self, env: jinja2.Environment, config: Config) -> None:
         """Retrieve static files from Jinja environment and add to collection."""
 
         def filter(name):
@@ -121,34 +130,26 @@ class File:
     `use_directory_urls` argument has no effect on non-Markdown files.
 
     File objects have the following properties, which are Unicode strings:
+    """
 
-    File.src_uri
-        The pure path (always '/'-separated) of the source file relative to the source directory.
+    src_uri: str
+    """The pure path (always '/'-separated) of the source file relative to the source directory."""
 
-    File.abs_src_path
-        The absolute concrete path of the source file.
+    abs_src_path: str
+    """The absolute concrete path of the source file. Will use backslashes on Windows."""
 
-    File.dest_uri
-        The pure path (always '/'-separated) of the destination file relative to the destination directory.
+    dest_uri: str
+    """The pure path (always '/'-separated) of the destination file relative to the destination directory."""
 
-    File.abs_dest_path
-        The absolute concrete path of the destination file.
+    abs_dest_path: str
+    """The absolute concrete path of the destination file. Will use backslashes on Windows."""
 
-    File.url
-        The url of the destination file relative to the destination directory as a string.
-    """
-
-    def __init__(self, path, src_dir, dest_dir, use_directory_urls):
-        self.page = None
-        self.src_path = path
-        self.abs_src_path = os.path.normpath(os.path.join(src_dir, self.src_path))
-        self.name = self._get_stem()
-        self.dest_uri = self._get_dest_path(use_directory_urls)
-        self.abs_dest_path = os.path.normpath(os.path.join(dest_dir, self.dest_path))
-        self.url = self._get_url(use_directory_urls)
+    url: str
+    """The URI of the destination file relative to the destination directory as a string."""
 
     @property
-    def src_path(self):
+    def src_path(self) -> str:
+        """Same as `src_uri` (and synchronized with it) but will use backslashes on Windows. Discouraged."""
         return os.path.normpath(self.src_uri)
 
     @src_path.setter
@@ -156,13 +157,23 @@ class File:
         self.src_uri = PurePath(value).as_posix()
 
     @property
-    def dest_path(self):
+    def dest_path(self) -> str:
+        """Same as `dest_uri` (and synchronized with it) but will use backslashes on Windows. Discouraged."""
         return os.path.normpath(self.dest_uri)
 
     @dest_path.setter
     def dest_path(self, value):
         self.dest_uri = PurePath(value).as_posix()
 
+    def __init__(self, path: str, src_dir: str, dest_dir: str, use_directory_urls: bool):
+        self.page = None
+        self.src_path = path
+        self.abs_src_path = os.path.normpath(os.path.join(src_dir, self.src_path))
+        self.name = self._get_stem()
+        self.dest_uri = self._get_dest_path(use_directory_urls)
+        self.abs_dest_path = os.path.normpath(os.path.join(dest_dir, self.dest_path))
+        self.url = self._get_url(use_directory_urls)
+
     def __eq__(self, other):
         return (
             isinstance(other, self.__class__)
@@ -177,13 +188,13 @@ class File:
             f" name='{self.name}', url='{self.url}')"
         )
 
-    def _get_stem(self):
+    def _get_stem(self) -> str:
         """Return the name of the file without it's extension."""
         filename = posixpath.basename(self.src_uri)
         stem, ext = posixpath.splitext(filename)
         return 'index' if stem in ('index', 'README') else stem
 
-    def _get_dest_path(self, use_directory_urls):
+    def _get_dest_path(self, use_directory_urls: bool) -> str:
         """Return destination path based on source path."""
         if self.is_documentation_page():
             parent, filename = posixpath.split(self.src_uri)
@@ -196,7 +207,7 @@ class File:
                 return posixpath.join(parent, self.name, 'index.html')
         return self.src_uri
 
-    def _get_url(self, use_directory_urls):
+    def _get_url(self, use_directory_urls: bool) -> str:
         """Return url based in destination path."""
         url = self.dest_uri
         dirname, filename = posixpath.split(url)
@@ -207,11 +218,11 @@ class File:
                 url = dirname + '/'
         return urlquote(url)
 
-    def url_relative_to(self, other):
+    def url_relative_to(self, other: 'File') -> str:
         """Return url for file relative to other file."""
         return utils.get_relative_url(self.url, other.url if isinstance(other, File) else other)
 
-    def copy_file(self, dirty=False):
+    def copy_file(self, dirty: bool = False) -> None:
         """Copy source file to destination, ensuring parent directories exist."""
         if dirty and not self.is_modified():
             log.debug(f"Skip copying unmodified file: '{self.src_uri}'")
@@ -219,33 +230,33 @@ class File:
             log.debug(f"Copying media file: '{self.src_uri}'")
             utils.copy_file(self.abs_src_path, self.abs_dest_path)
 
-    def is_modified(self):
+    def is_modified(self) -> bool:
         if os.path.isfile(self.abs_dest_path):
             return os.path.getmtime(self.abs_dest_path) < os.path.getmtime(self.abs_src_path)
         return True
 
-    def is_documentation_page(self):
+    def is_documentation_page(self) -> bool:
         """Return True if file is a Markdown page."""
         return utils.is_markdown_file(self.src_uri)
 
-    def is_static_page(self):
-        """Return True if file is a static page (html, xml, json)."""
+    def is_static_page(self) -> bool:
+        """Return True if file is a static page (HTML, XML, JSON)."""
         return self.src_uri.endswith(('.html', '.htm', '.xml', '.json'))
 
-    def is_media_file(self):
+    def is_media_file(self) -> bool:
         """Return True if file is not a documentation or static page."""
         return not (self.is_documentation_page() or self.is_static_page())
 
-    def is_javascript(self):
+    def is_javascript(self) -> bool:
         """Return True if file is a JavaScript file."""
         return self.src_uri.endswith(('.js', '.javascript'))
 
-    def is_css(self):
+    def is_css(self) -> bool:
         """Return True if file is a CSS file."""
         return self.src_uri.endswith('.css')
 
 
-def get_files(config):
+def get_files(config: Config) -> Files:
     """Walk the `docs_dir` and return a Files collection."""
     files = []
     exclude = ['.*', '/templates']
@@ -278,7 +289,7 @@ def get_files(config):
     return Files(files)
 
 
-def _sort_files(filenames):
+def _sort_files(filenames: Iterable[str]) -> List[str]:
     """Always sort `index` or `README` as first filename in list."""
 
     def key(f):
@@ -289,7 +300,7 @@ def _sort_files(filenames):
     return sorted(filenames, key=key)
 
 
-def _filter_paths(basename, path, is_dir, exclude):
+def _filter_paths(basename: str, path: str, is_dir: bool, exclude: Iterable[str]) -> bool:
     """.gitignore style file filtering."""
     for item in exclude:
         # Items ending in '/' apply only to directories.
diff --git a/mkdocs/structure/nav.py b/mkdocs/structure/nav.py
index 551cf5b0..26cdc983 100644
--- a/mkdocs/structure/nav.py
+++ b/mkdocs/structure/nav.py
@@ -1,6 +1,9 @@
 import logging
+from typing import Iterable, Iterator, List, Optional, Union
 from urllib.parse import urlsplit
 
+from mkdocs.config.base import Config
+from mkdocs.structure.files import Files
 from mkdocs.structure.pages import Page
 from mkdocs.utils import nest_paths
 
@@ -8,7 +11,7 @@ log = logging.getLogger(__name__)
 
 
 class Navigation:
-    def __init__(self, items, pages):
+    def __init__(self, items: List[Union[Page, 'Section', 'Link']], pages: List[Page]) -> None:
         self.items = items  # Nested List with full navigation of Sections, Pages, and Links.
         self.pages = pages  # Flat List of subset of Pages in nav, in order.
 
@@ -18,42 +21,66 @@ class Navigation:
                 self.homepage = page
                 break
 
+    homepage: Optional[Page]
+    """The [page][mkdocs.structure.pages.Page] object for the homepage of the site."""
+
+    pages: List[Page]
+    """A flat list of all [page][mkdocs.structure.pages.Page] objects contained in the navigation. """
+
     def __repr__(self):
         return '\n'.join([item._indent_print() for item in self])
 
-    def __iter__(self):
+    def __iter__(self) -> Iterator[Union[Page, 'Section', 'Link']]:
         return iter(self.items)
 
-    def __len__(self):
+    def __len__(self) -> int:
         return len(self.items)
 
 
 class Section:
-    def __init__(self, title, children):
+    def __init__(self, title: str, children: List[Union[Page, 'Section', 'Link']]) -> None:
         self.title = title
         self.children = children
 
         self.parent = None
         self.active = False
 
-        self.is_section = True
-        self.is_page = False
-        self.is_link = False
-
     def __repr__(self):
         return f"Section(title='{self.title}')"
 
-    def _get_active(self):
-        """Return active status of section."""
+    title: str
+    """The title of the section."""
+
+    parent: Optional['Section']
+    """The immediate parent of the section or `None` if the section is at the top level."""
+
+    children: Iterable[Union[Page, 'Section', 'Link']]
+    """An iterable of all child navigation objects. Children may include nested sections, pages and links."""
+
+    @property
+    def active(self) -> bool:
+        """
+        When `True`, indicates that a child page of this section is the current page and
+        can be used to highlight the section as the currently viewed section. Defaults
+        to `False`.
+        """
         return self.__active
 
-    def _set_active(self, value):
+    @active.setter
+    def active(self, value: bool):
         """Set active status of section and ancestors."""
         self.__active = bool(value)
         if self.parent is not None:
             self.parent.active = bool(value)
 
-    active = property(_get_active, _set_active)
+    is_section: bool = True
+    """Indicates that the navigation object is a "section" object. Always `True` for section objects."""
+
+    is_page: bool = False
+    """Indicates that the navigation object is a "page" object. Always `False` for section objects."""
+
+    is_link: bool = False
+    """Indicates that the navigation object is a "link" object. Always `False` for section objects."""
 
     @property
     def ancestors(self):
@@ -69,22 +96,40 @@ class Section:
 
 
 class Link:
-    def __init__(self, title, url):
+    def __init__(self, title: str, url: str):
         self.title = title
         self.url = url
         self.parent = None
 
-        # These should never change but are included for consistency with sections and pages.
-        self.children = None
-        self.active = False
-        self.is_section = False
-        self.is_page = False
-        self.is_link = True
-
     def __repr__(self):
         title = f"'{self.title}'" if (self.title is not None) else '[blank]'
         return f"Link(title={title}, url='{self.url}')"
 
+    title: str
+    """The title of the link. This would generally be used as the label of the link."""
+
+    url: str
+    """The URL that the link points to. The URL should always be an absolute URLs and
+    should not need to have `base_url` prepended."""
+
+    parent: Optional['Section']
+    """The immediate parent of the link. `None` if the link is at the top level."""
+
+    children: None = None
+    """Links do not contain children and the attribute is always `None`."""
+
+    active: bool = False
+    """External links cannot be "active" and the attribute is always `False`."""
+
+    is_section: bool = False
+    """Indicates that the navigation object is a "section" object. Always `False` for link objects."""
+
+    is_page: bool = False
+    """Indicates that the navigation object is a "page" object. Always `False` for link objects."""
+
+    is_link: bool = True
+    """Indicates that the navigation object is a "link" object. Always `True` for link objects."""
+
     @property
     def ancestors(self):
         if self.parent is None:
@@ -95,7 +140,7 @@ class Link:
         return '{}{}'.format('    ' * depth, repr(self))
 
 
-def get_navigation(files, config):
+def get_navigation(files: Files, config: Config) -> Navigation:
     """Build site navigation from config and files."""
     nav_config = config['nav'] or nest_paths(f.src_uri for f in files.documentation_pages())
     items = _data_to_navigation(nav_config, files, config)
@@ -142,7 +187,7 @@ def get_navigation(files, config):
     return Navigation(items, pages)
 
 
-def _data_to_navigation(data, files, config):
+def _data_to_navigation(data, files: Files, config: Config):
     if isinstance(data, dict):
         return [
             _data_to_navigation((key, value), files, config)
diff --git a/mkdocs/structure/pages.py b/mkdocs/structure/pages.py
index 1f24423a..cc180a91 100644
--- a/mkdocs/structure/pages.py
+++ b/mkdocs/structure/pages.py
@@ -1,22 +1,27 @@
 import logging
 import os
 import posixpath
+from typing import Any, Iterable, Mapping, Optional
 from urllib.parse import unquote as urlunquote
 from urllib.parse import urljoin, urlsplit, urlunsplit
+from xml.etree.ElementTree import Element
 
 import markdown
 from markdown.extensions import Extension
 from markdown.treeprocessors import Treeprocessor
 from markdown.util import AMP_SUBSTITUTE
 
-from mkdocs.structure.toc import get_toc
+from mkdocs.config.base import Config
+from mkdocs.structure import nav
+from mkdocs.structure.files import File, Files
+from mkdocs.structure.toc import AnchorLink, get_toc
 from mkdocs.utils import get_build_date, get_markdown_title, meta
 
 log = logging.getLogger(__name__)
 
 
 class Page:
-    def __init__(self, title, file, config):
+    def __init__(self, title: Optional[str], file: File, config: Config) -> None:
         file.page = self
         self.file = file
         self.title = title
@@ -28,10 +33,6 @@ class Page:
         self.next_page = None
         self.active = False
 
-        self.is_section = False
-        self.is_page = True
-        self.is_link = False
-
         self.update_date = get_build_date()
 
         self._set_canonical_url(config.get('site_url', None))
@@ -58,33 +59,97 @@ class Page:
     def _indent_print(self, depth=0):
         return '{}{}'.format('    ' * depth, repr(self))
 
-    def _get_active(self):
-        """Return active status of page."""
+    title: Optional[str]
+    """Contains the Title for the current page."""
+
+    content: Optional[str]
+    """The rendered Markdown as HTML, this is the contents of the documentation."""
+
+    toc: Iterable[AnchorLink]
+    """An iterable object representing the Table of contents for a page. Each item in
+    the `toc` is an [`AnchorLink`][mkdocs.structure.toc.AnchorLink]."""
+
+    meta: Mapping[str, Any]
+    """A mapping of the metadata included at the top of the markdown page."""
+
+    @property
+    def url(self) -> str:
+        """The URL of the page relative to the MkDocs `site_dir`."""
+        return '' if self.file.url == '.' else self.file.url
+
+    file: File
+    """The documentation [`File`][mkdocs.structure.files.File] that the page is being rendered from."""
+
+    abs_url: str
+    """The absolute URL of the page from the server root as determined by the value
+    assigned to the [site_url][] configuration setting. The value includes any
+    subdirectory included in the `site_url`, but not the domain. [base_url][] should
+    not be used with this variable."""
+
+    canonical_url: str
+    """The full, canonical URL to the current page as determined by the value assigned
+    to the [site_url][] configuration setting. The value includes the domain and any
+    subdirectory included in the `site_url`. [base_url][] should not be used with this
+    variable."""
+
+    @property
+    def active(self) -> bool:
+        """When `True`, indicates that this page is the currently viewed page. Defaults to `False`."""
         return self.__active
 
-    def _set_active(self, value):
+    @active.setter
+    def active(self, value: bool):
         """Set active status of page and ancestors."""
         self.__active = bool(value)
         if self.parent is not None:
             self.parent.active = bool(value)
 
-    active = property(_get_active, _set_active)
-
     @property
-    def is_index(self):
+    def is_index(self) -> bool:
         return self.file.name == 'index'
 
     @property
-    def is_top_level(self):
+    def is_top_level(self) -> bool:
         return self.parent is None
 
+    edit_url: str
+    """The full URL to the source page in the source repository. Typically used to
+    provide a link to edit the source page. [base_url][] should not be used with this
+    variable."""
+
     @property
-    def is_homepage(self):
+    def is_homepage(self) -> bool:
+        """Evaluates to `True` for the homepage of the site and `False` for all other pages."""
         return self.is_top_level and self.is_index and self.file.url in ['.', 'index.html']
 
-    @property
-    def url(self):
-        return '' if self.file.url == '.' else self.file.url
+    previous_page: Optional['Page']
+    """The [page][mkdocs.structure.pages.Page] object for the previous page or `None`.
+    The value will be `None` if the current page is the first item in the site navigation
+    or if the current page is not included in the navigation at all."""
+
+    next_page: Optional['Page']
+    """The [page][mkdocs.structure.pages.Page] object for the next page or `None`.
+    The value will be `None` if the current page is the last item in the site navigation
+    or if the current page is not included in the navigation at all."""
+
+    parent: Optional['nav.Section']
+    """The immediate parent of the page in the site navigation. `None` if the
+    page is at the top level."""
+
+    children: None = None
+    """Pages do not contain children and the attribute is always `None`."""
+
+    active: bool
+    """When `True`, indicates that this page is the currently viewed page. Defaults to `False`."""
+
+    is_section: bool = False
+    """Indicates that the navigation object is a "section" object. Always `False` for page objects."""
+
+    is_page: bool = True
+    """Indicates that the navigation object is a "page" object. Always `True` for page objects."""
+
+    is_link: bool = False
+    """Indicates that the navigation object is a "link" object. Always `False` for page objects."""
 
     @property
     def ancestors(self):
@@ -92,7 +157,7 @@ class Page:
             return []
         return [self.parent] + self.parent.ancestors
 
-    def _set_canonical_url(self, base):
+    def _set_canonical_url(self, base) -> None:
         if base:
             if not base.endswith('/'):
                 base += '/'
@@ -102,7 +167,7 @@ class Page:
             self.canonical_url = None
             self.abs_url = None
 
-    def _set_edit_url(self, repo_url, edit_uri):
+    def _set_edit_url(self, repo_url, edit_uri) -> None:
         if edit_uri:
             src_uri = self.file.src_uri
             edit_uri += src_uri
@@ -124,7 +189,7 @@ class Page:
         else:
             self.edit_url = None
 
-    def read_source(self, config):
+    def read_source(self, config: Config) -> None:
         source = config['plugins'].run_event('page_read_source', page=self, config=config)
         if source is None:
             try:
@@ -140,7 +205,7 @@ class Page:
         self.markdown, self.meta = meta.get_data(source)
         self._set_title()
 
-    def _set_title(self):
+    def _set_title(self) -> None:
         """
         Set the title for a Markdown document.
 
@@ -170,7 +235,7 @@ class Page:
 
         self.title = title
 
-    def render(self, config, files):
+    def render(self, config: Config, files: Files) -> None:
         """
         Convert the Markdown source file to HTML as per the config.
         """
@@ -186,11 +251,11 @@ class Page:
 
 
 class _RelativePathTreeprocessor(Treeprocessor):
-    def __init__(self, file, files):
+    def __init__(self, file: File, files: Files) -> None:
         self.file = file
         self.files = files
 
-    def run(self, root):
+    def run(self, root: Element) -> Element:
         """
         Update urls on anchors and images to make them relative
 
@@ -211,7 +276,7 @@ class _RelativePathTreeprocessor(Treeprocessor):
 
         return root
 
-    def path_to_url(self, url):
+    def path_to_url(self, url: str) -> str:
         scheme, netloc, path, query, fragment = urlsplit(url)
 
         if (
@@ -251,10 +316,10 @@ class _RelativePathExtension(Extension):
     registers the Treeprocessor.
     """
 
-    def __init__(self, file, files):
+    def __init__(self, file: File, files: Files) -> None:
         self.file = file
         self.files = files
 
-    def extendMarkdown(self, md):
+    def extendMarkdown(self, md) -> None:
         relpath = _RelativePathTreeprocessor(self.file, self.files)
         md.treeprocessors.register(relpath, "relpath", 0)
diff --git a/mkdocs/structure/toc.py b/mkdocs/structure/toc.py
index 9541b036..492e5712 100644
--- a/mkdocs/structure/toc.py
+++ b/mkdocs/structure/toc.py
@@ -5,6 +5,7 @@ For the sake of simplicity we use the Python-Markdown `toc` extension to
 generate a list of dicts for each toc item, and then store it as AnchorLinks to
 maintain compatibility with older versions of MkDocs.
 """
+from typing import List
 
 
 def get_toc(toc_tokens):
@@ -38,14 +39,24 @@ class AnchorLink:
     A single entry in the table of contents.
     """
 
-    def __init__(self, title, id, level):
+    def __init__(self, title: str, id: str, level: int):
         self.title, self.id, self.level = title, id, level
         self.children = []
 
+    title: str
+    """The text of the item."""
+
     @property
-    def url(self):
+    def url(self) -> str:
+        """The hash fragment of a URL pointing to the item."""
         return '#' + self.id
 
+    level: int
+    """The zero-based level of the item."""
+
+    children: List['AnchorLink']
+    """An iterable of any child items."""
+
     def __str__(self):
         return self.indent_print()
 


Test patch:
diff --git a/mkdocs/tests/config/config_options_tests.py b/mkdocs/tests/config/config_options_tests.py
index c0fb9996..3b17ed7d 100644
--- a/mkdocs/tests/config/config_options_tests.py
+++ b/mkdocs/tests/config/config_options_tests.py
@@ -7,6 +7,7 @@ from unittest.mock import patch
 import mkdocs
 from mkdocs.config import config_options
 from mkdocs.config.base import Config
+from mkdocs.exceptions import ConfigurationError
 from mkdocs.tests.base import tempdir
 from mkdocs.utils import yaml_load
 
@@ -854,6 +855,66 @@ class PrivateTest(unittest.TestCase):
             option.validate('somevalue')
 
 
+class SubConfigTest(unittest.TestCase):
+    def test_subconfig_wrong_type(self):
+        # Test that an error is raised if subconfig does not receive a dict
+        option = config_options.SubConfig()
+        with self.assertRaises(ConfigurationError):
+            option.validate("not_a_dict")
+        with self.assertRaises(ConfigurationError):
+            option.validate(("not_a_dict",))
+        with self.assertRaises(ConfigurationError):
+            option.validate(["not_a_dict"])
+
+    def test_subconfig_default(self):
+        """Default behaviour of subconfig: validation is ignored"""
+
+        # Nominal
+        option = config_options.SubConfig(('c', config_options.Choice(('foo', 'bar'))))
+        res = option.validate(dict(c='foo'))
+        assert option.warnings == []
+        assert res == dict(c='foo')
+
+        # Invalid option ! No error
+        option = config_options.SubConfig(('c', config_options.Choice(('foo', 'bar'))))
+        res = option.validate(dict(c=True))
+        assert option.warnings == []
+        assert res == dict(c=True)
+
+        # Missing option ! Will be considered optional with default None
+        option = config_options.SubConfig(('c', config_options.Choice(('foo', 'bar'))))
+        res = option.validate(dict())
+        assert option.warnings == []
+        assert res == dict(c=None)
+
+        # Unknown option ! No warning
+        option = config_options.SubConfig(('c', config_options.Choice(('foo', 'bar'))))
+        res = option.validate({"unknown_key_is_ok": 0})
+        assert option.warnings == []
+        assert res == {"c": None, "unknown_key_is_ok": 0}
+
+    def test_subconfig_strict(self):
+        """Strict validation mode for subconfigs."""
+
+        # Unknown option: warning
+        option = config_options.SubConfig(validate=True)
+        res = option.validate({"unknown": 0})
+        assert option.warnings == [('unknown', 'Unrecognised configuration name: unknown')]
+        assert res == {"unknown": 0}
+
+        # Invalid option: error
+        option = config_options.SubConfig(
+            ('c', config_options.Choice(('foo', 'bar'))),
+            validate=True,
+        )
+        with self.assertRaises(config_options.ValidationError):
+            option.validate(dict(c=True))
+
+        # Nominal
+        res = option.validate(dict(c='foo'))
+        assert res == dict(c='foo')
+
+
 class MarkdownExtensionsTest(unittest.TestCase):
     @patch('markdown.Markdown')
     def test_simple_list(self, mockMd):
diff --git a/mkdocs/tests/plugin_tests.py b/mkdocs/tests/plugin_tests.py
index a8896df0..4047c175 100644
--- a/mkdocs/tests/plugin_tests.py
+++ b/mkdocs/tests/plugin_tests.py
@@ -145,7 +145,7 @@ class TestPluginCollection(unittest.TestCase):
 
     def test_run_unknown_event_on_collection(self):
         collection = plugins.PluginCollection()
-        with self.assertRaises(AttributeError):
+        with self.assertRaises(KeyError):
             collection.run_event('unknown', 'page content')
 
     def test_run_build_error_event(self):