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 +::: mkdocs.structure.nav.Navigation.homepage + options: + show_root_full_path: false + heading_level: 5 -The [page](#page) object for the homepage of the site. +::: mkdocs.structure.nav.Navigation.pages + options: + show_root_full_path: false + heading_level: 5 -##### nav.pages - -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. +::: mkdocs.structure.pages.Page + options: + show_root_heading: false + show_root_toc_entry: true + members: [] + heading_level: 4 + All `page` objects contain the following attributes: -##### page.title +::: mkdocs.structure.pages.Page.title + options: + show_root_full_path: false + heading_level: 5 -Contains the Title for the current page. +::: mkdocs.structure.pages.Page.content + options: + show_root_full_path: false + heading_level: 5 -##### page.content - -The rendered Markdown as HTML, this is the contents of the documentation. - -##### page.toc - -An iterable object representing the Table of contents for a page. Each item in -the `toc` is an `AnchorLink` which contains the following attributes: - -* `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. ``` -##### 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 {{ page.title }} ``` -[base_url]: #base_url +::: mkdocs.structure.pages.Page.file + options: + show_root_full_path: false + heading_level: 5 -##### page.file - -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 +::: mkdocs.structure.pages.Page.canonical_url + options: + show_root_full_path: false + heading_level: 5 -##### page.canonical_url +::: mkdocs.structure.pages.Page.edit_url + options: + show_root_full_path: false + heading_level: 5 -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.is_homepage + options: + show_root_full_path: false + heading_level: 5 -##### page.edit_url - -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. - -##### 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 +::: mkdocs.structure.pages.Page.previous_page + options: + show_root_full_path: false + heading_level: 5 -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`. +::: mkdocs.structure.pages.Page.next_page + options: + show_root_full_path: false + heading_level: 5 -##### page.next_page +::: mkdocs.structure.pages.Page.parent + options: + show_root_full_path: false + heading_level: 5 -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`. +::: mkdocs.structure.pages.Page.children + options: + show_root_full_path: false + heading_level: 5 -##### page.parent +::: mkdocs.structure.pages.Page.active + options: + show_root_full_path: false + heading_level: 5 -The immediate parent of the page in the [site navigation](#nav). `None` if the -page is at the top level. +::: mkdocs.structure.pages.Page.is_section + options: + show_root_full_path: false + heading_level: 5 -##### page.children +::: mkdocs.structure.pages.Page.is_page + options: + show_root_full_path: false + heading_level: 5 -Pages do not contain children and the attribute is always `None`. +::: mkdocs.structure.pages.Page.is_link + options: + show_root_full_path: false + heading_level: 5 -##### page.active +#### AnchorLink -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.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: +::: mkdocs.structure.nav.Section + options: + show_root_heading: false + show_root_toc_entry: true + members: [] + heading_level: 4 -##### section.title +The following attributes are available on `section` objects: -The title of the section. +::: mkdocs.structure.nav.Section.title + options: + show_root_full_path: false + heading_level: 5 -##### section.parent +::: mkdocs.structure.nav.Section.parent + options: + show_root_full_path: false + heading_level: 5 -The immediate parent of the section or `None` if the section is at the top -level. +::: mkdocs.structure.nav.Section.children + options: + show_root_full_path: false + heading_level: 5 -##### section.children +::: mkdocs.structure.nav.Section.active + options: + show_root_full_path: false + heading_level: 5 -An iterable of all child navigation objects. Children may include nested -sections, pages and links. +::: mkdocs.structure.nav.Section.is_section + options: + show_root_full_path: false + heading_level: 5 -##### section.active +::: mkdocs.structure.nav.Section.is_page + options: + show_root_full_path: false + heading_level: 5 -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.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: +MkDocs page. -##### link.title +::: mkdocs.structure.nav.Link + options: + show_root_heading: false + show_root_toc_entry: true + members: [] + heading_level: 4 -The title of the link. This would generally be used as the label of the link. +The following attributes are available on `link` objects: -##### link.url +::: mkdocs.structure.nav.Link.title + options: + show_root_full_path: false + heading_level: 5 -The URL that the link points to. The URL should always be an absolute URLs and -should not need to have `base_url` prepended. +::: mkdocs.structure.nav.Link.url + options: + show_root_full_path: false + heading_level: 5 -##### link.parent +::: mkdocs.structure.nav.Link.parent + options: + show_root_full_path: false + heading_level: 5 -The immediate parent of the link. `None` if the link is at the top level. +::: mkdocs.structure.nav.Link.children + options: + show_root_full_path: false + heading_level: 5 -##### link.children +::: mkdocs.structure.nav.Link.active + options: + show_root_full_path: false + heading_level: 5 -Links do not contain children and the attribute is always `None`. +::: mkdocs.structure.nav.Link.is_section + options: + show_root_full_path: false + heading_level: 5 -##### link.active +::: mkdocs.structure.nav.Link.is_page + options: + show_root_full_path: false + heading_level: 5 -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.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 6c5c315d..7e38b647 100644 --- a/mkdocs/config/config_options.py +++ b/mkdocs/config/config_options.py @@ -9,45 +9,7 @@ from urllib.parse import urlsplit, urlunsplit import markdown from mkdocs import plugins, theme, utils -from mkdocs.config.base import 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. - """ +from mkdocs.config.base import BaseConfigOption, Config, ValidationError class SubConfig(BaseConfigOption): 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 5778fbcb..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 @@ -48,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) 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. - - File.abs_src_path - The absolute concrete path of the source file. - - File.dest_uri - 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. - - 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) + src_uri: str + """The pure path (always '/'-separated) of the source file relative to the source directory.""" + + abs_src_path: str + """The absolute concrete path of the source file. Will use backslashes on Windows.""" + + dest_uri: str + """The pure path (always '/'-separated) of the destination file relative to the destination directory.""" + + abs_dest_path: str + """The absolute concrete path of the destination file. Will use backslashes on Windows.""" + + 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 - @property - def is_homepage(self): - return self.is_top_level and self.is_index and self.file.url in ['.', 'index.html'] + 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 url(self): - return '' if self.file.url == '.' else self.file.url + 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'] + + 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()