Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/_unreleased.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ type = "feature"
description = "Add a pre-commit hook for pydoc-markdown."
author = "@RomainTT"
pr = "https://github.com/NiklasRosenstein/pydoc-markdown/pull/298"

4 changes: 3 additions & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ If you want to talk about a potential contribution before investing any time, pl

## Changelog entries

Pydoc-Markdown uses [Slam][] to manage changelogs. You should use the Slam CLI to add a new changelog entry, otherwise
Pydoc-Markdown uses [Slap][] to manage changelogs. You should use the Slap CLI to add a new changelog entry, otherwise
you need to manually generate a UUID-4.

$ slap changelog add -t <type> -d <changelog message> [--issue <issue_url>]

After you create the pull request, GitHub Actions will take care of injecting the PR URL into the changelog entry.

For a full list of accepted changelog types see [here](https://niklasrosenstein.github.io/slap/commands/changelog/)
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ keywords = ["documentation", "docs", "generator", "markdown", "pydoc"]
Homepage = "https://github.com/NiklasRosenstein/pydoc-markdown"

[tool.poetry.dependencies]
python = "^3.7"
python = "^3.8"
click = ">=7.1,<9.0"
"databind.core" = "^4.4.0"
"databind.json" = "^4.4.0"
Expand Down Expand Up @@ -83,6 +83,7 @@ hugo = "pydoc_markdown.contrib.renderers.hugo:HugoRenderer"
markdown = "pydoc_markdown.contrib.renderers.markdown:MarkdownRenderer"
mkdocs = "pydoc_markdown.contrib.renderers.mkdocs:MkdocsRenderer"
docusaurus = "pydoc_markdown.contrib.renderers.docusaurus:DocusaurusRenderer"
nextra = "pydoc_markdown.contrib.renderers.nextra:NextraRenderer"
jinja2 = "pydoc_markdown.contrib.renderers.jinja2:Jinja2Renderer"

[tool.poetry.plugins."pydoc_markdown.interfaces.SourceLinker"]
Expand Down
1 change: 0 additions & 1 deletion src/pydoc_markdown/contrib/renderers/docusaurus.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ def _render_side_bar_config(self, module_tree: t.Dict[t.Text, t.Any]) -> None:
"label": self.sidebar_top_level_label,
}
self._build_sidebar_tree(sidebar, module_tree)

if sidebar.get("items"):
if self.sidebar_top_level_module_label:
sidebar["items"][0]["label"] = self.sidebar_top_level_module_label
Expand Down
109 changes: 109 additions & 0 deletions src/pydoc_markdown/contrib/renderers/nextra.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# -*- coding: utf8 -*-

import dataclasses
import json
import logging
import os
import typing as t
from pathlib import Path

import docspec
import typing_extensions as te
from databind.core import DeserializeAs

from pydoc_markdown.contrib.renderers.markdown import MarkdownRenderer
from pydoc_markdown.interfaces import Context, Renderer

logger = logging.getLogger(__name__)


@dataclasses.dataclass
class CustomizedMarkdownRenderer(MarkdownRenderer):
"""We override some defaults in this subclass."""

#: Disabled because Docusaurus supports this automatically.
insert_header_anchors: bool = False

#: Escape html in docstring, otherwise it could lead to invalid html.
escape_html_in_docstring: bool = True

#: Conforms to Docusaurus header format.
render_module_header_template: str = (
"---\n" "sidebar_label: {relative_module_name}\n" "title: {module_name}\n" "---\n\n"
)

add_module_prefix: bool = False
use_fixed_header_levels: bool = False


@dataclasses.dataclass
class NextraRenderer(Renderer):
"""
Produces Markdown files for use in a Nextra docs website.
It creates files in a fixed layout that reflects the structure of the documented packages.
The files will be rendered into the directory specified with the #docs_base_path option.

### Options
"""

#: The #MarkdownRenderer configuration.
markdown: te.Annotated[MarkdownRenderer, DeserializeAs(CustomizedMarkdownRenderer)] = dataclasses.field(
default_factory=CustomizedMarkdownRenderer
)

#: The path where the Nextra docs content is. Defaults "docs" folder.
docs_base_path: str = "docs"

#: The output path inside the docs_base_path folder, used to output the
#: module reference.
relative_output_path: str = "pages"

#: The sidebar path inside the docs_base_path folder, used to output the
#: sidebar for the module reference.
relative_sidebar_path: str = "sidebar.json"

#: The top-level label in the sidebar. Default to 'Reference'. Can be set to null to
#: remove the sidebar top-level all together. This option assumes that there is only one top-level module.
sidebar_top_level_label: t.Optional[str] = "Reference"

#: The top-level module label in the sidebar. Default to null, meaning that the actual
#: module name will be used. This option assumes that there is only one top-level module.
sidebar_top_level_module_label: t.Optional[str] = None

def init(self, context: Context) -> None:
self.markdown.init(context)

def render(self, modules: t.List[docspec.Module]) -> None:
module_tree: t.Dict[str, t.Any] = {"children": {}, "edges": []}
output_path = Path(self.docs_base_path) / self.relative_output_path
for module in modules:
filepath = output_path

module_parts = module.name.split(".")
if module.location.filename.endswith("__init__.py"):
module_parts.append("__init__")

relative_module_tree = module_tree
intermediary_module = []

for module_part in module_parts[:-1]:
# update the module tree
intermediary_module.append(module_part)
intermediary_module_name = ".".join(intermediary_module)
relative_module_tree["children"].setdefault(intermediary_module_name, {"children": {}, "edges": []})
relative_module_tree = relative_module_tree["children"][intermediary_module_name]

# descend to the file
filepath = filepath / module_part

# create intermediary missing directories and get the full path
filepath.mkdir(parents=True, exist_ok=True)
filepath = filepath / f"{module_parts[-1]}.md"

with filepath.open("w", encoding=self.markdown.encoding) as fp:
logger.info("Render file %s", filepath)
self.markdown.render_single_page(fp, [module])

# only update the relative module tree if the file is not empty
relative_module_tree["edges"].append(os.path.splitext(str(filepath.relative_to(self.docs_base_path)))[0])

1 change: 1 addition & 0 deletions src/pydoc_markdown/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ def cli(
"mkdocs": static.DEFAULT_MKDOCS_CONFIG,
"hugo": static.DEFAULT_HUGO_CONFIG,
"docusaurus": static.DEFAULT_DOCUSAURUS_CONFIG,
"nextra": static.DEFAULT_NEXTRA_CONFIG
}
with open(filename, "w") as fp:
fp.write(source[bootstrap])
Expand Down
16 changes: 16 additions & 0 deletions src/pydoc_markdown/static.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
contents: [ my_project, my_project.* ]
""".lstrip()

#: Default configuration for Docusaurus to use Pydoc-Markdown.
DEFAULT_DOCUSAURUS_CONFIG = """
loaders:
- type: python
Expand All @@ -100,6 +101,21 @@
sidebar_top_level_label: 'Reference'
""".lstrip()

#: Default configuration for Nextra to use Pydoc-Markdown.
DEFAULT_NEXTRA_CONFIG = """
loaders:
- type: python
processors:
- type: filter
skip_empty_modules: true
- type: smart
- type: crossref
renderer:
type: nextra
docs_base_path: docs
relative_output_path: pages
""".lstrip()


#: Default configuration for Read the Docs to use Pydoc-Markdown.
READTHEDOCS_FILES = {
Expand Down