From 88c70c91ea6c615131dcb42c588d6ec1db1bd93c Mon Sep 17 00:00:00 2001 From: Ryan Galloway Date: Wed, 19 Aug 2026 06:42:25 -0700 Subject: [PATCH 1/4] Add hybrid mkpages and Sphinx docs build for Pages and objects.inv --- .github/workflows/publish-docs.yml | 49 +++- .gitignore | 3 +- docs/README.md | 3 +- docs/examples.md | 2 + docs/index.md | 5 +- docs/mkpages.yml | 17 ++ lib/pyseq/seq.py | 3 +- pyproject.toml | 16 +- scripts/build_pages_site.py | 391 ----------------------------- scripts/stage_mkpages_site.py | 120 +++++++++ sphinx/_static/custom.css | 243 ++++++++++++++++++ sphinx/api/index.rst | 12 + sphinx/conf.py | 45 ++++ sphinx/index.rst | 11 + 14 files changed, 515 insertions(+), 405 deletions(-) create mode 100644 docs/mkpages.yml delete mode 100644 scripts/build_pages_site.py create mode 100644 scripts/stage_mkpages_site.py create mode 100644 sphinx/_static/custom.css create mode 100644 sphinx/api/index.rst create mode 100644 sphinx/conf.py create mode 100644 sphinx/index.rst diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index aeaf1f0..38bbcaf 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -3,8 +3,7 @@ name: publish-docs on: push: branches: - - master - - main + - "**" workflow_dispatch: permissions: @@ -32,34 +31,68 @@ jobs: with: python-version: "3.11" - - name: Install package + - name: Install package and docs tooling run: | python -m pip install --upgrade pip setuptools wheel - python -m pip install -e ".[dev]" + python -m pip install mkpages + python -m pip install -e ".[dev,docs]" - name: Generate benchmark report run: | python scripts/benchmark.py --json benchmark.json > benchmark.md - - name: Build Jekyll source tree + - name: Stage mkpages content tree run: | - python scripts/build_pages_site.py \ - --output _site_src \ + python scripts/stage_mkpages_site.py \ + --output .mkpages-src \ --benchmark-summary benchmark.md \ --benchmark-json benchmark.json + - name: Build mkpages source tree + run: | + mkpages build .mkpages-src --output .mkpages + + - name: Build Sphinx API docs + run: | + sphinx-build -M dirhtml sphinx .sphinx + + - name: Merge Sphinx output into mkpages tree + run: | + cp -R .sphinx/dirhtml/api .mkpages/api + cp -R .sphinx/dirhtml/_static .mkpages/_static + cp -R .sphinx/dirhtml/genindex .mkpages/genindex + cp -R .sphinx/dirhtml/py-modindex .mkpages/py-modindex + cp -R .sphinx/dirhtml/search .mkpages/search + cp .sphinx/dirhtml/objects.inv .mkpages/objects.inv + cp .sphinx/dirhtml/searchindex.js .mkpages/searchindex.js + + - name: Allow Sphinx static assets through Jekyll + run: | + printf '\ninclude:\n - _static\n' >> .mkpages/_config.yml + - name: Build site with Jekyll uses: actions/jekyll-build-pages@v1 with: - source: _site_src + source: .mkpages destination: _site + - name: Verify published inventory + run: test -f _site/objects.inv + + - name: Upload downloadable site artifact + uses: actions/upload-artifact@v4 + with: + name: docs-site-${{ github.ref_name }} + path: _site + - name: Upload Pages artifact + if: github.ref_name == 'main' || github.ref_name == 'master' uses: actions/upload-pages-artifact@v3 with: path: _site deploy: + if: github.ref_name == 'main' || github.ref_name == 'master' environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} diff --git a/.gitignore b/.gitignore index e2de1e8..80d1e76 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ pyseq.egg-info/ default.env .codex .agents/ -.pytest_cache/ \ No newline at end of file +.pytest_cache/ +.mkpages/ \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index ed6287a..6e6fcc2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,4 +14,5 @@ Available guides: If you are evaluating pyseq for pipeline use, start with the examples guide and then review the CLI reference for the sequence-aware utilities included with -the package. +the package. For generated Python API details, see the [Sphinx API +reference](/api/). diff --git a/docs/examples.md b/docs/examples.md index 98fd80a..d199ac5 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -3,6 +3,8 @@ This guide expands on the examples in the main [README](../README.md) and shows a few common ways pyseq is used in production scripts and tools. +For the generated Python API reference, see [Sphinx API reference](/api/). + ## Parse a List of Frames into a Sequence ```python diff --git a/docs/index.md b/docs/index.md index 1c7157a..2ca551d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,5 @@

- pyseq logo + pyseq logo

pyseq is a **Python library for detecting, parsing, and formatting numbered @@ -127,7 +127,8 @@ Expected output: ## Learn More -- [PySeq Docs](README.md): docs overview +- [API Reference](/api/): generated Python module reference +- [PySeq Docs](/docs/): docs overview - [Examples](examples.md): Python and CLI usage patterns - [CLI Tools Reference](cli-tools.md): bundled sequence-aware utilities - [Formatting Reference](formatting.md): supported format directives diff --git a/docs/mkpages.yml b/docs/mkpages.yml new file mode 100644 index 0000000..2b239f1 --- /dev/null +++ b/docs/mkpages.yml @@ -0,0 +1,17 @@ +title: pyseq +description: Python library for numbered file sequences. +navigation: + - label: Home + href: / + - label: Usage + href: /docs/cli-tools/ + - label: Examples + href: /docs/examples/ + - label: Formatting + href: /docs/formatting/ + - label: Performance + href: /docs/performance/ + - label: API + href: /api/ + - label: GitHub + href: https://github.com/rsgalloway/pyseq diff --git a/lib/pyseq/seq.py b/lib/pyseq/seq.py index 3f4f23c..2f5223a 100755 --- a/lib/pyseq/seq.py +++ b/lib/pyseq/seq.py @@ -101,10 +101,10 @@ def padsize(item, frame): For example: the file item.001.exr will have a pad size of 3, and the file test.001001.exr will have a pad size of 6. - :param item: Item object. Signed frames use the digit width only; the leading ``-`` does not contribute to the padding width. + :param item: Item object. :param frame: The frame number token as a string. :returns: The size of the frame pad as an int. """ @@ -777,6 +777,7 @@ def append(self, item: Item, check_membership: bool = True): :param item: pyseq.Item object. :param check_membership: Check if `item` is a member. Can be useful if membership is checked prior to appending. + :exc:`SequenceError` raised if item is not a sequence member. """ diff --git a/pyproject.toml b/pyproject.toml index 8b57bd4..9520baf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,21 @@ classifiers = [ authors = [ { name = "Ryan Galloway", email = "ryan@rsgalloway.com" }, ] -optional-dependencies = { dev = ["pytest", "flake8==7.1.1", "mccabe==0.7.0", "isort==5.13.2", "black==24.8.0"], test = ["pytest"] } + +[project.optional-dependencies] +dev = [ + "pytest", + "flake8==7.1.1", + "mccabe==0.7.0", + "isort==5.13.2", + "black==24.8.0", +] +docs = [ + "Sphinx>=8,<9", +] +test = [ + "pytest", +] [tool.isort] profile = "black" diff --git a/scripts/build_pages_site.py b/scripts/build_pages_site.py deleted file mode 100644 index fa6c073..0000000 --- a/scripts/build_pages_site.py +++ /dev/null @@ -1,391 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2011-2025, Ryan Galloway (ryan@rsgalloway.com) -# - -"""Build a simple Jekyll-friendly docs site from repository markdown files.""" - -import argparse -import re -import shutil -from pathlib import Path -from typing import Optional - -LINK_PATTERNS = ( - (r"\(README\.md\)", "(index.html)"), - (r"\(docs/README\.md\)", "(docs/index.html)"), - (r"\(docs/([^)]+)\.md\)", r"(docs/\1.html)"), - (r"\(([^:)#]+)\.md\)", r"(\1.html)"), -) - - -def rewrite_links(content: str) -> str: - """Rewrite local markdown links for generated HTML output.""" - updated = content - for pattern, replacement in LINK_PATTERNS: - updated = re.sub(pattern, replacement, updated) - return updated - - -def extract_title(content: str, fallback: str) -> str: - """Extract the first markdown H1 title or use a fallback.""" - for line in content.splitlines(): - if line.startswith("# "): - return line[2:].strip() - return fallback - - -def wrap_markdown(content: str, title: str) -> str: - """Add minimal Jekyll front matter to markdown content.""" - return f"---\nlayout: default\ntitle: {title}\n---\n\n{content}" - - -def write_markdown_page(src: Path, dst: Path, fallback_title: str): - """Copy a markdown file into the site tree with front matter and fixed links.""" - content = src.read_text(encoding="utf-8") - title = extract_title(content, fallback_title) - content = rewrite_links(content) - dst.parent.mkdir(parents=True, exist_ok=True) - dst.write_text(wrap_markdown(content, title), encoding="utf-8") - - -def write_site_config(output_dir: Path): - """Write a minimal Jekyll config file.""" - config = """title: pyseq -description: Python library for numbered file sequences -markdown: kramdown -permalink: pretty -""" - (output_dir / "_config.yml").write_text(config, encoding="utf-8") - - -def write_layout(output_dir: Path): - """Write the shared Jekyll layout used by the generated docs site.""" - layout_dir = output_dir / "_layouts" - layout_dir.mkdir(parents=True, exist_ok=True) - template = """ - - - - - {% if page.title %}{{ page.title }} | {% endif %}{{ site.title }} - - - - -
- -
- {{ content }} -
-
- - -""" - (layout_dir / "default.html").write_text(template, encoding="utf-8") - - -def write_stylesheet(output_dir: Path): - """Write a minimal light stylesheet for the generated docs site.""" - assets_dir = output_dir / "assets" - assets_dir.mkdir(parents=True, exist_ok=True) - css = """:root { - --bg: #ffffff; - --panel: #ffffff; - --border: #d9e2ec; - --text: #102033; - --muted: #516172; - --accent: #0fba74; - --accent-dark: #0b7f55; - --code: #f4f8fb; -} - -* { box-sizing: border-box; } - -html, body { - margin: 0; - padding: 0; - background: var(--bg); - color: var(--text); - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - line-height: 1.7; -} - -a { - color: var(--accent-dark); - text-decoration: none; -} - -a:hover { - color: var(--accent); -} - -.site-shell { - max-width: 1040px; - margin: 0 auto; - padding: 24px 24px 72px; -} - -.site-header { - display: flex; - flex-wrap: wrap; - gap: 16px 24px; - align-items: center; - justify-content: space-between; - margin-bottom: 36px; -} - -.site-brand { - color: var(--text); - font-size: 0.98rem; - font-weight: 700; - letter-spacing: 0.01em; -} - -.site-nav { - display: flex; - flex-wrap: wrap; - gap: 16px; -} - -.site-nav a { - color: var(--muted); - font-size: 0.95rem; -} - -.site-nav a:hover { - color: var(--text); -} - -.site-main { - background: transparent; -} - -.site-main h1:first-child, -.site-main p:first-child img { - margin-top: 0; -} - -h1, h2, h3 { - color: var(--text); - line-height: 1.15; -} - -h1 { - font-size: 2.7rem; - margin: 0 0 1rem; -} - -h2 { - font-size: 1.5rem; - margin-top: 2.5rem; -} - -h3 { - font-size: 1.08rem; - margin-top: 1.5rem; -} - -p, li { - font-size: 1.02rem; -} - -code, pre { - font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; -} - -code { - background: var(--code); - border: 1px solid #e4ebf2; - border-radius: 8px; - padding: 0.12rem 0.4rem; -} - -pre { - background: var(--code); - border: 1px solid var(--border); - border-radius: 16px; - overflow-x: auto; - padding: 18px 20px; -} - -pre code { - background: transparent; - border: 0; - padding: 0; -} - -img { - max-width: 100%; - height: auto; -} - -blockquote { - border-left: 4px solid #b8c6d6; - color: var(--muted); - margin: 1.5rem 0; - padding-left: 1rem; -} - -table { - border-collapse: collapse; - width: 100%; -} - -th, td { - border: 1px solid var(--border); - padding: 0.7rem 0.8rem; - text-align: left; -} - -th { - background: #f2f6fa; -} - -@media (max-width: 720px) { - .site-shell { - padding: 18px 16px 56px; - } - - .site-header { - align-items: flex-start; - margin-bottom: 28px; - } - - .site-nav { - gap: 12px; - } - - h1 { - font-size: 2.15rem; - } -} -""" - (assets_dir / "site.css").write_text(css, encoding="utf-8") - - -def append_benchmark_section( - content: str, - benchmark_summary: Optional[Path] = None, - benchmark_json: Optional[Path] = None, -) -> str: - """Append the latest benchmark summary to the performance document.""" - sections = [content.rstrip(), "", "## Latest Benchmarks", ""] - - if benchmark_summary and benchmark_summary.exists(): - sections.extend( - [ - "This section is generated automatically by the docs publishing workflow,", - "which runs `scripts/benchmark.py` on the current `master` branch before", - "building the Pages site.", - "", - benchmark_summary.read_text(encoding="utf-8").strip(), - "", - ] - ) - else: - sections.extend( - [ - "This section is generated automatically by the docs publishing workflow", - "when benchmark artifacts are available.", - "", - ] - ) - - if benchmark_json and benchmark_json.exists(): - sections.extend(["### Downloads", "", "- [Benchmark JSON](../assets/benchmark.json)", ""]) - - return "\n".join(sections).rstrip() + "\n" - - -def copy_docs_assets(docs_dir: Path, output_dir: Path): - """Copy docs assets to both root assets/ and docs/assets/ for relative links.""" - assets_src = docs_dir / "assets" - if not assets_src.exists(): - return - - root_assets = output_dir / "assets" - docs_assets = output_dir / "docs" / "assets" - root_assets.mkdir(parents=True, exist_ok=True) - docs_assets.mkdir(parents=True, exist_ok=True) - - for src in assets_src.iterdir(): - if src.is_file(): - shutil.copy2(src, root_assets / src.name) - shutil.copy2(src, docs_assets / src.name) - - -def build_site(args): - repo_root = Path(args.repo_root).resolve() - output_dir = Path(args.output).resolve() - - if output_dir.exists(): - shutil.rmtree(output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - - write_site_config(output_dir) - write_layout(output_dir) - write_stylesheet(output_dir) - - # Home page from docs/index.md. - write_markdown_page(repo_root / "docs" / "index.md", output_dir / "index.md", "pyseq") - - benchmark_summary = Path(args.benchmark_summary) if args.benchmark_summary else None - benchmark_json = Path(args.benchmark_json) if args.benchmark_json else None - - # Docs pages. - docs_dir = repo_root / "docs" - for src in docs_dir.glob("*.md"): - if src.name == "index.md": - continue - if src.name == "README.md": - dst = output_dir / "docs" / "index.md" - fallback = "Docs" - else: - dst = output_dir / "docs" / src.name - fallback = src.stem.replace("-", " ").title() - if src.name == "performance.md": - content = src.read_text(encoding="utf-8") - content = append_benchmark_section(content, benchmark_summary, benchmark_json) - content = rewrite_links(content) - title = extract_title(content, fallback) - dst.parent.mkdir(parents=True, exist_ok=True) - dst.write_text(wrap_markdown(content, title), encoding="utf-8") - else: - write_markdown_page(src, dst, fallback) - - copy_docs_assets(docs_dir, output_dir) - - if benchmark_json and benchmark_json.exists(): - shutil.copy2(benchmark_json, output_dir / "assets" / "benchmark.json") - - cname = repo_root / "CNAME" - if cname.exists(): - shutil.copy2(cname, output_dir / "CNAME") - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--repo-root", default=".") - parser.add_argument("--output", required=True) - parser.add_argument("--benchmark-summary") - parser.add_argument("--benchmark-json") - args = parser.parse_args() - build_site(args) - - -if __name__ == "__main__": - main() diff --git a/scripts/stage_mkpages_site.py b/scripts/stage_mkpages_site.py new file mode 100644 index 0000000..55ccc5f --- /dev/null +++ b/scripts/stage_mkpages_site.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2011-2026, Ryan Galloway (ryan@rsgalloway.com) +# + +"""Stage repository docs into the layout expected by the mkpages Pages build.""" + +from __future__ import annotations + +import argparse +import shutil +from pathlib import Path +from typing import Optional + + +def copy_text(src: Path, dst: Path) -> None: + """Copy one text file, creating parent directories as needed.""" + dst.parent.mkdir(parents=True, exist_ok=True) + dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8") + + +def copy_tree(src: Path, dst: Path) -> None: + """Copy one directory tree if it exists.""" + if not src.exists(): + return + shutil.copytree(src, dst, dirs_exist_ok=True) + + +def append_benchmark_section( + content: str, + benchmark_summary: Optional[Path] = None, + benchmark_json: Optional[Path] = None, +) -> str: + """Append the latest benchmark summary to the performance guide.""" + sections = [content.rstrip(), "", "## Latest Benchmarks", ""] + + if benchmark_summary and benchmark_summary.exists(): + sections.extend( + [ + "This section is generated automatically by the docs publishing workflow,", + "which runs `scripts/benchmark.py` on the current `master` branch before", + "building the Pages site.", + "", + benchmark_summary.read_text(encoding="utf-8").strip(), + "", + ] + ) + else: + sections.extend( + [ + "This section is generated automatically by the docs publishing workflow", + "when benchmark artifacts are available.", + "", + ] + ) + + if benchmark_json and benchmark_json.exists(): + sections.extend(["### Downloads", "", "- [Benchmark JSON](../assets/benchmark.json)", ""]) + + return "\n".join(sections).rstrip() + "\n" + + +def stage_site( + repo_root: Path, + output_dir: Path, + benchmark_summary: Optional[Path] = None, + benchmark_json: Optional[Path] = None, +) -> None: + """Arrange docs so mkpages can preserve the existing published URLs.""" + docs_dir = repo_root / "docs" + + if output_dir.exists(): + shutil.rmtree(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + copy_text(docs_dir / "mkpages.yml", output_dir / "mkpages.yml") + copy_text(repo_root / "README.md", output_dir / "README.md") + copy_text(docs_dir / "index.md", output_dir / "index.md") + copy_text(docs_dir / "README.md", output_dir / "docs" / "index.md") + + for src in docs_dir.glob("*.md"): + if src.name in {"index.md", "README.md"}: + continue + dst = output_dir / "docs" / src.name + if src.name == "performance.md": + content = append_benchmark_section( + src.read_text(encoding="utf-8"), + benchmark_summary=benchmark_summary, + benchmark_json=benchmark_json, + ) + dst.parent.mkdir(parents=True, exist_ok=True) + dst.write_text(content, encoding="utf-8") + continue + copy_text(src, dst) + + copy_tree(docs_dir / "assets", output_dir / "assets") + copy_tree(docs_dir / "assets", output_dir / "docs" / "assets") + + if benchmark_json and benchmark_json.exists(): + copy_text(benchmark_json, output_dir / "assets" / "benchmark.json") + copy_text(benchmark_json, output_dir / "docs" / "assets" / "benchmark.json") + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repo-root", default=".") + parser.add_argument("--output", required=True) + parser.add_argument("--benchmark-summary") + parser.add_argument("--benchmark-json") + args = parser.parse_args() + stage_site( + Path(args.repo_root).resolve(), + Path(args.output).resolve(), + benchmark_summary=Path(args.benchmark_summary).resolve() if args.benchmark_summary else None, + benchmark_json=Path(args.benchmark_json).resolve() if args.benchmark_json else None, + ) + + +if __name__ == "__main__": + main() diff --git a/sphinx/_static/custom.css b/sphinx/_static/custom.css new file mode 100644 index 0000000..0b205fa --- /dev/null +++ b/sphinx/_static/custom.css @@ -0,0 +1,243 @@ +:root { + --pyseq-bg: #f6f8fb; + --pyseq-surface: #ffffff; + --pyseq-surface-soft: #f1f5f9; + --pyseq-border: #d9e2ec; + --pyseq-text: #102033; + --pyseq-muted: #516172; + --pyseq-accent: #0fba74; + --pyseq-accent-dark: #0b7f55; + --pyseq-code: #edf3f8; +} + +html, +body { + background: linear-gradient(180deg, #eef4fa 0%, #f8fbfd 100%); + color: var(--pyseq-text); + font-family: "Avenir Next", "Segoe UI", sans-serif; +} + +body { + font-size: 17px; + line-height: 1.7; +} + +div.document { + max-width: 1180px; + margin: 0 auto; + padding: 32px 24px 72px; +} + +div.documentwrapper, +div.bodywrapper { + margin: 0; +} + +div.documentwrapper { + margin-left: 312px; +} + +div.body { + background: var(--pyseq-surface); + border: 1px solid var(--pyseq-border); + border-radius: 24px; + box-shadow: 0 18px 48px rgba(16, 32, 51, 0.08); + padding: 40px 48px 48px; +} + +div.sphinxsidebar { + width: 280px; +} + +div.sphinxsidebarwrapper { + background: rgba(255, 255, 255, 0.78); + border: 1px solid rgba(217, 226, 236, 0.9); + border-radius: 22px; + box-shadow: 0 12px 30px rgba(16, 32, 51, 0.06); + padding: 24px 22px; + position: sticky; + top: 24px; +} + +div.sphinxsidebar h1.logo { + border-bottom: 1px solid var(--pyseq-border); + font-size: 1.4rem; + margin: 0 0 1rem; + padding-bottom: 0.9rem; +} + +div.sphinxsidebar h1.logo a { + color: var(--pyseq-text); +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4, +div.sphinxsidebar p, +div.sphinxsidebar ul, +div.sphinxsidebar .caption { + color: var(--pyseq-muted); +} + +div.sphinxsidebar a { + color: var(--pyseq-accent-dark); +} + +div.sphinxsidebar a:hover, +div.body a:hover { + color: var(--pyseq-accent); +} + +div.body a, +div.related a, +div.footer a { + color: var(--pyseq-accent-dark); +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: var(--pyseq-text); + font-family: Georgia, "Times New Roman", serif; + letter-spacing: -0.015em; + line-height: 1.12; +} + +h1 { + font-size: 3rem; + margin-top: 0; +} + +h2 { + font-size: 1.9rem; + margin-top: 2.4rem; +} + +h3 { + font-size: 1.35rem; + margin-top: 1.7rem; +} + +p, +li, +dd, +dt, +dl, +blockquote { + color: var(--pyseq-text); +} + +blockquote { + border-left: 4px solid #b8c6d6; + margin: 1.4rem 0; + padding-left: 1rem; +} + +code, +tt, +pre { + font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; +} + +code.literal, +.rst-content code.literal, +tt.descname, +tt.descclassname { + background: var(--pyseq-code); + border: 1px solid #d9e5f0; + border-radius: 8px; + color: #16324a; + padding: 0.1rem 0.38rem; +} + +div.highlight, +pre { + background: #f7fafc; + border: 1px solid var(--pyseq-border); + border-radius: 18px; + overflow-x: auto; + padding: 18px 20px; +} + +table.docutils, +table.docutils td, +table.docutils th { + border-color: var(--pyseq-border); +} + +table.docutils th { + background: var(--pyseq-surface-soft); +} + +dl.py.class, +dl.py.function, +dl.py.method, +dl.py.exception, +dl.py.property, +dl.py.attribute { + background: #fbfdff; + border: 1px solid var(--pyseq-border); + border-radius: 18px; + margin: 1.25rem 0; + padding: 1rem 1.1rem; +} + +dt.sig, +dt.sig-object { + border-bottom: 1px solid #e7eef5; + margin-bottom: 0.8rem; + padding-bottom: 0.7rem; +} + +.sig-name, +.sig-prename, +.sig-param-name { + color: #16324a; +} + +.sig-return-typehint, +.sig-param .n, +.sig-param .p { + color: var(--pyseq-muted); +} + +a.headerlink { + color: #8aa0b7; +} + +a.headerlink:hover { + color: var(--pyseq-accent); +} + +div.footer { + color: var(--pyseq-muted); + margin-top: 1.6rem; + padding: 0 24px 32px; + text-align: center; +} + +@media (max-width: 980px) { + div.document { + padding: 18px 14px 48px; + } + + div.documentwrapper { + margin-left: 0; + } + + div.body { + padding: 28px 24px 30px; + } + + div.sphinxsidebar { + float: none; + margin: 0 0 18px; + width: auto; + } + + div.sphinxsidebarwrapper { + position: static; + } +} diff --git a/sphinx/api/index.rst b/sphinx/api/index.rst new file mode 100644 index 0000000..831f487 --- /dev/null +++ b/sphinx/api/index.rst @@ -0,0 +1,12 @@ +API Reference +============= + +The documented objects on this page are exported from ``pyseq`` and published +under ``/api/`` so the root ``/objects.inv`` can resolve stable API URLs. + +.. automodule:: pyseq + :members: + :imported-members: + :exclude-members: deque, glob, iglob + :undoc-members: + :show-inheritance: diff --git a/sphinx/conf.py b/sphinx/conf.py new file mode 100644 index 0000000..ac742b7 --- /dev/null +++ b/sphinx/conf.py @@ -0,0 +1,45 @@ +"""Minimal Sphinx configuration for pyseq API docs and intersphinx inventory.""" + +from __future__ import annotations + +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +LIB = ROOT / "lib" + +if str(LIB) not in sys.path: + sys.path.insert(0, str(LIB)) + +import pyseq + + +project = "pyseq" +author = "Ryan Galloway" +copyright = "2011-2026, Ryan Galloway" +version = pyseq.__version__ +release = pyseq.__version__ + +extensions = [ + "sphinx.ext.autodoc", +] + +templates_path = ["_templates"] +exclude_patterns = ["_build"] + +autodoc_member_order = "bysource" + +html_theme = "alabaster" +html_theme_options = { + "description": "Python library for numbered file sequences", + "fixed_sidebar": True, + "page_width": "1180px", + "sidebar_width": "280px", +} +html_title = f"pyseq {release} API" +html_show_sourcelink = False +html_copy_source = False +html_static_path = ["_static"] +html_css_files = ["custom.css"] + diff --git a/sphinx/index.rst b/sphinx/index.rst new file mode 100644 index 0000000..e9b899a --- /dev/null +++ b/sphinx/index.rst @@ -0,0 +1,11 @@ +pyseq API documentation +======================= + +This Sphinx tree exists to publish pyseq's Python API reference and +``objects.inv`` for Intersphinx consumers. + +.. toctree:: + :maxdepth: 2 + + api/index + From a3c170245b114ddc4070befcbe0a2f794c98652a Mon Sep 17 00:00:00 2001 From: Ryan Galloway Date: Wed, 19 Aug 2026 06:45:37 -0700 Subject: [PATCH 2/4] Linting fixes --- lib/pyseq/seq.py | 2 +- scripts/stage_mkpages_site.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/pyseq/seq.py b/lib/pyseq/seq.py index 2f5223a..c5919c9 100755 --- a/lib/pyseq/seq.py +++ b/lib/pyseq/seq.py @@ -777,7 +777,7 @@ def append(self, item: Item, check_membership: bool = True): :param item: pyseq.Item object. :param check_membership: Check if `item` is a member. Can be useful if membership is checked prior to appending. - + :exc:`SequenceError` raised if item is not a sequence member. """ diff --git a/scripts/stage_mkpages_site.py b/scripts/stage_mkpages_site.py index 55ccc5f..c5d9195 100644 --- a/scripts/stage_mkpages_site.py +++ b/scripts/stage_mkpages_site.py @@ -111,7 +111,9 @@ def main() -> None: stage_site( Path(args.repo_root).resolve(), Path(args.output).resolve(), - benchmark_summary=Path(args.benchmark_summary).resolve() if args.benchmark_summary else None, + benchmark_summary=( + Path(args.benchmark_summary).resolve() if args.benchmark_summary else None + ), benchmark_json=Path(args.benchmark_json).resolve() if args.benchmark_json else None, ) From 79b6d6dc7e371c9b24cd9fdaa6298410c289d5d3 Mon Sep 17 00:00:00 2001 From: Ryan Galloway Date: Wed, 19 Aug 2026 06:47:04 -0700 Subject: [PATCH 3/4] Sanitize github ref name for build artifacts --- .github/workflows/publish-docs.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 38bbcaf..32a62b4 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -79,10 +79,17 @@ jobs: - name: Verify published inventory run: test -f _site/objects.inv + - name: Prepare artifact name + id: artifact_name + shell: bash + run: | + safe_ref=$(printf '%s' "${GITHUB_REF_NAME}" | tr '/\\:*?\"<>|\r\n' '-') + echo "value=docs-site-${safe_ref}" >> "$GITHUB_OUTPUT" + - name: Upload downloadable site artifact uses: actions/upload-artifact@v4 with: - name: docs-site-${{ github.ref_name }} + name: ${{ steps.artifact_name.outputs.value }} path: _site - name: Upload Pages artifact From 9b8514235361ccef4aa15df4a15a98e30da60f51 Mon Sep 17 00:00:00 2001 From: Ryan Galloway Date: Wed, 19 Aug 2026 07:58:41 -0700 Subject: [PATCH 4/4] Address PR review feedback for docs workflow and benchmark copy --- .github/workflows/publish-docs.yml | 2 +- scripts/stage_mkpages_site.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 32a62b4..21a3658 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -12,7 +12,7 @@ permissions: id-token: write concurrency: - group: pages + group: pages-${{ github.ref }} cancel-in-progress: true jobs: diff --git a/scripts/stage_mkpages_site.py b/scripts/stage_mkpages_site.py index c5d9195..8eb17d0 100644 --- a/scripts/stage_mkpages_site.py +++ b/scripts/stage_mkpages_site.py @@ -38,7 +38,7 @@ def append_benchmark_section( sections.extend( [ "This section is generated automatically by the docs publishing workflow,", - "which runs `scripts/benchmark.py` on the current `master` branch before", + "which runs `scripts/benchmark.py` for the current workflow ref before", "building the Pages site.", "", benchmark_summary.read_text(encoding="utf-8").strip(),