Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .claude/skills/docs-writing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,10 @@ section, saying why a reader would open it.

**The Python API is rendered from the docstrings.**
`docs/reference/api.md` holds one `:::` entry per name in `math_spec.__all__`,
and mkdocstrings renders each from its docstring. `docs/static/hooks.py`
renders one page per module under `src/math_spec/`, and puts them in the
Contributing group of the Development section as `Modules`. The prose of both is the docstring rules in
`AGENTS.md`.
for a model writer. `docs/reference/program.md` renders `math_spec.program`,
for whoever builds on the program. mkdocstrings renders both from the
docstrings, so their prose is the docstring rules in `AGENTS.md`. No other
module gets a page: an internal module is read in the source.

Mixing kinds is the most common failure. Rationale inside a reference section
makes the rules unskimmable, and rules inside an explanation page make the
Expand All @@ -117,7 +117,7 @@ prints from it. What a consumer does with a spec — the data it attaches, how i
solves, what it reads back — is that consumer's page, not this tree's
([what counts as language](../../../docs/about/what-counts-as-language.md)).
A rule about a consumer says only what the file guarantees it
([reading a loaded model](../../../docs/reference/reading.md)).
([reading a spec and its program](../../../docs/reference/reading.md)).

Answer the two questions before starting. If a page needs two kinds, it is
two sections with two headings, or two pages.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,8 @@ ms.to_typst(spec) # compiles without a TeX toolchain
A [symbol table](docs/reference/typeset.md#symbol-tables) gives the names their
conventional spelling, as in the first folded block.
[Print a model as math](docs/howto/print.md) does the same from a shell.
`to_spec` returns a `Spec`, and `spec.program` the model it builds
([reading a loaded model](docs/reference/reading.md#spec-and-program)).
`to_spec` returns a `Spec`, and `spec.program` its `Program`
([reading a spec and its program](docs/reference/reading.md#spec-and-program)).

## Documentation

Expand Down
48 changes: 0 additions & 48 deletions docs/about/file-and-program.md

This file was deleted.

2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ call.
- [Print a model as math](howto/print.md): LaTeX, Typst or Markdown, from the
file alone.
- [Check a model without data](howto/check.md): on your machine and in CI.
- [Reading a loaded model](reference/reading.md): for whoever writes an engine
- [Reading a spec and its program](reference/reading.md): for whoever writes an engine
or a renderer.

## Install it
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ task.

## Loading

The module `math_spec.program` holds the node and declaration classes of a
loaded model. [Reading a loaded model](reading.md) documents them.
The module `math_spec.program` holds the classes a `Program` is made of. The
[Program API](program.md) documents them.

::: math_spec.to_spec
options:
Expand Down
7 changes: 6 additions & 1 deletion docs/reference/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ its [language page](language/index.md).

## The file and what reads it

**Model**
: The optimisation problem a file states: its dimensions, the data it expects,
its decisions and its rules. A model holds no data. The Python objects that
hold it are named for what they are, a `Spec` or a `Program`.

**Spec**
: The file as written, checked: what `to_spec` returns
([reading a loaded model](reading.md#spec-and-program)).
([reading a spec and its program](reading.md#spec-and-program)).

**Program**
: What the file means, `spec.program`: every name typed, every macro expanded,
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/language/assumptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,5 +121,5 @@ the same _Assumptions_ heading as the written ones.
| `curve_breakpoints` | `lp` | each curve has at least two breakpoints |
| `curve_contiguous` | a block `points:` | the marked breakpoints are one consecutive run of at least one |

[Reading a loaded model](../reading.md#what-the-data-has-to-satisfy) says how
[Reading a spec and its program](../reading.md#what-the-data-has-to-satisfy) says how
a consumer runs them.
21 changes: 21 additions & 0 deletions docs/reference/program.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!--
SPDX-FileCopyrightText: math-spec contributors
SPDX-License-Identifier: CC-BY-4.0
-->

# Program API

This page documents every name that `math_spec.program` exports: the
declarations, the expression and predicate nodes, and the reports a program
answers. [Reading a spec and its program](reading.md) says how they fit together.

<!-- prettier-ignore-start -->

::: math_spec.program
options:
show_root_heading: false
show_root_toc_entry: false
heading_level: 2
members_order: alphabetical

<!-- prettier-ignore-end -->
20 changes: 16 additions & 4 deletions docs/reference/reading.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ SPDX-FileCopyrightText: math-spec contributors
SPDX-License-Identifier: CC-BY-4.0
-->

# Reading a loaded model
# Reading a spec and its program

This page is for whoever writes an engine that builds models, a renderer, or a
checker. A tool reads the model through two objects, `Spec` and `Program`.
Expand All @@ -14,9 +14,21 @@ A `Spec` holds the file as written: its `macros:`, its descriptions, and a
`piecewise:` block as one block. A `Program` holds the model the file builds:
every macro expanded, every name typed, every operator resolved to a node, and
every dimension and degree rule already checked. A curve stays one curve there
until [`spec.expand()`](#formulations-written-out) writes it out.
[The file and the program](../about/file-and-program.md) says why the two are
split, and which tool reads which.
until [`spec.expand()`](#formulations-written-out) writes it out. The
[Program API](program.md) documents every class a program holds.

Each tool reads the object that holds what it needs:

| Tool | Reads |
| -------------------------- | ------------------------------------------- |
| The typesetter | `spec.program`, or a `Program` handed to it |
| `advice` | `spec.program` |
| An engine that builds rows | the program of an expansion |
| A tool that rewrites files | the `Spec`, which alone holds the text |

The program keeps each curve as the one declaration the file states, so the
typesetter and `advice` read the model the author wrote. A program does not
hold its spec: a tool handed a bare `Program` has the model, not the file.

The curve below [expands](language/piecewise.md) into a weight per breakpoint,
a convexity row and one row per link:
Expand Down
113 changes: 1 addition & 112 deletions docs/static/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,15 @@
"""Hooks to run when building documentation."""

import re
import tempfile
from pathlib import Path

import mkdocs.plugins
from mkdocs.structure.files import File

TEMPDIR = tempfile.TemporaryDirectory()

# Add to this list if you want to ignore any source files from the documentation API reference
API_FILES_TO_IGNORE: list[str] = []


@mkdocs.plugins.event_priority(50)
def on_files(files: list, config: dict, **kwargs) -> list:
"""Link (1) top-level files to mkdocs files and (2) generate the python API documentation.
"""Link the top-level files the site shows, such as the changelog, into the docs.

Args:
files (list): mkdocs file list.
Expand All @@ -32,10 +26,6 @@ def on_files(files: list, config: dict, **kwargs) -> list:
for file in Path('./resources').glob('**/*.*'):
files.append(_new_file(file, config))
files.append(_new_file(Path('./CHANGELOG.md'), config))

api_nav = _api_gen(files, config)
_update_nav(api_nav, config)

return files


Expand All @@ -61,97 +51,6 @@ def _new_file(path: Path, config: dict, src_dir: str = '.') -> File:
)


def _api_gen(files: list, config: dict) -> dict:
"""Project Python API generator.

Args:
files (list): mkdocs file list.
config (dict): mkdocs config dictionary.

Returns:
dict: Python API navigation tree, to add into the mkdocs `nav` tree.
"""
source_dir = Path(config['watch'][0])
source_file = source_dir.parts[-1]
api_nav: dict = {'top_level': []}
for filepath in sorted(source_dir.rglob('[!_]*.py')):
rel_filepath = filepath.relative_to(source_dir)
if rel_filepath.as_posix() in API_FILES_TO_IGNORE:
continue
file = _py_to_md(source_file / rel_filepath, api_nav, config)
files.append(file)
return api_nav


def _py_to_md(filepath: Path, api_nav: dict, config: dict) -> File:
"""Create a markdown file for the API documentation for a given python file in the package source directory.

Markdown files are stored in a temporary directory, which will be cleaned after mkdocs has finished building the docs.

Args:
filepath (Path): Path to python file relative to the package source code directory.
api_nav (dict): Nested dictionary to fill with mkdocs navigation entries.
config (Config): mkdocs config dictionary.

Returns:
File: mkdocs object that links the temp file to the docs directory, ready to be added to the mkdocs file list.
"""
module_parts = filepath.with_suffix('').parts

module_name = '.'.join(module_parts)

api_file = 'reference' / filepath.with_suffix('.md')
api_full_filepath = Path(TEMPDIR.name) / api_file
api_full_filepath.parent.mkdir(exist_ok=True, parents=True)
api_full_filepath.write_text(f'::: {module_name}')

nav_component = {module_name: api_file.as_posix()}
if len(module_parts) > 2: # i.e., in a nested directory
parent_module = '.'.join(module_parts[:2])
if parent_module not in api_nav:
api_nav[parent_module] = [nav_component]
else:
api_nav[parent_module].append(nav_component)
else:
api_nav['top_level'].append(nav_component)
return _new_file(api_file, config, TEMPDIR.name)


def _update_nav(api_nav: dict, config: dict) -> None:
"""Append the per-module API pages to the Contributing group of Development, as `Modules`.

Mkdocs navigation is composed of lists of dictionaries.
Lists nesting defines navigation nesting, dictionary keys are the page names, and values are the pointers to markdown files.

Args:
api_nav (dict): Python API navigation tree.
config (dict): mkdocs config dictionary (in which `nav` can be found).
"""
modules_nav = {'Modules': [*api_nav.pop('top_level'), *[{k: v} for k, v in api_nav.items()]]}
_get_nav_list(_get_nav_list(config['nav'], 'Development'), 'Contributing').append(modules_nav)


def _get_nav_list(nav: list[dict | str], ref: str) -> list:
"""Get navigation entry sub-page list.

Navigation list entries can be dictionaries or strings.
Sub-list entries can then also be dictionaries or strings. E.g.,

```python
[{"Page Title": ["sub-page-1", {"Sub-Page-2 Title": "sub-page-2"}, ...], ...}]
```

Args:
nav (list[dict | str]): MKdocs `nav` config entry.
ref (str): Page title reference to return the sub-list of.

Returns:
list: Nav sub-list linked to `ref`.
"""
nav_ref = [idx for idx in nav if isinstance(idx, dict) and set(idx.keys()) == {ref}][0]
return nav_ref[ref]


#: A fenced block, indented or not — its contents are nobody's to rewrite, and
#: a ```math one is the superfences entry's in `mkdocs.yml`.
FENCED_BLOCK = re.compile(r'^[ \t]*```.*?^[ \t]*```$', re.DOTALL | re.MULTILINE)
Expand Down Expand Up @@ -188,13 +87,3 @@ def on_page_markdown(markdown: str, **kwargs) -> str:
kept = FENCED_BLOCK.sub(lambda m: spans.append(m[0]) or f'\x00{len(spans) - 1}\x00', markdown)
rewritten = GITHUB_INLINE_MATH.sub(lambda m: f'${m["math"]}$', kept)
return re.sub(r'\x00(\d+)\x00', lambda m: spans[int(m[1])], rewritten)


@mkdocs.plugins.event_priority(-100)
def on_post_build(**kwargs):
"""After mkdocs has finished building the docs, remove the temporary directory of markdown files.

Args:
**kwargs: Automatic MKDocs hook inputs.
"""
TEMPDIR.cleanup()
7 changes: 3 additions & 4 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,11 @@ nav:
# renderer, a checker), whoever changes math-spec itself, and the proofs of
# concept: the notation page, which renders the typesetting test model, and
# the PyPSA pages. The PyPSA pages stay in `docs/examples/`, where
# `tools/gallery.py` writes them. `docs/static/hooks.py` appends one page per module under
# `src/math_spec/` to Contributing, as `Modules`.
# `tools/gallery.py` writes them.
- Development:
- Building on math-spec:
- Reading a loaded model: reference/reading.md
- The file and the program: about/file-and-program.md
- Reading a spec and its program: reference/reading.md
- Program API: reference/program.md
- What counts as language: about/what-counts-as-language.md
- Contributing:
- contributing.md
Expand Down
2 changes: 1 addition & 1 deletion src/math_spec/typesetting/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def typeset(

Args:
model: Anything :func:`math_spec.to_spec` accepts, or a
:class:`~math_spec.program.Program`. A loaded model or a program
:class:`~math_spec.program.Program`. A ``Spec`` or a ``Program``
is rendered as it stands, so printing one model in several formats
reads and checks the file once rather than once per format, and a
curve prints as the curve it states. Pass ``spec.expand()`` for the rows a solver holds
Expand Down
Loading