Skip to content

Contributing#

This guide shows how to report a bug, set up a clone of mathspec, change it, and submit the change. You need git and a GitHub account.

Report a bug or ask a question#

To find a first change to make, start with the good first issues.

Setting up a development environment#

Every command of the project runs in an environment and package manager (pixi), which installs Python and the tools that the checks use.

Fork the repository on GitHub, and clone your fork:

git clone https://github.com/<your-account>/mathspec.git
cd mathspec

Install pixi, as its official instructions describe. Then, in your clone, install the environment and the commit hooks:

pixi install
pixi run pre-commit-install

On every commit, the hooks format Python, Markdown, YAML and TOML. They also lint and type-check the Python, check spelling, and check the licence headers.

To check that the environment works, run the test suite:

pixi run test

The run ends with a summary line that counts the passed tests and has no failed count.

Run the checks before you push#

To run the checks by hand, use these commands:

pixi run lint           # every commit hook, over every file
pixi run test           # the test suite
pixi run test-coverage  # the test suite, with a coverage report
pixi run compile-tex    # every spec in the tree, printed to standalone LaTeX and compiled
pixi run ci             # lint, test, a strict docs build and compile-tex

The checks on each pull request (CI) run the same four tasks as pixi run ci. CI also checks the version headings in CHANGELOG.md.

Change the documentation#

The pages under docs/ are Markdown, and a static site generator (Zensical) builds them from mkdocs.yml. The build is strict, so a dead link or a stale anchor fails it.

Each page is one of the four kinds that Diátaxis defines, and its kind sets its folder:

  • a tutorial, in docs/tutorials/
  • a how-to guide, in docs/howto/
  • reference, in docs/reference/, with the example pages in docs/examples/
  • explanation, in docs/about/, which is the About section of the nav

A page that a spec writer does not need goes under Development in the nav. The docs-writing skill sets the rules that each kind meets. The prose-writing skill sets the rules for sentences in pages, issues and PRs. Inside docs/, link relatively. Outside it, write the full GitHub URL.

To preview the site while you edit, run:

pixi run docs-serve

The site is then at http://127.0.0.1:8000, and it rebuilds when you save a page.

I have updated the README.md

The home page includes six named sections of the README, and not a copy:

  • the badges
  • the benefits
  • what a spec declares
  • the example spec
  • the engines
  • the status note

A section is delimited in the README by --8<-- "README.md:name". Edit inside the markers, and the next build shows the change.

Keep the sections free of links, or link absolutely. A relative link resolves against docs/index.md on the site and against the repository root on GitHub, and only one of those can be right.

I have changed what a spec prints

A tool writes some blocks of the docs, and tests/test_docs.py lists every page that contains one in its GENERATED table. A test compares each block to its generator. To change a block, run its generator and read the diff:

pixi run python -m tools.home_math       # docs/index.md and README.md, from examples/dispatch.yaml
pixi run python -m tools.notation        # docs/reference/notation.md, from tests/typesetting/golden/model.yaml
pixi run python -m tools.spec_math       # the operator table on docs/reference/language/operators.md
pixi run python -m tools.gallery         # the example pages, from examples/
pixi run python -m tools.expansion_math  # docs/howto/see-an-expansion.md, from tests/expand/

Each of these tools takes --check, which reports drift and writes nothing. Two more sets of files are generated, and their failing tests print the same commands:

pixi run python -m tools.schema              # schema/mathspec.schema.json
pixi run python -m tests.typesetting.golden  # tests/typesetting/golden/*.out
I want to add a new page

Add a Markdown file under the folder of its kind, and add it to the nav key in mkdocs.yml:

nav:
  - How-to guides:
      - howto/index.md
      - My page: howto/my-page.md

The page then appears under How-to guides. pixi run test fails for a page with no nav entry.

The Python API has three fixed pages, and a new module gets no page of its own. A plugin that writes pages from docstrings (mkdocstrings) renders the Spec API and Program API pages from the whole of mathspec.spec and mathspec.program. A new name in either module appears in the next build. A new name at the top level of mathspec needs its own ::: entry on the Python API page.

What a new operator has to satisfy#

A construct built into the language, which no file can add, is an operator. mathspec accepts a new operator only when it meets these conditions:

  • A template substituted into an expression (a macro) can call the operator. Everything a modeller passes to the operator is the value of a keyword argument, such as over=snapshot.
  • It reads a bounded number of rows for each row of its result. It may filter rows on a column they already have, or join each row against a parameter or a relation. It may also read neighbouring rows at a fixed offset, or only the coordinate labels.
  • If it reads every row, it reads them in one pass over the data. That pass ends before a tool that builds the model (an engine) builds its first chunk of rows.
  • It does not call itself. mathspec refuses the same recursion in a macro and suggests which call to remove.

New constructs explains the first two conditions.

Name the classes#

One construct passes through three layers of the code. Each class name has a suffix that identifies its layer:

Layer Suffix Example
YAML (mathspec.spec) Spec VariableSpec, PiecewiseSpec
Syntax (mathspec._expression_parser, mathspec._condition_parser) Node NameNode, UnresolvedComparisonNode
Program (mathspec.program) none Variable, Piecewise

In the program, the node for a use of a name ends in Reference. VariableReference refers to a Variable, and ExpressionReference refers to a NamedExpression.

A node class is named after the operation, not after the operator in the file. One operator can resolve to two nodes:

Operator Node Operation
sum(over=) Sum a sum that removes the dimensions it names
sum(by=) Sum over a Join a join, and the sum over the axes that the join adds
at(by=) Join a join whose groups are one row, with no sum over it
shift(along=) Translate a re-index along one dimension
sum_back(along=) WindowSum a sum over a trailing window

No name is abbreviated, except the acronym Sos and the key dims.

Add an operator#

An operator has a class in every layer of the code, so you change each layer in order:

  1. Check that the operator meets the conditions.
  2. Start with the grammar, which usually needs no change, because f(x, k=v) already parses.
  3. Declare the signature in _operators.BUILTINS. The signature lists the keyword arguments by kind, and the usage line that a refusal quotes.
  4. Write the node in program.py, and how _expression_resolver.py builds the node from the signature. Name the node class as Name the classes sets.
  5. Write the dimension rule in _dimensions.py, and the degree rule in _degree.py.
  6. Write how _typesetting prints the node in LaTeX, Typst and Markdown.
  7. Call the operator in tests/typesetting/golden/model.yaml. A test fails until that file calls every operator.
  8. Add the entry to the language reference.

Then regenerate the golden files and the notation page, and read the diff:

pixi run python -m tests.typesetting.golden
pixi run python -m tools.notation

The operator is finished when three things are true:

  • to_spec compiles it into the program.
  • The typesetter prints it in all three formats.
  • An engine builds the same model from a spec that uses the new operator and from that spec written out by hand.

Submit a change#

To contribute a change:

  1. Fork the repository and set up a development environment, as the development guide describes.
  2. In your clone, create a feature branch (git checkout -b new-fix-or-feature).
  3. Commit your changes to the feature branch.
  4. Push the branch to GitHub (git push origin new-fix-or-feature).
  5. On GitHub, open a pull request from the feature branch.

When you contribute for the first time, ask your reviewer to add you as a contributor.

Open a pull request#

Before you open a pull request (PR), check that you have:

  • Written the PR title as a conventional commit subject, as Write the PR title describes.
  • Added the PR's line to CHANGELOG.md, if the PR needs one.
  • Added or updated the documentation for your change.
  • Added tests for new functionality.

In the PR body, summarise what changed and why.

Write the PR title#

Merges are squashed, so the PR title becomes the commit on main. Write it so it also works as the line you add to CHANGELOG.md by hand. It must be a conventional commit subject:

<type>[(scope)]: <subject>

feat(language): a coordinate may declare its own label space
fix(parser): a where clause with a trailing comma loads
docs(language): a comparison on a relation is false where the relation has no row

A feat, fix, perf, refactor, docs or revert PR adds its title, with a link to the PR, under ## Upcoming version in CHANGELOG.md. A chore, test, ci, build or style PR adds no line. The Conventional commit subject check enforces the format on every pull request, and the Changelog line check enforces the line. The label no changelog opts a PR out of the second. See RELEASING.md.

Beyond the subject line, write the body that the change needs: what changed, and its impact.

Follow the code conventions#

The code follows the Style Guide for Python Code (PEP 8). The formatter and linter ruff applies the rules that pyproject.toml sets, such as the maximum line length. The commit hooks run ruff on every commit. You can also set up your editor to run ruff when you save a file, as the ruff documentation describes.

Write a docstring in the Google docstring style. In src/, ruff checks the form of each docstring, but it does not require a docstring on every name.

The pull request page then lists the CI checks. The change is ready for review when every check passes.

See also