Skip to content

Commit 3a50df9

Browse files
Introduce Sphinx Versioning (#57)
* Sphinx Versioning * Linking latest dir rather than copying * Function signaturevariable name fixed * source validaton for latest * Try catch for git tags absence Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Lint and format issue solve * lint fix * Indentation fix Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 613a61f commit 3a50df9

File tree

6 files changed

+178
-16
lines changed

6 files changed

+178
-16
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ python run.py build
6868
python run.py build-docs
6969
```
7070

71+
> ***Note: When releasing a new version, update ``switcher.json`` in ``docs/source/_static/``
72+
to include the new tag in the version dropdown for documentation.***
73+
7174
Options:
7275
- `--skip-build` (`-s`): Skip building before generating docs
7376

@@ -159,6 +162,7 @@ We welcome contributions! Please see our [Contributing Guide](https://github.com
159162

160163
We use [Semantic Versioning](https://semver.org/). For available versions, see the [tags on this repository](https://github.com/Autodesk/moldflow-api/tags).
161164

165+
162166
## License
163167

164168
This project is licensed under the Apache License 2.0 - see the [LICENSE](https://github.com/Autodesk/moldflow-api/blob/main/LICENSE) file for details.

docs/source/_static/switcher.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
[
2+
{
3+
"version": "v26.0.5",
4+
"name": "v26.0.5 (latest)",
5+
"url": "../v26.0.5/",
6+
"is_latest": true
7+
},
8+
{
9+
"version": "v26.0.4",
10+
"name": "v26.0.4",
11+
"url": "../v26.0.4/",
12+
"is_latest": false
13+
},
14+
{
15+
"version": "v26.0.3",
16+
"name": "v26.0.3",
17+
"url": "../v26.0.3/",
18+
"is_latest": false
19+
},
20+
{
21+
"version": "v26.0.2",
22+
"name": "v26.0.2",
23+
"url": "../v26.0.2/",
24+
"is_latest": false
25+
},
26+
{
27+
"version": "v26.0.1",
28+
"name": "v26.0.1",
29+
"url": "../v26.0.1/",
30+
"is_latest": false
31+
},
32+
{
33+
"version": "v26.0.0",
34+
"name": "v26.0.0",
35+
"url": "../v26.0.0/",
36+
"is_latest": false
37+
}
38+
]

docs/source/conf.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
# https://www.sphinx-doc.org/en/master/usage/configuration.html
99
import os
1010
import sys
11+
from pathlib import Path
1112

1213
sys.path.insert(
1314
0, os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'src', 'moldflow')
@@ -29,9 +30,15 @@
2930
'sphinx.ext.napoleon', # Supports Google-style/Numpy-style docstrings
3031
'sphinx.ext.viewcode',
3132
'sphinx_autodoc_typehints',
33+
'sphinx_multiversion',
3234
]
3335

3436
templates_path = ['_templates']
37+
smv_tag_whitelist = r'^v?\d+\.\d+\.\d+$'
38+
smv_branch_whitelist = r'^$'
39+
smv_remote_whitelist = r'^origin$'
40+
smv_latest_version = 'latest'
41+
3542
exclude_patterns = []
3643

3744
# -- Options for autodoc -----------------------------------------------------
@@ -53,9 +60,14 @@
5360
html_theme_options = {
5461
"back_to_top_button": False,
5562
"github_url": "https://github.com/Autodesk/moldflow-api",
56-
"external_links": "",
63+
"external_links": [
64+
{"name": "Changelog", "url": "https://github.com/Autodesk/moldflow-api/releases"}
65+
],
5766
"footer_end": "",
5867
"footer_start": "copyright",
68+
"navbar_start": ["navbar-logo", "version-switcher"],
69+
"navbar_end": ["theme-switcher", "navbar-icon-links"],
70+
"switcher": {"json_url": "_static/switcher.json", "version_match": smv_latest_version},
5971
}
6072
html_static_path = ['_static']
6173
html_title = "Moldflow API"
@@ -72,5 +84,34 @@ def skip_member(app, what, name, obj, skip, options):
7284
return skip
7385

7486

87+
def set_context_switcher(app, pagename, templatename, context, doctree):
88+
"""Set version_match in the switcher to show the correct version label."""
89+
90+
# Try output directory name (e.g., v26.0.2) - most reliable for sphinx-multiversion
91+
version_name = None
92+
if hasattr(app, "builder") and hasattr(app.builder, "outdir"):
93+
outdir_name = Path(app.builder.outdir).name
94+
if outdir_name and outdir_name != "html":
95+
version_name = outdir_name
96+
97+
# Fallback to sphinx-multiversion context
98+
if not version_name:
99+
current = context.get("current_version")
100+
if current:
101+
version_name = getattr(current, "name", None) or (
102+
current.get("name") if isinstance(current, dict) else None
103+
)
104+
105+
# Final fallback
106+
if not version_name:
107+
version_name = smv_latest_version
108+
109+
# Update switcher config for this page
110+
switcher = dict(app.config.html_theme_options.get("switcher", {}))
111+
switcher["version_match"] = version_name
112+
context["theme_switcher"] = switcher
113+
114+
75115
def setup(app):
76116
app.connect("autodoc-skip-member", skip_member)
117+
app.connect("html-page-context", set_context_switcher)

docs/source/readme.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1255,7 +1255,12 @@ The project includes a ``run.py`` script with several useful commands:
12551255
- ``python run.py test`` - Run tests
12561256
- ``python run.py lint`` - Run code linting
12571257
- ``python run.py format`` - Format code with black
1258-
- ``python run.py build-docs`` - Build documentation
1258+
- ``python run.py build-docs`` - Build versioned documentation (HTML uses git tags for the
1259+
navigation dropdown; run ``git fetch --tags`` locally before building)
1260+
1261+
.. note::
1262+
When releasing a new version, update ``switcher.json`` in ``docs/source/_static/``
1263+
to include the new tag in the version dropdown.
12591264

12601265
Contributing
12611266
============

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pydata-sphinx-theme==0.16.1
1010
pylint==3.3.4
1111
pytest==8.3.4
1212
sphinx==8.1.3
13+
sphinx-multiversion==0.2.4
1314
sphinx-autodoc-typehints==3.0.1
1415
twine==6.1.0
1516
PyGithub==2.7.0

run.py

Lines changed: 87 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@
6262
import shutil
6363
import glob
6464
from urllib.parse import urlparse
65-
6665
import docopt
6766
from github import Github
6867
import polib
68+
from packaging.version import InvalidVersion, Version
6969

7070

7171
WINDOWS = platform.system() == 'Windows'
@@ -86,7 +86,9 @@
8686
LOCALE_DIR = os.path.join(MOLDFLOW_DIR, 'locale')
8787
DOCS_DIR = os.path.join(ROOT_DIR, 'docs')
8888
DOCS_SOURCE_DIR = os.path.join(DOCS_DIR, 'source')
89+
DOCS_STATIC_DIR = os.path.join(DOCS_SOURCE_DIR, '_static')
8990
DOCS_BUILD_DIR = os.path.join(DOCS_DIR, 'build')
91+
DOCS_HTML_DIR = os.path.join(DOCS_BUILD_DIR, 'html')
9092
COVERAGE_HTML_DIR = os.path.join(ROOT_DIR, 'htmlcov')
9193
DIST_DIR = os.path.join(ROOT_DIR, 'dist')
9294

@@ -100,6 +102,7 @@
100102
VERSION_FILE = os.path.join(ROOT_DIR, VERSION_JSON)
101103
DIST_FILES = os.path.join(ROOT_DIR, 'dist', '*')
102104
PYTHON_FILES = [MOLDFLOW_DIR, DOCS_SOURCE_DIR, TEST_DIR, "run.py"]
105+
SWITCHER_JSON = os.path.join(DOCS_STATIC_DIR, 'switcher.json')
103106

104107

105108
def run_command(args, cwd=os.getcwd(), extra_env=None):
@@ -325,6 +328,51 @@ def build_mo():
325328
)
326329

327330

331+
def create_latest_alias(build_output: str) -> None:
332+
"""Create a 'latest' alias pointing to the newest version using symlinks when possible."""
333+
version_dirs = [d for d in os.listdir(build_output) if d.startswith('v')]
334+
if not version_dirs:
335+
return
336+
337+
def version_key(v):
338+
try:
339+
return Version(v.lstrip('v'))
340+
except InvalidVersion:
341+
return Version("0.0.0")
342+
343+
sorted_versions = sorted(version_dirs, key=version_key, reverse=True)
344+
latest_version = sorted_versions[0]
345+
latest_src = os.path.join(build_output, latest_version)
346+
latest_dest = os.path.join(build_output, 'latest')
347+
348+
# Verify source exists before proceeding
349+
if not os.path.exists(latest_src):
350+
logging.error("Source directory for 'latest' alias does not exist: %s", latest_src)
351+
return
352+
353+
# Clean up any existing 'latest' entry first
354+
if os.path.islink(latest_dest):
355+
os.unlink(latest_dest)
356+
elif os.path.isdir(latest_dest):
357+
shutil.rmtree(latest_dest)
358+
elif os.path.exists(latest_dest):
359+
os.remove(latest_dest)
360+
361+
# Try creating a symbolic link first (most efficient)
362+
logging.info("Creating 'latest' alias for %s", latest_version)
363+
try:
364+
os.symlink(latest_src, latest_dest, target_is_directory=True)
365+
logging.info("Created symbolic link: latest -> %s", latest_version)
366+
except (OSError, NotImplementedError) as err:
367+
# Fall back to copying if symlinks aren't supported
368+
logging.warning(
369+
"Could not create symbolic link for 'latest' alias (%s); "
370+
"falling back to copying documentation.",
371+
err,
372+
)
373+
shutil.copytree(latest_src, latest_dest)
374+
375+
328376
def build_docs(target, skip_build):
329377
"""Build Documentation"""
330378

@@ -338,19 +386,44 @@ def build_docs(target, skip_build):
338386
shutil.rmtree(DOCS_BUILD_DIR)
339387

340388
try:
341-
run_command(
342-
[
343-
sys.executable,
344-
'-m',
345-
'sphinx',
346-
'build',
347-
'-M',
348-
target,
349-
DOCS_SOURCE_DIR,
350-
DOCS_BUILD_DIR,
351-
],
352-
ROOT_DIR,
353-
)
389+
if target == 'html':
390+
build_output = os.path.join(DOCS_BUILD_DIR, 'html')
391+
try:
392+
# fmt: off
393+
run_command(
394+
[
395+
sys.executable, '-m', 'sphinx_multiversion',
396+
DOCS_SOURCE_DIR, build_output
397+
],
398+
ROOT_DIR,
399+
)
400+
except Exception as err:
401+
logging.error(
402+
"Failed to build documentation with sphinx_multiversion.\n"
403+
"This can happen if no Git tags or branches match your version pattern.\n"
404+
"Try running 'git fetch --tags' and ensure version tags exist in the repo.\n"
405+
"Underlying error: %s",
406+
str(err),
407+
)
408+
# Re-raise so the outer handler can log the general failure as well.
409+
raise
410+
# fmt: on
411+
create_latest_alias(build_output)
412+
else:
413+
# For other targets such as latex, pdf, etc.
414+
run_command(
415+
[
416+
sys.executable,
417+
'-m',
418+
'sphinx',
419+
'build',
420+
'-M',
421+
target,
422+
DOCS_SOURCE_DIR,
423+
DOCS_BUILD_DIR,
424+
],
425+
ROOT_DIR,
426+
)
354427
logging.info('Sphinx documentation built successfully.')
355428
except Exception as err:
356429
logging.error(

0 commit comments

Comments
 (0)