diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..c5fcd9f --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,46 @@ +name: Docs + +on: + pull_request: + push: + branches: [main] + tags: + - '*' + +jobs: + build-deploy-docs: + name: Docs + runs-on: ubuntu-latest + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.11.0 + with: + access_token: ${{ github.token }} + + - uses: actions/checkout@v3 + with: + submodules: true + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: "pip" + + + - name: Install deps + run: | + pip install --upgrade pip wheel + pip install -r requirements.txt + + - name: Build Docs + run: | + cd docs + make + + - name: Trigger docs site rebuild + if: github.ref == 'refs/heads/main' + run: | + curl -X POST https://api.github.com/repos/tskit-dev/tskit-site/dispatches \ + -H 'Accept: application/vnd.github.everest-preview+json' \ + -u AdminBot-tskit:${{ secrets.ADMINBOT_TOKEN }} \ + --data '{"event_type":"build-docs"}' \ No newline at end of file diff --git a/.gitignore b/.gitignore index b6e4761..0e5278e 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,7 @@ instance/ # Sphinx documentation docs/_build/ +docs/_autoimages/ # PyBuilder target/ diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 120000 index 0000000..04c99a5 --- /dev/null +++ b/docs/CHANGELOG.md @@ -0,0 +1 @@ +../CHANGELOG.md \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..6192f64 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,16 @@ + +# Need to set PYTHONPATH so that we pick up the local tsbrowse +PYPATH=${PWD}/.. +TSBROWSE_VERSION:=$(shell PYTHONPATH=${PYPATH} \ + python3 -c 'import tsbrowse; print(tsbrowse.__version__.split("+")[0])') + +dev: + PYTHONPATH=${PYPATH} ./build.sh + +dist: + @echo Building distribution for tsbrowse version ${TSBROWSE_VERSION} + sed -i s/__TSBROWSE_VERSION__/${TSBROWSE_VERSION}/g _config.yml + PYTHONPATH=${PYPATH} ./build.sh + +clean: + rm -fR _build diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..2dc588c --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,61 @@ +# Book settings +# Learn more at https://jupyterbook.org/customize/config.html + +title: Tsbrowse manual +author: Tsbrowse Developers +copyright: "2024" +only_build_toc_files: true + +execute: + execute_notebooks: cache + +launch_buttons: + binderhub_url: "" + +repository: + url: https://github.com/tskit-dev/tsbrowse + branch: main + path_to_book: docs + +html: + use_issues_button: true + use_repository_button: true + use_edit_page_button: true + +sphinx: + extra_extensions: + - sphinx_copybutton + - sphinx_design + - sphinx.ext.autodoc + - sphinx.ext.autosummary + - sphinx.ext.doctest + - sphinx.ext.viewcode + - sphinx.ext.intersphinx + - sphinx_issues + local_extensions: + tsbrowse_image_extension: . + + config: + html_theme: sphinx_book_theme + myst_enable_extensions: + - colon_fence + - deflist + - dollarmath + issues_github_path: tskit-dev/tsbrowse + intersphinx_mapping: + python: ["https://docs.python.org/3/", null] + matplotlib: ["https://matplotlib.org/stable", null] + numpy: ["https://numpy.org/doc/stable/", null] + pandas: ["https://pandas.pydata.org/pandas-docs/stable", null] + pyslim: ["https://tskit.dev/pyslim/docs/latest/", null] + tskit: ["https://tskit.dev/tskit/docs/stable/", null] + tskit-tutorials: ["https://tskit.dev/tutorials/", null] + msprime: ["https://tskit.dev/msprime/docs/stable/", null] + + autodoc_member_order: bysource + + suppress_warnings: + - etoc.toctree + - ref + + diff --git a/docs/_toc.yml b/docs/_toc.yml new file mode 100644 index 0000000..aef5f2d --- /dev/null +++ b/docs/_toc.yml @@ -0,0 +1,19 @@ +format: jb-book +root: index +parts: +- caption: Getting started + chapters: + - file: intro + - file: install + +- caption: Pages + chapters: + - file: overview + - file: edges + +- caption: Extras + chapters: + - file: contributing + - file: CHANGELOG + - url: https://tskit.dev/community/ + title: Community \ No newline at end of file diff --git a/docs/build.sh b/docs/build.sh new file mode 100755 index 0000000..2417d47 --- /dev/null +++ b/docs/build.sh @@ -0,0 +1,20 @@ +#/bin/bash + +# Jupyter-build doesn't have an option to automatically show the +# saved reports, which makes it difficult to debug the reasons for +# build failures in CI. This is a simple wrapper to handle that. + +REPORTDIR=_build/html/reports + +jupyter-book build -nW --keep-going . +RETVAL=$? +if [ $RETVAL -ne 0 ]; then + if [ -e $REPORTDIR ]; then + echo "Error occured; showing saved reports" + cat $REPORTDIR/* + fi +else + # Clear out any old reports + rm -f $REPORTDIR/* +fi +exit $RETVAL diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..85f112a --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,11 @@ +(contributing)= + +# Contributing to tsbrowse + +All contributions, bug reports, documentation improvements and ideas are welcome. If you think +there is anything missing, please open an [issue](https://github.com/tskit-dev/tsbrowse/issues) +or [pull request](https://github.com/tskit-dev/tsbrowse/pulls) on Github. + +## Quick start + +TODO! Add information for developers here. diff --git a/docs/edges.md b/docs/edges.md new file mode 100644 index 0000000..77434f4 --- /dev/null +++ b/docs/edges.md @@ -0,0 +1,7 @@ +(sec_edges)= + +# Edges page + +TODO! Explain page + +![Edges Page](tsbrowse:example.tsbrowse:edges) \ No newline at end of file diff --git a/docs/example.tsbrowse b/docs/example.tsbrowse new file mode 100644 index 0000000..86caf8f Binary files /dev/null and b/docs/example.tsbrowse differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..4ecd52a --- /dev/null +++ b/docs/index.md @@ -0,0 +1,7 @@ +(sec_tsbrowse_docs_mainpage)= + +# Tsbrowse documentation + +tsbrowse is an open-source Python package for visualising ARGs in the [tskit](https://tskit.dev/tskit/docs/) tree sequence format. + +TODO! Add links to other pages diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000..90de1b8 --- /dev/null +++ b/docs/install.md @@ -0,0 +1,5 @@ +(install)= + +# Installing tsbrowse + +TODO! Add information on install and deps, then link to intro diff --git a/docs/intro.md b/docs/intro.md new file mode 100644 index 0000000..ee39c06 --- /dev/null +++ b/docs/intro.md @@ -0,0 +1,5 @@ +(sec_intro)= + +# Introduction to tsbrowse + +TODO! Add a quick start guide with example tree sequences diff --git a/docs/license.md b/docs/license.md new file mode 100644 index 0000000..233aa52 --- /dev/null +++ b/docs/license.md @@ -0,0 +1,8 @@ +(license)= + +# License + +```{eval-rst} +.. include:: ../../../LICENSE + :literal: +``` diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 0000000..47bc96c --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,7 @@ +(sec_overview)= + +# Overview page + +TODO! Explain page + +![Overview Page](tsbrowse:example.tsbrowse:overview) diff --git a/docs/tsbrowse_image_extension.py b/docs/tsbrowse_image_extension.py new file mode 100644 index 0000000..ec882f4 --- /dev/null +++ b/docs/tsbrowse_image_extension.py @@ -0,0 +1,99 @@ +import re +import time +from pathlib import Path + +import panel as pn +from playwright.sync_api import sync_playwright +from sphinx.util import logging + +from tsbrowse import app +from tsbrowse import model + +logger = logging.getLogger(__name__) + + +class TSBrowsePreprocessor: + def __init__(self, app): + self.app = app + self.src_dir = Path(app.srcdir) + self.image_dir = self.src_dir / "_autoimages" + self.image_dir.mkdir(parents=True, exist_ok=True) + self.processed_images = set() + + def extract_image_specs(self, content): + pattern = r"!\[(.*?)\]\(tsbrowse:(.*?):(.*?)\)" + return re.findall(pattern, content) + + def generate_image(self, ts_file, page_name): + output_filename = f"{Path(ts_file).stem}_{page_name}.png" + output_path = self.image_dir / output_filename + + if output_path in self.processed_images: + return output_filename + + logger.info(f"Generating screenshot for {ts_file}:{page_name}") + + tsm = model.TSModel(ts_file) + app_ = app.App(tsm) + url = "http://localhost:11337" + server = pn.serve(app_.view, port=11337, threaded=True, show=False) + try: + # Wait for server to come up + time.sleep(2) + with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page() + page.set_viewport_size({"width": 1920, "height": 1080}) + page.goto(url) + page.get_by_role("button", name=page_name.title()).click() + # Wait for page to load, would be better to have an element to wait for + # But hard to do generically + time.sleep(4) + logger.info(f"Screenshotting to {output_path}") + page.screenshot(path=str(output_path)) + browser.close() + except Exception as e: + logger.error(f"Failed to generate screenshot: {e}") + return None + finally: + server.stop() + + self.processed_images.add(output_path) + return output_filename + + def process_content(self, content, docname): + image_specs = self.extract_image_specs(content) + if not image_specs: + return content + + modified_content = content + for alt_text, ts_file, page_name in image_specs: + image_filename = self.generate_image(ts_file, page_name) + if image_filename: + old_ref = f"![{alt_text}](tsbrowse:{ts_file}:{page_name})" + new_ref = f"![{alt_text}](/_autoimages/{image_filename})" + modified_content = modified_content.replace(old_ref, new_ref) + + return modified_content + + +def setup(app): + logger.info("Setting up TSBrowse preprocessor") + preprocessor = TSBrowsePreprocessor(app) + + def process_source(app, docname, source): + content = source[0] + if "tsbrowse:" not in content: + return + logger.info(f"Found tsbrowse references in {docname}") + modified_content = preprocessor.process_content(content, docname) + if modified_content != content: + source[0] = modified_content + + app.connect("source-read", process_source) + + return { + "version": "0.1", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/requirements.txt b/requirements.txt index 6e71498..12ac41c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ coveralls daiquiri datashader hvplot +jupyter-book matplotlib msprime panel @@ -12,5 +13,6 @@ pre-commit pytest pytest-playwright selenium +sphinx_issues tskit tszip diff --git a/tests/test_ui.py b/tests/test_ui.py index fc0254e..e260569 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -27,7 +27,7 @@ def test_component(page, port, tmpdir, save_screenshots): component = app.App(tsm) url = f"http://localhost:{port}" - server = pn.serve(component.view(), port=port, threaded=True, show=False) + server = pn.serve(component.view, port=port, threaded=True, show=False) time.sleep(2) page.goto(url)