Skip to content

Commit 029e1d4

Browse files
Merge pull request #76 from Caltech-IPAC/FIREFLY-1862-docs-tests-cleanup
FIREFLY-1862: Cleanup the firefly-client docs and example notebooks
2 parents 50f08e2 + eb7b1ff commit 029e1d4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+3920
-1602
lines changed

.github/workflows/publish-docs.yml

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ on:
44
branches:
55
- master
66

7+
pull_request:
8+
branches:
9+
- master
10+
711
workflow_dispatch: # Manual trigger
812

913
jobs:
@@ -13,6 +17,11 @@ jobs:
1317
steps:
1418
- uses: actions/checkout@v4
1519

20+
- name: Install system dependencies (pandoc)
21+
run: |
22+
sudo apt-get update
23+
sudo apt-get install -y pandoc
24+
1625
- uses: actions/setup-python@v5
1726
with:
1827
python-version: '3.10'
@@ -33,9 +42,25 @@ jobs:
3342
make clean
3443
make html
3544
36-
- name: Deploy docs
45+
# Deploy stable docs (root of gh-pages) only from master pushes
46+
- name: Deploy docs (stable)
47+
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
3748
uses: peaceiris/actions-gh-pages@v3
3849
with:
3950
github_token: ${{ secrets.GITHUB_TOKEN }}
4051
publish_dir: ./docs/_build/html
4152

53+
# Deploy PR preview docs to gh-pages under pr/<number>/
54+
- name: Deploy docs (PR preview)
55+
if: github.event_name == 'pull_request'
56+
uses: peaceiris/actions-gh-pages@v3
57+
with:
58+
github_token: ${{ secrets.GITHUB_TOKEN }}
59+
publish_dir: ./docs/_build/html
60+
destination_dir: pr/${{ github.event.pull_request.number }}
61+
keep_files: true
62+
63+
- name: Print PR preview URL
64+
if: github.event_name == 'pull_request'
65+
run: |
66+
echo "Deployed PR docs preview at: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/pr/${{ github.event.pull_request.number }}/"

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,5 @@ Python API for [Firefly](http://github.com/Caltech-IPAC/firefly), IPAC's Advance
77

88
You can find the documentation at https://caltech-ipac.github.io/firefly_client.
99

10-
NOTE: Many parts of this documentation are a work in progress because the firefly_client API and its use cases evolved since it was written.
1110
Please report any issues or suggestions on the [GitHub issues](https://github.com/Caltech-IPAC/firefly_client/issues).
1211

docs/_ext/script_pages.py

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
"""Sphinx extension: generate per-script RST pages, copy .py into built _sources, and rewrite source links."""
2+
import os
3+
import glob
4+
import io
5+
from sphinx.util import logging
6+
7+
logger = logging.getLogger(__name__)
8+
9+
MARKER_TEMPLATE = '.. AUTO-GENERATED from {}'
10+
11+
12+
def _title_from_script(path):
13+
"""Extract a title from the first commented heading line; fallback to filename."""
14+
title = os.path.splitext(os.path.basename(path))[0]
15+
try:
16+
with io.open(path, 'r', encoding='utf-8') as fh:
17+
first_line = fh.readline()
18+
# Skip shebang (#!) if present
19+
if first_line and first_line.lstrip().startswith('#!'):
20+
first_line = fh.readline()
21+
# Look for first comment line
22+
title_comment = None
23+
if first_line and first_line.lstrip().startswith('#'):
24+
title_comment = first_line.strip().lstrip('#').strip()
25+
if title_comment:
26+
title = title_comment
27+
except Exception:
28+
pass
29+
return title
30+
31+
32+
def _generate_rst_for_scripts(app):
33+
"""Create a <name>.rst next to each <name>.py under docs/usage/examples/.
34+
35+
- If `<name>.rst` already exists and does NOT start with our auto-generated marker,
36+
we leave it alone (so user-written RST isn't overwritten).
37+
- Otherwise we create or overwrite the RST to include the script via literalinclude.
38+
"""
39+
logger = logging.getLogger(__name__)
40+
srcdir = app.srcdir
41+
42+
patterns = [
43+
os.path.join(srcdir, 'usage', 'examples', '*.py'),
44+
]
45+
py_files = []
46+
for pat in patterns:
47+
py_files.extend(glob.glob(pat))
48+
py_files = sorted(set(py_files))
49+
50+
generated = 0
51+
52+
for script_fpath in py_files:
53+
script_basename = os.path.basename(script_fpath)
54+
# Skip conf.py and other build files
55+
if script_basename in ('conf.py',):
56+
continue
57+
58+
rst_path = os.path.splitext(script_fpath)[0] + '.rst'
59+
marker = MARKER_TEMPLATE.format(script_basename)
60+
61+
# If rst exists and not generated by us, skip it
62+
if os.path.exists(rst_path):
63+
try:
64+
with io.open(rst_path, 'r', encoding='utf-8') as rh:
65+
first = rh.readline().strip()
66+
if first != marker:
67+
logger.info(f'Skipping existing RST: {rst_path}')
68+
continue
69+
except Exception:
70+
# If we can't read, skip to be safe
71+
logger.warning(f'Could not read existing RST {rst_path}; skipping')
72+
continue
73+
74+
# Build content
75+
title = _title_from_script(script_fpath)
76+
underline = '=' * len(title)
77+
# Make the literalinclude path relative to the generated RST location
78+
rst_dir = os.path.dirname(rst_path)
79+
rel = os.path.relpath(script_fpath, rst_dir)
80+
lines = [
81+
marker,
82+
'',
83+
title,
84+
underline,
85+
'',
86+
f'.. literalinclude:: {rel}',
87+
' :language: python',
88+
' :linenos:',
89+
'',
90+
]
91+
92+
try:
93+
with io.open(rst_path, 'w', encoding='utf-8') as oh:
94+
oh.write('\n'.join(lines))
95+
generated += 1
96+
logger.info(f'Generated {rst_path} from {script_basename}')
97+
except Exception as e:
98+
logger.warning(f'Failed to write {rst_path}: {e}')
99+
100+
logger.info(f'Generated {generated} script RST files')
101+
102+
103+
def _remove_generated_rst(app, exception):
104+
"""Remove RST files that were auto-generated for scripts and copy .py into built _sources.
105+
106+
Safety rules:
107+
- Only remove files whose first line starts with the auto-generated marker
108+
('.. AUTO-GENERATED from ...'). This avoids deleting user-authored RST.
109+
- Only remove files after a successful build (exception is None).
110+
"""
111+
logger = logging.getLogger(__name__)
112+
113+
# If build failed, do not remove files so devs can inspect outputs
114+
if exception is not None:
115+
logger.info('Build failed; leaving auto-generated RST files in place')
116+
return
117+
118+
srcdir = app.srcdir
119+
patterns = [
120+
os.path.join(srcdir, 'usage', 'examples', '*.rst'),
121+
]
122+
123+
rst_files = []
124+
for pat in patterns:
125+
rst_files.extend(glob.glob(pat))
126+
127+
removed = 0
128+
for rst in sorted(set(rst_files)):
129+
try:
130+
with io.open(rst, 'r', encoding='utf-8') as fh:
131+
first = fh.readline().strip()
132+
if first.startswith(MARKER_TEMPLATE.format('')):
133+
# If building HTML, copy the original .py into the built _sources
134+
# directory so the "View source" link can serve the real Python file.
135+
try:
136+
py_full = os.path.splitext(rst)[0] + '.py'
137+
if os.path.exists(py_full) and getattr(app, 'builder', None) is not None:
138+
try:
139+
builder_name = getattr(app.builder, 'name', None)
140+
outdir = getattr(app.builder, 'outdir', None)
141+
except Exception:
142+
builder_name = None
143+
outdir = None
144+
145+
if builder_name == 'html' and outdir:
146+
dest_dir = os.path.join(outdir, '_sources')
147+
rel_py = os.path.relpath(py_full, app.srcdir)
148+
dest_path = os.path.join(dest_dir, rel_py)
149+
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
150+
# Copy file contents
151+
try:
152+
with io.open(py_full, 'r', encoding='utf-8') as rf, io.open(dest_path, 'w', encoding='utf-8') as wf:
153+
wf.write(rf.read())
154+
logger.info(f'Copied Python source to built _sources: {dest_path}')
155+
except Exception as e:
156+
logger.warning(f'Failed to write built source {dest_path}: {e}')
157+
158+
except Exception:
159+
# non-fatal; continue to removal attempt
160+
pass
161+
162+
try:
163+
os.remove(rst)
164+
removed += 1
165+
logger.info(f'Removed generated RST: {rst}')
166+
except Exception as e:
167+
logger.warning(f'Failed to remove generated RST {rst}: {e}')
168+
except Exception as e:
169+
logger.warning(f'Could not read {rst}; skipping removal: {e}')
170+
171+
logger.info(f'Removed {removed} auto-generated script RST files')
172+
173+
174+
def _rewrite_sourcelink_to_py(app, pagename, templatename, context, doctree):
175+
"""If the current page was generated from a script RST, point "View source" to the .py.
176+
177+
This mirrors nbsphinx behaviour for notebooks where the "Show source" link
178+
points to the original notebook instead of the generated RST.
179+
"""
180+
try:
181+
# doc2path with base=None returns a path relative to srcdir
182+
rst_rel = app.env.doc2path(pagename, base=None)
183+
except Exception:
184+
return
185+
186+
rst_full = os.path.join(app.srcdir, rst_rel)
187+
if not os.path.exists(rst_full):
188+
return
189+
190+
try:
191+
with io.open(rst_full, 'r', encoding='utf-8') as fh:
192+
first = fh.readline().strip()
193+
except Exception:
194+
return
195+
196+
# Only rewrite for our auto-generated script RST files
197+
if not first.startswith(MARKER_TEMPLATE.format('')):
198+
return
199+
200+
py_full = os.path.splitext(rst_full)[0] + '.py'
201+
if not os.path.exists(py_full):
202+
return
203+
204+
# Make the path relative to the source directory (what Sphinx expects)
205+
rel_py = os.path.relpath(py_full, app.srcdir)
206+
207+
# Replace the template context variable so the HTML theme will link to .py
208+
context['sourcename'] = rel_py
209+
210+
211+
def setup(app):
212+
# Generate per-script RST files before reading sources
213+
app.connect('builder-inited', _generate_rst_for_scripts)
214+
# Remove the generated per-script RST files after a successful build
215+
app.connect('build-finished', _remove_generated_rst)
216+
# Make "View source" point to the original .py for auto-generated script pages
217+
app.connect('html-page-context', _rewrite_sourcelink_to_py)
218+
219+
return {
220+
'version': '1.0',
221+
'parallel_read_safe': True,
222+
'parallel_write_safe': True,
223+
}
202 KB
Loading

docs/conf.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,32 @@
66
# -- Project information -----------------------------------------------------
77
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
88

9+
# Make local extension modules under _ext/ importable
10+
import os
11+
import sys
12+
sys.path.insert(0, os.path.abspath('_ext'))
13+
914
project = 'firefly_client'
10-
copyright = '2024, Caltech/IPAC Firefly Developers'
15+
copyright = '2026, Caltech/IPAC Firefly Developers'
1116
author = 'Caltech/IPAC Firefly Developers'
1217

1318
# -- General configuration ---------------------------------------------------
1419
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
1520

1621
extensions = [
1722
'sphinx_automodapi.automodapi',
18-
'myst_parser'
23+
'myst_parser',
24+
'nbsphinx',
25+
'script_pages', # custom extension for auto-generating RST files for examples/*.py scripts
1926
]
2027

2128
templates_path = ['_templates']
22-
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
29+
exclude_patterns = [
30+
'_build', 'Thumbs.db', '.DS_Store',
31+
# exclude any docs in subdirectories under usage/examples/ since they are
32+
# not included in any toctree and are for internal use only
33+
'usage/examples/*/**'
34+
]
2335

2436

2537
# -- Options for HTML output -------------------------------------------------
@@ -52,3 +64,16 @@
5264

5365
# -- Options for extensions -------------------------------------------------
5466
myst_heading_anchors = 3
67+
68+
# nbsphinx configuration: render notebooks and Python scripts
69+
# Do not execute notebooks during docs build; use stored outputs if present.
70+
nbsphinx_execute = 'never'
71+
72+
# Allow build to continue even if some notebooks error (useful for demos).
73+
nbsphinx_allow_errors = True
74+
75+
# Ensure Pygments syntax highlighting for Jupyter code cells.
76+
nbsphinx_codecell_lexer = 'ipython'
77+
78+
# Remove the .txt suffix that gets added to source files
79+
html_sourcelink_suffix = ''

docs/development/guide.rst

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,37 @@ Local development
44

55
Get latest source from ``master`` branch at https://github.com/Caltech-IPAC/firefly_client.
66

7+
.. code-block:: shell
8+
9+
git clone https://github.com/Caltech-IPAC/firefly_client.git
10+
cd firefly_client
11+
712
813
Environment Setup
914
-----------------
1015

11-
TBD
16+
Create a Python virtual environment and install required dependencies.
17+
The folllowing commands demonstrate how to do this using ``conda`` (assuming you have `miniconda <https://www.anaconda.com/docs/getting-started/miniconda/>`_ installed on your system):
18+
19+
.. code-block:: shell
20+
21+
conda create -n ffpy -c conda-forge python jupyter astropy # jupyter and astropy are needed for running examples
22+
conda activate ffpy
23+
pip install -e ".[docs]" # editable installation with docs dependencies
1224
1325
26+
Now you can run the examples notebooks/scripts in the ``examples/`` directory.
27+
This can be done by starting a Jupyter Notebook or JupyterLab session from the terminal (``jupyter notebook`` or ``jupyter lab``), or by using an IDE that supports running Python notebooks or scripts (like VSCode, IntelliJ, etc.).
28+
29+
.. note::
30+
The changes you make to the source code will be reflected when you run the examples since the package is installed in editable mode.
31+
But make sure to restart the active Python kernel/session to pick up the changes.
32+
1433
Building documentation
1534
----------------------
1635

17-
Make sure you have the virtual/conda environment activated and documentation
18-
dependencies installed in that environment.
36+
Make sure you have the virtual environment activated and documentation
37+
dependencies installed in that environment (``[docs]``).
1938

2039
Then do:
2140

@@ -28,4 +47,13 @@ Open ``docs/_build/html/index.html`` in your browser to see the documentation
2847
website.
2948

3049
Each time you make a change in documentation source, build it using
31-
above command and reload the above file in browser.
50+
above command and reload the above html file in browser.
51+
52+
.. note::
53+
The Sphinx docs include rendered Jupyter notebooks (via ``nbsphinx``), which can require **pandoc**.
54+
If you see an error like ``PandocMissing``, install pandoc first (e.g., ``brew install pandoc`` on macOS).
55+
56+
Development Tests/Examples
57+
--------------------------
58+
59+
Refer to the `examples/development_tests directory <https://github.com/Caltech-IPAC/firefly_client/tree/master/examples/development_tests>`_ of firefly-client GitHub repository.

0 commit comments

Comments
 (0)