Contributing¶
Potato is an academic open-source project, and most of what it does now came from someone needing an annotation type that did not exist yet. Bug fixes, new annotation types, documentation corrections and example projects are all welcome.
There is no Contributor License Agreement to sign. Some projects ask you to sign one so they can relicense your code later; Potato does not. You keep the copyright in what you write, and opening a pull request offers it under GPLv3+, the licence the rest of the repository uses. Participation is covered by the Code of Conduct.
You do not need to open an issue before writing code, though one is welcome if you want a sanity check on an approach first. For anything that touches the schema registry or adds a subsystem, a short description up front usually saves a round of review.
If you have not contributed to an open-source project before, the changes that
land most easily are a documentation correction, a new example config under
examples/, or a fix for a bug you hit yourself. GitHub's
fork-and-pull-request guide
covers the mechanics this guide assumes.
Getting set up¶
Fork the repository on GitHub first and clone your fork, since a pull request comes from your own copy:
git clone https://github.com/YOUR-USERNAME/potato.git
cd potato
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt # runtime dependencies
pip install -r requirements-test.txt # pytest and browser-test tooling
pip install -e . # install Potato from this checkout
setup.py declares python_requires>=3.7; CI runs 3.11, so match that if you
can.
The editable install is not optional
Without -e, pip copies Potato into site-packages and that copy shadows
your checkout. The server starts, your edits do nothing, and nothing warns
you. If a change appears to have no effect, run
python -c "import potato; print(potato.__path__)" from outside the
repository: it should print your checkout, and if it prints a
site-packages path, remove that copy with pip uninstall potato-annotation
and reinstall with -e.
Optional extras pull in dependencies for specific features, none of which are needed to run a basic task:
| Extra | Installs support for |
|---|---|
ai |
OpenAI, Anthropic, Gemini and Ollama endpoints |
formats |
PDF, DOCX, Markdown and spreadsheet ingestion |
preview |
Headless-browser screenshots for potato preview |
export |
Parquet output |
huggingface |
Hugging Face dataset push and pull |
Install them with pip install -e ".[ai,formats]".
Run something¶
Every example is a self-contained project with config.yaml at its root, and
most set task_dir: ., so run them from the repository root:
python potato/flask_server.py start examples/classification/single-choice/config.yaml -p 8000
If you see ConfigSecurityError: Path '..' resolves to ... outside the project
directory, you are in the wrong working directory rather than looking at a bug.
When you are working on the annotation interface itself, skip the login, consent and instruction screens:
python potato/flask_server.py start <config.yaml> -p 8000 --debug --debug-phase annotation
--debug also turns on auto-reload and drops admin authentication, so never
use it on anything reachable from outside your machine.
To check that a config parses and renders without booting a server:
python -m potato.preview_cli config.yaml
python -m potato.preview_cli config.yaml --format html > preview.html
Where things live¶
| Area | Path | What it holds |
|---|---|---|
| Server startup | potato/flask_server.py |
App creation, data loading, CLI entry |
| Routes | potato/routes.py |
Annotation workflow and admin endpoints |
| Item state | potato/item_state_management.py |
Items and assignment strategies |
| User state | potato/user_state_management.py |
Per-user progress, phases, annotations |
| Config | potato/server_utils/config_module.py |
Loading, validation, path security |
| Annotation types | potato/server_utils/schemas/ |
One module per type, plus registry.py |
| Display types | potato/server_utils/displays/ |
How an item is shown, separate from how it is annotated |
| AI endpoints | potato/ai/ |
Model backends for label suggestions |
| Frontend | potato/static/, potato/templates/ |
annotation.js, span-core.js, Jinja templates |
Two things to know before you add anything:
- Routes have to be registered twice. A live
potato startserves the app built bycreate_app(), so a handler that only carries an@app.routedecorator returns 404 in production while working fine in some tests. Add a matchingadd_url_ruleinconfigure_routes(). - Annotation pages and phase pages render through different paths.
Annotation pages go through
render_page_with_annotations(); consent, instructions, training and survey pages go throughget_current_page_html(). A new conditional<script>or template variable inbase_template_v2.htmlhas to be wired into both, or the feature silently does nothing on half the workflow.
Adding an annotation type¶
This is the most common substantial contribution and the one with the longest checklist: a working type also has to persist its annotations and appear in the generated config schema.
1. Write the generator. Create potato/server_utils/schemas/my_schema.py
with a function taking the scheme dict and returning (html, keybindings):
def generate_my_schema_layout(annotation_scheme):
...
return html, keybindings
2. Register it. In potato/server_utils/schemas/registry.py, import the
generator inside _register_builtin_schemas() and add a SchemaDefinition:
SchemaDefinition(
name="my_schema",
generator=generate_my_schema_layout,
required_fields=["name", "description"],
optional_fields=["every", "other", "key", "the", "generator", "reads"],
supports_keybindings=False,
description="One line describing the type",
),
Declare every key the generator reads. optional_fields is the only source for
the published JSON Schema and for the per-type table in
docs/configuration/config_reference.md, so an undeclared key does not exist as
far as editors, agents, or the spec are concerned, and a user's editor will
underline a working option as invalid. Keys handled by shared helpers
(label_requirement, layout, display_logic and friends) belong in
registry.UNIVERSAL_OPTIONAL_FIELDS instead, and keys the server writes onto a
scheme at runtime (annotation_id,
_allocated_keys) belong in INTERNAL_SCHEME_FIELDS and must stay out of the
per-type lists. tests/unit/test_schema_registry_field_coverage.py reads your
module and fails on any key you read but did not declare.
You do not need to touch valid_types in config_module.py. It comes from
schema_registry.get_supported_types(). (Display types are a separate registry
with its own list; adding one of those is a different checklist.)
Set single_select=True only if your type emits several inputs carrying
different label_name values for one logical answer, the way radio and
likert do. Types that emit one fixed label_name and simply overwrite it must
leave it False; setting it wrongly deletes real annotations.
3. Export it from potato/server_utils/schemas/__init__.py, following the
existing lines.
4. Test it in two tiers. A unit test in tests/unit/test_my_schema.py for
the generated HTML, and a Selenium test in
tests/selenium/test_my_schema_ui.py for what a user does with it.
Persistence tests are easy to get wrong. Browsers cache form state across a
refresh, so a test that calls driver.refresh() and then reads a hidden input's
value passes even when the server stored nothing. Navigate away and back
instead:
- Annotate instance N and wait out the 1.5s save debounce.
- Click Next, then Previous.
- Assert on visible state — tile highlighting, checked boxes, CSS classes — not just hidden input values.
- Optionally confirm server-side through
/get_annotations?instance_id=<id>.
Four functions in annotation.js have to handle your inputs, or annotations
will render and then vanish: syncAnnotationsFromDOM(), saveAnnotations(),
clearAllFormInputs() and populateInputValues(). Reusing an existing input
pattern — a hidden input carrying data-modified / data-server-set — is
usually less work than teaching all four about a new one.
See the Testing guide for tier conventions and the rule that test
files must live under tests/.
5. Document it and ship an example. A page under
docs/annotation-types/<category>/my_schema.md, an entry for it in the
mkdocs.yml nav, and a runnable example at
examples/<category>/my-schema-example/ with config.yaml at its root and
sample data in data/. Example configs open with the schema modeline so
editors can validate them:
# yaml-language-server: $schema=https://potatoannotator.readthedocs.io/en/latest/schemas/potato-config.schema.json
MkDocs publishes every file under docs/ whether the nav references it or not,
so an unlisted page is live, indexed, and reachable only by guessing its URL.
tests/unit/test_docs_nav_drift.py checks that every page is listed.
6. Regenerate the derived artifacts. Several checked-in files are generated from the code, and CI compares them against a fresh run:
python scripts/generate_config_schema.py # potato/schemas/potato-config.schema.json
python scripts/generate_config_reference.py # docs/configuration/config_reference.md
python scripts/generate_llms_full.py # after any docs change
7. Look at it. Add your type to the SCHEMAS dict in
scripts/screenshot_batch2.py and run:
python scripts/screenshot_batch2.py --schemas my_schema --output-dir screenshots/verify
Open the _full.png and _form.png output and check for invisible controls,
right-shifted layouts, overlapping labels, and grid types that fail to use the
full width. No test in the suite checks for any of that.
Documentation and examples¶
Anything a user can configure needs a docs page, a nav entry, and at least one complete YAML example. Changing an existing option means updating the affected pages in the same PR.
That said, this is a project expectation rather than a gate on your first contribution. A one-line bug fix needs no new documentation. If you fix something real and the prose around it is beyond what you want to take on, say so in the PR and it can be picked up in review — a fix with missing docs is better than no fix.
House conventions when you do write docs:
- Lowercase-with-hyphens filenames.
- YAML examples in YAML, not JSON-shaped YAML.
- Cross-reference with relative paths:
[Quality Control](quality_control.md). - Screenshots in
docs/img/screenshots/with descriptive names. - Run
python scripts/generate_llms_full.pyafterwards, ortest_llms_full_is_currentfails.
Code style¶
PEP 8, meaningful names, docstrings on public functions and classes, and comments where the logic is not obvious. There is no formatter or linter in CI, so the standard is the surrounding file: match the conventions of the module you are editing rather than reformatting it.
Opening a pull request¶
Branch from master, push the branch to your fork, and open the pull request
against master:
git checkout -b fix/span-offset-drift
# ... work ...
git commit -m "fix(span): keep offsets stable when labels are re-rendered"
git push origin fix/span-offset-drift
GitHub then offers a "Compare & pull request" button on your fork.
Keep the change focused. A bug fix and a refactor in one pull request take several times as long to review as the same work split in two.
Commit messages in this repository follow the type(scope): summary convention
(feat(deploy):, fix(ci):, docs:), and matching it is appreciated, but no
PR gets bounced over a prefix. Explain why the change is right rather than
restating the diff.
Before you open the PR:
- [ ] Run the test tier that matches your change:
pytest tests/unit/for logic,tests/server/for endpoints and state,tests/selenium/for anything a user clicks. The full suite is slow and includes browser tests. - [ ] Check your failures against the known pre-existing failures — several tests fail on a clean checkout and are not your doing.
- [ ] Regenerate any derived artifacts you invalidated (see above).
- [ ] Say in the description what you changed, why, and how you checked it. Screenshots for anything visual.
CI runs on every pull request, but not the full suite: browsers and wall clock
make that impractical. It checks that the checked-in generated files (the config
JSON Schema, the OpenAPI spec, llms-full.txt) still match what the code
produces, runs the fast tests that guard them, and builds the docs with
mkdocs build --strict, where a broken relative link fails the build.
Everything else is on your local run and on review.
Review is done by the maintainers, who are academics with teaching loads, so turnaround varies. What gets looked at first:
- Does it work for cases beyond the one that prompted it: list inputs as well as strings, multiple annotators, an empty dataset?
- Does the state survive a round trip? Anything storing annotations gets checked against the navigate-away-and-back behaviour above.
- Is user-supplied content sanitized? Instance text renders through
sanitize_html;| safein a template without it is an XSS hole. - Are file paths validated? Config paths go through
validate_path_security()so a task cannot read outside its directory. - Is a new config key declared where the generated schema can see it?
Reporting bugs and requesting features¶
File issues at github.com/davidjurgens/potato/issues. The things that make a bug report actionable are the config that triggered it (with any credentials removed), what you expected, what happened instead, your Python version and OS, and the server console output. Browser-console output helps for anything visual.
For a feature request, describe the annotation problem you are trying to solve rather than only the mechanism you have in mind — the problem is often solvable with a type that already exists, and when it is not, it tells us what the new one has to do.
For questions about using Potato rather than developing it, start with the FAQ and the Getting Started guide, or email jurgens@umich.edu.
Related documentation¶
| Document | Covers |
|---|---|
| Developer Guide | Architecture, REST API, extension points |
| Testing | Test tiers, fixtures, drift guards, known failures |
| Configuration Reference | Every config key, generated from the registry |
| Schemas and Templates | Gallery of annotation types |