From ef774f480fef6136ae0041e03c065ea2f43c6e59 Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Mon, 12 Jan 2026 09:48:47 +0530 Subject: [PATCH 1/8] Sphinx Versioning --- README.md | 4 +++ docs/source/_static/switcher.json | 38 ++++++++++++++++++++ docs/source/conf.py | 43 +++++++++++++++++++++- docs/source/readme.rst | 7 +++- requirements.txt | 1 + run.py | 60 +++++++++++++++++++++++-------- 6 files changed, 137 insertions(+), 16 deletions(-) create mode 100644 docs/source/_static/switcher.json diff --git a/README.md b/README.md index 3275017..cc71939 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,9 @@ python run.py build python run.py build-docs ``` +> ***Note: When releasing a new version, update ``switcher.json`` in ``docs/source/_static/`` + to include the new tag in the version dropdown for documentation.*** + Options: - `--skip-build` (`-s`): Skip building before generating docs @@ -159,6 +162,7 @@ We welcome contributions! Please see our [Contributing Guide](https://github.com We use [Semantic Versioning](https://semver.org/). For available versions, see the [tags on this repository](https://github.com/Autodesk/moldflow-api/tags). + ## License 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. diff --git a/docs/source/_static/switcher.json b/docs/source/_static/switcher.json new file mode 100644 index 0000000..33b7da0 --- /dev/null +++ b/docs/source/_static/switcher.json @@ -0,0 +1,38 @@ +[ + { + "version": "v26.0.5", + "name": "v26.0.5 (latest)", + "url": "../v26.0.5/", + "is_latest": true + }, + { + "version": "v26.0.4", + "name": "v26.0.4", + "url": "../v26.0.4/", + "is_latest": false + }, + { + "version": "v26.0.3", + "name": "v26.0.3", + "url": "../v26.0.3/", + "is_latest": false + }, + { + "version": "v26.0.2", + "name": "v26.0.2", + "url": "../v26.0.2/", + "is_latest": false + }, + { + "version": "v26.0.1", + "name": "v26.0.1", + "url": "../v26.0.1/", + "is_latest": false + }, + { + "version": "v26.0.0", + "name": "v26.0.0", + "url": "../v26.0.0/", + "is_latest": false + } +] \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index c9c2b29..7ed1a52 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -8,6 +8,7 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html import os import sys +from pathlib import Path sys.path.insert( 0, os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'src', 'moldflow') @@ -29,9 +30,15 @@ 'sphinx.ext.napoleon', # Supports Google-style/Numpy-style docstrings 'sphinx.ext.viewcode', 'sphinx_autodoc_typehints', + 'sphinx_multiversion', ] templates_path = ['_templates'] +smv_tag_whitelist = r'^v?\d+\.\d+\.\d+$' +smv_branch_whitelist = r'^$' +smv_remote_whitelist = r'^origin$' +smv_latest_version = 'latest' + exclude_patterns = [] # -- Options for autodoc ----------------------------------------------------- @@ -53,9 +60,14 @@ html_theme_options = { "back_to_top_button": False, "github_url": "https://github.com/Autodesk/moldflow-api", - "external_links": "", + "external_links": [ + {"name": "Changelog", "url": "https://github.com/Autodesk/moldflow-api/releases"} + ], "footer_end": "", "footer_start": "copyright", + "navbar_start": ["navbar-logo", "version-switcher"], + "navbar_end": ["theme-switcher", "navbar-icon-links"], + "switcher": {"json_url": "_static/switcher.json", "version_match": smv_latest_version}, } html_static_path = ['_static'] html_title = "Moldflow API" @@ -72,5 +84,34 @@ def skip_member(app, what, name, obj, skip, options): return skip +def set_context_switcher(app, _, __, context, ___): + """Set version_match in the switcher to show the correct version label.""" + + # Try output directory name (e.g., v26.0.2) - most reliable for sphinx-multiversion + version_name = None + if hasattr(app, "builder") and hasattr(app.builder, "outdir"): + outdir_name = Path(app.builder.outdir).name + if outdir_name and outdir_name != "html": + version_name = outdir_name + + # Fallback to sphinx-multiversion context + if not version_name: + current = context.get("current_version") + if current: + version_name = getattr(current, "name", None) or ( + current.get("name") if isinstance(current, dict) else None + ) + + # Final fallback + if not version_name: + version_name = smv_latest_version + + # Update switcher config for this page + switcher = dict(app.config.html_theme_options.get("switcher", {})) + switcher["version_match"] = version_name + context["theme_switcher"] = switcher + + def setup(app): app.connect("autodoc-skip-member", skip_member) + app.connect("html-page-context", set_context_switcher) diff --git a/docs/source/readme.rst b/docs/source/readme.rst index 8abbc6c..5855a49 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -1255,7 +1255,12 @@ The project includes a ``run.py`` script with several useful commands: - ``python run.py test`` - Run tests - ``python run.py lint`` - Run code linting - ``python run.py format`` - Format code with black -- ``python run.py build-docs`` - Build documentation +- ``python run.py build-docs`` - Build versioned documentation (HTML uses git tags for the + navigation dropdown; run ``git fetch --tags`` locally before building) + +.. note:: + When releasing a new version, update ``switcher.json`` in ``docs/source/_static/`` + to include the new tag in the version dropdown. Contributing ============ diff --git a/requirements.txt b/requirements.txt index ee98e74..2e1dad7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ pydata-sphinx-theme==0.16.1 pylint==3.3.4 pytest==8.3.4 sphinx==8.1.3 +sphinx-multiversion==0.2.4 sphinx-autodoc-typehints==3.0.1 twine==6.1.0 PyGithub==2.7.0 diff --git a/run.py b/run.py index 5c25149..0a3d8e9 100644 --- a/run.py +++ b/run.py @@ -62,10 +62,10 @@ import shutil import glob from urllib.parse import urlparse - import docopt from github import Github import polib +from packaging.version import InvalidVersion, Version WINDOWS = platform.system() == 'Windows' @@ -86,7 +86,9 @@ LOCALE_DIR = os.path.join(MOLDFLOW_DIR, 'locale') DOCS_DIR = os.path.join(ROOT_DIR, 'docs') DOCS_SOURCE_DIR = os.path.join(DOCS_DIR, 'source') +DOCS_STATIC_DIR = os.path.join(DOCS_SOURCE_DIR, '_static') DOCS_BUILD_DIR = os.path.join(DOCS_DIR, 'build') +DOCS_HTML_DIR = os.path.join(DOCS_BUILD_DIR, 'html') COVERAGE_HTML_DIR = os.path.join(ROOT_DIR, 'htmlcov') DIST_DIR = os.path.join(ROOT_DIR, 'dist') @@ -100,6 +102,7 @@ VERSION_FILE = os.path.join(ROOT_DIR, VERSION_JSON) DIST_FILES = os.path.join(ROOT_DIR, 'dist', '*') PYTHON_FILES = [MOLDFLOW_DIR, DOCS_SOURCE_DIR, TEST_DIR, "run.py"] +SWITCHER_JSON = os.path.join(DOCS_STATIC_DIR, 'switcher.json') def run_command(args, cwd=os.getcwd(), extra_env=None): @@ -325,6 +328,26 @@ def build_mo(): ) +def create_latest_alias(build_output): + """Create a 'latest' alias pointing to the newest version""" + version_dirs = [d for d in os.listdir(build_output) if d.startswith('v')] + if version_dirs: + + def version_key(v): + try: + return Version(v.lstrip('v')) + except InvalidVersion: + return Version("0.0.0") + + sorted_versions = sorted(version_dirs, key=version_key, reverse=True) + latest_version = sorted_versions[0] + latest_src = os.path.join(build_output, latest_version) + latest_dest = os.path.join(build_output, 'latest') + + logging.info("Creating 'latest' alias for %s", latest_version) + shutil.copytree(latest_src, latest_dest, dirs_exist_ok=True) + + def build_docs(target, skip_build): """Build Documentation""" @@ -338,19 +361,28 @@ def build_docs(target, skip_build): shutil.rmtree(DOCS_BUILD_DIR) try: - run_command( - [ - sys.executable, - '-m', - 'sphinx', - 'build', - '-M', - target, - DOCS_SOURCE_DIR, - DOCS_BUILD_DIR, - ], - ROOT_DIR, - ) + if target == 'html': + build_output = os.path.join(DOCS_BUILD_DIR, 'html') + run_command( + [sys.executable, '-m', 'sphinx_multiversion', DOCS_SOURCE_DIR, build_output], + ROOT_DIR, + ) + create_latest_alias(build_output) + else: + # For other targets such as latex, pdf, etc. + run_command( + [ + sys.executable, + '-m', + 'sphinx', + 'build', + '-M', + target, + DOCS_SOURCE_DIR, + DOCS_BUILD_DIR, + ], + ROOT_DIR, + ) logging.info('Sphinx documentation built successfully.') except Exception as err: logging.error( From b61a0be81a2411ab825b9d9adb7585ce01f92f36 Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Mon, 12 Jan 2026 10:12:44 +0530 Subject: [PATCH 2/8] Linking latest dir rather than copying --- run.py | 54 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/run.py b/run.py index 0a3d8e9..23d3295 100644 --- a/run.py +++ b/run.py @@ -328,24 +328,44 @@ def build_mo(): ) -def create_latest_alias(build_output): - """Create a 'latest' alias pointing to the newest version""" +def create_latest_alias(build_output: str) -> None: + """Create a 'latest' alias pointing to the newest version using symlinks when possible.""" version_dirs = [d for d in os.listdir(build_output) if d.startswith('v')] - if version_dirs: - - def version_key(v): - try: - return Version(v.lstrip('v')) - except InvalidVersion: - return Version("0.0.0") - - sorted_versions = sorted(version_dirs, key=version_key, reverse=True) - latest_version = sorted_versions[0] - latest_src = os.path.join(build_output, latest_version) - latest_dest = os.path.join(build_output, 'latest') - - logging.info("Creating 'latest' alias for %s", latest_version) - shutil.copytree(latest_src, latest_dest, dirs_exist_ok=True) + if not version_dirs: + return + + def version_key(v): + try: + return Version(v.lstrip('v')) + except InvalidVersion: + return Version("0.0.0") + + sorted_versions = sorted(version_dirs, key=version_key, reverse=True) + latest_version = sorted_versions[0] + latest_src = os.path.join(build_output, latest_version) + latest_dest = os.path.join(build_output, 'latest') + + # Clean up any existing 'latest' entry first + if os.path.islink(latest_dest): + os.unlink(latest_dest) + elif os.path.isdir(latest_dest): + shutil.rmtree(latest_dest) + elif os.path.exists(latest_dest): + os.remove(latest_dest) + + # Try creating a symbolic link first (most efficient) + logging.info("Creating 'latest' alias for %s", latest_version) + try: + os.symlink(latest_src, latest_dest, target_is_directory=True) + logging.info("Created symbolic link: latest -> %s", latest_version) + except (OSError, NotImplementedError) as err: + # Fall back to copying if symlinks aren't supported + logging.warning( + "Could not create symbolic link for 'latest' alias (%s); " + "falling back to copying documentation.", + err, + ) + shutil.copytree(latest_src, latest_dest) def build_docs(target, skip_build): From 2764555d00dbb653216bf54493f27941761f11a9 Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Mon, 12 Jan 2026 10:21:55 +0530 Subject: [PATCH 3/8] Function signaturevariable name fixed --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7ed1a52..71053fd 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -84,7 +84,7 @@ def skip_member(app, what, name, obj, skip, options): return skip -def set_context_switcher(app, _, __, context, ___): +def set_context_switcher(app, pagename, templatename, context, doctree): """Set version_match in the switcher to show the correct version label.""" # Try output directory name (e.g., v26.0.2) - most reliable for sphinx-multiversion From fab79c3a546b726c728818e6cd971e744b19aa07 Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Mon, 12 Jan 2026 11:07:21 +0530 Subject: [PATCH 4/8] source validaton for latest --- run.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/run.py b/run.py index 23d3295..c3d83ce 100644 --- a/run.py +++ b/run.py @@ -345,6 +345,11 @@ def version_key(v): latest_src = os.path.join(build_output, latest_version) latest_dest = os.path.join(build_output, 'latest') + # Verify source exists before proceeding + if not os.path.exists(latest_src): + logging.error("Source directory for 'latest' alias does not exist: %s", latest_src) + return + # Clean up any existing 'latest' entry first if os.path.islink(latest_dest): os.unlink(latest_dest) From eae28d7b08ccca27d37d22f37d42f5fde42767a6 Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Mon, 12 Jan 2026 11:08:45 +0530 Subject: [PATCH 5/8] Try catch for git tags absence Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- run.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/run.py b/run.py index c3d83ce..a27447e 100644 --- a/run.py +++ b/run.py @@ -388,10 +388,21 @@ def build_docs(target, skip_build): try: if target == 'html': build_output = os.path.join(DOCS_BUILD_DIR, 'html') - run_command( - [sys.executable, '-m', 'sphinx_multiversion', DOCS_SOURCE_DIR, build_output], - ROOT_DIR, - ) + try: + run_command( + [sys.executable, '-m', 'sphinx_multiversion', DOCS_SOURCE_DIR, build_output], + ROOT_DIR, + ) + except Exception as err: + logging.error( + "Failed to build documentation with sphinx_multiversion.\n" + "This can happen if no Git tags or branches match your version pattern.\n" + "Try running 'git fetch --tags' and ensure that version tags exist in the repository.\n" + "Underlying error: %s", + str(err), + ) + # Re-raise so the outer handler can log the general failure as well. + raise create_latest_alias(build_output) else: # For other targets such as latex, pdf, etc. From c5331fdd4f811da91b02e3c611a1820a79a2ec20 Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Mon, 12 Jan 2026 18:28:21 +0530 Subject: [PATCH 6/8] Lint and format issue solve --- run.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/run.py b/run.py index c3d83ce..3164c30 100644 --- a/run.py +++ b/run.py @@ -388,10 +388,15 @@ def build_docs(target, skip_build): try: if target == 'html': build_output = os.path.join(DOCS_BUILD_DIR, 'html') + # fmt: off run_command( - [sys.executable, '-m', 'sphinx_multiversion', DOCS_SOURCE_DIR, build_output], + [ + sys.executable, '-m', 'sphinx_multiversion', + DOCS_SOURCE_DIR, build_output + ], ROOT_DIR, ) + # fmt: on create_latest_alias(build_output) else: # For other targets such as latex, pdf, etc. From a1a3f714c661bc7a3c4bccbd3804815cf865b357 Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Mon, 12 Jan 2026 18:46:17 +0530 Subject: [PATCH 7/8] lint fix --- run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.py b/run.py index 70ad00c..5315d33 100644 --- a/run.py +++ b/run.py @@ -401,7 +401,7 @@ def build_docs(target, skip_build): logging.error( "Failed to build documentation with sphinx_multiversion.\n" "This can happen if no Git tags or branches match your version pattern.\n" - "Try running 'git fetch --tags' and ensure that version tags exist in the repository.\n" + "Try running 'git fetch --tags' and ensure version tags exist in the repo.\n" "Underlying error: %s", str(err), ) From 8fa732a49265772755028ed1d2dd34db1efee3a4 Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Mon, 12 Jan 2026 18:57:09 +0530 Subject: [PATCH 8/8] Indentation fix Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- run.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/run.py b/run.py index 5315d33..c477948 100644 --- a/run.py +++ b/run.py @@ -392,9 +392,9 @@ def build_docs(target, skip_build): # fmt: off run_command( [ - sys.executable, '-m', 'sphinx_multiversion', - DOCS_SOURCE_DIR, build_output - ], + sys.executable, '-m', 'sphinx_multiversion', + DOCS_SOURCE_DIR, build_output + ], ROOT_DIR, ) except Exception as err: