Skip to content

Python API#

This page documents every name that import mathspec exports, grouped by 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
mathspec.program what the file means: Program, its nodes, and Advice Program API
mathspec.errors what you catch: the error tree, and did_you_mean Errors below

Loading#

mathspec.to_spec(spec) #

Load and validate a spec definition — the language's front door.

Everything decidable without data is decided here: schema shape, every rule one declaration is held to against the others, every expression and where string, and every macro template.

PARAMETER DESCRIPTION
spec

A YAML path — a Path, or a str with no newline in it — the YAML text itself as a str with one, a mapping, or a loaded Spec.

TYPE: str | Path | Mapping[str, object] | Spec

RETURNS DESCRIPTION
Spec

The schema as the file declares it, piecewise: intact.

RAISES DESCRIPTION
LanguageError

Anything the language does not accept, a text that is not a mapping of sections included.

FileNotFoundError

A str with no newline that names no file.

Source code in src/mathspec/validation.py
def to_spec(spec: str | Path | Mapping[str, object] | Spec) -> Spec:
    """Load and validate a spec definition — the language's front door.

    Everything decidable without data is decided here: schema shape, every
    rule one declaration is held to against the others, every expression and
    where string, and every macro template.

    Args:
        spec: A YAML path — a [`Path`][], or a ``str`` with no
            newline in it — the YAML text itself as a ``str`` with one, a
            mapping, or a loaded [`Spec`][].

    Returns:
        The schema *as the file declares it*, ``piecewise:`` intact.

    Raises:
        LanguageError: Anything the language does not accept, a text that is
            not a mapping of sections included.
        FileNotFoundError: A ``str`` with no newline that names no file.
    """
    if isinstance(spec, (list, tuple)):
        msg = 'a spec is one file, one dict or one Spec, never a list of them; merge the declarations into one dict.'
        raise SchemaError(msg)
    if isinstance(spec, Spec):
        return spec
    return Spec.model_validate(spec if isinstance(spec, Mapping) else read_spec(spec))

Composing#

Compose a spec from several files shows both in use.

mathspec.merge(fragments, description=None) #

fragments composed as peers, each owning the math it declares.

PARAMETER DESCRIPTION
fragments

Each fragment as a YAML path, YAML text, a mapping, or a loaded Spec. A sum writes its terms in the order of the list.

TYPE: Sequence[Source]

description

What the composed spec is. A fragment's own description is about the fragment, and is not carried.

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
Spec

The composed spec, loaded. A given declaration a sibling introduces is

Spec

folded away; one nothing introduces stays under given:.

RAISES DESCRIPTION
LanguageError

A fragment does not load on its own; two fragments declare one name; two fragments say different things about one dimension, relation or given declaration; a fragment reads a name as something other than what its sibling introduces, as another kind of thing, or over fewer dimensions than its body carries; a fragment adds a term to a variable, a parameter, a constraint or a definition written as cases:; a term reads its own sum through another fragment; a name no fragment defines is read by nothing but the fragments that add a term to it, or by one fragment alone; the readers of such a name write its dims in different orders; two fragments are written against different language versions; two fragments set the objective; or the composed spec does not load.

FileNotFoundError

A str with no newline that names no file.

TypeError

fragments is one path rather than a list.

Source code in src/mathspec/composition.py
def merge(fragments: Sequence[Source], description: str | None = None) -> Spec:
    """*fragments* composed as peers, each owning the math it declares.

    Args:
        fragments: Each fragment as a YAML path, YAML text, a mapping, or a
            loaded [`Spec`][mathspec.spec.Spec]. A sum writes its terms in
            the order of the list.
        description: What the composed spec is. A fragment's own
            ``description`` is about the fragment, and is not carried.

    Returns:
        The composed spec, loaded. A given declaration a sibling introduces is
        folded away; one nothing introduces stays under ``given:``.

    Raises:
        LanguageError: A fragment does not load on its own; two fragments
            declare one name; two fragments say different things about one
            dimension, relation or given declaration; a fragment reads a name as
            something other than what its sibling introduces, as another kind
            of thing, or over fewer dimensions than its body carries; a fragment
            adds a term to a variable, a parameter, a constraint or a
            definition written as ``cases:``; a term reads its own sum through
            another fragment; a name no fragment defines is read by nothing
            but the fragments that add a term to it, or by one fragment alone;
            the readers of such a name write its dims in different orders; two
            fragments are written against different language versions; two
            fragments set the objective; or the composed spec does not load.
        FileNotFoundError: A ``str`` with no newline that names no file.
        TypeError: *fragments* is one path rather than a list.
    """
    loaded = {name: _fragment(name, fragment) for name, fragment in _labelled(fragments, 'fragments').items()}
    read = {name: spec.to_dict() for name, spec in loaded.items()}
    merged: dict[str, object] = {'version': _one_version(read)}
    if description is not None:
        merged['description'] = description
    for section in SHARED_SECTIONS:
        if agreed := _agreed(read, section, _singular(section), 'give one of them a name of its own'):
            merged[section] = agreed
    asked = {name: spec.given.model_dump(exclude_unset=True) for name, spec in loaded.items()}
    readings = {
        kind: _agreed(asked, kind, label, 'read it over one frame', claims=_reading_claims)
        for kind, label in GIVEN_KINDS.items()
    }
    for section in OWNED_SECTIONS:
        if claimed := _claimed(read, section):
            merged[section] = claimed
    summed = _summed(loaded, merged, readings['expressions'], read)
    if expressions := {**_mapping(merged.get('expressions')), **summed}:
        merged['expressions'] = {key: _without(block, 'adds_to') for key, block in expressions.items()}
    if given := _folded(read, merged, loaded, readings):
        merged['given'] = given
    if (objective := _one_objective(read)) is not None:
        merged['objective'] = objective
    return to_spec(merged)

mathspec.override(base, patches) #

base with each patch laid over it in turn.

PARAMETER DESCRIPTION
base

The spec being extended: a YAML path, YAML text, a mapping, or a loaded Spec.

TYPE: Source

patches

Each patch as a YAML path, YAML text, a mapping, or a loaded Spec. Each is laid on the base with every earlier patch laid on it, so a later patch wins a field an earlier one writes.

TYPE: Sequence[Source]

RETURNS DESCRIPTION
Spec

The patched spec, loaded.

RAISES DESCRIPTION
LanguageError

The base does not load; the patched spec does not load; a patch edits or removes a declaration its base does not declare; a patch creates one that is not whole; a patch redeclares or removes a dimension or a relation; or a patch sets a whole section to null.

FileNotFoundError

A str with no newline that names no file.

TypeError

patches is one path rather than a list.

Source code in src/mathspec/composition.py
def override(base: Source, patches: Sequence[Source]) -> Spec:
    """*base* with each patch laid over it in turn.

    Args:
        base: The spec being extended: a YAML path, YAML text, a mapping, or a
            loaded [`Spec`][mathspec.spec.Spec].
        patches: Each patch as a YAML path, YAML text, a mapping, or a loaded
            [`Spec`][mathspec.spec.Spec]. Each is laid on the base with every
            earlier patch laid on it, so a later patch wins a field an earlier
            one writes.

    Returns:
        The patched spec, loaded.

    Raises:
        LanguageError: The base does not load; the patched spec does not
            load; a patch edits or removes a declaration its base does not
            declare; a patch creates one that is not whole; a patch redeclares
            or removes a dimension or a relation; or a patch sets a whole
            section to ``null``.
        FileNotFoundError: A ``str`` with no newline that names no file.
        TypeError: *patches* is one path rather than a list.
    """
    read = {name: _declarations(patch) for name, patch in _labelled(patches, 'patches').items()}
    result = to_spec(base).to_dict()
    for name, patch in read.items():
        result = _lay_over(result, deepcopy(patch), name)
    return to_spec(result)

Typesetting#

mathspec.to_latex(spec, **options) #

Render spec as LaTeX (amsmath align). See typeset.

Source code in src/mathspec/typesetting/__init__.py
def to_latex(spec: str | Path | Mapping[str, object] | Spec | Program, **options: Unpack[_Options]) -> str:
    """Render *spec* as LaTeX (amsmath ``align``). See [`typeset`][]."""
    return typeset(spec, 'latex', **options)

mathspec.to_typst(spec, **options) #

Render spec as Typst. See typeset.

Source code in src/mathspec/typesetting/__init__.py
def to_typst(spec: str | Path | Mapping[str, object] | Spec | Program, **options: Unpack[_Options]) -> str:
    """Render *spec* as Typst. See [`typeset`][]."""
    return typeset(spec, 'typst', **options)

mathspec.to_markdown(spec, **options) #

Render spec as GitHub-flavoured Markdown. See typeset.

Source code in src/mathspec/typesetting/__init__.py
def to_markdown(spec: str | Path | Mapping[str, object] | Spec | Program, **options: Unpack[_Options]) -> str:
    """Render *spec* as GitHub-flavoured Markdown. See [`typeset`][]."""
    return typeset(spec, 'markdown', **options)

mathspec.typeset(spec, fmt, *, symbols=None, standalone=False, legend=True, numbered=True, inline_expressions=False) #

Render spec's math in fmt.

PARAMETER DESCRIPTION
spec

Anything mathspec.to_spec accepts, or a Program. A Spec or a Program is rendered as it stands, so printing one spec 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 instead.

TYPE: str | Path | Mapping[str, object] | Spec | Program

fmt

What spells the math — a FormatName.

TYPE: 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.

TYPE: str | Path | Mapping[str, object] | SymbolTable | None DEFAULT: None

standalone

Emit a compilable document rather than a fragment.

TYPE: bool DEFAULT: False

legend

Prepend the sets/parameters/variables table. The spec's own description: opens the document either way — it is what the file says it is, not a symbol table.

TYPE: bool DEFAULT: True

numbered

Number the equations.

TYPE: bool DEFAULT: True

inline_expressions

Substitute each plain named expression into the equations that use it, rather than printing its symbol there and its definition once. A cased expression is a definition either way: its block is taller than the line it would sit in.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
str

The rendered text.

RAISES DESCRIPTION
ValueError

fmt names no format.

LanguageError

A spec that does not compile; it does not print.

SchemaError

A symbol table entry naming nothing in the spec, or a table written in a notation fmt does not read.

Source code in src/mathspec/typesetting/__init__.py
def typeset(
    spec: str | Path | Mapping[str, object] | Spec | Program,
    fmt: FormatName,
    *,
    symbols: str | Path | Mapping[str, object] | SymbolTable | None = None,
    standalone: bool = False,
    legend: bool = True,
    numbered: bool = True,
    inline_expressions: bool = False,
) -> str:
    """Render *spec*'s math in *fmt*.

    Args:
        spec: Anything [`mathspec.to_spec`][] accepts, or a
            [`Program`][]. A ``Spec`` or a ``Program``
            is rendered as it stands, so printing one spec 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
            instead.
        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.
        standalone: Emit a compilable document rather than a fragment.
        legend: Prepend the sets/parameters/variables table. The spec's own
            ``description:`` opens the document either way — it is what the
            file says it is, not a symbol table.
        numbered: Number the equations.
        inline_expressions: Substitute each plain named expression into the equations that
            use it, rather than printing its symbol there and its definition
            once. A cased expression is a definition either way: its block is
            taller than the line it would sit in.

    Returns:
        The rendered text.

    Raises:
        ValueError: *fmt* names no format.
        LanguageError: A spec that does not compile; it does not print.
        SchemaError: A symbol table entry naming nothing in the spec, or a
            table written in a notation *fmt* does not read.
    """
    walk = _walk(spec, fmt, symbols, inline_expressions=inline_expressions)
    program, format_ = walk.program, walk.format

    rendered = [
        format_.section(title, format_.equations(lines, numbered=numbered))
        for title, lines in walk.equations()
        if lines
    ]

    blocks = [format_.note(format_.escape(program.description))] if program.description else []
    if legend:
        explained, noticed = Legend(program, walk.symbols, format_), notice(program)
        blocks += [
            format_.section(title, format_.glossary(entries))
            for title, entries in explained.glossaries(noticed, walk.defined())
        ]
        blocks += [format_.note(text) for text in explained.convention_notes()]
        blocks += [format_.note(text) for text in explained.translation_notes(noticed)]
        blocks += [format_.note(text) for text in explained.position_notes(noticed)]
    return format_.document([*blocks, *rendered], standalone=standalone)

mathspec.typeset_declaration(spec, name, fmt, *, symbols=None, inline_expressions=True) #

Render one declaration as the bare line the document prints for it.

The line the whole-spec render prints for it — a named expression's or a mask's definition, a constraint, an assumption, a piecewise: curve, or a variable's domain, quantifier included — with no document, label, equation number or math delimiters around it, for a math context the caller lays out: a docstring, a table cell. A line on its own has no Definitions section beside it, so the plain named expressions it uses are substituted unless inline_expressions says otherwise; a cased one prints by symbol, and a second call with its name prints its block.

PARAMETER DESCRIPTION
spec

Anything mathspec.to_spec accepts, or a Program.

TYPE: str | Path | Mapping[str, object] | Spec | Program

name

A named expression, mask, constraint, assumption, piecewise: block or variable the spec declares.

TYPE: str

fmt

What spells the math — a FormatName.

TYPE: FormatName

symbols

How names print; see typeset.

TYPE: str | Path | Mapping[str, object] | SymbolTable | None DEFAULT: None

inline_expressions

Substitute the plain named expressions the line uses, so it stands on its own; False prints their symbols, as the document does. A plain expression asked for by name prints its definition either way.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
str

The line, math only.

RAISES DESCRIPTION
ValueError

fmt names no format.

LanguageError

A spec that does not compile; it does not print.

SchemaError

name is declared as none of the six, as two — a constraint may share a variable's name — or under given:, which prints in the legend rather than as a line; or a symbol table entry names nothing in the spec.

Source code in src/mathspec/typesetting/__init__.py
def typeset_declaration(
    spec: str | Path | Mapping[str, object] | Spec | Program,
    name: str,
    fmt: FormatName,
    *,
    symbols: str | Path | Mapping[str, object] | SymbolTable | None = None,
    inline_expressions: bool = True,
) -> str:
    """Render one declaration as the bare line the document prints for it.

    The line the whole-spec render prints for it — a named expression's
    or a mask's definition, a constraint, an assumption, a ``piecewise:``
    curve, or a variable's domain, quantifier included —
    with no document, label, equation number or math delimiters around it, for
    a math context the caller lays out: a docstring, a table cell. A line on
    its own has no Definitions section beside it, so the plain named
    expressions it uses are substituted unless *inline_expressions* says otherwise; a cased
    one prints by symbol, and a second call with its name prints its block.

    Args:
        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 [`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
            does. A plain expression asked for by name prints its definition
            either way.

    Returns:
        The line, math only.

    Raises:
        ValueError: *fmt* names no format.
        LanguageError: A spec that does not compile; it does not print.
        SchemaError: *name* is declared as none of the six, as two — a
            constraint may share a variable's name — or under ``given:``, which
            prints in the legend rather than as a line; or a symbol table entry
            names nothing in the spec.
    """
    walk = _walk(spec, fmt, symbols, inline_expressions=inline_expressions)
    given = walk.program.given
    givens = {
        'parameter': given.parameters,
        'variable': given.variables,
        'expression': given.expressions,
        'constraint': given.constraints,
        'mask': given.masks,
    }
    given_kind = next((kind for kind, group in givens.items() if name in group), None)
    if given_kind is not None:
        msg = (
            f"'{name}' is a given {given_kind}, and a given declaration prints no line of its own — "
            f"this file reads it and does not build it. It prints in the legend, under 'Given', "
            f'so call typeset() for the whole spec.'
        )
        raise SchemaError(msg)
    return walk.format.equation(walk.line(name))

mathspec.FormatName = Literal['latex', 'markdown', 'typst'] module-attribute #

mathspec.SymbolTable(notation, indices=dict(), sets=dict(), names=dict()) dataclass #

How a reader wants the spec to print — notation only, kept out of the spec.

Every entry is a spelling, printed verbatim. notation: says which language they are written in, and a render in the other one refuses::

notation: latex
dimensions:
  snapshot: {index: t, set: "\\mathcal{T}"}
  plant:    {index: n}
names:
  marginal_cost: "c^{\\mathrm{marg}}"

An entry naming nothing in the spec is an error naming the near miss.

ATTRIBUTE DESCRIPTION
notation

The language the entries are written in; load lower-cases it.

TYPE: Notation

indices = field(default_factory=dict) class-attribute instance-attribute #

names = field(default_factory=dict) class-attribute instance-attribute #

notation instance-attribute #

sets = field(default_factory=dict) class-attribute instance-attribute #

checked_against(program) #

Reject entries naming nothing in program or in what its formulations state, with the near miss.

A name a piecewise: or sos: block emits counts as declared, so one table spells both readings of a spec: the blocks as the file states them, and the rows expand writes out.

Source code in src/mathspec/typesetting/symbols.py
def checked_against(self, program: Program) -> SymbolTable:
    """Reject entries naming nothing in *program* or in what its formulations state, with the near miss.

    A name a ``piecewise:`` or ``sos:`` block emits counts as declared, so
    one table spells both readings of a spec: the blocks as the file states
    them, and the rows [`expand`][mathspec.spec.Spec.expand] writes out.
    """
    dims = set(program.dimensions)
    everything = dims | _declared(program) | _emitted(program)
    errors = [
        *(_unknown_entry(d, 'dimensions', dims) for d in {*self.indices, *self.sets} - dims),
        *(_unknown_entry(n, 'names', everything - dims) for n in set(self.names) - everything),
    ]
    if errors:
        raise SchemaError('\n'.join(sorted(errors)))
    return self

load(source) classmethod #

A table from a YAML path or the mapping it parses to.

RAISES DESCRIPTION
SchemaError

An unknown section, a section or a dimension that is not a mapping, or a notation: that is missing or not latex/typst.

Source code in src/mathspec/typesetting/symbols.py
@classmethod
def load(cls, source: str | Path | Mapping[str, object]) -> SymbolTable:
    """A table from a YAML path or the mapping it parses to.

    Raises:
        SchemaError: An unknown section, a section or a dimension that is
            not a mapping, or a ``notation:`` that is missing or not
            ``latex``/``typst``.
    """
    raw = dict(source) if isinstance(source, Mapping) else read_yaml(Path(source))
    unknown = set(raw) - {'notation', 'dimensions', 'names'}
    if unknown:
        msg = f'symbol table: unknown section(s) {sorted(unknown)}. Valid sections: notation, dimensions, names.'
        raise SchemaError(msg)
    if 'notation' not in raw:
        msg = "symbol table: 'notation:' is required — latex or typst, the language the entries are written in."
        raise SchemaError(msg)
    notation = str(raw['notation']).lower()
    if notation not in NOTATIONS:
        msg = f'symbol table: unknown notation {raw["notation"]!r}. Valid notations: latex, typst.'
        raise SchemaError(msg)

    indices: dict[str, str] = {}
    sets: dict[str, str] = {}
    for dim, spec in _section(raw, 'dimensions').items():
        if not isinstance(spec, Mapping):
            msg = f"symbol table: dimension '{dim}' must be a mapping like {{index: t, set: '\\\\mathcal{{T}}'}}"
            raise SchemaError(msg)
        extra = set(spec) - {'index', 'set'}
        if extra:
            msg = f"symbol table: dimension '{dim}' has unknown key(s) {sorted(extra)}. Valid keys: index, set."
            raise SchemaError(msg)
        if 'index' in spec:
            indices[dim] = str(spec['index'])
        if 'set' in spec:
            sets[dim] = str(spec['set'])

    return cls(
        notation=cast('Notation', notation),
        indices=indices,
        sets=sets,
        names={k: str(v) for k, v in _section(raw, 'names').items()},
    )

Advice#

advice returns a tuple of Advice.

mathspec.advice(spec) #

Everything the language advises about spec, decided without data.

Advice is a note, not a refusal: a file with advice still loads.

PARAMETER DESCRIPTION
spec

Anything to_spec accepts, or a Program, read as it arrived. A piecewise: or sos: block is read as the rows it states, so the answer is the one its expansion gets, with nothing expanded.

TYPE: str | Path | Mapping[str, object] | Spec | Program

RETURNS DESCRIPTION
Advice

The never-an-axis advice in declaration order, then one note per

...

declaration the program reads and does not build, then the

tuple[Advice, ...]

unboundedness advice; str() of each is its sentence.

RAISES DESCRIPTION
LanguageError

spec does not load; to_spec says why.

FileNotFoundError

A str with no newline that names no file.

Source code in src/mathspec/advising.py
def advice(spec: str | Path | Mapping[str, object] | Spec | Program) -> tuple[Advice, ...]:
    """Everything the language advises about *spec*, decided without data.

    Advice is a note, not a refusal: a file with advice still loads.

    Args:
        spec: Anything [`to_spec`][] accepts, or a [`Program`][], read as
            it arrived. A ``piecewise:`` or ``sos:`` block is read as the rows
            it states, so the answer is the one its expansion gets, with
            nothing expanded.

    Returns:
        The never-an-axis advice in declaration order, then one note per
        declaration the program reads and does not build, then the
        unboundedness advice; ``str()`` of each is its sentence.

    Raises:
        LanguageError: *spec* does not load; [`to_spec`][] says why.
        FileNotFoundError: A ``str`` with no newline that names no file.
    """
    program = spec if isinstance(spec, Program) else to_spec(spec).program
    return tuple(_never_an_axis(program) + _given(program) + unbounded_notes(program))

Errors#

mathspec.errors #

What the language raises: the error tree, and the one wording a consumer's own refusals share.

MathSpecError #

Bases: ValueError

Base class for every error this package raises on purpose.

LanguageError #

Bases: MathSpecError

The spec is not sayable in the language, or does not obey its rules.

SchemaError #

Bases: LanguageError

What a load refuses: an unknown key, a bad dtype, a duplicate YAML key, an unparseable or unresolvable expression.

DimensionError #

Bases: LanguageError

A dim-set rule was violated. Raised at load time, before any data.

did_you_mean(name, known, *, label='Declared', listing=True) #

The repair clause for an unrecognised name: the near miss, or the set, or nothing where listing is off.

Source code in src/mathspec/errors.py
def did_you_mean(name: str, known: Iterable[str], *, label: str = 'Declared', listing: bool = True) -> str:
    """The repair clause for an unrecognised name: the near miss, or the set, or nothing where *listing* is off."""
    candidates = sorted(known)
    near = difflib.get_close_matches(name, candidates, n=1, cutoff=0.6)
    if near:
        return f"Did you mean '{near[0]}'?"
    return f'{label}: {", ".join(candidates) or "nothing"}.' if listing else ''

unordered(context, construct, dimension) #

The refusal for construct reading the order of dimension, which is not declared ordered.

Source code in src/mathspec/errors.py
def unordered(context: str, construct: str, dimension: str) -> str:
    """The refusal for *construct* reading the order of *dimension*, which is not declared ordered."""
    return (
        f"{context}: {construct} reads the order of '{dimension}', which is not declared ordered, so the "
        f'order it read would be the row order of the data. Declare the order part of the model with '
        f"'{dimension}: {{ordered: true}}' under dimensions:."
    )

schema_error(exc) #

A pydantic ValidationError as one of ours.

Returns the original LanguageError subclass where exactly one error carries one, and a SchemaError otherwise.

Source code in src/mathspec/errors.py
def schema_error(exc: ValidationError) -> LanguageError:
    """A pydantic ``ValidationError`` as one of ours.

    Returns the original [`LanguageError`][] subclass where exactly one
    error carries one, and a [`SchemaError`][] otherwise.
    """
    errors = exc.errors()
    lines = []
    for error in errors:
        message = error.get('msg', '').removeprefix('Value error, ')
        where = '.'.join(str(part) for part in error.get('loc', ()))
        lines.append(f'{where}: {message}' if where else message)
    text = '\n'.join(lines) or str(exc)

    if len(errors) == 1:
        original = errors[0].get('ctx', {}).get('error')
        if isinstance(original, LanguageError):
            return type(original)(text)
    return SchemaError(text)

prefixed(context, e) #

e under context, once — an expansion error already carries it.

Source code in src/mathspec/errors.py
def prefixed(context: str, e: ValueError) -> str:
    """*e* under *context*, once — an expansion error already carries it."""
    return str(e) if str(e).startswith(context) else f'{context}: {e}'

case_context(name, label) #

The context an error inside one arm of a cased expression is reported under.

PARAMETER DESCRIPTION
name

The named expression the arm belongs to.

TYPE: str

label

The case's name, or None for the block's otherwise:.

TYPE: str | None

Source code in src/mathspec/errors.py
def case_context(name: str, label: str | None) -> str:
    """The context an error inside one arm of a cased expression is reported under.

    Args:
        name: The named expression the arm belongs to.
        label: The case's name, or ``None`` for the block's ``otherwise:``.
    """
    where = 'otherwise' if label is None else f"case '{label}'"
    return f"Named expression '{name}', {where}"