Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/source/_static/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,37 @@ h1, h2, h3, h4, h5, h6 {
}
}

/* Dropdown container in the brand slot (above search bar) */
.ek-project-selector-brand {
padding: 0.4rem 0.75rem 0.6rem;
}

/* Related projects dropdown */
.ek-project-selector {
display: flex;
align-items: center;
gap: 0.4rem;
}

.ek-project-selector select {
flex: 1;
min-width: 0;
background: #131320;
color: #fff;
border: 1px solid #444;
border-radius: 4px;
padding: 0.3rem 0.4rem;
font-size: 0.8rem;
font-family: inherit;
cursor: pointer;
outline: none;
appearance: auto;
}

.ek-project-selector select:focus {
border-color: #FCE54B;
}

/* Homepage card styling */
.sd-card .sd-card-img-top {
height: 100px;
Expand Down
44 changes: 44 additions & 0 deletions docs/source/_static/custom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
document.addEventListener("DOMContentLoaded", function () {
// Packages list is injected at build time from earthkit-packages.yml via earthkit-packages.js
var packages = window.earthkitPackages || [];
if (!packages.length) return;

// Build the <select> from the YAML-derived packages list
var select = document.createElement("select");
select.setAttribute("aria-label", "Select earthkit documentation");

// Placeholder option
var placeholder = document.createElement("option");
placeholder.value = "";
placeholder.textContent = "Explore the earthkit ecosystem";
placeholder.disabled = true;
placeholder.selected = true;
select.appendChild(placeholder);

packages.forEach(function (p) {
var opt = document.createElement("option");
opt.value = p.url;
opt.textContent = p.name;
select.appendChild(opt);
});

// Navigate immediately on selection
select.addEventListener("change", function () {
var url = select.value;
if (url) {
window.open(url, "_blank", "noopener,noreferrer");
select.value = "";
}
});

// Wrapper div
var wrapper = document.createElement("div");
wrapper.className = "ek-project-selector";
wrapper.appendChild(select);

// Mount into the brand placeholder above the search bar
var mount = document.getElementById("ek-project-selector-mount");
if (mount) {
mount.appendChild(wrapper);
}
});
5 changes: 5 additions & 0 deletions docs/source/_templates/autosummary/function.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{{ objname | escape | underline}}

.. currentmodule:: {{ module }}

.. autofunction:: {{ objname }}
14 changes: 14 additions & 0 deletions docs/source/_templates/sidebar/brand.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{# Earthkit sidebar brand: clickable logo + package selector mount point #}
<a class="sidebar-brand" href="{{ pathto(master_doc) }}">
{%- if theme_light_logo and theme_dark_logo %}
<div class="sidebar-logo-container">
<img class="sidebar-logo only-light" src="{{ pathto('_static/' + theme_light_logo, 1) }}" alt="{{ project }}"/>
<img class="sidebar-logo only-dark" src="{{ pathto('_static/' + theme_dark_logo, 1) }}" alt="{{ project }}"/>
</div>
{%- elif theme_light_logo %}
<div class="sidebar-logo-container">
<img class="sidebar-logo" src="{{ pathto('_static/' + theme_light_logo, 1) }}" alt="{{ project }}"/>
</div>
{%- endif %}
</a>
<div id="ek-project-selector-mount" class="ek-project-selector-brand"></div>
113 changes: 109 additions & 4 deletions docs/source/clean_autodocs.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,18 @@
while keeping full module paths as page titles
"""

import importlib
import re
import sys
import types
from pathlib import Path

# Ensure the project source tree takes precedence over any installed version of
# the package so that __all__ reflects the current source code.
_SRC_DIR = str(Path(__file__).resolve().parent.parent.parent / "src")
if _SRC_DIR not in sys.path:
sys.path.insert(0, _SRC_DIR)

# Import configuration from conf.py
import conf

Expand Down Expand Up @@ -70,15 +79,104 @@ def get_short_name(module_name: str) -> str:
return module_name


def clean_toctree(content: str, hidden_modules: list[str]) -> str:
def get_module_api(module_name: str) -> list[str]:
"""Get the public API of a module from its __all__ attribute.

Dynamically imports the module and returns ``__all__`` if present.

Args:
module_name: Full module name (e.g., 'earthkit.transforms.temporal')

Returns:
List of public member names, or empty list if not available.

"""
try:
module = importlib.import_module(module_name)
all_names = list(getattr(module, "__all__", []))
# Exclude submodule/subpackage entries — those are already represented
# in the toctree and must not get their own autosummary pages.
# Exclude dunder names (e.g. __version__) — these are internal and
# should never appear as individual API pages.
return [
name for name in all_names
if not isinstance(getattr(module, name, None), types.ModuleType)
and not (name.startswith("__") and name.endswith("__"))
]
except ImportError as exc:
print(f"Warning: could not import {module_name}: {exc}")
return []


def replace_automodule_with_autosummary(content: str, module_name: str) -> str:
"""Replace the automodule :members: block with an autosummary table.

The module docstring is preserved via a bare ``.. automodule::`` directive
(without ``:members:``). A ``.. autosummary::`` table with a per-module
``:toctree:`` subdirectory is appended so that sphinx-build generates a
dedicated HTML page for every public function.

Args:
content: RST file content
module_name: Full module name (e.g., 'earthkit.transforms.temporal')

Returns:
Updated RST content.

"""
members = get_module_api(module_name)
short_name = get_short_name(module_name)

# Always produce a bare automodule (docstring only — no inherited :members:
# options) to avoid documenting imported members inline, which would clash
# with the per-function pages generated by autosummary.
# The :no-members: / :no-imported-members: directives explicitly override
# the autodoc_default_options set in conf.py.
block_lines = [
f".. automodule:: {module_name}",
" :no-members:",
" :no-imported-members:",
"",
]

if members:
block_lines += [
f".. currentmodule:: {module_name}",
"",
".. autosummary::",
" :toctree: generated/" + short_name,
"",
]
for member in members:
block_lines.append(f" {member}")
block_lines.append("")

new_block = "\n".join(block_lines)

# Replace the entire automodule block (with any autodoc options) that
# sphinx-apidoc emits. The block starts with ``.. automodule::`` and
# continues through consecutive option lines (`` :option:``).
content = re.sub(
r"\.\. automodule::[ \t]+" + re.escape(module_name) + r"(?:\n[ \t]+:.*)*\n",
new_block + "\n",
content,
)
return content


def clean_toctree(content: str, hidden_modules: list[str], max_depth: int | None = None) -> str:
"""Clean up toctree entries in RST content.

- Removes entries for hidden/private modules
- Updates entries to use short display names: 'temporal <earthkit.transforms.temporal>'
- Optionally overrides :maxdepth: to control how deep the TOC is rendered on the page

Args:
content: RST file content
hidden_modules: List of module names to hide
max_depth: If set, replaces :maxdepth: in every toctree with this number.
This only affects the TOC rendering depth on the page; all linked pages
are still fully built and reachable.

Returns:
Updated RST content
Expand Down Expand Up @@ -113,7 +211,10 @@ def clean_toctree(content: str, hidden_modules: list[str]) -> str:

# Check if this is a toctree option (starts with :)
if stripped.startswith(":"):
result_lines.append(line)
if max_depth is not None and stripped.startswith(":maxdepth:"):
result_lines.append(f"{current_indent}:maxdepth: {max_depth}")
else:
result_lines.append(line)
continue

# This is a toctree entry - extract the module name
Expand Down Expand Up @@ -166,9 +267,13 @@ def clean_autodocs():
files_deleted += 1
continue

# Clean up toctree entries in remaining files
# Clean up toctree entries and replace automodule with autosummary
content = rst_file.read_text()
new_content = clean_toctree(content, hidden_modules)
# Limit the toctree depth on the top-level package page to 1 so it only
# lists subpackage names. All child pages are still fully built.
toctree_depth = 1 if module_name == MODULE_PREFIX else None
new_content = clean_toctree(content, hidden_modules, max_depth=toctree_depth)
new_content = replace_automodule_with_autosummary(new_content, module_name)

if new_content != content:
rst_file.write_text(new_content)
Expand Down
39 changes: 34 additions & 5 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information

import datetime
import json
import os
import sys

import yaml

on_rtd = os.environ.get("READTHEDOCS") == "True"

if on_rtd:
Expand Down Expand Up @@ -49,7 +52,7 @@
# Links to the documentation of other projects via cross-references (see list below)
"sphinx.ext.intersphinx",
# Generates summary tables for modules/classes/functions
# "sphinx.ext.autosummary",
"sphinx.ext.autosummary",
# Allows citing BibTeX bibliographic entries in reStructuredText
# "sphinxcontrib.bibtex",
# Tests snippets in documentation by running embedded Python examples
Expand Down Expand Up @@ -82,6 +85,10 @@
"show-inheritance": True,
}

autosummary_generate = True
autosummary_generate_overwrite = True
autosummary_imported_members = False

# GitHub links configuration
extlinks = {
"pr": ("https://github.com/ecmwf/earthkit-transforms/pull/%s", "PR #%s"),
Expand All @@ -108,15 +115,13 @@
# These modules will not appear in the API documentation sidebar
autodocs_hidden_modules = [
"aggregate",
"version"
]

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []

# TODO: remove for version 1.0
nbsphinx_allow_errors = True
exclude_patterns: list[str] = []

# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
Expand All @@ -129,6 +134,11 @@
"custom.css",
]

html_js_files = [
"earthkit-packages.js", # generated from earthkit-packages.yml at build time
"custom.js",
]

html_favicon = "https://raw.githubusercontent.com/ecmwf/logos/refs/heads/main/logos/earthkit/earthkit-logo-only.svg"

d_thing = (
Expand All @@ -139,6 +149,25 @@
"1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25"
".54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"
)


def _write_earthkit_packages_js(app):
"""Read earthkit-packages.yml and write a JS data file into the output _static dir."""
config_path = os.path.join(os.path.dirname(__file__), "earthkit-packages.yml")
with open(config_path) as fh:
config = yaml.safe_load(fh)
packages = config.get("packages", [])
static_dir = os.path.join(app.outdir, "_static")
os.makedirs(static_dir, exist_ok=True)
js_path = os.path.join(static_dir, "earthkit-packages.js")
with open(js_path, "w") as fh:
fh.write(f"window.earthkitPackages = {json.dumps(packages)};\n")


def setup(app):
app.connect("builder-inited", _write_earthkit_packages_js)


html_theme_options = {
"light_css_variables": {
"color-sidebar-background": "#131320",
Expand Down
17 changes: 17 additions & 0 deletions docs/source/earthkit-packages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# List of earthkit documentation packages shown in the sidebar dropdown.

packages:
- name: earthkit
url: https://earthkit.readthedocs.io/en/latest
- name: earthkit-data
url: https://earthkit-data.readthedocs.io/en/latest
- name: earthkit-geo
url: https://earthkit-geo.readthedocs.io/en/latest
- name: earthkit-hydro
url: https://earthkit-hydro.readthedocs.io/en/latest
- name: earthkit-meteo
url: https://earthkit-meteo.readthedocs.io/en/latest
- name: earthkit-plots
url: https://earthkit-plots.readthedocs.io/en/latest
- name: earthkit-transforms
url: https://earthkit-transforms.readthedocs.io/en/latest
12 changes: 0 additions & 12 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,3 @@ statistics analysis accross time dimensions/coordinates.
release-notes/index
licence
genindex


.. toctree::
:maxdepth: 2
:caption: Related projects
:hidden:

earthkit <https://earthkit.readthedocs.io/en/latest>
earthkit-data <https://earthkit-data.readthedocs.io/en/latest>
earthkit-plots <https://earthkit-plots.readthedocs.io/en/latest>
earthkit-meteo <https://earthkit-meteo.readthedocs.io/en/latest>
earthkit-hydro <https://earthkit-hydro.readthedocs.io/en/latest>
Loading