From 01be507e30b05db0a4c44ef05ba62b2098010653 Mon Sep 17 00:00:00 2001 From: Oleh Prypin Date: Sat, 17 Jun 2023 12:39:05 +0200 Subject: [PATCH] Define a base class for all navigation item classes --- docs/dev-guide/themes.md | 6 ++--- mkdocs/structure/__init__.py | 36 +++++++++++++++++++++++++++ mkdocs/structure/nav.py | 48 ++++++++++-------------------------- mkdocs/structure/pages.py | 34 ++++++------------------- 4 files changed, 59 insertions(+), 65 deletions(-) diff --git a/docs/dev-guide/themes.md b/docs/dev-guide/themes.md index 32496f04..8f83c155 100644 --- a/docs/dev-guide/themes.md +++ b/docs/dev-guide/themes.md @@ -408,7 +408,7 @@ on the homepage: show_root_full_path: false heading_level: 5 -::: mkdocs.structure.pages.Page.parent +::: mkdocs.structure.StructureItem.parent options: show_root_full_path: false heading_level: 5 @@ -479,7 +479,7 @@ The following attributes are available on `section` objects: show_root_full_path: false heading_level: 5 -::: mkdocs.structure.nav.Section.parent +::: mkdocs.structure.StructureItem.parent options: show_root_full_path: false heading_level: 5 @@ -533,7 +533,7 @@ The following attributes are available on `link` objects: show_root_full_path: false heading_level: 5 -::: mkdocs.structure.nav.Link.parent +::: mkdocs.structure.StructureItem.parent options: show_root_full_path: false heading_level: 5 diff --git a/mkdocs/structure/__init__.py b/mkdocs/structure/__init__.py index e69de29b..69356405 100644 --- a/mkdocs/structure/__init__.py +++ b/mkdocs/structure/__init__.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import abc +from typing import TYPE_CHECKING, Iterable + +if TYPE_CHECKING: + from mkdocs.structure.nav import Section + + +class StructureItem(metaclass=abc.ABCMeta): + """An item in MkDocs structure - see concrete subclasses Section, Page or Link.""" + + @abc.abstractmethod + def __init__(self): + ... + + parent: Section | None = None + """The immediate parent of the item in the site navigation. `None` if it's at the top level.""" + + @property + def is_top_level(self) -> bool: + return self.parent is None + + title: str | None + is_section: bool = False + is_page: bool = False + is_link: bool = False + + @property + def ancestors(self) -> Iterable[StructureItem]: + if self.parent is None: + return [] + return [self.parent, *self.parent.ancestors] + + def _indent_print(self, depth=0): + return (' ' * depth) + repr(self) diff --git a/mkdocs/structure/nav.py b/mkdocs/structure/nav.py index 558fbe29..f2aadd9f 100644 --- a/mkdocs/structure/nav.py +++ b/mkdocs/structure/nav.py @@ -4,6 +4,7 @@ import logging from typing import TYPE_CHECKING, Iterator, TypeVar from urllib.parse import urlsplit +from mkdocs.structure import StructureItem from mkdocs.structure.pages import Page from mkdocs.utils import nest_paths @@ -16,7 +17,7 @@ log = logging.getLogger(__name__) class Navigation: - def __init__(self, items: list[Page | Section | Link], pages: list[Page]) -> None: + def __init__(self, items: list, 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. @@ -32,34 +33,30 @@ class Navigation: pages: list[Page] """A flat list of all [page][mkdocs.structure.pages.Page] objects contained in the navigation.""" - def __repr__(self): + def __str__(self) -> str: return '\n'.join(item._indent_print() for item in self) - def __iter__(self) -> Iterator[Page | Section | Link]: + def __iter__(self) -> Iterator: return iter(self.items) def __len__(self) -> int: return len(self.items) -class Section: - def __init__(self, title: str, children: list[Page | Section | Link]) -> None: +class Section(StructureItem): + def __init__(self, title: str, children: list[StructureItem]) -> None: self.title = title self.children = children - self.parent = None self.active = False def __repr__(self): - return f"Section(title='{self.title}')" + return f"Section(title={self.title!r})" title: str """The title of the section.""" - parent: Section | None - """The immediate parent of the section or `None` if the section is at the top level.""" - - children: list[Page | Section | Link] + children: list[StructureItem] """An iterable of all child navigation objects. Children may include nested sections, pages and links.""" @property @@ -87,28 +84,21 @@ class Section: is_link: bool = False """Indicates that the navigation object is a "link" object. Always `False` for section objects.""" - @property - def ancestors(self): - if self.parent is None: - return [] - return [self.parent] + self.parent.ancestors - - def _indent_print(self, depth=0): - ret = ['{}{}'.format(' ' * depth, repr(self))] + def _indent_print(self, depth: int = 0): + ret = [super()._indent_print(depth)] for item in self.children: ret.append(item._indent_print(depth + 1)) return '\n'.join(ret) -class Link: +class Link(StructureItem): def __init__(self, title: str, url: str): self.title = title self.url = url - self.parent = None def __repr__(self): - title = f"'{self.title}'" if (self.title is not None) else '[blank]' - return f"Link(title={title}, url='{self.url}')" + title = f"{self.title!r}" if self.title is not None else '[blank]' + return f"Link(title={title}, url={self.url!r})" title: str """The title of the link. This would generally be used as the label of the link.""" @@ -117,9 +107,6 @@ class Link: """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: Section | None - """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`.""" @@ -135,15 +122,6 @@ class Link: 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: - return [] - return [self.parent] + self.parent.ancestors - - def _indent_print(self, depth=0): - return '{}{}'.format(' ' * depth, repr(self)) - def get_navigation(files: Files, config: MkDocsConfig) -> Navigation: """Build site navigation from config and files.""" diff --git a/mkdocs/structure/pages.py b/mkdocs/structure/pages.py index 4f38f2ea..a888a5d9 100644 --- a/mkdocs/structure/pages.py +++ b/mkdocs/structure/pages.py @@ -5,7 +5,7 @@ import logging import os import posixpath import warnings -from typing import TYPE_CHECKING, Any, Callable, Mapping, MutableMapping +from typing import TYPE_CHECKING, Any, Callable, MutableMapping from urllib.parse import unquote as urlunquote from urllib.parse import urljoin, urlsplit, urlunsplit @@ -15,6 +15,7 @@ import markdown.postprocessors import markdown.treeprocessors from markdown.util import AMP_SUBSTITUTE +from mkdocs.structure import StructureItem from mkdocs.structure.toc import get_toc from mkdocs.utils import get_build_date, get_markdown_title, meta, weak_property @@ -23,7 +24,6 @@ if TYPE_CHECKING: from mkdocs.config.defaults import MkDocsConfig from mkdocs.structure.files import File, Files - from mkdocs.structure.nav import Section from mkdocs.structure.toc import TableOfContents _unescape: Callable[[str], str] @@ -36,17 +36,14 @@ except AttributeError: log = logging.getLogger(__name__) -class Page: - def __init__( - self, title: str | None, file: File, config: MkDocsConfig | Mapping[str, Any] - ) -> None: +class Page(StructureItem): + def __init__(self, title: str | None, file: File, config: MkDocsConfig) -> None: file.page = self self.file = file if title is not None: self.title = title # Navigation attributes - self.parent = None self.children = None self.previous_page = None self.next_page = None @@ -74,12 +71,9 @@ class Page: ) def __repr__(self): - title = f"'{self.title}'" if (self.title is not None) else '[blank]' + title = f"{self.title!r}" if self.title is not None else '[blank]' url = self.abs_url or self.file.url - return f"Page(title={title}, url='{url}')" - - def _indent_print(self, depth=0): - return '{}{}'.format(' ' * depth, repr(self)) + return f"Page(title={title}, url={url!r})" markdown: str | None """The original Markdown content from the file.""" @@ -133,10 +127,6 @@ class Page: def is_index(self) -> bool: return self.file.name == 'index' - @property - def is_top_level(self) -> bool: - return self.parent is None - edit_url: str | None """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 @@ -157,10 +147,6 @@ class Page: 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: Section | None - """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`.""" @@ -173,12 +159,6 @@ class Page: is_link: bool = False """Indicates that the navigation object is a "link" object. Always `False` for page objects.""" - @property - def ancestors(self): - if self.parent is None: - return [] - return [self.parent] + self.parent.ancestors - def _set_canonical_url(self, base: str | None) -> None: if base: if not base.endswith('/'): @@ -242,7 +222,7 @@ class Page: ) @weak_property - def title(self) -> str | None: + def title(self) -> str | None: # type: ignore[override] """ Returns the title for the current page.