Skip to content

Commit 419d711

Browse files
committed
docs: setup mkdocs
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>
1 parent ca2562d commit 419d711

10 files changed

+481
-1
lines changed

.github/workflows/docs.yml

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: docs
2+
3+
on:
4+
push:
5+
6+
env:
7+
STABLE_PYTHON_VERSION: "3.12"
8+
PYTEST_ADDOPTS: --color=yes
9+
10+
jobs:
11+
build:
12+
name: Build docs
13+
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- name: Checkout code
18+
uses: actions/checkout@v4
19+
with:
20+
fetch-depth: 0
21+
22+
- name: Set up Python
23+
uses: actions/setup-python@v4
24+
with:
25+
python-version: ${{ env.STABLE_PYTHON_VERSION }}
26+
cache: pip
27+
28+
- name: Install Hatch
29+
run: pip install --upgrade hatch
30+
31+
- name: Build docs
32+
run: |
33+
hatch run mkdocs build
34+
35+
- name: Upload artifact
36+
uses: actions/upload-pages-artifact@v2
37+
with:
38+
path: site
39+
40+
publish:
41+
name: Publish docs
42+
43+
needs: build
44+
runs-on: ubuntu-latest
45+
permissions:
46+
contents: read
47+
pages: write
48+
id-token: write
49+
environment:
50+
name: github-pages
51+
url: ${{ steps.deployment.outputs.page_url }}
52+
53+
steps:
54+
- name: Deploy to GitHub Pages
55+
id: deployment
56+
uses: actions/deploy-pages@v2

.markdownlint.yml

+2
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ list-marker-space:
55
ol_single: 2
66
ol_multi: 2
77
line-length: false
8+
ul-indent:
9+
indent: 4

.pre-commit-config.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ repos:
2323
- id: check-toml
2424
- id: check-xml
2525
- id: check-yaml
26+
exclude: |
27+
(?x)^(
28+
mkdocs.yml
29+
)$
2630
2731
- repo: https://gitlab.com/bmares/check-json5
2832
rev: v1.0.0

docs/SUMMARY.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<!-- markdownlint-disable first-line-heading -->
2+
3+
- [Home](README.md)
4+
- [Changelog](CHANGELOG.md)
5+
- [Contributing](CONTRIBUTING.md)
6+
- [Pact](pact/)
7+
- [Examples](examples/)

docs/scripts/markdown.py

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""
2+
Script to merge Markdown documentation from the main codebase into the docs.
3+
4+
This script is run by mkdocs-gen-files when the documentation is built and
5+
imports Markdown documentation from the main codebase so that it can be included
6+
in the documentation site. For example, a Markdown file located at
7+
`some/path/foo.md` will be treated as if it was located at
8+
`docs/some/path/foo.md` without the need for symlinks or copying the file.
9+
10+
If the destination file already exists (either because it is a real file, or was
11+
otherwise already generated), the script will raise a RuntimeError.
12+
"""
13+
14+
import subprocess
15+
import sys
16+
from pathlib import Path
17+
18+
import mkdocs_gen_files
19+
from mkdocs_gen_files.editor import FilesEditor
20+
21+
EDITOR = FilesEditor.current()
22+
23+
# These paths are relative to the project root, *not* the current file.
24+
SRC_ROOT = "."
25+
DOCS_DEST = "."
26+
27+
# List of all files version controlled files in the SRC_ROOT
28+
ALL_FILES = sorted(
29+
map(
30+
Path,
31+
subprocess.check_output(["git", "ls-files", SRC_ROOT]) # noqa: S603, S607
32+
.decode("utf-8")
33+
.splitlines(),
34+
),
35+
)
36+
37+
38+
for source_path in filter(lambda p: p.suffix == ".md", ALL_FILES):
39+
if source_path.parts[0] == "docs":
40+
continue
41+
dest_path = Path(DOCS_DEST, source_path)
42+
43+
if str(dest_path) in EDITOR.files:
44+
print( # noqa: T201
45+
f"Unable to copy {source_path} to {dest_path} because the file already"
46+
" exists at the destination.",
47+
file=sys.stderr,
48+
)
49+
msg = f"File {dest_path} already exists."
50+
raise RuntimeError(msg)
51+
52+
with Path(source_path).open("r", encoding="utf-8") as fi, mkdocs_gen_files.open(
53+
dest_path,
54+
"w",
55+
encoding="utf-8",
56+
) as fd:
57+
fd.write(fi.read())
58+
59+
mkdocs_gen_files.set_edit_path(
60+
dest_path,
61+
f"https://github.com/pact-foundation/pact-python/edit/develop/{source_path}",
62+
)

docs/scripts/other.py

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""
2+
Create placeholder files for all other files in the codebase.
3+
4+
This script is run by mkdocs-gen-files when the documentation is built and
5+
creates placeholder files for all other files in the codebase. This is done so
6+
that the documentation site can link to all files in the codebase, even if they
7+
aren't part of the documentation proper.
8+
9+
If the files are binary, they are copied as-is (e.g. for images), otherwise a
10+
HTML redirect is created.
11+
12+
If the destination file already exists (either because it is a real file, or was
13+
otherwise already generated), the script will ignore the current file and
14+
continue silently.
15+
"""
16+
17+
import subprocess
18+
from pathlib import Path
19+
from typing import TYPE_CHECKING
20+
21+
import mkdocs_gen_files
22+
from mkdocs_gen_files.editor import FilesEditor
23+
24+
if TYPE_CHECKING:
25+
import io
26+
27+
EDITOR = FilesEditor.current()
28+
29+
# These paths are relative to the project root, *not* the current file.
30+
SRC_ROOT = "."
31+
DOCS_DEST = "."
32+
33+
# List of all files version controlled files in the SRC_ROOT
34+
ALL_FILES = sorted(
35+
map(
36+
Path,
37+
subprocess.check_output(["git", "ls-files", SRC_ROOT]) # noqa: S603, S607
38+
.decode("utf-8")
39+
.splitlines(),
40+
),
41+
)
42+
43+
44+
def is_binary(buffer: bytes) -> bool:
45+
"""
46+
Determine whether the given buffer is binary or not.
47+
48+
The check is done by attempting to decode the buffer as UTF-8. If this
49+
succeeds, the buffer is not binary. If it fails, the buffer is binary.
50+
51+
The entire buffer will be checked, therefore if checking whether a file is
52+
binary, only the start of the file should be passed.
53+
54+
Args:
55+
buffer:
56+
The buffer to check.
57+
58+
Returns:
59+
True if the buffer is binary, False otherwise.
60+
"""
61+
try:
62+
buffer.decode("utf-8")
63+
except UnicodeDecodeError:
64+
return True
65+
else:
66+
return False
67+
68+
69+
for source_path in ALL_FILES:
70+
if not source_path.is_file():
71+
continue
72+
if source_path.parts[0] in ["docs"]:
73+
continue
74+
75+
dest_path = Path(DOCS_DEST, source_path)
76+
77+
if str(dest_path) in EDITOR.files:
78+
continue
79+
80+
fi: "io.IOBase"
81+
with Path(source_path).open("rb") as fi:
82+
buf = fi.read(2048)
83+
84+
if is_binary(buf):
85+
if source_path.stat().st_size < 16 * 2**20:
86+
# Copy the file only if it's less than 16MB.
87+
with Path(source_path).open("rb") as fi, mkdocs_gen_files.open(
88+
dest_path,
89+
"wb",
90+
) as fd:
91+
fd.write(fi.read())
92+
else:
93+
# File is too big, create a redirect.
94+
url = (
95+
"https://github.com"
96+
"/pact-foundation/pact-python"
97+
"/raw"
98+
"/develop"
99+
f"/{source_path}"
100+
)
101+
with mkdocs_gen_files.open(dest_path, "w", encoding="utf-8") as fd:
102+
fd.write(f'<meta http-equiv="refresh" content="0; url={url}">')
103+
fd.write(f"# Redirecting to {url}...")
104+
fd.write(f"[Click here if you are not redirected]({url})")
105+
106+
mkdocs_gen_files.set_edit_path(
107+
dest_path,
108+
f"https://github.com/pact-foundation/pact-python/edit/develop/{source_path}",
109+
)
110+
111+
else:
112+
with Path(source_path).open("r", encoding="utf-8") as fi, mkdocs_gen_files.open(
113+
dest_path,
114+
"w",
115+
encoding="utf-8",
116+
) as fd:
117+
fd.write(fi.read())

docs/scripts/python.py

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""
2+
Script used by mkdocs-gen-files to generate documentation for Pact Python.
3+
4+
The script is run by mkdocs-gen-files when the documentation is built in order
5+
to generate documentation from Python docstrings.
6+
"""
7+
8+
import subprocess
9+
from pathlib import Path
10+
from typing import Union
11+
12+
import mkdocs_gen_files
13+
14+
15+
def process_python(src: str, dest: Union[str, None] = None) -> None:
16+
"""
17+
Process the Python files in the given directory.
18+
19+
The source directory is relative to the root of the repository, and only
20+
Python files which are version controlled are processed. The generated
21+
documentation may optionally written to a different directory.
22+
"""
23+
dest = dest or src
24+
25+
# List of all files version controlled files in the SRC_ROOT
26+
files = sorted(
27+
map(
28+
Path,
29+
subprocess.check_output(["git", "ls-files", src]) # noqa: S603, S607
30+
.decode("utf-8")
31+
.splitlines(),
32+
),
33+
)
34+
files = sorted(filter(lambda p: p.suffix == ".py", files))
35+
36+
for source_path in files:
37+
module_path = source_path.relative_to(src).with_suffix("")
38+
doc_path = source_path.relative_to(src).with_suffix(".md")
39+
full_doc_path = Path(dest, doc_path)
40+
41+
parts = [src, *module_path.parts]
42+
43+
# Skip __main__ modules
44+
if parts[-1] == "__main__":
45+
continue
46+
47+
# The __init__ modules are implicit in the directory structure.
48+
if parts[-1] == "__init__":
49+
parts = parts[:-1]
50+
full_doc_path = full_doc_path.parent / "README.md"
51+
52+
if full_doc_path.exists():
53+
with mkdocs_gen_files.open(full_doc_path, "a", encoding="utf-8") as fd:
54+
python_identifier = ".".join(parts)
55+
print("# " + parts[-1], file=fd)
56+
print("::: " + python_identifier, file=fd)
57+
else:
58+
with mkdocs_gen_files.open(full_doc_path, "w", encoding="utf-8") as fd:
59+
python_identifier = ".".join(parts)
60+
print("# " + parts[-1], file=fd)
61+
print("::: " + python_identifier, file=fd)
62+
63+
mkdocs_gen_files.set_edit_path(
64+
full_doc_path,
65+
f"https://github.com/pact-foundation/pact-python/edit/master/pact/{module_path}.py",
66+
)
67+
68+
69+
process_python("pact")
70+
process_python("examples")

docs/scripts/ruff.toml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
extend = "../../pyproject.toml"
2+
3+
[lint]
4+
ignore = [
5+
"INP001", # Forbid implicit namespaces
6+
]

0 commit comments

Comments
 (0)