diff --git a/CHANGELOG.md b/CHANGELOG.md index 83ab8671..10ca947b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ it releases that version ([RELEASING.md](https://github.com/energy-models/mathsp - feat(language)!: `missing:` says what a missing row means, a table short of a row is refused unless the file says otherwise, and a variable's `absence:` is now `missing:` ([#810](https://github.com/energy-models/mathspec/pull/810)) - docs(pypsa): a risk preference with weight zero builds the CVaR variables and refuses quadratic costs, as in PyPSA ([#849](https://github.com/energy-models/mathspec/pull/849)) - docs(pypsa): storage that retires before the last counted snapshot closes a global limit at its last active level ([#850](https://github.com/energy-models/mathspec/pull/850)) +- refactor(api): the top level holds what you call, and spec, program and errors hold what you get back or catch ([#837](https://github.com/energy-models/mathspec/pull/837)) - refactor(program)!: the node a use of a named expression stands as is `NamedExpression`, beside `NamedMask`, and `Named` is gone ([#822](https://github.com/energy-models/mathspec/pull/822)) - docs(pypsa): the pypsa spec names its repeated row conditions as masks, and its topic files read them under `given: masks:` ([#825](https://github.com/energy-models/mathspec/pull/825)) - feat(language): a where predicate is named once under `masks:`, and another file reads it under `given: masks:` ([#821](https://github.com/energy-models/mathspec/pull/821)) diff --git a/docs/about/what-counts-as-public-api.md b/docs/about/what-counts-as-public-api.md index af55dc5a..6bdafd1e 100644 --- a/docs/about/what-counts-as-public-api.md +++ b/docs/about/what-counts-as-public-api.md @@ -22,6 +22,22 @@ A function may join the public API when both of these hold: Wherever a feature can be a key in the file, it is one: a key shows up in a git diff, the typesetter prints it, and an engine in another language reads it. +## Where a name lives + +The top level holds what you call. Every other public name lives in one of +three modules, and the module follows from how you get the name: + +| You get it | It lives in | Such as | +| --------------------------- | ------------------ | ---------------------------------------- | +| by calling a function | `mathspec` | `to_spec`, `merge`, `typeset`, `advice` | +| from what a `Spec` holds | `mathspec.spec` | `Spec`, `VariableBlock`, `BUILTIN_NAMES` | +| from what a `Program` holds | `mathspec.program` | `Program`, `Sum`, `Mask`, `Advice` | +| by catching it | `mathspec.errors` | `LanguageError`, `SchemaError` | + +`SymbolTable` and `FormatName` are at the top level too: you build or name +them to pass them to the typesetter. `tests/test_public_surface.py` holds each +module to its rule. + ## What every function keeps - **No state.** No registry, no plugin, and no setting that changes what a @@ -32,7 +48,7 @@ diff, the typesetter prints it, and an engine in another language reads it. talks about a file the language accepts, and changes nothing. - **Nothing is written out unasked.** A `piecewise:` or `sos:` block stays the block until a caller calls - [`spec.expand()`](../reference/api.md#mathspec.Spec.expand). + [`spec.expand()`](../reference/spec.md#mathspec.spec.Spec.expand). What a solver or file format can take, how the numbers attach to the names, and which solver runs are each engine's to decide diff --git a/docs/howto/compare.md b/docs/howto/compare.md index a40b1005..2d564df7 100644 --- a/docs/howto/compare.md +++ b/docs/howto/compare.md @@ -74,7 +74,7 @@ lists what the form sorts and what it keeps. ``` 5. **Compare from Python** where the comparison is one step of a longer - script. [`to_yaml`](../reference/api.md#mathspec.Spec.to_yaml) writes the + script. [`to_yaml`](../reference/spec.md#mathspec.spec.Spec.to_yaml) writes the same text with `canonical=True`: ```python diff --git a/docs/howto/see-an-expansion.md b/docs/howto/see-an-expansion.md index c7ad947e..0112ce25 100644 --- a/docs/howto/see-an-expansion.md +++ b/docs/howto/see-an-expansion.md @@ -428,7 +428,7 @@ the set out too. -[`Spec.expand()`](../reference/api.md#mathspec.Spec.expand) lists what +[`Spec.expand()`](../reference/spec.md#mathspec.spec.Spec.expand) lists what the call accepts, and [writing a formulation out](../reference/language/piecewise.md#writing-a-formulation-out) says what each block emits. diff --git a/docs/reference/api.md b/docs/reference/api.md index bd78e246..1b779ce6 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -6,27 +6,24 @@ SPDX-License-Identifier: CC-BY-4.0 # Python API This page documents every name that `import mathspec` exports, grouped by -task. +task. The top level holds what you call. Three modules hold the rest: + +| Module | Holds | Documented on | +| ------------------ | ------------------------------------------------------- | ------------------------- | +| `mathspec.spec` | what the file says: `Spec` and its blocks | [Spec API](spec.md) | +| `mathspec.program` | what the file means: `Program`, its nodes, and `Advice` | [Program API](program.md) | +| `mathspec.errors` | what you catch: the error tree, and `did_you_mean` | [Errors](#errors) below | ## Loading -The module `mathspec.program` holds the classes a `Program` is made of. The -[Program API](program.md) documents them. - ::: mathspec.to_spec options: show_root_heading: true show_root_toc_entry: true heading_level: 3 -::: mathspec.Spec - options: - show_root_heading: true - show_root_toc_entry: true - heading_level: 3 - ## Composing [Compose a spec from several files](../howto/compose.md) shows both in use. @@ -75,7 +72,7 @@ The module `mathspec.program` holds the classes a `Program` is made of. The show_root_toc_entry: true heading_level: 3 -::: mathspec.FORMATS +::: mathspec.FormatName options: show_root_heading: true show_root_toc_entry: true @@ -89,19 +86,9 @@ The module `mathspec.program` holds the classes a `Program` is made of. The ## Advice -::: mathspec.advice - options: - show_root_heading: true - show_root_toc_entry: true - heading_level: 3 - -::: mathspec.Advice - options: - show_root_heading: true - show_root_toc_entry: true - heading_level: 3 +`advice` returns a tuple of [`Advice`](program.md#mathspec.program.Advice). -::: mathspec.AdviceKind +::: mathspec.advice options: show_root_heading: true show_root_toc_entry: true @@ -109,42 +96,11 @@ The module `mathspec.program` holds the classes a `Program` is made of. The ## Errors -::: mathspec.MathSpecError - options: - show_root_heading: true - show_root_toc_entry: true - heading_level: 3 - -::: mathspec.LanguageError - options: - show_root_heading: true - show_root_toc_entry: true - heading_level: 3 - -::: mathspec.SchemaError - options: - show_root_heading: true - show_root_toc_entry: true - heading_level: 3 - -::: mathspec.DimensionError - options: - show_root_heading: true - show_root_toc_entry: true - heading_level: 3 - -## Names - -::: mathspec.BUILTIN_NAMES - options: - show_root_heading: true - show_root_toc_entry: true - heading_level: 3 - -::: mathspec.did_you_mean +::: mathspec.errors options: show_root_heading: true show_root_toc_entry: true heading_level: 3 + members_order: source diff --git a/docs/reference/language/errors.md b/docs/reference/language/errors.md index 83ff9126..6fb75ac8 100644 --- a/docs/reference/language/errors.md +++ b/docs/reference/language/errors.md @@ -24,7 +24,7 @@ Check for typos, or ensure 'p_charge' is declared. ## What `advice` warns about -`ms.advice(spec)` returns a tuple of `ms.Advice`, one per warning, and +`ms.advice(spec)` returns a tuple of `mathspec.program.Advice`, one per warning, and `python -m mathspec check spec.yaml` prints them. Advice is a warning: the file loads. diff --git a/docs/reference/language/piecewise.md b/docs/reference/language/piecewise.md index b9cc22fe..b5dafbce 100644 --- a/docs/reference/language/piecewise.md +++ b/docs/reference/language/piecewise.md @@ -191,7 +191,7 @@ expansion writes that the file already declares is refused at load too. ## Writing a formulation out Writing a formulation out replaces the block with the variables and constraints -it states. [`Spec.expand()`](../api.md#mathspec.Spec.expand) is the +it states. [`Spec.expand()`](../spec.md#mathspec.spec.Spec.expand) is the call, and [see what a curve or a set expands to](../../howto/see-an-expansion.md) shows a spec before and after. diff --git a/docs/reference/program.md b/docs/reference/program.md index eeb68604..382e4a8f 100644 --- a/docs/reference/program.md +++ b/docs/reference/program.md @@ -6,8 +6,8 @@ SPDX-License-Identifier: CC-BY-4.0 # Program API This page documents every name that `mathspec.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. +declarations, the expression and predicate nodes, the reports a program +answers, and the `Advice` that [`advice`](api.md#mathspec.advice) returns. [Reading a spec and its program](reading.md) says how they fit together. diff --git a/docs/reference/reading.md b/docs/reference/reading.md index 40b107f6..9eb8f6d7 100644 --- a/docs/reference/reading.md +++ b/docs/reference/reading.md @@ -90,7 +90,7 @@ is one the file declared. ## Formulations written out A program holds each curve and each set as one declaration until -[`Spec.expand()`](api.md#mathspec.Spec.expand) writes it out. An engine that +[`Spec.expand()`](spec.md#mathspec.spec.Spec.expand) writes it out. An engine that builds rows reads the program of `spec.expand('piecewise')` if it takes a set, and the program of `spec.expand()` if it does not. The program of an expansion holds no curve: diff --git a/docs/reference/spec.md b/docs/reference/spec.md new file mode 100644 index 00000000..34fdaa5c --- /dev/null +++ b/docs/reference/spec.md @@ -0,0 +1,23 @@ + + +# Spec API + +This page documents every name that `mathspec.spec` exports: `Spec`, the +blocks its sections hold, and the operator names an expression may call. +[`to_spec`](api.md#mathspec.to_spec) returns a `Spec`. +[Reading a spec and its program](reading.md) says how a spec and its program +fit together. + + + +::: mathspec.spec + options: + show_root_heading: false + show_root_toc_entry: false + heading_level: 2 + members_order: alphabetical + + diff --git a/docs/reference/typeset.md b/docs/reference/typeset.md index 8db03f81..a6451a33 100644 --- a/docs/reference/typeset.md +++ b/docs/reference/typeset.md @@ -46,7 +46,7 @@ a flag. The [Python API](api.md#typesetting) gives each signature. - The spec's `description:` opens the document. - A `piecewise:` block prints as one line: the curve it states, over the frame it states one curve per coordinate of. To print its rows, print - [`spec.expand()`](api.md#mathspec.Spec.expand) or pass `--expand` + [`spec.expand()`](spec.md#mathspec.spec.Spec.expand) or pass `--expand` ([see an expansion](../howto/see-an-expansion.md)). - An [`assumptions:`](language/assumptions.md) entry prints under an **Assumptions** heading, last, beside what each curve assumes of its diff --git a/mkdocs.yml b/mkdocs.yml index 1d4d4822..b10b778f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -87,6 +87,7 @@ nav: - Development: - Building on mathspec: - Reading a spec and its program: reference/reading.md + - Spec API: reference/spec.md - Program API: reference/program.md - What counts as language: about/what-counts-as-language.md - Contributing: diff --git a/schema/mathspec.schema.json b/schema/mathspec.schema.json index 7343eafc..b7e40315 100644 --- a/schema/mathspec.schema.json +++ b/schema/mathspec.schema.json @@ -984,7 +984,7 @@ }, "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, - "description": "The declared math \u2014 one YAML file, or one dict, validated. Nothing here has seen data.\n\nA ``Spec`` that exists has passed the whole language: constructing one by\nany route \u2014 ``to_spec``, [`model_validate`][], the constructor \u2014 runs\nevery load-time check, expression pass included, and raises\n[`LanguageError`][] on a spec the language refuses.\nHolding one is the proof, so nothing downstream checks it again.\n\nThe API is the thirteen declaration sections plus ``version`` and\n``description``, three ways back out \u2014 [`to_dict`][] for the spec as\ndata, [`to_yaml`][] for the file a reviewer reads, [`expand`][] for the\nspec with its formulations written out as plain rows \u2014 and [`program`][], the\nspec typed, which every reader after load walks. Everything else on this\nclass is pydantic's, not a contract this package keeps.", + "description": "The declared math \u2014 one YAML file, or one dict, validated. Nothing here has seen data.\n\nA ``Spec`` that exists has passed the whole language: constructing one by\nany route \u2014 ``to_spec``, [`model_validate`][], the constructor \u2014 runs\nevery load-time check, expression pass included, and raises\n[`LanguageError`][mathspec.errors.LanguageError] on a spec the language refuses.\nHolding one is the proof, so nothing downstream checks it again.\n\nThe API is the thirteen declaration sections plus ``version`` and\n``description``, three ways back out \u2014 [`to_dict`][] for the spec as\ndata, [`to_yaml`][] for the file a reviewer reads, [`expand`][] for the\nspec with its formulations written out as plain rows \u2014 and [`program`][], the\nspec typed, which every reader after load walks. Everything else on this\nclass is pydantic's, not a contract this package keeps.", "properties": { "assumptions": { "additionalProperties": { diff --git a/src/mathspec/__init__.py b/src/mathspec/__init__.py index 505b6214..d4a1a247 100644 --- a/src/mathspec/__init__.py +++ b/src/mathspec/__init__.py @@ -4,21 +4,26 @@ """The language: what a YAML file may say, and what it means. -Two public states — a [`Spec`][] is what the file *says*, -and its [`program`][mathspec.spec.Spec.program] is what it *means* — and -[`to_spec`][], the one door to both. Everything between them — both grammars -and the tree they build — is package-private, because a consumer reads a -program instead. +The top level is what a consumer calls: [`to_spec`][], the one door to a +file, and the verbs over what it returns. The rest of the surface is three +modules, each with one rule: + +- [`mathspec.spec`][] is what the file *says*: [`Spec`][mathspec.spec.Spec] + and the blocks it holds. +- [`mathspec.program`][] is what the file *means*: the + [`Program`][mathspec.program.Program] a spec lowers to, its nodes, and the + [`Advice`][mathspec.program.Advice] the language gives about it. +- [`mathspec.errors`][] is what a consumer catches. + +Everything between them — both grammars and the tree they build — is +package-private, because a consumer reads a program instead. """ -from mathspec import program +from mathspec import errors, program, spec from mathspec.advising import advice from mathspec.composition import merge, override -from mathspec.errors import Advice, AdviceKind, DimensionError, LanguageError, MathSpecError, SchemaError, did_you_mean -from mathspec.operators import BUILTIN_NAMES -from mathspec.spec import Spec from mathspec.typesetting import ( - FORMATS, + FormatName, SymbolTable, to_latex, to_markdown, @@ -29,21 +34,14 @@ from mathspec.validation import to_spec __all__ = [ - 'BUILTIN_NAMES', - 'FORMATS', - 'Advice', - 'AdviceKind', - 'DimensionError', - 'LanguageError', - 'MathSpecError', - 'SchemaError', - 'Spec', + 'FormatName', 'SymbolTable', 'advice', - 'did_you_mean', + 'errors', 'merge', 'override', 'program', + 'spec', 'to_latex', 'to_markdown', 'to_spec', diff --git a/src/mathspec/advising.py b/src/mathspec/advising.py index ccb7092a..939e1464 100644 --- a/src/mathspec/advising.py +++ b/src/mathspec/advising.py @@ -12,8 +12,7 @@ from typing import TYPE_CHECKING from mathspec.boundedness import unbounded_notes -from mathspec.errors import Advice -from mathspec.program import Join, Program, walk +from mathspec.program import Advice, Join, Program, walk from mathspec.validation import to_spec if TYPE_CHECKING: diff --git a/src/mathspec/boundedness.py b/src/mathspec/boundedness.py index f3546b83..422b14d5 100644 --- a/src/mathspec/boundedness.py +++ b/src/mathspec/boundedness.py @@ -15,9 +15,9 @@ from typing import TYPE_CHECKING, Literal, assert_never -from mathspec.errors import Advice from mathspec.program import ( Add, + Advice, Cases, Constant, Divide, diff --git a/src/mathspec/errors.py b/src/mathspec/errors.py index 6da5172a..8eca8e7f 100644 --- a/src/mathspec/errors.py +++ b/src/mathspec/errors.py @@ -2,13 +2,12 @@ # # SPDX-License-Identifier: MIT -"""What the language says back about a file: the errors it raises, and the advice it gives.""" +"""What the language raises: the error tree, and the one wording a consumer's own refusals share.""" from __future__ import annotations import difflib -from dataclasses import dataclass -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING if TYPE_CHECKING: from collections.abc import Iterable @@ -16,31 +15,8 @@ from pydantic import ValidationError -#: Which pass an [`Advice`][] comes from. Closed, like the operator set: a -#: consumer filtering on it can enumerate every value. -AdviceKind = Literal['never-an-axis', 'given', 'unbounded'] - - -@dataclass(frozen=True) -class Advice: - """One thing the language advises about a file it accepts. - - Never an error: each is what a half-written spec looks like too. A - consumer prints it, or filters on ``kind`` and ``subject``; the text is the - language's, so no consumer writes its own. - - Attributes: - kind: The pass that said it. - subject: The declaration it is about — a dimension name, a variable name. - text: The sentence, naming the rewrite. - """ - - kind: AdviceKind - subject: str - text: str - - def __str__(self) -> str: - return self.text +#: What ``mathspec.errors`` promises a consumer. +__all__ = ['DimensionError', 'LanguageError', 'MathSpecError', 'SchemaError', 'did_you_mean'] class MathSpecError(ValueError): diff --git a/src/mathspec/program.py b/src/mathspec/program.py index 65c760b3..bb11278c 100644 --- a/src/mathspec/program.py +++ b/src/mathspec/program.py @@ -7,13 +7,14 @@ The second public state, and the one a consumer reads. A [`Program`][] is the file typed, section for section: every declaration it makes, with names resolved, shapes fixed and every rule decidable without data checked, and no -data at all. Lowering, as a [`Spec`][] loads, is the only +data at all. Lowering, as a [`Spec`][mathspec.spec.Spec] loads, is the only thing that builds one, so nothing here re-checks a hand-built one. Node and declaration classes are matched with ``isinstance``. The rules a node's structure does not show is [`children`][]; the questions over the walk are [`walk_regions`][], [`walk`][] and the filters beside them. A -resolved ``where`` arrives as a [`Mask`][]. Frozen dataclasses only — no +resolved ``where`` arrives as a [`Mask`][], and what +[`advice`][mathspec.advice] says about a program as [`Advice`][]. Frozen dataclasses only — no execution logic, and nothing imported from a consumer. How a consumer reads one: ``docs/reference/reading.md``. """ @@ -37,6 +38,8 @@ #: What ``mathspec.program`` promises a consumer, sorted. __all__ = [ 'Add', + 'Advice', + 'AdviceKind', 'And', 'Assumption', 'Axis', @@ -1703,3 +1706,35 @@ def __and__(self, other: Mask) -> Mask: def __or__(self, other: Mask) -> Mask: """Either mask — construction absorbs a literal side rather than burying it.""" return Mask(Or(self.root, other.root)) + + +# -------------------------------------------------------------------------- +# Advice +# -------------------------------------------------------------------------- + + +#: Which pass an [`Advice`][] comes from. Closed, like the operator set: a +#: consumer filtering on it can enumerate every value. +AdviceKind = Literal['never-an-axis', 'given', 'unbounded'] + + +@dataclass(frozen=True) +class Advice: + """One thing the language advises about a file it accepts. + + Never an error: each is what a half-written spec looks like too. A + consumer prints it, or filters on ``kind`` and ``subject``; the text is the + language's, so no consumer writes its own. + + Attributes: + kind: The pass that said it. + subject: The declaration it is about — a dimension name, a variable name. + text: The sentence, naming the rewrite. + """ + + kind: AdviceKind + subject: str + text: str + + def __str__(self) -> str: + return self.text diff --git a/src/mathspec/spec.py b/src/mathspec/spec.py index d27d5a85..736bbf8c 100644 --- a/src/mathspec/spec.py +++ b/src/mathspec/spec.py @@ -2,9 +2,12 @@ # # SPDX-License-Identifier: MIT -"""The YAML surface's types — every block a file may contain, rooted at [`Spec`][]. +"""The file: what it says, as every block it may contain, rooted at [`Spec`][]. -Nothing here has seen data. +The first public state. A [`Spec`][] holds one file's sections as the blocks +below, and [`BUILTIN_NAMES`][] is the closed set of operators an expression in +one may call. Nothing here has seen data; what the file means is its +[`program`][mathspec.spec.Spec.program]. """ from __future__ import annotations @@ -30,6 +33,7 @@ from mathspec._expression_parser import NAME, ComparisonOperator from mathspec.errors import did_you_mean, schema_error +from mathspec.operators import BUILTIN_NAMES from mathspec.program import ( DimensionDtype, MissingReading, @@ -51,6 +55,36 @@ from pydantic_core import CoreSchema +#: What ``mathspec.spec`` promises a consumer, sorted. +__all__ = [ + 'BUILTIN_NAMES', + 'AssumptionBlock', + 'BoundsBlock', + 'ConstraintBlock', + 'Curvature', + 'DimensionBlock', + 'ExpressionBlock', + 'ExpressionCase', + 'Formulation', + 'GivenBlock', + 'GivenConstraintBlock', + 'GivenExpressionBlock', + 'GivenMaskBlock', + 'GivenParameterBlock', + 'GivenVariableBlock', + 'MacroBlock', + 'MaskBlock', + 'ObjectiveBlock', + 'ParameterBlock', + 'PiecewiseBlock', + 'PiecewiseLink', + 'RelationBlock', + 'SosBlock', + 'Spec', + 'VariableBlock', +] + + class _StrictBlock(BaseModel): """Base for every schema block: unknown keys are an error, not a shrug. @@ -1024,7 +1058,7 @@ class Spec(_StrictBlock): A ``Spec`` that exists has passed the whole language: constructing one by any route — ``to_spec``, [`model_validate`][], the constructor — runs every load-time check, expression pass included, and raises - [`LanguageError`][] on a spec the language refuses. + [`LanguageError`][mathspec.errors.LanguageError] on a spec the language refuses. Holding one is the proof, so nothing downstream checks it again. The API is the thirteen declaration sections plus ``version`` and diff --git a/src/mathspec/typesetting/__init__.py b/src/mathspec/typesetting/__init__.py index ffe441de..6d1dcdd9 100644 --- a/src/mathspec/typesetting/__init__.py +++ b/src/mathspec/typesetting/__init__.py @@ -120,7 +120,7 @@ def typeset( 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 instead. - fmt: What spells the math — a key of [`FORMATS`][]. + fmt: What spells the math — a [`FormatName`][]. symbols: How names print, as a [`SymbolTable`][], a path or a mapping. Names it does not carry are derived, and it must be written in *fmt*'s notation. @@ -188,7 +188,7 @@ def typeset_declaration( spec: Anything [`mathspec.to_spec`][] accepts, or a [`Program`][]. name: A named expression, mask, constraint, assumption, ``piecewise:`` block or variable the spec declares. - fmt: What spells the math — a key of [`FORMATS`][]. + fmt: What spells the math — a [`FormatName`][]. symbols: How names print; see [`typeset`][]. inline_expressions: Substitute the plain named expressions the line uses, so it stands on its own; ``False`` prints their symbols, as the document diff --git a/tests/fixtures.py b/tests/fixtures.py index d654020f..8f6dc4b8 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -10,12 +10,12 @@ from pathlib import Path from typing import TYPE_CHECKING, Any -from mathspec import Spec from mathspec._expression_parser import ComparisonNode from mathspec._yaml import parse_yaml, read_yaml from mathspec.errors import SchemaError from mathspec.expansion import parse_and_expand from mathspec.resolution import Namespace, mask_of, resolve_expression, resolve_where_text +from mathspec.spec import Spec from mathspec.validation import to_spec if TYPE_CHECKING: diff --git a/tests/test_advice.py b/tests/test_advice.py index 61b37ec8..0258718c 100644 --- a/tests/test_advice.py +++ b/tests/test_advice.py @@ -17,7 +17,8 @@ import pytest -from mathspec import AdviceKind, advice, to_spec +from mathspec import advice, to_spec +from mathspec.program import AdviceKind from tests.fixtures import SMALL_MODEL, raw_of, varied EXAMPLES = Path(__file__).resolve().parents[1] / 'examples' diff --git a/tests/test_composition.py b/tests/test_composition.py index fdae211b..08784f58 100644 --- a/tests/test_composition.py +++ b/tests/test_composition.py @@ -23,8 +23,9 @@ import yaml import mathspec.spec as spec_module -from mathspec import LanguageError, merge, override, to_markdown, to_spec +from mathspec import merge, override, to_markdown, to_spec from mathspec.canonical import canonical_yaml +from mathspec.errors import LanguageError from tests.fixtures import DISPATCH_MODEL, SMALL_MODEL, varied if TYPE_CHECKING: diff --git a/tests/test_degree.py b/tests/test_degree.py index 1b55a271..4483376e 100644 --- a/tests/test_degree.py +++ b/tests/test_degree.py @@ -12,8 +12,8 @@ import pytest -from mathspec import LanguageError from mathspec.degree import calls_dual, check_binary, check_expression +from mathspec.errors import LanguageError from mathspec.program import carries_variable from mathspec.resolution import Namespace from tests.fixtures import SMALL_MODEL, expression_of, schema_of diff --git a/tests/test_docs.py b/tests/test_docs.py index 4d5870c4..ef822d39 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -266,6 +266,7 @@ def test_every_page_under_docs_has_a_nav_entry(): #: page of its own: `mathspec` re-exports what a consumer calls from it. API_PAGES = { 'mathspec': Path('docs') / 'reference' / 'api.md', + 'mathspec.spec': Path('docs') / 'reference' / 'spec.md', 'mathspec.program': Path('docs') / 'reference' / 'program.md', } diff --git a/tests/test_given.py b/tests/test_given.py index ba355282..aa0e2e0a 100644 --- a/tests/test_given.py +++ b/tests/test_given.py @@ -16,7 +16,9 @@ import pytest -from mathspec import FORMATS, LanguageError, advice, merge, to_markdown, to_spec, typeset +from mathspec import advice, merge, to_markdown, to_spec, typeset +from mathspec.errors import LanguageError +from mathspec.typesetting import FORMATS from tests.fixtures import BALANCE #: One component file: it pins the flow at its own port, and the column it diff --git a/tests/test_library_example.py b/tests/test_library_example.py index 6d613ae5..c6f897aa 100644 --- a/tests/test_library_example.py +++ b/tests/test_library_example.py @@ -15,7 +15,8 @@ import pytest -from mathspec import LanguageError, merge, override, to_markdown, to_spec +from mathspec import merge, override, to_markdown, to_spec +from mathspec.errors import LanguageError from mathspec.typesetting import FORMATS, typeset from tests.fixtures import EXAMPLES from tools import gallery diff --git a/tests/test_lowering.py b/tests/test_lowering.py index 83fa33cd..c143cca1 100644 --- a/tests/test_lowering.py +++ b/tests/test_lowering.py @@ -15,8 +15,9 @@ import pytest -from mathspec import LanguageError, Spec, to_spec +from mathspec import to_spec from mathspec._where_parser import parse_where +from mathspec.errors import LanguageError from mathspec.exclusivity import overlapping from mathspec.program import ( Add, @@ -63,6 +64,7 @@ where_children, ) from mathspec.resolution import Namespace +from mathspec.spec import Spec from tests.fixtures import DISPATCH_MODEL, EXAMPLES, SMALL_MODEL, expanded, expression_of, schema_of, varied, where_of DISPATCH_YAML = EXAMPLES / 'dispatch.yaml' diff --git a/tests/test_masks.py b/tests/test_masks.py index 4ea9a1f6..3f209115 100644 --- a/tests/test_masks.py +++ b/tests/test_masks.py @@ -17,8 +17,10 @@ import pytest -from mathspec import FORMATS, LanguageError, advice, merge, override, to_spec, typeset, typeset_declaration +from mathspec import advice, merge, override, to_spec, typeset, typeset_declaration +from mathspec.errors import LanguageError from mathspec.program import Mask, NamedMask, ParameterDefined, VariableDefined +from mathspec.typesetting import FORMATS from tests.fixtures import SMALL_MODEL, varied #: A unit stands in a period between its build year and its retirement, and diff --git a/tests/test_public_surface.py b/tests/test_public_surface.py index d8391142..d1ab4b46 100644 --- a/tests/test_public_surface.py +++ b/tests/test_public_surface.py @@ -4,41 +4,48 @@ """The export surface, pinned — because a consumer depends on it by name. -`mathspec.__all__` is what another repository is allowed to import, so an -addition to it is a decision, and the table below is where it is recorded. +Four modules, one rule each: `mathspec` is what a consumer calls, +`mathspec.spec` what the file says, `mathspec.program` what it means, and +`mathspec.errors` what a consumer catches. An addition to any of them is a +decision, and the tables below are where it is recorded. """ from __future__ import annotations import ast +import inspect +import types from pathlib import Path import pytest import mathspec -from mathspec import Spec, program, typesetting +from mathspec import errors, program, spec +from mathspec.spec import Spec #: Every name `mathspec` promises. Grouped as a reader meets them, not #: alphabetically: the alphabetical form is `__all__` itself, and repeating it #: here would make the two one list checked against itself. SURFACE = frozenset( { - # the two public states, and the door to both - 'Spec', 'to_spec', 'program', - # the error tree, and the one wording a consumer's own refusals share - 'MathSpecError', 'LanguageError', 'SchemaError', 'DimensionError', - 'did_you_mean', + # the door to a file, and the three modules behind it + 'to_spec', 'spec', 'program', 'errors', # the verdicts a consumer asks for rather than re-deriving - 'advice', 'Advice', 'AdviceKind', - # the closed operator set, the one vocabulary with no Literal form, which a consumer pins its table against - 'BUILTIN_NAMES', - # typesetting - 'FORMATS', 'SymbolTable', 'typeset', 'typeset_declaration', 'to_latex', 'to_typst', 'to_markdown', + 'advice', + # typesetting, and the two inputs it takes + 'typeset', 'typeset_declaration', 'to_latex', 'to_typst', 'to_markdown', 'FormatName', 'SymbolTable', # the two file-level verbs: peers composed, and patches laid over a base 'merge', 'override', } ) # fmt: skip +#: What the top level holds besides functions: the three modules, and the two +#: things a consumer builds or names to pass to `typeset`. +NOT_CALLED = frozenset({'spec', 'program', 'errors', 'FormatName', 'SymbolTable'}) + +#: The names `mathspec.spec` exports without defining them. +SPEC_REEXPORTS = frozenset({'BUILTIN_NAMES'}) + #: What `Spec` promises beyond the sections a file declares: the two ways back #: out, the verb that writes a formulation out, and the program the file means. #: A `model_`-prefixed name is pydantic's, not a contract this project keeps. @@ -47,8 +54,9 @@ #: The modules whose `__all__` a consumer imports from. MODULES = [ pytest.param(mathspec, id='mathspec'), - pytest.param(typesetting, id='typesetting'), + pytest.param(spec, id='spec'), pytest.param(program, id='program'), + pytest.param(errors, id='errors'), ] @@ -108,3 +116,29 @@ def test_the_program_module_exports_everything_it_defines(): assert declared == defined, ( f'only in __all__: {sorted(declared - defined)}; defined but unexported: {sorted(defined - declared)}' ) + + +def test_the_top_level_is_what_a_consumer_calls(): + """A type a consumer receives lives in `spec` or `program`, and an error in `errors`.""" + held = {n for n in mathspec.__all__ if n not in NOT_CALLED and not inspect.isfunction(getattr(mathspec, n))} + assert not held, f'not a function, so not top level: {sorted(held)}' + modules = {n for n in mathspec.__all__ if isinstance(getattr(mathspec, n), types.ModuleType)} + assert modules == {'spec', 'program', 'errors'}, f'the top level re-exports {sorted(modules)}' + + +def test_the_spec_module_exports_every_class_it_defines(): + """`Spec` hands out its blocks, so each one is public: a block class added without a decision fails here.""" + declared = set(spec.__all__) + classes = {n for n, obj in vars(spec).items() if inspect.isclass(obj) and obj.__module__ == spec.__name__} + public = {n for n in classes if not n.startswith('_')} + assert public <= declared, f'defined but unexported: {sorted(public - declared)}' + stray = declared - _defined_by(spec) - SPEC_REEXPORTS + assert not stray, f'exported but neither defined nor a pinned re-export: {sorted(stray)}' + + +def test_the_errors_module_exports_the_error_tree(): + """Every exception class is one a consumer catches, so none is left out.""" + raised = {n for n, obj in vars(errors).items() if inspect.isclass(obj) and issubclass(obj, Exception)} + assert raised <= set(errors.__all__), f'unexported errors: {sorted(raised - set(errors.__all__))}' + others = {n for n in errors.__all__ if not inspect.isclass(getattr(errors, n))} + assert others == {'did_you_mean'}, f'errors exports {sorted(others)} besides the error tree' diff --git a/tests/test_pypsa_split.py b/tests/test_pypsa_split.py index dbc939c0..2ad33b80 100644 --- a/tests/test_pypsa_split.py +++ b/tests/test_pypsa_split.py @@ -20,8 +20,10 @@ import pytest import yaml -from mathspec import FORMATS, LanguageError, merge, to_spec, typeset +from mathspec import merge, to_spec, typeset from mathspec.canonical import canonical_yaml +from mathspec.errors import LanguageError +from mathspec.typesetting import FORMATS from tests.fixtures import BALANCE from tests.test_terms import DEMAND, FLEET from tools.gallery import split_index diff --git a/tests/test_several_files_page.py b/tests/test_several_files_page.py index 6fdae6be..e821fc36 100644 --- a/tests/test_several_files_page.py +++ b/tests/test_several_files_page.py @@ -20,8 +20,8 @@ import pytest -from mathspec import LanguageError from mathspec.__main__ import main +from mathspec.errors import LanguageError PAGE = Path(__file__).resolve().parent.parent / 'docs' / 'several-files.md' TEXT = PAGE.read_text() diff --git a/tests/test_terms.py b/tests/test_terms.py index 36f6f584..acaec422 100644 --- a/tests/test_terms.py +++ b/tests/test_terms.py @@ -16,18 +16,10 @@ import pytest -from mathspec import ( - FORMATS, - LanguageError, - advice, - merge, - override, - to_markdown, - to_spec, - typeset, - typeset_declaration, -) +from mathspec import advice, merge, override, to_markdown, to_spec, typeset, typeset_declaration from mathspec.canonical import canonical_yaml +from mathspec.errors import LanguageError +from mathspec.typesetting import FORMATS from tests.fixtures import BALANCE, BUS_DIMS, BUS_FRAME, INJECTION #: A generator fleet: what it puts in is its term. diff --git a/tests/typesetting/test_declaration.py b/tests/typesetting/test_declaration.py index 3213c376..248c718a 100644 --- a/tests/typesetting/test_declaration.py +++ b/tests/typesetting/test_declaration.py @@ -10,7 +10,8 @@ import pytest -from mathspec import LanguageError, SchemaError, typeset_declaration +from mathspec import typeset_declaration +from mathspec.errors import LanguageError, SchemaError from tests.fixtures import DISPATCH_MODEL as DISPATCH from tests.fixtures import varied from tests.typesetting.fixtures import EVERY_FORMAT diff --git a/tools/schema.py b/tools/schema.py index fd671f09..d0b1e839 100644 --- a/tools/schema.py +++ b/tools/schema.py @@ -21,7 +21,7 @@ from pathlib import Path from typing import Any -from mathspec import Spec +from mathspec.spec import Spec PATH = Path(__file__).resolve().parent.parent / 'schema' / 'mathspec.schema.json' DIALECT = 'https://json-schema.org/draft/2020-12/schema'