File: update_gh_pages.py

package info (click to toggle)
python-envisage 7.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,880 kB
  • sloc: python: 8,696; makefile: 76; sh: 5
file content (196 lines) | stat: -rw-r--r-- 6,761 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# (C) Copyright 2007-2023 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

"""
Helper script for gh-pages documentation updates.

This script helps maintain the documentation in the gh-pages branch
of the repository; that documentation is automatically served by
GitHub and made available via docs.enthought.com.

The intended structure of the gh-pages branch is:

- the root directory contains documentation matching the contents
  of the 'main' branch of the codebase.
- named subdirectories with names of the form <major>.<minor> contain
  documentation for released versions of the package.
- the root directory also contains a 'latest' symlink pointing to the
  docs matching the latest release (i.e., the release with highest
  version number).

Example usage
-------------
In the examples below we assume that:

- the current working directory is the root of the repository
- the gh-pages branch has been checked out into ../docs
  (for example using `git worktree add ../docs gh-pages`)
- documentation has been built locally via Sphinx and is in docs/build/html

Then to update the docs in the root directory of the gh-pages branch (for
example after a push to the main branch), do:

    python docs/update_gh_pages.py docs/build/html ../docs

After releasing version 7.3.2 (for example) of the package, to update the
docs in the 7.3/ subdirectory of the gh-pages branch, do:

    python docs/update_gh_pages.py docs/build/html ../docs --tag 7.3.2

Note that for a bugfix release, the intention is that the docs for the bugfix
release (e.g., 7.3.2) overwrite the docs for the previous release with the same
<major>.<minor> version (e.g., 7.3.1). The docs end up in the 7.3/ subdirectory
of the gh-pages tree in both cases.
"""

import argparse
import pathlib
import re
import shutil

#: Matcher for names of directories containing release docs.
RELEASE_DOCS_DIR_MATCHER = re.compile(r"\d+\.\d+").fullmatch

#: Name of the symlink that points to the latest docs
LATEST = "latest"


def release_version(dir_name: str) -> list[int]:
    """
    Mapping from release docs directory names to orderable values.

    E.g., '7.13' -> (7, 13).
    """
    return [int(piece) for piece in dir_name.split(".")]


def subdir_from_tagname(version: str) -> str:
    """
    Map a version tag (e.g., '7.2.1') to the gh-pages subdirectory containing
    docs for that tag (e.g., '7.2').
    """
    subdir = ".".join(version.split(".")[:2])
    if not RELEASE_DOCS_DIR_MATCHER(subdir):
        raise RuntimeError(
            f"tagname {version} does not have the expected form"
        )
    return subdir


def update_latest_symlink(docs_dir: pathlib.Path) -> None:
    """
    Update the 'latest' symlink to point to documentation for the most recent
    release.

    docs_dir should point to the root gh-pages directory.
    """
    all_release_docs = [
        child.name
        for child in docs_dir.iterdir()
        if child.is_dir() and RELEASE_DOCS_DIR_MATCHER(child.name)
    ]
    latest_docs = max(all_release_docs, key=release_version)

    # Remove existing symlink if present.
    latest_symlink = docs_dir / LATEST
    if latest_symlink.is_symlink():
        print(f"Removing symlink {latest_symlink}")
        latest_symlink.unlink()

    # Create new symlink
    print(f"Updating symlink {latest_symlink} to point to {latest_docs}")
    latest_symlink.symlink_to(latest_docs, target_is_directory=True)


def remove_existing_docs(docs_dir: pathlib.Path) -> None:
    """
    Remove existing documentation files and directories.

    Skips hidden files and directories (like .nojekyll and .git), and
    ignores directories whose name matches <major>.<minor> - these are
    directories that contain previous documentation versions.
    """
    print(f"Removing existing documentation from {docs_dir} ...")
    for child in docs_dir.iterdir():
        if child.name.startswith("."):
            print(f"  Not removing hidden file or directory {child}")
        elif child.is_file():
            print(f"  Removing file {child}")
            child.unlink()
        elif child.is_dir():
            if RELEASE_DOCS_DIR_MATCHER(child.name):
                print(f"  Not removing release docs directory {child}")
            elif child.is_symlink() and child.name == LATEST:
                print(f"  Not removing symlink {child}")
            else:
                print(f"  Removing directory {child}")
                shutil.rmtree(child)
        else:
            raise RuntimeError("Not a file or directory: {child}: aborting")


def copy_new_docs(source_docs: pathlib.Path, target_dir: pathlib.Path) -> None:
    """
    Copy new documentation into place.

    Copies newly-built docs from their build location (e.g., docs/build/html)
    to the target directory in the gh-pages branch.

    Hidden files and directories (for example .buildinfo, .nojekyll, .doctrees)
    are ignored.
    """
    print(f"Copying docs from {source_docs} to {target_dir} ...")
    for child in source_docs.iterdir():
        if child.name.startswith("."):
            print(f"  Not copying hidden file or directory {child.name}")
        elif child.is_file():
            print(f"  Copying file {child} to {target_dir}")
            shutil.copyfile(child, target_dir / child.name)
        elif child.is_dir():
            print(f"  Copying directory {child} to {target_dir}")
            shutil.copytree(child, target_dir / child.name)
        else:
            raise RuntimeError("Not a file or directory: {child}: aborting")


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "source",
        help="Directory containing newly-built documentation",
        type=pathlib.Path,
    )
    parser.add_argument(
        "target",
        help="Directory containing the gh-pages checkout",
        type=pathlib.Path,
    )
    parser.add_argument(
        "--tag",
        help="Release tag name (when updating for a release)",
    )
    args = parser.parse_args()

    if args.tag is None:
        target = args.target
    else:
        target = args.target / subdir_from_tagname(args.tag)
        if not target.exists():
            print(f"Creating target directory {target}")
            target.mkdir()

    remove_existing_docs(target)
    copy_new_docs(args.source, target)
    if args.tag is not None:
        update_latest_symlink(args.target)


if __name__ == "__main__":
    main()