Skip to content

Python API#

Look up the signature, the arguments and the errors of every name that import mathspec exports, grouped by task. The top level holds what you call, and three modules hold the rest:

Module Holds Documented on
mathspec.spec what the file says: Spec and its parts 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

A module whose name starts with an underscore is not public API (where a name lives).

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 entry 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 spec 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 entry 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 spec *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 = 'to_spec takes one file, one dict or one Spec, not a list. Merge the entries 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 entry 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 entry; 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 entry 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 entry; 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 the same dims', 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(entry, 'adds_to') for key, entry 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 an entry 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 an entry 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: _entries(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 symbol tables, the notation the equations use included. 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 symbol tables, the notation the equations use
            included. 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(items))
            for title, items in explained.glossaries(noticed, walk.defined())
        ]
    return format_.document([*blocks, *rendered], standalone=standalone)

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

Render one entry 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 sos: set, a piecewise: curve, an assumption, 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 definition.

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, sos: entry, piecewise: entry, assumption 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 seven, 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_line(
    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 entry 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 ``sos:`` set, a ``piecewise:``
    curve, an assumption, 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 definition.

    Args:
        spec: Anything [`mathspec.to_spec`][] accepts, or a [`Program`][].
        name: A named expression, mask, constraint, ``sos:`` entry,
            ``piecewise:`` entry, assumption 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 seven, 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}, which prints no line of its own. Call typeset() for the "
            f"whole spec, and find it in the legend under 'Given'."
        )
        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: entry emits counts as declared, so one table spells both readings of a spec: the entries 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:`` entry emits counts as declared, so
    one table spells both readings of a spec: the entries 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 missing. Write notation: latex or notation: typst."
        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: entry 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 file order, then one note per

...

entry 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:`` entry 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 file order, then one note per
        entry 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 would come from the row order of the data. Declare '
        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 entry'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 entry's ``otherwise:``.
    """
    where = 'otherwise' if label is None else f"case '{label}'"
    return f"Named expression '{name}', {where}"