Skip to content

Commit

Permalink
Merge pull request #34 from emlys/example/generate-docs
Browse files Browse the repository at this point in the history
Custom sphinx extension to generate docs from args spec
  • Loading branch information
emlys authored Sep 16, 2021
2 parents d369b8e + 840b5e9 commit fe31914
Show file tree
Hide file tree
Showing 15 changed files with 1,161 additions and 56 deletions.
13 changes: 8 additions & 5 deletions .github/workflows/build-sphinx.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,21 @@ jobs:
done
exit $exitcode
- name: Check out sample data
run: make get_sampledata
- name: Run custom extension tests
run: make test_investspec

- name: Demo the custom extension
run: make demo_investspec

- name: Build sphinx docs
run: make html

- name: Check links
# this has rarely run for hours for no apparent reason
timeout-minutes: 2
continue-on-error: true
run: make linkcheck

- name: Build sphinx docs
run: make html

- name: Set up GCP SDK
if: ${{ github.ref == 'refs/heads/main' }}
uses: google-github-actions/setup-gcloud@master
Expand Down
45 changes: 31 additions & 14 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,18 @@ GIT_SAMPLE_DATA_REPO := https://bitbucket.org/natcap/invest-sample-data.g
GIT_SAMPLE_DATA_REPO_PATH := invest-sample-data
GIT_SAMPLE_DATA_REPO_REV := c8df675a2c446bf8d00ffd8f0cbab933f7d5c25a

.PHONY: help clean html changes linkcheck
.PHONY: help clean html changes linkcheck prep_sampledata test_investspec demo_investspec

help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " clean to remove the build directory"
@echo " html to make standalone HTML files"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " get_sampledata to check out the invest-sample-data repo"
@echo " prep_sampledata to create modified tables in invest-sample-data that display nicely"
@echo " clean to remove the build directory"
@echo " html to make standalone HTML files"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " $(GIT_SAMPLE_DATA_REPO_PATH) to check out the invest-sample-data repo"
@echo " prep_sampledata to create modified tables in invest-sample-data that display nicely"
@echo " test_investspec to run unit tests for the custom Sphinx extension"
@echo " demo_investspec to run a demo using the custom Sphinx extension"

clean:
-rm -rf $(BUILDDIR)/*
Expand All @@ -46,14 +48,29 @@ linkcheck: $(SOURCEDIR)
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."

get_sampledata:
-git clone $(GIT_SAMPLE_DATA_REPO) $(GIT_SAMPLE_DATA_REPO_PATH)
git -C $(GIT_SAMPLE_DATA_REPO_PATH) fetch
git -C $(GIT_SAMPLE_DATA_REPO_PATH) lfs install
git -C $(GIT_SAMPLE_DATA_REPO_PATH) lfs fetch
git -C $(GIT_SAMPLE_DATA_REPO_PATH) checkout $(GIT_SAMPLE_DATA_REPO_REV)
test_investspec:
cd extensions/investspec && python -m unittest test.test_investspec

prep_sampledata:
CUSTOM_EXTENSION_TEST_DIR = extensions/investspec/test
demo_investspec:
# remove any old build files
rm -rf $(CUSTOM_EXTENSION_TEST_DIR)/build
# install the mock module `test_module`
pip install $(CUSTOM_EXTENSION_TEST_DIR)/test_module
# -W: fail on warning
# -a: write all files, not just new or changed files
sphinx-build -W -a -b html $(CUSTOM_EXTENSION_TEST_DIR) $(CUSTOM_EXTENSION_TEST_DIR)/build

# initialize the sample data repo and check out the commit
$(GIT_SAMPLE_DATA_REPO_PATH):
mkdir $(GIT_SAMPLE_DATA_REPO_PATH) && cd $(GIT_SAMPLE_DATA_REPO_PATH)
git -C $(GIT_SAMPLE_DATA_REPO_PATH) init
git -C $(GIT_SAMPLE_DATA_REPO_PATH) remote add origin $(GIT_SAMPLE_DATA_REPO)
git -C $(GIT_SAMPLE_DATA_REPO_PATH) fetch --depth 1 origin $(GIT_SAMPLE_DATA_REPO_REV)
# GIT_LFS_SKIP_SMUDGE=1 prevents getting all the lfs files, we only need the CSVs
GIT_LFS_SKIP_SMUDGE=1 git -C $(GIT_SAMPLE_DATA_REPO_PATH) checkout $(GIT_SAMPLE_DATA_REPO_REV)

prep_sampledata: $(GIT_SAMPLE_DATA_REPO_PATH)
# take selections of tables that are too long to display in full
head -n1 invest-sample-data/pollination/landcover_biophysical_table.csv > invest-sample-data/pollination/landcover_biophysical_table_modified.csv
tail -n3 invest-sample-data/pollination/landcover_biophysical_table.csv >> invest-sample-data/pollination/landcover_biophysical_table_modified.csv
Expand Down
80 changes: 80 additions & 0 deletions extensions/investspec/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# investspec extension for Sphinx

This is a custom Sphinx extension that generates documentation of InVEST model inputs from the model's `ARGS_SPEC`.
Its purpose is to help us reduce duplicated information and provide consistent, user-friendly documentation.
The `investspec` extension provides the `:investspec:` role, which can be used inline in RST files to insert generated documentation anywhere you want.

## Setup

`investspec` is already set up in the `conf.py` in this repo.

In case we need to use it anywhere else, here's how to set it up:
In the `conf.py` file for the source RST, add the `investspec/` root directory to `sys.path` so that Sphinx can find it:
```
sys.path.append(os.path.abspath('../extensions/investspec')) # or other path as appropriate
```
and add `investspec` to the list of extensions:
```
extensions = ['investspec']
```
to avoid writing out `natcap.invest. ...` before the module name every time, set the module prefix:
```
investspec_module_prefix = 'natcap.invest'
```

## Usage

The `investspec` role takes two arguments: `` :investspec:`module key` ``

`module` (or `f'{investspec_module_prefix}.{module}'` if `investspec_module_prefix` is defined) must be an importable python module. It must have an attribute `ARGS_SPEC` that is a well-formed InVEST args spec dictionary.

The second argument specifies which (nested) arg to document. It is a period-separated series of dictionary keys accessed starting at `ARGS_SPEC.args`. For example:
```
ARGS_SPEC = {
"model_name": "InVEST Model",
"args": {
"biophysical_table_path": {
"type": "csv",
"name": "Biophysical Table"
"columns": {
"lucode": {"type": "integer"},
"path": {
"type": "vector",
"fields": {
"value": {
"type": "integer"
}
}
}
}
...
}
```
If this model is located at `natcap.invest.model_name`, then you can auto-document:

- `` :investspec:`model_name biophysical_table_path` ``

- `` :investspec:`model_name biophysical_table_path.columns.path` ``

- `` :investspec:`model_name biophysical_table_path.columns.path.fields.value` ``

You can document any arg in the `ARGS_SPEC.args` dictionary this way. This includes any nested dictionary with a `type` attribute:

- top-level args
- any row or column within a csv's `"rows"` or `"columns"` dict
- any field within a vector's `"fields"` dict
- any file or directory within a directory's `"contents"` dict

## What is not documented
- `expression`s for `number` types. This can be any python expression, so it may be too complicated to to auto-format into human readable text. Any limits on a `number`'s value should also be described in the `about` text.
- Conditional requirements (`"required": <str>`). This can be any python expression, so it may be too complicated to auto-format into human readable text. For any conditionally-required input, the conditions upon which it is required should also be described in the `about` text.

## Limitations
- This implementation can only generate output that uses standard docutils features, and no sphinx-specific features. See natcap/invest.users-guide#35 for details.
- Relies on the `ARGS_SPEC` being complete. For example, columns in a table's `columns` attribute should either all have an `about` attribute, or none have an `about` attribute. However, it is still valid for only some to have an `about` attribute. If some are missing, it will work, but the generated docs will look a little strange.

## Tests
From the top level of this repo, you can run `make test_investspec` to run a set of unit tests (`extensions/investspec/test/test_investspec.py`). These only test the formatting logic.

`make demo_investspec` exists as a sort-of integration test to prove that the extension works without errors. The output is not checked for correctness. It installs the mock module in `extensions/investspec/test/test_module`, then builds HTML docs from `extensions/investspec/test/index.rst`, using the `investspec` role. You can look at the output in `extensions/investspec/test/build` for examples of what the role does.
Loading

0 comments on commit fe31914

Please sign in to comment.