From 7f130bc99b64de80686310f88bb632f3c52ba41f Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Sun, 10 Nov 2024 18:22:08 +0100 Subject: [PATCH 01/28] chore: cleanup --- mkdocs_mknodes/commands/build_page.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mkdocs_mknodes/commands/build_page.py b/mkdocs_mknodes/commands/build_page.py index 1f5ec7d..a4b6ac0 100644 --- a/mkdocs_mknodes/commands/build_page.py +++ b/mkdocs_mknodes/commands/build_page.py @@ -12,7 +12,7 @@ from jinja2.exceptions import TemplateNotFound from jinjarope import envtests, htmlfilters import logfire -from mkdocs import exceptions, utils as mkdocs_utils +from mkdocs import exceptions from mkdocs.structure.files import Files, InclusionLevel from mkdocs.structure.nav import Navigation, get_navigation from mkdocs.structure.pages import Page @@ -381,14 +381,15 @@ def get_context( ) -> TemplateContext: """Return the template context for a given page or template.""" if page is not None: - base_url = mkdocs_utils.get_relative_url(".", page.url) + base_url = htmlfilters.relative_url_mkdocs(".", page.url) extra_javascript = [ - mkdocs_utils.normalize_url(str(script), page, base_url) + htmlfilters.normalize_url(str(script), page.url if page else None, base_url) for script in config.extra_javascript ] extra_css = [ - mkdocs_utils.normalize_url(path, page, base_url) for path in config.extra_css + htmlfilters.normalize_url(path, page.url if page else None, base_url) + for path in config.extra_css ] if isinstance(files, Files): From e21e337e83af1328eead264c7621d846dc183533 Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Sun, 10 Nov 2024 18:41:48 +0100 Subject: [PATCH 02/28] chore: config work --- mkdocs_mknodes/appconfig/themeconfig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs_mknodes/appconfig/themeconfig.py b/mkdocs_mknodes/appconfig/themeconfig.py index d1fce72..bec07e4 100644 --- a/mkdocs_mknodes/appconfig/themeconfig.py +++ b/mkdocs_mknodes/appconfig/themeconfig.py @@ -77,7 +77,7 @@ class ThemeConfig(BaseModel): ``` """ - static_templates: list[str] | None = Field(None) + static_templates: list[str] | None = Field(default_factory=list) """Defines templates to be rendered as static pages, regardless of nav structure. !!! info "Common Use Cases" From 6e88a3a982512c96b13d647b4a43b0b4ddd3686f Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Sun, 10 Nov 2024 18:57:21 +0100 Subject: [PATCH 03/28] chore: less None for AppConfig --- mkdocs_mknodes/appconfig/appconfig.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/mkdocs_mknodes/appconfig/appconfig.py b/mkdocs_mknodes/appconfig/appconfig.py index 1dc1750..6113cb5 100644 --- a/mkdocs_mknodes/appconfig/appconfig.py +++ b/mkdocs_mknodes/appconfig/appconfig.py @@ -11,7 +11,6 @@ BaseModel, DirectoryPath, Field, - HttpUrl, ValidationInfo, field_validator, ) @@ -160,8 +159,8 @@ class AppConfig(ConfigFile): copyright: '© 2024 MyProject Team. All rights reserved.' ``` """ - - repo_url: HttpUrl | None = Field(None) + # pydantic.HttpUrl + repo_url: str | None = Field(None) """Link to your project's source code repository. !!! info "Supported Platforms" @@ -255,7 +254,7 @@ class AppConfig(ConfigFile): The targeted callable gets the project instance as an argument and optionally keyword arguments from setting below. """ - build_fn_arguments: dict[str, Any] | None = None + build_kwargs: dict[str, Any] | None = None """Keyword arguments passed to the build script / callable. Build scripts may have keyword arguments. You can set them by using this setting. @@ -484,7 +483,7 @@ class AppConfig(ConfigFile): ``` """ - extra_javascript: list[str | ExtraJavascript] | None = Field(None) + extra_javascript: list[str | ExtraJavascript] = Field(default_factory=list) """Custom JavaScript files to include in the documentation. !!! info "File Types" @@ -543,7 +542,7 @@ class AppConfig(ConfigFile): ``` """ - hooks: list[str] | None = Field(None) + hooks: list[str] = Field(default_factory=list) """Python scripts that extend the build process. !!! info "Hook Types" @@ -567,7 +566,7 @@ class AppConfig(ConfigFile): ``` """ - nav: list[dict[str, Any] | str] | None = Field(None) + nav: list[dict[str, Any] | str] = Field(default_factory=list) """Defines the hierarchical structure of the documentation navigation. Each item can be a simple path to a file or a section with nested items. @@ -617,7 +616,9 @@ class AppConfig(ConfigFile): ``` """ - validation: validationconfig.ValidationConfig | None = Field(None) + validation: validationconfig.ValidationConfig = Field( + default_factory=validationconfig.ValidationConfig + ) """Controls the validation behavior for links, content, and navigation. !!! info "Validation Levels" @@ -649,7 +650,7 @@ class AppConfig(ConfigFile): ``` """ # DirectoryPath would be nice - watch: list[str] | None = Field(None) + watch: list[str] = Field(default_factory=list) """Additional directories to monitor for changes during development. !!! info "Behavior" @@ -778,7 +779,7 @@ def validate_ip_port(cls, v: str) -> str: def get_builder(self) -> Callable[..., Any]: build_fn = classhelpers.to_callable(self.build_fn) - build_kwargs = self.build_fn_arguments or {} + build_kwargs = self.build_kwargs or {} return functools.partial(build_fn, **build_kwargs) def set_theme( From a51c44baa6df84bb529b569a91359282ad88e592 Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Sun, 10 Nov 2024 19:01:53 +0100 Subject: [PATCH 04/28] chore: cleanup --- configs/mkdocs_mkdocs.yml | 2 +- mkdocs_mknodes/plugin/mknodesconfig.py | 4 ++-- mkdocs_mknodes/plugin/pluginconfig.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/configs/mkdocs_mkdocs.yml b/configs/mkdocs_mkdocs.yml index a44089f..ae35f86 100644 --- a/configs/mkdocs_mkdocs.yml +++ b/configs/mkdocs_mkdocs.yml @@ -9,7 +9,7 @@ plugins: repo_path: https://github.com/mkdocs/mkdocs.git clone_depth: 100 build_fn: mkdocs_mknodes:parse - kwargs: + build_kwargs: pages: - title: Home type: MkText diff --git a/mkdocs_mknodes/plugin/mknodesconfig.py b/mkdocs_mknodes/plugin/mknodesconfig.py index c21b32c..17bb27a 100644 --- a/mkdocs_mknodes/plugin/mknodesconfig.py +++ b/mkdocs_mknodes/plugin/mknodesconfig.py @@ -136,7 +136,7 @@ def from_yaml_file( The targeted callable gets the project instance as an argument and optionally keyword arguments from setting below. """ - kwargs = c.Optional(c.Type(dict)) + build_kwargs = c.Optional(c.Type(dict)) """Keyword arguments passed to the build script / callable. Build scripts may have keyword arguments. You can set them by using this setting. @@ -235,7 +235,7 @@ def from_yaml_file( def get_builder(self) -> Callable[..., Any]: build_fn = classhelpers.to_callable(self.build_fn) - build_kwargs = self.kwargs or {} + build_kwargs = self.build_kwargs or {} return functools.partial(build_fn, **build_kwargs) def get_jinja_config(self) -> jinjarope.EnvConfig: diff --git a/mkdocs_mknodes/plugin/pluginconfig.py b/mkdocs_mknodes/plugin/pluginconfig.py index b606037..3ed2e03 100644 --- a/mkdocs_mknodes/plugin/pluginconfig.py +++ b/mkdocs_mknodes/plugin/pluginconfig.py @@ -25,7 +25,7 @@ class PluginConfig(base.Config): The targeted callable gets the project instance as an argument and optionally keyword arguments from setting below. """ - kwargs = c.Optional(c.Type(dict)) + build_kwargs = c.Optional(c.Type(dict)) """Keyword arguments passed to the build script / callable. Build scripts may have keyword arguments. You can set them by using this setting. @@ -130,7 +130,7 @@ class PluginConfig(base.Config): def get_builder(self) -> Callable[..., Any]: build_fn = classhelpers.to_callable(self.build_fn) - build_kwargs = self.kwargs or {} + build_kwargs = self.build_kwargs or {} return functools.partial(build_fn, **build_kwargs) def get_jinja_config(self) -> jinjarope.EnvConfig: From f9fde31719fd8f2e45725008770912223d8777cc Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Sun, 10 Nov 2024 19:37:22 +0100 Subject: [PATCH 05/28] chore: cleanup --- mkdocs_mknodes/mkdocsconfig.py | 2 +- mkdocs_mknodes/plugin/mknodesconfig.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mkdocs_mknodes/mkdocsconfig.py b/mkdocs_mknodes/mkdocsconfig.py index 734f758..0301e8a 100644 --- a/mkdocs_mknodes/mkdocsconfig.py +++ b/mkdocs_mknodes/mkdocsconfig.py @@ -97,7 +97,7 @@ def load_config( cfg.load_file(fd) # Then load the options to overwrite anything in the config. - cfg.load_dict(options) + cfg.update(options) errors, warnings = cfg.validate() diff --git a/mkdocs_mknodes/plugin/mknodesconfig.py b/mkdocs_mknodes/plugin/mknodesconfig.py index 17bb27a..95d097d 100644 --- a/mkdocs_mknodes/plugin/mknodesconfig.py +++ b/mkdocs_mknodes/plugin/mknodesconfig.py @@ -94,9 +94,9 @@ def from_yaml( config_file_path = getattr(fd, "name", None) cfg = cls(config_file_path=config_file_path) dct = yamling.load_yaml(fd, resolve_inherit=True) - cfg.load_dict(dct) + cfg.update(dct) # Then load the options to overwrite anything in the config. - cfg.load_dict(options) + cfg.update(options) errors, warnings = cfg.validate() From 78ecfbc69a9ff6f2db2a266e07fb0ac4cfa2870f Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Sun, 10 Nov 2024 19:53:36 +0100 Subject: [PATCH 06/28] chore: nav validator --- mkdocs_mknodes/appconfig/appconfig.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mkdocs_mknodes/appconfig/appconfig.py b/mkdocs_mknodes/appconfig/appconfig.py index 6113cb5..8c7950b 100644 --- a/mkdocs_mknodes/appconfig/appconfig.py +++ b/mkdocs_mknodes/appconfig/appconfig.py @@ -763,6 +763,15 @@ def validate_extra_javascript( items.append(item) return items + @field_validator("nav", mode="before") + @classmethod + def validate_nav( + cls, values: list[dict[str, Any] | str] | None + ) -> list[dict[str, Any] | str]: + if values is None: + return [] + return values + @field_validator("dev_addr", mode="before") @classmethod def validate_ip_port(cls, v: str) -> str: From 1e318dc64b7ed60eb034ed9972e2ad6ade811a73 Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Sun, 10 Nov 2024 19:57:04 +0100 Subject: [PATCH 07/28] chore: duplicate cfg stuff for now --- configs/mkdocs_mkdocs.yml | 55 +++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 4 ++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/configs/mkdocs_mkdocs.yml b/configs/mkdocs_mkdocs.yml index ae35f86..cb8c77a 100644 --- a/configs/mkdocs_mkdocs.yml +++ b/configs/mkdocs_mkdocs.yml @@ -4,6 +4,61 @@ site_name: MkDocs site_description: A MkDocs page created by MkNodes repo_url: "https://github.com/mkdocs/mkdocs/" site_url: https://phil65.github.io/mknodes/mkdocs/ +repo_path: https://github.com/mkdocs/mkdocs.git +clone_depth: 100 +build_fn: mkdocs_mknodes:parse +build_kwargs: + pages: + - title: Home + type: MkText + text: "{{ metadata.description }}" + is_index: true + - Usage: + - type: MkTemplate + title: Getting started + template: https://raw.githubusercontent.com/mkdocs/mkdocs/master/docs/getting-started.md + - type: MkTemplate + title: Configuration + template: https://raw.githubusercontent.com/mkdocs/mkdocs/master/docs/user-guide/configuration.md + - type: MkTemplate + title: Deploying your docs + template: https://raw.githubusercontent.com/mkdocs/mkdocs/master/docs/user-guide/deploying-your-docs.md + - type: MkTemplate + title: Installation + template: https://raw.githubusercontent.com/mkdocs/mkdocs/master/docs/user-guide/installation.md + - title: API + type: MkDoc + section_name: "API" + recursive: true + - title: CLI + type: MkCliDoc + show_subcommands: true + condition: "{{ metadata.cli }}" + - Development: + - title: Changelog + type: MkChangelog + - title: Code of Conduct + type: MkCodeOfConduct + - title: Contributing + type: MkCommitConventions + - title: Pull requests + type: MkPullRequestGuidelines + - title: Dependencies + type: MkPage + items: + - title: Dependency table + type: MkDependencyTable + - title: Dependency tree + type: MkPipDepTree + direction: LR + - title: Dependencies + type: MkDependencyTable + - title: MkDocs Plugins + condition: '{{ "mkdocs.plugins" in metadata.entry_points }}' + type: MkPluginFlow + - title: License + type: MkLicense + plugins: - mknodes: repo_path: https://github.com/mkdocs/mkdocs.git diff --git a/mkdocs.yml b/mkdocs.yml index 38222eb..3e906f4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,7 +4,9 @@ repo_url: "https://github.com/phil65/mkdocs_mknodes/" site_url: https://phil65.github.io/mkdocs-mknodes/ site_author: Philipp Temminghoff copyright: Copyright © 2023 Philipp Temminghoff - +build_fn: mkdocs_mknodes.manual.root:Build.build +show_page_info: true +global_resources: false theme: name: material custom_dir: overrides From ecb0cfdf8d24228e907228910e46fc68aeeca2e3 Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Sun, 10 Nov 2024 20:40:31 +0100 Subject: [PATCH 08/28] chore: use settings from root level --- configs/mkdocs_mkdocs.yml | 100 ++++---------- mkdocs.yml | 159 +++++++++++------------ mkdocs_mknodes/builders/configbuilder.py | 24 ++-- mkdocs_mknodes/commands/serve.py | 11 +- mkdocs_mknodes/mkdocsconfig.py | 8 +- mkdocs_mknodes/plugin/__init__.py | 30 ++--- mkdocs_mknodes/plugin/mknodesconfig.py | 6 +- mkdocs_mknodes/plugin/pluginconfig.py | 139 +------------------- 8 files changed, 141 insertions(+), 336 deletions(-) diff --git a/configs/mkdocs_mkdocs.yml b/configs/mkdocs_mkdocs.yml index cb8c77a..ad69d85 100644 --- a/configs/mkdocs_mkdocs.yml +++ b/configs/mkdocs_mkdocs.yml @@ -8,69 +8,12 @@ repo_path: https://github.com/mkdocs/mkdocs.git clone_depth: 100 build_fn: mkdocs_mknodes:parse build_kwargs: - pages: - - title: Home - type: MkText - text: "{{ metadata.description }}" - is_index: true - - Usage: - - type: MkTemplate - title: Getting started - template: https://raw.githubusercontent.com/mkdocs/mkdocs/master/docs/getting-started.md - - type: MkTemplate - title: Configuration - template: https://raw.githubusercontent.com/mkdocs/mkdocs/master/docs/user-guide/configuration.md - - type: MkTemplate - title: Deploying your docs - template: https://raw.githubusercontent.com/mkdocs/mkdocs/master/docs/user-guide/deploying-your-docs.md - - type: MkTemplate - title: Installation - template: https://raw.githubusercontent.com/mkdocs/mkdocs/master/docs/user-guide/installation.md - - title: API - type: MkDoc - section_name: "API" - recursive: true - - title: CLI - type: MkCliDoc - show_subcommands: true - condition: "{{ metadata.cli }}" - - Development: - - title: Changelog - type: MkChangelog - - title: Code of Conduct - type: MkCodeOfConduct - - title: Contributing - type: MkCommitConventions - - title: Pull requests - type: MkPullRequestGuidelines - - title: Dependencies - type: MkPage - items: - - title: Dependency table - type: MkDependencyTable - - title: Dependency tree - type: MkPipDepTree - direction: LR - - title: Dependencies - type: MkDependencyTable - - title: MkDocs Plugins - condition: '{{ "mkdocs.plugins" in metadata.entry_points }}' - type: MkPluginFlow - - title: License - type: MkLicense - -plugins: - - mknodes: - repo_path: https://github.com/mkdocs/mkdocs.git - clone_depth: 100 - build_fn: mkdocs_mknodes:parse - build_kwargs: - pages: - - title: Home - type: MkText - text: "{{ metadata.description }}" - is_index: true - - Usage: + pages: + - title: Home + type: MkText + text: "{{ metadata.description }}" + is_index: true + - Usage: - type: MkTemplate title: Getting started template: https://raw.githubusercontent.com/mkdocs/mkdocs/master/docs/getting-started.md @@ -83,15 +26,15 @@ plugins: - type: MkTemplate title: Installation template: https://raw.githubusercontent.com/mkdocs/mkdocs/master/docs/user-guide/installation.md - - title: API - type: MkDoc - section_name: "API" - recursive: true - - title: CLI - type: MkCliDoc - show_subcommands: true - condition: "{{ metadata.cli }}" - - Development: + - title: API + type: MkDoc + section_name: "API" + recursive: true + - title: CLI + type: MkCliDoc + show_subcommands: true + condition: "{{ metadata.cli }}" + - Development: - title: Changelog type: MkChangelog - title: Code of Conduct @@ -103,11 +46,11 @@ plugins: - title: Dependencies type: MkPage items: - - title: Dependency table - type: MkDependencyTable - - title: Dependency tree - type: MkPipDepTree - direction: LR + - title: Dependency table + type: MkDependencyTable + - title: Dependency tree + type: MkPipDepTree + direction: LR - title: Dependencies type: MkDependencyTable - title: MkDocs Plugins @@ -115,3 +58,6 @@ plugins: type: MkPluginFlow - title: License type: MkLicense + +plugins: + - mknodes diff --git a/mkdocs.yml b/mkdocs.yml index 3e906f4..e3c66a8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,92 +8,89 @@ build_fn: mkdocs_mknodes.manual.root:Build.build show_page_info: true global_resources: false theme: - name: material - custom_dir: overrides - icon: - logo: material/graph-outline - palette: - # Palette toggle for automatic mode - - media: "(prefers-color-scheme)" - toggle: - icon: material/brightness-auto - name: Switch to light mode + name: material + custom_dir: overrides + icon: + logo: material/graph-outline + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode - # Palette toggle for light mode - - media: "(prefers-color-scheme: light)" - scheme: default - primary: custom - toggle: - icon: material/brightness-7 - name: Switch to dark mode + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: custom + toggle: + icon: material/brightness-7 + name: Switch to dark mode - # Palette toggle for dark mode - - media: "(prefers-color-scheme: dark)" - scheme: slate - primary: custom - toggle: - icon: material/brightness-4 - name: Switch to system preference - features: - - announce.dismiss - - content.action.edit - - content.code.copy - - content.code.select - - content.code.annotate - - content.tooltips - # - content.tabs.link - - navigation.tracking # update URL based on current item in TOC - - navigation.path # shows breadcrumbs - - navigation.tabs # make top level tabs - - navigation.indexes # documents can be directly attached to sections (overview pages) - - navigation.footer # next/previous page buttons in footer - - navigation.top # adds back-to-top button - # - navigation.sections # top-level sections are rendered as groups - # - navigation.expand # expand all subsections in left sidebar by default - - toc.follow # makes toc follow scrolling - # - toc.integrate # integrates toc into left menu - - search.highlight - - search.suggest - # - search.share + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: custom + toggle: + icon: material/brightness-4 + name: Switch to system preference + features: + - announce.dismiss + - content.action.edit + - content.code.copy + - content.code.select + - content.code.annotate + - content.tooltips + # - content.tabs.link + - navigation.tracking # update URL based on current item in TOC + - navigation.path # shows breadcrumbs + - navigation.tabs # make top level tabs + - navigation.indexes # documents can be directly attached to sections (overview pages) + - navigation.footer # next/previous page buttons in footer + - navigation.top # adds back-to-top button + # - navigation.sections # top-level sections are rendered as groups + # - navigation.expand # expand all subsections in left sidebar by default + - toc.follow # makes toc follow scrolling + # - toc.integrate # integrates toc into left menu + - search.highlight + - search.suggest + # - search.share plugins: - - search - - mknodes: - build_fn: mkdocs_mknodes.manual.root:Build.build - show_page_info: true - global_resources: false - - mkdocstrings: - default_handler: python - handlers: - python: - import: - - url: https://docs.python.org/3/objects.inv - domains: [std, py] - - url: https://phil65.github.io/mknodes/objects.inv - domains: [std, py] - options: - extensions: - - griffe_fieldz: { include_inherited: true } - # - griffe_pydantic: - # schema: true - # https://mkdocstrings.github.io/python/usage/ - show_signature_annotations: true - show_symbol_type_toc: true - show_symbol_type_heading: true - show_root_toc_entry: false - # merge_init_into_class: true - ignore_init_summary: true - inherited_members: false - signature_crossrefs: true - separate_signature: true - line_length: 90 - preload_modules: - - mknodes + - search + - mknodes + - mkdocstrings: + default_handler: python + handlers: + python: + import: + - url: https://docs.python.org/3/objects.inv + domains: [std, py] + - url: https://phil65.github.io/mknodes/objects.inv + domains: [std, py] + options: + extensions: + - griffe_fieldz: { include_inherited: true } + # - griffe_pydantic: + # schema: true + # https://mkdocstrings.github.io/python/usage/ + show_signature_annotations: true + show_symbol_type_toc: true + show_symbol_type_heading: true + show_root_toc_entry: false + # merge_init_into_class: true + ignore_init_summary: true + inherited_members: false + signature_crossrefs: true + separate_signature: true + line_length: 90 + preload_modules: + - mknodes markdown_extensions: - - attr_list - - pymdownx.emoji - - toc: - permalink: true + - attr_list + - pymdownx.emoji + - toc: + permalink: true # extra: # social: # - icon: fontawesome/brands/github diff --git a/mkdocs_mknodes/builders/configbuilder.py b/mkdocs_mknodes/builders/configbuilder.py index 496c9f7..f1cb86c 100644 --- a/mkdocs_mknodes/builders/configbuilder.py +++ b/mkdocs_mknodes/builders/configbuilder.py @@ -4,10 +4,10 @@ import os from typing import Any -from mknodes.info import mkdocsconfigfile import yamling from mkdocs_mknodes import telemetry +from mkdocs_mknodes.appconfig import appconfig from mkdocs_mknodes.plugin import mknodesconfig @@ -17,7 +17,7 @@ class ConfigBuilder: def __init__( self, - configs: list[mkdocsconfigfile.MkDocsConfigFile] | None = None, + configs: list[appconfig.AppConfig] | None = None, repo_path: str | None = ".", build_fn: str | None = None, clone_depth: int | None = 100, @@ -28,7 +28,7 @@ def __init__( self.clone_depth = clone_depth def add_config_file(self, path: str | os.PathLike[str]): - cfg = mkdocsconfigfile.MkDocsConfigFile(path) + cfg = appconfig.AppConfig.from_yaml_file(path) self.configs.append(cfg) def build_mkdocs_config( @@ -36,19 +36,17 @@ def build_mkdocs_config( ) -> mknodesconfig.MkNodesConfig: cfg = self.configs[0] if site_dir: - cfg["site_dir"] = site_dir - for plugin in cfg["plugins"]: - if "mknodes" in plugin: - if self.repo_path is not None: - plugin["mknodes"]["repo_path"] = self.repo_path - if self.build_fn is not None: - plugin["mknodes"]["build_fn"] = self.build_fn - if self.clone_depth is not None: - plugin["mknodes"]["clone_depth"] = self.clone_depth + cfg.site_dir = site_dir + if self.repo_path is not None: + cfg.repo_path = self.repo_path + if self.build_fn is not None: + cfg.build_fn = self.build_fn + if self.clone_depth is not None: + cfg.clone_depth = self.clone_depth # cfg = {**cfg, **kwargs} text = yamling.dump_yaml(dict(cfg)) buffer = io.StringIO(text) - buffer.name = cfg.path + buffer.name = cfg.config_file_path config = mknodesconfig.MkNodesConfig.from_yaml(buffer, **kwargs) for k, v in config.items(): diff --git a/mkdocs_mknodes/commands/serve.py b/mkdocs_mknodes/commands/serve.py index 4674452..3e61f59 100644 --- a/mkdocs_mknodes/commands/serve.py +++ b/mkdocs_mknodes/commands/serve.py @@ -44,11 +44,12 @@ def serve( kwargs: Optional config values (overrides value from config) """ config = mkdocsconfigfile.MkDocsConfigFile(config_path) - config.update_mknodes_section( - repo_url=repo_path, - build_fn=build_fn, - clone_depth=clone_depth, - ) + if repo_path is not None: + config._data["repo_path"] = repo_path + if build_fn is not None: + config._data["build_fn"] = build_fn + if clone_depth is not None: + config._data["clone_depth"] = clone_depth if theme and theme != "material": config.remove_plugin("social") config.remove_plugin("tags") diff --git a/mkdocs_mknodes/mkdocsconfig.py b/mkdocs_mknodes/mkdocsconfig.py index 0301e8a..7712877 100644 --- a/mkdocs_mknodes/mkdocsconfig.py +++ b/mkdocs_mknodes/mkdocsconfig.py @@ -10,7 +10,6 @@ from typing import Any, TextIO from urllib import parse -import jinjarope from mknodes.info import contexts from mknodes.mdlib import mdconverter from mknodes.utils import pathhelpers, reprhelpers @@ -203,7 +202,7 @@ def get_edit_url(self, edit_path: str | None) -> str | None: edit_uri = self.edit_uri or "edit/main/" if not edit_uri.startswith(("?", "#")) and not repo_url.endswith("/"): repo_url += "/" - rel_path = self.plugin.config.build_fn.split(":")[0] + rel_path = self.build_fn.split(":")[0] if not rel_path.endswith(".py"): rel_path = rel_path.replace(".", "/") rel_path += ".py" @@ -214,11 +213,6 @@ def get_edit_url(self, edit_path: str | None) -> str | None: rel_path = edit_path return parse.urljoin(base_url, rel_path) - def get_jinja_config(self) -> jinjarope.EnvConfig: - cfg = self.plugin.config.get_jinja_config() - cfg.loader |= jinjarope.FileSystemLoader(self.docs_dir) - return cfg - def add_js( self, path: str, diff --git a/mkdocs_mknodes/plugin/__init__.py b/mkdocs_mknodes/plugin/__init__.py index 36ae0dc..3ae5313 100644 --- a/mkdocs_mknodes/plugin/__init__.py +++ b/mkdocs_mknodes/plugin/__init__.py @@ -51,8 +51,8 @@ def on_startup(self, *, command: CommandStr, dirty: bool): def on_config(self, config: mknodesconfig.MkNodesConfig): # type: ignore """Create the project based on MkDocs config.""" - if self.config.build_folder: - self.build_folder = pathlib.Path(self.config.build_folder) + if config.build_folder: + self.build_folder = pathlib.Path(config.build_folder) else: self._dir = tempfile.TemporaryDirectory( prefix="mknodes_", @@ -61,7 +61,7 @@ def on_config(self, config: mknodesconfig.MkNodesConfig): # type: ignore self.build_folder = pathlib.Path(self._dir.name) logger.debug("Creating temporary dir %s", self._dir.name) - if not self.config.build_fn: + if not config.build_fn: return self.linkprovider = linkprovider.LinkProvider( base_url=config.site_url or "", @@ -73,8 +73,8 @@ def on_config(self, config: mknodesconfig.MkNodesConfig): # type: ignore data=dict(config.theme), ) git_repo = reporegistry.get_repo( - str(self.config.repo_path or "."), - clone_depth=self.config.clone_depth, + str(config.repo_path or "."), + clone_depth=config.clone_depth, ) self.folderinfo = folderinfo.FolderInfo(git_repo.working_dir) self.context = contexts.ProjectContext( @@ -83,7 +83,7 @@ def on_config(self, config: mknodesconfig.MkNodesConfig): # type: ignore # github=self.folderinfo.github.context, theme=self.theme.context, links=self.linkprovider, - env_config=self.config.get_jinja_config(), + env_config=config.get_jinja_config(), ) def on_files(self, files: Files, *, config: mknodesconfig.MkNodesConfig) -> Files: # type: ignore @@ -96,11 +96,11 @@ def on_files(self, files: Files, *, config: mknodesconfig.MkNodesConfig) -> File - Templates - CSS files """ - if not self.config.build_fn: + if not config.build_fn: return files logger.info("Generating pages...") - build_fn = self.config.get_builder() + build_fn = config.get_builder() self.root = mk.MkNav(context=self.context) build_fn(theme=self.theme, root=self.root) logger.debug("Finished building page.") @@ -131,9 +131,9 @@ def on_files(self, files: Files, *, config: mknodesconfig.MkNodesConfig) -> File ) collector = buildcollector.BuildCollector( backends=[mkdocs_backend, markdown_backend], - show_page_info=self.config.show_page_info, - global_resources=self.config.global_resources, - render_by_default=self.config.render_by_default, + show_page_info=config.show_page_info, + global_resources=config.global_resources, + render_by_default=config.render_by_default, ) self.build_info = collector.collect(self.root, self.theme) if nav_dict := self.root.nav.to_nav_dict(): @@ -168,7 +168,7 @@ def on_env( env: jinja2.Environment, /, *, - config: MkDocsConfig, + config: mknodesconfig.MkNodesConfig, # type: ignore files: Files, ) -> jinja2.Environment | None: """Add our own info to the MkDocs environment.""" @@ -176,7 +176,7 @@ def on_env( env.globals["mknodes"] = rope_env.globals env.filters |= rope_env.filters logger.debug("Added macros / filters to MkDocs jinja2 environment.") - if self.config.rewrite_theme_templates: + if config.rewrite_theme_templates: assert env.loader env.loader = jinjarope.RewriteLoader(env.loader, rewriteloader.rewrite) logger.debug("Injected Jinja2 Rewrite loader.") @@ -212,9 +212,9 @@ def on_page_markdown( def on_post_build(self, *, config: mknodesconfig.MkNodesConfig) -> None: # type: ignore """Delete the temporary template files.""" - if not config.theme.custom_dir or not self.config.build_fn: + if not config.theme.custom_dir or not config.build_fn: return - if self.config.auto_delete_generated_templates: + if config.auto_delete_generated_templates: logger.debug("Deleting page templates...") for template in self.build_info.templates: assert template.filename diff --git a/mkdocs_mknodes/plugin/mknodesconfig.py b/mkdocs_mknodes/plugin/mknodesconfig.py index 95d097d..5846041 100644 --- a/mkdocs_mknodes/plugin/mknodesconfig.py +++ b/mkdocs_mknodes/plugin/mknodesconfig.py @@ -247,7 +247,11 @@ def get_jinja_config(self) -> jinjarope.EnvConfig: # undefined=self.jinja_on_undefined, loader=jinjarope.loaders.from_json(self.jinja_loaders), ) - cfg.loader |= jinjarope.FileSystemLoader(self.docs_dir) # type: ignore + docs_loader = jinjarope.FileSystemLoader(self.docs_dir) + if cfg.loader: + cfg.loader |= docs_loader # type: ignore + else: + cfg.loader = docs_loader return cfg # @property diff --git a/mkdocs_mknodes/plugin/pluginconfig.py b/mkdocs_mknodes/plugin/pluginconfig.py index 3ed2e03..8b15ee0 100644 --- a/mkdocs_mknodes/plugin/pluginconfig.py +++ b/mkdocs_mknodes/plugin/pluginconfig.py @@ -2,143 +2,8 @@ from __future__ import annotations -from collections.abc import Callable -import functools -from typing import Any - -import jinjarope -from mkdocs.config import base, config_options as c -from mknodes.utils import classhelpers +from mkdocs.config import base class PluginConfig(base.Config): - build_fn = c.Type(str, default="mkdocs_mknodes:parse") - """Path to the build script / callable. - - Possible formats: - - - `my.module:Class.build_fn` (must be a classmethod / staticmethod) - - `my.module:build_fn` - - `path/to/file.py:build_fn` - - Can also be remote. - The targeted callable gets the project instance as an argument and optionally - keyword arguments from setting below. - """ - build_kwargs = c.Optional(c.Type(dict)) - """Keyword arguments passed to the build script / callable. - - Build scripts may have keyword arguments. You can set them by using this setting. - """ - repo_path = c.Type(str, default=".") - """Path to the repository to create a website for. (`http://....my_project.git`)""" - clone_depth = c.Type(int, default=100) - """Clone depth in case the repository is remote. (Required for `git-changelog`).""" - build_folder = c.Optional(c.Type(str)) - """Folder to create the Markdown files in. - - If no folder is set, **MkNodes** will generate a temporary dir.""" - show_page_info = c.Type(bool, default=False) - """Append an admonition box with build-related information. - - If True, all pages get added an expandable admonition box at the bottom, - containing information about the created page. - This includes: - - Metadata - - Resources - - Code which created the page (needs the page to be created via decorators, or - the `generated_by` attribute of the `MkPage` needs to be set manually) - """ - rewrite_theme_templates = c.Type(bool, default=True) - """Add additional functionality to themes by rewriting template files. - - MkNodes can rewrite the HTML templates of Themes in order to add additional - functionality. - - Right now, enabling this feature allows these options for the **Material-MkDocs** - theme: - - use iconify icons instead of the **Material-MkDocs** icons - - setting the theme features "navigation.indexes" and "navigation.expand" via - page metadata. - """ - auto_delete_generated_templates = c.Type(bool, default=True) - """Delete the generated HTML templates when build is finished. - - MkNodes may generate HTML template overrides during the build process and - deletes them after build. Using this setting, the deletion can be prevented. - """ - render_by_default = c.Type(bool, default=True) - """Render all pages in the jinja environment. - - This allows to render jinja in the **MkNodes** environment outside of the `MkJinja` - nodes. - - This setting can be overridden by setting the page metadata field "render_macros". - """ - global_resources = c.Type(bool, default=True) - """Make resources globally available. - - If True, then the resources inferred from the nodes will be put into all HTML pages. - (This reflects the "default" MkDocs mechanism of putting extra CSS / JS into the - config file) - If False, then MkNodes will put the CSS / JS only into the pages which need it. - (the resources will be moved into the appropriate page template blocks) - """ - jinja_loaders = c.Optional(c.ListOfItems(c.Type(dict))) - """List containing additional jinja loaders to use. - - Dictionaries must have the `type` key set to either "filesystem" or "fsspec". - - Examples: - ``` yaml - plugins: - - mknodes: - jinja_loaders: - - type: fsspec - path: github:// - repo: mknodes - org: phil65 - ``` - """ - jinja_extensions = c.Optional(c.ListOfItems(c.Type(str))) - """List containing additional jinja extensions to use. - - Examples: - ``` yaml - plugins: - - mknodes: - jinja_extensions: - - jinja2_ansible_filters.AnsibleCoreFiltersExtension - ``` - """ - jinja_block_start_string = c.Optional(c.Type(str)) - """Jinja block start string.""" - jinja_block_end_string = c.Optional(c.Type(str)) - """Jinja block end string.""" - jinja_variable_start_string = c.Optional(c.Type(str)) - """Jinja variable start string.""" - jinja_variable_end_string = c.Optional(c.Type(str)) - """Jinja variable end string.""" - jinja_on_undefined = c.Type(str, default="strict") - """Jinja undefined macro behavior.""" - llm_base_url = c.Type(str, default="http://localhost:11434") - """Base URL for LLM usage.""" - llm_token = c.Optional(c.Type(str)) - """(Optional) token for the LLM API.""" - llm_model_name = c.Optional(c.Type(str)) - """LLM model name to use.""" - - def get_builder(self) -> Callable[..., Any]: - build_fn = classhelpers.to_callable(self.build_fn) - build_kwargs = self.build_kwargs or {} - return functools.partial(build_fn, **build_kwargs) - - def get_jinja_config(self) -> jinjarope.EnvConfig: - return jinjarope.EnvConfig( - block_start_string=self.jinja_block_start_string or "{%", - block_end_string=self.jinja_block_end_string or "%}", - variable_start_string=self.jinja_variable_start_string or r"{{", - variable_end_string=self.jinja_variable_end_string or r"}}", - # undefined=self.jinja_on_undefined, - loader=jinjarope.loaders.from_json(self.jinja_loaders), - ) + """Empty PluginConfigs.""" From c299396f3d7ad89e1036f59c74d8371cc9391cab Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Sun, 10 Nov 2024 21:05:55 +0100 Subject: [PATCH 09/28] chore: cleanup --- mkdocs_mknodes/commands/build_page.py | 62 ++------------------ mkdocs_mknodes/commands/templatecontext.py | 67 ++++++++++++++++++++++ 2 files changed, 71 insertions(+), 58 deletions(-) create mode 100644 mkdocs_mknodes/commands/templatecontext.py diff --git a/mkdocs_mknodes/commands/build_page.py b/mkdocs_mknodes/commands/build_page.py index a4b6ac0..f7b0c33 100644 --- a/mkdocs_mknodes/commands/build_page.py +++ b/mkdocs_mknodes/commands/build_page.py @@ -5,7 +5,7 @@ from collections.abc import Collection, Sequence from datetime import UTC, datetime import os -from typing import TYPE_CHECKING, Any, TypedDict +from typing import TYPE_CHECKING, Any from urllib.parse import urljoin, urlsplit import jinja2 @@ -21,7 +21,7 @@ from mkdocs_mknodes import telemetry from mkdocs_mknodes.builders import configbuilder -from mkdocs_mknodes.commands import utils +from mkdocs_mknodes.commands import templatecontext, utils if TYPE_CHECKING: @@ -33,19 +33,6 @@ logger = telemetry.get_plugin_logger(__name__) -class TemplateContext(TypedDict): - nav: Navigation - pages: Sequence[File] - base_url: str - extra_css: Sequence[str] # Do not use, prefer `config.extra_css`. - extra_javascript: Sequence[str] # Do not use, prefer `config.extra_javascript`. - mkdocs_version: str - mknodes_version: str - build_date_utc: datetime - config: MkNodesConfig - page: Page | None - - DRAFT_CONTENT = ( '
' "DRAFT" @@ -242,7 +229,7 @@ def _build_page( logger.debug("Building page %s", page.file.src_uri) # Activate page. Signals to theme that this is the current page. page.active = True - ctx = get_context(nav, doc_files, config, page) + ctx = templatecontext.get_context(nav, doc_files, config, page) # Allow 'template:' override in md source files. template = env.get_template(page.meta.get("template", "main.html")) # Run `page_context` plugin events. @@ -294,7 +281,7 @@ def _build_template( base_url = urlsplit(config.site_url or "/").path else: base_url = htmlfilters.relative_url_mkdocs(".", name) - context = get_context(nav, files, config, base_url=base_url) + context = templatecontext.get_context(nav, files, config, base_url=base_url) ctx = config.plugins.on_template_context(context, template_name=name, config=config) # type: ignore output = template.render(ctx) return config.plugins.on_post_template(output, template_name=name, config=config) @@ -372,47 +359,6 @@ def get_build_timestamp(*, pages: Collection[Page] | None = None) -> int: return int(dt.timestamp()) -def get_context( - nav: Navigation, - files: Sequence[File] | Files, - config: MkNodesConfig, - page: Page | None = None, - base_url: str = "", -) -> TemplateContext: - """Return the template context for a given page or template.""" - if page is not None: - base_url = htmlfilters.relative_url_mkdocs(".", page.url) - - extra_javascript = [ - htmlfilters.normalize_url(str(script), page.url if page else None, base_url) - for script in config.extra_javascript - ] - extra_css = [ - htmlfilters.normalize_url(path, page.url if page else None, base_url) - for path in config.extra_css - ] - - if isinstance(files, Files): - files = files.documentation_pages() - - import mkdocs - - import mkdocs_mknodes - - return TemplateContext( - nav=nav, - pages=files, - base_url=base_url, - extra_css=extra_css, - extra_javascript=extra_javascript, - mknodes_version=mkdocs_mknodes.__version__, - mkdocs_version=mkdocs.__version__, - build_date_utc=utils.get_build_datetime(), - config=config, - page=page, - ) - - if __name__ == "__main__": from mkdocs_mknodes.appconfig import appconfig diff --git a/mkdocs_mknodes/commands/templatecontext.py b/mkdocs_mknodes/commands/templatecontext.py new file mode 100644 index 0000000..740eb89 --- /dev/null +++ b/mkdocs_mknodes/commands/templatecontext.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from collections.abc import Sequence +from datetime import datetime +from typing import TypedDict + +from jinjarope import htmlfilters +from mkdocs import utils +from mkdocs.structure.files import File, Files +from mkdocs.structure.nav import Navigation +from mkdocs.structure.pages import Page + +from mkdocs_mknodes.plugin.mknodesconfig import MkNodesConfig + + +class TemplateContext(TypedDict): + nav: Navigation + pages: Sequence[File] + base_url: str + extra_css: Sequence[str] # Do not use, prefer `config.extra_css`. + extra_javascript: Sequence[str] # Do not use, prefer `config.extra_javascript`. + mkdocs_version: str + mknodes_version: str + build_date_utc: datetime + config: MkNodesConfig + page: Page | None + + +def get_context( + nav: Navigation, + files: Sequence[File] | Files, + config: MkNodesConfig, + page: Page | None = None, + base_url: str = "", +) -> TemplateContext: + """Return the template context for a given page or template.""" + if page is not None: + base_url = htmlfilters.relative_url_mkdocs(".", page.url) + + extra_javascript = [ + htmlfilters.normalize_url(str(script), page.url if page else None, base_url) + for script in config.extra_javascript + ] + extra_css = [ + htmlfilters.normalize_url(path, page.url if page else None, base_url) + for path in config.extra_css + ] + + if isinstance(files, Files): + files = files.documentation_pages() + + import mkdocs + + import mkdocs_mknodes + + return TemplateContext( + nav=nav, + pages=files, + base_url=base_url, + extra_css=extra_css, + extra_javascript=extra_javascript, + mknodes_version=mkdocs_mknodes.__version__, + mkdocs_version=mkdocs.__version__, + build_date_utc=utils.get_build_datetime(), + config=config, + page=page, + ) From 1a373697812ef87a14c87f2659883d70902b1d93 Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Sun, 10 Nov 2024 21:10:32 +0100 Subject: [PATCH 10/28] chore: cleanup --- mkdocs_mknodes/commands/build_page.py | 27 +++------------------------ mkdocs_mknodes/commands/utils.py | 24 +++++++++++++++++++++++- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/mkdocs_mknodes/commands/build_page.py b/mkdocs_mknodes/commands/build_page.py index f7b0c33..7d8effc 100644 --- a/mkdocs_mknodes/commands/build_page.py +++ b/mkdocs_mknodes/commands/build_page.py @@ -2,8 +2,7 @@ from __future__ import annotations -from collections.abc import Collection, Sequence -from datetime import UTC, datetime +from collections.abc import Sequence import os from typing import TYPE_CHECKING, Any from urllib.parse import urljoin, urlsplit @@ -310,7 +309,8 @@ def _build_theme_template( pathhelpers.write_file(output.encode(), output_path) if template_name == "sitemap.xml": docs = files.documentation_pages() - ts = get_build_timestamp(pages=[f.page for f in docs if f.page is not None]) + pages = [f.page for f in docs if f.page is not None] + ts = utils.get_build_timestamp(pages=pages) utils.write_gzip(f"{output_path}.gz", output, timestamp=ts) else: logger.info("Template skipped: %r generated empty output.", template_name) @@ -338,27 +338,6 @@ def _build_extra_template( logger.info("Template skipped: %r generated empty output.", template_name) -def get_build_timestamp(*, pages: Collection[Page] | None = None) -> int: - """Returns the number of seconds since the epoch for the latest updated page. - - In reality this is just today's date because that's how pages' update time - is populated. - - Args: - pages: Optional collection of pages to determine timestamp from - - Returns: - Unix timestamp as integer - """ - if pages: - # Lexicographic comparison is OK for ISO date. - date_string = max(p.update_date for p in pages) - dt = datetime.fromisoformat(date_string).replace(tzinfo=UTC) - else: - dt = utils.get_build_datetime() - return int(dt.timestamp()) - - if __name__ == "__main__": from mkdocs_mknodes.appconfig import appconfig diff --git a/mkdocs_mknodes/commands/utils.py b/mkdocs_mknodes/commands/utils.py index b58292d..cea66cc 100644 --- a/mkdocs_mknodes/commands/utils.py +++ b/mkdocs_mknodes/commands/utils.py @@ -3,7 +3,7 @@ from __future__ import annotations import collections -from collections.abc import Callable, Iterable +from collections.abc import Callable, Collection, Iterable import contextlib import datetime import functools @@ -17,6 +17,7 @@ from mkdocs import exceptions from mkdocs.structure.files import File, Files, InclusionLevel, _file_sort_key +from mkdocs.structure.pages import Page import pathspec import upath @@ -211,3 +212,24 @@ def is_error_template(path: str) -> bool: True if path matches error template pattern """ return bool(_ERROR_TEMPLATE_RE.match(path)) + + +def get_build_timestamp(*, pages: Collection[Page] | None = None) -> int: + """Returns the number of seconds since the epoch for the latest updated page. + + In reality this is just today's date because that's how pages' update time + is populated. + + Args: + pages: Optional collection of pages to determine timestamp from + + Returns: + Unix timestamp as integer + """ + if pages: + # Lexicographic comparison is OK for ISO date. + date_string = max(p.update_date for p in pages) + dt = datetime.datetime.fromisoformat(date_string).replace(tzinfo=datetime.UTC) + else: + dt = get_build_datetime() + return int(dt.timestamp()) From 6d24d5b774c7cce8580fb4965cd7040268dd9b9f Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Mon, 11 Nov 2024 11:46:37 +0100 Subject: [PATCH 11/28] chore: config work --- mkdocs_mknodes/appconfig/validationconfig.py | 16 +-- mkdocs_mknodes/builders/configbuilder.py | 2 +- mkdocs_mknodes/commands/serve.py | 14 +-- mkdocs_mknodes/plugin/mknodesconfig.py | 119 ++++++++++--------- 4 files changed, 77 insertions(+), 74 deletions(-) diff --git a/mkdocs_mknodes/appconfig/validationconfig.py b/mkdocs_mknodes/appconfig/validationconfig.py index 28d3429..dcec3bf 100644 --- a/mkdocs_mknodes/appconfig/validationconfig.py +++ b/mkdocs_mknodes/appconfig/validationconfig.py @@ -2,7 +2,7 @@ from enum import Enum -from pydantic import BaseModel +from pydantic import BaseModel, Field class ValidationLevel(Enum): @@ -32,10 +32,10 @@ class Links(BaseModel): class ValidationConfig(BaseModel): - nav: Nav | None = None - links: Links | None = None - omitted_files: ValidationLevel | None = None - not_found: ValidationLevel | None = None - absolute_links: ValidationLevelForAbsolute | None = None - anchors: ValidationLevel | None = None - unrecognized_links: ValidationLevel | None = None + nav: Nav = Field(default_factory=Nav) + links: Links = Field(default_factory=Links) + omitted_files: ValidationLevel = ValidationLevel.info + not_found: ValidationLevel = ValidationLevel.warn + absolute_links: ValidationLevelForAbsolute = ValidationLevelForAbsolute.info + anchors: ValidationLevel = ValidationLevel.info + unrecognized_links: ValidationLevel = ValidationLevel.info diff --git a/mkdocs_mknodes/builders/configbuilder.py b/mkdocs_mknodes/builders/configbuilder.py index f1cb86c..96e6a4a 100644 --- a/mkdocs_mknodes/builders/configbuilder.py +++ b/mkdocs_mknodes/builders/configbuilder.py @@ -44,7 +44,7 @@ def build_mkdocs_config( if self.clone_depth is not None: cfg.clone_depth = self.clone_depth # cfg = {**cfg, **kwargs} - text = yamling.dump_yaml(dict(cfg)) + text = yamling.dump_yaml(cfg.model_dump(mode="json")) buffer = io.StringIO(text) buffer.name = cfg.config_file_path config = mknodesconfig.MkNodesConfig.from_yaml(buffer, **kwargs) diff --git a/mkdocs_mknodes/commands/serve.py b/mkdocs_mknodes/commands/serve.py index 3e61f59..0b688fa 100644 --- a/mkdocs_mknodes/commands/serve.py +++ b/mkdocs_mknodes/commands/serve.py @@ -10,7 +10,6 @@ from urllib.parse import urlsplit # from mkdocs.commands import serve as serve_ -from mknodes.info import mkdocsconfigfile from mknodes.utils import log import yamling @@ -43,16 +42,16 @@ def serve( theme: Theme to use kwargs: Optional config values (overrides value from config) """ - config = mkdocsconfigfile.MkDocsConfigFile(config_path) + config = mknodesconfig.MkNodesConfig.from_yaml_file(config_path, validate=False) if repo_path is not None: - config._data["repo_path"] = repo_path + config.repo_path = repo_path if build_fn is not None: - config._data["build_fn"] = build_fn + config.build_fn = build_fn if clone_depth is not None: - config._data["clone_depth"] = clone_depth + config.clone_depth = clone_depth if theme and theme != "material": - config.remove_plugin("social") - config.remove_plugin("tags") + # config.remove_plugin("social") + # config.remove_plugin("tags") kwargs["theme"] = theme text = yamling.dump_yaml(dict(config)) stream = io.StringIO(text) @@ -130,7 +129,6 @@ def get_config() -> mknodesconfig.MkNodesConfig: is_clean = build_type == "clean" is_dirty = build_type == "dirty" - config = get_config() config.plugins.on_startup(command=("build" if is_clean else "serve"), dirty=is_dirty) diff --git a/mkdocs_mknodes/plugin/mknodesconfig.py b/mkdocs_mknodes/plugin/mknodesconfig.py index 5846041..81cc9fb 100644 --- a/mkdocs_mknodes/plugin/mknodesconfig.py +++ b/mkdocs_mknodes/plugin/mknodesconfig.py @@ -76,6 +76,7 @@ def from_yaml( config_file: str | TextIO | None = None, *, config_file_path: str | None = None, + validate: bool = True, **kwargs: Any, ) -> Self: """Load the configuration for a given file object or name. @@ -97,19 +98,19 @@ def from_yaml( cfg.update(dct) # Then load the options to overwrite anything in the config. cfg.update(options) - - errors, warnings = cfg.validate() - - for config_name, warning in warnings + errors: - logger.warning("Config value %r: %s", config_name, warning) - for k, v in cfg.items(): - logger.debug("Config value %r = %r", k, v) - if len(errors) > 0: - msg = f"Aborted with {len(errors)} configuration errors!" - raise SystemExit(msg) - if cfg.strict and len(warnings) > 0: - msg = f"Aborted with {len(warnings)} configuration warnings in 'strict' mode!" - raise SystemExit(msg) + if validate: + errors, warnings = cfg.validate() + + for config_name, warning in warnings + errors: + logger.warning("Config value %r: %s", config_name, warning) + for k, v in cfg.items(): + logger.debug("Config value %r = %r", k, v) + if len(errors) > 0: + msg = f"Aborted with {len(errors)} configuration errors!" + raise SystemExit(msg) + if cfg.strict and len(warnings) > 0: + msg = f"Strict mode: Aborted with {len(warnings)} config warnings" + raise SystemExit(msg) return cfg @classmethod @@ -117,11 +118,12 @@ def from_yaml_file( cls, file: str | os.PathLike[str], config_file_path: str | None = None, + validate: bool = True, ) -> Self: # cfg = yamling.load_yaml_file(file, resolve_inherit=True) config_str = upath.UPath(file).read_text() str_io = io.StringIO(config_str) - return cls.from_yaml(str_io, config_file_path=config_file_path) + return cls.from_yaml(str_io, config_file_path=config_file_path, validate=validate) build_fn = c.Type(str, default="mkdocs_mknodes:parse") """Path to the build script / callable. @@ -195,43 +197,44 @@ def from_yaml_file( If False, then MkNodes will put the CSS / JS only into the pages which need it. (the resources will be moved into the appropriate page template blocks) """ - jinja_loaders = c.Optional(c.ListOfItems(c.Type(dict))) - """List containing additional jinja loaders to use. - - Dictionaries must have the `type` key set to either "filesystem" or "fsspec". - - Examples: - ``` yaml - plugins: - - mknodes: - jinja_loaders: - - type: fsspec - path: github:// - repo: mknodes - org: phil65 - ``` - """ - jinja_extensions = c.Optional(c.ListOfItems(c.Type(str))) - """List containing additional jinja extensions to use. - - Examples: - ``` yaml - plugins: - - mknodes: - jinja_extensions: - - jinja2_ansible_filters.AnsibleCoreFiltersExtension - ``` - """ - jinja_block_start_string = c.Optional(c.Type(str)) - """Jinja block start string.""" - jinja_block_end_string = c.Optional(c.Type(str)) - """Jinja block end string.""" - jinja_variable_start_string = c.Optional(c.Type(str)) - """Jinja variable start string.""" - jinja_variable_end_string = c.Optional(c.Type(str)) - """Jinja variable end string.""" - jinja_on_undefined = c.Type(str, default="strict") - """Jinja undefined macro behavior.""" + jinja_config = c.Type(dict, default={}) + # jinja_loaders = c.Optional(c.ListOfItems(c.Type(dict))) + # """List containing additional jinja loaders to use. + + # Dictionaries must have the `type` key set to either "filesystem" or "fsspec". + + # Examples: + # ``` yaml + # plugins: + # - mknodes: + # jinja_loaders: + # - type: fsspec + # path: github:// + # repo: mknodes + # org: phil65 + # ``` + # """ + # jinja_extensions = c.Optional(c.ListOfItems(c.Type(str))) + # """List containing additional jinja extensions to use. + + # Examples: + # ``` yaml + # plugins: + # - mknodes: + # jinja_extensions: + # - jinja2_ansible_filters.AnsibleCoreFiltersExtension + # ``` + # """ + # jinja_block_start_string = c.Optional(c.Type(str)) + # """Jinja block start string.""" + # jinja_block_end_string = c.Optional(c.Type(str)) + # """Jinja block end string.""" + # jinja_variable_start_string = c.Optional(c.Type(str)) + # """Jinja variable start string.""" + # jinja_variable_end_string = c.Optional(c.Type(str)) + # """Jinja variable end string.""" + # jinja_on_undefined = c.Type(str, default="strict") + # """Jinja undefined macro behavior.""" def get_builder(self) -> Callable[..., Any]: build_fn = classhelpers.to_callable(self.build_fn) @@ -240,12 +243,14 @@ def get_builder(self) -> Callable[..., Any]: def get_jinja_config(self) -> jinjarope.EnvConfig: cfg = jinjarope.EnvConfig( - block_start_string=self.jinja_block_start_string or "{%", - block_end_string=self.jinja_block_end_string or "%}", - variable_start_string=self.jinja_variable_start_string or r"{{", - variable_end_string=self.jinja_variable_end_string or r"}}", - # undefined=self.jinja_on_undefined, - loader=jinjarope.loaders.from_json(self.jinja_loaders), + block_start_string=self.jinja_config.get("jinja_block_start_string") or "{%", + block_end_string=self.jinja_config.get("jinja_block_end_string") or "%}", + variable_start_string=self.jinja_config.get("jinja_variable_start_string") + or r"{{", + variable_end_string=self.jinja_config.get("jinja_variable_end_string") + or r"}}", + # undefined=self.jinja_config.get("jinja_on_undefined"), + loader=jinjarope.loaders.from_json(self.jinja_config.get("jinja_loaders")), ) docs_loader = jinjarope.FileSystemLoader(self.docs_dir) if cfg.loader: From 7d7959da7cf28af3a680bd7b993d2b8f6aa28827 Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Mon, 11 Nov 2024 13:27:30 +0100 Subject: [PATCH 12/28] chore: make build_page content a class --- mkdocs_mknodes/commands/build_page.py | 693 ++++++++++++++++---------- mkdocs_mknodes/commands/utils.py | 14 +- 2 files changed, 432 insertions(+), 275 deletions(-) diff --git a/mkdocs_mknodes/commands/build_page.py b/mkdocs_mknodes/commands/build_page.py index 7d8effc..09c4359 100644 --- a/mkdocs_mknodes/commands/build_page.py +++ b/mkdocs_mknodes/commands/build_page.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Sequence +from collections.abc import Callable, Sequence import os from typing import TYPE_CHECKING, Any from urllib.parse import urljoin, urlsplit @@ -39,6 +39,418 @@ ) +class MkDocsSiteBuilder: + """Main class for building MkDocs sites.""" + + def __init__(self, config: MkNodesConfig | None = None): + """Initialize the site builder. + + Args: + config: Optional MkDocs configuration + """ + self.config = config or MkNodesConfig() + + def build_from_config( + self, + config_path: str | os.PathLike[str], + repo_path: str, + build_fn: str | None, + *, + site_dir: str | None = None, + clone_depth: int = 100, + **kwargs: Any, + ) -> None: + """Build a MkNodes-based website from config file. + + Args: + config_path: Path to the MkDocs config file + repo_path: Repository path/URL to build docs for + build_fn: Fully qualified name of build function to use + site_dir: Output directory for built site + clone_depth: Number of commits to fetch for Git repos + kwargs: Additional config overrides passed to MkDocs + """ + cfg_builder = configbuilder.ConfigBuilder( + repo_path=repo_path, build_fn=build_fn, clone_depth=clone_depth + ) + cfg_builder.add_config_file(config_path) + self.config = cfg_builder.build_mkdocs_config(site_dir=site_dir, **kwargs) + + with logfire.span("plugins callback: on_startup", config=self.config): + self.config.plugins.on_startup(command="build", dirty=False) + self.build_site() + with logfire.span("plugins callback: shutdown", config=self.config): + self.config.plugins.on_shutdown() + + @utils.handle_exceptions + @utils.count_warnings + def build_site(self, live_server_url: str | None = None, dirty: bool = False) -> None: + """Build a MkNodes-based website. + + Args: + live_server_url: An optional URL of the live server to use + dirty: Do a dirty build + """ + if self.config is None: + msg = "Configuration must be set before building site" + raise ValueError(msg) + + with logfire.span("plugins callback: on_config", config=self.config): + self.config = self.config.plugins.on_config(self.config) + with logfire.span("plugins callback: on_pre_build", config=self.config): + self.config.plugins.on_pre_build(config=self.config) + + if not dirty: + logger.info("Cleaning site directory") + pathhelpers.clean_directory(self.config.site_dir) + else: # pragma: no cover + logger.warning( + "A 'dirty' build is being performed (for site dev purposes only)" + ) + + if not live_server_url: # pragma: no cover + logger.info("Building documentation to directory: %s", self.config.site_dir) + if dirty and envtests.contains_files(self.config.site_dir): + logger.info( + "The directory contains stale files. Use --clean to remove them." + ) + + files = utils.get_files(self.config) + env = self.config.theme.get_env() + files.add_files_from_theme(env, self.config) + + with logfire.span("plugins callback: on_files", files=files, config=self.config): + files = self.config.plugins.on_files(files, config=self.config) + + utils.set_exclusions(files._files, self.config) + nav = get_navigation(files, self.config) + + with logfire.span("plugins callback: on_nav", config=self.config, nav=nav): + nav = self.config.plugins.on_nav(nav, config=self.config, files=files) + + self._process_pages(files, live_server_url, dirty) + + with logfire.span("plugins callback: on_env", env=env, config=self.config): + env = self.config.plugins.on_env(env, config=self.config, files=files) + + with logfire.span("copy_static_files"): + inclusion = ( + InclusionLevel.is_in_serve + if live_server_url + else InclusionLevel.is_included + ) + files.copy_static_files(dirty=dirty, inclusion=inclusion) + + self._build_templates(env, files, nav) + self._build_pages(files, nav, env, dirty, inclusion) + + with logfire.span("plugins callback: on_post_build", config=self.config): + self.config.plugins.on_post_build(config=self.config) + + def _process_pages( + self, files: Files, live_server_url: str | None, dirty: bool + ) -> None: + """Process all pages, reading their content and applying plugins. + + Args: + files: Collection of files to process + live_server_url: Optional live server URL + dirty: Whether this is a dirty build + """ + excluded: list[str] = [] + inclusion = ( + InclusionLevel.is_in_serve if live_server_url else InclusionLevel.is_included + ) + + with logfire.span("populate pages"): + for file in files.documentation_pages(inclusion=inclusion): + with logfire.span(f"populate page for {file.src_uri}", file=file): + logger.debug("Reading: %s", file.src_uri) + if file.page is None and file.inclusion.is_not_in_nav(): + Page(None, file, self.config) + if live_server_url and file.inclusion.is_excluded(): + excluded.append(urljoin(live_server_url, file.url)) + assert file.page is not None + self._populate_page(file.page, files, dirty) + + if excluded: + excluded_str = "\n - ".join(excluded) + logger.info( + "The following pages are being built only for the preview " + "but will be excluded from `mkdocs build` per `draft_docs` config:" + "\n - %s", + excluded_str, + ) + + def _populate_page(self, page: Page, files: Files, dirty: bool = False) -> None: + """Read page content from docs_dir and render Markdown. + + Args: + page: Page to populate + files: Collection of files + dirty: Whether this is a dirty build + """ + self.config._current_page = page + try: + if dirty and not page.file.is_modified(): + return + + with logfire.span( + "plugins callback: on_pre_page", page=page, config=self.config + ): + page = self.config.plugins.on_pre_page( + page, config=self.config, files=files + ) + + with logfire.span("read_source", page=page): + page.read_source(self.config) + assert page.markdown is not None + + with logfire.span( + "plugins callback: on_page_markdown", page=page, config=self.config + ): + page.markdown = self.config.plugins.on_page_markdown( + page.markdown, page=page, config=self.config, files=files + ) + + with logfire.span("render", page=page, config=self.config): + page.render(self.config, files) + assert page.content is not None + + with logfire.span( + "plugins callback: on_page_content", page=page, config=self.config + ): + page.content = self.config.plugins.on_page_content( + page.content, page=page, config=self.config, files=files + ) + except Exception as e: + message = f"Error reading page '{page.file.src_uri}':" + if not isinstance(e, exceptions.BuildError): + message += f" {e}" + logger.exception(message) + raise + finally: + self.config._current_page = None + + def _build_templates( + self, env: jinja2.Environment, files: Files, nav: Navigation + ) -> None: + """Build all templates. + + Args: + env: Jinja environment + files: Collection of files + nav: Navigation structure + """ + with logfire.span("build_templates"): + for template in self.config.theme.static_templates: + self._build_theme_template(template, env, files, nav) + for template in self.config.extra_templates: + self._build_extra_template(template, files, nav) + + def _build_pages( + self, + files: Files, + nav: Navigation, + env: jinja2.Environment, + dirty: bool, + inclusion: Callable[[InclusionLevel], bool], + ) -> None: + """Build all pages. + + Args: + files: Collection of files + nav: Navigation structure + env: Jinja environment + dirty: Whether this is a dirty build + inclusion: Inclusion level for pages + """ + logger.debug("Building markdown pages.") + doc_files = files.documentation_pages(inclusion=inclusion) + + with logfire.span("build_pages"): + for file in doc_files: + assert file.page + excl = file.inclusion.is_excluded() + with logfire.span(f"build_page {file.page.url}", page=file.page): + self._build_page(file.page, doc_files, nav, env, dirty, excl) + + log_level = self.config.validation.links.anchors + with logfire.span("validate_anchor_links"): + for file in doc_files: + assert file.page is not None + file.page.validate_anchor_links(files=files, log_level=log_level) + + def _build_page( + self, + page: Page, + doc_files: Sequence[File], + nav: Navigation, + env: jinja2.Environment, + dirty: bool = False, + excluded: bool = False, + ) -> None: + """Build a single page. + + Args: + page: Page to build + doc_files: Collection of documentation files + nav: Navigation structure + env: Jinja environment + dirty: Whether this is a dirty build + excluded: Whether the page is excluded + """ + self.config._current_page = page + try: + if dirty and not page.file.is_modified(): + return + + logger.debug("Building page %s", page.file.src_uri) + page.active = True + + ctx = templatecontext.get_context(nav, doc_files, self.config, page) + template = env.get_template(page.meta.get("template", "main.html")) + ctx = self.config.plugins.on_page_context( + ctx, # type: ignore + page=page, + config=self.config, # type: ignore + nav=nav, + ) + + if excluded: + page.content = DRAFT_CONTENT + (page.content or "") + + output = template.render(ctx) + output = self.config.plugins.on_post_page( + output, page=page, config=self.config + ) + + if output.strip(): + text = output.encode("utf-8", errors="xmlcharrefreplace") + pathhelpers.write_file(text, page.file.abs_dest_path) + else: + logger.info( + "Page skipped: '%s'. Generated empty output.", page.file.src_uri + ) + + except Exception as e: + message = f"Error building page '{page.file.src_uri}':" + if not isinstance(e, exceptions.BuildError): + message += f" {e}" + logger.exception(message) + raise + finally: + page.active = False + self.config._current_page = None + + def _build_template( + self, + name: str, + template: jinja2.Template, + files: Files, + nav: Navigation, + ) -> str: + """Build a template and return its rendered output. + + Args: + name: Template name + template: Template object + files: Collection of files + nav: Navigation structure + + Returns: + Rendered template as string + """ + template = self.config.plugins.on_pre_template( + template, template_name=name, config=self.config + ) + + if utils.is_error_template(name): + base_url = urlsplit(self.config.site_url or "/").path + else: + base_url = htmlfilters.relative_url_mkdocs(".", name) + + context = templatecontext.get_context(nav, files, self.config, base_url=base_url) + ctx = self.config.plugins.on_template_context( + context, # type: ignore + template_name=name, + config=self.config, # type: ignore + ) + output = template.render(ctx) + return self.config.plugins.on_post_template( + output, template_name=name, config=self.config + ) + + def _build_theme_template( + self, + template_name: str, + env: jinja2.Environment, + files: Files, + nav: Navigation, + ) -> None: + """Build a theme template. + + Args: + template_name: Name of the template + env: Jinja environment + files: Collection of files + nav: Navigation structure + """ + logger.debug("Building theme template: %s", template_name) + + try: + template = env.get_template(template_name) + except TemplateNotFound: + logger.warning("Template skipped: %r not found in theme dirs.", template_name) + return + + output = self._build_template(template_name, template, files, nav) + + if output.strip(): + output_path = upath.UPath(self.config.site_dir) / template_name + pathhelpers.write_file(output.encode(), output_path) + if template_name == "sitemap.xml": + docs = files.documentation_pages() + pages = [f.page for f in docs if f.page is not None] + ts = utils.get_build_timestamp(pages=pages) + utils.write_gzip(f"{output_path}.gz", output, timestamp=ts) + else: + logger.info("Template skipped: %r generated empty output.", template_name) + + def _build_extra_template( + self, + template_name: str, + files: Files, + nav: Navigation, + ) -> None: + """Build a user template not part of the theme. + + Args: + template_name: Name of the template + files: Collection of files + nav: Navigation structure + """ + logger.debug("Building extra template: %s", template_name) + + file = files.get_file_from_path(template_name) + if file is None: + logger.warning("Template skipped: %r not found in docs_dir.", template_name) + return + + try: + template = jinja2.Template(file.content_string) + except Exception: + logger.exception("Error reading template %r", template_name) + return + + output = self._build_template(template_name, template, files, nav) + if output.strip(): + pathhelpers.write_file(output.encode(), file.abs_dest_path) + else: + logger.info("Template skipped: %r generated empty output.", template_name) + + +# Backward compatibility functions def build( config_path: str | os.PathLike[str], repo_path: str, @@ -47,7 +459,7 @@ def build( site_dir: str | None = None, clone_depth: int = 100, **kwargs: Any, -): +) -> None: """Build a MkNodes-based website. Args: @@ -58,20 +470,17 @@ def build( clone_depth: Number of commits to fetch for Git repos kwargs: Additional config overrides passed to MkDocs """ - cfg_builder = configbuilder.ConfigBuilder( - repo_path=repo_path, build_fn=build_fn, clone_depth=clone_depth + builder = MkDocsSiteBuilder() + builder.build_from_config( + config_path=config_path, + repo_path=repo_path, + build_fn=build_fn, + site_dir=site_dir, + clone_depth=clone_depth, + **kwargs, ) - cfg_builder.add_config_file(config_path) - config = cfg_builder.build_mkdocs_config(site_dir=site_dir, **kwargs) - with logfire.span("plugins callback: on_startup", config=config): - config.plugins.on_startup(command="build", dirty=False) - _build(config) - with logfire.span("plugins callback: shutdown", config=config): - config.plugins.on_shutdown() -@utils.handle_exceptions -@utils.count_warnings def _build( config: MkNodesConfig, live_server_url: str | None = None, @@ -79,268 +488,18 @@ def _build( ) -> None: """Build a MkNodes-based website. Also used for serving. - This method does NOT call the the startup / shutdown event hooks. - If that is desired, build() should be called. - Args: config: Config to use live_server_url: An optional URL of the live server to use dirty: Do a dirty build """ - with logfire.span("plugins callback: on_config", config=config): - config = config.plugins.on_config(config) - with logfire.span("plugins callback: on_pre_build", config=config): - config.plugins.on_pre_build(config=config) - - if not dirty: - logger.info("Cleaning site directory") - pathhelpers.clean_directory(config.site_dir) - else: # pragma: no cover - logger.warning("A 'dirty' build is being performed (for site dev purposes only)") - if not live_server_url: # pragma: no cover - logger.info("Building documentation to directory: %s", config.site_dir) - if dirty and envtests.contains_files(config.site_dir): - logger.info("The directory contains stale files. Use --clean to remove them.") - # First gather all data from all files/pages to ensure all data is - # consistent across all pages. - files = utils.get_files(config) - env = config.theme.get_env() - files.add_files_from_theme(env, config) - with logfire.span("plugins callback: on_files", files=files, config=config): - files = config.plugins.on_files(files, config=config) - # If plugins have added files without setting inclusion level, calculate it again. - utils.set_exclusions(files._files, config) - nav = get_navigation(files, config) - # Run `nav` plugin events. - with logfire.span("plugins callback: on_nav", config=config, nav=nav): - nav = config.plugins.on_nav(nav, config=config, files=files) - logger.debug("Reading markdown pages.") - excluded: list[str] = [] - inclusion = ( - InclusionLevel.is_in_serve if live_server_url else InclusionLevel.is_included - ) - with logfire.span("populate pages"): - for file in files.documentation_pages(inclusion=inclusion): - with logfire.span(f"populate page for {file.src_uri}", file=file): - logger.debug("Reading: %s", file.src_uri) - if file.page is None and file.inclusion.is_not_in_nav(): - Page(None, file, config) - if live_server_url and file.inclusion.is_excluded(): - excluded.append(urljoin(live_server_url, file.url)) - assert file.page is not None - _populate_page(file.page, config, files, dirty) - if excluded: - excluded_str = "\n - ".join(excluded) - logger.info( - "The following pages are being built only for the preview " - "but will be excluded from `mkdocs build` per `draft_docs` config:" - "\n - %s", - excluded_str, - ) - with logfire.span("plugins callback: on_env", env=env, config=config): - env = config.plugins.on_env(env, config=config, files=files) - logger.debug("Copying static assets.") - with logfire.span("copy_static_files"): - files.copy_static_files(dirty=dirty, inclusion=inclusion) - - with logfire.span("build_templates"): - for template in config.theme.static_templates: - _build_theme_template(template, env, files, config, nav) - for template in config.extra_templates: - _build_extra_template(template, files, config, nav) - - logger.debug("Building markdown pages.") - doc_files = files.documentation_pages(inclusion=inclusion) - with logfire.span("build_pages"): - for file in doc_files: - assert file.page - excl = file.inclusion.is_excluded() - with logfire.span(f"build_page {file.page.url}", page=file.page): - _build_page(file.page, config, doc_files, nav, env, dirty, excl) - log_level = config.validation.links.anchors - with logfire.span("validate_anchor_links"): - for file in doc_files: - assert file.page is not None - file.page.validate_anchor_links(files=files, log_level=log_level) - # Run `post_build` plugin events. - with logfire.span("plugins callback: on_post_build", config=config): - config.plugins.on_post_build(config=config) - - -def _populate_page( - page: Page, config: MkNodesConfig, files: Files, dirty: bool = False -) -> None: - """Read page content from docs_dir and render Markdown.""" - config._current_page = page - try: - # When --dirty is used, only read the page if the file has been modified since the - # previous build of the output. - if dirty and not page.file.is_modified(): - return - - with logfire.span("plugins callback: on_pre_page", page=page, config=config): - # Run the `pre_page` plugin event - page = config.plugins.on_pre_page(page, config=config, files=files) - - with logfire.span("read_source", page=page): - page.read_source(config) - assert page.markdown is not None - - # Run `page_markdown` plugin events. - with logfire.span("plugins callback: on_page_markdown", page=page, config=config): - page.markdown = config.plugins.on_page_markdown( - page.markdown, page=page, config=config, files=files - ) - with logfire.span("render", page=page, config=config): - page.render(config, files) - assert page.content is not None - - with logfire.span("plugins callback: on_page_content", page=page, config=config): - page.content = config.plugins.on_page_content( - page.content, page=page, config=config, files=files - ) - except Exception as e: - message = f"Error reading page '{page.file.src_uri}':" - # Prevent duplicated error msg because it will be printed immediately afterwards. - if not isinstance(e, exceptions.BuildError): - message += f" {e}" - logger.exception(message) - raise - finally: - config._current_page = None - - -def _build_page( - page: Page, - config: MkNodesConfig, - doc_files: Sequence[File], - nav: Navigation, - env: jinja2.Environment, - dirty: bool = False, - excluded: bool = False, -) -> None: - """Pass a Page to theme template and write output to site_dir.""" - config._current_page = page - try: - # only build pages if the file has been modified since the previous build - if dirty and not page.file.is_modified(): - return - logger.debug("Building page %s", page.file.src_uri) - # Activate page. Signals to theme that this is the current page. - page.active = True - ctx = templatecontext.get_context(nav, doc_files, config, page) - # Allow 'template:' override in md source files. - template = env.get_template(page.meta.get("template", "main.html")) - # Run `page_context` plugin events. - ctx = config.plugins.on_page_context(ctx, page=page, config=config, nav=nav) # type: ignore - - if excluded: - page.content = DRAFT_CONTENT + (page.content or "") - # Render the template. - output = template.render(ctx) - # Run `post_page` plugin events. - output = config.plugins.on_post_page(output, page=page, config=config) - - # Write the output file. - if output.strip(): - text = output.encode("utf-8", errors="xmlcharrefreplace") - pathhelpers.write_file(text, page.file.abs_dest_path) - else: - logger.info("Page skipped: '%s'. Generated empty output.", page.file.src_uri) - - except Exception as e: - message = f"Error building page '{page.file.src_uri}':" - # Prevent duplicated the error message because - # it will be printed immediately afterwards. - if not isinstance(e, exceptions.BuildError): - message += f" {e}" - logger.error(message) # noqa: TRY400 - raise - finally: - # Deactivate page - page.active = False - config._current_page = None - - -def _build_template( - name: str, - template: jinja2.Template, - files: Files, - config: MkNodesConfig, - nav: Navigation, -) -> str: - """Return rendered output for given template as a string.""" - template = config.plugins.on_pre_template(template, template_name=name, config=config) - - if utils.is_error_template(name): - # Force absolute URLs for error pages. Docs root & server root might differ. - # See https://github.com/mkdocs/mkdocs/issues/77. - # However, if site_url is not set, assume the docs root and server root - # are the same. See https://github.com/mkdocs/mkdocs/issues/1598. - base_url = urlsplit(config.site_url or "/").path - else: - base_url = htmlfilters.relative_url_mkdocs(".", name) - context = templatecontext.get_context(nav, files, config, base_url=base_url) - ctx = config.plugins.on_template_context(context, template_name=name, config=config) # type: ignore - output = template.render(ctx) - return config.plugins.on_post_template(output, template_name=name, config=config) - - -def _build_theme_template( - template_name: str, - env: jinja2.Environment, - files: Files, - config: MkNodesConfig, - nav: Navigation, -) -> None: - """Build a template using the theme environment.""" - logger.debug("Building theme template: %s", template_name) - - try: - template = env.get_template(template_name) - except TemplateNotFound: - logger.warning("Template skipped: %r not found in theme dirs.", template_name) - return - - output = _build_template(template_name, template, files, config, nav) - - if output.strip(): - output_path = upath.UPath(config.site_dir) / template_name - pathhelpers.write_file(output.encode(), output_path) - if template_name == "sitemap.xml": - docs = files.documentation_pages() - pages = [f.page for f in docs if f.page is not None] - ts = utils.get_build_timestamp(pages=pages) - utils.write_gzip(f"{output_path}.gz", output, timestamp=ts) - else: - logger.info("Template skipped: %r generated empty output.", template_name) - - -def _build_extra_template( - template_name: str, files: Files, config: MkNodesConfig, nav: Navigation -): - """Build user templates which are not part of the theme.""" - logger.debug("Building extra template: %s", template_name) - - file = files.get_file_from_path(template_name) - if file is None: - logger.warning("Template skipped: %r not found in docs_dir.", template_name) - return - try: - template = jinja2.Template(file.content_string) - except Exception as e: # noqa: BLE001 - logger.warning("Error reading template %r: %s", template_name, e) - return - output = _build_template(template_name, template, files, config, nav) - if output.strip(): - pathhelpers.write_file(output.encode(), file.abs_dest_path) - else: - logger.info("Template skipped: %r generated empty output.", template_name) + builder = MkDocsSiteBuilder(config) + builder.build_site(live_server_url=live_server_url, dirty=dirty) if __name__ == "__main__": from mkdocs_mknodes.appconfig import appconfig - config = appconfig.AppConfig.from_yaml_file("mkdocs.yml") - print(config.model_dump()) + app_cfg = appconfig.AppConfig.from_yaml_file("mkdocs.yml") + print(app_cfg.model_dump()) build("mkdocs.yml", ".", "mkdocs_mknodes.manual.root:Build.build") diff --git a/mkdocs_mknodes/commands/utils.py b/mkdocs_mknodes/commands/utils.py index cea66cc..9393769 100644 --- a/mkdocs_mknodes/commands/utils.py +++ b/mkdocs_mknodes/commands/utils.py @@ -59,13 +59,13 @@ def get_counts(self) -> list[tuple[str, int]]: def count_warnings(fn: Callable[..., T]) -> Callable[..., T]: @functools.wraps(fn) - def wrapped(config: MkNodesConfig, *args, **kwargs) -> T: + def wrapped(self, *args, **kwargs) -> T: start = time.monotonic() warning_counter = CountHandler() warning_counter.setLevel(logging.WARNING) - if config.strict: + if self.config.strict: # Access config through self logging.getLogger("mkdocs").addHandler(warning_counter) - result = fn(config, *args, **kwargs) + result = fn(self, *args, **kwargs) counts = warning_counter.get_counts() if counts: msg = ", ".join(f"{v} {k.lower()}s" for k, v in counts) @@ -82,19 +82,17 @@ def wrapped(config: MkNodesConfig, *args, **kwargs) -> T: def handle_exceptions(fn: Callable[..., T]) -> Callable[..., T]: @functools.wraps(fn) - def wrapped(config: MkNodesConfig, *args, **kwargs) -> T: + def wrapped(self, *args, **kwargs) -> T: try: - return fn(config, *args, **kwargs) + return fn(self, *args, **kwargs) except Exception as e: # Run `build_error` plugin events. - config.plugins.on_build_error(error=e) + self.config.plugins.on_build_error(error=e) # Access config through self if isinstance(e, exceptions.BuildError): msg = "Aborted with a BuildError!" logger.exception(msg) raise exceptions.Abort(msg) from e raise - # finally: - # logging.getLogger("mkdocs").removeHandler(warning_counter) return wrapped From 6e9ff655e5719fa288d6a267e513ef40f4f07671 Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Mon, 11 Nov 2024 13:54:10 +0100 Subject: [PATCH 13/28] chore: split SiteBuilder into MarkdownBuilder and HTMLBuilder --- mkdocs_mknodes/commands/build_page.py | 163 ++++++++++++++------------ 1 file changed, 88 insertions(+), 75 deletions(-) diff --git a/mkdocs_mknodes/commands/build_page.py b/mkdocs_mknodes/commands/build_page.py index 09c4359..89c4dec 100644 --- a/mkdocs_mknodes/commands/build_page.py +++ b/mkdocs_mknodes/commands/build_page.py @@ -5,11 +5,11 @@ from collections.abc import Callable, Sequence import os from typing import TYPE_CHECKING, Any -from urllib.parse import urljoin, urlsplit +from urllib.parse import urlsplit import jinja2 from jinja2.exceptions import TemplateNotFound -from jinjarope import envtests, htmlfilters +from jinjarope import htmlfilters import logfire from mkdocs import exceptions from mkdocs.structure.files import Files, InclusionLevel @@ -39,11 +39,14 @@ ) -class MkDocsSiteBuilder: - """Main class for building MkDocs sites.""" +class MarkdownBuilder: + """Handles the initial phase of building Websites. + + File collection and markdown processing. + """ def __init__(self, config: MkNodesConfig | None = None): - """Initialize the site builder. + """Initialize the markdown builder. Args: config: Optional MkDocs configuration @@ -59,8 +62,8 @@ def build_from_config( site_dir: str | None = None, clone_depth: int = 100, **kwargs: Any, - ) -> None: - """Build a MkNodes-based website from config file. + ) -> tuple[Navigation, Files]: + """Build markdown content from config file. Args: config_path: Path to the MkDocs config file @@ -69,6 +72,9 @@ def build_from_config( site_dir: Output directory for built site clone_depth: Number of commits to fetch for Git repos kwargs: Additional config overrides passed to MkDocs + + Returns: + Navigation structure """ cfg_builder = configbuilder.ConfigBuilder( repo_path=repo_path, build_fn=build_fn, clone_depth=clone_depth @@ -78,21 +84,23 @@ def build_from_config( with logfire.span("plugins callback: on_startup", config=self.config): self.config.plugins.on_startup(command="build", dirty=False) - self.build_site() - with logfire.span("plugins callback: shutdown", config=self.config): - self.config.plugins.on_shutdown() + + nav, files = self.process_markdown() + return nav, files @utils.handle_exceptions @utils.count_warnings - def build_site(self, live_server_url: str | None = None, dirty: bool = False) -> None: - """Build a MkNodes-based website. + def process_markdown(self, dirty: bool = False) -> tuple[Navigation, Files]: + """Process markdown files and build navigation structure. Args: - live_server_url: An optional URL of the live server to use dirty: Do a dirty build + + Returns: + Navigation structure """ if self.config is None: - msg = "Configuration must be set before building site" + msg = "Configuration must be set before processing markdown" raise ValueError(msg) with logfire.span("plugins callback: on_config", config=self.config): @@ -103,17 +111,6 @@ def build_site(self, live_server_url: str | None = None, dirty: bool = False) -> if not dirty: logger.info("Cleaning site directory") pathhelpers.clean_directory(self.config.site_dir) - else: # pragma: no cover - logger.warning( - "A 'dirty' build is being performed (for site dev purposes only)" - ) - - if not live_server_url: # pragma: no cover - logger.info("Building documentation to directory: %s", self.config.site_dir) - if dirty and envtests.contains_files(self.config.site_dir): - logger.info( - "The directory contains stale files. Use --clean to remove them." - ) files = utils.get_files(self.config) env = self.config.theme.get_env() @@ -128,73 +125,33 @@ def build_site(self, live_server_url: str | None = None, dirty: bool = False) -> with logfire.span("plugins callback: on_nav", config=self.config, nav=nav): nav = self.config.plugins.on_nav(nav, config=self.config, files=files) - self._process_pages(files, live_server_url, dirty) - - with logfire.span("plugins callback: on_env", env=env, config=self.config): - env = self.config.plugins.on_env(env, config=self.config, files=files) + self._process_pages(files) + return nav, files - with logfire.span("copy_static_files"): - inclusion = ( - InclusionLevel.is_in_serve - if live_server_url - else InclusionLevel.is_included - ) - files.copy_static_files(dirty=dirty, inclusion=inclusion) - - self._build_templates(env, files, nav) - self._build_pages(files, nav, env, dirty, inclusion) - - with logfire.span("plugins callback: on_post_build", config=self.config): - self.config.plugins.on_post_build(config=self.config) - - def _process_pages( - self, files: Files, live_server_url: str | None, dirty: bool - ) -> None: + def _process_pages(self, files: Files) -> None: """Process all pages, reading their content and applying plugins. Args: files: Collection of files to process - live_server_url: Optional live server URL - dirty: Whether this is a dirty build """ - excluded: list[str] = [] - inclusion = ( - InclusionLevel.is_in_serve if live_server_url else InclusionLevel.is_included - ) - with logfire.span("populate pages"): - for file in files.documentation_pages(inclusion=inclusion): + for file in files.documentation_pages(): with logfire.span(f"populate page for {file.src_uri}", file=file): logger.debug("Reading: %s", file.src_uri) if file.page is None and file.inclusion.is_not_in_nav(): Page(None, file, self.config) - if live_server_url and file.inclusion.is_excluded(): - excluded.append(urljoin(live_server_url, file.url)) assert file.page is not None - self._populate_page(file.page, files, dirty) - - if excluded: - excluded_str = "\n - ".join(excluded) - logger.info( - "The following pages are being built only for the preview " - "but will be excluded from `mkdocs build` per `draft_docs` config:" - "\n - %s", - excluded_str, - ) + self._populate_page(file.page, files) - def _populate_page(self, page: Page, files: Files, dirty: bool = False) -> None: + def _populate_page(self, page: Page, files: Files) -> None: """Read page content from docs_dir and render Markdown. Args: page: Page to populate files: Collection of files - dirty: Whether this is a dirty build """ self.config._current_page = page try: - if dirty and not page.file.is_modified(): - return - with logfire.span( "plugins callback: on_pre_page", page=page, config=self.config ): @@ -232,6 +189,51 @@ def _populate_page(self, page: Page, files: Files, dirty: bool = False) -> None: finally: self.config._current_page = None + +class HTMLBuilder: + """Handles the HTML generation phase of building MkDocs sites.""" + + def __init__(self, config: MkNodesConfig): + """Initialize the HTML builder. + + Args: + config: MkDocs configuration + """ + self.config = config + + def build_html( + self, + nav: Navigation, + files: Files, + live_server_url: str | None = None, + dirty: bool = False, + ) -> None: + """Build HTML files from processed markdown. + + Args: + nav: Navigation structure + files: Collection of files + live_server_url: An optional URL of the live server to use + dirty: Whether this is a dirty build + """ + env = self.config.theme.get_env() + with logfire.span("plugins callback: on_env", env=env, config=self.config): + env = self.config.plugins.on_env(env, config=self.config, files=files) + + with logfire.span("copy_static_files"): + inclusion = ( + InclusionLevel.is_in_serve + if live_server_url + else InclusionLevel.is_included + ) + files.copy_static_files(dirty=dirty, inclusion=inclusion) + + self._build_templates(env, files, nav) + self._build_pages(files, nav, env, dirty, inclusion) + + with logfire.span("plugins callback: on_post_build", config=self.config): + self.config.plugins.on_post_build(config=self.config) + def _build_templates( self, env: jinja2.Environment, files: Files, nav: Navigation ) -> None: @@ -470,8 +472,8 @@ def build( clone_depth: Number of commits to fetch for Git repos kwargs: Additional config overrides passed to MkDocs """ - builder = MkDocsSiteBuilder() - builder.build_from_config( + md_builder = MarkdownBuilder() + nav, files = md_builder.build_from_config( config_path=config_path, repo_path=repo_path, build_fn=build_fn, @@ -480,6 +482,9 @@ def build( **kwargs, ) + html_builder = HTMLBuilder(md_builder.config) + html_builder.build_html(nav, files) + def _build( config: MkNodesConfig, @@ -493,8 +498,16 @@ def _build( live_server_url: An optional URL of the live server to use dirty: Do a dirty build """ - builder = MkDocsSiteBuilder(config) - builder.build_site(live_server_url=live_server_url, dirty=dirty) + md_builder = MarkdownBuilder(config) + nav, files = md_builder.process_markdown(dirty=dirty) + + html_builder = HTMLBuilder(config) + html_builder.build_html( + nav=nav, + files=files, + live_server_url=live_server_url, + dirty=dirty, + ) if __name__ == "__main__": From 88856b4b7075c9ca9c2170f968652a073cbaa2df Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Mon, 11 Nov 2024 14:12:43 +0100 Subject: [PATCH 14/28] chore: import fix --- mkdocs_mknodes/commands/build_page.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mkdocs_mknodes/commands/build_page.py b/mkdocs_mknodes/commands/build_page.py index 89c4dec..6fb8165 100644 --- a/mkdocs_mknodes/commands/build_page.py +++ b/mkdocs_mknodes/commands/build_page.py @@ -21,13 +21,12 @@ from mkdocs_mknodes import telemetry from mkdocs_mknodes.builders import configbuilder from mkdocs_mknodes.commands import templatecontext, utils +from mkdocs_mknodes.plugin.mknodesconfig import MkNodesConfig if TYPE_CHECKING: from mkdocs.structure.files import File - from mkdocs_mknodes.plugin.mknodesconfig import MkNodesConfig - logger = telemetry.get_plugin_logger(__name__) From 9b8554fb7cc2d5f27bc539e29274e0ca3dfccd5a Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Mon, 11 Nov 2024 15:49:00 +0100 Subject: [PATCH 15/28] chore: config work --- mkdocs_mknodes/appconfig/appconfig.py | 15 +++++++++++++-- mkdocs_mknodes/builders/configbuilder.py | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/mkdocs_mknodes/appconfig/appconfig.py b/mkdocs_mknodes/appconfig/appconfig.py index 8c7950b..36866d8 100644 --- a/mkdocs_mknodes/appconfig/appconfig.py +++ b/mkdocs_mknodes/appconfig/appconfig.py @@ -16,6 +16,7 @@ ) from pydantic.functional_validators import BeforeValidator from pydantic_core import PydanticCustomError +import upath from mkdocs_mknodes.appconfig import jinjaconfig, themeconfig, validationconfig from mkdocs_mknodes.appconfig.base import ConfigFile @@ -261,7 +262,7 @@ class AppConfig(ConfigFile): """ repo_path: str = "." """Path to the repository to create a website for. (`http://....my_project.git`)""" - clone_depth: int = 100 + clone_depth: int | None = 100 """Clone depth in case the repository is remote. (Required for `git-changelog`).""" build_folder: str | None = None """Folder to create the Markdown files in. @@ -318,7 +319,7 @@ class AppConfig(ConfigFile): Allows setting up loaders, extensions and the render behavior. """ - docs_dir: DirectoryPath = Field("docs") + docs_dir: str = Field("docs/") """Directory containing documentation markdown source files. !!! info "Path Resolution" @@ -772,6 +773,16 @@ def validate_nav( return [] return values + @field_validator("docs_dir", mode="before") + @classmethod + def validate_docs_dir(cls, value: str, info: ValidationInfo) -> str: + config_file_path = info.data["config_file_path"] + config_dir = upath.UPath(config_file_path).parent if config_file_path else None + path = upath.UPath(value) + if config_dir and not path.is_absolute(): + path = config_dir / path + return str(path.resolve()) + @field_validator("dev_addr", mode="before") @classmethod def validate_ip_port(cls, v: str) -> str: diff --git a/mkdocs_mknodes/builders/configbuilder.py b/mkdocs_mknodes/builders/configbuilder.py index 96e6a4a..7c390a7 100644 --- a/mkdocs_mknodes/builders/configbuilder.py +++ b/mkdocs_mknodes/builders/configbuilder.py @@ -44,7 +44,7 @@ def build_mkdocs_config( if self.clone_depth is not None: cfg.clone_depth = self.clone_depth # cfg = {**cfg, **kwargs} - text = yamling.dump_yaml(cfg.model_dump(mode="json")) + text = yamling.dump_yaml(cfg.model_dump(mode="json", exclude_none=True)) buffer = io.StringIO(text) buffer.name = cfg.config_file_path config = mknodesconfig.MkNodesConfig.from_yaml(buffer, **kwargs) From 2c6c39f66690d4d795eeb6653b13868b330215c8 Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Mon, 11 Nov 2024 18:19:58 +0100 Subject: [PATCH 16/28] chore: cleanup --- mkdocs_mknodes/commands/build_page.py | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/mkdocs_mknodes/commands/build_page.py b/mkdocs_mknodes/commands/build_page.py index 6fb8165..751b0f4 100644 --- a/mkdocs_mknodes/commands/build_page.py +++ b/mkdocs_mknodes/commands/build_page.py @@ -127,21 +127,21 @@ def process_markdown(self, dirty: bool = False) -> tuple[Navigation, Files]: self._process_pages(files) return nav, files + @logfire.instrument("Populate pages") def _process_pages(self, files: Files) -> None: """Process all pages, reading their content and applying plugins. Args: files: Collection of files to process """ - with logfire.span("populate pages"): - for file in files.documentation_pages(): - with logfire.span(f"populate page for {file.src_uri}", file=file): - logger.debug("Reading: %s", file.src_uri) - if file.page is None and file.inclusion.is_not_in_nav(): - Page(None, file, self.config) - assert file.page is not None - self._populate_page(file.page, files) - + for file in files.documentation_pages(): + logger.debug("Reading: %s", file.src_uri) + if file.page is None and file.inclusion.is_not_in_nav(): + Page(None, file, self.config) + assert file.page is not None + self._populate_page(file.page, files) + + @logfire.instrument("populate page for {file.src_uri}") def _populate_page(self, page: Page, files: Files) -> None: """Read page content from docs_dir and render Markdown. @@ -233,6 +233,7 @@ def build_html( with logfire.span("plugins callback: on_post_build", config=self.config): self.config.plugins.on_post_build(config=self.config) + @logfire.instrument("Build templates") def _build_templates( self, env: jinja2.Environment, files: Files, nav: Navigation ) -> None: @@ -243,11 +244,10 @@ def _build_templates( files: Collection of files nav: Navigation structure """ - with logfire.span("build_templates"): - for template in self.config.theme.static_templates: - self._build_theme_template(template, env, files, nav) - for template in self.config.extra_templates: - self._build_extra_template(template, files, nav) + for template in self.config.theme.static_templates: + self._build_theme_template(template, env, files, nav) + for template in self.config.extra_templates: + self._build_extra_template(template, files, nav) def _build_pages( self, From 2a4c689027bf058874c83ecde9ee6873c5df17c2 Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Mon, 11 Nov 2024 19:07:18 +0100 Subject: [PATCH 17/28] chore: logfire fix --- mkdocs_mknodes/commands/build_page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs_mknodes/commands/build_page.py b/mkdocs_mknodes/commands/build_page.py index 751b0f4..130f88e 100644 --- a/mkdocs_mknodes/commands/build_page.py +++ b/mkdocs_mknodes/commands/build_page.py @@ -141,7 +141,7 @@ def _process_pages(self, files: Files) -> None: assert file.page is not None self._populate_page(file.page, files) - @logfire.instrument("populate page for {file.src_uri}") + @logfire.instrument("populate page for {page.file.src_uri}") def _populate_page(self, page: Page, files: Files) -> None: """Read page content from docs_dir and render Markdown. From a1bc4215ad99cb04d7d936d713421261ea28e6cb Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Mon, 11 Nov 2024 19:14:05 +0100 Subject: [PATCH 18/28] chore: cleanup --- mkdocs_mknodes/builders/configbuilder.py | 3 +++ mkdocs_mknodes/commands/serve.py | 17 ----------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/mkdocs_mknodes/builders/configbuilder.py b/mkdocs_mknodes/builders/configbuilder.py index 7c390a7..3a4b42d 100644 --- a/mkdocs_mknodes/builders/configbuilder.py +++ b/mkdocs_mknodes/builders/configbuilder.py @@ -43,6 +43,9 @@ def build_mkdocs_config( cfg.build_fn = self.build_fn if self.clone_depth is not None: cfg.clone_depth = self.clone_depth + if cfg.theme.name != "material": + cfg.remove_plugin("social") + cfg.remove_plugin("tags") # cfg = {**cfg, **kwargs} text = yamling.dump_yaml(cfg.model_dump(mode="json", exclude_none=True)) buffer = io.StringIO(text) diff --git a/mkdocs_mknodes/commands/serve.py b/mkdocs_mknodes/commands/serve.py index 0b688fa..3a9b01c 100644 --- a/mkdocs_mknodes/commands/serve.py +++ b/mkdocs_mknodes/commands/serve.py @@ -50,8 +50,6 @@ def serve( if clone_depth is not None: config.clone_depth = clone_depth if theme and theme != "material": - # config.remove_plugin("social") - # config.remove_plugin("tags") kwargs["theme"] = theme text = yamling.dump_yaml(dict(config)) stream = io.StringIO(text) @@ -59,21 +57,6 @@ def serve( _serve(config_file=stream, livereload=False, **kwargs) # type: ignore[arg-type] -def serve_node(node, repo_path: str = "."): - text = f""" - import mknodes - - def build(project): - page = project.root.add_page(is_index=True, hide="toc") - page += '''{node!s}''' - - - """ - p = pathlib.Path("docs/test.py") - p.write_text(text) - serve(repo_url=repo_path, site_script=p) - - @contextlib.contextmanager def catch_exceptions(config: mknodesconfig.MkNodesConfig): """Context manager used to clean up in case of build error. From 3bade72170dd360889d122eb573994f8e2404eb0 Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Mon, 11 Nov 2024 20:00:15 +0100 Subject: [PATCH 19/28] chore: move error_handler fn to Server cls --- mkdocs_mknodes/commands/serve.py | 8 -------- mkdocs_mknodes/liveserver.py | 8 ++++++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/mkdocs_mknodes/commands/serve.py b/mkdocs_mknodes/commands/serve.py index 3a9b01c..0c94367 100644 --- a/mkdocs_mknodes/commands/serve.py +++ b/mkdocs_mknodes/commands/serve.py @@ -133,14 +133,6 @@ def builder(config: mknodesconfig.MkNodesConfig | None = None): mount_path=mount_path(config), ) - def error_handler(code: int) -> bytes | None: - if code not in (404, 500): - return None - error_page = site_dir / f"{code}.html" - return error_page.read_bytes() if error_page.is_file() else None - - server.error_handler = error_handler - with catch_exceptions(config): # Perform the initial build builder(config) diff --git a/mkdocs_mknodes/liveserver.py b/mkdocs_mknodes/liveserver.py index b37c4aa..8379d28 100644 --- a/mkdocs_mknodes/liveserver.py +++ b/mkdocs_mknodes/liveserver.py @@ -107,8 +107,6 @@ def __init__( self.url = _serve_url(host, port, mount_path) self.build_delay = 0.1 self.shutdown_delay = shutdown_delay - # To allow custom error pages. - self.error_handler: Callable[[int], bytes | None] = lambda code: None super().__init__((host, port), _Handler, bind_and_activate=False) self.set_app(self.serve_request) @@ -133,6 +131,12 @@ def __init__( self._watched_paths: dict[str, int] = {} self._watch_refs: dict[str, Any] = {} + def error_handler(self, code: int) -> bytes | None: + if code not in (404, 500): + return None + error_page = self.root / f"{code}.html" + return error_page.read_bytes() if error_page.is_file() else None + def watch(self, path: str | os.PathLike[str], *, recursive: bool = True) -> None: """Add the 'path' to watched paths. From 7981a73194af9a8ee1ba801c71e1e03a03f29a3e Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Mon, 11 Nov 2024 20:54:46 +0100 Subject: [PATCH 20/28] chore: cleanup --- mkdocs_mknodes/builders/configbuilder.py | 4 ++-- mkdocs_mknodes/commands/build_page.py | 27 ++++-------------------- mkdocs_mknodes/commands/serve.py | 13 ++---------- 3 files changed, 8 insertions(+), 36 deletions(-) diff --git a/mkdocs_mknodes/builders/configbuilder.py b/mkdocs_mknodes/builders/configbuilder.py index 3a4b42d..aead0a1 100644 --- a/mkdocs_mknodes/builders/configbuilder.py +++ b/mkdocs_mknodes/builders/configbuilder.py @@ -27,8 +27,8 @@ def __init__( self.build_fn = build_fn self.clone_depth = clone_depth - def add_config_file(self, path: str | os.PathLike[str]): - cfg = appconfig.AppConfig.from_yaml_file(path) + def add_config_file(self, path: str | os.PathLike[str], **overrides: Any): + cfg = appconfig.AppConfig.from_yaml_file(path, **overrides) self.configs.append(cfg) def build_mkdocs_config( diff --git a/mkdocs_mknodes/commands/build_page.py b/mkdocs_mknodes/commands/build_page.py index 130f88e..fedd6d0 100644 --- a/mkdocs_mknodes/commands/build_page.py +++ b/mkdocs_mknodes/commands/build_page.py @@ -55,31 +55,20 @@ def __init__(self, config: MkNodesConfig | None = None): def build_from_config( self, config_path: str | os.PathLike[str], - repo_path: str, - build_fn: str | None, - *, - site_dir: str | None = None, - clone_depth: int = 100, **kwargs: Any, ) -> tuple[Navigation, Files]: """Build markdown content from config file. Args: config_path: Path to the MkDocs config file - repo_path: Repository path/URL to build docs for - build_fn: Fully qualified name of build function to use - site_dir: Output directory for built site - clone_depth: Number of commits to fetch for Git repos - kwargs: Additional config overrides passed to MkDocs + kwargs: Additional config overrides Returns: Navigation structure """ - cfg_builder = configbuilder.ConfigBuilder( - repo_path=repo_path, build_fn=build_fn, clone_depth=clone_depth - ) + cfg_builder = configbuilder.ConfigBuilder() cfg_builder.add_config_file(config_path) - self.config = cfg_builder.build_mkdocs_config(site_dir=site_dir, **kwargs) + self.config = cfg_builder.build_mkdocs_config(**kwargs) with logfire.span("plugins callback: on_startup", config=self.config): self.config.plugins.on_startup(command="build", dirty=False) @@ -472,15 +461,7 @@ def build( kwargs: Additional config overrides passed to MkDocs """ md_builder = MarkdownBuilder() - nav, files = md_builder.build_from_config( - config_path=config_path, - repo_path=repo_path, - build_fn=build_fn, - site_dir=site_dir, - clone_depth=clone_depth, - **kwargs, - ) - + nav, files = md_builder.build_from_config(config_path, site_dir=site_dir, **kwargs) html_builder = HTMLBuilder(md_builder.config) html_builder.build_html(nav, files) diff --git a/mkdocs_mknodes/commands/serve.py b/mkdocs_mknodes/commands/serve.py index 0c94367..c0e06be 100644 --- a/mkdocs_mknodes/commands/serve.py +++ b/mkdocs_mknodes/commands/serve.py @@ -11,6 +11,7 @@ # from mkdocs.commands import serve as serve_ from mknodes.utils import log +import upath import yamling from mkdocs_mknodes import liveserver, paths @@ -26,9 +27,6 @@ def serve( config_path: str | os.PathLike[str] = paths.CFG_DEFAULT, - repo_path: str = ".", - build_fn: str = paths.DEFAULT_BUILD_FN, - clone_depth: int = 100, theme: str | None = None, **kwargs: Any, ): @@ -42,16 +40,9 @@ def serve( theme: Theme to use kwargs: Optional config values (overrides value from config) """ - config = mknodesconfig.MkNodesConfig.from_yaml_file(config_path, validate=False) - if repo_path is not None: - config.repo_path = repo_path - if build_fn is not None: - config.build_fn = build_fn - if clone_depth is not None: - config.clone_depth = clone_depth if theme and theme != "material": kwargs["theme"] = theme - text = yamling.dump_yaml(dict(config)) + text = upath.UPath(config_path).read_text() stream = io.StringIO(text) stream.name = str(config_path) _serve(config_file=stream, livereload=False, **kwargs) # type: ignore[arg-type] From fa6322af1ca00451c5e84153f027bc1320eef94d Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Mon, 11 Nov 2024 21:03:53 +0100 Subject: [PATCH 21/28] chore: prep --- mkdocs_mknodes/builders/configbuilder.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/mkdocs_mknodes/builders/configbuilder.py b/mkdocs_mknodes/builders/configbuilder.py index aead0a1..a08a85f 100644 --- a/mkdocs_mknodes/builders/configbuilder.py +++ b/mkdocs_mknodes/builders/configbuilder.py @@ -32,7 +32,10 @@ def add_config_file(self, path: str | os.PathLike[str], **overrides: Any): self.configs.append(cfg) def build_mkdocs_config( - self, site_dir: str | os.PathLike[str] | None = None, **kwargs: Any + self, + site_dir: str | os.PathLike[str] | None = None, + infer_watch_paths: bool = False, + **kwargs: Any, ) -> mknodesconfig.MkNodesConfig: cfg = self.configs[0] if site_dir: @@ -51,7 +54,18 @@ def build_mkdocs_config( buffer = io.StringIO(text) buffer.name = cfg.config_file_path config = mknodesconfig.MkNodesConfig.from_yaml(buffer, **kwargs) - + if infer_watch_paths: + watch_paths = [*config.watch, *_infer_watch_paths(config)] + config.watch = list(set(watch_paths)) for k, v in config.items(): logger.debug("%s: %s", k, v) return config + + +def _infer_watch_paths(config: mknodesconfig.MkNodesConfig) -> list[str]: + paths_to_watch: list[str] = [] + paths_to_watch.append(config.docs_dir) + if config.config_file_path: + paths_to_watch.append(config.config_file_path) + paths_to_watch.extend(config.theme.dirs) + return paths_to_watch From be42e50d2ee20a5302800d5f86840b0f33418d95 Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Mon, 11 Nov 2024 21:13:34 +0100 Subject: [PATCH 22/28] chore: cleanup --- mkdocs_mknodes/backends/mkdocsbackend.py | 12 ++++++++++-- mkdocs_mknodes/plugin/mkdocshelpers.py | 25 ------------------------ 2 files changed, 10 insertions(+), 27 deletions(-) delete mode 100644 mkdocs_mknodes/plugin/mkdocshelpers.py diff --git a/mkdocs_mknodes/backends/mkdocsbackend.py b/mkdocs_mknodes/backends/mkdocsbackend.py index a6135e5..46f1dde 100644 --- a/mkdocs_mknodes/backends/mkdocsbackend.py +++ b/mkdocs_mknodes/backends/mkdocsbackend.py @@ -14,7 +14,7 @@ from mkdocs_mknodes import mkdocsconfig, telemetry from mkdocs_mknodes.backends import buildbackend -from mkdocs_mknodes.plugin import mkdocsbuilder, mkdocshelpers +from mkdocs_mknodes.plugin import mkdocsbuilder logger = telemetry.get_plugin_logger(__name__) @@ -62,7 +62,15 @@ def files(self) -> files_.Files: [Files]: https://github.com/mkdocs/mkdocs/blob/master/mkdocs/structure/files.py """ - files = sorted(self._mk_files.values(), key=mkdocshelpers.file_sorter) + + def file_sorter(f: files_.File) -> tuple[str, ...]: + parts = pathlib.PurePath(f.src_path).parts + return tuple( + chr(f.name != "index" if i == len(parts) - 1 else 2) + p + for i, p in enumerate(parts) + ) + + files = sorted(self._mk_files.values(), key=file_sorter) return files_.Files(files) def write_files(self, files): diff --git a/mkdocs_mknodes/plugin/mkdocshelpers.py b/mkdocs_mknodes/plugin/mkdocshelpers.py deleted file mode 100644 index a839488..0000000 --- a/mkdocs_mknodes/plugin/mkdocshelpers.py +++ /dev/null @@ -1,25 +0,0 @@ -"""The Mkdocs Plugin.""" - -from __future__ import annotations - -import pathlib - -from mkdocs.structure.files import File, Files - -from mkdocs_mknodes import telemetry - - -logger = telemetry.get_plugin_logger(__name__) - - -def file_sorter(f: File): - parts = pathlib.PurePath(f.src_path).parts - return tuple( - chr(f.name != "index" if i == len(parts) - 1 else 2) + p - for i, p in enumerate(parts) - ) - - -def merge_files(*files: Files) -> Files: - file_list = [i for j in files for i in j] - return Files(sorted(file_list, key=file_sorter)) From 3b0f9d60b083c481e771582e4d0ef1c35332d77b Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Mon, 11 Nov 2024 21:18:17 +0100 Subject: [PATCH 23/28] chore: examples fix --- configs/mkdocs_mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/configs/mkdocs_mkdocs.yml b/configs/mkdocs_mkdocs.yml index ad69d85..4af6a5c 100644 --- a/configs/mkdocs_mkdocs.yml +++ b/configs/mkdocs_mkdocs.yml @@ -7,6 +7,7 @@ site_url: https://phil65.github.io/mknodes/mkdocs/ repo_path: https://github.com/mkdocs/mkdocs.git clone_depth: 100 build_fn: mkdocs_mknodes:parse +docs_dir: ../docs build_kwargs: pages: - title: Home From ed7cd20ddd398617e91b66f1a8c73a39ca67451e Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Mon, 11 Nov 2024 22:01:03 +0100 Subject: [PATCH 24/28] chore: cleanup --- mkdocs_mknodes/commands/build_page.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/mkdocs_mknodes/commands/build_page.py b/mkdocs_mknodes/commands/build_page.py index fedd6d0..c6ffe06 100644 --- a/mkdocs_mknodes/commands/build_page.py +++ b/mkdocs_mknodes/commands/build_page.py @@ -238,6 +238,7 @@ def _build_templates( for template in self.config.extra_templates: self._build_extra_template(template, files, nav) + @logfire.instrument("Build pages") def _build_pages( self, files: Files, @@ -257,20 +258,15 @@ def _build_pages( """ logger.debug("Building markdown pages.") doc_files = files.documentation_pages(inclusion=inclusion) - - with logfire.span("build_pages"): - for file in doc_files: - assert file.page - excl = file.inclusion.is_excluded() - with logfire.span(f"build_page {file.page.url}", page=file.page): - self._build_page(file.page, doc_files, nav, env, dirty, excl) - log_level = self.config.validation.links.anchors - with logfire.span("validate_anchor_links"): - for file in doc_files: - assert file.page is not None + for file in doc_files: + assert file.page + excl = file.inclusion.is_excluded() + self._build_page(file.page, doc_files, nav, env, dirty, excl) + with logfire.span("validate_anchor_links"): file.page.validate_anchor_links(files=files, log_level=log_level) + @logfire.instrument("Build page {page.file.url}") def _build_page( self, page: Page, From cee6e11ac101cee2ea980fb53fb0c81600fb4c1e Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Sat, 16 Nov 2024 03:45:00 +0100 Subject: [PATCH 25/28] build: bump pre-commit-config --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2582659..1dd1ab2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,13 +36,13 @@ repos: additional_dependencies: [mkdocs, orjson, pydantic] - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.7.3 + rev: v0.7.4 hooks: - id: ruff - id: ruff-format - repo: https://github.com/commitizen-tools/commitizen - rev: v3.30.0 + rev: v3.30.1 hooks: - id: commitizen stages: [commit-msg] From 99a681c0174d86647bbc3c8cd230eabed5e945e3 Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Sat, 16 Nov 2024 16:15:53 +0100 Subject: [PATCH 26/28] chore: mknodes adjustment --- mkdocs_mknodes/manual/get_started_section.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs_mknodes/manual/get_started_section.py b/mkdocs_mknodes/manual/get_started_section.py index 9052fd6..b9a2f61 100644 --- a/mkdocs_mknodes/manual/get_started_section.py +++ b/mkdocs_mknodes/manual/get_started_section.py @@ -27,7 +27,7 @@ def _(page: mk.MkPage): @router.route_page("Plugin configuration", hide="toc") def _(page: mk.MkPage): page += mk.MkTemplate("plugin_configuration.jinja") - eps = page.ctx.metadata.entry_points.get("mkdocs.plugins", []) + eps = page.ctx.metadata.entry_points.get_group("mkdocs.plugins") page += mk.MkDocStrings( eps[0].load().config_class, show_root_toc_entry=False, From ffe121aaf391b4eec44b81c42951bd4090eb75d8 Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Sat, 16 Nov 2024 16:16:46 +0100 Subject: [PATCH 27/28] chore: cleanup --- mkdocs_mknodes/commands/build_page.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/mkdocs_mknodes/commands/build_page.py b/mkdocs_mknodes/commands/build_page.py index c6ffe06..90836fb 100644 --- a/mkdocs_mknodes/commands/build_page.py +++ b/mkdocs_mknodes/commands/build_page.py @@ -207,15 +207,11 @@ def build_html( env = self.config.theme.get_env() with logfire.span("plugins callback: on_env", env=env, config=self.config): env = self.config.plugins.on_env(env, config=self.config, files=files) - + inclusion = ( + InclusionLevel.is_in_serve if live_server_url else InclusionLevel.is_included + ) with logfire.span("copy_static_files"): - inclusion = ( - InclusionLevel.is_in_serve - if live_server_url - else InclusionLevel.is_included - ) files.copy_static_files(dirty=dirty, inclusion=inclusion) - self._build_templates(env, files, nav) self._build_pages(files, nav, env, dirty, inclusion) From f99d7cca81b639eae6d0896c058b250de1ae7ce4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:14:07 +0000 Subject: [PATCH 28/28] build(deps): bump codecov/codecov-action from 4 to 5 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 422f8b5..e21b814 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ jobs: run: | uv run pytest --cov-report=xml - - uses: codecov/codecov-action@v4 + - uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: false