├── .devcontainer.json ├── .flake8 ├── .github ├── CODE_OF_CONDUCT.md └── workflows │ └── ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── README.md ├── _extensions └── interlinks │ ├── .gitignore │ ├── _extension.yml │ ├── interlinks.lua │ ├── objects.txt │ └── test.qmd ├── case_studies ├── objects_sqla.inv ├── parsing.qmd ├── some_package.py ├── sphinx-inventory.md └── sphinx-inventory.qmd ├── docs ├── .gitignore ├── GITHUB.qmd ├── _filters │ └── replace-readme-links.lua ├── _quarto.yml ├── about.qmd ├── examples │ ├── images │ │ ├── pins-home.jpeg │ │ ├── shiny-home.jpeg │ │ ├── shinyswatch-home.jpeg │ │ ├── siuba-home.jpeg │ │ └── vetiver-home.jpeg │ └── index.qmd ├── get-started │ ├── advanced-layouts.qmd │ ├── architecture.qmd │ ├── basic-building.qmd │ ├── basic-content.qmd │ ├── basic-docs.qmd │ ├── crossrefs.qmd │ ├── dev-big-picture.qmd │ ├── dev-dataclasses.qmd │ ├── dev-docstrings.qmd │ ├── dev-prepare.qmd │ ├── dev-renderers.qmd │ ├── docstring-examples.qmd │ ├── docstring-style.qmd │ ├── extending.qmd │ ├── extra-build-sequence.qmd │ ├── interlinks-autolink.qmd │ ├── interlinks.qmd │ ├── overview.qmd │ └── sidebar.qmd ├── objects-test.txt ├── styles.css └── tutorials │ └── index.qmd ├── examples ├── .gitignore ├── auto-package │ └── _quarto.yml ├── dascore │ ├── .gitignore │ ├── _quarto.yml │ ├── generate_api.py │ └── index.md ├── pkgdown │ ├── .gitignore │ ├── _quarto.yml │ ├── objects.json │ └── reference │ │ ├── Builder.build.qmd │ │ ├── Builder.qmd │ │ ├── get_object.qmd │ │ ├── index.qmd │ │ └── preview.qmd ├── single-page │ ├── .gitignore │ ├── _quarto.yml │ ├── objects.json │ └── reference │ │ └── index.qmd └── weird-install │ ├── _quarto.yml │ └── src │ └── some_module.py ├── pyproject.toml ├── quartodoc ├── __init__.py ├── __main__.py ├── _griffe_compat │ ├── __init__.py │ ├── _generate_stubs.py │ ├── dataclasses.py │ ├── docstrings.py │ └── expressions.py ├── _pydantic_compat.py ├── ast.py ├── autosummary.py ├── builder │ ├── __init__.py │ ├── _node.py │ ├── blueprint.py │ ├── collect.py │ ├── utils.py │ └── write.py ├── interlinks.py ├── inventory.py ├── layout.py ├── pandoc │ ├── __init__.py │ ├── blocks.py │ ├── components.py │ └── inlines.py ├── parsers.py ├── renderers │ ├── __init__.py │ ├── base.py │ └── md_renderer.py ├── static │ └── styles.css ├── tests │ ├── __init__.py │ ├── __snapshots__ │ │ ├── test_builder.ambr │ │ ├── test_renderers.ambr │ │ └── test_validation.ambr │ ├── example.py │ ├── example_alias_target.py │ ├── example_alias_target__nested.py │ ├── example_attribute.py │ ├── example_class.py │ ├── example_docstring_styles.py │ ├── example_dynamic.py │ ├── example_interlinks │ │ ├── .gitignore │ │ ├── README.md │ │ ├── _inv │ │ │ └── other_objects.json │ │ ├── _quarto.yml │ │ ├── interlinks.lua │ │ ├── objects.json │ │ ├── spec.yml │ │ ├── test.md │ │ └── test.qmd │ ├── example_signature.py │ ├── example_star_imports.py │ ├── example_stubs.py │ ├── example_stubs.pyi │ ├── pandoc │ │ ├── test_blocks.py │ │ └── test_inlines.py │ ├── test_ast.py │ ├── test_basic.py │ ├── test_builder.py │ ├── test_builder_blueprint.py │ ├── test_cli.py │ ├── test_interlinks.py │ ├── test_layout.py │ ├── test_renderers.py │ └── test_validation.py └── validation.py └── scripts ├── build_tmp_starter.py └── filter-spec ├── generate_files.py └── generate_test_qmd.py /.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/python:3", 3 | "features": { 4 | "ghcr.io/rocker-org/devcontainer-features/quarto-cli:1": { 5 | "version": "prerelease" 6 | } 7 | }, 8 | "forwardPorts": [ 9 | 8888 10 | ], 11 | "portsAttributes": { 12 | "8888": { 13 | "label": "Jupyter" 14 | } 15 | }, 16 | "customizations": { 17 | "vscode": { 18 | "extensions": [ 19 | "ms-toolsai.jupyter" 20 | ] 21 | } 22 | }, 23 | "onCreateCommand": { 24 | "prep-python": "python3 -m pip install --upgrade setuptools jupyterlab ipython jupyterlab-quarto -e '.[dev]'" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = docs, test_*, .flake8, examples 3 | max-line-length = 90 4 | ignore = 5 | # line too long 6 | E501, 7 | # line before binary operator 8 | W503, 9 | # redefinition of unused function name 10 | F811 11 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at codeofconduct@posit.co. 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series of 85 | actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or permanent 92 | ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within the 112 | community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.1, available at 118 | . 119 | 120 | Community Impact Guidelines were inspired by 121 | [Mozilla's code of conduct enforcement ladder][https://github.com/mozilla/inclusion]. 122 | 123 | For answers to common questions about this code of conduct, see the FAQ at 124 | . Translations are available at . 125 | 126 | [homepage]: https://www.contributor-covenant.org 127 | 128 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: ["main", "dev-*"] 7 | pull_request: 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | test: 13 | name: "Test" 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | # Checks based on python versions --- 19 | python-version: ["3.9", "3.10"] 20 | requirements: [""] 21 | 22 | include: 23 | - name: "pydantic v1" 24 | requirements: "pydantic<2.0.0" 25 | python-version: "3.10" 26 | 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-python@v2 30 | with: 31 | python-version: "${{ matrix.python-version }}" 32 | - name: Install dev dependencies 33 | run: | 34 | python -m pip install --upgrade pip 35 | 36 | # include requirements if specified 37 | if [ -n "$REQUIREMENTS" ]; then 38 | python -m pip install $REQUIREMENTS '.[dev]' 39 | else 40 | python -m pip install '.[dev]' 41 | fi 42 | env: 43 | REQUIREMENTS: ${{ matrix.requirements }} 44 | - name: Run tests 45 | run: | 46 | pytest --cov 47 | - name: Upload results to Codecov 48 | uses: codecov/codecov-action@v4 49 | with: 50 | token: ${{ secrets.CODECOV_TOKEN }} 51 | 52 | pre-commit: 53 | name: "pre-commit checks" 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@v2 57 | - uses: actions/setup-python@v2 58 | with: 59 | python-version: "3.10" 60 | - name: Install dev dependencies 61 | run: | 62 | python -m pip install --upgrade pip 63 | python -m pip install ".[dev]" 64 | - uses: pre-commit/action@v3.0.0 65 | 66 | release-pypi: 67 | name: "Release to pypi" 68 | runs-on: ubuntu-latest 69 | if: github.event_name == 'release' 70 | needs: [test] 71 | steps: 72 | - uses: actions/checkout@v2 73 | - uses: actions/setup-python@v2 74 | with: 75 | python-version: "3.10" 76 | - name: "Build Package" 77 | run: | 78 | python -m pip install build wheel 79 | python -m build --sdist --wheel 80 | - name: "Deploy to Test PyPI" 81 | uses: pypa/gh-action-pypi-publish@release/v1 82 | with: 83 | user: __token__ 84 | password: ${{ secrets.PYPI_API_TOKEN }} 85 | 86 | build-docs: 87 | runs-on: ubuntu-latest 88 | steps: 89 | - uses: actions/checkout@v2 90 | - uses: actions/setup-python@v2 91 | with: 92 | python-version: "3.10" 93 | - name: Install dependencies 94 | run: | 95 | python -m pip install ".[dev]" 96 | # TODO: temporary installs for examples 97 | # once quartodoc is stable we should move into their own libraries 98 | python -m pip install shiny shinylive 99 | python -m pip install --no-deps dascore==0.0.8 100 | - uses: quarto-dev/quarto-actions/setup@v2 101 | - name: Test building starter template 102 | run: | 103 | make test-overview-template 104 | - name: Build docs 105 | run: | 106 | make docs-build 107 | # push to netlify ------------------------------------------------------- 108 | # set release name ---- 109 | 110 | - name: Configure pull release name 111 | if: ${{github.event_name == 'pull_request'}} 112 | run: | 113 | echo "RELEASE_NAME=pr-${PR_NUMBER}" >> $GITHUB_ENV 114 | env: 115 | PR_NUMBER: ${{ github.event.number }} 116 | - name: Configure branch release name 117 | if: ${{github.event_name != 'pull_request'}} 118 | run: | 119 | # use branch name, but replace slashes. E.g. feat/a -> feat-a 120 | echo "RELEASE_NAME=${GITHUB_REF_NAME/\//-}" >> $GITHUB_ENV 121 | # deploy ---- 122 | - name: Create Github Deployment 123 | uses: bobheadxi/deployments@v0.4.3 124 | id: deployment 125 | with: 126 | step: start 127 | token: ${{ secrets.GITHUB_TOKEN }} 128 | env: ${{ env.RELEASE_NAME }} 129 | ref: ${{ github.head_ref }} 130 | transient: true 131 | logs: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" 132 | 133 | - name: Netlify docs preview 134 | run: | 135 | npm install -g netlify-cli 136 | # push main branch to production, others to preview -- 137 | if [ "${ALIAS}" == "main" ]; then 138 | netlify deploy --dir=docs/_build --alias="main" 139 | else 140 | netlify deploy --dir=docs/_build --alias="${ALIAS}" 141 | fi 142 | env: 143 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} 144 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 145 | ALIAS: ${{ steps.deployment.outputs.env }} 146 | 147 | - name: Update Github Deployment 148 | uses: bobheadxi/deployments@v0.4.3 149 | if: ${{ always() }} 150 | with: 151 | step: finish 152 | token: ${{ secrets.GITHUB_TOKEN }} 153 | status: ${{ job.status }} 154 | deployment_id: ${{ steps.deployment.outputs.deployment_id }} 155 | env_url: "https://${{ steps.deployment.outputs.env }}--quartodoc.netlify.app" 156 | logs: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" 157 | - uses: peaceiris/actions-gh-pages@v3 158 | if: github.event_name == 'release' 159 | with: 160 | github_token: ${{ secrets.GITHUB_TOKEN }} 161 | publish_dir: docs/_build 162 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Notebooks =================================================================== 2 | *.ipynb 3 | 4 | # Mac OSX ===================================================================== 5 | .DS_Store 6 | 7 | # Vim ========================================================================= 8 | .*.sw[po] 9 | 10 | # Python ====================================================================== 11 | # Byte-compiled / optimized / DLL files 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # C extensions 17 | *.so 18 | 19 | # Distribution / packaging 20 | .Python 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | pip-wheel-metadata/ 34 | share/python-wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .nox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *.cover 60 | *.py,cover 61 | .hypothesis/ 62 | .pytest_cache/ 63 | 64 | # Translations 65 | *.mo 66 | *.pot 67 | 68 | # Django stuff: 69 | *.log 70 | local_settings.py 71 | db.sqlite3 72 | db.sqlite3-journal 73 | 74 | # Flask stuff: 75 | instance/ 76 | .webassets-cache 77 | 78 | # Scrapy stuff: 79 | .scrapy 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | 84 | # PyBuilder 85 | target/ 86 | 87 | # Jupyter Notebook 88 | .ipynb_checkpoints 89 | 90 | # IPython 91 | profile_default/ 92 | ipython_config.py 93 | 94 | # pyenv 95 | .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | .Rproj.user 141 | 142 | # Local Netlify folder 143 | .netlify 144 | 145 | /.luarc.json 146 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: ".*\\.(csv)|(md)|(json)|(ambr)" 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v2.4.0 5 | hooks: 6 | - id: trailing-whitespace 7 | - id: end-of-file-fixer 8 | - id: check-yaml 9 | args: ["--unsafe"] 10 | - id: check-added-large-files 11 | - repo: https://github.com/psf/black 12 | rev: 23.7.0 13 | hooks: 14 | - id: black 15 | - repo: https://github.com/PyCQA/flake8 16 | rev: 6.1.0 17 | hooks: 18 | - id: flake8 19 | types: 20 | - python 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 quartodoc authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Who maintains quartodoc 2 | 3 | The quartodoc package was created by and is currently maintained by Michael Chow . [Posit Software, PBC](https://posit.co/products/open-source/) is a copyright holder and funder of this package. 4 | 5 | Several individuals in the community have taken an active role in helping to maintain this package and submit fixes. Those individuals are shown in the git changelog. 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | EXAMPLE_INTERLINKS=quartodoc/tests/example_interlinks 2 | 3 | README.md: README.qmd 4 | quarto render $< 5 | 6 | 7 | # These 2 rules are used to generate the example_interlinks folder, 8 | # which contains a full example for the interlinks filter to be tested 9 | 10 | $(EXAMPLE_INTERLINKS): scripts/filter-spec/generate_files.py 11 | python3 $< 12 | 13 | $(EXAMPLE_INTERLINKS)/test.qmd: scripts/filter-spec/generate_test_qmd.py 14 | python3 $< 15 | 16 | $(EXAMPLE_INTERLINKS)/test.md: $(EXAMPLE_INTERLINKS)/test.qmd _extensions/interlinks/interlinks.lua 17 | cd $(EXAMPLE_INTERLINKS) && quarto render test.qmd --to gfm 18 | 19 | 20 | examples/%/_site: examples/%/_quarto.yml 21 | cd examples/$* \ 22 | && quarto add --no-prompt ../.. \ 23 | && quarto add --no-prompt quarto-ext/shinylive 24 | cd examples/$* && quartodoc build --config _quarto.yml --verbose 25 | cd examples/$* && quartodoc interlinks 26 | quarto render $(dir $<) 27 | 28 | 29 | docs/examples/%: examples/%/_site 30 | rm -rf docs/examples/$* 31 | cp -rv $< $@ 32 | 33 | docs-build-examples: docs/examples/single-page docs/examples/pkgdown docs/examples/auto-package 34 | 35 | docs-build-readme: export BUILDING_README = 1 36 | docs-build-readme: 37 | # note that the input file is named GITHUB.qmd, because quart does not 38 | # render files named README.qmd, and it is very cumbersome to work around 39 | # this very strange behavior 40 | cd docs \ 41 | && quarto render GITHUB.qmd \ 42 | --to gfm \ 43 | --output README.md \ 44 | --output-dir .. 45 | 46 | docs-build: export PLUM_SIMPLE_DOC=1 47 | docs-build: docs-build-examples 48 | cd docs && quartodoc build --verbose 49 | cd docs && quartodoc interlinks 50 | cd docs && quarto add --no-prompt .. 51 | quarto render docs 52 | 53 | test-overview-template: 54 | python scripts/build_tmp_starter.py 55 | 56 | test-interlinks: quartodoc/tests/example_interlinks/test.md 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | 4 | [![CI](https://github.com/machow/quartodoc/actions/workflows/ci.yml/badge.svg)](https://github.com/machow/quartodoc/actions/workflows/ci.yml) 5 | 6 | **quartodoc** lets you quickly generate Python package API reference 7 | documentation using Markdown and [Quarto](https://quarto.org). quartodoc 8 | is designed as an alternative to 9 | [Sphinx](https://www.sphinx-doc.org/en/master/). 10 | 11 | Check out the below screencast for a walkthrough of creating a 12 | documentation site, or read on for instructions. 13 | 14 |

15 | 16 | 17 | 18 | 19 |

20 | 21 |
22 | 23 | ## Installation 24 | 25 | ``` bash 26 | python -m pip install quartodoc 27 | ``` 28 | 29 | or from GitHub 30 | 31 | ``` bash 32 | python -m pip install git+https://github.com/machow/quartodoc.git 33 | ``` 34 | 35 | > [!IMPORTANT] 36 | > 37 | > ### Install Quarto 38 | > 39 | > If you haven’t already, you’ll need to [install 40 | > Quarto](https://quarto.org/docs/get-started/) before you can use 41 | > quartodoc. 42 | 43 | ## Basic use 44 | 45 | Getting started with quartodoc takes two steps: configuring quartodoc, 46 | then generating documentation pages for your library. 47 | 48 | You can configure quartodoc alongside the rest of your Quarto site in 49 | the 50 | [`_quarto.yml`](https://quarto.org/docs/projects/quarto-projects.html) 51 | file you are already using for Quarto. To [configure 52 | quartodoc](https://machow.github.io/quartodoc/get-started/basic-docs.html#site-configuration), 53 | you need to add a `quartodoc` section to the top level your 54 | `_quarto.yml` file. Below is a minimal example of a configuration that 55 | documents the `quartodoc` package: 56 | 57 | 58 | 59 | ``` yaml 60 | project: 61 | type: website 62 | 63 | # tell quarto to read the generated sidebar 64 | metadata-files: 65 | - reference/_sidebar.yml 66 | 67 | # tell quarto to read the generated styles 68 | format: 69 | html: 70 | css: 71 | - reference/_styles-quartodoc.css 72 | 73 | quartodoc: 74 | # the name used to import the package you want to create reference docs for 75 | package: quartodoc 76 | 77 | # write sidebar and style data 78 | sidebar: reference/_sidebar.yml 79 | css: reference/_styles-quartodoc.css 80 | 81 | sections: 82 | - title: Some functions 83 | desc: Functions to inspect docstrings. 84 | contents: 85 | # the functions being documented in the package. 86 | # you can refer to anything: class methods, modules, etc.. 87 | - get_object 88 | - preview 89 | ``` 90 | 91 | Now that you have configured quartodoc, you can generate the reference 92 | API docs with the following command: 93 | 94 | ``` bash 95 | quartodoc build 96 | ``` 97 | 98 | This will create a `reference/` directory with an `index.qmd` and 99 | documentation pages for listed functions, like `get_object` and 100 | `preview`. 101 | 102 | Finally, preview your website with quarto: 103 | 104 | ``` bash 105 | quarto preview 106 | ``` 107 | 108 | ## Rebuilding site 109 | 110 | You can preview your `quartodoc` site using the following commands: 111 | 112 | First, watch for changes to the library you are documenting so that your 113 | docs will automatically re-generate: 114 | 115 | ``` bash 116 | quartodoc build --watch 117 | ``` 118 | 119 | Second, preview your site: 120 | 121 | ``` bash 122 | quarto preview 123 | ``` 124 | 125 | ## Looking up objects 126 | 127 | Generating API reference docs for Python objects involves two pieces of 128 | configuration: 129 | 130 | 1. the package name. 131 | 2. a list of objects for content. 132 | 133 | quartodoc can look up a wide variety of objects, including functions, 134 | modules, classes, attributes, and methods: 135 | 136 | ``` yaml 137 | quartodoc: 138 | package: quartodoc 139 | sections: 140 | - title: Some section 141 | desc: "" 142 | contents: 143 | - get_object # function: quartodoc.get_object 144 | - ast.preview # submodule func: quartodoc.ast.preview 145 | - MdRenderer # class: quartodoc.MdRenderer 146 | - MdRenderer.render # method: quartodoc.MDRenderer.render 147 | - renderers # module: quartodoc.renderers 148 | ``` 149 | 150 | The functions listed in `contents` are assumed to be imported from the 151 | package. 152 | 153 | ## Learning more 154 | 155 | Go [to the next 156 | page](https://machow.github.io/quartodoc/get-started/basic-docs.html) to 157 | learn how to configure quartodoc sites, or check out these handy pages: 158 | 159 | - [Examples 160 | page](https://machow.github.io/quartodoc/examples/index.html): sites 161 | using quartodoc. 162 | - [Tutorials 163 | page](https://machow.github.io/quartodoc/tutorials/index.html): 164 | screencasts of building a quartodoc site. 165 | - [Docstring issues and 166 | examples](https://machow.github.io/quartodoc/get-started/docstring-examples.html): 167 | common issues when formatting docstrings. 168 | - [Programming, the big 169 | picture](https://machow.github.io/quartodoc/get-started/dev-big-picture.html): 170 | the nitty gritty of how quartodoc works, and how to extend it. 171 | -------------------------------------------------------------------------------- /_extensions/interlinks/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.pdf 3 | *_files/ 4 | -------------------------------------------------------------------------------- /_extensions/interlinks/_extension.yml: -------------------------------------------------------------------------------- 1 | title: Interlinks 2 | author: Michael Chow 3 | version: 1.1.0 4 | quarto-required: ">=1.2.0" 5 | contributes: 6 | filters: 7 | - interlinks.lua 8 | -------------------------------------------------------------------------------- /_extensions/interlinks/objects.txt: -------------------------------------------------------------------------------- 1 | # Sphinx inventory version 2 2 | # Project: quartodoc 3 | # Version: 0.0.9999 4 | # The remainder of this file is compressed using zlib. 5 | some_func py:function 1 api/some_func.html - 6 | a.b.c py:function 1 api/a.b.c.html - 7 | quartodoc.Auto py:class 1 api/Auto.html - 8 | -------------------------------------------------------------------------------- /_extensions/interlinks/test.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | filters: 3 | - interlinks.lua 4 | interlinks: 5 | autolink: true 6 | aliases: 7 | quartodoc: null 8 | #sources: 9 | # test: 10 | # url: https://example.com 11 | --- 12 | 13 | * `some_func` 14 | * `some_func()` 15 | * `some_func(a=1)` 16 | * `some_func()`{.qd-no-link} 17 | * `some_func + some_func` 18 | * `a.b.c` 19 | * `~a.b.c` 20 | * `a.b.c()` 21 | * `quartodoc.Auto()` 22 | * `Auto()` -------------------------------------------------------------------------------- /case_studies/objects_sqla.inv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machow/quartodoc/70aec122d550b1d78970c2368779f47594decf69/case_studies/objects_sqla.inv -------------------------------------------------------------------------------- /case_studies/parsing.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | format: markdown 3 | jupyter: 4 | jupytext: 5 | text_representation: 6 | extension: .qmd 7 | format_name: quarto 8 | format_version: '1.0' 9 | jupytext_version: 1.14.1 10 | kernelspec: 11 | display_name: Python 3 (ipykernel) 12 | language: python 13 | name: python3 14 | --- 15 | 16 | # Running pytkdocs 17 | 18 | ```{python} 19 | #| tags: [] 20 | import pytkdocs.cli 21 | 22 | config = { 23 | "objects": [ 24 | { 25 | "path": "some_package", 26 | "new_path_syntax": False, 27 | "members": True, 28 | "inherited_members": False, 29 | "filters": [ 30 | "!^_[^_]" 31 | ], 32 | "docstring_style": "numpy", 33 | #"docstring_options": { 34 | # "replace_admonitions": True 35 | #} 36 | } 37 | ] 38 | } 39 | 40 | res = pytkdocs.cli.process_config(config) 41 | ``` 42 | 43 | ```{python} 44 | list(res.keys()) 45 | ``` 46 | 47 | ```{python} 48 | res["objects"][0]["children"]["some_package.some_function"]["docstring_sections"] 49 | ``` 50 | 51 | ```{python} 52 | res["objects"][0]["children"]["some_package"]["children"] 53 | ``` 54 | 55 | ## Running griffe 56 | 57 | ```{python} 58 | from griffe import GriffeLoader 59 | from griffe import Parser, parse 60 | 61 | loader = GriffeLoader() 62 | 63 | sb2 = loader.load("some_package") 64 | 65 | parse(sb2.functions["some_function"].docstring, Parser.numpy) 66 | ``` 67 | -------------------------------------------------------------------------------- /case_studies/some_package.py: -------------------------------------------------------------------------------- 1 | class SomeClass: 2 | def __init__(self): 3 | """TODO""" 4 | 5 | pass 6 | 7 | 8 | def some_function(x: int, *args, **kwargs): 9 | """Return some thing. 10 | 11 | More about this function. 12 | 13 | Parameters 14 | ---------- 15 | x: int 16 | The `x` parameter. 17 | *args: tuple 18 | Positional arguments. 19 | **kwargs: dict, optional 20 | Keyword arguments. 21 | 22 | 23 | Returns 24 | ------- 25 | int 26 | A number 27 | 28 | See Also 29 | -------- 30 | another_function : does something else 31 | 32 | Notes 33 | ----- 34 | This is a note. 35 | 36 | Examples 37 | -------- 38 | 39 | This is the first example. 40 | 41 | >>> some_function(1) 42 | 2 43 | 44 | This is the seconds example. 45 | 46 | >>> some_function(2) 47 | 3 48 | 49 | 50 | """ 51 | 52 | return x + 1 53 | 54 | 55 | def another_function(): 56 | """Another function.""" 57 | 58 | pass 59 | -------------------------------------------------------------------------------- /case_studies/sphinx-inventory.md: -------------------------------------------------------------------------------- 1 | 2 | # Sphinx inventory files 3 | 4 | ## Resources 5 | 6 | - [sphinx inventory v2 field 7 | descriptions](https://sphobjinv.readthedocs.io/en/latest/syntax.html) 8 | 9 | ## Previewing an inventory file 10 | 11 | ``` bash 12 | python -m sphinx.ext.intersphinx 13 | ``` 14 | 15 | - [sphinx.util.inventory.InventoryFile](https://github.com/sphinx-doc/sphinx/blob/5e9550c78e3421dd7dcab037021d996841178f67/sphinx/util/inventory.py#L74) 16 | for opening the files. 17 | 18 | ``` python 19 | from sphinx.util.inventory import InventoryFile 20 | from os import path 21 | 22 | SQLALCHEMY_DOCS_URL = "https://docs.sqlalchemy.org/" 23 | SQLALCHEMY_INV_URL = f"{SQLALCHEMY_DOCS_URL}/objects.inv" 24 | 25 | with open("objects_sqla.inv", "rb") as f: 26 | inv = InventoryFile.load(f, SQLALCHEMY_DOCS_URL, path.join) 27 | ``` 28 | 29 | ``` python 30 | list(inv) 31 | ``` 32 | 33 | ['py:module', 34 | 'py:function', 35 | 'py:parameter', 36 | 'py:class', 37 | 'py:method', 38 | 'py:attribute', 39 | 'py:exception', 40 | 'py:data', 41 | 'std:label', 42 | 'std:term', 43 | 'std:doc'] 44 | 45 | ``` python 46 | inv["py:function"]["sqlalchemy.create_engine"] 47 | ``` 48 | 49 | ('SQLAlchemy', 50 | '1.4', 51 | 'https://docs.sqlalchemy.org/core/engines.html#sqlalchemy.create_engine', 52 | '-') 53 | -------------------------------------------------------------------------------- /case_studies/sphinx-inventory.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | format: gfm 3 | jupyter: 4 | jupytext: 5 | text_representation: 6 | extension: .qmd 7 | format_name: quarto 8 | format_version: '1.0' 9 | jupytext_version: 1.14.1 10 | kernelspec: 11 | display_name: Python 3 (ipykernel) 12 | language: python 13 | name: python3 14 | --- 15 | 16 | # Sphinx inventory files 17 | 18 | ## Resources 19 | 20 | * [sphinx inventory v2 field descriptions](https://sphobjinv.readthedocs.io/en/latest/syntax.html) 21 | 22 | ## Previewing an inventory file 23 | 24 | ```bash 25 | python -m sphinx.ext.intersphinx 26 | ``` 27 | 28 | * [sphinx.util.inventory.InventoryFile](https://github.com/sphinx-doc/sphinx/blob/5e9550c78e3421dd7dcab037021d996841178f67/sphinx/util/inventory.py#L74) for opening the files. 29 | 30 | ```{python} 31 | from sphinx.util.inventory import InventoryFile 32 | from os import path 33 | 34 | SQLALCHEMY_DOCS_URL = "https://docs.sqlalchemy.org/" 35 | SQLALCHEMY_INV_URL = f"{SQLALCHEMY_DOCS_URL}/objects.inv" 36 | 37 | with open("objects_sqla.inv", "rb") as f: 38 | inv = InventoryFile.load(f, SQLALCHEMY_DOCS_URL, path.join) 39 | ``` 40 | 41 | ```{python} 42 | list(inv) 43 | ``` 44 | 45 | ```{python} 46 | inv["py:function"]["sqlalchemy.create_engine"] 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /.quarto/ 2 | _site 3 | reference 4 | 5 | # examples 6 | examples/pkgdown 7 | examples/single-page 8 | 9 | # reference folder 10 | api 11 | 12 | # interlinks 13 | _inv 14 | objects.json 15 | _sidebar.yml 16 | _extensions 17 | 18 | /.luarc.json 19 | -------------------------------------------------------------------------------- /docs/GITHUB.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: quartodoc 3 | replace_base_domain: "https://machow.github.io/quartodoc" 4 | replace_rel_path: "/get-started" 5 | filters: 6 | - _filters/replace-readme-links.lua 7 | format: gfm 8 | --- 9 | 10 | ```{=markdown} 11 | [![CI](https://github.com/machow/quartodoc/actions/workflows/ci.yml/badge.svg)](https://github.com/machow/quartodoc/actions/workflows/ci.yml) 12 | ``` 13 | 14 | {{< include get-started/overview.qmd >}} 15 | -------------------------------------------------------------------------------- /docs/_filters/replace-readme-links.lua: -------------------------------------------------------------------------------- 1 | function Meta(meta) 2 | replace_base_domain = tostring(meta.replace_base_domain[1].text) 3 | replace_rel_path = tostring(meta.replace_rel_path[1].text) 4 | end 5 | 6 | -- pandoc Link object: replace the .qmd in the target with .html 7 | function Link(link) 8 | -- replace .qmd with .html in target 9 | -- replace beginning with replace_base_domain, accounting for / and ./ in paths 10 | -- e.g. ./overview.qmd -> {replace_rel_path}/get-started/overview.html 11 | -- e.g. /index.qmd -> {replace_base_domain}/index.html 12 | -- e.g. https://example.com -> https://example.com 13 | if link.target:match("%.qmd$") or link.target:match("%.qmd#.*$") then 14 | link.target = link.target:gsub("%.qmd", ".html") 15 | if link.target:match("^%./") then 16 | link.target = link.target:gsub("^%./", replace_base_domain .. replace_rel_path .. "/") 17 | end 18 | if link.target:match("^/") then 19 | link.target = link.target:gsub("^/", replace_base_domain .. "/") 20 | end 21 | -- if target does not start with http, do same as above 22 | if not link.target:match("^http") then 23 | link.target = replace_base_domain .. replace_rel_path .. "/" .. link.target 24 | end 25 | end 26 | 27 | return link 28 | end 29 | 30 | return { 31 | { 32 | Meta = Meta 33 | }, 34 | { 35 | Link = Link 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /docs/_quarto.yml: -------------------------------------------------------------------------------- 1 | project: 2 | type: website 3 | output-dir: _build 4 | resources: 5 | - examples/single-page 6 | - examples/pkgdown 7 | - examples/auto-package 8 | - objects.txt 9 | - objects-test.txt 10 | 11 | metadata-files: 12 | - api/_sidebar.yml 13 | 14 | filters: 15 | - "interlinks" 16 | 17 | interlinks: 18 | fast: true 19 | autolink: true 20 | aliases: 21 | quartodoc: [null, qd] 22 | sources: 23 | python: 24 | url: https://docs.python.org/3/ 25 | griffe: 26 | url: https://mkdocstrings.github.io/griffe/ 27 | qd2: 28 | url: https://machow.github.io/quartodoc/ 29 | inv: objects-test.txt 30 | 31 | 32 | website: 33 | title: "quartodoc" 34 | page-navigation: true 35 | navbar: 36 | left: 37 | - file: get-started/overview.qmd 38 | text: Get Started 39 | - file: examples/ 40 | text: Examples 41 | - file: tutorials/ 42 | text: Tutorials 43 | - href: api/ 44 | text: Reference 45 | 46 | right: 47 | - icon: github 48 | href: https://github.com/machow/quartodoc/ 49 | sidebar: 50 | - id: get-started 51 | title: Get Started 52 | style: floating 53 | align: left 54 | contents: 55 | - get-started/overview.qmd 56 | - section: "Basic Use" 57 | contents: 58 | - get-started/basic-docs.qmd 59 | - get-started/basic-content.qmd 60 | - get-started/basic-building.qmd 61 | - get-started/crossrefs.qmd 62 | - get-started/sidebar.qmd 63 | - get-started/extending.qmd 64 | 65 | - section: Interlinking 66 | contents: 67 | - get-started/interlinks.qmd 68 | - get-started/interlinks-autolink.qmd 69 | 70 | - section: "Docstrings" 71 | contents: 72 | - get-started/docstring-style.qmd 73 | - get-started/docstring-examples.qmd 74 | 75 | - section: "Programming" 76 | contents: 77 | - get-started/dev-big-picture.qmd 78 | - get-started/dev-prepare.qmd 79 | - get-started/dev-docstrings.qmd 80 | - get-started/dev-renderers.qmd 81 | - section: "Extra Topics" 82 | contents: 83 | - get-started/extra-build-sequence.qmd 84 | 85 | 86 | format: 87 | html: 88 | theme: cosmo 89 | css: 90 | - api/_styles-quartodoc.css 91 | - styles.css 92 | toc: true 93 | 94 | 95 | quartodoc: 96 | style: pkgdown 97 | dir: api 98 | package: quartodoc 99 | render_interlinks: true 100 | renderer: 101 | style: markdown 102 | table_style: description-list 103 | sidebar: "api/_sidebar.yml" 104 | css: "api/_styles-quartodoc.css" 105 | sections: 106 | - title: Preparation Functions 107 | desc: | 108 | These functions fetch and analyze Python objects, including parsing docstrings. 109 | They prepare a basic representation of your doc site that can be rendered and built. 110 | contents: 111 | - Auto 112 | - blueprint 113 | - collect 114 | - get_object 115 | - preview 116 | 117 | - title: Docstring Renderers 118 | desc: | 119 | Renderers convert parsed docstrings into a target format, like markdown. 120 | options: 121 | dynamic: true 122 | contents: 123 | - name: MdRenderer 124 | children: linked 125 | - MdRenderer.render 126 | - MdRenderer.render_annotation 127 | - MdRenderer.render_header 128 | - MdRenderer.signature 129 | - MdRenderer.summarize 130 | 131 | - title: API Builders 132 | desc: | 133 | Builders are responsible for building documentation. They tie all the pieces 134 | of quartodoc together, and can be defined in your _quarto.yml config. 135 | contents: 136 | - kind: auto 137 | name: Builder 138 | members: [] 139 | - Builder.from_quarto_config 140 | - Builder.build 141 | - Builder.write_index 142 | - Builder.write_doc_pages 143 | - Builder.write_sidebar 144 | - Builder.create_inventory 145 | 146 | - title: Inventory links 147 | desc: | 148 | Inventory files map a function's name to its corresponding url in your docs. 149 | These functions allow you to create and transform inventory files. 150 | contents: 151 | - create_inventory 152 | - convert_inventory 153 | 154 | - title: "Data models" 155 | 156 | - subtitle: "Structural" 157 | desc: | 158 | Classes for specifying the broad structure your docs. 159 | contents: 160 | - layout.Layout 161 | - layout.Section 162 | - layout.Page 163 | - layout.SectionElement 164 | - layout.ContentElement 165 | 166 | - subtitle: "Docable" 167 | desc: | 168 | Classes representing python objects to be rendered. 169 | contents: 170 | - name: layout.Doc 171 | members: [] 172 | - layout.DocFunction 173 | - layout.DocAttribute 174 | - layout.DocModule 175 | - layout.DocClass 176 | - layout.Link 177 | - layout.Item 178 | - layout.ChoicesChildren 179 | 180 | - subtitle: "Docstring patches" 181 | desc: | 182 | Most of the classes for representing python objects live 183 | in [](`griffe.dataclasses`) or [](`griffe.docstrings.dataclasses`). 184 | However, the `quartodoc.ast` module has a number of custom classes to fill 185 | in support for some important docstring sections. 186 | contents: 187 | - ast.DocstringSectionSeeAlso 188 | - ast.DocstringSectionNotes 189 | - ast.DocstringSectionWarnings 190 | - ast.ExampleCode 191 | - ast.ExampleText 192 | -------------------------------------------------------------------------------- /docs/about.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "About" 3 | --- 4 | 5 | About this site 6 | -------------------------------------------------------------------------------- /docs/examples/images/pins-home.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machow/quartodoc/70aec122d550b1d78970c2368779f47594decf69/docs/examples/images/pins-home.jpeg -------------------------------------------------------------------------------- /docs/examples/images/shiny-home.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machow/quartodoc/70aec122d550b1d78970c2368779f47594decf69/docs/examples/images/shiny-home.jpeg -------------------------------------------------------------------------------- /docs/examples/images/shinyswatch-home.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machow/quartodoc/70aec122d550b1d78970c2368779f47594decf69/docs/examples/images/shinyswatch-home.jpeg -------------------------------------------------------------------------------- /docs/examples/images/siuba-home.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machow/quartodoc/70aec122d550b1d78970c2368779f47594decf69/docs/examples/images/siuba-home.jpeg -------------------------------------------------------------------------------- /docs/examples/images/vetiver-home.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machow/quartodoc/70aec122d550b1d78970c2368779f47594decf69/docs/examples/images/vetiver-home.jpeg -------------------------------------------------------------------------------- /docs/examples/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | jupyter: 3 | kernel: python3 4 | --- 5 | 6 | :::::: {.column-page } 7 | 8 | 9 | ```{python} 10 | #| output: asis 11 | #| echo: false 12 | 13 | def gallery(entries): 14 | combined = "\n\n".join(entries) 15 | 16 | return f":::::: {{.gallery .list .grid}}\n{combined}\n::::::" 17 | 18 | 19 | def gallery_entry(src_thumbnail: str, href: str, title: str, href_ref=None, href_source=None): 20 | href_ref = href if href_ref is None else href_ref 21 | href_source = href if href_source is None else href_source 22 | 23 | return f""" 24 | 42 | """ 43 | 44 | def gallery_entry_cta(): 45 | href = "https://github.com/machow/quartodoc/discussions/new?category=general&title=New%20Doc%20Site:" 46 | return f""" 47 | 52 | """ 53 | 54 | print( 55 | gallery([ 56 | gallery_entry( 57 | "images/siuba-home.jpeg", 58 | "https://siuba.org", 59 | "Siuba", 60 | href_ref = "https://siuba.org/api", 61 | href_source = "https://github.com/machow/siuba.org", 62 | ), 63 | gallery_entry( 64 | "images/pins-home.jpeg", 65 | "https://rstudio.github.io/pins-python", 66 | "Pins", 67 | href_ref = "https://rstudio.github.io/pins-python/reference", 68 | href_source = "http://github.com/rstudio/pins-python", 69 | ), 70 | gallery_entry( 71 | "images/vetiver-home.jpeg", 72 | "https://rstudio.github.io/vetiver-python", 73 | "Vetiver", 74 | href_ref = "https://rstudio.github.io/vetiver-python/stable/reference", 75 | href_source = "https://github.com/rstudio/vetiver-python", 76 | ), 77 | gallery_entry( 78 | "images/shiny-home.jpeg", 79 | "https://shiny.rstudio.com/py/", 80 | "Shiny", 81 | href_ref = "https://shiny.rstudio.com/py/api", 82 | href_source = "https://github.com/rstudio/py-shiny", 83 | ), 84 | gallery_entry( 85 | "images/shinyswatch-home.jpeg", 86 | "https://posit-dev.github.io/py-shinyswatch/", 87 | "Shinyswatch", 88 | href_ref = "https://posit-dev.github.io/py-shinyswatch/reference", 89 | href_source = "https://github.com/posit-dev/py-shinyswatch", 90 | ), 91 | gallery_entry_cta() 92 | ]) 93 | ) 94 | ``` 95 | 96 | 97 | :::::: 98 | -------------------------------------------------------------------------------- /docs/get-started/advanced-layouts.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Advanced layouts 3 | jupyter: 4 | kernelspec: 5 | display_name: Python 3 (ipykernel) 6 | language: python 7 | name: python3 8 | --- 9 | 10 | ## Pages 11 | 12 | ## Children options 13 | 14 | ## 15 | -------------------------------------------------------------------------------- /docs/get-started/architecture.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Architecture 3 | jupyter: 4 | kernelspec: 5 | display_name: Python 3 (ipykernel) 6 | language: python 7 | name: python3 8 | --- 9 | 10 | :::{.column-page-right} 11 | 12 | ```{mermaid} 13 | %%| fig-width: 10 14 | classDiagram 15 | 16 | class BuilderConfig { 17 | style: str 18 | package: str 19 | version: str = None 20 | dir: str = "Reference" 21 | title: str = "Function reference" 22 | sections: list[SectionConfig] 23 | out_inventory: str = "objects.json" 24 | out_inder: str = "index.qmd" 25 | renderer: Renderer 26 | } 27 | 28 | class SectionConfig { 29 | title: str 30 | desc: str 31 | // 32 | // list of api functions 33 | contents: list[str] 34 | } 35 | 36 | 37 | class Introspect { 38 | // functions for analyzing python objects 39 | // and docstrings 40 | get_object(module, object_name) -> griffe object 41 | 42 | } 43 | 44 | class Inventory { 45 | // functions to work with sphinx inventories, 46 | // which are used for cross-references 47 | convert_inventory(in_name, out_name) 48 | create_inventory(project, version, items, ...) 49 | 50 | } 51 | class Renderer { 52 | style: str 53 | header_level: int = 2 54 | show_signature: bool = True 55 | hook_pre: Callable = None 56 | render(el: griffe object) 57 | } 58 | 59 | class Builder { 60 | // Includes all BuilderConfig properties 61 | ...BuilderConfig 62 | 63 | // 64 | // via create_* methods 65 | items: dict[str, griffe object] 66 | inventory: sphobjinv.Inventory 67 | 68 | 69 | build() 70 | create_items() 71 | create_inventory() 72 | fetch_object_uri() 73 | fetch_object_dispname() 74 | render_index() 75 | write_doc_pages() 76 | from_config() -> Builder 77 | } 78 | 79 | class BuilderPkgdown { 80 | // write R pkgdown style docs 81 | style: "pkgdown" 82 | render_index() 83 | fetch_object_uri() 84 | fetch_object_dispname() 85 | } 86 | 87 | class BuilderSinglePage { 88 | // writes one big page of docs 89 | style: "single-page" 90 | render_index() 91 | fetch_object_uri() 92 | write_doc_pages() 93 | } 94 | 95 | class MdRenderer { 96 | render() 97 | } 98 | 99 | 100 | Builder <|-- BuilderPkgdown 101 | Builder <|-- BuilderSinglePage 102 | BuilderConfig --> SectionConfig 103 | BuilderConfig <-- Builder: from_config 104 | Introspect <-- Builder: create_items 105 | Inventory <-- Builder: create_inventory(self.package, ..., self.items, self.fetch_*) 106 | Renderer <-- Builder 107 | Renderer <|-- MdRenderer 108 | ``` 109 | 110 | ::: 111 | -------------------------------------------------------------------------------- /docs/get-started/basic-building.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Building and debugging docs 3 | jupyter: 4 | kernelspec: 5 | display_name: Python 3 (ipykernel) 6 | language: python 7 | name: python3 8 | --- 9 | 10 | **tl;dr**: Once you've configured quartodoc in your `_quarto.yml` file, use the following commands to build and preview a documentation site. 11 | 12 | ## `quartodoc build`: Create doc files 13 | 14 | Automatically generate `.qmd` files with reference api documentation. This is written by default to the reference/ folder in your quarto project. 15 | 16 | ```bash 17 | quartodoc build 18 | ``` 19 | 20 | If you are iterating on your docstrings while previewing your site with `quarto preview`, you will likely want to rebuild the doc pages automatically when docstrings change. The `--watch` flag does exactly this. 21 | 22 | ```bash 23 | quartodoc build --watch 24 | ``` 25 | 26 | For more information on the `quartodoc build` command, use `--help` in the terminal like so: 27 | 28 | ```bash 29 | quartodoc build --help 30 | ``` 31 | 32 | ```{python} 33 | #|echo: false 34 | !quartodoc build --help 35 | ``` 36 | 37 | 38 | ## `quartodoc interlinks`: Create inventory files 39 | 40 | Inventory files facilitate linking to API doc pages within and across `quartodoc` sites. This is optional. 41 | 42 | ```bash 43 | quartodoc interlinks 44 | ``` 45 | 46 | ## `quarto preview`: Preview the site 47 | 48 | Use `quarto` to preview the site: 49 | 50 | ```bash 51 | quarto preview 52 | ``` 53 | 54 | ## Speeding up preview 55 | 56 | ### Rewriting doc files 57 | 58 | By default, the `quartodoc build` only re-writes doc pages when it detects 59 | a change to their content. This helps prevent `quarto preview` from trying 60 | to re-render every doc page--including those that haven't changed. 61 | 62 | ### Selectively building doc pages 63 | 64 | Use the filter option with `quartodoc build` to generate a subset of doc pages. 65 | This is useful when you have a many (e.g. several hundred) doc pages, and want 66 | to test a change on a single page. 67 | 68 | ```bash 69 | quartodoc build --filter 'get_object' 70 | ``` 71 | 72 | This option also accepts a wildcard pattern, which causes it to build docs for all matching objects. 73 | 74 | ```bash 75 | # write the docs for the MdRenderer class, and any of its methods 76 | # (e.g. MdRenderer.renderer) 77 | quartodoc build --filter 'MdRenderer*' 78 | ``` 79 | 80 | :::{.callout-note} 81 | When using a name with a wildcard, be sure to put it in single quotes! 82 | Otherwise, your shell may try to "expand it" to match file names. 83 | ::: -------------------------------------------------------------------------------- /docs/get-started/basic-docs.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configuring site 3 | jupyter: 4 | kernelspec: 5 | display_name: Python 3 (ipykernel) 6 | language: python 7 | name: python3 8 | --- 9 | 10 | ## Site configuration 11 | 12 | quartodoc is configured by adding a `quartodoc` section to your `_quarto.yml`: 13 | 14 | ```yaml 15 | quartodoc: 16 | style: pkgdown 17 | dir: reference 18 | package: quartodoc 19 | sections: 20 | - title: Some functions 21 | desc: Functions to inspect docstrings. 22 | contents: 23 | - get_object 24 | - preview 25 | ``` 26 | 27 | ### Top-level options 28 | 29 | The `quartodoc` section takes a `style` field, specifying which [](`quartodoc.Builder`) 30 | to use (currently "pkgdown" or "single-page"; see [Examples](/examples/)). 31 | 32 | ```{python} 33 | #| echo: false 34 | #| output: asis 35 | from quartodoc import get_object, MdRenderer 36 | 37 | obj = get_object("quartodoc", "Builder") 38 | renderer = MdRenderer() 39 | 40 | doc_parts = obj.docstring.parsed 41 | doc_params = [entry for entry in doc_parts if entry.kind.name == "parameters"][0] 42 | print(renderer.render(doc_params)) 43 | ``` 44 | 45 | ### Section options 46 | 47 | The `sections` field defines which functions to document. 48 | 49 | It commonly requires three pieces of configuration: 50 | 51 | * `title`: a title for the section 52 | * `desc`: a description for the section 53 | * `contents`: a list of functions to document 54 | 55 | You can also replace `title` with `subtitle` to create a sub-section. 56 | -------------------------------------------------------------------------------- /docs/get-started/crossrefs.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Linking to pages 3 | jupyter: 4 | kernelspec: 5 | display_name: Python 3 (ipykernel) 6 | language: python 7 | name: python3 8 | --- 9 | 10 | This page describes the different ways that quartodoc can link to other pages, 11 | where each style of link is used, and how to set them up. 12 | 13 | Here are the key details covered on this page: 14 | 15 | * Link to a page (.qmd file) using the path to the file. 16 | * Link to a function name by using the [interlinks filter's](./interlinks.qmd) special syntaxes. 17 | * Link type annotations in documented functions by enabling the [interlinks filter](./interlinks.qmd). 18 | 19 | :::{.callout-tip title="Guide pages on each linking style"} 20 | 21 | Here are some examples of different linking styles and their corresponding guide pages. 22 | 23 | | Link syntax | Example | Guide page | 24 | | --- | --- | --- | 25 | | Qmd path | ``[get_object](/reference/get_object.qmd)`` | [Quarto - markdown basics](https://quarto.org/docs/authoring/markdown-basics.html#links-images) | 26 | | Interlink (full) | ``[](`quartodoc.get_object`)`` | [Interlinks filter](./interlinks.qmd) | 27 | | Interlink (autolink) | `` `quartodoc.get_object` `` | [Interlinks filter - autolink mode](./interlinks-autolink.qmd) | 28 | ::: 29 | 30 | ## Linking by path 31 | 32 | You can use [quarto's markdown linking syntax](https://quarto.org/docs/authoring/markdown-basics.html#links-images) 33 | to link to function docs, by using the path to the generated documentation file. 34 | 35 | Here are some examples: 36 | 37 | | code | result | 38 | | ---- | ------ | 39 | | ``[get_object](/reference/get_object.qmd)`` | [get_object](/reference/get_object.qmd) | 40 | | ``[link text](/reference/MdRenderer.qmd)`` | [link text](/reference/MdRenderer.qmd) | 41 | 42 | 43 | ## Linking by function name 44 | 45 | Use quartodoc's [interlinking filter](./interlinks.qmd) to link to functions using only their names: 46 | 47 | | code | result | 48 | | ---- | ------ | 49 | | ``[](`quartodoc.get_object`)`` | [](`quartodoc.get_object`) | 50 | 51 | Notice that the link above puts the function name in backticks, rather than using 52 | the path to its documentation: `` `quartodoc.get_object` ``. 53 | 54 | You can also use this approach to link to other documentation sites. 55 | For example, including links to quartodoc, or https://docs.python.org/3 using function names. 56 | 57 | See the [interlinks documentation](./interlinks.qmd) for set up and usage. 58 | 59 | 60 | ## The "See Also" section 61 | 62 | A major goal of quartodoc is to automatically turn text in the "See Also" section 63 | of docstrings into links. 64 | 65 | See [this issue](https://github.com/machow/quartodoc/issues/21) for more details 66 | on parsing See Also sections, and [this issue](https://github.com/machow/quartodoc/issues/22) 67 | on turning type annotations into links. 68 | 69 | ## Type annotations in docstrings 70 | 71 | Creating links from type annotations for a function and their reference pages is supported via the interlinks filter. See the [Rendering Interlinks in API Docs](./interlinks.qmd#rendering-interlinks-in-api-docs) section on the interlinks page for more. 72 | 73 | Essentially, interlinking type annotations requires: 74 | 75 | * Setting the quartodoc `render_interlinks` config option to `true`. 76 | * Enabling the [interlinks filter](./interlinks.qmd). -------------------------------------------------------------------------------- /docs/get-started/dev-big-picture.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "The big picture: Builder" 3 | jupyter: 4 | kernelspec: 5 | display_name: Python 3 (ipykernel) 6 | language: python 7 | name: python3 8 | --- 9 | 10 | While the "Basic Use" section covered how to configure and build a site with quartodoc, this section focuses on using quartodoc as a Python program. 11 | 12 | Programming with quartodoc will help with debugging, tinkering, and extending things. 13 | 14 | ## Overview 15 | 16 | When a user runs `quartodoc build`, they 17 | 18 | * Create a [Builder](`quartodoc.Builder`) object, with the quartodoc config loaded as a [layout.Layout](`quartodoc.layout.Layout`). 19 | * Use [blueprint](`quartodoc.blueprint`) to process the layout into a plan for building the website. 20 | * Use [collect](`quartodoc.collect`) to get pages to render, and info on where documented objects live. 21 | 22 | This page will cover the basics of the Builder and this process. 23 | 24 | ## The Builder 25 | 26 | The code below shows a Builder object being loaded from a `_quarto.yml` config (loaded as a Python dictionary). 27 | 28 | ```{python} 29 | import yaml 30 | 31 | from quartodoc import Builder, blueprint, collect, MdRenderer 32 | 33 | cfg = yaml.safe_load(""" 34 | quartodoc: 35 | package: quartodoc 36 | style: pkgdown 37 | sections: 38 | - title: "Some section" 39 | desc: "Some description" 40 | contents: 41 | - name: MdRenderer 42 | members: ["render", "summarize"] 43 | children: separate 44 | """) 45 | 46 | builder = Builder.from_quarto_config(cfg) 47 | builder 48 | ``` 49 | 50 | Note that .from_quarto_config used the `style:` field to decide which Builder to create 51 | (in this case, `BuilderPkgdown`). 52 | 53 | We can view the config as a [layout.Layout](`quartodoc.layout.Layout`), by looking at the `.layout` attribute. 54 | 55 | ```{python} 56 | builder.layout 57 | ``` 58 | 59 | This can be a bit difficult to read, so quartodoc implements a [preview](`quartodoc.preview`) function, which spaces things out. 60 | 61 | ```{python} 62 | from quartodoc import preview 63 | preview(builder.layout) 64 | ``` 65 | 66 | Notice the following: 67 | 68 | * `preview` represents calls like `Layout()` with a box to the left, and then 69 | a pipe connecting it to each of its arguments. 70 | * The content entry `MdRenderer` is represented by an [Auto](`quartodoc.Auto`) class. 71 | This specifies a Python object to look up and document. 72 | 73 | We can follow the path in the preview above, to pull out just this first piece of content containing `MdRenderer`: 74 | 75 | ```{python} 76 | content = builder.layout.sections[0].contents[0] 77 | preview(content) 78 | ``` 79 | 80 | Next, we'll look at `blueprint()`, which processes the layout, including transforming `Auto` objects (like the one representing the `MdRenderer` above) into more concrete instructions. 81 | 82 | ## From config to blueprint 83 | 84 | The code below shows how `blueprint()` transforms the `Auto` entry for `MdRenderer`. 85 | 86 | ```{python} 87 | bp = blueprint(builder.layout) 88 | bp_contents = bp.sections[0].contents[0] 89 | preview(bp_contents, max_depth=3) 90 | ``` 91 | 92 | Notice two key pieces: 93 | 94 | * The `Auto` element is now a [layout.Page](`quartodoc.layout.Page`). 95 | The `.path` indicates that the documentation will be on a page called `"MdRenderer"`. 96 | * The content of the page is a [layout.DocClass](`quartodoc.layout.DocClass). 97 | This element holds everything needed to render this doc, including the class signature 98 | and parsed docstring. 99 | 100 | Importantly, the `.members` attribute stores how to render the class methods we listed in our configuration yaml, `.render()` and `.summarize()`: 101 | 102 | ```{python} 103 | preview(bp_contents.contents[0].members, max_depth=2) 104 | ``` 105 | 106 | Note that they are also a instances of `Page` (`MemberPage` to be exact). 107 | Before to building the site, we need to `collect()` all the pages. 108 | 109 | 110 | 111 | ## Collecting pages and items 112 | 113 | The [collect](`quartodoc.collect`) function pulls out two important pieces of information: 114 | 115 | * **pages** - each page to be rendered. 116 | * **items** - information on where each documented object lives in the site, which is used for things like [interlinks](interlinks.qmd). 117 | 118 | ```{python} 119 | pages, items = collect(bp, builder.dir) 120 | preview(pages, max_depth=3) 121 | ``` 122 | 123 | The code below shows a preview of the items. 124 | 125 | ```{python} 126 | preview(items, max_depth=2) 127 | ``` 128 | 129 | Notice that if you wanted to look up `quartodoc.MdRenderer.render`, the first item's `.uri` attribute shows the URL for it, relative to wherever the doc site is hosted. 130 | 131 | 132 | ## Rendering and writing 133 | 134 | A `Builder` instantiates a `Renderer` (like [](`~quartodoc.MdRenderer`)). 135 | Use the `.renderer` attribute to access it: 136 | 137 | ```{python} 138 | builder.renderer 139 | ``` 140 | 141 | The `render` method of of the [](`~quartodoc.MdRenderer`) returns a markdown string that can be rendered by Quarto: 142 | ```{python} 143 | print(builder.renderer.render(pages[0])) 144 | ``` 145 | 146 | :::{.callout-note} 147 | ### Cross References 148 | 149 | The `{ #quartodoc.MdRenderer.render }` in the output above is extended Quarto markdown that is a [cross reference](https://quarto.org/docs/authoring/cross-references.html). 150 | 151 | ::: 152 | 153 | ## Writing pages 154 | 155 | The builder has a number of methods it uses while materializing files that will be rendered by Quarto. 156 | The main method is [.build()](`quartodoc.Builder.build`). 157 | See the [Builder section of the API](#api-builders) for a list of methods, 158 | or this [giant build process diagram](/get-started/extra-build-sequence.qmd) for a full breakdown. -------------------------------------------------------------------------------- /docs/get-started/dev-dataclasses.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Which dataclass do I need?" 3 | jupyter: 4 | kernelspec: 5 | display_name: Python 3 (ipykernel) 6 | language: python 7 | name: python3 8 | --- 9 | 10 | Choosing between all the dataclasses 11 | 12 | * `griffe.dataclasses` 13 | * `griffe.docstrings.dataclasses` 14 | * `quartodoc.ast` 15 | * `quartodoc.layout` -------------------------------------------------------------------------------- /docs/get-started/dev-docstrings.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Inspecting docstrings 3 | jupyter: 4 | kernelspec: 5 | display_name: Python 3 (ipykernel) 6 | language: python 7 | name: python3 8 | --- 9 | 10 | quartodoc uses the library [griffe](https://github.com/mkdocstrings/griffe) to load and parse docstrings. 11 | 12 | ## Docstring structure 13 | 14 | quartodoc currently expects docstrings to be in the [numpydocstring](https://numpydoc.readthedocs.io/en/latest/format.html) format. 15 | 16 | Docstrings are loaded and parsed using [griffe](https://mkdocstrings.github.io/griffe), 17 | which uses custom data classes to represent the structure of a program: 18 | 19 | - [griffe.dataclasses](https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses) - represent the structure of python objects. 20 | - [griffe.docstrings.dataclasses](https://mkdocstrings.github.io/griffe/reference/griffe/docstrings/dataclasses/#griffe.docstrings.dataclasses) - represent the structure of parsed docstrings. 21 | 22 | 23 | 24 | ## Reading docstrings 25 | 26 | Use the function [get_object](/api/#get_object) to read in a docstring from a module. 27 | 28 | 29 | ```{python} 30 | from quartodoc import get_object, preview 31 | 32 | f_obj = get_object("quartodoc", "get_object") 33 | f_obj 34 | ``` 35 | 36 | The result above is a griffe object representing the function `quartodoc.get_object`, 37 | which has two important attributes: 38 | 39 | * `.name`: the function's name. 40 | * `.parameters`: the function's parameters. 41 | * `.docstring.value`: the actual docstring 42 | * `.docstring.parsed`: the docstring parsed into a tree of griffe objects 43 | 44 | ### Function name 45 | 46 | ```{python} 47 | f_obj.name 48 | ``` 49 | 50 | ### Function parameters 51 | 52 | ```{python} 53 | f_obj.parameters 54 | ``` 55 | 56 | ### Raw docstring value 57 | 58 | ```{python} 59 | print(f_obj.docstring.value) 60 | ``` 61 | 62 | ### Parsed docstring 63 | 64 | ```{python} 65 | f_obj.docstring.parsed 66 | ``` 67 | 68 | The docstring into a tree lets us define visitors, which can visit each element and 69 | do useful things. For example, print a high-level overview of its structure, or render it to markdown. 70 | 71 | ## Previewing docstrings 72 | 73 | Use the preview function to see the overall structure of a parsed docstring. 74 | 75 | ```{python} 76 | from quartodoc import get_object, preview 77 | 78 | f_obj = get_object("quartodoc", "get_object") 79 | ``` 80 | 81 | ### Raw docstring 82 | 83 | 84 | ```{python} 85 | print(f_obj.docstring.value) 86 | ``` 87 | 88 | ### Preview 89 | 90 | 91 | ```{python} 92 | preview(f_obj.docstring.parsed) 93 | ``` 94 | 95 | ## Parsing other docstring formats 96 | 97 | Currently, quartodoc expects docstrings in the numpydoc format. 98 | However, the tool it uses under the hood (griffe) is easy to customize, and supports multiple formats. 99 | 100 | See the griffe [loading docs](https://mkdocstrings.github.io/griffe/loading/) for instructions. 101 | Specifically, the [GriffeLoader](https://mkdocstrings.github.io/griffe/reference/griffe/loader/#griffe.loader.GriffeLoader) takes options for customizing docstring parsing. 102 | -------------------------------------------------------------------------------- /docs/get-started/dev-renderers.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Rendering docstrings 3 | jupyter: 4 | kernelspec: 5 | display_name: Python 3 (ipykernel) 6 | language: python 7 | name: python3 8 | --- 9 | 10 | The previous section covered how to read and preview parsed docstrings. 11 | In this section, we'll look at how to render a parsed docstring into a format 12 | that can be used in documentation, like markdown or HTML. 13 | 14 | ## Setting up problem 15 | 16 | Suppose that we wanted to take a function like `get_object()` and render a summary, with: 17 | 18 | * The number of parameters it takes. 19 | * The number of sections in its parsed docstring. 20 | 21 | For `get_object()` it might look like the following: 22 | 23 | ``` 24 | ## get_object 25 | N PARAMETERS: 3 26 | SECTIONS: A docstring with 4 pieces 27 | ``` 28 | 29 | ## Inspecting a function 30 | 31 | As covered in the previous section, we can preview information about `get_object()`. 32 | 33 | ```{python} 34 | from quartodoc import get_object, preview 35 | 36 | f_obj = get_object("quartodoc", "get_object") 37 | 38 | preview(f_obj, max_depth=3) 39 | ``` 40 | 41 | Note the following pieces: 42 | 43 | * `preview()` takes a max_depth argument, that limits how much information it shows. 44 | * `get_object()` takes 3 parameters. 45 | * `get_object()` has a docstring with 4 sections. 46 | 47 | Importantly, the nodes (`█`) in the tree mention the name class of the python objects 48 | being previewed (e.g. `Alias`, `Expression`, `Parameters`). 49 | We'll need these to specify how to render objects of each class. 50 | 51 | ## Generic dispatch 52 | 53 | Generic dispatch is the main programming technique used by quartodoc renderers. 54 | It let's you define how a function (like `render()`) should operate on different 55 | types of objects. 56 | 57 | ```{python} 58 | from plum import dispatch 59 | 60 | from griffe import Alias, Object, Docstring 61 | 62 | 63 | @dispatch 64 | def render(el: object): 65 | print(f"Default rendering: {type(el)}") 66 | 67 | 68 | @dispatch 69 | def render(el: Alias): 70 | print("Alias rendering") 71 | render(el.parameters) 72 | 73 | 74 | @dispatch 75 | def render(el: list): 76 | print("List rendering") 77 | [render(entry) for entry in el] 78 | 79 | 80 | render(f_obj) 81 | ``` 82 | 83 | ## Defining a Renderer 84 | 85 | quartodoc uses tree visitors to render parsed docstrings to formats like markdown and HTML. 86 | Tree visitors define how each type of object in the parse tree should be handled. 87 | 88 | ```{python} 89 | from griffe import Alias, Object, Docstring 90 | 91 | from quartodoc import get_object 92 | from plum import dispatch 93 | from typing import Union 94 | 95 | 96 | class SomeRenderer: 97 | def __init__(self, header_level: int = 1): 98 | self.header_level = header_level 99 | 100 | @dispatch 101 | def render(self, el): 102 | raise NotImplementedError(f"Unsupported type: {type(el)}") 103 | 104 | @dispatch 105 | def render(self, el: Union[Alias, Object]): 106 | header = "#" * self.header_level 107 | str_header = f"{header} {el.name}" 108 | str_params = f"N PARAMETERS: {len(el.parameters)}" 109 | str_sections = "SECTIONS: " + self.render(el.docstring) 110 | 111 | # return something pretty 112 | return "\n".join([str_header, str_params, str_sections]) 113 | 114 | @dispatch 115 | def render(self, el: Docstring): 116 | return f"A docstring with {len(el.parsed)} pieces" 117 | 118 | 119 | f_obj = get_object("quartodoc", "get_object") 120 | 121 | print(SomeRenderer(header_level=2).render(f_obj)) 122 | ``` 123 | 124 | Note 3 big pieces: 125 | 126 | * **Generic dispatch**: The plum `dispatch` function decorates each `render` method. The type annotations 127 | specify the types of data each version of render should dispatch on. 128 | * **Default behavior**: The first `render` method ensures a `NotImplementedError` is raised by default. 129 | * **Tree walking**: `render` methods often call `render` again on sub elements. 130 | 131 | 132 | ## Completing the Renderer 133 | 134 | While the above example showed a simple example with a `.render` method, a complete renderer will often do two more things: 135 | 136 | * Subclass an existing renderer. 137 | * Also override other methods like `.summarize()` 138 | 139 | ```{python} 140 | from quartodoc import MdRenderer 141 | 142 | class NewRenderer(MdRenderer): 143 | style = "new_renderer" 144 | 145 | @dispatch 146 | def render(self, el): 147 | print("calling parent method for render") 148 | return super().render(el) 149 | 150 | @dispatch 151 | def summarize(self, el): 152 | print("calling parent method for summarize") 153 | return super().summarize(el) 154 | ``` 155 | 156 | For a list of methods, see the [](`~quartodoc.MdRenderer`) docs. 157 | 158 | -------------------------------------------------------------------------------- /docs/get-started/docstring-examples.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Common issues and examples 3 | jupyter: 4 | kernelspec: 5 | display_name: Python 3 (ipykernel) 6 | language: python 7 | name: python3 8 | --- 9 | 10 | This page provides examples for commonly encountered situations (and some funky ones). 11 | 12 | See the [numpydoc sections guide][numpydoc] for more information and examples. 13 | 14 | [numpydoc]: https://numpydoc.readthedocs.io/en/latest/format.html#sections 15 | 16 | ## Examples: using code blocks 17 | 18 | Often, the Examples section of docstrings contain code examples. 19 | 20 | The Examples section supports two formats for code examples: 21 | 22 | * **doctest syntax** - code starts with `>>>`. 23 | * **markdown syntax** - surrounding code with three backticks (```` ``` ````) 24 | * **quarto syntax** - similar to markdown syntax (e.g. ```` ```{python} ````), but will execute code in the docs. 25 | 26 | Below is an example including each. 27 | 28 | 29 | ``` 30 | Examples 31 | -------- 32 | 33 | doctest syntax: 34 | 35 | >>> 1 + 1 36 | 2 37 | 38 | markdown syntax: 39 | 40 | ```python 41 | 1 + 1 42 | ``` 43 | 44 | quarto syntax: 45 | 46 | ```{{python}} 47 | 1 + 1 48 | ``` 49 | ``` 50 | 51 | Note that different syntaxes are handled differently: 52 | 53 | * doctest and markdown syntax: rendered without executing. 54 | * quarto syntax: executed by quarto when you run commands like `quarto render`. 55 | 56 | See the quarto documentation on [code blocks](https://quarto.org/docs/computations/python.html#code-blocks) for more detail. 57 | 58 | 59 | ## Examples, etc..: the "s" matters 60 | 61 | The numpydoc spec pluralizes section most names. 62 | If you leave off the "s", then they may be misparsed. 63 | 64 | For example, the docstring below erroneously has a "Return" section: 65 | 66 | ``` 67 | Return 68 | ------ 69 | 70 | some_name: int 71 | a description of the return result 72 | ``` 73 | 74 | In this case, the section won't be parsed, but written directly into the doc page. 75 | This means that "Return" would show up as a level 2 header. 76 | 77 | Here is a list of pluralized section names: 78 | 79 | * Parameters 80 | * Returns 81 | * Yields 82 | * Receives 83 | * Other Parameters 84 | * Raises 85 | * Warns 86 | * Warnings 87 | * Notes 88 | * References 89 | * Examples 90 | 91 | ## Returns: using type annotation 92 | 93 | In order to use the return type annotation of a function, use the following syntax. 94 | 95 | ``` 96 | Returns 97 | -------- 98 | : 99 | Some description of result 100 | ``` 101 | 102 | Below is a full example. 103 | 104 | ```{python} 105 | def f() -> int: 106 | """Some func 107 | 108 | Returns 109 | ------- 110 | : 111 | A number 112 | """ 113 | 114 | ``` 115 | 116 | See the [numpydoc Returns specification](https://numpydoc.readthedocs.io/en/latest/format.html#returns) for more on the general form of the Returns section. 117 | 118 | 119 | ## Using interlinks in docstrings 120 | 121 | quartodoc supports linking to functions using the [interlinks quarto filter](./interlinks.qmd) (and linking in general using [quarto link syntax](./crossrefs.qmd)). 122 | 123 | The code below shows an interlink, along with a regular quarto link. 124 | 125 | ```python 126 | def f(): 127 | """A function. 128 | 129 | Interlinks filter: 130 | 131 | See [](`quartodoc.get_object`) 132 | 133 | Regular quarto link (to a page in your docs): 134 | 135 | See the [reference](/reference/index.qmd) page. 136 | """ 137 | ``` 138 | 139 | :::{.callout-note} 140 | Linking to functions documented outside your package must be configured in the [interlinks filter](./interlinks.qmd). 141 | ::: 142 | 143 | 144 | ## How do I document a class? 145 | 146 | See [this numpydoc page on documenting classes](https://numpydoc.readthedocs.io/en/latest/format.html#documenting-classes). 147 | 148 | Below is a simple example of a class docstring. 149 | 150 | ```python 151 | class MyClass: 152 | """A great class. 153 | 154 | Parameters 155 | ---------- 156 | a: 157 | Some parameter. 158 | 159 | Attributes 160 | ---------- 161 | x: 162 | An integer 163 | """ 164 | 165 | 166 | x: int = 1 167 | 168 | 169 | def __init__(self, a: str): 170 | self.a = a 171 | ``` 172 | 173 | Note these two important pieces: 174 | 175 | * Document your `__init__` method parameters on the class docstring. 176 | * You can use an `Attributes` section in your docstring. 177 | 178 | -------------------------------------------------------------------------------- /docs/get-started/docstring-style.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Docstring formats 3 | jupyter: 4 | kernelspec: 5 | display_name: Python 3 (ipykernel) 6 | language: python 7 | name: python3 8 | --- 9 | 10 | quartodoc prefers numpy style for docstrings, but can support other styles by configuring parser in your [quartodoc site options](./basic-docs.qmd) of `_quarto.yml`: 11 | 12 | ```yaml 13 | quartodoc: 14 | parser: google 15 | ``` 16 | 17 | Currently, google, sphinx, and numpy are supported. Parsing is handled by the tool [griffe](https://github.com/mkdocstrings/griffe). 18 | 19 | ### Resources 20 | 21 | See the [numpydoc sections guide][numpydoc] for more information and examples. 22 | 23 | [numpydoc]: https://numpydoc.readthedocs.io/en/latest/format.html#sections -------------------------------------------------------------------------------- /docs/get-started/extending.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: More customization 3 | --- 4 | 5 | This page details 3 common aspects of quartodoc you can extend: 6 | 7 | * The reference index page generated (which lists all your functions). 8 | * The way docstrings are renderered. 9 | * The overall building process. 10 | 11 | 12 | ## Using a custom index page 13 | 14 | Use a custom index page to add content before or after the automatically generated 15 | API index. 16 | 17 | You can do this by setting quartodoc's `out_index` to be something other than `index.qmd`, 18 | and then including it in a custom `index.qmd` file. 19 | 20 | First, set `out_index` in your `_quarto.yml`: 21 | 22 | ```yaml 23 | website: 24 | navbar: 25 | left: 26 | - file: reference/index.qmd 27 | text: Reference 28 | 29 | quartodoc: 30 | dir: reference 31 | out_index: reference/_api_index.qmd 32 | ``` 33 | 34 | Then, create the file `reference/index.qmd` to have the form: 35 | 36 | ```bash 37 | --- 38 | --- 39 | 40 | Some custom content. 41 | 42 | 43 | {{{< include /reference/_api_index.qmd >}}} 44 | 45 | 46 | More content stuff. 47 | ``` 48 | 49 | Notice that the shortcode `{{< include ... >}}` is used to insert the index file generated 50 | by quartodoc (`_api_index.qmd`). 51 | 52 | 53 | ## Using a custom Renderer 54 | 55 | Use a custom renderer to add custom content after a renderered docstring, or 56 | to change the rendering process in general. 57 | 58 | You can do this by creating a custom file for your renderer in your docs folder, like `_renderer.py`, 59 | and then referencing it in your `_quarto.yml`. 60 | 61 | ```yaml 62 | quartodoc: 63 | renderer: 64 | style: _renderer.py 65 | ``` 66 | 67 | See the [Rendering docstrings](/get-started/renderers.qmd) page for instructions on 68 | creating a custom renderer, and the [](`quartodoc.MdRenderer`) docs for more information. 69 | 70 | ## Using a custom Builder 71 | 72 | Since the Builder controls the full quartodoc build process, using a custom builder 73 | provides total flexibility. This option currently isn't available, but would be easy 74 | to enable. 75 | 76 | Please leave a note on [this issue](https://github.com/machow/quartodoc/issues/34) if you 77 | need to use a custom builder. 78 | -------------------------------------------------------------------------------- /docs/get-started/extra-build-sequence.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Build sequence diagram 3 | jupyter: 4 | kernelspec: 5 | display_name: Python 3 (ipykernel) 6 | language: python 7 | name: python3 8 | --- 9 | 10 | This sequence diagram shows the process behind `quartodoc build`. 11 | See the API docs for [](`~quartodoc.Builder`), [](`~quartodoc.MdRenderer`), and the preperation functions ([](`~quartodoc.Auto`), [](`~quartodoc.blueprint`), [](`~quartodoc.collect`)) 12 | 13 | ```{mermaid} 14 | %%| fig-width: 100% 15 | sequenceDiagram 16 | Note left of CLI: _quarto.yml config 17 | CLI->>+Builder: .from_quarto_config() 18 | Builder->>MdRenderer: .from_config() 19 | MdRenderer-->>Builder: renderer 20 | Builder-->>-CLI: builder 21 | 22 | 23 | CLI->>+Builder: .build() 24 | 25 | Note over Builder: prepare site 26 | Builder->>+PrepFunctions: blueprint(self.layout) 27 | loop over Auto 28 | PrepFunctions->>PrepFunctions: get_object(name) 29 | end 30 | PrepFunctions-->>Builder: blueprint 31 | 32 | Builder->>PrepFunctions: collect(blueprint) 33 | PrepFunctions-->>Builder: pages, items 34 | 35 | Note over Builder: write the site 36 | Builder->>+Builder: write_index(blueprint) 37 | Builder->>MdRenderer: renderer.summarize(...) 38 | Note right of MdRenderer: Describe each object
on the index 39 | MdRenderer-->>Builder: index content 40 | 41 | Builder->>Builder: write_sidebar(blueprint) 42 | 43 | Builder->>Builder: write_doc_pages(pages) 44 | loop over pages 45 | Builder->>+MdRenderer: renderer.render(...) 46 | MdRenderer->>MdRenderer: .render_header(...) 47 | MdRenderer->>MdRenderer: .signature(...) 48 | MdRenderer->>MdRenderer: .render_annotation(...) 49 | Note right of MdRenderer: for all rendered types 50 | opt table of members 51 | MdRenderer->>MdRenderer: .summarize(...) 52 | end 53 | MdRenderer-->>-Builder: rendered docstring 54 | end 55 | 56 | Builder->>Builder: create_inventory(items) 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/get-started/interlinks-autolink.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Interlinks filter - autolink mode 3 | --- 4 | 5 | Autolink mode enables you to convert inline code, like `` `get_object()` ``, into a link to the function's reference page. It is an interlinks filter setting, enabled by setting `autolink: true`. 6 | 7 | ## Basic use 8 | 9 | Here is a basic example of enabling autolink mode in your `_quarto.yml` file: 10 | 11 | ```yaml 12 | filters: 13 | # requires running quarto add machow/quartodoc 14 | - interlinks 15 | 16 | interlinks: 17 | # enable autolink mode 18 | autolink: true 19 | # aliases allow you to refer to functions 20 | # without their module name, or using a shortened name 21 | aliases: 22 | quartodoc: [null, qd] 23 | ``` 24 | 25 | Note that in addition to enabling autolink mode, the `_quarto.yml` above uses `aliases:` to allow you to refer to `quartodoc` functions, without needing the module name. For example, using `get_object` instead of `quartodoc.get_object`. 26 | 27 | | link style | link syntax | result | 28 | | --- | --- | --- | 29 | | full path | `` `quartodoc.get_object()` `` | `quartodoc.get_object()` | 30 | | alias (qd) | `` `qd.get_object()` `` | `qd.get_object()` | 31 | | alias (null) | `` `get_object()` `` | `get_object()` | 32 | | shortening (`~~`) | `` `~~quartodoc.get_object()` `` | `~~quartodoc.get_object()` | 33 | | short dot (`~~.`) | `` `~~.quartodoc.get_object()` `` | `~~.quartodoc.get_object()` | 34 | | unmatched link | `` `~~unmatched_func()` `` | `unmatched_func()` | 35 | 36 | ## What gets autolinked? 37 | 38 | Any inline code that resembles a item name in [interlink syntax](./interlinks.qmd#link-formats) will be autolinked. 39 | In addition, autolink mode supports names with parentheses at the end. 40 | Below are some examples. 41 | 42 | Linked: 43 | 44 | * `` `a.b.c` `` 45 | * `` `a.b.c()` `` 46 | 47 | Not linked: 48 | 49 | * `` `a.b.c(x=1)` `` 50 | * `` `a.b.c + a.b.c` `` 51 | * `` `-a.b.c` `` 52 | 53 | ## Disable autolink on item 54 | 55 | Use the `qd-no-link` class to disable autolinking on a single piece of code. For example, `` `some_func()`{.qd-no-link} ``. 56 | 57 | ## Disable autolink on page 58 | 59 | Set autolink to false in the YAML top-matter for a page, in order to disable autolinking on that page. 60 | 61 | ``` 62 | --- 63 | interlinks: 64 | autolink: false 65 | --- 66 | 67 | Autolink won't apply here: `some_func()` 68 | 69 | or here: `another_func()` 70 | ``` 71 | 72 | This works because quarto uses the YAML top-matter to override your `_quarto.yml` settings. 73 | Technically, it merges your `_quarto.yml` and top-matter settings together, by doing the following: 74 | 75 | * Take `_quarto.yml` settings. 76 | * Override with any new top-matter settings. 77 | - dictionary items replace each other (e.g. `autolink: false` replaces old setting). 78 | - list items are appended. -------------------------------------------------------------------------------- /docs/get-started/interlinks.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Interlinks filter 3 | jupyter: 4 | kernelspec: 5 | display_name: Python 3 (ipykernel) 6 | language: python 7 | name: python3 8 | --- 9 | 10 | 11 | The interlinks filter allows you to provide crossreferences within and between documentation. 12 | It consists of three pieces: 13 | 14 | 1. **Install**: adding the extension to your quarto project. 15 | 1. **Configure**: specifying sphinx inventories for the filter to use in your `_quarto.yml` config. 16 | 2. **Run**: Generating sphinx inventories for the filter to use. 17 | 18 | ## Installing 19 | 20 | Use the [quarto add command](https://quarto.org/docs/extensions/filters.html#distribution) 21 | to install the interlinks filter: 22 | 23 | ```bash 24 | quarto add machow/quartodoc 25 | ``` 26 | 27 | :::{.callout-note} 28 | The code for the filter can be found in quartodoc's 29 | [_extension folder](https://github.com/machow/quartodoc/tree/main/_extensions/interlinks) 30 | ::: 31 | 32 | ## Configuring the interlinks filter 33 | 34 | Configure the filter in `_quarto.yml` or on specific pages, by adding these sections: 35 | 36 | ```yaml 37 | filters: 38 | - interlinks 39 | 40 | interlinks: 41 | sources: 42 | numpy: 43 | url: https://numpy.org/doc/stable/ 44 | python: 45 | url: https://docs.python.org/3/ 46 | quartodoc-test: 47 | url: https://machow.github.io/quartodoc 48 | inv: objects-test.txt 49 | ``` 50 | 51 | Notice 2 important pieces in this config: 52 | 53 | * The `numpy` and `python` fields indicate that we're getting inventories for the 54 | library numpy, and python builtin libraries. 55 | * The `url` fields indicate where inventory files can be found. 56 | * The `inv` field lets you specify the name of the inventory file. By default, it assumes its `objects.inv`. 57 | 58 | By default, downloaded inventory files will be saved in the `_inv` folder of your 59 | documentation directory. 60 | 61 | 62 | ### Experimental fast option 63 | 64 | Use the experimental `fast: true` option to speed up the interlinks filter. 65 | 66 | ```yaml 67 | interlinks: 68 | fast: true 69 | sources: 70 | ``` 71 | 72 | By default inventory files are saved as JSON, but this option keeps them as text files, 73 | and attempts to parse them much faster. 74 | 75 | :::{.callout-warning} 76 | Be sure to install the latest version of the interlinks filter, using `quarto add machow/quartodoc`. 77 | ::: 78 | 79 | ### Rendering interlinks in API docs 80 | 81 | quartodoc can convert type annotations in function signatures to interlinks. 82 | 83 | In order to enable this behavior, set `render_interlinks: true` in the quartodoc config. 84 | 85 | 86 | ```yaml 87 | quartodoc: 88 | render_interlinks: true 89 | ``` 90 | 91 | 92 | 93 | ## Running the interlinks filter 94 | 95 | First, build the reference for your own site, which includes an objects.json inventory: 96 | 97 | ```bash 98 | python -m quartodoc build 99 | ``` 100 | 101 | Second, retrieve the inventory files for any other sources: 102 | 103 | ```bash 104 | python -m quartodoc interlinks 105 | ``` 106 | 107 | Finally you should see the filter run when previewing your docs: 108 | 109 | ```bash 110 | quarto preview 111 | ``` 112 | 113 | 114 | ## Link formats 115 | 116 | 117 | | style | link text | syntax | output | 118 | | ----- | ---- | ------ | ------ | 119 | | manual | | `[a link](../api/#get_object)` | [a link](../api/#get_object) | 120 | | md | custom | `` [some explanation](`quartodoc.get_object`) `` | [some explanation](`quartodoc.get_object`) | 121 | | md | default | `` [](`quartodoc.get_object`) `` | [](`quartodoc.get_object`) | 122 | | md | shortened | `` [](`~quartodoc.get_object`) `` | [](`~quartodoc.get_object`) | 123 | 124 | 129 | 130 | ## Link aliases 131 | 132 | Use `interlinks.aliases` in `_quarto.yml` to refer to functions without their module name (or using a shortened version of it). 133 | 134 | For example the following config sets aliases for `quartodoc` and `pandas`: 135 | 136 | ```yaml 137 | interlinks: 138 | aliases: 139 | quartodoc: null 140 | pandas: pd 141 | ``` 142 | 143 | In this case, you can refer to `quartodoc.get_object` as `get_object`, and `pandas.DataFrame` as `pd.DataFrame`. 144 | 145 | 146 | ## Link filtering syntax 147 | 148 | Sometimes multiple documentation sites use the same target (e.g. function) names. 149 | The inventory format includes multiple pieces of information that can be used to 150 | refer to a specific entry in the inventory: 151 | 152 | * `inventory_name` 153 | * `role`: what kind of object is it? e.g. function, class. 154 | * `domain`: what kind of piece of documentation is it? For example, `"py"` indicates 155 | it is a python function, and `"c"` indicates it's a C function. This lets sites 156 | document libraries that are implemented in multiple languages. 157 | 158 | Filtering by these pieces of information can be down using the following syntax: 159 | 160 | ```rst 161 | :external+inventory_name:domain:role:`target` 162 | :domain:role:`target` 163 | :role:`target` 164 | `target` 165 | ``` 166 | 167 | Notice that this syntax allows you to go from more specific information (i.e. `` `target` `` on the right), 168 | to least specific information (`role`, then `domain`). 169 | 170 | In practice, it's often enough to specify the role of a function, like: 171 | 172 | * `` :function:`quartodoc.get_object` `` 173 | * `` :class:`quartodoc.MdRenderer` `` 174 | 175 | 176 | ### Example: python.org print 177 | 178 | For example, python.org has two entries for the name `print`. 179 | 180 | | domain | role | link syntax | 181 | | --- | --- | --- | 182 | | std | 2to3fixer | [``[](:std:2to3fixer:`print`)``](:std:2to3fixer:`print`) | 183 | | py | function | [``[](:py:function:`print`)``](:py:function:`print`) | 184 | 185 | 186 | ## What is a sphinx inventory file? 187 | 188 | Sphinx inventory files provide information about where the documentation for 189 | functions live on a website. 190 | 191 | Most sphinx sites name them `object.inv`: 192 | 193 | * numpy: https://numpy.org/doc/stable/objects.inv 194 | * python: https://docs.python.org/3/objects.inv 195 | 196 | See the [sphobjinv docs](https://sphobjinv.readthedocs.io/en/stable/) for thorough 197 | details on these files, and how they're used in sphinx. 198 | 199 | :::{.callout-note} 200 | [objects-test.txt](https://github.com/machow/quartodoc/tree/main/docs/objects-test.txt) is an example file with one entry: [](`qd2.Auto`). 201 | ::: 202 | 203 | ## More information 204 | 205 | Under the hood, quarto doc generates sphinx inventories for an API e using [create_inventory](/api/#sec-create_inventory), 206 | and then dumps it to JSON using [convert_inventory](/api/#sec-convert_inventory). 207 | 208 | For an overview of the sphinx inventory format, see [the sphobjinv docs](https://sphobjinv.readthedocs.io). 209 | 210 | The rough idea is that this plugin will behave similar to [jupyterbook linking](https://jupyterbook.org/en/stable/content/references.html), 211 | which supports both some intersphinx syntax, but also markdown syntax. 212 | -------------------------------------------------------------------------------- /docs/get-started/overview.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overview 3 | aliases: 4 | - ../index.html 5 | jupyter: 6 | kernelspec: 7 | display_name: Python 3 (ipykernel) 8 | language: python 9 | name: python3 10 | --- 11 | 12 | 13 | **quartodoc** lets you quickly generate Python package API reference documentation using Markdown and [Quarto](https://quarto.org). 14 | quartodoc is designed as an alternative to [Sphinx](https://www.sphinx-doc.org/en/master/). 15 | 16 | 17 | Check out the below screencast for a walkthrough of creating a documentation site, or read on for instructions. 18 | 19 | 20 | ```{python} 21 | #| echo: false 22 | #| output: asis 23 | 24 | # this code ensures that the proper html for the tutorial screencast is used, 25 | # depending on whether it's being rendered for the github README, or doc site. 26 | import os 27 | 28 | if "BUILDING_README" in os.environ: 29 | # I don't know why, but we need to repeat the Installation header here. 30 | # or quarto makes it disappear when we generate the readme 31 | print(""" 32 |

33 | 34 | 35 | 36 |

37 | 38 |
39 | 40 | """) 41 | else: 42 | print(""" 43 |
44 |
45 | """) 46 | 47 | ``` 48 | 49 | ## Installation 50 | 51 | ```bash 52 | python -m pip install quartodoc 53 | ``` 54 | or from GitHub 55 | 56 | ```bash 57 | python -m pip install git+https://github.com/machow/quartodoc.git 58 | ``` 59 | 60 | :::{.callout-important} 61 | 62 | ### Install Quarto 63 | 64 | If you haven't already, you'll need to [install Quarto](https://quarto.org/docs/get-started/) before you can use quartodoc. 65 | ::: 66 | 67 | 68 | ## Basic use 69 | 70 | Getting started with quartodoc takes two steps: configuring quartodoc, then generating documentation pages for your library. 71 | 72 | You can configure quartodoc alongside the rest of your Quarto site in the [`_quarto.yml`](https://quarto.org/docs/projects/quarto-projects.html) file you are already using for Quarto. To [configure quartodoc](./basic-docs.qmd#site-configuration), you need to add a `quartodoc` section to the top level your `_quarto.yml` file. Below is a minimal example of a configuration that documents the `quartodoc` package: 73 | 74 | 75 | 76 | ```yaml 77 | project: 78 | type: website 79 | 80 | # tell quarto to read the generated sidebar 81 | metadata-files: 82 | - reference/_sidebar.yml 83 | 84 | # tell quarto to read the generated styles 85 | format: 86 | html: 87 | css: 88 | - reference/_styles-quartodoc.css 89 | 90 | quartodoc: 91 | # the name used to import the package you want to create reference docs for 92 | package: quartodoc 93 | 94 | # write sidebar and style data 95 | sidebar: reference/_sidebar.yml 96 | css: reference/_styles-quartodoc.css 97 | 98 | sections: 99 | - title: Some functions 100 | desc: Functions to inspect docstrings. 101 | contents: 102 | # the functions being documented in the package. 103 | # you can refer to anything: class methods, modules, etc.. 104 | - get_object 105 | - preview 106 | ``` 107 | 108 | Now that you have configured quartodoc, you can generate the reference API docs with the following command: 109 | 110 | ```bash 111 | quartodoc build 112 | ``` 113 | 114 | This will create a `reference/` directory with an `index.qmd` and documentation 115 | pages for listed functions, like `get_object` and `preview`. 116 | 117 | Finally, preview your website with quarto: 118 | 119 | ```bash 120 | quarto preview 121 | ``` 122 | 123 | ## Rebuilding site 124 | 125 | You can preview your `quartodoc` site using the following commands: 126 | 127 | First, watch for changes to the library you are documenting so that your docs will automatically re-generate: 128 | 129 | ```bash 130 | quartodoc build --watch 131 | ``` 132 | 133 | Second, preview your site: 134 | 135 | ```bash 136 | quarto preview 137 | ``` 138 | 139 | ## Looking up objects 140 | 141 | Generating API reference docs for Python objects involves two pieces of configuration: 142 | 143 | 1. the package name. 144 | 2. a list of objects for content. 145 | 146 | quartodoc can look up a wide variety of objects, including functions, modules, classes, attributes, and methods: 147 | 148 | ```yaml 149 | quartodoc: 150 | package: quartodoc 151 | sections: 152 | - title: Some section 153 | desc: "" 154 | contents: 155 | - get_object # function: quartodoc.get_object 156 | - ast.preview # submodule func: quartodoc.ast.preview 157 | - MdRenderer # class: quartodoc.MdRenderer 158 | - MdRenderer.render # method: quartodoc.MDRenderer.render 159 | - renderers # module: quartodoc.renderers 160 | ``` 161 | 162 | The functions listed in `contents` are assumed to be imported from the package. 163 | 164 | 165 | ## Learning more 166 | 167 | Go [to the next page](basic-docs.qmd) to learn how to configure quartodoc sites, or check out these handy pages: 168 | 169 | * [Examples page](/examples/index.qmd): sites using quartodoc. 170 | * [Tutorials page](/tutorials/index.qmd): screencasts of building a quartodoc site. 171 | * [Docstring issues and examples](./docstring-examples.qmd): common issues when formatting docstrings. 172 | * [Programming, the big picture](./dev-big-picture.qmd): the nitty gritty of how quartodoc works, and how to extend it. 173 | -------------------------------------------------------------------------------- /docs/get-started/sidebar.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sidebar navigation 3 | --- 4 | 5 | quartodoc can generate a sidebar on the lefthand side of the page, with a list of your functions. 6 | 7 | In order to create a sidebar for your docs, add the following options to your `_quarto.yml`: 8 | 9 | ```yaml 10 | # tell quarto to read the sidebar file 11 | metadata-files: 12 | - _sidebar.yml 13 | 14 | 15 | # tell quartodoc to generate the sidebar file 16 | quartodoc: 17 | sidebar: "_sidebar.yml" 18 | # other options ... 19 | ``` 20 | 21 | Note that running `python -m quartodoc build` will now produce a file called `_sidebar.yml`, 22 | with a [Quarto sidebar configuration](https://quarto.org/docs/websites/website-navigation.html#side-navigation). 23 | The Quarto [`metadata-files` option](https://quarto.org/docs/projects/quarto-projects.html#metadata-includes) ensures 24 | it's included with the configuration in `_quarto.yml`. 25 | 26 | ::: { .callout-note} 27 | Here is what the sidebar for the [quartodoc reference page](/api) looks like: 28 | 29 |
30 | ```yaml 31 | {{< include /api/_sidebar.yml >}} 32 | ``` 33 |
34 | 35 | ::: 36 | 37 | ## Customizing the sidebar 38 | 39 | `sidebar` can also accept additional [sidebar options from the choices available in Quarto](https://quarto.org/docs/websites/website-navigation.html#side-navigation). These options are passed directly to Quarto, and can be used to customize the sidebar's appearance and behavior, or include additional content. 40 | 41 | When using a dictionary for `sidebar`, use `file` to specify the sidebar file (defaults to `_quartodoc-sidebar.yml` if not provided). You can also provide additional content in the sidebar. Tell quartodoc where to include your package documentation in the sidebar with the `"{{ contents }}"` placeholder. 42 | 43 | ```yaml 44 | quartodoc: 45 | sidebar: 46 | file: "_sidebar.yml" 47 | style: docked 48 | search: true 49 | collapse-level: 2 50 | contents: 51 | - text: "Introduction" 52 | href: introduction.qmd 53 | - section: "Reference" 54 | contents: 55 | - "{{ contents }}" 56 | - text: "Basics" 57 | href: basics-summary.qmd 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/objects-test.txt: -------------------------------------------------------------------------------- 1 | # Sphinx inventory version 2 2 | # Project: quartodoc 3 | # Version: 0.0.9999 4 | # The remainder of this file is compressed using zlib. 5 | qd2.Auto py:class 1 api/Auto.html#quartodoc.Auto - 6 | -------------------------------------------------------------------------------- /docs/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://rsms.me/inter/inter.css'); 2 | html { font-family: 'Inter', sans-serif; } 3 | @supports (font-variation-settings: normal) { 4 | html { font-family: 'Inter var', sans-serif; } 5 | } 6 | 7 | :root { 8 | --bs-body-font-family: inter; 9 | --bs-body-font-size: 16px; 10 | } 11 | 12 | /* css styles */ 13 | 14 | .cell-output pre code { 15 | white-space: pre-wrap; 16 | } 17 | 18 | 19 | /* sidebar formatting */ 20 | 21 | .sidebar a.nav-link { 22 | font-size: 14.4px; 23 | font-weight: 400; 24 | } 25 | 26 | 27 | .sidebar-item-container .text-start { 28 | font-weight: 600; 29 | font-size: 14.4px !important; 30 | } 31 | 32 | .sidebar-item-text { 33 | /*color: rgba(60, 60, 60, 0.7);*/ 34 | font-weight: 500; 35 | font-size: 14px; 36 | line-height: 22px; 37 | } 38 | 39 | .sidebar-item { 40 | margin-top: 0px; 41 | } 42 | 43 | .sidebar-item-section { 44 | padding-top: 16px; 45 | } 46 | 47 | .sidebar-section { 48 | padding-left: 0px !important; 49 | } 50 | 51 | .sidebar-item-section .sidebar-item-section { 52 | padding-top: 0px; 53 | padding-left: 10px; 54 | } 55 | 56 | 57 | td:first-child { 58 | text-wrap: nowrap; 59 | text-size-adjust: 100%; 60 | } 61 | 62 | td:first-child a { 63 | word-break: normal; 64 | } 65 | 66 | 67 | /* Example Gallery */ 68 | 69 | .gallery .gallery-card-body { 70 | height: 250px; 71 | padding-bottom: 20px; 72 | } 73 | 74 | .gallery .gallery-card-cta { 75 | font-size: 32px; 76 | text-align: center; 77 | padding: 100px 45px 45px 45px; 78 | width: 100%; 79 | background-color: rgb(248, 249, 250); 80 | } 81 | 82 | .gallery a { 83 | text-decoration: none; 84 | } 85 | 86 | 87 | .gallery .gallery-card-body img { 88 | width: 100%; 89 | height: 100%; 90 | object-fit: contain; 91 | } 92 | 93 | .gallery .card-header { 94 | font-size: 14px; 95 | text-decoration: none; 96 | text-align: center; 97 | } 98 | 99 | .gallery .card-header h6 { 100 | margin-top: 0.5rem; 101 | } 102 | 103 | .gallery .card-header a { 104 | text-decoration: none; 105 | } 106 | 107 | .gallery-links ul { 108 | list-style-type: none; 109 | } 110 | 111 | .gallery-links li { 112 | display: inline; 113 | margin-right: 60px; 114 | 115 | } 116 | -------------------------------------------------------------------------------- /docs/tutorials/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tutorials 3 | --- 4 | 5 | 6 | ## Building a basic doc site 7 | 8 | This screencast walks through the process of build API documentation, by re-creating 9 | the [pins API documentation](https://rstudio.github.io/pins-python/reference/) from scratch. 10 | 11 |
12 | 13 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | objects.json 2 | _extensions 3 | _inv 4 | _sidebar.yml 5 | -------------------------------------------------------------------------------- /examples/auto-package/_quarto.yml: -------------------------------------------------------------------------------- 1 | project: 2 | type: website 3 | resources: 4 | - objects.json 5 | 6 | website: 7 | title: pkgdown example 8 | navbar: 9 | left: 10 | - href: https://machow.github.io/quartodoc/ 11 | text: quartodoc home 12 | - file: reference/index.qmd 13 | text: "Reference" 14 | right: 15 | - icon: github 16 | href: https://github.com/machow/quartodoc/tree/main/examples/pkgdown 17 | 18 | format: 19 | html: 20 | toc: true 21 | 22 | quartodoc: 23 | style: pkgdown 24 | dir: reference 25 | package: quartodoc 26 | -------------------------------------------------------------------------------- /examples/dascore/.gitignore: -------------------------------------------------------------------------------- 1 | /.quarto/ 2 | 3 | _site 4 | reference 5 | objects.json 6 | -------------------------------------------------------------------------------- /examples/dascore/_quarto.yml: -------------------------------------------------------------------------------- 1 | project: 2 | type: website 3 | 4 | filters: 5 | - interlinks 6 | 7 | website: 8 | title: pkgdown example 9 | navbar: 10 | left: 11 | - file: reference/index.qmd 12 | text: "Reference" 13 | right: 14 | - icon: github 15 | href: https://github.com/machow/quartodoc/tree/main/examples/dascore 16 | 17 | format: 18 | html: 19 | theme: cosmo 20 | toc: true 21 | 22 | interlinks: 23 | sources: 24 | python: 25 | url: https://docs.python.org/3/ 26 | 27 | quartodoc: 28 | style: pkgdown 29 | dir: reference 30 | package: dascore 31 | display_name: relative 32 | renderer: 33 | style: markdown 34 | display_name: relative 35 | sections: 36 | - title: clients 37 | desc: "" 38 | contents: 39 | - clients.dirspool.DirectorySpool 40 | - clients.dirspool.DirectorySpool.get_contents 41 | - clients.dirspool.DirectorySpool.select 42 | - clients.dirspool.DirectorySpool.spool_path 43 | - clients.dirspool.DirectorySpool.update 44 | - clients.filespool.FileSpool.update 45 | - title: patch 46 | desc: "" 47 | contents: 48 | - core.patch.Patch 49 | - core.patch.Patch.attrs 50 | - core.patch.Patch.coord_dims 51 | - core.patch.Patch.coords 52 | - core.patch.Patch.data 53 | - core.patch.Patch.dims 54 | - core.patch.Patch.equals 55 | - core.patch.Patch.io 56 | - core.patch.Patch.new 57 | - core.patch.Patch.pipe 58 | - core.patch.Patch.shape 59 | - core.patch.Patch.to_xarray 60 | - core.patch.Patch.tran 61 | - core.patch.Patch.update_attrs 62 | - core.patch.Patch.viz 63 | - title: schema 64 | desc: "" 65 | contents: 66 | - core.schema.SimpleValidator.func 67 | - core.schema.SimpleValidator.validate 68 | - title: spool 69 | desc: "" 70 | contents: 71 | - core.spool.BaseSpool.chunk 72 | - core.spool.BaseSpool.get_contents 73 | - core.spool.BaseSpool.select 74 | - core.spool.BaseSpool.update 75 | - core.spool.DataFrameSpool.chunk 76 | - core.spool.DataFrameSpool.get_contents 77 | - core.spool.DataFrameSpool.new_from_df 78 | - core.spool.DataFrameSpool.select 79 | - core.spool.spool 80 | - core.spool.spool_from_patch 81 | - core.spool.spool_from_patch_list 82 | - core.spool.spool_from_spool 83 | - core.spool.spool_from_str 84 | - title: examples 85 | desc: "" 86 | contents: 87 | - examples.get_example_patch 88 | - examples.get_example_spool 89 | - examples.sin_wave_patch 90 | - examples.spool_to_directory 91 | - title: IO 92 | desc: "" 93 | contents: 94 | - io.core.FiberIO.get_format 95 | - io.core.FiberIO.implements_get_format 96 | - io.core.FiberIO.implements_scan 97 | - io.core.FiberIO.read 98 | - io.core.FiberIO.scan 99 | - io.core.FiberIO.write 100 | - io.core.read 101 | - io.core.scan 102 | - io.dasdae.core.DASDAEV1.get_format 103 | - io.dasdae.core.DASDAEV1.index 104 | - io.dasdae.core.DASDAEV1.read 105 | - io.dasdae.core.DASDAEV1.scan 106 | - io.dasdae.core.DASDAEV1.write 107 | - io.indexer.AbstractIndexer.update 108 | - io.indexer.DirectoryIndexer.clear_cache 109 | - io.indexer.DirectoryIndexer.ensure_path_exists 110 | - io.indexer.DirectoryIndexer.get_contents 111 | - io.indexer.DirectoryIndexer.update 112 | - io.pickle.core.PickleIO.get_format 113 | - io.pickle.core.PickleIO.read 114 | - io.pickle.core.PickleIO.write 115 | - io.tdms.core.TDMSFormatterV4713.get_format 116 | - io.tdms.core.TDMSFormatterV4713.read 117 | - io.tdms.core.TDMSFormatterV4713.scan 118 | - io.tdms.utils.parse_time_stamp 119 | - io.tdms.utils.type_not_supported 120 | - io.terra15.core.Terra15FormatterV4.get_format 121 | - io.terra15.core.Terra15FormatterV4.read 122 | - io.terra15.core.Terra15FormatterV4.scan 123 | - io.wav.core.WavIO.write 124 | - title: Procs 125 | desc: "" 126 | contents: 127 | - proc.aggregate.aggregate 128 | - proc.basic.abs 129 | - proc.basic.normalize 130 | - proc.basic.rename 131 | - proc.basic.squeeze 132 | - proc.basic.transpose 133 | - proc.detrend.detrend 134 | - proc.filter.pass_filter 135 | - proc.resample.decimate 136 | - proc.resample.interpolate 137 | - proc.resample.iresample 138 | - proc.resample.resample 139 | - proc.select.select 140 | - title: Transforms 141 | desc: "" 142 | contents: 143 | - transform.fft.rfft 144 | - transform.spectro.spectrogram 145 | - transform.strain.velocity_to_strain_rate 146 | - title: Utils 147 | desc: "" 148 | contents: 149 | - utils.chunk.ChunkManager.get_instruction_df 150 | - utils.coords.assign_coords 151 | - utils.coords.Coords.get 152 | - utils.coords.Coords.to_nested_dict 153 | - utils.coords.Coords.update 154 | # - utils.docs.compose_docstring 155 | - utils.downloader.fetch 156 | - utils.hdf5.HDFPatchIndexManager.decode_table 157 | - utils.hdf5.HDFPatchIndexManager.encode_table 158 | - utils.hdf5.HDFPatchIndexManager.get_index 159 | - utils.hdf5.HDFPatchIndexManager.has_index 160 | - utils.hdf5.HDFPatchIndexManager.hdf_kwargs 161 | - utils.hdf5.HDFPatchIndexManager.last_updated_timestamp 162 | - utils.hdf5.HDFPatchIndexManager.validate_version 163 | - utils.hdf5.HDFPatchIndexManager.write_update 164 | - utils.mapping.FrozenDict.copy 165 | - utils.misc.all_close 166 | - utils.misc.append_func 167 | - utils.misc.check_evenly_sampled 168 | - utils.misc.get_slice 169 | - utils.misc.iterate 170 | - utils.misc.register_func 171 | - utils.misc.suppress_warnings 172 | - utils.patch.check_patch_attrs 173 | - utils.patch.check_patch_dims 174 | - utils.patch.copy_attrs 175 | - utils.patch.get_default_patch_name 176 | - utils.patch.get_dim_value_from_kwargs 177 | - utils.patch.get_start_stop_step 178 | - utils.patch.merge_patches 179 | - utils.patch.patch_function 180 | - utils.patch.scan_patches 181 | - utils.pd.adjust_segments 182 | - utils.pd.fill_defaults_from_pydantic 183 | - utils.pd.get_column_names_from_dim 184 | - utils.pd.get_dim_names_from_columns 185 | - utils.pd.get_interval_columns 186 | - utils.pd.get_regex 187 | - utils.pd.list_ser_to_str 188 | - utils.pd.yield_slice_from_kwargs 189 | - utils.progress.track 190 | # TODO: some of these were removed from the more recent dascore version 191 | # - utils.time.array_to_datetime64 192 | # - utils.time.array_to_number 193 | # - utils.time.array_to_timedelta64 194 | #- utils.time.datetime_to_float 195 | #- utils.time.float_to_datetime 196 | #- utils.time.float_to_num 197 | #- utils.time.float_to_timedelta64 198 | #- utils.time.get_max_min_times 199 | #- utils.time.get_select_time 200 | #- utils.time.is_datetime64 201 | #- utils.time.pass_time_delta 202 | #- utils.time.series_to_timedelta64_series 203 | #- utils.time.str_to_datetime64 204 | #- utils.time.time_delta_from_str 205 | #- utils.time.to_datetime64 206 | #- utils.time.to_number 207 | #- utils.time.to_timedelta64 208 | #- utils.time.unpack_pandas_time_delta 209 | - title: Vizualization 210 | desc: "" 211 | contents: 212 | - viz.spectrogram.spectrogram 213 | - viz.waterfall.waterfall 214 | -------------------------------------------------------------------------------- /examples/dascore/generate_api.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # text_representation: 5 | # extension: .py 6 | # format_name: light 7 | # format_version: '1.5' 8 | # jupytext_version: 1.13.6 9 | # kernelspec: 10 | # display_name: venv-quartodoc 11 | # language: python 12 | # name: venv-quartodoc 13 | # --- 14 | 15 | # + 16 | from griffe import dataclasses as dc 17 | from griffe.loader import GriffeLoader 18 | from griffe.docstrings.parsers import Parser 19 | from pathlib import Path 20 | from plum import dispatch 21 | from quartodoc import MdRenderer 22 | 23 | griffe = GriffeLoader(docstring_parser=Parser("numpy")) 24 | mod = griffe.load_module("dascore") 25 | 26 | # + 27 | 28 | # These functions don't render for one of two reasons: they contain a section 29 | # type that is currently unsupported in the renderer (these are easy to add!), 30 | # or due to a small bug with how annotations are rendered. 31 | IGNORE = { 32 | "get_format", 33 | "write", 34 | "patches_to_df", 35 | "iter_files", 36 | "format_dtypes", 37 | "open_hdf5_file", 38 | "chunk", 39 | "get_intervals", 40 | "filter_df", 41 | "scan_to_df", 42 | } 43 | 44 | renderer = MdRenderer() 45 | 46 | 47 | class AutoSummary: 48 | def __init__(self, dir_name: str): 49 | self.dir_name = dir_name 50 | 51 | @staticmethod 52 | def full_name(el): 53 | return f"{el.parent.canonical_path}.{el.name}" 54 | 55 | @dispatch 56 | def visit(self, el): 57 | raise TypeError(f"Unsupported type: {type(el)}") 58 | 59 | @dispatch 60 | def visit(self, el: dc.Module): 61 | print(f"MOD: {el.canonical_path}") 62 | for name, class_ in el.classes.items(): 63 | self.visit(class_) 64 | 65 | for name, func in el.functions.items(): 66 | self.visit(func) 67 | 68 | for name, mod in el.modules.items(): 69 | self.visit(mod) 70 | 71 | @dispatch 72 | def visit(self, el: dc.Class): 73 | if el.name.startswith("_"): 74 | return 75 | 76 | print(f"CLASS: {self.full_name(el)}") 77 | for name, method in el.members.items(): 78 | self.visit(method) 79 | 80 | @dispatch 81 | def visit(self, el: dc.Alias): 82 | # Should skip Aliases, since dascore API docs match its 83 | # filestructure. 84 | return None 85 | 86 | @dispatch 87 | def visit(self, el: dc.Function): 88 | if el.name.startswith("_"): 89 | return 90 | if el.name in IGNORE: 91 | return 92 | 93 | full_name = self.full_name(el) 94 | print(f"FUNCTION: {full_name}") 95 | 96 | p_root = Path(self.dir_name) 97 | p_root.mkdir(exist_ok=True) 98 | 99 | p_func = p_root / f"{full_name}.md" 100 | p_func.write_text(renderer.to_md(el)) 101 | 102 | @dispatch 103 | def visit(self, el: dc.Attribute): 104 | if el.name.startswith("_"): 105 | return 106 | 107 | # a class attribute 108 | print(f"ATTR: {self.full_name(el)}") 109 | 110 | 111 | # - 112 | 113 | AutoSummary("api").visit(mod) 114 | -------------------------------------------------------------------------------- /examples/dascore/index.md: -------------------------------------------------------------------------------- 1 | ## Building this site 2 | 3 | ``` 4 | make build 5 | ``` 6 | 7 | ## Example links 8 | 9 | [](:function:`print`) 10 | 11 | [](`dascore.clients.dirspool.DirectorySpool`) 12 | -------------------------------------------------------------------------------- /examples/pkgdown/.gitignore: -------------------------------------------------------------------------------- 1 | /.quarto/ 2 | 3 | _site 4 | -------------------------------------------------------------------------------- /examples/pkgdown/_quarto.yml: -------------------------------------------------------------------------------- 1 | project: 2 | type: website 3 | resources: 4 | - objects.json 5 | 6 | website: 7 | title: pkgdown example 8 | navbar: 9 | left: 10 | - href: https://machow.github.io/quartodoc/ 11 | text: quartodoc home 12 | - file: reference/index.qmd 13 | text: "Reference" 14 | right: 15 | - icon: github 16 | href: https://github.com/machow/quartodoc/tree/main/examples/pkgdown 17 | 18 | format: 19 | html: 20 | theme: 21 | light: minty 22 | toc: true 23 | 24 | quartodoc: 25 | style: pkgdown 26 | dir: reference 27 | package: quartodoc 28 | sections: 29 | - title: Some functions 30 | desc: These functions inspect and parse docstrings. 31 | contents: 32 | - get_object 33 | - preview 34 | - Builder 35 | - Builder.build 36 | -------------------------------------------------------------------------------- /examples/pkgdown/objects.json: -------------------------------------------------------------------------------- 1 | {"project": "quartodoc", "version": "0.0.9999", "count": 4, "items": [{"name": "quartodoc.get_object", "domain": "py", "role": "function", "priority": "1", "uri": "reference/get_object.html", "dispname": "get_object"}, {"name": "quartodoc.preview", "domain": "py", "role": "function", "priority": "1", "uri": "reference/preview.html", "dispname": "preview"}, {"name": "quartodoc.Builder", "domain": "py", "role": "class", "priority": "1", "uri": "reference/Builder.html", "dispname": "Builder"}, {"name": "quartodoc.Builder.build", "domain": "py", "role": "function", "priority": "1", "uri": "reference/Builder.build.html", "dispname": "Builder.build"}]} -------------------------------------------------------------------------------- /examples/pkgdown/reference/Builder.build.qmd: -------------------------------------------------------------------------------- 1 | ## build { #build } 2 | 3 | `build(self)` 4 | 5 | Build index page, sphinx inventory, and individual doc pages. -------------------------------------------------------------------------------- /examples/pkgdown/reference/Builder.qmd: -------------------------------------------------------------------------------- 1 | ## Builder { #Builder } 2 | 3 | `Builder(self, package, sections, version=None, dir='reference', title='Function reference', renderer='markdown', out_index=None, sidebar=None, use_interlinks=False, display_name='name')` 4 | 5 | Base class for building API docs. 6 | 7 | ### Parameters 8 | 9 | | Name | Type | Description | Default | 10 | |----------------|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------| 11 | | `package` | str | The name of the package. | required | 12 | | `sections` | list[Any] | A list of sections, with items to document. | required | 13 | | `version` | str \| None | The package version. By default this attempts to look up the current package version (TODO). | `None` | 14 | | `dir` | str | Name of API directory. | `'reference'` | 15 | | `title` | str | Title of the API index page. | `'Function reference'` | 16 | | `renderer` | dict \| Renderer \| str | The renderer used to convert docstrings (e.g. to markdown). | `'markdown'` | 17 | | `out_index` | str | The output path of the index file, used to list all API functions. | `None` | 18 | | `sidebar` | str \| None | The output path for a sidebar yaml config (by default no config generated). | `None` | 19 | | `display_name` | str | The default name shown for documented functions. Either "name", "relative", "full", or "canonical". These options range from just the function name, to its full path relative to its package, to including the package name, to its the its full path relative to its .__module__. | `'name'` | -------------------------------------------------------------------------------- /examples/pkgdown/reference/get_object.qmd: -------------------------------------------------------------------------------- 1 | ## get_object { #get_object } 2 | 3 | `get_object(module, object_name, parser='numpy', load_aliases=True, modules_collection=None)` 4 | 5 | Fetch a griffe object. 6 | 7 | ### Parameters 8 | 9 | | Name | Type | Description | Default | 10 | |----------------------|---------------------------|------------------------------------------------------------------------------------|-----------| 11 | | `module` | str | A module name. | required | 12 | | `object_name` | str | A function name. | required | 13 | | `parser` | str | A docstring parser to use. | `'numpy'` | 14 | | `load_aliases` | | For aliases that were imported from other modules, should we load that module? | `True` | 15 | | `modules_collection` | None \| ModulesCollection | A griffe [](`~griffe.collections.ModulesCollection`), used to hold loaded modules. | `None` | 16 | 17 | ### See Also 18 | 19 | get_function: a deprecated function. 20 | 21 | ### Examples 22 | 23 | ```python 24 | >>> get_function("quartodoc", "get_function") 25 | >> from quartodoc import get_object 11 | >>> obj = get_object("quartodoc", "get_object") 12 | ``` 13 | 14 | ```python 15 | >>> preview(obj.docstring.parsed) 16 | ... 17 | ``` 18 | 19 | ```python 20 | >>> preview(obj) 21 | ... 22 | ``` -------------------------------------------------------------------------------- /examples/single-page/.gitignore: -------------------------------------------------------------------------------- 1 | /.quarto/ 2 | 3 | _site 4 | -------------------------------------------------------------------------------- /examples/single-page/_quarto.yml: -------------------------------------------------------------------------------- 1 | project: 2 | type: website 3 | resources: 4 | - objects.json 5 | 6 | website: 7 | title: single-page example 8 | navbar: 9 | left: 10 | - href: https://machow.github.io/quartodoc/ 11 | text: quartodoc home 12 | - file: reference/index.qmd 13 | text: "Reference" 14 | right: 15 | - icon: github 16 | href: https://github.com/machow/quartodoc/tree/main/examples/single-page 17 | 18 | format: 19 | html: 20 | theme: minty 21 | toc: true 22 | 23 | quartodoc: 24 | style: single-page 25 | dir: reference 26 | package: quartodoc 27 | sections: 28 | - title: Some functions 29 | desc: These functions inspect and parse docstrings. 30 | contents: 31 | - get_object 32 | - preview 33 | - Builder 34 | - Builder.build 35 | -------------------------------------------------------------------------------- /examples/single-page/objects.json: -------------------------------------------------------------------------------- 1 | {"project": "quartodoc", "version": "0.0.9999", "count": 4, "items": [{"name": "quartodoc.get_object", "domain": "py", "role": "function", "priority": "1", "uri": "reference/index.html#quartodoc.get_object", "dispname": "get_object"}, {"name": "quartodoc.preview", "domain": "py", "role": "function", "priority": "1", "uri": "reference/index.html#quartodoc.preview", "dispname": "preview"}, {"name": "quartodoc.Builder", "domain": "py", "role": "class", "priority": "1", "uri": "reference/index.html#quartodoc.Builder", "dispname": "Builder"}, {"name": "quartodoc.Builder.build", "domain": "py", "role": "function", "priority": "1", "uri": "reference/index.html#quartodoc.Builder.build", "dispname": "Builder.build"}]} -------------------------------------------------------------------------------- /examples/single-page/reference/index.qmd: -------------------------------------------------------------------------------- 1 | ## get_object { #get_object } 2 | 3 | `get_object(module, object_name, parser='numpy', load_aliases=True, modules_collection=None)` 4 | 5 | Fetch a griffe object. 6 | 7 | ### Parameters 8 | 9 | | Name | Type | Description | Default | 10 | |----------------------|---------------------------|------------------------------------------------------------------------------------|-----------| 11 | | `module` | str | A module name. | required | 12 | | `object_name` | str | A function name. | required | 13 | | `parser` | str | A docstring parser to use. | `'numpy'` | 14 | | `load_aliases` | | For aliases that were imported from other modules, should we load that module? | `True` | 15 | | `modules_collection` | None \| ModulesCollection | A griffe [](`~griffe.collections.ModulesCollection`), used to hold loaded modules. | `None` | 16 | 17 | ### See Also 18 | 19 | get_function: a deprecated function. 20 | 21 | ### Examples 22 | 23 | ```python 24 | >>> get_function("quartodoc", "get_function") 25 | >> from quartodoc import get_object 38 | >>> obj = get_object("quartodoc", "get_object") 39 | ``` 40 | 41 | ```python 42 | >>> preview(obj.docstring.parsed) 43 | ... 44 | ``` 45 | 46 | ```python 47 | >>> preview(obj) 48 | ... 49 | ``` 50 | 51 | ## Builder { #Builder } 52 | 53 | `Builder(self, package, sections, version=None, dir='reference', title='Function reference', renderer='markdown', out_index=None, sidebar=None, use_interlinks=False, display_name='name')` 54 | 55 | Base class for building API docs. 56 | 57 | ### Parameters 58 | 59 | | Name | Type | Description | Default | 60 | |----------------|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------| 61 | | `package` | str | The name of the package. | required | 62 | | `sections` | list[Any] | A list of sections, with items to document. | required | 63 | | `version` | str \| None | The package version. By default this attempts to look up the current package version (TODO). | `None` | 64 | | `dir` | str | Name of API directory. | `'reference'` | 65 | | `title` | str | Title of the API index page. | `'Function reference'` | 66 | | `renderer` | dict \| Renderer \| str | The renderer used to convert docstrings (e.g. to markdown). | `'markdown'` | 67 | | `out_index` | str | The output path of the index file, used to list all API functions. | `None` | 68 | | `sidebar` | str \| None | The output path for a sidebar yaml config (by default no config generated). | `None` | 69 | | `display_name` | str | The default name shown for documented functions. Either "name", "relative", "full", or "canonical". These options range from just the function name, to its full path relative to its package, to including the package name, to its the its full path relative to its .__module__. | `'name'` | 70 | 71 | ## build { #build } 72 | 73 | `build(self)` 74 | 75 | Build index page, sphinx inventory, and individual doc pages. -------------------------------------------------------------------------------- /examples/weird-install/_quarto.yml: -------------------------------------------------------------------------------- 1 | # quarto stuff ---------------------------------------------------------------- 2 | project: 3 | type: website 4 | 5 | website: 6 | title: weird-install example 7 | navbar: 8 | left: 9 | - file: reference/index.qmd 10 | text: "Reference" 11 | 12 | 13 | # quartodoc ------------------------------------------------------------------- 14 | 15 | quartodoc: 16 | package: null 17 | source_dir: ./src 18 | sections: 19 | - title: Some functions 20 | desc: These functions inspect and parse docstrings. 21 | contents: 22 | - some_module.some_function 23 | -------------------------------------------------------------------------------- /examples/weird-install/src/some_module.py: -------------------------------------------------------------------------------- 1 | def some_function(): 2 | """A function defined in a random python script""" 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools_scm] 6 | 7 | [tool.setuptools.packages.find] 8 | include = ["quartodoc"] 9 | 10 | [tool.pytest.ini_options] 11 | markers = [] 12 | testpaths = ["quartodoc"] 13 | 14 | [project] 15 | name = "quartodoc" 16 | authors = [{name="Michael Chow", email="michael.chow@posit.co"}] 17 | license.file = "LICENSE" 18 | description = "Generate API documentation with Quarto." 19 | readme = "README.md" 20 | keywords = ["documentation", "quarto"] 21 | classifiers = [ 22 | "License :: OSI Approved :: MIT License", 23 | "Operating System :: Microsoft :: Windows", 24 | "Operating System :: Unix", 25 | "Operating System :: MacOS", 26 | "Programming Language :: Python :: 3 :: Only", 27 | "Programming Language :: Python :: 3.9", 28 | "Programming Language :: Python :: 3.10", 29 | "Programming Language :: Python :: 3.11" 30 | ] 31 | dynamic = ["version"] 32 | requires-python = ">=3.9" 33 | dependencies = [ 34 | "black", 35 | "click", 36 | "griffe >= 0.33", 37 | "sphobjinv >= 2.3.1", 38 | "tabulate >= 0.9.0", 39 | "importlib-metadata >= 5.1.0", 40 | "importlib-resources >= 5.10.2", 41 | "pydantic", 42 | "pyyaml", 43 | "requests", 44 | "typing-extensions >= 4.4.0", 45 | "watchdog >= 3.0.0", 46 | "plum-dispatch < 2.0.0; python_version < '3.10'", 47 | "plum-dispatch > 2.0.0; python_version >= '3.10'" 48 | ] 49 | 50 | [project.urls] 51 | homepage = "https://machow.github.io/quartodoc" 52 | repository = "https://github.com/machow/quartodoc" 53 | ci = "https://github.com/machow/quartodoc/actions" 54 | 55 | 56 | [project.optional-dependencies] 57 | dev = [ 58 | "pytest<8.0.0", 59 | "pytest-cov", 60 | "jupyterlab", 61 | "jupytext", 62 | "syrupy", 63 | "pre-commit" 64 | ] 65 | 66 | [project.scripts] 67 | quartodoc = "quartodoc.__main__:cli" 68 | -------------------------------------------------------------------------------- /quartodoc/__init__.py: -------------------------------------------------------------------------------- 1 | """quartodoc is a package for building delightful python API documentation. 2 | """ 3 | 4 | # flake8: noqa 5 | 6 | from .autosummary import get_function, get_object, Builder 7 | from .renderers import MdRenderer 8 | from .inventory import convert_inventory, create_inventory 9 | from .ast import preview 10 | from .builder.blueprint import blueprint 11 | from .builder.collect import collect 12 | from .layout import Auto 13 | 14 | __all__ = ( 15 | "Auto", 16 | "blueprint", 17 | "collect", 18 | "convert_inventory", 19 | "create_inventory", 20 | "get_object", 21 | "preview", 22 | "Builder", 23 | "BuilderPkgdown", 24 | "BuilderSinglePage", 25 | "MdRenderer", 26 | ) 27 | -------------------------------------------------------------------------------- /quartodoc/_griffe_compat/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | try: 4 | from griffe import GriffeLoader 5 | from griffe import ModulesCollection, LinesCollection 6 | 7 | from . import dataclasses 8 | from . import docstrings 9 | from . import expressions 10 | 11 | from griffe import Parser, parse, parse_numpy 12 | from griffe import AliasResolutionError 13 | except ImportError: 14 | from griffe.loader import GriffeLoader 15 | from griffe.collections import ModulesCollection, LinesCollection 16 | 17 | import griffe.dataclasses as dataclasses 18 | import griffe.docstrings.dataclasses as docstrings 19 | import griffe.expressions as expressions 20 | 21 | from griffe.docstrings.parsers import Parser, parse 22 | from griffe.exceptions import AliasResolutionError 23 | -------------------------------------------------------------------------------- /quartodoc/_griffe_compat/_generate_stubs.py: -------------------------------------------------------------------------------- 1 | """Generate the submodules which import parts of griffe (e.g. dataclasses.py) 2 | 3 | This process balances having to import specific objects from griffe, against 4 | importing everything via the top-level griffe module. It does this by generating 5 | modules for objects commonly used together in quartodoc. 6 | 7 | * dataclasses: represent python objects 8 | * docstrings: represent python docstrings 9 | * expressions: represent annotation expressions 10 | 11 | Run using: python -m quartodoc._griffe_compat._generate_stubs 12 | """ 13 | 14 | import ast 15 | import black 16 | import copy 17 | import griffe 18 | import inspect 19 | from pathlib import Path 20 | 21 | 22 | def fetch_submodule(ast_body, submodule: str): 23 | ast_imports = [ 24 | obj 25 | for obj in ast_body 26 | if isinstance(obj, ast.ImportFrom) and obj.module == submodule 27 | ] 28 | 29 | if len(ast_imports) > 1: 30 | raise Exception(f"Found {len(ast_imports)} imports for {submodule}") 31 | elif not ast_imports: 32 | raise Exception(f"Could not find import for {submodule}") 33 | 34 | return ast_imports[0] 35 | 36 | 37 | def code_for_imports(mod_code: str, submodules: list[str]) -> str: 38 | res = [] 39 | mod = ast.parse(mod_code) 40 | for submod in submodules: 41 | expr = fetch_submodule(mod.body, submod) 42 | expr.module = "griffe" 43 | new_expr = copy.copy(expr) 44 | new_expr.module = "griffe" 45 | res.append(ast.unparse(new_expr)) 46 | 47 | return black.format_str( 48 | "# flake8: noqa\n\n" + "\n".join(res), mode=black.FileMode() 49 | ) 50 | 51 | 52 | def generate_griffe_stub(out_path: Path, mod, submodules: list[str]): 53 | res = code_for_imports(inspect.getsource(mod), submodules) 54 | out_path.write_text(res) 55 | 56 | 57 | MAPPINGS = { 58 | "dataclasses": [ 59 | "_griffe.models", 60 | "_griffe.mixins", 61 | "_griffe.enumerations", 62 | ], 63 | "docstrings": ["_griffe.docstrings.models"], 64 | "expressions": ["_griffe.expressions"], 65 | } 66 | 67 | if __name__ == "__main__": 68 | for out_name, submodules in MAPPINGS.items(): 69 | generate_griffe_stub( 70 | Path(__file__).parent / f"{out_name}.py", griffe, submodules 71 | ) 72 | -------------------------------------------------------------------------------- /quartodoc/_griffe_compat/dataclasses.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from griffe import ( 4 | Alias, 5 | Attribute, 6 | Class, 7 | Decorator, 8 | Docstring, 9 | Function, 10 | Module, 11 | Object, 12 | Parameter, 13 | Parameters, 14 | ) 15 | from griffe import ( 16 | DelMembersMixin, 17 | GetMembersMixin, 18 | ObjectAliasMixin, 19 | SerializationMixin, 20 | SetMembersMixin, 21 | ) 22 | from griffe import ( 23 | BreakageKind, 24 | DocstringSectionKind, 25 | ExplanationStyle, 26 | Kind, 27 | LogLevel, 28 | ObjectKind, 29 | ParameterKind, 30 | Parser, 31 | ) 32 | -------------------------------------------------------------------------------- /quartodoc/_griffe_compat/docstrings.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from griffe import ( 4 | DocstringAdmonition, 5 | DocstringAttribute, 6 | DocstringClass, 7 | DocstringDeprecated, 8 | DocstringElement, 9 | DocstringFunction, 10 | DocstringModule, 11 | DocstringNamedElement, 12 | DocstringParameter, 13 | DocstringRaise, 14 | DocstringReceive, 15 | DocstringReturn, 16 | DocstringSection, 17 | DocstringSectionAdmonition, 18 | DocstringSectionAttributes, 19 | DocstringSectionClasses, 20 | DocstringSectionDeprecated, 21 | DocstringSectionExamples, 22 | DocstringSectionFunctions, 23 | DocstringSectionModules, 24 | DocstringSectionOtherParameters, 25 | DocstringSectionParameters, 26 | DocstringSectionRaises, 27 | DocstringSectionReceives, 28 | DocstringSectionReturns, 29 | DocstringSectionText, 30 | DocstringSectionWarns, 31 | DocstringSectionYields, 32 | DocstringWarn, 33 | DocstringYield, 34 | ) 35 | -------------------------------------------------------------------------------- /quartodoc/_griffe_compat/expressions.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from griffe import ( 4 | Expr, 5 | ExprAttribute, 6 | ExprBinOp, 7 | ExprBoolOp, 8 | ExprCall, 9 | ExprCompare, 10 | ExprComprehension, 11 | ExprConstant, 12 | ExprDict, 13 | ExprDictComp, 14 | ExprExtSlice, 15 | ExprFormatted, 16 | ExprGeneratorExp, 17 | ExprIfExp, 18 | ExprJoinedStr, 19 | ExprKeyword, 20 | ExprLambda, 21 | ExprList, 22 | ExprListComp, 23 | ExprName, 24 | ExprNamedExpr, 25 | ExprParameter, 26 | ExprSet, 27 | ExprSetComp, 28 | ExprSlice, 29 | ExprSubscript, 30 | ExprTuple, 31 | ExprUnaryOp, 32 | ExprVarKeyword, 33 | ExprVarPositional, 34 | ExprYield, 35 | ExprYieldFrom, 36 | get_annotation, 37 | get_base_class, 38 | get_condition, 39 | get_expression, 40 | safe_get_annotation, 41 | safe_get_base_class, 42 | safe_get_condition, 43 | safe_get_expression, 44 | ) 45 | -------------------------------------------------------------------------------- /quartodoc/_pydantic_compat.py: -------------------------------------------------------------------------------- 1 | try: 2 | from pydantic.v1 import ( 3 | BaseModel, 4 | Field, 5 | Extra, 6 | PrivateAttr, 7 | ValidationError, 8 | ) # noqa 9 | except ImportError: 10 | from pydantic import BaseModel, Field, Extra, PrivateAttr, ValidationError # noqa 11 | -------------------------------------------------------------------------------- /quartodoc/builder/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machow/quartodoc/70aec122d550b1d78970c2368779f47594decf69/quartodoc/builder/__init__.py -------------------------------------------------------------------------------- /quartodoc/builder/_node.py: -------------------------------------------------------------------------------- 1 | # Note: this class is in its own file to ensure compatibility with python 3.9. 2 | # If it were in another file, then we would have to change all our types to 3 | # use Union and Optional, or pyndantic would error. 4 | 5 | from __future__ import annotations 6 | 7 | from quartodoc._pydantic_compat import BaseModel 8 | from typing import Any, Optional 9 | 10 | 11 | class Node(BaseModel): 12 | level: int = -1 13 | value: Any = None 14 | parent: Optional[Node] = None 15 | 16 | 17 | Node.update_forward_refs() 18 | -------------------------------------------------------------------------------- /quartodoc/builder/collect.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from quartodoc import layout 4 | from plum import dispatch 5 | 6 | from .utils import PydanticTransformer, ctx_node 7 | 8 | 9 | # Visitor --------------------------------------------------------------------- 10 | 11 | 12 | class CollectTransformer(PydanticTransformer): 13 | def __init__(self, base_dir: str): 14 | self.base_dir = base_dir 15 | self.items: list[layout.Item] = [] 16 | self.pages: list[layout.Page] = [] 17 | 18 | def find_page_node(self): 19 | crnt_node = orig_node = ctx_node.get() # noqa 20 | 21 | is_parent = False 22 | 23 | while is_parent is False: 24 | if crnt_node.value is None: 25 | raise ValueError( 26 | f"No page detected above current element: {crnt_node.value}" 27 | ) 28 | 29 | if isinstance(crnt_node.value, layout.Page): 30 | return crnt_node 31 | 32 | crnt_node = crnt_node.parent 33 | 34 | return crnt_node 35 | 36 | @dispatch 37 | def exit(self, el: layout.Doc): 38 | page_node = self.find_page_node() 39 | p_el = page_node.value 40 | 41 | uri = f"{self.base_dir}/{p_el.path}.html#{el.anchor}" 42 | 43 | name_path = el.obj.path 44 | canonical_path = el.obj.canonical_path 45 | 46 | # item corresponding to the specified path ---- 47 | # e.g. this might be a top-level import 48 | self.items.append( 49 | layout.Item(name=name_path, obj=el.obj, uri=uri, dispname=None) 50 | ) 51 | 52 | if name_path != canonical_path: 53 | # item corresponding to the canonical path ---- 54 | # this is where the object is defined (which may be deep in a submodule) 55 | self.items.append( 56 | layout.Item( 57 | name=canonical_path, obj=el.obj, uri=uri, dispname=name_path 58 | ) 59 | ) 60 | 61 | return el 62 | 63 | @dispatch 64 | def exit(self, el: layout.Page): 65 | self.pages.append(el) 66 | 67 | return el 68 | 69 | 70 | def collect(el: layout._Base, base_dir: str): 71 | """Return all pages and items in a layout. 72 | 73 | Parameters 74 | ---------- 75 | el: 76 | An element, like layout.Section or layout.Page, to collect pages and items from. 77 | base_dir: 78 | The directory where API pages will live. 79 | 80 | 81 | """ 82 | 83 | trans = CollectTransformer(base_dir=base_dir) 84 | trans.visit(el) 85 | 86 | return trans.pages, trans.items 87 | -------------------------------------------------------------------------------- /quartodoc/builder/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from contextvars import ContextVar 4 | from plum import dispatch 5 | from typing import Union 6 | 7 | from quartodoc._pydantic_compat import BaseModel 8 | from ._node import Node 9 | 10 | 11 | # Transformer ----------------------------------------------------------------- 12 | 13 | ctx_node: ContextVar[Node] = ContextVar("node") 14 | 15 | 16 | class WorkaroundKeyError(Exception): 17 | """Represents a KeyError. 18 | 19 | Note that this is necessary to work around a bug in plum dispatch, which 20 | intercepts KeyErrors, and then causes an infinite recursion by re-calling 21 | the dispatcher. 22 | """ 23 | 24 | 25 | class PydanticTransformer: 26 | LOG = False 27 | 28 | def _log(self, step: str, el): 29 | if self.LOG: 30 | print(f"{step}: {type(el)} {el}") 31 | 32 | @dispatch 33 | def visit(self, el): 34 | self._log("PARENT VISITING", el) 35 | 36 | old_node = ctx_node.get(None) 37 | if old_node is None: 38 | old_node = Node() 39 | 40 | new_node = Node(level=old_node.level + 1, value=el, parent=old_node) 41 | 42 | token = ctx_node.set(new_node) 43 | 44 | try: 45 | result = self.enter(el) 46 | return self.exit(result) 47 | finally: 48 | ctx_node.reset(token) 49 | 50 | @dispatch 51 | def enter(self, el): 52 | self._log("GENERIC ENTER", el) 53 | return el 54 | 55 | @dispatch 56 | def exit(self, el): 57 | self._log("GENERIC EXIT", el) 58 | return el 59 | 60 | @dispatch 61 | def enter(self, el: BaseModel): 62 | self._log("GENERIC ENTER", el) 63 | new_kwargs = {} 64 | 65 | has_change = False 66 | for field, value in el: 67 | result = self.visit(value) 68 | if result is not value: 69 | has_change = True 70 | new_kwargs[field] = result 71 | else: 72 | new_kwargs[field] = value 73 | 74 | if has_change: 75 | return el.__class__(**new_kwargs) 76 | 77 | return el 78 | 79 | @dispatch 80 | def enter(self, el: Union[list, tuple]): 81 | self._log("GENERIC ENTER", el) 82 | final = [] 83 | 84 | # has_change = False 85 | for child in el: 86 | result = self.visit(child) 87 | if result is not child: 88 | # has_change = True 89 | final.append(result) 90 | else: 91 | final.append(child) 92 | 93 | # for now just return a copy always 94 | return el.__class__(final) 95 | 96 | 97 | # Implementations ------------------------------------------------------------- 98 | 99 | 100 | class _TypeExtractor(PydanticTransformer): 101 | def __init__(self, target_cls): 102 | self.target_cls = target_cls 103 | self.results = [] 104 | 105 | @dispatch 106 | def exit(self, el): 107 | if isinstance(el, self.target_cls): 108 | self.results.append(el) 109 | 110 | return el 111 | 112 | @classmethod 113 | def run(cls, target_cls, el): 114 | extractor = cls(target_cls) 115 | extractor.visit(el) 116 | 117 | return extractor.results 118 | 119 | 120 | extract_type = _TypeExtractor.run 121 | -------------------------------------------------------------------------------- /quartodoc/builder/write.py: -------------------------------------------------------------------------------- 1 | from plum import dispatch 2 | from quartodoc.autosummary import Builder 3 | 4 | 5 | @dispatch 6 | def write_doc_pages(builder: Builder): 7 | return builder.write_doc_pages() 8 | 9 | 10 | @dispatch 11 | def write_index(builder: Builder): 12 | return builder.write_index() 13 | 14 | 15 | @dispatch 16 | def build(builder: Builder): 17 | return builder.build() 18 | -------------------------------------------------------------------------------- /quartodoc/inventory.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sphobjinv as soi 4 | 5 | from ._griffe_compat import dataclasses as dc 6 | from plum import dispatch 7 | from quartodoc import layout 8 | 9 | from typing import Union, Callable 10 | 11 | 12 | # Inventory files ============================================================= 13 | # 14 | # inventories have this form: 15 | # { 16 | # "project": "Siuba", "version": "0.4.2", "count": 2, 17 | # "items": [ 18 | # { 19 | # "name": "siuba.dply.verbs.mutate", 20 | # "domain": "py", 21 | # "role": "function", 22 | # "priority": 0, 23 | # "uri": "api/verbs-mutate-transmute/", 24 | # "dispname": "-" 25 | # }, 26 | # ... 27 | # ] 28 | # } 29 | 30 | 31 | # @click.command() 32 | # @click.option("--in-name", help="Name of input inventory file") 33 | # @click.option("--out-name", help="Name of result (defaults to .json)") 34 | def convert_inventory(in_name: "Union[str, soi.Inventory]", out_name=None): 35 | """Convert a sphinx inventory file to json. 36 | 37 | Parameters 38 | ---------- 39 | in_name: str or sphobjinv.Inventory file 40 | Name of inventory file. 41 | out_name: str, optional 42 | Output file name. 43 | 44 | """ 45 | 46 | import json 47 | from pathlib import Path 48 | 49 | if out_name is None: 50 | if isinstance(in_name, str): 51 | out_name = Path(in_name).with_suffix(".json") 52 | else: 53 | raise TypeError() 54 | 55 | if isinstance(in_name, soi.Inventory): 56 | inv = in_name 57 | else: 58 | inv = soi.Inventory(in_name) 59 | 60 | out = _to_clean_dict(inv) 61 | 62 | json.dump(out, open(out_name, "w")) 63 | 64 | 65 | def create_inventory( 66 | project: str, 67 | version: str, 68 | items: "list[dc.Object | dc.Alias]", 69 | uri: "str | Callable[dc.Object, str]" = lambda s: f"{s.canonical_path}.html", 70 | dispname: "str | Callable[dc.Object, str]" = "-", 71 | ) -> soi.Inventory(): 72 | """Return a sphinx inventory file. 73 | 74 | Parameters 75 | ---------- 76 | project: str 77 | Name of the project (often the package name). 78 | version: str 79 | Version of the project (often the package version). 80 | items: str 81 | A docstring parser to use. 82 | uri: 83 | Link relative to the docs where the items documentation lives. 84 | dispname: 85 | Name to be shown when a link to the item is made. 86 | 87 | Examples 88 | -------- 89 | 90 | >>> f_obj = get_object("quartodoc", "create_inventory") 91 | >>> inv = create_inventory("example", "0.0", [f_obj]) 92 | >>> inv 93 | Inventory(project='example', version='0.0', source_type=) 94 | 95 | To preview the inventory, we can convert it to a dictionary: 96 | 97 | >>> _to_clean_dict(inv) 98 | {'project': 'example', 99 | 'version': '0.0', 100 | 'count': 1, 101 | 'items': [{'name': 'quartodoc.create_inventory', 102 | 'domain': 'py', 103 | 'role': 'function', 104 | 'priority': '1', 105 | 'uri': 'quartodoc.create_inventory.html', 106 | 'dispname': '-'}]} 107 | 108 | """ 109 | 110 | inv = soi.Inventory() 111 | inv.project = project 112 | inv.version = version 113 | 114 | soi_items = [_create_inventory_item(x, uri, dispname) for x in items] 115 | 116 | inv.objects.extend(soi_items) 117 | 118 | return inv 119 | 120 | 121 | def _to_clean_dict(inv: soi.Inventory): 122 | """Similar to Inventory.json_dict(), but with a list of items.""" 123 | 124 | obj = inv.json_dict() 125 | 126 | long = list(obj.items()) 127 | meta, entries = long[:3], [v for k, v in long[3:]] 128 | 129 | out = dict(meta) 130 | out["items"] = entries 131 | 132 | return out 133 | 134 | 135 | @dispatch 136 | def _create_inventory_item( 137 | item: Union[dc.Object, dc.Alias], 138 | uri, 139 | dispname="-", 140 | priority="1", 141 | ) -> soi.DataObjStr: 142 | target = item 143 | 144 | return soi.DataObjStr( 145 | name=target.path, 146 | domain="py", 147 | role=target.kind.value, 148 | priority=priority, 149 | uri=_maybe_call(uri, target), 150 | dispname=_maybe_call(dispname, target), 151 | ) 152 | 153 | 154 | @dispatch 155 | def _create_inventory_item( 156 | item: layout.Item, *args, priority="1", **kwargs 157 | ) -> soi.DataObjStr: 158 | return soi.DataObjStr( 159 | name=item.name, 160 | domain="py", 161 | role=item.obj.kind.value, 162 | priority=priority, 163 | uri=item.uri, 164 | dispname=item.dispname or "-", 165 | ) 166 | 167 | 168 | def _maybe_call(s: "str | Callable", obj): 169 | if callable(s): 170 | return s(obj) 171 | elif isinstance(s, str): 172 | return s 173 | 174 | raise TypeError(f"Expected string or callable, received: {type(s)}") 175 | -------------------------------------------------------------------------------- /quartodoc/pandoc/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This pandoc sub-package simpilifies writing pandoc markdown in python. 3 | 4 | It implements the specification that creates markdown that would be 5 | supported by lua [filters](https://pandoc.org/lua-filters.html). Specifically 6 | the [pandoc-module](https://pandoc.org/lua-filters.html#module-pandoc) 7 | """ 8 | -------------------------------------------------------------------------------- /quartodoc/pandoc/components.py: -------------------------------------------------------------------------------- 1 | """ 2 | Specification: https://pandoc.org/lua-filters.html#element-components-1 3 | """ 4 | from __future__ import annotations 5 | 6 | import typing 7 | from dataclasses import dataclass 8 | 9 | if typing.TYPE_CHECKING: 10 | from typing import Optional, Sequence 11 | 12 | __all__ = ("Attr",) 13 | 14 | 15 | @dataclass 16 | class Attr: 17 | """ 18 | Create a new set of attributes (Attr) 19 | """ 20 | 21 | identifier: Optional[str] = None 22 | classes: Optional[Sequence[str]] = None 23 | attributes: Optional[dict[str, str]] = None 24 | 25 | def __str__(self): 26 | """ 27 | Return attr the contents of curly bracked attributes 28 | 29 | e.g. 30 | 31 | #id1 .class1 .class2 width="50%" height="50%" 32 | 33 | not 34 | 35 | {#id1 .class1 .class2 width="50%" height="50%"} 36 | """ 37 | parts = [] 38 | if self.identifier: 39 | parts.append(f"#{self.identifier}") 40 | 41 | if self.classes: 42 | parts.append(" ".join(f".{c}" for c in self.classes)) 43 | 44 | if self.attributes: 45 | parts.append(" ".join(f'{k}="{v}"' for k, v in self.attributes.items())) 46 | 47 | return " ".join(parts) 48 | 49 | @property 50 | def html(self): 51 | """ 52 | Represent Attr as it would appear in an HTML tag 53 | 54 | e.g. 55 | 56 | id="id1" class="class1 class2" width="50%" height="50%" 57 | """ 58 | parts = [] 59 | 60 | if self.identifier: 61 | parts.append(f'id="{self.identifier}"') 62 | 63 | if self.classes: 64 | s = " ".join(c for c in self.classes) 65 | parts.append(f'class="{s}"') 66 | 67 | if self.attributes: 68 | parts.append(" ".join(f'{k}="{v}"' for k, v in self.attributes.items())) 69 | return " ".join(parts) 70 | 71 | @property 72 | def empty(self) -> bool: 73 | """ 74 | Return True if Attr has no content 75 | """ 76 | return not (self.identifier or self.classes or self.attributes) 77 | -------------------------------------------------------------------------------- /quartodoc/pandoc/inlines.py: -------------------------------------------------------------------------------- 1 | """ 2 | Specifition is at https://pandoc.org/lua-filters.html#inline 3 | """ 4 | from __future__ import annotations 5 | 6 | import collections.abc as abc 7 | import sys 8 | 9 | from dataclasses import dataclass 10 | from pathlib import Path 11 | from typing import Optional, Sequence, Union 12 | 13 | if sys.version_info >= (3, 10): 14 | from typing import TypeAlias 15 | else: 16 | TypeAlias = "TypeAlias" 17 | 18 | from quartodoc.pandoc.components import Attr 19 | 20 | __all__ = ( 21 | "Code", 22 | "Emph", 23 | "Image", 24 | "Inline", 25 | "Inlines", 26 | "Link", 27 | "Span", 28 | "Str", 29 | "Strong", 30 | ) 31 | 32 | SEP = " " 33 | 34 | 35 | class Inline: 36 | """ 37 | Base class for inline elements 38 | """ 39 | 40 | def __str__(self): 41 | """ 42 | Return Inline element as markdown 43 | """ 44 | raise NotImplementedError(f"__str__ method not implemented for: {type(self)}") 45 | 46 | @property 47 | def html(self): 48 | """ 49 | Return Inline element as HTML code 50 | 51 | This method is useful for cases where markdown is not versatile 52 | enough for a specific outcome. 53 | """ 54 | raise NotImplementedError( 55 | f"html property method not implemented for: {type(self)}" 56 | ) 57 | 58 | @property 59 | def as_list_item(self): 60 | """ 61 | An inline as a list item 62 | """ 63 | return str_as_list_item(str(self)) 64 | 65 | 66 | # TypeAlias declared here to avoid forward-references which 67 | # break beartype 68 | InlineContentItem = Union[str, Inline, None] 69 | InlineContent: TypeAlias = Union[InlineContentItem, Sequence[InlineContentItem]] 70 | 71 | 72 | @dataclass 73 | class Inlines(Inline): 74 | """ 75 | Sequence of inline elements 76 | """ 77 | 78 | elements: Optional[Sequence[InlineContent]] = None 79 | 80 | def __str__(self): 81 | if not self.elements: 82 | return "" 83 | return join_inline_content(self.elements) 84 | 85 | 86 | @dataclass 87 | class Str(Inline): 88 | """ 89 | A String 90 | """ 91 | 92 | content: Optional[str] = None 93 | 94 | def __str__(self): 95 | return self.content or "" 96 | 97 | 98 | @dataclass 99 | class Span(Inline): 100 | """ 101 | A Span 102 | """ 103 | 104 | content: Optional[InlineContent] = None 105 | attr: Optional[Attr] = None 106 | 107 | def __str__(self): 108 | """ 109 | Return span content as markdown 110 | """ 111 | content = inlinecontent_to_str(self.content) 112 | attr = self.attr or "" 113 | return f"[{content}]{{{attr}}}" 114 | 115 | 116 | @dataclass 117 | class Link(Inline): 118 | """ 119 | A Link 120 | """ 121 | 122 | content: Optional[InlineContent] = None 123 | target: Optional[str] = None 124 | title: Optional[str] = None 125 | attr: Optional[Attr] = None 126 | 127 | def __str__(self): 128 | """ 129 | Return link as markdown 130 | """ 131 | title = f' "{self.title}"' if self.title else "" 132 | content = inlinecontent_to_str(self.content) 133 | attr = f"{{{self.attr}}}" if self.attr else "" 134 | return f"[{content}]({self.target}{title}){attr}" 135 | 136 | 137 | @dataclass 138 | class Code(Inline): 139 | """ 140 | Code (inline) 141 | """ 142 | 143 | text: Optional[str] = None 144 | attr: Optional[Attr] = None 145 | 146 | def __str__(self): 147 | """ 148 | Return link as markdown 149 | """ 150 | content = self.text or "" 151 | attr = f"{{{self.attr}}}" if self.attr else "" 152 | return f"`{content}`{attr}" 153 | 154 | @property 155 | def html(self): 156 | """ 157 | Code (inline) rendered as html 158 | 159 | Notes 160 | ----- 161 | Generates html as if the `--no-highlight` option as passed 162 | to pandoc 163 | """ 164 | content = self.text or "" 165 | attr = f" {self.attr.html}" if self.attr else "" 166 | return f"{content}" 167 | 168 | 169 | @dataclass 170 | class Strong(Inline): 171 | """ 172 | Strongly emphasized text 173 | """ 174 | 175 | content: Optional[InlineContent] = None 176 | 177 | def __str__(self): 178 | """ 179 | Return link as markdown 180 | """ 181 | if not self.content: 182 | return "" 183 | 184 | content = inlinecontent_to_str(self.content) 185 | return f"**{content}**" 186 | 187 | 188 | @dataclass 189 | class Emph(Inline): 190 | """ 191 | Emphasized text 192 | """ 193 | 194 | content: Optional[InlineContent] = None 195 | 196 | def __str__(self): 197 | """ 198 | Return link as markdown 199 | """ 200 | if not self.content: 201 | return "" 202 | 203 | content = inlinecontent_to_str(self.content) 204 | return f"*{content}*" 205 | 206 | 207 | @dataclass 208 | class Image(Inline): 209 | """ 210 | Image 211 | """ 212 | 213 | caption: Optional[str] = None 214 | src: Optional[Path | str] = None 215 | title: Optional[str] = None 216 | attr: Optional[Attr] = None 217 | 218 | def __str__(self): 219 | """ 220 | Return image as markdown 221 | """ 222 | caption = self.caption or "" 223 | src = self.src or "" 224 | title = f' "{self.title}"' if self.title else "" 225 | attr = f"{{{self.attr}}}" if self.attr else "" 226 | return f"![{caption}]({src}{title}){attr}" 227 | 228 | 229 | # Helper functions 230 | 231 | 232 | def join_inline_content(content: Sequence[InlineContent]) -> str: 233 | """ 234 | Join a sequence of inlines into one string 235 | """ 236 | return SEP.join(inlinecontent_to_str(c) for c in content if c) 237 | 238 | 239 | def inlinecontent_to_str(content: Optional[InlineContent]): 240 | """ 241 | Covert inline content to a string 242 | 243 | A single item block is converted to a string. 244 | A block sequence is coverted to a string of strings with a 245 | space separating the string for each item in the sequence. 246 | """ 247 | if not content: 248 | return "" 249 | elif isinstance(content, (str, Inline)): 250 | return str(content) 251 | elif isinstance(content, abc.Sequence): 252 | return join_inline_content(content) 253 | else: 254 | raise TypeError(f"Could not process type: {type(content)}") 255 | 256 | 257 | def str_as_list_item(s: str) -> str: 258 | """ 259 | How a string becomes a list item 260 | """ 261 | return f"{s}\n" 262 | -------------------------------------------------------------------------------- /quartodoc/parsers.py: -------------------------------------------------------------------------------- 1 | DEFAULT_OPTIONS = { 2 | "numpy": { 3 | "allow_section_blank_line": True, 4 | } 5 | } 6 | 7 | 8 | def get_parser_defaults(name: str): 9 | return DEFAULT_OPTIONS.get(name, {}) 10 | -------------------------------------------------------------------------------- /quartodoc/renderers/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from .base import Renderer 4 | from .md_renderer import MdRenderer 5 | -------------------------------------------------------------------------------- /quartodoc/renderers/base.py: -------------------------------------------------------------------------------- 1 | import re 2 | import typing 3 | 4 | from plum import dispatch 5 | 6 | if typing.TYPE_CHECKING: 7 | from ..autosummary import Builder 8 | 9 | # utils ----------------------------------------------------------------------- 10 | 11 | 12 | def escape(val: str): 13 | return f"`{val}`" 14 | 15 | 16 | def sanitize(val: str, allow_markdown=False, escape_quotes=False): 17 | # sanitize common tokens that break tables 18 | res = val.replace("\n", " ").replace("|", "\\|") 19 | 20 | # sanitize elements that get turned into smart quotes 21 | # this is to avoid defaults that are strings having their 22 | # quotes screwed up. 23 | if escape_quotes: 24 | res = res.replace("'", r"\'").replace('"', r"\"") 25 | 26 | # sanitize elements that can get interpreted as markdown links 27 | # or citations 28 | if not allow_markdown: 29 | return res.replace("[", "\\[").replace("]", "\\]") 30 | 31 | return res 32 | 33 | 34 | def convert_rst_link_to_md(rst): 35 | expr = ( 36 | r"((:external(\+[a-zA-Z\._]+))?(:[a-zA-Z\._]+)?:[a-zA-Z\._]+:`~?[a-zA-Z\._]+`)" 37 | ) 38 | 39 | return re.sub(expr, r"[](\1)", rst, flags=re.MULTILINE) 40 | 41 | 42 | # render ----------------------------------------------------------------------- 43 | 44 | 45 | class Renderer: 46 | style: str 47 | _registry: "dict[str, Renderer]" = {} 48 | 49 | def __init_subclass__(cls, **kwargs): 50 | super().__init_subclass__(**kwargs) 51 | 52 | if cls.style in cls._registry: 53 | raise KeyError(f"A builder for style {cls.style} already exists") 54 | 55 | cls._registry[cls.style] = cls 56 | 57 | @classmethod 58 | def from_config(cls, cfg: "dict | Renderer | str"): 59 | if isinstance(cfg, Renderer): 60 | return cfg 61 | elif isinstance(cfg, str): 62 | style, cfg = cfg, {} 63 | elif isinstance(cfg, dict): 64 | style = cfg["style"] 65 | cfg = {k: v for k, v in cfg.items() if k != "style"} 66 | else: 67 | raise TypeError(type(cfg)) 68 | 69 | if style.endswith(".py"): 70 | import os 71 | import sys 72 | import importlib 73 | 74 | # temporarily add the current directory to sys path and import 75 | # this ensures that even if we're executing the quartodoc cli, 76 | # we can import a custom _renderer.py file. 77 | # it probably isn't ideal, but seems like a more convenient 78 | # option than requiring users to register entrypoints. 79 | sys.path.append(os.getcwd()) 80 | 81 | try: 82 | mod = importlib.import_module(style.rsplit(".", 1)[0]) 83 | return mod.Renderer(**cfg) 84 | finally: 85 | sys.path.pop() 86 | 87 | subclass = cls._registry[style] 88 | return subclass(**cfg) 89 | 90 | @dispatch 91 | def render(self, el): 92 | raise NotImplementedError(f"render method does not support type: {type(el)}") 93 | 94 | def _pages_written(self, builder: "Builder"): 95 | """ 96 | Called after all the qmd pages have been render and written to disk 97 | 98 | It is called before the documented items are written to an inventory 99 | file. This is a chance for the renderer to add to the documented items 100 | and write the pages to them to disk. 101 | 102 | Parameters 103 | ---------- 104 | builder : 105 | There builder using this renderer to generate documentation. 106 | 107 | Notes 108 | ----- 109 | This method is provided for experimental purposes and it is not bound 110 | to be available for long, or have the same form. 111 | """ 112 | ... 113 | -------------------------------------------------------------------------------- /quartodoc/static/styles.css: -------------------------------------------------------------------------------- 1 | /* styles for parameter tables, etc.. ---- 2 | */ 3 | 4 | .doc-section dt code { 5 | background: none; 6 | } 7 | 8 | .doc-section dt { 9 | /* background-color: lightyellow; */ 10 | display: block; 11 | } 12 | 13 | .doc-section dl dd { 14 | margin-left: 3rem; 15 | } 16 | -------------------------------------------------------------------------------- /quartodoc/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machow/quartodoc/70aec122d550b1d78970c2368779f47594decf69/quartodoc/tests/__init__.py -------------------------------------------------------------------------------- /quartodoc/tests/__snapshots__/test_builder.ambr: -------------------------------------------------------------------------------- 1 | # serializer version: 1 2 | # name: test_builder_generate_sidebar 3 | ''' 4 | website: 5 | sidebar: 6 | - contents: 7 | - reference/index.qmd 8 | - contents: 9 | - reference/a_func.qmd 10 | section: first section 11 | - contents: 12 | - contents: 13 | - reference/a_attr.qmd 14 | section: a subsection 15 | section: second section 16 | id: reference 17 | - id: dummy-sidebar 18 | 19 | ''' 20 | # --- 21 | # name: test_builder_generate_sidebar_options 22 | ''' 23 | website: 24 | sidebar: 25 | - contents: 26 | - href: introduction.qmd 27 | text: Introduction 28 | - contents: 29 | - reference/index.qmd 30 | - contents: 31 | - reference/a_func.qmd 32 | section: first section 33 | - contents: 34 | - contents: 35 | - reference/a_attr.qmd 36 | section: a subsection 37 | section: second section 38 | section: Reference 39 | - href: basics-summary.qmd 40 | text: Basics 41 | id: reference 42 | search: true 43 | style: docked 44 | - id: dummy-sidebar 45 | 46 | ''' 47 | # --- 48 | -------------------------------------------------------------------------------- /quartodoc/tests/__snapshots__/test_validation.ambr: -------------------------------------------------------------------------------- 1 | # serializer version: 1 2 | # name: test_misplaced_kindpage 3 | ''' 4 | Code: 5 | 6 | quartodoc: 7 | package: zzz 8 | sections: 9 | - kind: page 10 | 11 | 12 | Error: 13 | Configuration error for YAML: 14 | - Missing field `path` for element 0 in the list for `sections`, which you need when setting `kind: page`. 15 | 16 | ''' 17 | # --- 18 | # name: test_missing_name_contents 19 | ''' 20 | Code: 21 | 22 | quartodoc: 23 | package: zzz 24 | sections: 25 | - title: Section 1 26 | - title: Section 2 27 | contents: 28 | 29 | # name missing here ---- 30 | - children: linked 31 | 32 | - name: MdRenderer 33 | 34 | Error: 35 | Configuration error for YAML: 36 | - Missing field `name` for element 0 in the list for `contents` located in element 1 in the list for `sections` 37 | 38 | ''' 39 | # --- 40 | -------------------------------------------------------------------------------- /quartodoc/tests/example.py: -------------------------------------------------------------------------------- 1 | """A module""" 2 | 3 | # flake8: noqa 4 | 5 | from quartodoc.tests.example_alias_target import ( 6 | alias_target as a_alias, 7 | nested_alias_target as a_nested_alias, 8 | ) 9 | 10 | 11 | def a_func(): 12 | """A function""" 13 | 14 | 15 | a_attr = 1 16 | """An attribute""" 17 | 18 | 19 | class AClass: 20 | """A class""" 21 | 22 | a_attr = 1 23 | """A class attribute""" 24 | 25 | def a_method(): 26 | """A method""" 27 | -------------------------------------------------------------------------------- /quartodoc/tests/example_alias_target.py: -------------------------------------------------------------------------------- 1 | from quartodoc.tests.example_alias_target__nested import ( # noqa: F401 2 | nested_alias_target, 3 | NestedClass as ClassAlias, 4 | tabulate as external_alias, 5 | ) 6 | 7 | 8 | def alias_target(): 9 | """An alias target""" 10 | 11 | 12 | class AClass: 13 | some_method = nested_alias_target 14 | -------------------------------------------------------------------------------- /quartodoc/tests/example_alias_target__nested.py: -------------------------------------------------------------------------------- 1 | """ 2 | This function gets imported in example_alias_target, and from there imported into example. 3 | """ 4 | 5 | from tabulate import tabulate # noqa: F401 6 | 7 | 8 | def nested_alias_target(): 9 | """A nested alias target""" 10 | 11 | 12 | class Parent: 13 | def parent_method(self): 14 | """p1 method""" 15 | 16 | 17 | class NestedClass(Parent): 18 | def f(self): 19 | """a method""" 20 | 21 | manually_attached = nested_alias_target 22 | -------------------------------------------------------------------------------- /quartodoc/tests/example_attribute.py: -------------------------------------------------------------------------------- 1 | a: int = 1 2 | """I am an attribute docstring""" 3 | 4 | 5 | class SomeClass: 6 | a: int = 1 7 | """I am a class attribute docstring""" 8 | -------------------------------------------------------------------------------- /quartodoc/tests/example_class.py: -------------------------------------------------------------------------------- 1 | class C: 2 | """The short summary. 3 | 4 | The extended summary, 5 | which may be multiple lines. 6 | 7 | Parameters 8 | ---------- 9 | x: 10 | Uses signature type. 11 | y: int 12 | Uses manual type. 13 | 14 | """ 15 | 16 | SOME_ATTRIBUTE: float 17 | """An attribute""" 18 | 19 | def __init__(self, x: str, y: int): 20 | self.x = x 21 | self.y = y 22 | self.z: int = 1 23 | """A documented init attribute""" 24 | 25 | def some_method(self): 26 | """A method""" 27 | 28 | @property 29 | def some_property(self): 30 | """A property""" 31 | 32 | @classmethod 33 | def some_class_method(cls): 34 | """A class method""" 35 | 36 | class D: 37 | """A nested class""" 38 | 39 | 40 | class Child(C): 41 | def some_new_method(self): 42 | """A new method""" 43 | 44 | 45 | class AttributesTable: 46 | """The short summary. 47 | 48 | Attributes 49 | ---------- 50 | x: 51 | Uses signature type 52 | y: int 53 | Uses manual type 54 | z: 55 | Defined in init 56 | """ 57 | 58 | x: str 59 | y: int 60 | """This docstring should not be used""" 61 | 62 | def __init__(self): 63 | self.z: float = 1 64 | -------------------------------------------------------------------------------- /quartodoc/tests/example_docstring_styles.py: -------------------------------------------------------------------------------- 1 | def f_google(a, b: str): 2 | """A google style docstring. 3 | 4 | Args: 5 | a (int): The a parameter. 6 | b: The b parameter. 7 | 8 | Custom Admonition: 9 | Some text. 10 | """ 11 | 12 | 13 | def f_sphinx(a, b: str): 14 | """A sphinx style docstring. 15 | 16 | :param a: The a parameter. 17 | :type a: int 18 | :param b: The b parameter. 19 | """ 20 | 21 | 22 | def f_numpy(a, b: str): 23 | """A numpy style docstring. 24 | 25 | Parameters 26 | ---------- 27 | a: int 28 | The a parameter. 29 | b: 30 | The b parameter. 31 | 32 | Custom Admonition 33 | ----------------- 34 | Some text. 35 | """ 36 | 37 | 38 | # we set an option by default in griffe's numpy parsing 39 | # to allow linebreaks in parameter tables 40 | def f_numpy_with_linebreaks(a, b: str): 41 | """A numpy style docstring. 42 | 43 | Parameters 44 | ---------- 45 | a: int 46 | The a parameter. 47 | 48 | b: 49 | The b parameter. 50 | """ 51 | -------------------------------------------------------------------------------- /quartodoc/tests/example_dynamic.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | NOTE = "Notes\n----\nI am a note" 4 | 5 | 6 | a: int 7 | """The a module attribute""" 8 | 9 | b: str = "2" 10 | """The b module attribute""" 11 | 12 | 13 | def f(a, b, c): 14 | """Return something 15 | 16 | {note} 17 | """ 18 | 19 | 20 | f.__doc__ = f.__doc__.format(note=NOTE) 21 | 22 | 23 | class AClass: 24 | def simple(self, x): 25 | """A simple method""" 26 | 27 | def dynamic_doc(self, x): 28 | ... 29 | 30 | dynamic_doc.__doc__ = """A dynamic method""" 31 | 32 | # note that we could use the partialmethod, but I am not sure how to 33 | # correctly set its __doc__ attribute in that case. 34 | dynamic_create = partial(dynamic_doc, x=1) 35 | dynamic_create.__doc__ = dynamic_doc.__doc__ 36 | 37 | 38 | class InstanceAttrs: 39 | """Some InstanceAttrs class""" 40 | 41 | z: int 42 | """The z attribute""" 43 | 44 | def __init__(self, a: int, b: str): 45 | self.a = a 46 | self.b = b 47 | """The b attribute""" 48 | 49 | 50 | some_instance = InstanceAttrs(1, 1) 51 | some_instance.__doc__ = "Dynamic instance doc" 52 | -------------------------------------------------------------------------------- /quartodoc/tests/example_interlinks/.gitignore: -------------------------------------------------------------------------------- 1 | /.quarto/ 2 | _site 3 | -------------------------------------------------------------------------------- /quartodoc/tests/example_interlinks/README.md: -------------------------------------------------------------------------------- 1 | # Interlinks test example 2 | 3 | This folder contains a full interlinks setup, along with a test specification ([`./spec.yml`](./spec.yml)). -------------------------------------------------------------------------------- /quartodoc/tests/example_interlinks/_inv/other_objects.json: -------------------------------------------------------------------------------- 1 | {"project": "other", "version": "0.0.999", "count": 1, "items": [{"name": "quartodoc.get_object", "domain": "py", "role": "function", "priority": "1", "uri": "api/get_object.html#quartodoc.get_object", "dispname": "-"}]} -------------------------------------------------------------------------------- /quartodoc/tests/example_interlinks/_quarto.yml: -------------------------------------------------------------------------------- 1 | filters: 2 | - interlinks.lua 3 | 4 | interlinks: 5 | sources: 6 | other: 7 | # note that url is usually the http address it is 8 | # fetched from, but we generate the inventory for 9 | # this source manually. 10 | url: "other+" 11 | -------------------------------------------------------------------------------- /quartodoc/tests/example_interlinks/interlinks.lua: -------------------------------------------------------------------------------- 1 | ../../../_extensions/interlinks/interlinks.lua -------------------------------------------------------------------------------- /quartodoc/tests/example_interlinks/objects.json: -------------------------------------------------------------------------------- 1 | {"project": "quartodoc", "version": "0.0.999", "count": 4, "items": [{"name": "quartodoc.layout", "domain": "py", "role": "module", "priority": "1", "uri": "api/layout.html#quartodoc.layout", "dispname": "-"}, {"name": "quartodoc.MdRenderer.style", "domain": "py", "role": "attribute", "priority": "1", "uri": "api/MdRenderer.html#quartodoc.MdRenderer.style", "dispname": "-"}, {"name": "quartodoc.MdRenderer.render", "domain": "py", "role": "function", "priority": "1", "uri": "api/MdRenderer.html#quartodoc.MdRenderer.render", "dispname": "-"}, {"name": "quartodoc.MdRenderer", "domain": "py", "role": "class", "priority": "1", "uri": "api/MdRenderer.html#quartodoc.MdRenderer", "dispname": "-"}]} -------------------------------------------------------------------------------- /quartodoc/tests/example_interlinks/spec.yml: -------------------------------------------------------------------------------- 1 | # Specification for testing an interlinks filter. 2 | # Each entry in this spec is a test case, and may contain the following fields: 3 | # * input: the raw interlink text in the qmd 4 | # * output_link: expect a Link with this target 5 | # * output_text: expect a Link with this content 6 | # * output_element: expect a custom element.. 7 | # - kind: the element kind (Link, Code, Unchanged) 8 | # - 9 | # * error: expect an error with this name 10 | # * warning: expect a warning with this name 11 | # roles ----------------------------------------------------------------------- 12 | # can look up module 13 | - input: "[](`quartodoc.layout`)" 14 | output_link: /api/layout.html#quartodoc.layout 15 | output_text: quartodoc.layout 16 | 17 | # can look up class 18 | - input: "[](`quartodoc.MdRenderer`)" 19 | output_link: /api/MdRenderer.html#quartodoc.MdRenderer 20 | output_text: quartodoc.MdRenderer 21 | 22 | # can look up function 23 | - input: "[](`quartodoc.MdRenderer.render`)" 24 | output_link: /api/MdRenderer.html#quartodoc.MdRenderer.render 25 | output_text: quartodoc.MdRenderer.render 26 | 27 | # can look up attribute 28 | - input: "[](`quartodoc.MdRenderer.style`)" 29 | output_link: /api/MdRenderer.html#quartodoc.MdRenderer.style 30 | output_text: quartodoc.MdRenderer.style 31 | 32 | # reference syntax ------------------------------------------------------------ 33 | # errors for invalid reference syntax (missing backtick) 34 | - input: "[](`quartodoc.layout)" 35 | error: "RefSyntaxError" 36 | 37 | # passes through normal links 38 | - input: "[](http://example.com)" 39 | output_element: 40 | kind: Unchanged 41 | content: http://example.com 42 | 43 | # reference text styles ------------------------------------------------------- 44 | # ref with custom text 45 | - input: "[some explanation](`quartodoc.layout`)" 46 | output_link: /api/layout.html#quartodoc.layout 47 | output_text: some explanation 48 | 49 | # ref with no text defaults to name 50 | - input: "[](`quartodoc.layout`)" 51 | output_link: /api/layout.html#quartodoc.layout 52 | output_text: quartodoc.layout 53 | 54 | # ref with tilde uses short name 55 | - input: "[](`~quartodoc.layout`)" 56 | output_text: layout 57 | 58 | # filters --------------------------------------------------------------------- 59 | # filters by function role 60 | - input: "[](:function:`quartodoc.MdRenderer.render`)" 61 | output_link: /api/MdRenderer.html#quartodoc.MdRenderer.render 62 | output_text: quartodoc.MdRenderer.render 63 | 64 | # filters by shorthand func role 65 | - input: "[](:func:`quartodoc.MdRenderer.render`)" 66 | output_link: /api/MdRenderer.html#quartodoc.MdRenderer.render 67 | output_text: quartodoc.MdRenderer.render 68 | 69 | # filters by attribute role 70 | - input: "[](:attribute:`quartodoc.MdRenderer.style`)" 71 | output_link: /api/MdRenderer.html#quartodoc.MdRenderer.style 72 | output_text: quartodoc.MdRenderer.style 73 | 74 | # filters by class role 75 | - input: "[](:class:`quartodoc.MdRenderer`)" 76 | output_link: /api/MdRenderer.html#quartodoc.MdRenderer 77 | output_text: quartodoc.MdRenderer 78 | 79 | # filters by module role 80 | - input: "[](:module:`quartodoc.layout`)" 81 | output_link: /api/layout.html#quartodoc.layout 82 | output_text: quartodoc.layout 83 | 84 | # filters by domain and role 85 | - input: "[](:py:module:`quartodoc.layout`)" 86 | output_link: /api/layout.html#quartodoc.layout 87 | output_text: quartodoc.layout 88 | 89 | # filters by external, domain, and role 90 | - input: "[](:external+other:py:function:`quartodoc.get_object`)" 91 | output_link: other+api/get_object.html#quartodoc.get_object 92 | output_text: quartodoc.get_object 93 | 94 | # warns for a missing entry in external inventory 95 | - input: "[](:external+other:`quartodoc.layout`)" 96 | output_element: 97 | kind: Code 98 | content: ":external+other:`quartodoc.layout`" 99 | warning: "InvLookupError" 100 | 101 | # warns for an attribute filter that is really a function 102 | - input: "[](:attribute:`quartodoc.MdRenderer.render`)" 103 | warning: "InvLookupError" 104 | 105 | # warns for it fails look up when module role is written as mod 106 | - input: "[](:mod:`quartodoc.layout`)" 107 | warning: "InvLookupError" 108 | -------------------------------------------------------------------------------- /quartodoc/tests/example_interlinks/test.md: -------------------------------------------------------------------------------- 1 | 2 | ## `` [](`quartodoc.layout`) `` 3 | 4 | output: [`quartodoc.layout`](/api/layout.html#quartodoc.layout) 5 | 6 | █─TestSpecEntry 7 | ├─input = '[](`quartodoc.layout`)' 8 | ├─output_text = 'quartodoc.layout' 9 | └─output_link = '/api/layout.html#quartodoc.layout' 10 | 11 | ## `` [](`quartodoc.MdRenderer`) `` 12 | 13 | output: 14 | [`quartodoc.MdRenderer`](/api/MdRenderer.html#quartodoc.MdRenderer) 15 | 16 | █─TestSpecEntry 17 | ├─input = '[](`quartodoc.MdRenderer`)' 18 | ├─output_text = 'quartodoc.MdRenderer' 19 | └─output_link = '/api/MdRenderer.html#quartodoc.MdRenderer' 20 | 21 | ## `` [](`quartodoc.MdRenderer.render`) `` 22 | 23 | output: 24 | [`quartodoc.MdRenderer.render`](/api/MdRenderer.html#quartodoc.MdRenderer.render) 25 | 26 | █─TestSpecEntry 27 | ├─input = '[](`quartodoc.MdRenderer.render`)' 28 | ├─output_text = 'quartodoc.MdRenderer.render' 29 | └─output_link = '/api/MdRenderer.html#quartodoc.MdRenderer.render' 30 | 31 | ## `` [](`quartodoc.MdRenderer.style`) `` 32 | 33 | output: 34 | [`quartodoc.MdRenderer.style`](/api/MdRenderer.html#quartodoc.MdRenderer.style) 35 | 36 | █─TestSpecEntry 37 | ├─input = '[](`quartodoc.MdRenderer.style`)' 38 | ├─output_text = 'quartodoc.MdRenderer.style' 39 | └─output_link = '/api/MdRenderer.html#quartodoc.MdRenderer.style' 40 | 41 | ## `` [](`quartodoc.layout) `` 42 | 43 | output: `` 44 | 45 | █─TestSpecEntry 46 | ├─input = '[](`quartodoc.layout)' 47 | └─error = 'RefSyntaxError' 48 | 49 | ## `[](http://example.com)` 50 | 51 | output: [](http://example.com) 52 | 53 | █─TestSpecEntry 54 | ├─input = '[](http://example.com)' 55 | └─output_element = █─Unchanged 56 | └─content = 'http://example.com' 57 | 58 | ## `` [some explanation](`quartodoc.layout`) `` 59 | 60 | output: [some explanation](/api/layout.html#quartodoc.layout) 61 | 62 | █─TestSpecEntry 63 | ├─input = '[some explanation](`quartodoc.layout`)' 64 | ├─output_text = 'some explanation' 65 | └─output_link = '/api/layout.html#quartodoc.layout' 66 | 67 | ## `` [](`quartodoc.layout`) `` 68 | 69 | output: [`quartodoc.layout`](/api/layout.html#quartodoc.layout) 70 | 71 | █─TestSpecEntry 72 | ├─input = '[](`quartodoc.layout`)' 73 | ├─output_text = 'quartodoc.layout' 74 | └─output_link = '/api/layout.html#quartodoc.layout' 75 | 76 | ## `` [](`~quartodoc.layout`) `` 77 | 78 | output: [`layout`](/api/layout.html#quartodoc.layout) 79 | 80 | █─TestSpecEntry 81 | ├─input = '[](`~quartodoc.layout`)' 82 | └─output_text = 'layout' 83 | 84 | ## `` [](:function:`quartodoc.MdRenderer.render`) `` 85 | 86 | output: 87 | [`quartodoc.MdRenderer.render`](/api/MdRenderer.html#quartodoc.MdRenderer.render) 88 | 89 | █─TestSpecEntry 90 | ├─input = '[](:function:`quartodoc.MdRenderer.render`)' 91 | ├─output_text = 'quartodoc.MdRenderer.render' 92 | └─output_link = '/api/MdRenderer.html#quartodoc.MdRenderer.render' 93 | 94 | ## `` [](:func:`quartodoc.MdRenderer.render`) `` 95 | 96 | output: 97 | [`quartodoc.MdRenderer.render`](/api/MdRenderer.html#quartodoc.MdRenderer.render) 98 | 99 | █─TestSpecEntry 100 | ├─input = '[](:func:`quartodoc.MdRenderer.render`)' 101 | ├─output_text = 'quartodoc.MdRenderer.render' 102 | └─output_link = '/api/MdRenderer.html#quartodoc.MdRenderer.render' 103 | 104 | ## `` [](:attribute:`quartodoc.MdRenderer.style`) `` 105 | 106 | output: 107 | [`quartodoc.MdRenderer.style`](/api/MdRenderer.html#quartodoc.MdRenderer.style) 108 | 109 | █─TestSpecEntry 110 | ├─input = '[](:attribute:`quartodoc.MdRenderer.style`)' 111 | ├─output_text = 'quartodoc.MdRenderer.style' 112 | └─output_link = '/api/MdRenderer.html#quartodoc.MdRenderer.style' 113 | 114 | ## `` [](:class:`quartodoc.MdRenderer`) `` 115 | 116 | output: 117 | [`quartodoc.MdRenderer`](/api/MdRenderer.html#quartodoc.MdRenderer) 118 | 119 | █─TestSpecEntry 120 | ├─input = '[](:class:`quartodoc.MdRenderer`)' 121 | ├─output_text = 'quartodoc.MdRenderer' 122 | └─output_link = '/api/MdRenderer.html#quartodoc.MdRenderer' 123 | 124 | ## `` [](:module:`quartodoc.layout`) `` 125 | 126 | output: [`quartodoc.layout`](/api/layout.html#quartodoc.layout) 127 | 128 | █─TestSpecEntry 129 | ├─input = '[](:module:`quartodoc.layout`)' 130 | ├─output_text = 'quartodoc.layout' 131 | └─output_link = '/api/layout.html#quartodoc.layout' 132 | 133 | ## `` [](:py:module:`quartodoc.layout`) `` 134 | 135 | output: [`quartodoc.layout`](/api/layout.html#quartodoc.layout) 136 | 137 | █─TestSpecEntry 138 | ├─input = '[](:py:module:`quartodoc.layout`)' 139 | ├─output_text = 'quartodoc.layout' 140 | └─output_link = '/api/layout.html#quartodoc.layout' 141 | 142 | ## `` [](:external+other:py:function:`quartodoc.get_object`) `` 143 | 144 | output: 145 | [`quartodoc.get_object`](other+api/get_object.html#quartodoc.get_object) 146 | 147 | █─TestSpecEntry 148 | ├─input = '[](:external+other:py:function:`quartodoc.get_obj ... 149 | ├─output_text = 'quartodoc.get_object' 150 | └─output_link = 'other+api/get_object.html#quartodoc.get_object' 151 | 152 | ## `` [](:external+other:`quartodoc.layout`) `` 153 | 154 | output: `quartodoc.layout` 155 | 156 | █─TestSpecEntry 157 | ├─input = '[](:external+other:`quartodoc.layout`)' 158 | ├─output_element = █─Code 159 | │ └─content = ':external+other:`quartodoc.layout`' 160 | └─warning = 'InvLookupError' 161 | 162 | ## `` [](:attribute:`quartodoc.MdRenderer.render`) `` 163 | 164 | output: `quartodoc.MdRenderer.render` 165 | 166 | █─TestSpecEntry 167 | ├─input = '[](:attribute:`quartodoc.MdRenderer.render`)' 168 | └─warning = 'InvLookupError' 169 | 170 | ## `` [](:mod:`quartodoc.layout`) `` 171 | 172 | output: `quartodoc.layout` 173 | 174 | █─TestSpecEntry 175 | ├─input = '[](:mod:`quartodoc.layout`)' 176 | └─warning = 'InvLookupError' 177 | -------------------------------------------------------------------------------- /quartodoc/tests/example_interlinks/test.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | format: 3 | gfm 4 | filters: 5 | - interlinks.lua 6 | --- 7 | 8 | ## `` [](`quartodoc.layout`) `` 9 | 10 | output: [](`quartodoc.layout`) 11 | 12 | ``` 13 | █─TestSpecEntry 14 | ├─input = '[](`quartodoc.layout`)' 15 | ├─output_text = 'quartodoc.layout' 16 | └─output_link = '/api/layout.html#quartodoc.layout' 17 | ``` 18 | 19 | 20 | 21 | 22 | ## `` [](`quartodoc.MdRenderer`) `` 23 | 24 | output: [](`quartodoc.MdRenderer`) 25 | 26 | ``` 27 | █─TestSpecEntry 28 | ├─input = '[](`quartodoc.MdRenderer`)' 29 | ├─output_text = 'quartodoc.MdRenderer' 30 | └─output_link = '/api/MdRenderer.html#quartodoc.MdRenderer' 31 | ``` 32 | 33 | 34 | 35 | 36 | ## `` [](`quartodoc.MdRenderer.render`) `` 37 | 38 | output: [](`quartodoc.MdRenderer.render`) 39 | 40 | ``` 41 | █─TestSpecEntry 42 | ├─input = '[](`quartodoc.MdRenderer.render`)' 43 | ├─output_text = 'quartodoc.MdRenderer.render' 44 | └─output_link = '/api/MdRenderer.html#quartodoc.MdRenderer.render' 45 | ``` 46 | 47 | 48 | 49 | 50 | ## `` [](`quartodoc.MdRenderer.style`) `` 51 | 52 | output: [](`quartodoc.MdRenderer.style`) 53 | 54 | ``` 55 | █─TestSpecEntry 56 | ├─input = '[](`quartodoc.MdRenderer.style`)' 57 | ├─output_text = 'quartodoc.MdRenderer.style' 58 | └─output_link = '/api/MdRenderer.html#quartodoc.MdRenderer.style' 59 | ``` 60 | 61 | 62 | 63 | 64 | ## `` [](`quartodoc.layout) `` 65 | 66 | output: [](`quartodoc.layout) 67 | 68 | ``` 69 | █─TestSpecEntry 70 | ├─input = '[](`quartodoc.layout)' 71 | └─error = 'RefSyntaxError' 72 | ``` 73 | 74 | 75 | 76 | 77 | ## `` [](http://example.com) `` 78 | 79 | output: [](http://example.com) 80 | 81 | ``` 82 | █─TestSpecEntry 83 | ├─input = '[](http://example.com)' 84 | └─output_element = █─Unchanged 85 | └─content = 'http://example.com' 86 | ``` 87 | 88 | 89 | 90 | 91 | ## `` [some explanation](`quartodoc.layout`) `` 92 | 93 | output: [some explanation](`quartodoc.layout`) 94 | 95 | ``` 96 | █─TestSpecEntry 97 | ├─input = '[some explanation](`quartodoc.layout`)' 98 | ├─output_text = 'some explanation' 99 | └─output_link = '/api/layout.html#quartodoc.layout' 100 | ``` 101 | 102 | 103 | 104 | 105 | ## `` [](`quartodoc.layout`) `` 106 | 107 | output: [](`quartodoc.layout`) 108 | 109 | ``` 110 | █─TestSpecEntry 111 | ├─input = '[](`quartodoc.layout`)' 112 | ├─output_text = 'quartodoc.layout' 113 | └─output_link = '/api/layout.html#quartodoc.layout' 114 | ``` 115 | 116 | 117 | 118 | 119 | ## `` [](`~quartodoc.layout`) `` 120 | 121 | output: [](`~quartodoc.layout`) 122 | 123 | ``` 124 | █─TestSpecEntry 125 | ├─input = '[](`~quartodoc.layout`)' 126 | └─output_text = 'layout' 127 | ``` 128 | 129 | 130 | 131 | 132 | ## `` [](:function:`quartodoc.MdRenderer.render`) `` 133 | 134 | output: [](:function:`quartodoc.MdRenderer.render`) 135 | 136 | ``` 137 | █─TestSpecEntry 138 | ├─input = '[](:function:`quartodoc.MdRenderer.render`)' 139 | ├─output_text = 'quartodoc.MdRenderer.render' 140 | └─output_link = '/api/MdRenderer.html#quartodoc.MdRenderer.render' 141 | ``` 142 | 143 | 144 | 145 | 146 | ## `` [](:func:`quartodoc.MdRenderer.render`) `` 147 | 148 | output: [](:func:`quartodoc.MdRenderer.render`) 149 | 150 | ``` 151 | █─TestSpecEntry 152 | ├─input = '[](:func:`quartodoc.MdRenderer.render`)' 153 | ├─output_text = 'quartodoc.MdRenderer.render' 154 | └─output_link = '/api/MdRenderer.html#quartodoc.MdRenderer.render' 155 | ``` 156 | 157 | 158 | 159 | 160 | ## `` [](:attribute:`quartodoc.MdRenderer.style`) `` 161 | 162 | output: [](:attribute:`quartodoc.MdRenderer.style`) 163 | 164 | ``` 165 | █─TestSpecEntry 166 | ├─input = '[](:attribute:`quartodoc.MdRenderer.style`)' 167 | ├─output_text = 'quartodoc.MdRenderer.style' 168 | └─output_link = '/api/MdRenderer.html#quartodoc.MdRenderer.style' 169 | ``` 170 | 171 | 172 | 173 | 174 | ## `` [](:class:`quartodoc.MdRenderer`) `` 175 | 176 | output: [](:class:`quartodoc.MdRenderer`) 177 | 178 | ``` 179 | █─TestSpecEntry 180 | ├─input = '[](:class:`quartodoc.MdRenderer`)' 181 | ├─output_text = 'quartodoc.MdRenderer' 182 | └─output_link = '/api/MdRenderer.html#quartodoc.MdRenderer' 183 | ``` 184 | 185 | 186 | 187 | 188 | ## `` [](:module:`quartodoc.layout`) `` 189 | 190 | output: [](:module:`quartodoc.layout`) 191 | 192 | ``` 193 | █─TestSpecEntry 194 | ├─input = '[](:module:`quartodoc.layout`)' 195 | ├─output_text = 'quartodoc.layout' 196 | └─output_link = '/api/layout.html#quartodoc.layout' 197 | ``` 198 | 199 | 200 | 201 | 202 | ## `` [](:py:module:`quartodoc.layout`) `` 203 | 204 | output: [](:py:module:`quartodoc.layout`) 205 | 206 | ``` 207 | █─TestSpecEntry 208 | ├─input = '[](:py:module:`quartodoc.layout`)' 209 | ├─output_text = 'quartodoc.layout' 210 | └─output_link = '/api/layout.html#quartodoc.layout' 211 | ``` 212 | 213 | 214 | 215 | 216 | ## `` [](:external+other:py:function:`quartodoc.get_object`) `` 217 | 218 | output: [](:external+other:py:function:`quartodoc.get_object`) 219 | 220 | ``` 221 | █─TestSpecEntry 222 | ├─input = '[](:external+other:py:function:`quartodoc.get_obj ... 223 | ├─output_text = 'quartodoc.get_object' 224 | └─output_link = 'other+api/get_object.html#quartodoc.get_object' 225 | ``` 226 | 227 | 228 | 229 | 230 | ## `` [](:external+other:`quartodoc.layout`) `` 231 | 232 | output: [](:external+other:`quartodoc.layout`) 233 | 234 | ``` 235 | █─TestSpecEntry 236 | ├─input = '[](:external+other:`quartodoc.layout`)' 237 | ├─output_element = █─Code 238 | │ └─content = ':external+other:`quartodoc.layout`' 239 | └─warning = 'InvLookupError' 240 | ``` 241 | 242 | 243 | 244 | 245 | ## `` [](:attribute:`quartodoc.MdRenderer.render`) `` 246 | 247 | output: [](:attribute:`quartodoc.MdRenderer.render`) 248 | 249 | ``` 250 | █─TestSpecEntry 251 | ├─input = '[](:attribute:`quartodoc.MdRenderer.render`)' 252 | └─warning = 'InvLookupError' 253 | ``` 254 | 255 | 256 | 257 | 258 | ## `` [](:mod:`quartodoc.layout`) `` 259 | 260 | output: [](:mod:`quartodoc.layout`) 261 | 262 | ``` 263 | █─TestSpecEntry 264 | ├─input = '[](:mod:`quartodoc.layout`)' 265 | └─warning = 'InvLookupError' 266 | ``` 267 | -------------------------------------------------------------------------------- /quartodoc/tests/example_signature.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | 4 | def no_annotations(a, b=1, *args, c, d=2, **kwargs): 5 | """A function with a signature""" 6 | 7 | 8 | def yes_annotations( 9 | a: int, b: int = 1, *args: list[str], c: int, d: int, **kwargs: dict[str, str] 10 | ): 11 | """A function with a signature""" 12 | 13 | 14 | def pos_only(x, /, a, b=2): 15 | ... 16 | 17 | 18 | def kw_only(x, *, a, b=2): 19 | ... 20 | 21 | 22 | def early_args(x, *args, a, b=2, **kwargs): 23 | ... 24 | 25 | 26 | def late_args(x, a, b=2, *args, **kwargs): 27 | ... 28 | 29 | 30 | class C: 31 | ... 32 | 33 | 34 | def a_complex_signature(x: "list[C | int | None]", y: "pathlib.Pathlib", z): 35 | """ 36 | Parameters 37 | ---------- 38 | x: 39 | The x parameter 40 | y: 41 | The y parameter 42 | z: 43 | The z parameter (unannotated) 44 | """ 45 | -------------------------------------------------------------------------------- /quartodoc/tests/example_star_imports.py: -------------------------------------------------------------------------------- 1 | from quartodoc.tests.example import * # noqa 2 | -------------------------------------------------------------------------------- /quartodoc/tests/example_stubs.py: -------------------------------------------------------------------------------- 1 | def f(a, b): 2 | """The original docstring.""" 3 | -------------------------------------------------------------------------------- /quartodoc/tests/example_stubs.pyi: -------------------------------------------------------------------------------- 1 | def f(a: int, b: str): 2 | """The stub docstring""" 3 | -------------------------------------------------------------------------------- /quartodoc/tests/pandoc/test_inlines.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from quartodoc.pandoc.inlines import ( 4 | Code, 5 | Emph, 6 | Image, 7 | Inline, 8 | Inlines, 9 | Link, 10 | Span, 11 | Str, 12 | Strong, 13 | ) 14 | from quartodoc.pandoc.components import Attr 15 | 16 | # NOTE: 17 | # To make it easy to cross-check what the generated html code will be, 18 | # it should be possible to copy the markdown code on the right-hand side 19 | # of the assert statements and paste it at 20 | # https://pandoc.org/try/ 21 | 22 | 23 | def test_code(): 24 | s = "a = 1" 25 | 26 | c = Code(s) 27 | assert str(c) == "`a = 1`" 28 | assert c.html == "a = 1" 29 | 30 | c = Code(s, Attr("code-id", ["c1", "c2"])) 31 | assert str(c) == "`a = 1`{#code-id .c1 .c2}" 32 | assert c.html == 'a = 1' 33 | 34 | 35 | def test_emph(): 36 | e = Emph("a") 37 | assert str(e) == "*a*" 38 | 39 | e = Emph("") 40 | assert str(e) == "" 41 | 42 | 43 | def test_image(): 44 | img = Image("Image Caption", "path/to/image.png", "Image Title") 45 | assert str(img) == '![Image Caption](path/to/image.png "Image Title")' 46 | 47 | img = Image( 48 | src="image.png", 49 | attr=Attr(classes=["c1"], attributes={"width": "50%", "height": "60%"}), 50 | ) 51 | assert str(img) == '![](image.png){.c1 width="50%" height="60%"}' 52 | 53 | 54 | def test_inline(): 55 | i = Inline() 56 | 57 | with pytest.raises(NotImplementedError): 58 | str(i) 59 | 60 | with pytest.raises(NotImplementedError): 61 | i.html 62 | 63 | 64 | def test_inlines(): 65 | i = Inlines(["a", Span("b"), Emph("c")]) 66 | assert str(i) == "a [b]{} *c*" 67 | 68 | i = Inlines(["a", None, Span("b"), Emph("c"), None]) 69 | assert str(i) == "a [b]{} *c*" 70 | 71 | i = Inlines([None, None, None]) 72 | assert str(i) == "" 73 | 74 | i = Inlines(["a", Span("b"), Emph("c"), ["d", Strong("e")]]) 75 | assert str(i) == "a [b]{} *c* d **e**" 76 | 77 | i = Inlines(["a", Span("b"), Emph("c"), Inlines(["d", Strong("e")])]) 78 | assert str(i) == "a [b]{} *c* d **e**" 79 | 80 | 81 | def test_link(): 82 | l = Link("Link Text", "https://abc.com") 83 | assert str(l) == "[Link Text](https://abc.com)" 84 | 85 | l = Link("Link Text", "https://abc.com", "Link Title") 86 | assert str(l) == '[Link Text](https://abc.com "Link Title")' 87 | 88 | 89 | def test_span(): 90 | s = Span("a") 91 | assert str(s) == "[a]{}" 92 | 93 | s = Span("a", Attr("span-id", classes=["c1", "c2"], attributes={"data-value": "1"})) 94 | assert str(s) == '[a]{#span-id .c1 .c2 data-value="1"}' 95 | 96 | s = Span([Span("a"), Span("b"), "c"]) 97 | assert str(s) == "[[a]{} [b]{} c]{}" 98 | 99 | 100 | def test_str(): 101 | s = Str("a") 102 | assert str(s) == "a" 103 | 104 | s = Str() 105 | assert str(s) == "" 106 | 107 | 108 | def test_strong(): 109 | s = Strong("a") 110 | assert str(s) == "**a**" 111 | 112 | s = Strong("") 113 | assert str(s) == "" 114 | 115 | 116 | def test_seq_inlinecontent(): 117 | s = Span( 118 | [Str("a"), Emph("b"), Code("c = 3", Attr(classes=["py"]))], 119 | Attr(classes=["c1", "c2"]), 120 | ) 121 | assert str(s) == "[a *b* `c = 3`{.py}]{.c1 .c2}" 122 | -------------------------------------------------------------------------------- /quartodoc/tests/test_ast.py: -------------------------------------------------------------------------------- 1 | import quartodoc.ast as qast 2 | import pytest 3 | 4 | from quartodoc._griffe_compat import docstrings as ds 5 | from quartodoc._griffe_compat import dataclasses as dc 6 | from quartodoc._griffe_compat import parse_numpy 7 | 8 | from quartodoc import get_object 9 | 10 | 11 | @pytest.mark.parametrize( 12 | "el, body", 13 | [ 14 | ("See Also\n---", ""), 15 | ("See Also\n---\n", ""), 16 | ("See Also\n--------", ""), 17 | ("See Also\n---\nbody text", "body text"), 18 | ("See Also\n---\nbody text", "body text"), 19 | ], 20 | ) 21 | def test_transform_docstring_section(el, body): 22 | src = ds.DocstringSectionText(el, title=None) 23 | res = qast._DocstringSectionPatched.transform(src) 24 | 25 | assert len(res) == 1 26 | assert isinstance(res[0], qast.DocstringSectionSeeAlso) 27 | assert res[0].title == "See Also" 28 | assert res[0].value == body 29 | 30 | 31 | def test_transform_docstring_section_multiple(): 32 | # Note the starting text is not a section header 33 | # so this should be split into two sections: short description, and see also. 34 | src = ds.DocstringSectionText("A short description\n\nSee Also\n---\n") 35 | res = qast._DocstringSectionPatched.transform(src) 36 | 37 | assert len(res) == 2 38 | assert res[0].value == "A short description\n\n" 39 | assert isinstance(res[1], qast.DocstringSectionSeeAlso) 40 | assert res[1].title == "See Also" 41 | assert res[1].value == "" 42 | 43 | 44 | @pytest.mark.parametrize( 45 | "el, cls", 46 | [ 47 | ("See Also\n---", qast.DocstringSectionSeeAlso), 48 | ("Warnings\n---", qast.DocstringSectionWarnings), 49 | ("Notes\n---", qast.DocstringSectionNotes), 50 | ], 51 | ) 52 | def test_transform_docstring_section_subtype(el, cls): 53 | # using transform method ---- 54 | src = ds.DocstringSectionText(el, title=None) 55 | res = qast._DocstringSectionPatched.transform(src) 56 | 57 | assert len(res) == 1 58 | assert isinstance(res[0], cls) 59 | 60 | # using transform function ---- 61 | parsed = parse_numpy(dc.Docstring(el)) 62 | assert len(parsed) == 1 63 | 64 | res2 = qast.transform(parsed) 65 | assert len(res2) == 1 66 | assert isinstance(res2[0], cls) 67 | 68 | 69 | @pytest.mark.xfail(reason="TODO: sections get grouped into single element") 70 | def test_transform_docstring_section_clump(): 71 | docstring = "See Also---\n\nWarnings\n---\n\nNotes---\n\n" 72 | parsed = parse_numpy(dc.Docstring(docstring)) 73 | 74 | assert len(parsed) == 1 75 | 76 | # res = transform(parsed[0]) 77 | 78 | # what to do here? this should more reasonably be handled when transform 79 | # operates on the root. 80 | 81 | 82 | def test_preview_no_fail(capsys): 83 | qast.preview(get_object("quartodoc", "get_object")) 84 | 85 | res = capsys.readouterr() 86 | 87 | assert "get_object" in res.out 88 | 89 | 90 | @pytest.mark.xfail 91 | def test_preview_warn_alias_no_load(): 92 | # fetch an alias to pydantic.BaseModel, without loading pydantic 93 | # attempting to get alias.target will fail, but preview should still work. 94 | obj = get_object("quartodoc.ast.BaseModel", load_aliases=False) 95 | with pytest.warns(UserWarning) as record: 96 | qast.preview(obj) 97 | 98 | msg = record[0].message.args[0] 99 | assert ( 100 | "Could not resolve Alias target `quartodoc._pydantic_compat.BaseModel`" in msg 101 | ) 102 | 103 | 104 | @pytest.mark.parametrize( 105 | "text, dst", 106 | [("One\n---\nab\n\nTwo\n---\n\ncd", [("One", "ab\n\n"), ("Two", "\ncd")])], 107 | ) 108 | def test_transform_split_sections(text, dst): 109 | res = qast._DocstringSectionPatched.split_sections(text) 110 | assert res == dst 111 | -------------------------------------------------------------------------------- /quartodoc/tests/test_builder.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import yaml 3 | 4 | from pathlib import Path 5 | from quartodoc import layout as lo 6 | from quartodoc import Builder, blueprint 7 | 8 | 9 | @pytest.fixture 10 | def builder(tmp_path): 11 | section = lo.Section( 12 | title="abc", 13 | desc="xyz", 14 | contents=[ 15 | lo.Auto(name="get_object"), 16 | lo.Auto( 17 | name="MdRenderer", members=["render", "summarize"], children="separate" 18 | ), 19 | ], 20 | ) 21 | 22 | builder = Builder(package="quartodoc", sections=[section], dir=str(tmp_path)) 23 | 24 | yield builder 25 | 26 | 27 | def test_builder_build_filter_simple(builder): 28 | builder.build(filter="get_object") 29 | 30 | assert (Path(builder.dir) / "get_object.qmd").exists() 31 | assert not (Path(builder.dir) / "MdRenderer.qmd").exists() 32 | 33 | 34 | def test_builder_build_filter_wildcard_class(builder): 35 | builder.build(filter="MdRenderer*") 36 | 37 | len(list(Path(builder.dir).glob("Mdrenderer*"))) == 3 38 | 39 | 40 | def test_builder_build_filter_wildcard_methods(builder): 41 | builder.build(filter="MdRenderer.*") 42 | 43 | len(list(Path(builder.dir).glob("Mdrenderer.*"))) == 2 44 | 45 | 46 | def test_builder_auto_options(): 47 | cfg = yaml.safe_load( 48 | """ 49 | quartodoc: 50 | package: quartodoc 51 | options: 52 | members: [a_func, a_attr] 53 | sections: 54 | - contents: [quartodoc.tests.example] 55 | """ 56 | ) 57 | 58 | builder = Builder.from_quarto_config(cfg) 59 | assert builder.layout.options.members == ["a_func", "a_attr"] 60 | 61 | 62 | def test_builder_generate_sidebar(tmp_path, snapshot): 63 | cfg = yaml.safe_load( 64 | """ 65 | quartodoc: 66 | package: quartodoc.tests.example 67 | sections: 68 | - title: first section 69 | desc: some description 70 | contents: [a_func] 71 | - title: second section 72 | desc: title description 73 | - subtitle: a subsection 74 | desc: subtitle description 75 | contents: 76 | - a_attr 77 | """ 78 | ) 79 | 80 | builder = Builder.from_quarto_config(cfg) 81 | bp = blueprint(builder.layout) 82 | d_sidebar = builder._generate_sidebar(bp) 83 | 84 | assert yaml.dump(d_sidebar) == snapshot 85 | 86 | 87 | def test_builder_generate_sidebar_options(tmp_path, snapshot): 88 | cfg = yaml.safe_load( 89 | """ 90 | quartodoc: 91 | package: quartodoc.tests.example 92 | sidebar: 93 | style: docked 94 | search: true 95 | contents: 96 | - text: "Introduction" 97 | href: introduction.qmd 98 | - section: "Reference" 99 | contents: 100 | - "{{ contents }}" 101 | - text: "Basics" 102 | href: basics-summary.qmd 103 | sections: 104 | - title: first section 105 | desc: some description 106 | contents: [a_func] 107 | - title: second section 108 | desc: title description 109 | - subtitle: a subsection 110 | desc: subtitle description 111 | contents: 112 | - a_attr 113 | """ 114 | ) 115 | 116 | builder = Builder.from_quarto_config(cfg) 117 | assert builder.sidebar["file"] == "_quartodoc-sidebar.yml" # default value 118 | 119 | bp = blueprint(builder.layout) 120 | 121 | d_sidebar = builder._generate_sidebar(bp) 122 | assert "website" in d_sidebar 123 | assert "sidebar" in d_sidebar["website"] 124 | 125 | qd_sidebar = d_sidebar["website"]["sidebar"][0] 126 | assert "file" not in qd_sidebar 127 | assert qd_sidebar["style"] == "docked" 128 | assert qd_sidebar["search"] 129 | 130 | assert yaml.dump(d_sidebar) == snapshot 131 | -------------------------------------------------------------------------------- /quartodoc/tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pathlib import Path 4 | from quartodoc.__main__ import build 5 | 6 | 7 | def test_cli_build_config_path(tmpdir): 8 | p_tmp = Path(tmpdir) 9 | p_config = Path(p_tmp, "_quarto.yml") 10 | p_config.write_text( 11 | """ 12 | quartodoc: 13 | package: quartodoc 14 | sections: 15 | - contents: 16 | - get_object 17 | """ 18 | ) 19 | 20 | # calling click CLI objects directly is super cumbersome --- 21 | try: 22 | build(["--config", str(p_config)]) 23 | except SystemExit: 24 | pass 25 | 26 | res = list(p_tmp.glob("reference/*")) 27 | 28 | assert len(res) == 2 29 | assert "get_object.qmd" in [p.name for p in res] 30 | -------------------------------------------------------------------------------- /quartodoc/tests/test_interlinks.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import sphobjinv 3 | import pytest 4 | import yaml 5 | 6 | from quartodoc import interlinks 7 | from quartodoc.interlinks import ( 8 | Inventories, 9 | Ref, 10 | TestSpec, 11 | TestSpecEntry, 12 | parse_md_style_link, 13 | Link, 14 | inventory_from_url, 15 | ) 16 | from importlib_resources import files 17 | 18 | # load test spec at import time, so that we can feed each spec entry 19 | # as an individual test case using parametrize 20 | _raw = yaml.safe_load(open(files("quartodoc") / "tests/example_interlinks/spec.yml")) 21 | spec = TestSpec(__root__=_raw).__root__ 22 | 23 | 24 | @pytest.fixture 25 | def invs(): 26 | invs = Inventories.from_quarto_config( 27 | str(files("quartodoc") / "tests/example_interlinks/_quarto.yml") 28 | ) 29 | 30 | return invs 31 | 32 | 33 | # def test_inventories_from_config(): 34 | # invs = Inventories.from_quarto_config( 35 | # str(files("quartodoc") / "tests/example_interlinks/_quarto.yml") 36 | # ) 37 | 38 | 39 | @pytest.mark.parametrize( 40 | "raw,dst", 41 | [ 42 | ("`print`", Ref(target="print")), 43 | (":function:`print`", Ref(role="function", target="print")), 44 | (":py:function:`print`", Ref(domain="py", role="function", target="print")), 45 | ( 46 | ":external:function:`print`", 47 | Ref(role="function", target="print", external=True), 48 | ), 49 | (":external+abc:`print`", Ref(target="print", invname="abc", external=True)), 50 | ], 51 | ) 52 | def test_ref_from_string(raw, dst): 53 | src = Ref.from_string(raw) 54 | 55 | assert src == dst 56 | 57 | 58 | @pytest.mark.parametrize("entry", spec) 59 | def test_spec_entry(invs: Inventories, entry: TestSpecEntry): 60 | ref_str, text = parse_md_style_link(entry.input) 61 | ref_str = ref_str.replace("`", "%60") # weird, but matches pandoc 62 | 63 | # set up error and warning contexts ---- 64 | # pytest uses context managers to check warnings and errors 65 | # so we either create the relevant cm or uses a no-op cm 66 | if entry.error: 67 | ctx_err = pytest.raises(getattr(interlinks, entry.error)) 68 | else: 69 | ctx_err = contextlib.nullcontext() 70 | 71 | if entry.warning: 72 | ctx_warn = pytest.warns(UserWarning, match=entry.warning) 73 | else: 74 | ctx_warn = contextlib.nullcontext() 75 | 76 | # fetch link ---- 77 | with ctx_warn as rec_warn, ctx_err as rec_err: # noqa 78 | el = invs.pandoc_ref_to_anchor(ref_str, text) 79 | 80 | if entry.error: 81 | # return on errors, since no result produced 82 | return 83 | 84 | # output assertions ---- 85 | if entry.output_link is not None or entry.output_text is not None: 86 | assert isinstance(el, Link) 87 | 88 | if entry.output_link: 89 | assert entry.output_link == el.url 90 | 91 | if entry.output_text: 92 | assert entry.output_text == el.content 93 | elif entry.output_element: 94 | assert el == entry.output_element 95 | 96 | 97 | def test_inventory_from_url_local_roundtrip(tmp_path): 98 | inv = sphobjinv.Inventory() 99 | inv.project = "abc" 100 | inv.version = "0.0.1" 101 | 102 | soi_items = [ 103 | sphobjinv.DataObjStr( 104 | name="foo", domain="py", role="method", priority="1", uri="$", dispname="-" 105 | ) 106 | ] 107 | inv.objects.extend(soi_items) 108 | 109 | text = inv.data_file() 110 | sphobjinv.writebytes(tmp_path / "objects.txt", text) 111 | sphobjinv.writebytes(tmp_path / "objects.inv", sphobjinv.compress(text)) 112 | 113 | res1 = inventory_from_url("file://" + str(tmp_path / "objects.txt")) 114 | res2 = inventory_from_url("file://" + str(tmp_path / "objects.inv")) 115 | 116 | assert isinstance(res1, sphobjinv.Inventory) 117 | assert isinstance(res2, sphobjinv.Inventory) 118 | -------------------------------------------------------------------------------- /quartodoc/tests/test_layout.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from quartodoc.layout import Layout, Page, Text, Section # noqa 4 | 5 | from quartodoc._pydantic_compat import ValidationError 6 | 7 | 8 | @pytest.mark.parametrize( 9 | "cfg, res", 10 | [ 11 | ( 12 | {"kind": "page", "package": "abc", "path": "xyz", "contents": []}, 13 | Page(package="abc", path="xyz", contents=[]), 14 | ), 15 | ( 16 | { 17 | "kind": "page", 18 | "package": "abc", 19 | "path": "xyz", 20 | "contents": [{"kind": "text", "contents": "abc"}], 21 | }, 22 | Page(package="abc", path="xyz", contents=[Text(contents="abc")]), 23 | ), 24 | ], 25 | ) 26 | def test_layout_from_config(cfg, res): 27 | page = Page(**cfg) 28 | assert page == res 29 | 30 | layout = Layout(sections=[cfg]) 31 | assert layout.sections[0] == res 32 | 33 | 34 | @pytest.mark.parametrize( 35 | "kwargs, msg_part", 36 | [ 37 | ({}, "must specify a title, subtitle, or contents field"), 38 | ({"title": "x", "subtitle": "y"}, "cannot specify both"), 39 | ], 40 | ) 41 | def test_section_validation_fails(kwargs, msg_part): 42 | with pytest.raises(ValueError) as exc_info: 43 | Section(**kwargs) 44 | 45 | assert msg_part in exc_info.value.args[0] 46 | 47 | 48 | def test_layout_extra_forbidden(): 49 | with pytest.raises(ValidationError) as exc_info: 50 | Section(title="abc", desc="xyz", contents=[], zzzzz=1) 51 | 52 | assert "extra fields not permitted" in str(exc_info.value) 53 | -------------------------------------------------------------------------------- /quartodoc/tests/test_validation.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import yaml 3 | 4 | from textwrap import indent, dedent 5 | 6 | from quartodoc.autosummary import Builder 7 | 8 | 9 | def check_ValueError(cfg: str): 10 | "Check that a ValueError is raised when creating a `Builder` instance. Return the error message as a string." 11 | 12 | d = yaml.safe_load(cfg) 13 | with pytest.raises(ValueError) as e: 14 | Builder.from_quarto_config(d) 15 | 16 | fmt_cfg = indent(dedent(cfg), " " * 4) 17 | fmt_value = indent(str(e.value), " " * 4) 18 | return f"""\ 19 | Code:\n{fmt_cfg} 20 | Error:\n{fmt_value} 21 | """ 22 | 23 | 24 | def test_missing_name_contents(snapshot): 25 | "Test that a missing name in contents raises an error in a different section." 26 | 27 | cfg = """ 28 | quartodoc: 29 | package: zzz 30 | sections: 31 | - title: Section 1 32 | - title: Section 2 33 | contents: 34 | 35 | # name missing here ---- 36 | - children: linked 37 | 38 | - name: MdRenderer 39 | """ 40 | 41 | msg = check_ValueError(cfg) 42 | assert msg == snapshot 43 | 44 | 45 | def test_misplaced_kindpage(snapshot): 46 | "Test that a misplaced kind: page raises an error" 47 | 48 | cfg = """ 49 | quartodoc: 50 | package: zzz 51 | sections: 52 | - kind: page 53 | 54 | """ 55 | 56 | msg = check_ValueError(cfg) 57 | assert msg == snapshot 58 | -------------------------------------------------------------------------------- /quartodoc/validation.py: -------------------------------------------------------------------------------- 1 | """User-friendly messages for configuration validation errors. 2 | 3 | This module has largely two goals: 4 | 5 | * Show the most useful error (pydantic starts with the highest, broadest one). 6 | * Make the error message very user-friendly. 7 | 8 | The key dynamic for understanding formatting is that pydantic is very forgiving. 9 | It will coerce values to the target type, and by default allows extra fields. 10 | Critically, if you have a union of types, it will try each type in order until it 11 | finds a match. In this case, it reports errors for everything it tried. 12 | 13 | For example, consider this config: 14 | 15 | quartodoc: 16 | package: zzz 17 | sections: 18 | - title: Section 1 19 | contents: 20 | # name missing here ---- 21 | - children: linked 22 | 23 | In this case, the first element of contents is missing a name field. Since the 24 | first type in the union for content elements is _AutoDefault, that is what it 25 | tries (and logs an error about name). However, it then goes down the list of other 26 | types in the union and logs errors for those (e.g. Doc). This produce a lot of 27 | confusing messages, because nowhere does it make clear what type its trying to create. 28 | 29 | We don't want error messages for everything it tried, just the first type in the union. 30 | (For a discriminated union, it uses the `kind:` field to know what the first type to try is). 31 | """ 32 | 33 | 34 | def fmt_all(e): 35 | # if not pydantic.__version__.startswith("1."): 36 | # # error reports are much better in pydantic v2 37 | # # so we just use those. 38 | # return str(e) 39 | 40 | errors = [fmt(err) for err in e.errors() if fmt(err)] 41 | 42 | # the last error is the most specific, while earlier ones can be 43 | # for alternative union types that didn't work out. 44 | main_error = errors[0] 45 | 46 | msg = f"Configuration error for YAML:\n - {main_error}" 47 | return msg 48 | 49 | 50 | def fmt(err: dict): 51 | "format error messages from pydantic." 52 | 53 | # each entry of loc is a new level in the config tree 54 | # 0 is root 55 | # 1 is sections 56 | # 2 is a section entry 57 | # 3 is contents 58 | # 4 is a content item 59 | # 5 might be Auto.members, etc.. 60 | # 6 might be an Auto.members item 61 | 62 | # type: value_error.discriminated_union.missing_discriminator 63 | # type: value_error.missing 64 | # type: value_error.extra 65 | msg = "" 66 | if err["msg"].startswith("Discriminator"): 67 | return msg 68 | if err["type"] == "value_error.missing": 69 | msg += "Missing field" 70 | else: 71 | msg += err["msg"] + ":" 72 | 73 | if "loc" in err: 74 | if len(err["loc"]) == 1: 75 | msg += f" from root level: `{err['loc'][0]}`" 76 | elif len(err["loc"]) == 3: 77 | msg += f" `{err['loc'][2]}` for element {err['loc'][1]} in the list for `{err['loc'][0]}`" 78 | elif len(err["loc"]) == 4 and err["loc"][2] == "Page": 79 | msg += f" `{err['loc'][3]}` for element {err['loc'][1]} in the list for `{err['loc'][0]}`, which you need when setting `kind: page`." 80 | elif len(err["loc"]) == 5: 81 | msg += f" `{err['loc'][4]}` for element {err['loc'][3]} in the list for `{err['loc'][2]}` located in element {err['loc'][1]} in the list for `{err['loc'][0]}`" 82 | elif len(err["loc"]) == 6 and err["loc"][4] == "Auto": 83 | msg += f" `{err['loc'][5]}` for element {err['loc'][3]} in the list for `{err['loc'][2]}` located in element {err['loc'][1]} in the list for `{err['loc'][0]}`" 84 | else: 85 | return str(err) # so we can debug and include more cases 86 | else: 87 | msg += str(err) 88 | return msg 89 | -------------------------------------------------------------------------------- /scripts/build_tmp_starter.py: -------------------------------------------------------------------------------- 1 | import re 2 | import tempfile 3 | import subprocess 4 | from pathlib import Path 5 | from quartodoc.__main__ import build 6 | 7 | p = Path("docs/get-started/overview.qmd") 8 | overview = p.read_text() 9 | 10 | indx = overview.index("") 11 | yml_blurb = overview[indx:] 12 | 13 | match = re.search(r"```yaml\s*(.*?)```", yml_blurb, re.DOTALL) 14 | if match is None: 15 | raise Exception() 16 | 17 | template = match.group(1) 18 | 19 | with tempfile.TemporaryDirectory() as tmpdir: 20 | tmpdir = Path(tmpdir) 21 | # Write the template to a file 22 | p_quarto = tmpdir / "_quarto.yml" 23 | p_quarto.write_text(template) 24 | 25 | try: 26 | build(["--config", str(p_quarto), "--filter", "quartodoc"]) 27 | except SystemExit as e: 28 | if e.code != 0: 29 | raise Exception() from e 30 | subprocess.run(["quarto", "render", str(p_quarto.parent)]) 31 | 32 | print("SITE RENDERED SUCCESSFULLY") 33 | -------------------------------------------------------------------------------- /scripts/filter-spec/generate_files.py: -------------------------------------------------------------------------------- 1 | # %% 2 | from quartodoc import blueprint, collect, create_inventory, convert_inventory 3 | import quartodoc.layout as lo 4 | import shutil 5 | 6 | from pathlib import Path 7 | 8 | repo_dir = Path(__file__).parent.parent.parent 9 | base_dir = repo_dir / "quartodoc" / "tests" / "example_interlinks" 10 | base_dir.mkdir(exist_ok=True, parents=True) 11 | 12 | shutil.copy(Path(__file__).parent / "_quarto.yml", base_dir / "_quarto.yml") 13 | 14 | 15 | # %% 16 | layout = lo.Layout( 17 | sections=[ 18 | lo.Section( 19 | title="some title", 20 | desc="some description", 21 | contents=[ 22 | lo.Auto(name="layout", members=[]), 23 | lo.Auto(name="MdRenderer", members=["style", "render"]), 24 | ], 25 | ) 26 | ], 27 | package="quartodoc", 28 | ) 29 | 30 | bp = blueprint(layout) 31 | pages, items = collect(bp, "api") 32 | inv = create_inventory("quartodoc", "0.0.999", items) 33 | convert_inventory(inv, base_dir / "objects.json") 34 | 35 | # %% 36 | layout2 = lo.Layout( 37 | sections=[ 38 | lo.Section( 39 | title="some title", 40 | desc="some description", 41 | contents=[lo.Auto(name="get_object")], 42 | ) 43 | ], 44 | package="quartodoc", 45 | ) 46 | 47 | _, items2 = collect(blueprint(layout2), "api") 48 | inv2 = create_inventory("other", "0.0.999", items2) 49 | 50 | p_inv = base_dir / "_inv" 51 | p_inv.mkdir(exist_ok=True) 52 | 53 | convert_inventory(inv2, p_inv / "other_objects.json") 54 | # %% 55 | -------------------------------------------------------------------------------- /scripts/filter-spec/generate_test_qmd.py: -------------------------------------------------------------------------------- 1 | #%% 2 | from quartodoc.tests.test_interlinks import spec 3 | from quartodoc import preview 4 | 5 | 6 | # %% 7 | spec 8 | 9 | # %% 10 | 11 | template = """ 12 | ## {input} 13 | 14 | output: {output} 15 | 16 | ``` 17 | {preview} 18 | ``` 19 | """ 20 | 21 | # # {input} 22 | # output: {output} 23 | # 24 | # {preview of entry} 25 | results = [] 26 | for ii, entry in enumerate(spec): 27 | results.append(template.format( 28 | input = "`` " + entry.input + " ``", 29 | output = entry.input, 30 | preview = preview(entry, as_string=True) 31 | ) 32 | ) 33 | 34 | final = "\n\n\n".join(results) 35 | 36 | 37 | # %% 38 | --------------------------------------------------------------------------------