plesty-sdk 0.0.2.dev2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plesty_sdk-0.0.2.dev2.dist-info/METADATA +59 -0
- plesty_sdk-0.0.2.dev2.dist-info/RECORD +20 -0
- plesty_sdk-0.0.2.dev2.dist-info/WHEEL +4 -0
- plesty_sdk-0.0.2.dev2.dist-info/entry_points.txt +2 -0
- plesty_sdk-0.0.2.dev2.dist-info/licenses/COPYING +674 -0
- plesty_sdk-0.0.2.dev2.dist-info/licenses/COPYING.LESSER +165 -0
- plesty_sdk-0.0.2.dev2.dist-info/licenses/LICENSE +165 -0
- plestysdk/__init__.py +1 -0
- plestysdk/assets/docs-build-gitlab-ci.yml +11 -0
- plestysdk/assets/logos/plesty_icon.png +0 -0
- plestysdk/assets/logos/plesty_logo.png +0 -0
- plestysdk/assets/ruff.toml +11 -0
- plestysdk/cache.py +106 -0
- plestysdk/cli.py +32 -0
- plestysdk/commands/check.py +146 -0
- plestysdk/commands/docs/__init__.py +35 -0
- plestysdk/commands/docs/deploy.py +193 -0
- plestysdk/commands/docs/serve.py +80 -0
- plestysdk/commands/docs/sphinx_builder.py +158 -0
- plestysdk/context.py +116 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""CLI commands for previewing and deploying Sphinx documentation pages."""
|
|
2
|
+
|
|
3
|
+
# Copyright (C) 2025, 2026 Christopher Borchers, fvrehlinger, Maximilian
|
|
4
|
+
# Heller, Michael Zopf (names in alphabetic order wrt. surnames)
|
|
5
|
+
#
|
|
6
|
+
# This file is part of plesty-sdk which is part of the Plesty library.
|
|
7
|
+
#
|
|
8
|
+
# plesty-sdk is free software: you can redistribute it and/or modify it under
|
|
9
|
+
# the terms of the GNU Lesser General Public License as published by the Free
|
|
10
|
+
# Software Foundation, either version 3 of the License, or (at your option) any
|
|
11
|
+
# later version.
|
|
12
|
+
#
|
|
13
|
+
# plesty-sdk is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
14
|
+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
|
15
|
+
# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License along
|
|
18
|
+
# with plesty-sdk. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
|
|
22
|
+
from plesty_sdk.commands.docs.deploy import deploy_command
|
|
23
|
+
from plesty_sdk.commands.docs.serve import serve_command
|
|
24
|
+
from plesty_sdk.context import ApplicationContext, pass_context
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@click.group('docs')
|
|
28
|
+
@pass_context
|
|
29
|
+
def docs_command(context: ApplicationContext):
|
|
30
|
+
"""Preview and deploy documentation pages."""
|
|
31
|
+
context.ensure_docs_index()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
docs_command.add_command(deploy_command)
|
|
35
|
+
docs_command.add_command(serve_command)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""The plesty docs deploy command."""
|
|
2
|
+
|
|
3
|
+
# Copyright (C) 2025, 2026 Christopher Borchers, fvrehlinger, Maximilian
|
|
4
|
+
# Heller, Michael Zopf (names in alphabetic order wrt. surnames)
|
|
5
|
+
#
|
|
6
|
+
# This file is part of plesty-sdk which is part of the Plesty library.
|
|
7
|
+
#
|
|
8
|
+
# plesty-sdk is free software: you can redistribute it and/or modify it under
|
|
9
|
+
# the terms of the GNU Lesser General Public License as published by the Free
|
|
10
|
+
# Software Foundation, either version 3 of the License, or (at your option) any
|
|
11
|
+
# later version.
|
|
12
|
+
#
|
|
13
|
+
# plesty-sdk is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
14
|
+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
|
15
|
+
# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License along
|
|
18
|
+
# with plesty-sdk. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
|
|
20
|
+
import contextlib
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import shutil
|
|
24
|
+
import subprocess
|
|
25
|
+
from importlib import metadata, resources
|
|
26
|
+
from packaging.version import InvalidVersion, Version
|
|
27
|
+
from tempfile import TemporaryDirectory
|
|
28
|
+
|
|
29
|
+
import click
|
|
30
|
+
|
|
31
|
+
from plesty_sdk.commands.docs.sphinx_builder import build_sphinx_app
|
|
32
|
+
from plesty_sdk.context import ApplicationContext, pass_context
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@click.command(
|
|
36
|
+
name='deploy', short_help="Build and push documentation pages to a remote branch."
|
|
37
|
+
)
|
|
38
|
+
@click.argument('remote_url', required=True)
|
|
39
|
+
@click.argument('target_branch', required=True)
|
|
40
|
+
@click.option(
|
|
41
|
+
"--release/--latest",
|
|
42
|
+
is_flag=True,
|
|
43
|
+
required=True,
|
|
44
|
+
help="Whether to deploy the documentation pages for a "
|
|
45
|
+
"release version or the latest development version.",
|
|
46
|
+
)
|
|
47
|
+
@pass_context
|
|
48
|
+
def deploy_command(
|
|
49
|
+
context: ApplicationContext, remote_url: str, target_branch: str, release: bool
|
|
50
|
+
):
|
|
51
|
+
"""Build and push the documentation pages for the Plesty project in the
|
|
52
|
+
current working directory to the <TARGET_BRANCH> of the repository at
|
|
53
|
+
<REMOTE_URL>. The branch is created if it does not yet exist.
|
|
54
|
+
"""
|
|
55
|
+
if release:
|
|
56
|
+
try:
|
|
57
|
+
version_str = metadata.version(context.pyproject_toml['project']['name'])
|
|
58
|
+
except metadata.PackageNotFoundError:
|
|
59
|
+
try:
|
|
60
|
+
version_str = context.pyproject_toml['project']['version']
|
|
61
|
+
except KeyError:
|
|
62
|
+
raise click.ClickException(
|
|
63
|
+
"The project version could not be retrieved either from the build "
|
|
64
|
+
"metadata - as the project is not installed in the current "
|
|
65
|
+
"environment - or directly from the pyproject.toml file."
|
|
66
|
+
)
|
|
67
|
+
try:
|
|
68
|
+
version = Version(version_str)
|
|
69
|
+
except InvalidVersion:
|
|
70
|
+
raise click.ClickException("The project version is not PEP 440 compliant.")
|
|
71
|
+
version_tag = f'{version.major}.{version.minor}'
|
|
72
|
+
build_dir = os.path.join('html', 'releases', version_tag)
|
|
73
|
+
else:
|
|
74
|
+
version_tag = None
|
|
75
|
+
build_dir = os.path.join('html', 'latest')
|
|
76
|
+
|
|
77
|
+
with TemporaryDirectory() as tmp_dir:
|
|
78
|
+
with contextlib.chdir(tmp_dir):
|
|
79
|
+
# check whether target branch exists on remote
|
|
80
|
+
if run_git(
|
|
81
|
+
['ls-remote', '--exit-code', '--heads', remote_url, target_branch],
|
|
82
|
+
expected_exit_codes={0, 2}
|
|
83
|
+
) == 0:
|
|
84
|
+
click.echo(f"Branch {target_branch} exists on remote. Cloning...")
|
|
85
|
+
run_git(['clone', '--branch', target_branch, '--depth', '1', remote_url, '.'])
|
|
86
|
+
else:
|
|
87
|
+
click.echo(
|
|
88
|
+
f"Branch {target_branch} does not yet exist "
|
|
89
|
+
"on remote. Creating orphan branch..."
|
|
90
|
+
)
|
|
91
|
+
run_git(['init'])
|
|
92
|
+
run_git(['checkout', '--orphan', target_branch])
|
|
93
|
+
with resources.as_file(
|
|
94
|
+
resources.files('plesty_sdk').joinpath('assets', 'docs-build-gitlab-ci.yml')
|
|
95
|
+
) as gitlab_ci_yml:
|
|
96
|
+
shutil.copyfile(gitlab_ci_yml, os.path.join(tmp_dir, '.gitlab-ci.yml'))
|
|
97
|
+
run_git(['add', '.gitlab-ci.yml'])
|
|
98
|
+
run_git(['commit', '--message', 'Initialize documentation branch.'])
|
|
99
|
+
|
|
100
|
+
click.echo("Building documentation...")
|
|
101
|
+
build_sphinx_app(
|
|
102
|
+
context.docs_settings,
|
|
103
|
+
version_tag,
|
|
104
|
+
build_dir=os.path.join(tmp_dir, build_dir),
|
|
105
|
+
delete_artifacts=True,
|
|
106
|
+
)
|
|
107
|
+
if version_tag is not None:
|
|
108
|
+
update_versions_json(os.path.join(tmp_dir, 'html', 'releases'), version_tag)
|
|
109
|
+
|
|
110
|
+
with contextlib.chdir(tmp_dir):
|
|
111
|
+
click.echo("Analyzing changes...")
|
|
112
|
+
run_git(['add', '--all'])
|
|
113
|
+
if run_git(['diff', '--staged', '--quiet'], expected_exit_codes={0, 1}) == 0:
|
|
114
|
+
click.echo("No changes made to the documentation pages.")
|
|
115
|
+
else:
|
|
116
|
+
commit_message = (
|
|
117
|
+
f"Add or update documentation pages for version {version_tag}."
|
|
118
|
+
) if version_tag is not None else "Update latest documentation pages."
|
|
119
|
+
run_git(['commit', '--message', commit_message])
|
|
120
|
+
run_git(['push', remote_url, target_branch])
|
|
121
|
+
click.echo("Changes to the documentation pages successfully pushed.")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def run_git(args: list[str], expected_exit_codes: set[int] = {0}) -> int:
|
|
125
|
+
"""Run a git command in the current working directory.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
args: The arguments and flags to be passed to the git command.
|
|
129
|
+
expected_exit_codes: Exit codes that should not raise an exception.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
The exit code of the git command.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
click.ClickException: If the exit code is not in `expected_exit_codes`.
|
|
136
|
+
"""
|
|
137
|
+
if shutil.which('git') is None:
|
|
138
|
+
raise click.ClickException("Git is not installed or not found in PATH.")
|
|
139
|
+
result = subprocess.run((command := ['git'] + args), capture_output=True)
|
|
140
|
+
if result.returncode in expected_exit_codes:
|
|
141
|
+
return result.returncode
|
|
142
|
+
else:
|
|
143
|
+
error_message = result.stderr.decode('utf-8', errors='replace')
|
|
144
|
+
raise click.ClickException(f"Failed to run {' '.join(command)}.\n{error_message}")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def version_tuple(version_tag: str) -> tuple[int, int]:
|
|
148
|
+
"""Parse a version tag into a tuple of the form (major, minor).
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
version_tag: The version tag to be parsed in 'major.minor' format.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Version tuple of the form (major, minor) as integers.
|
|
155
|
+
"""
|
|
156
|
+
major, minor = version_tag.split('.')
|
|
157
|
+
return (int(major), int(minor))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def update_versions_json(releases_dir: str, new_version_tag: str) -> None:
|
|
161
|
+
"""Update the versions.json file to include the newly built version in the correct order
|
|
162
|
+
without duplicates. If necessary, update the automatic redirection to the newest version.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
releases_dir: The directory containing the versions.json file.
|
|
166
|
+
new_version_tag: The version tag to be added.
|
|
167
|
+
"""
|
|
168
|
+
versions_json_path = os.path.join(releases_dir, 'versions.json')
|
|
169
|
+
try:
|
|
170
|
+
with open(versions_json_path, 'rt', encoding='utf-8') as file:
|
|
171
|
+
version_list: list[dict[str, str]] = json.load(file)
|
|
172
|
+
except FileNotFoundError:
|
|
173
|
+
version_list = []
|
|
174
|
+
|
|
175
|
+
new_version = version_tuple(new_version_tag)
|
|
176
|
+
new_version_entry = {'version': new_version_tag, 'url': f'/{new_version_tag}/'}
|
|
177
|
+
i = 0
|
|
178
|
+
while True:
|
|
179
|
+
if i == len(version_list):
|
|
180
|
+
version_list.append(new_version_entry)
|
|
181
|
+
break
|
|
182
|
+
old_version = version_tuple(version_list[i]['version'])
|
|
183
|
+
if new_version == old_version:
|
|
184
|
+
break
|
|
185
|
+
if new_version > old_version:
|
|
186
|
+
version_list.insert(i, new_version_entry)
|
|
187
|
+
break
|
|
188
|
+
i += 1
|
|
189
|
+
|
|
190
|
+
with open(versions_json_path, 'wt', encoding='utf-8') as file:
|
|
191
|
+
json.dump(version_list, file, indent=4)
|
|
192
|
+
with open(os.path.join(releases_dir, 'index.html'), 'wt') as file:
|
|
193
|
+
file.write(f'<script>window.location.replace("{version_list[0]['url']}")</script>')
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""The plesty docs serve command."""
|
|
2
|
+
|
|
3
|
+
# Copyright (C) 2025, 2026 Christopher Borchers, fvrehlinger, Maximilian
|
|
4
|
+
# Heller, Michael Zopf (names in alphabetic order wrt. surnames)
|
|
5
|
+
#
|
|
6
|
+
# This file is part of plesty-sdk which is part of the Plesty library.
|
|
7
|
+
#
|
|
8
|
+
# plesty-sdk is free software: you can redistribute it and/or modify it under
|
|
9
|
+
# the terms of the GNU Lesser General Public License as published by the Free
|
|
10
|
+
# Software Foundation, either version 3 of the License, or (at your option) any
|
|
11
|
+
# later version.
|
|
12
|
+
#
|
|
13
|
+
# plesty-sdk is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
14
|
+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
|
15
|
+
# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License along
|
|
18
|
+
# with plesty-sdk. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
from threading import Thread
|
|
23
|
+
|
|
24
|
+
import click
|
|
25
|
+
import livereload
|
|
26
|
+
import watchfiles
|
|
27
|
+
|
|
28
|
+
from plesty_sdk.cache import ensure_cache
|
|
29
|
+
from plesty_sdk.commands.docs.sphinx_builder import build_sphinx_app
|
|
30
|
+
from plesty_sdk.context import ApplicationContext, pass_context
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
logging.getLogger('livereload').disabled = True
|
|
34
|
+
for name in ['tornado.access', 'tornado.application', 'tornado.general']:
|
|
35
|
+
logging.getLogger(name).setLevel(logging.CRITICAL)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@click.command(
|
|
39
|
+
name='serve', short_help="Preview the documentation pages using a local webserver."
|
|
40
|
+
)
|
|
41
|
+
@click.option(
|
|
42
|
+
'--dev-addr',
|
|
43
|
+
default='localhost:8000',
|
|
44
|
+
help="IP address and port",
|
|
45
|
+
show_default=True,
|
|
46
|
+
metavar="<IP:PORT>",
|
|
47
|
+
)
|
|
48
|
+
@pass_context
|
|
49
|
+
def serve_command(context: ApplicationContext, dev_addr: str):
|
|
50
|
+
"""Start a local development webserver to preview the documentation pages
|
|
51
|
+
for the Plesty project in the current working directory. Changes to the
|
|
52
|
+
.md or .rst files are applied immediately via live reload.
|
|
53
|
+
"""
|
|
54
|
+
ensure_cache()
|
|
55
|
+
sphinx_app = build_sphinx_app(
|
|
56
|
+
context.docs_settings,
|
|
57
|
+
version_tag=None,
|
|
58
|
+
build_dir=os.path.join('.plesty', 'docs-build'),
|
|
59
|
+
delete_artifacts=False,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def watch_changes():
|
|
63
|
+
for _ in watchfiles.watch(sphinx_app.srcdir):
|
|
64
|
+
sphinx_app.build()
|
|
65
|
+
|
|
66
|
+
watcher_thread = Thread(target=watch_changes, daemon=True)
|
|
67
|
+
watcher_thread.start()
|
|
68
|
+
|
|
69
|
+
server = livereload.Server()
|
|
70
|
+
dev_host, dev_port = dev_addr.split(':')
|
|
71
|
+
click.echo(f"Serving preview at http://{dev_host}:{dev_port}.")
|
|
72
|
+
click.echo(
|
|
73
|
+
f"Changes in ./{os.path.relpath(sphinx_app.srcdir)} "
|
|
74
|
+
"are detected and immediately visible."
|
|
75
|
+
)
|
|
76
|
+
click.echo("Press Ctrl+C to stop the server.")
|
|
77
|
+
server.serve(
|
|
78
|
+
host=dev_host, port=int(dev_port), root=os.path.join('.plesty', 'docs-build')
|
|
79
|
+
)
|
|
80
|
+
click.echo("Server shut down.")
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Sphinx app configuration and build process."""
|
|
2
|
+
|
|
3
|
+
# Copyright (C) 2025, 2026 Christopher Borchers, fvrehlinger, Maximilian
|
|
4
|
+
# Heller, Michael Zopf (names in alphabetic order wrt. surnames)
|
|
5
|
+
#
|
|
6
|
+
# This file is part of plesty-sdk which is part of the Plesty library.
|
|
7
|
+
#
|
|
8
|
+
# plesty-sdk is free software: you can redistribute it and/or modify it under
|
|
9
|
+
# the terms of the GNU Lesser General Public License as published by the Free
|
|
10
|
+
# Software Foundation, either version 3 of the License, or (at your option) any
|
|
11
|
+
# later version.
|
|
12
|
+
#
|
|
13
|
+
# plesty-sdk is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
14
|
+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
|
15
|
+
# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License along
|
|
18
|
+
# with plesty-sdk. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import shutil
|
|
22
|
+
from datetime import date
|
|
23
|
+
from importlib import resources
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from sphinx.application import Sphinx
|
|
27
|
+
|
|
28
|
+
from plesty_sdk.context import DocumentationSettings
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def make_sphinx_config(
|
|
32
|
+
settings: DocumentationSettings, version_tag: str | None, logo_dir: os.PathLike[str]
|
|
33
|
+
) -> dict[str, Any]:
|
|
34
|
+
"""Set up the Sphinx configuration dictionary customized for the project.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
settings: The documentation settings.
|
|
38
|
+
version_tag: The version for which the documentation pages are built.
|
|
39
|
+
logo_dir: The directory containing the logo files.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
The Sphinx configuration dictionary.
|
|
43
|
+
"""
|
|
44
|
+
config = dict[str, Any](
|
|
45
|
+
project = settings.site_name,
|
|
46
|
+
copyright = f"{date.today().year}, Plesty Development Team",
|
|
47
|
+
extensions = [
|
|
48
|
+
'myst_parser',
|
|
49
|
+
'sphinx.ext.autodoc',
|
|
50
|
+
'sphinx.ext.napoleon',
|
|
51
|
+
'sphinxcontrib.mermaid',
|
|
52
|
+
'sphinx_design',
|
|
53
|
+
'sphinx_copybutton',
|
|
54
|
+
],
|
|
55
|
+
source_suffix = {
|
|
56
|
+
'.rst': 'restructuredtext',
|
|
57
|
+
'.md': 'markdown',
|
|
58
|
+
},
|
|
59
|
+
# HTML theme
|
|
60
|
+
html_theme = 'pydata_sphinx_theme',
|
|
61
|
+
html_theme_options = {
|
|
62
|
+
'navbar_center': ['navbar-nav'],
|
|
63
|
+
'secondary_sidebar_items': ['page-toc', 'sourcelink'],
|
|
64
|
+
'footer_start': ['copyright'],
|
|
65
|
+
'footer_center': [],
|
|
66
|
+
'footer_end': ['theme-version'],
|
|
67
|
+
'logo': {
|
|
68
|
+
'text': settings.site_name,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
html_sidebars = {
|
|
72
|
+
'*': [],
|
|
73
|
+
},
|
|
74
|
+
html_logo = os.path.join(logo_dir, 'plesty_logo.png'),
|
|
75
|
+
html_favicon = os.path.join(logo_dir, 'plesty_icon.png'),
|
|
76
|
+
# MyST configuration
|
|
77
|
+
myst_enable_extensions = ['colon_fence'],
|
|
78
|
+
myst_fence_as_directive = ['mermaid'],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if version_tag is not None:
|
|
82
|
+
config['html_theme_options']['navbar_center'].insert(0, 'version-switcher')
|
|
83
|
+
config['html_theme_options']['switcher'] = {
|
|
84
|
+
'json_url': '/versions.json',
|
|
85
|
+
'version_match': version_tag,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if settings.api_reference:
|
|
89
|
+
project_root = os.getcwd()
|
|
90
|
+
if not os.path.isdir(src_dir := os.path.join(project_root, 'src')):
|
|
91
|
+
src_dir = project_root
|
|
92
|
+
|
|
93
|
+
import_packages = [
|
|
94
|
+
entry.path for entry in os.scandir(src_dir) if (
|
|
95
|
+
entry.is_dir()
|
|
96
|
+
and entry.name != "tests"
|
|
97
|
+
and os.path.isfile(os.path.join(entry.path, '__init__.py'))
|
|
98
|
+
)
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
if import_packages:
|
|
102
|
+
config['extensions'].append('autoapi.extension')
|
|
103
|
+
config.update(
|
|
104
|
+
autoapi_generate_api_docs = True,
|
|
105
|
+
autoapi_add_toctree_entry = True,
|
|
106
|
+
autoapi_root = 'reference',
|
|
107
|
+
autoapi_dirs = import_packages,
|
|
108
|
+
autoapi_python_class_content = 'both',
|
|
109
|
+
napoleon_google_docstring = True,
|
|
110
|
+
autodoc_typehints = 'both',
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if settings.toc_file is not None:
|
|
114
|
+
config['extensions'].append('sphinx_external_toc')
|
|
115
|
+
config.update(
|
|
116
|
+
suppress_warnings = ["etoc.toctree"],
|
|
117
|
+
external_toc_path = settings.toc_file,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return config
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def build_sphinx_app(
|
|
124
|
+
settings: DocumentationSettings,
|
|
125
|
+
version_tag: str | None,
|
|
126
|
+
build_dir: str,
|
|
127
|
+
delete_artifacts: bool,
|
|
128
|
+
) -> Sphinx:
|
|
129
|
+
"""Build the Sphinx application.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
settings: The documentation settings.
|
|
133
|
+
version_tag: The version for which the documentation pages are built.
|
|
134
|
+
build_dir: The location where the documentation pages are built.
|
|
135
|
+
delete_artifacts: Whether to delete Sphinx build artifacts.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
The Sphinx application object.
|
|
139
|
+
"""
|
|
140
|
+
if os.path.isdir(build_dir):
|
|
141
|
+
shutil.rmtree(build_dir)
|
|
142
|
+
|
|
143
|
+
with resources.as_file(resources.files('plesty_sdk').joinpath('assets', 'logos')) as logo_dir:
|
|
144
|
+
app = Sphinx(
|
|
145
|
+
srcdir=settings.docs_dir,
|
|
146
|
+
confdir=None,
|
|
147
|
+
outdir=build_dir,
|
|
148
|
+
doctreedir=os.path.join(build_dir, '.doctrees'),
|
|
149
|
+
buildername='html',
|
|
150
|
+
confoverrides=make_sphinx_config(settings, version_tag, logo_dir),
|
|
151
|
+
)
|
|
152
|
+
app.build()
|
|
153
|
+
|
|
154
|
+
if delete_artifacts:
|
|
155
|
+
shutil.rmtree(os.path.join(build_dir, '.doctrees'))
|
|
156
|
+
os.remove(os.path.join(build_dir, '.buildinfo'))
|
|
157
|
+
|
|
158
|
+
return app
|
plestysdk/context.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Cross-command context management."""
|
|
2
|
+
|
|
3
|
+
# Copyright (C) 2025, 2026 Christopher Borchers, fvrehlinger, Maximilian
|
|
4
|
+
# Heller, Michael Zopf (names in alphabetic order wrt. surnames)
|
|
5
|
+
#
|
|
6
|
+
# This file is part of plesty-sdk which is part of the Plesty library.
|
|
7
|
+
#
|
|
8
|
+
# plesty-sdk is free software: you can redistribute it and/or modify it under
|
|
9
|
+
# the terms of the GNU Lesser General Public License as published by the Free
|
|
10
|
+
# Software Foundation, either version 3 of the License, or (at your option) any
|
|
11
|
+
# later version.
|
|
12
|
+
#
|
|
13
|
+
# plesty-sdk is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
14
|
+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
|
15
|
+
# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License along
|
|
18
|
+
# with plesty-sdk. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import tomllib
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
import click
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DocumentationSettings:
|
|
28
|
+
"""Settings for generating the documentation pages,
|
|
29
|
+
which can be configured in the pyproject.toml file.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
site_name: str
|
|
33
|
+
"""The title of the website present on each page next to the Plesty logo."""
|
|
34
|
+
docs_dir: str
|
|
35
|
+
"""The directory containing the .md or .rst files to generate the documentation from."""
|
|
36
|
+
api_reference: bool
|
|
37
|
+
"""Whether to automatically generate API reference pages."""
|
|
38
|
+
toc_file: str | None
|
|
39
|
+
"""The path to the external TOC file."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, pyproject_toml: dict[str, Any]) -> None:
|
|
42
|
+
"""Read the documentation settings specified by the user in the [tool.plesty.docs]
|
|
43
|
+
section of the pyproject.toml file and set default values for missing settings.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
pyproject_toml: The parsed content of the pyproject.toml file.
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
plesty_docs_section: dict[str, Any] = pyproject_toml['tool']['plesty']['docs']
|
|
50
|
+
except KeyError:
|
|
51
|
+
plesty_docs_section = {}
|
|
52
|
+
|
|
53
|
+
self.site_name = plesty_docs_section.get('site-name', pyproject_toml['project']['name'])
|
|
54
|
+
self.docs_dir = os.path.join(os.getcwd(), plesty_docs_section.get('docs-dir', 'docs'))
|
|
55
|
+
self.api_reference = plesty_docs_section.get('api-reference', True)
|
|
56
|
+
|
|
57
|
+
if (toc_file := plesty_docs_section.get('toc-file', 'toc.yaml')) == '':
|
|
58
|
+
self.toc_file = None
|
|
59
|
+
else:
|
|
60
|
+
self.toc_file = os.path.join(self.docs_dir, toc_file)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ApplicationContext:
|
|
64
|
+
"""Container class for storing data shared across multiple commands."""
|
|
65
|
+
|
|
66
|
+
pyproject_toml: dict[str, Any]
|
|
67
|
+
"""The parsed content of the pyproject.toml file in the current working directory."""
|
|
68
|
+
docs_settings: DocumentationSettings
|
|
69
|
+
"""The documentation settings."""
|
|
70
|
+
|
|
71
|
+
def __init__(self) -> None:
|
|
72
|
+
"""Parse the pyproject.toml file and read the documentation settings."""
|
|
73
|
+
pyproject_toml_path = os.path.join(os.getcwd(), 'pyproject.toml')
|
|
74
|
+
try:
|
|
75
|
+
with open(pyproject_toml_path, 'rb') as file:
|
|
76
|
+
self.pyproject_toml = tomllib.load(file)
|
|
77
|
+
except FileNotFoundError:
|
|
78
|
+
raise click.ClickException(
|
|
79
|
+
"No pyproject.toml file found in the current "
|
|
80
|
+
f"working directory at {pyproject_toml_path}."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
self.docs_settings = DocumentationSettings(self.pyproject_toml)
|
|
84
|
+
|
|
85
|
+
def ensure_docs_index(self) -> None:
|
|
86
|
+
"""Ensure that the docs directory exists and contains an index.md or index.rst
|
|
87
|
+
file, as well as the external TOC file, if specified in the documentation settings.
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
click.ClickException: If a required file is missing.
|
|
91
|
+
"""
|
|
92
|
+
if not os.path.isdir(self.docs_settings.docs_dir):
|
|
93
|
+
raise click.ClickException(
|
|
94
|
+
f"No docs directory found at {self.docs_settings.docs_dir}. Create one or ensure "
|
|
95
|
+
"that the correct docs directory is specified in the pyproject.toml file."
|
|
96
|
+
)
|
|
97
|
+
if not (
|
|
98
|
+
os.path.isfile(os.path.join(self.docs_settings.docs_dir, 'index.md'))
|
|
99
|
+
or os.path.isfile(os.path.join(self.docs_settings.docs_dir, 'index.rst'))
|
|
100
|
+
):
|
|
101
|
+
raise click.ClickException(
|
|
102
|
+
"No index.md or index.rst master file found in the docs "
|
|
103
|
+
"directory (entry point for the documentation generation)."
|
|
104
|
+
)
|
|
105
|
+
if not (
|
|
106
|
+
self.docs_settings.toc_file is None or os.path.isfile(self.docs_settings.toc_file)
|
|
107
|
+
):
|
|
108
|
+
raise click.ClickException(
|
|
109
|
+
f"No external TOC file found at {self.docs_settings.toc_file}. Create one or "
|
|
110
|
+
"ensure that the correct path is specified in the pyproject.toml file. If you "
|
|
111
|
+
"do not want to use an external table of contents, specify an empty string."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
pass_context = click.make_pass_decorator(ApplicationContext, ensure=True)
|
|
116
|
+
"""Decorator to pass the application context to a Click command as the first argument."""
|