Skip to content

Commit

Permalink
docs: setup mkdocs
Browse files Browse the repository at this point in the history
MkDocs is a popular tool in the Python ecosystem to generate
documentation. It has support for parsing Python docstrings in order to
generate API documentation quite easily.

Signed-off-by: JP-Ellis <josh@jpellis.me>
  • Loading branch information
JP-Ellis committed Oct 24, 2023
1 parent ab02630 commit d555b56
Show file tree
Hide file tree
Showing 9 changed files with 469 additions and 0 deletions.
55 changes: 55 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Generate and Publish Docs

on:
push:

env:
STABLE_PYTHON_VERSION: "3.11"
PYTEST_ADDOPTS: --color=yes

jobs:
build:
name: Build docs

runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.STABLE_PYTHON_VERSION }}

- name: Install Hatch
run: pip install --upgrade hatch

- name: Build docs
run: |
hatch run mkdocs build
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
with:
path: site

publish:
name: Publish docs

needs: build
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}

steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
4 changes: 4 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ repos:
- id: check-toml
- id: check-xml
- id: check-yaml
exclude: |
(?x)^(
mkdocs.yml
)$
- repo: https://gitlab.com/bmares/check-json5
rev: v1.0.0
Expand Down
5 changes: 5 additions & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- [Home](README.md)
- [Changelog](CHANGELOG.md)
- [Contributing](CONTRIBUTING.md)
- [Pact](pact/)
- [Examples](examples/)
62 changes: 62 additions & 0 deletions docs/scripts/markdown.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""
Script to merge Markdown documentation from the main codebase into the docs.
This script is run by mkdocs-gen-files when the documentation is built and
imports Markdown documentation from the main codebase so that it can be included
in the documentation site. For example, a Markdown file located at
`some/path/foo.md` will be treated as if it was located at
`docs/some/path/foo.md` without the need for symlinks or copying the file.
If the destination file already exists (either because it is a real file, or was
otherwise already generated), the script will raise a RuntimeError.
"""

import subprocess
import sys
from pathlib import Path

import mkdocs_gen_files
from mkdocs_gen_files.editor import FilesEditor

EDITOR = FilesEditor.current()

# These paths are relative to the project root, *not* the current file.
SRC_ROOT = "."
DOCS_DEST = "."

# List of all files version controlled files in the SRC_ROOT
ALL_FILES = sorted(
map(
Path,
subprocess.check_output(["git", "ls-files", SRC_ROOT]) # noqa: S603, S607
.decode("utf-8")
.splitlines(),
),
)


for source_path in filter(lambda p: p.suffix == ".md", ALL_FILES):
if source_path.parts[0] == "docs":
continue
dest_path = Path(DOCS_DEST, source_path)

if str(dest_path) in EDITOR.files:
print( # noqa: T201
f"Unable to copy {source_path} to {dest_path} because the file already"
" exists at the destination.",
file=sys.stderr,
)
msg = f"File {dest_path} already exists."
raise RuntimeError(msg)

with Path(source_path).open("r", encoding="utf-8") as fi, mkdocs_gen_files.open(
dest_path,
"w",
encoding="utf-8",
) as fd:
fd.write(fi.read())

mkdocs_gen_files.set_edit_path(
dest_path,
f"https://github.com/pact-foundation/pact-python/edit/develop/{source_path}",
)
117 changes: 117 additions & 0 deletions docs/scripts/other.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""
Create placeholder files for all other files in the codebase.
This script is run by mkdocs-gen-files when the documentation is built and
creates placeholder files for all other files in the codebase. This is done so
that the documentation site can link to all files in the codebase, even if they
aren't part of the documentation proper.
If the files are binary, they are copied as-is (e.g. for images), otherwise a
HTML redirect is created.
If the destination file already exists (either because it is a real file, or was
otherwise already generated), the script will ignore the current file and
continue silently.
"""

import subprocess
from pathlib import Path
from typing import TYPE_CHECKING

import mkdocs_gen_files
from mkdocs_gen_files.editor import FilesEditor

if TYPE_CHECKING:
import io

EDITOR = FilesEditor.current()

# These paths are relative to the project root, *not* the current file.
SRC_ROOT = "."
DOCS_DEST = "."

# List of all files version controlled files in the SRC_ROOT
ALL_FILES = sorted(
map(
Path,
subprocess.check_output(["git", "ls-files", SRC_ROOT]) # noqa: S603, S607
.decode("utf-8")
.splitlines(),
),
)


def is_binary(buffer: bytes) -> bool:
"""
Determine whether the given buffer is binary or not.
The check is done by attempting to decode the buffer as UTF-8. If this
succeeds, the buffer is not binary. If it fails, the buffer is binary.
The entire buffer will be checked, therefore if checking whether a file is
binary, only the start of the file should be passed.
Args:
buffer:
The buffer to check.
Returns:
True if the buffer is binary, False otherwise.
"""
try:
buffer.decode("utf-8")
except UnicodeDecodeError:
return True
else:
return False


for source_path in ALL_FILES:
if not source_path.is_file():
continue
if source_path.parts[0] in ["docs"]:
continue

dest_path = Path(DOCS_DEST, source_path)

if str(dest_path) in EDITOR.files:
continue

fi: "io.IOBase"
with Path(source_path).open("rb") as fi:
buf = fi.read(2048)

if is_binary(buf):
if source_path.stat().st_size < 16 * 2**20:
# Copy the file only if it's less than 16MB.
with Path(source_path).open("rb") as fi, mkdocs_gen_files.open(
dest_path,
"wb",
) as fd:
fd.write(fi.read())
else:
# File is too big, create a redirect.
url = (
"https://github.com"
"/pact-foundation/pact-python"
"/raw"
"/develop"
f"/{source_path}"
)
with mkdocs_gen_files.open(dest_path, "w", encoding="utf-8") as fd:
fd.write(f'<meta http-equiv="refresh" content="0; url={url}">')
fd.write(f"# Redirecting to {url}...")
fd.write(f"[Click here if you are not redirected]({url})")

mkdocs_gen_files.set_edit_path(
dest_path,
f"https://github.com/pact-foundation/pact-python/edit/develop/{source_path}",
)

else:
with Path(source_path).open("r", encoding="utf-8") as fi, mkdocs_gen_files.open(
dest_path,
"w",
encoding="utf-8",
) as fd:
fd.write(fi.read())
70 changes: 70 additions & 0 deletions docs/scripts/python.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""
Script used by mkdocs-gen-files to generate documentation for Pact Python.
The script is run by mkdocs-gen-files when the documentation is built in order
to generate documentation from Python docstrings.
"""
import subprocess
from pathlib import Path
from typing import Union

import mkdocs_gen_files


def process_python(src: str, dest: Union[str, None] = None) -> None:
"""
Process the Python files in the given directory.
The source directory is relative to the root of the repository, and only
Python files which are version controlled are processed. The generated
documentation may optionally written to a different directory.
"""
dest = dest or src

# List of all files version controlled files in the SRC_ROOT
files = sorted(
map(
Path,
subprocess.check_output(["git", "ls-files", src]) # noqa: S603, S607
.decode("utf-8")
.splitlines(),
),
)
files = filter(lambda p: p.suffix == ".py", files)
files = sorted(files)

for source_path in files:
module_path = source_path.relative_to(src).with_suffix("")
doc_path = source_path.relative_to(src).with_suffix(".md")
full_doc_path = Path(dest, doc_path)

parts = [src, *module_path.parts]

# Skip __main__ modules
if parts[-1] == "__main__":
continue

# The __init__ modules are implicit in the directory structure.
if parts[-1] == "__init__":
parts = parts[:-1]
full_doc_path = full_doc_path.parent / "README.md"

if full_doc_path.exists():
with mkdocs_gen_files.open(full_doc_path, "a", encoding="utf-8") as fd:
python_identifier = ".".join(parts)
print("# " + parts[-1], file=fd)
print("::: " + python_identifier, file=fd)
else:
with mkdocs_gen_files.open(full_doc_path, "w", encoding="utf-8") as fd:
python_identifier = ".".join(parts)
print("# " + parts[-1], file=fd)
print("::: " + python_identifier, file=fd)

mkdocs_gen_files.set_edit_path(
full_doc_path,
f"https://github.com/pact-foundation/pact-python/edit/master/pact/{module_path}.py",
)


process_python("pact")
process_python("examples")
4 changes: 4 additions & 0 deletions docs/scripts/ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
extend = "../../pyproject.toml"
ignore = [
"INP001", # Forbid implicit namespaces
]
Loading

0 comments on commit d555b56

Please sign in to comment.