diff --git a/mathics/builtin/arithfns/basic.py b/mathics/builtin/arithfns/basic.py index 27f300298..c96e1b416 100644 --- a/mathics/builtin/arithfns/basic.py +++ b/mathics/builtin/arithfns/basic.py @@ -15,6 +15,7 @@ IntegerM1, Number, RationalOneHalf, + String, ) from mathics.core.attributes import ( A_FLAT, @@ -34,11 +35,21 @@ ) from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression -from mathics.core.symbols import Symbol, SymbolNull, SymbolPower, SymbolTimes +from mathics.core.list import ListExpression +from mathics.core.symbols import ( + Symbol, + SymbolHoldForm, + SymbolNull, + SymbolPower, + SymbolTimes, + SymbolTrue, +) from mathics.core.systemsymbols import ( SymbolBlank, SymbolComplexInfinity, SymbolIndeterminate, + SymbolInfix, + SymbolLeft, SymbolPattern, SymbolSequence, ) @@ -144,7 +155,7 @@ class Divide(InfixOperator): expected_args = 2 formats = { - (("InputForm", "OutputForm"), "Divide[x_, y_]"): ( + ("InputForm", "Divide[x_, y_]"): ( 'Infix[{HoldForm[x], HoldForm[y]}, "/", 400, Left]' ), } @@ -160,6 +171,24 @@ class Divide(InfixOperator): summary_text = "divide a number" + def format_outputform(self, x, y, evaluation): + "(OutputForm,): Divide[x_, y_]" + use_2d = ( + evaluation.definitions.get_ownvalues("System`$Use2DOutputForm")[0].replace + is SymbolTrue + ) + if not use_2d: + return Expression( + SymbolInfix, + ListExpression( + Expression(SymbolHoldForm, x), Expression(SymbolHoldForm, y) + ), + String("/"), + Integer(400), + SymbolLeft, + ) + return None + class Minus(PrefixOperator): """ @@ -350,10 +379,21 @@ class Power(InfixOperator, MPMathFunction): Expression(SymbolPattern, Symbol("x"), Expression(SymbolBlank)), RationalOneHalf, ): "HoldForm[Sqrt[x]]", - (("InputForm", "OutputForm"), "x_ ^ y_"): ( + (("InputForm",), "x_ ^ y_"): ( 'Infix[{HoldForm[x], HoldForm[y]}, "^", 590, Right]' ), - ("", "x_ ^ y_"): ( + (("OutputForm",), "x_ ^ y_"): ( + "If[$Use2DOutputForm, " + "Superscript[HoldForm[x], HoldForm[y]], " + 'Infix[{HoldForm[x], HoldForm[y]}, "^", 590, Right]]' + ), + ( + ( + "StandardForm", + "TraditionalForm", + ), + "x_ ^ y_", + ): ( "PrecedenceForm[Superscript[PrecedenceForm[HoldForm[x], 590]," " HoldForm[y]], 590]" ), diff --git a/mathics/builtin/forms/print.py b/mathics/builtin/forms/print.py index 2e47e2fe9..93ff4c51f 100644 --- a/mathics/builtin/forms/print.py +++ b/mathics/builtin/forms/print.py @@ -20,7 +20,7 @@ from mathics.core.symbols import SymbolFalse, SymbolTrue from mathics.core.systemsymbols import SymbolInputForm, SymbolOutputForm from mathics.format.box.makeboxes import is_print_form_callback -from mathics.format.form import render_input_form, render_output_form +from mathics.format.form import render_input_form sort_order = "mathics.builtin.forms.general-purpose-forms" @@ -258,7 +258,21 @@ def eval_makeboxes_outputform(expr: BaseElement, evaluation: Evaluation, **kwarg Build a 2D representation of the expression using only keyboard characters. """ - text_outputform = str(render_output_form(expr, evaluation, **kwargs)) + from mathics.builtin.box.layout import InterpretationBox + from mathics.format.form.outputform import render_output_form + from mathics.format.render.prettyprint import render_2d_text + + if ( + evaluation.definitions.get_ownvalues("System`$Use2DOutputForm")[0].replace + is SymbolTrue + ): + text_outputform = str(render_2d_text(expr, evaluation, **{"2d": True})) + + if "\n" in text_outputform: + text_outputform = "\n" + text_outputform + else: # 1D + text_outputform = str(render_output_form(expr, evaluation, **kwargs)) + pane = PaneBox(String('"' + text_outputform + '"')) return InterpretationBox( pane, Expression(SymbolOutputForm, expr), **{"System`Editable": SymbolFalse} diff --git a/mathics/builtin/forms/variables.py b/mathics/builtin/forms/variables.py index 720b9a24d..e7022fbbb 100644 --- a/mathics/builtin/forms/variables.py +++ b/mathics/builtin/forms/variables.py @@ -5,7 +5,12 @@ """ -from mathics.core.attributes import A_LOCKED, A_PROTECTED, A_READ_PROTECTED +from mathics.core.attributes import ( + A_LOCKED, + A_NO_ATTRIBUTES, + A_PROTECTED, + A_READ_PROTECTED, +) from mathics.core.builtin import Builtin, Predefined from mathics.core.list import ListExpression @@ -179,3 +184,35 @@ class PrintForms_(Predefined): def evaluate(self, evaluation): return ListExpression(*evaluation.definitions.printforms) + + +class Use2DOutputForm_(Predefined): + r""" +
+
'$Use2DOutputForm' +
internal variable that controls if 'OutputForm[expr]' is shown \ + in one line (standard Mathics behavior) or \ + or in a prettyform-like multiline output (the standard way in WMA). + The default value is 'False', keeping the standard Mathics behavior. +
+ + >> $Use2DOutputForm + = False + >> OutputForm[a^b] + = a ^ b + >> $Use2DOutputForm = True; OutputForm[a ^ b] + = + . b + . a + + Setting the variable back to False go back to the normal behavior: + >> $Use2DOutputForm = False; OutputForm[a ^ b] + = a ^ b + """ + + attributes = A_NO_ATTRIBUTES + name = "$Use2DOutputForm" + rules = { + "$Use2DOutputForm": "False", + } + summary_text = "use the 2D OutputForm" diff --git a/mathics/doc/doc_entries.py b/mathics/doc/doc_entries.py index a16f2acf2..11b777947 100644 --- a/mathics/doc/doc_entries.py +++ b/mathics/doc/doc_entries.py @@ -10,7 +10,6 @@ import logging import re from abc import ABC -from os import getenv from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Sequence, Tuple from mathics.core.evaluation import Message, Print, _Out diff --git a/mathics/format/render/pane_text.py b/mathics/format/render/pane_text.py new file mode 100644 index 000000000..288c1c379 --- /dev/null +++ b/mathics/format/render/pane_text.py @@ -0,0 +1,576 @@ +""" +This module produces a "pretty-print" inspired 2d text representation. + +This code is completely independent from Mathics objects, so it could live +alone in a different package. +""" + +from typing import List, Optional, Union + +from sympy.printing.pretty.pretty_symbology import vobj +from sympy.printing.pretty.stringpict import prettyForm, stringPict + + +class TextBlock(prettyForm): + def __init__(self, text, base=0, padding=0, height=1, width=0): + super().__init__(text, base) + assert padding == 0 + assert height == 1 + assert width == 0 + + @staticmethod + def stack(self, *args, align="c"): + if align == "c": + return super.stack(*args) + max_width = max((block.width() for block in args)) + if align == "l": + new_args = [] + for block in args: + block_width = block.width() + if block_width == max_width: + new_args.append(block) + else: + fill_block = TextBlock((max_width - block_width) * " ") + new_block = TextBlock(*TextBlock.next(block, fill_block)) + new_args.append(new_block) + return super.stack(*args) + else: # align=="r" + new_args = [] + for block in args: + block_width = block.width() + if block_width == max_width: + new_args.append(block) + else: + fill_block = TextBlock((max_width - block_width) * " ") + new_block = TextBlock( + *TextBlock.next( + fill_block, + block, + ) + ) + new_args.append(new_block) + return super.stack(*args) + + def root(self, n=None): + """Produce a nice root symbol. + Produces ugly results for big n inserts. + """ + # XXX not used anywhere + # XXX duplicate of root drawing in pretty.py + # put line over expression + result = TextBlock(*self.above("_" * self.width())) + # construct right half of root symbol + height = self.height() + slash = "\n".join(" " * (height - i - 1) + "/" + " " * i for i in range(height)) + slash = stringPict(slash, height - 1) + # left half of root symbol + if height > 2: + downline = stringPict("\\ \n \\", 1) + else: + downline = stringPict("\\") + # put n on top, as low as possible + if n is not None and n.width() > downline.width(): + downline = downline.left(" " * (n.width() - downline.width())) + downline = downline.above(n) + # build root symbol + root = TextBlock(*downline.right(slash)) + # glue it on at the proper height + # normally, the root symbel is as high as self + # which is one less than result + # this moves the root symbol one down + # if the root became higher, the baseline has to grow too + root.baseline = result.baseline - result.height() + root.height() + return result.left(root) + + +class OldTextBlock: + lines: List[str] + width: int + height: int + base: int + + @staticmethod + def _build_attributes(lines, width=0, height=0, base=0): + width = max(width, max(len(line) for line in lines)) if lines else 0 + + # complete lines: + lines = [ + line if len(line) == width else (line + (width - len(line)) * " ") + for line in lines + ] + + if base < 0: + height = height - base + empty_line = width * " " + lines = (-base) * [empty_line] + lines + base = -base + if height > len(lines): + empty_line = width * " " + lines = lines + (height - len(lines)) * [empty_line] + else: + height = len(lines) + + return (lines, width, height, base) + + def __init__(self, text, base=0, padding=0, height=1, width=0): + if isinstance(text, str): + if text == "": + lines = [] + else: + lines = text.split("\n") + else: + lines = sum((line.split("\n") for line in text), []) + if padding: + padding_spaces = padding * " " + lines = [padding_spaces + line.replace("\t", " ") for line in lines] + else: + lines = [line.replace("\t", " ") for line in lines] + + self.lines, self.width, self.height, self.baseline = self._build_attributes( + lines, width, height, base + ) + + @property + def text(self): + return "\n".join(self.lines) + + @text.setter + def text(self, value): + raise TypeError("TextBlock is inmutable") + + def __str__(self): + return self.text + + def __repr__(self): + return self.text + + def __add__(self, tb): + result = TextBlock("") + result += self + result += tb + return result + + def __iadd__(self, tb): + """In-place addition""" + if isinstance(tb, str): + tb = TextBlock(tb) + base = self.base + other_base = tb.base + left_lines = self.lines + right_lines = tb.lines + offset = other_base - base + if offset > 0: + left_lines = left_lines + offset * [self.width * " "] + base = other_base + elif offset < 0: + offset = -offset + right_lines = right_lines + offset * [tb.width * " "] + + offset = len(right_lines) - len(left_lines) + if offset > 0: + left_lines = offset * [self.width * " "] + left_lines + elif offset < 0: + right_lines = (-offset) * [tb.width * " "] + right_lines + + return TextBlock( + list(left + right for left, right in zip(left_lines, right_lines)), + base=base, + ) + + def ajust_base(self, base: int): + """ + if base is larger than self.base, + adds lines at the bottom of the text + and update self.base + """ + if base > self.base: + diff = base - self.base + result = TextBlock( + self.lines + diff * [" "], self.width, self.height, self.base + ) + + return result + + def ajust_width(self, width: int, align: str = "c"): + def padding(lines, diff): + if diff > 0: + if align == "c": + left_pad = int(diff / 2) + right_pad = diff - left_pad + lines = [ + (left_pad * " " + line + right_pad * " ") for line in lines + ] + elif align == "r": + lines = [(diff * " " + line) for line in lines] + else: + lines = [(line + diff * " ") for line in lines] + return lines + + diff_width = width - self.width + if diff_width <= 0: + return self + + new_lines = padding(self.lines, diff_width) + return TextBlock(new_lines, base=self.base) + + def box(self): + top = "+" + self.width * "-" + "+" + out = "\n".join("|" + line + "|" for line in self.lines) + out = top + "\n" + out + "\n" + top + return TextBlock(out, self.base + 1) + + def join(self, iterable): + result = TextBlock("") + for i, item in enumerate(iterable): + if i == 0: + result = item + else: + result = result + self + item + return result + + def stack(self, top, align: str = "c"): + if isinstance(top, str): + top = TextBlock(top) + + bottom = self + bottom_width, top_width = bottom.width, top.width + + if bottom_width > top_width: + top = top.ajust_width(bottom_width, align=align) + elif bottom_width < top_width: + bottom = bottom.ajust_width(top_width, align=align) + + return TextBlock(top.lines + bottom.lines, base=self.base) # type: ignore[union-attr] + + +def _draw_integral_symbol(height: int) -> TextBlock: + if height % 2 == 0: + height = height + 1 + result = TextBlock(vobj("int", height), (height - 1) // 2) + return result + + +def bracket(inner: Union[str, TextBlock]) -> TextBlock: + if isinstance(inner, str): + inner = TextBlock(inner) + + return TextBlock(*inner.parens("[", "]")) + + +def curly_braces(inner: Union[str, TextBlock]) -> TextBlock: + if isinstance(inner, str): + inner = TextBlock(inner) + return TextBlock(*inner.parens("{", "}")) + + +def draw_vertical( + pen: str, height, base=0, left_padding=0, right_padding=0 +) -> TextBlock: + """ + build a TextBlock with a vertical line of height `height` + using the string `pen`. If paddings are given, + spaces are added to the sides. + For example, `draw_vertical("=", 3)` produces + TextBlock(("=\n" + "=\n" + "=", base=base + ) + """ + pen = (left_padding * " ") + str(pen) + (right_padding * " ") + return TextBlock("\n".join(height * [pen]), base=base) + + +def fraction(a: Union[TextBlock, str], b: Union[TextBlock, str]) -> TextBlock: + """ + A TextBlock representation of + a Fraction + """ + if isinstance(a, str): + a = TextBlock(a) + if isinstance(b, str): + b = TextBlock(b) + return a / b + + +def grid(items: list, **options) -> TextBlock: + """ + Process items and build a TextBlock + """ + result: TextBlock = TextBlock("") + + if not items: + return result + + # Ensure that items is a list + items = list(items) + # Ensure that all are TextBlock or list + items = [TextBlock(item) if isinstance(item, str) else item for item in items] + + # options + col_border = options.get("col_border", False) + row_border = options.get("row_border", False) + + # normalize widths: + widths: list = [1] + try: + widths = [1] * max( + len(item) for item in items if isinstance(item, (tuple, list)) + ) + except ValueError: + pass + + full_width: int = 0 + for row in items: + if isinstance(row, TextBlock): + full_width = max(full_width, row.width()) + else: + for index, item in enumerate(row): + widths[index] = max(widths[index], item.width()) + + total_width: int = sum(widths) + max(0, len(widths) - 1) * 3 + + if full_width > total_width: + widths[-1] = widths[-1] + full_width - total_width + total_width = full_width + + # Set the borders + + if row_border: + if col_border: + interline = TextBlock("+" + "+".join((w + 2) * "-" for w in widths) + "+") + else: + interline = TextBlock((sum(w + 3 for w in widths) - 2) * "-") + full_width = interline.width() - 4 + else: + if col_border: + interline = ( + TextBlock("|") + + TextBlock("|".join((w + 2) * " " for w in widths)) + + TextBlock("|") + ) + full_width = max(0, interline.width() - 4) + else: + interline = TextBlock((sum(w + 3 for w in widths) - 3) * " ") + full_width = max(0, interline.width() - 4) + + def normalize_widths(row): + if isinstance(row, TextBlock): + return [row.ajust_width(max(0, full_width), align="l")] + return [item.ajust_width(widths[i]) for i, item in enumerate(row)] + + items = [normalize_widths(row) for row in items] + + if col_border: + for i, row in enumerate(items): + row_height: int = max(item.height for item in row) + row_base: int = max(item.base for item in row) + col_sep = draw_vertical( + "|", height=row_height, base=row_base, left_padding=1, right_padding=1 + ) + + if row: + field, *rest_row_txt = row + new_row_txt = field + for field in rest_row_txt: + new_row_txt = TextBlock( + *TextBlock.next(new_row_txt, col_sep, field) + ) + else: + new_row_txt = TextBlock("") + vertical_line = draw_vertical( + "|", row_height, base=row_base, left_padding=1 + ) + new_row_txt = TextBlock( + *TextBlock.next(vertical_line, new_row_txt, vertical_line) + ) + if i == 0: + if row_border: + new_row_txt = new_row_txt.stack(interline, align="l") + result = new_row_txt + else: + new_row_txt = new_row_txt.stack(interline, align="l") + result = new_row_txt.stack(result, align="l") + else: + for i, row in enumerate(items): + separator = TextBlock(" ") + if row: + field, *rest = row + new_row_txt = field + for field in rest: + new_row_txt = TextBlock( + *TextBlock.next(new_row_txt, separator, field) + ) + else: + new_row_txt = TextBlock("") + if i == 0: + if row_border: + new_row_txt = new_row_txt.stack(interline, align="l") + result = new_row_txt + else: + new_row_txt = new_row_txt.stack(interline, align="l") + result = new_row_txt.stack(result, align="l") + + if row_border: + result = interline.stack(result, align="l") + + result.baseline = int(result.height() / 2) + return result + + +def integral_indefinite( + integrand: Union[TextBlock, str], var: Union[TextBlock, str] +) -> TextBlock: + # TODO: handle list of vars + # TODO: use utf as an option + if isinstance(var, str): + var = TextBlock(var) + + if isinstance(integrand, str): + integrand = TextBlock(integrand) + + int_symb: TextBlock = _draw_integral_symbol(integrand.height()) + return TextBlock(*TextBlock.next(int_symb, integrand, TextBlock(" d"), var)) + + +def integral_definite( + integrand: Union[TextBlock, str], + var: Union[TextBlock, str], + a: Union[TextBlock, str], + b: Union[TextBlock, str], +) -> TextBlock: + # TODO: handle list of vars + # TODO: use utf as an option + if isinstance(var, str): + var = TextBlock(var) + if isinstance(integrand, str): + integrand = TextBlock(integrand) + if isinstance(a, str): + a = TextBlock(a) + if isinstance(b, str): + b = TextBlock(b) + + h_int = integrand.height() + symbol_height = h_int + # for ascii, symbol_height +=2 + int_symb = _draw_integral_symbol(symbol_height) + orig_baseline = int_symb.baseline + int_symb = subsuperscript(int_symb, a, b) + return TextBlock(*TextBlock.next(int_symb, integrand, TextBlock(" d"), var)) + + +def parenthesize(inner: Union[str, TextBlock]) -> TextBlock: + if isinstance(inner, str): + inner = TextBlock(inner) + + return TextBlock(*inner.parens()) + + +def sqrt_block( + a: Union[TextBlock, str], index: Optional[Union[TextBlock, str]] = None +) -> TextBlock: + """ + Sqrt Text Block + """ + if isinstance(a, str): + a = TextBlock(a) + if index is None: + index = "" + if isinstance(index, str): + index = TextBlock(index) + + return TextBlock(*a.root(index)) + + a_height = a.height + result_2 = TextBlock( + "\n".join("|" + line for line in a.text.split("\n")), base=a.base + ) + result_2 = result_2.stack((a.width + 1) * "_", align="l") + half_height = int(a_height / 2 + 1) + + result_1 = TextBlock( + "\n".join( + [ + (int(i) * " " + "\\" + int((half_height - i - 1)) * " ") + for i in range(half_height) + ] + ), + base=a.base, + ) + if index is not None: + result_1 = result_1.stack(index, align="c") + return result_1 + result_2 + + +def subscript(base: Union[TextBlock, str], a: Union[TextBlock, str]) -> TextBlock: + """ + Join b with a as a subscript. + """ + if isinstance(a, str): + a = TextBlock(a) + if isinstance(base, str): + base = TextBlock(base) + + a = TextBlock(*TextBlock.next(TextBlock(base.width() * " "), a)) + base = TextBlock(*TextBlock.next(base, TextBlock(a.width() * " "))) + result = TextBlock(*TextBlock.below(base, a)) + return result + + +def subsuperscript( + base: Union[TextBlock, str], a: Union[TextBlock, str], b: Union[TextBlock, str] +) -> TextBlock: + """ + Join base with a as a superscript and b as a subscript + """ + if isinstance(base, str): + base = TextBlock(base) + if isinstance(a, str): + a = TextBlock(a) + if isinstance(b, str): + b = TextBlock(b) + + # Ensure that a and b have the same width + width_diff = a.width() - b.width() + if width_diff < 0: + a = TextBlock(*TextBlock.next(a, TextBlock((-width_diff) * " "))) + elif width_diff > 0: + b = TextBlock(*TextBlock.next(b, TextBlock((width_diff) * " "))) + + indx_spaces = b.width() * " " + base_spaces = base.width() * " " + a = TextBlock(*TextBlock.next(TextBlock(base_spaces), a)) + b = TextBlock(*TextBlock.next(TextBlock(base_spaces), b)) + base = TextBlock(*TextBlock.next(base, TextBlock(base_spaces))) + result = TextBlock(*TextBlock.below(base, a)) + result = TextBlock(*TextBlock.above(result, b)) + return result + + +def superscript(base: Union[TextBlock, str], a: Union[TextBlock, str]) -> TextBlock: + if isinstance(a, str): + a = TextBlock(a) + if isinstance(base, str): + base = TextBlock(base) + + base_width, a_width = base.width(), a.width() + a = TextBlock(*TextBlock.next(TextBlock(base_width * " "), a)) + base = TextBlock(*TextBlock.next(base, TextBlock(a_width * " "))) + result = TextBlock(*TextBlock.above(base, a)) + return result + + +def join_blocks(*blocks) -> TextBlock: + """ + Concatenate blocks. + The same that the idiom + TextBlock(*TextBlock.next(*blocks)) + """ + return TextBlock(*TextBlock.next(*blocks)) + + +TEXTBLOCK_COMMA = TextBlock(",") +TEXTBLOCK_MINUS = TextBlock("-") +TEXTBLOCK_NULL = TextBlock("") +TEXTBLOCK_PLUS = TextBlock("+") +TEXTBLOCK_QUOTE = TextBlock("'") +TEXTBLOCK_SPACE = TextBlock(" ") diff --git a/mathics/format/render/prettyprint.py b/mathics/format/render/prettyprint.py new file mode 100644 index 000000000..8b42bbb15 --- /dev/null +++ b/mathics/format/render/prettyprint.py @@ -0,0 +1,864 @@ +""" +This module builts the 2D string associated to the OutputForm +""" + +from typing import Callable, Dict, List, Union + +from mathics.core.atoms import ( + Integer, + Integer1, + Integer2, + IntegerM1, + PrecisionReal, + Rational, + Real, + String, +) +from mathics.core.element import BaseElement +from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression +from mathics.core.list import ListExpression +from mathics.core.number import dps +from mathics.core.symbols import Atom, Symbol, SymbolTimes +from mathics.core.systemsymbols import ( + SymbolDerivative, + SymbolInfix, + SymbolNone, + SymbolOutputForm, + SymbolPower, + SymbolStandardForm, + SymbolTraditionalForm, +) +from mathics.format.box import compare_precedence, do_format # , format_element +from mathics.format.box.numberform import numberform_to_boxes +from mathics.format.render.pane_text import ( + TEXTBLOCK_COMMA, + TEXTBLOCK_MINUS, + TEXTBLOCK_NULL, + TEXTBLOCK_PLUS, + TEXTBLOCK_QUOTE, + TEXTBLOCK_SPACE, + TextBlock, + bracket, + curly_braces, + fraction, + grid, + integral_definite, + integral_indefinite, + join_blocks, + parenthesize, + sqrt_block, + subscript, + subsuperscript, + superscript, +) + +SymbolNonAssociative = Symbol("System`NonAssociative") +SymbolPostfix = Symbol("System`Postfix") +SymbolPrefix = Symbol("System`Prefix") +SymbolRight = Symbol("System`Right") +SymbolLeft = Symbol("System`Left") + + +TEXTBLOCK_ARROBA = TextBlock("@") +TEXTBLOCK_BACKQUOTE = TextBlock("`") +TEXTBLOCK_DOUBLESLASH = TextBlock("//") +TEXTBLOCK_GRAPHICS = TextBlock("-Graphics-") +TEXTBLOCK_GRAPHICS3D = TextBlock("-Graphics3D-") +TEXTBLOCK_ONE = TextBlock("1") +TEXTBLOCK_TILDE = TextBlock("~") + +#### Functions that convert Expressions in TextBlock + + +expr_to_2d_text_map: Dict[str, Callable] = {} + + +# This Exception if the expression should +# be processed by the default routine +class _WrongFormattedExpression(Exception): + pass + + +class IsNotGrid(Exception): + pass + + +class IsNot2DArray(Exception): + pass + + +def render_2d_text(expr: BaseElement, evaluation: Evaluation, **kwargs): + """ + Build a 2d text from an `Expression` + """ + ## TODO: format the expression + format_expr: Expression = do_format(expr, evaluation, SymbolOutputForm) # type: ignore + + # Strip HoldForm + while format_expr.has_form("HoldForm", 1): # type: ignore + format_expr = format_expr.elements[0] + + lookup_name = format_expr.get_head().get_lookup_name() + try: + result = expr_to_2d_text_map[lookup_name](format_expr, evaluation, **kwargs) + return result + except _WrongFormattedExpression: + # If the key is not present, or the execution fails for any reason, use + # the default + pass + except KeyError: + pass + return _default_render_2d_text(format_expr, evaluation, **kwargs) + + +def _default_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + """ + Default representation of a function + """ + expr_head = expr.head + head = render_2d_text(expr_head, evaluation, **kwargs) + comma = join_blocks(TEXTBLOCK_COMMA, TEXTBLOCK_SPACE) + elements = [render_2d_text(elem, evaluation) for elem in expr.elements] + result = elements.pop(0) if elements else TEXTBLOCK_SPACE + while elements: + result = join_blocks(result, comma, elements.pop(0)) + + if kwargs.get("_Form", SymbolStandardForm) is SymbolTraditionalForm: + return join_blocks(head, parenthesize(result)) + return join_blocks(head, bracket(result)) + + +def _divide(num, den, evaluation, **kwargs): + if kwargs.get("2d", False): + return fraction( + render_2d_text(num, evaluation, **kwargs), + render_2d_text(den, evaluation, **kwargs), + ) + infix_form = Expression( + SymbolInfix, ListExpression(num, den), String("/"), Integer(400), SymbolLeft + ) + return render_2d_text(infix_form, evaluation, **kwargs) + + +def _strip_1_parm_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + if len(expr.elements) != 1: + raise _WrongFormattedExpression + return render_2d_text(expr.elements[0], evaluation, **kwargs) + + +expr_to_2d_text_map["System`HoldForm"] = _strip_1_parm_render_2d_text +expr_to_2d_text_map["System`InputForm"] = _strip_1_parm_render_2d_text + + +def derivative_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + """Derivative operator""" + head = expr.get_head() + if head is SymbolDerivative: + return _default_render_2d_text(expr, evaluation, **kwargs) + super_head = head.get_head() + if super_head is SymbolDerivative: + expr_elements = expr.elements + if len(expr_elements) != 1: + return _default_render_2d_text(expr, evaluation, **kwargs) + function_head = render_2d_text(expr_elements[0], evaluation, **kwargs) + derivatives = head.elements + if len(derivatives) == 1: + order_iv = derivatives[0] + if order_iv == Integer1: + return join_blocks(function_head, TEXTBLOCK_QUOTE) + elif order_iv == Integer2: + return join_blocks(function_head, TEXTBLOCK_QUOTE, TEXTBLOCK_QUOTE) + + if not kwargs["2d"]: + return _default_render_2d_text(expr, evaluation, **kwargs) + + comma = TEXTBLOCK_COMMA + superscript_tb, *rest_derivatives = ( + render_2d_text(order, evaluation, **kwargs) for order in derivatives + ) + for order in rest_derivatives: + superscript_tb = join_blocks(superscript_tb, comma, order) + + superscript_tb = parenthesize(superscript_tb) + return superscript(function_head, superscript_tb) + + # Full Function with arguments: delegate to the default conversion. + # It will call us again with the head + return _default_render_2d_text(expr, evaluation, **kwargs) + + +expr_to_2d_text_map["System`Derivative"] = derivative_render_2d_text + + +def divide_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + if len(expr.elements) != 2: + raise _WrongFormattedExpression + num, den = expr.elements + return _divide(num, den, evaluation, **kwargs) + + +expr_to_2d_text_map["System`Divide"] = divide_render_2d_text + + +def graphics(expr: Expression, evaluation: Evaluation, **kwargs) -> TextBlock: + return TEXTBLOCK_GRAPHICS + + +expr_to_2d_text_map["System`Graphics"] = graphics + + +def graphics3d(expr: Expression, evaluation: Evaluation, **kwargs) -> TextBlock: + return TEXTBLOCK_GRAPHICS3D + + +expr_to_2d_text_map["System`Graphics3D"] = graphics3d + + +def grid_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + if len(expr.elements) == 0: + raise IsNotGrid + if len(expr.elements) > 1 and not expr.elements[1].has_form( + ["Rule", "RuleDelayed"], 2 + ): + raise IsNotGrid + if not expr.elements[0].has_form("List", None): + raise IsNotGrid + + elements = expr.elements[0].elements + rows = [] + for idx, item in enumerate(elements): + if item.has_form("List", None): + rows.append( + [ + render_2d_text(item_elem, evaluation, **kwargs) + for item_elem in item.elements + ] + ) + else: + rows.append(render_2d_text(item, evaluation, **kwargs)) + + return grid(rows) + + +expr_to_2d_text_map["System`Grid"] = grid_render_2d_text + + +def integer_render_2d_text(n: Integer, evaluation: Evaluation, **kwargs): + return TextBlock(str(n.value)) + + +expr_to_2d_text_map["System`Integer"] = integer_render_2d_text + + +def integrate_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + elems = list(expr.elements) + if len(elems) > 2 or not kwargs.get("2d", False): + raise _WrongFormattedExpression + + integrand = elems.pop(0) + result = render_2d_text(integrand, evaluation, **kwargs) + while elems: + var = elems.pop(0) + if var.has_form("List", 3): + var_txt, a, b = ( + render_2d_text(item, evaluation, **kwargs) for item in var.elements + ) + result = integral_definite(result, var_txt, a, b) + elif isinstance(var, Symbol): + var_txt = render_2d_text(var, evaluation, **kwargs) + result = integral_indefinite(result, var_txt) + else: + break + return result + + +expr_to_2d_text_map["System`Integrate"] = integrate_render_2d_text + + +def list_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + result, *rest_elems = ( + render_2d_text(elem, evaluation, **kwargs) for elem in expr.elements + ) + comma_tb = join_blocks(TEXTBLOCK_COMMA, TEXTBLOCK_SPACE) + for next_elem in rest_elems: + result = TextBlock(*TextBlock.next(result, comma_tb, next_elem)) + return curly_braces(result) + + +expr_to_2d_text_map["System`List"] = list_render_2d_text + + +def mathmlform_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + # boxes = format_element(expr.elements[0], evaluation) + boxes = Expression( + Symbol("System`MakeBoxes"), expr.elements[0], SymbolStandardForm + ).evaluate(evaluation) + return TextBlock(boxes.boxes_to_mathml()) # type: ignore[union-attr] + + +expr_to_2d_text_map["System`MathMLForm"] = mathmlform_render_2d_text + + +def matrixform_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + # return parenthesize(tableform_render_2d_text(expr, evaluation, **kwargs)) + return tableform_render_2d_text(expr, evaluation, **kwargs) + + +expr_to_2d_text_map["System`MatrixForm"] = matrixform_render_2d_text + + +def plus_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + elements = expr.elements + result = TEXTBLOCK_NULL + tb_minus = join_blocks(TEXTBLOCK_SPACE, TEXTBLOCK_MINUS, TEXTBLOCK_SPACE) + tb_plus = join_blocks(TEXTBLOCK_SPACE, TEXTBLOCK_PLUS, TEXTBLOCK_SPACE) + for i, elem in enumerate(elements): + if elem.has_form("Times", None): + # If the first element is -1, remove it and use + # a minus sign. Otherwise, if negative, do not add a sign. + first = elem.elements[0] + if isinstance(first, Integer): + if first.value == -1: + result = join_blocks( + result, + tb_minus, + render_2d_text( + Expression(SymbolTimes, *elem.elements[1:]), + evaluation, + **kwargs, + ), + ) + continue + elif first.value < 0: + result = join_blocks( + result, + TEXTBLOCK_SPACE, + render_2d_text(elem, evaluation, **kwargs), + ) + continue + elif isinstance(first, Real): + if first.value < 0: + result = join_blocks( + result, + TEXTBLOCK_SPACE, + render_2d_text(elem, evaluation, **kwargs), + ) + continue + result = join_blocks( + result, tb_plus, render_2d_text(elem, evaluation, **kwargs) + ) + ## TODO: handle complex numbers? + else: + elem_txt = render_2d_text(elem, evaluation, **kwargs) + if (compare_precedence(elem, 310) or -1) < 0: + elem_txt = parenthesize(elem_txt) + result = join_blocks(result, tb_plus, elem_txt) + elif i == 0 or ( + (isinstance(elem, Integer) and elem.value < 0) + or (isinstance(elem, Real) and elem.value < 0) + ): + result = join_blocks(result, elem_txt) + else: + result = join_blocks( + result, + tb_plus, + render_2d_text(elem, evaluation, **kwargs), + ) + return result + + +expr_to_2d_text_map["System`Plus"] = plus_render_2d_text + + +def power_render_2d_text(expr: Expression, evaluation: Evaluation, **kwargs): + if len(expr.elements) != 2: + raise _WrongFormattedExpression + if kwargs.get("2d", False): + base, exponent = ( + render_2d_text(elem, evaluation, **kwargs) for elem in expr.elements + ) + if (compare_precedence(expr.elements[0], 590) or 1) == -1: + base = parenthesize(base) + return superscript(base, exponent) + + infix_form = Expression( + SymbolInfix, + ListExpression(*(expr.elements)), + String("^"), + Integer(590), + SymbolRight, + ) + return render_2d_text(infix_form, evaluation, **kwargs) + + +expr_to_2d_text_map["System`Power"] = power_render_2d_text + + +def pre_pos_fix_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + elements = expr.elements + if not (0 <= len(elements) <= 4): + raise _WrongFormattedExpression + + group = None + precedence = 670 + # Processing the first argument: + head = expr.get_head() + target = expr.elements[0] + if isinstance(target, Atom): + raise _WrongFormattedExpression + + operands = list(target.elements) + if len(operands) != 1: + raise _WrongFormattedExpression + + # Processing the second argument, if it is there: + if len(elements) > 1: + ops = elements[1] + ops_txt = [render_2d_text(ops, evaluation, **kwargs)] + else: + if head is SymbolPrefix: + default_symb = TEXTBLOCK_ARROBA + ops_txt = join_blocks( + render_2d_text(head, evaluation, **kwargs), default_symb + ) + else: # head is SymbolPostfix: + default_symb = TEXTBLOCK_DOUBLESLASH + ops_txt = join_blocks( + default_symb, render_2d_text(head, evaluation, **kwargs) + ) + + # Processing the third argument, if it is there: + if len(elements) > 2: + if isinstance(elements[2], Integer): + precedence = elements[2].value + else: + raise _WrongFormattedExpression + + # Processing the forth argument, if it is there: + if len(elements) > 3: + group = elements[3] + if group not in (SymbolNone, SymbolLeft, SymbolRight, SymbolNonAssociative): + raise _WrongFormattedExpression + if group is SymbolNone: + group = None + + operand = operands[0] + cmp_precedence = compare_precedence(operand, precedence) + target_txt = render_2d_text(operand, evaluation, **kwargs) + if cmp_precedence is not None and cmp_precedence != -1: + target_txt = parenthesize(target_txt) + + return ( + join_blocks(ops_txt[0], target_txt) + if head is SymbolPrefix + else join_blocks(target_txt, ops_txt[0]) + ) + + +expr_to_2d_text_map["System`Prefix"] = pre_pos_fix_render_2d_text +expr_to_2d_text_map["System`Postfix"] = pre_pos_fix_render_2d_text + + +def infix_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + elements = expr.elements + if not (0 <= len(elements) <= 4): + raise _WrongFormattedExpression + + group = None + precedence = 670 + # Processing the first argument: + head = expr.get_head() + target = expr.elements[0] + if isinstance(target, Atom): + raise _WrongFormattedExpression + + operands = list(target.elements) + + if len(operands) < 2: + raise _WrongFormattedExpression + + # Processing the second argument, if it is there: + if len(elements) > 1: + ops = elements[1] + if head is SymbolInfix: + # This is not the WMA behaviour, but the Mathics current implementation requires it: + num_ops = 1 + if ops.has_form("List", None): + num_ops = len(ops.elements) + ops_lst = [ + render_2d_text(op, evaluation, **kwargs) for op in ops.elements + ] + else: + ops_lst = [render_2d_text(ops, evaluation, **kwargs)] + elif head in (SymbolPrefix, SymbolPostfix): + ops_txt = [render_2d_text(ops, evaluation, **kwargs)] + else: + num_ops = 1 + default_symb = join_blocks(TEXTBLOCK_SPACE, TEXTBLOCK_TILDE, TEXTBLOCK_SPACE) + ops_lst = [ + join_blocks( + default_symb, + render_2d_text(head, evaluation, **kwargs), + default_symb, + ) + ] + + # Processing the third argument, if it is there: + if len(elements) > 2: + if isinstance(elements[2], Integer): + precedence = elements[2].value + else: + raise _WrongFormattedExpression + + # Processing the forth argument, if it is there: + if len(elements) > 3: + group = elements[3] + if group not in (SymbolNone, SymbolLeft, SymbolRight, SymbolNonAssociative): + raise _WrongFormattedExpression + if group is SymbolNone: + group = None + + parenthesized = group in (None, SymbolRight, SymbolNonAssociative) + for index, operand in enumerate(operands): + operand_txt = render_2d_text(operand, evaluation, **kwargs) + cmp_precedence = compare_precedence(operand, precedence) + if cmp_precedence is not None and ( + cmp_precedence == -1 or (cmp_precedence == 0 and parenthesized) + ): + operand_txt = parenthesize(operand_txt) + + if index == 0: + result = operand_txt + # After the first element, for lateral + # associativity, parenthesized is flipped: + if group in (SymbolLeft, SymbolRight): + parenthesized = not parenthesized + else: + space = TEXTBLOCK_SPACE + if str(ops_lst[index % num_ops]) != " ": + result_lst = [ + result, + space, + ops_lst[index % num_ops], + space, + operand_txt, + ] + else: + result_lst = [result, space, operand_txt] + + return join_blocks(*result_lst) + + +expr_to_2d_text_map["System`Infix"] = infix_render_2d_text + + +def precedenceform_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + if len(expr.elements) == 2: + return render_2d_text(expr.elements[0], evaluation, **kwargs) + raise _WrongFormattedExpression + + +expr_to_2d_text_map["System`PrecedenceForm"] = precedenceform_render_2d_text + + +def rational_render_2d_text( + n: Union[Rational, Expression], evaluation: Evaluation, **kwargs +): + if n.has_form("Rational", 2): + num, den = n.elements # type: ignore[union-attr] + else: + num, den = n.numerator(), n.denominator() # type: ignore[union-attr] + return _divide(num, den, evaluation, **kwargs) + + +expr_to_2d_text_map["System`Rational"] = rational_render_2d_text + + +def real_render_2d_text(n: Real, evaluation: Evaluation, **kwargs): + if not isinstance(n, Real): + raise _WrongFormattedExpression + py_digits, py_options = kwargs.setdefault( + "_numberform_args", + ( + (None, None), + {}, + ), + ) + py_options["_Form"] = "System`OutputForm" + digits, padding = py_digits + if digits is None: + digits = dps(n.get_precision()) if isinstance(n, PrecisionReal) else 6 + + result = numberform_to_boxes(n, digits, padding, evaluation, py_options) + if isinstance(result, String): + return result.value + return result.boxes_to_text() + + +expr_to_2d_text_map["System`Real"] = real_render_2d_text + + +def sqrt_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + if not 1 <= len(expr.elements) <= 2: + raise _WrongFormattedExpression + if kwargs.get("2d", False): + return sqrt_block( + *(render_2d_text(item, evaluation, **kwargs) for item in expr.elements) + ) + raise _WrongFormattedExpression + + +expr_to_2d_text_map["System`Sqrt"] = sqrt_render_2d_text + + +def subscript_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + if len(expr.elements) != 2: + raise _WrongFormattedExpression + if kwargs.get("2d", False): + return subscript( + *(render_2d_text(item, evaluation, **kwargs) for item in expr.elements) + ) + raise _WrongFormattedExpression + + +expr_to_2d_text_map["System`Subscript"] = subscript_render_2d_text + + +def subsuperscript_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + if len(expr.elements) != 3: + raise _WrongFormattedExpression + if kwargs.get("2d", False): + return subsuperscript( + *(render_2d_text(item, evaluation, **kwargs) for item in expr.elements) + ) + raise _WrongFormattedExpression + + +expr_to_2d_text_map["System`Subsuperscript"] = subsuperscript_render_2d_text + + +def string_render_2d_text(expr: String, evaluation: Evaluation, **kwargs) -> TextBlock: + lines = expr.value.split("\n") + max_len = max([len(line) for line in lines]) + lines = [line + (max_len - len(line)) * " " for line in lines] + return TextBlock("\n".join(lines)) + + +expr_to_2d_text_map["System`String"] = string_render_2d_text + + +def stringform_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + strform = expr.elements[0] + if not isinstance(strform, String): + raise _WrongFormattedExpression + + items = list( + render_2d_text(item, evaluation, **kwargs) for item in expr.elements[1:] + ) + + curr_indx = 0 + parts = strform.value.split("`") + result = TextBlock(parts[0]) + if len(parts) == 1: + return result + + quote_open = True + remaining = len(parts) - 1 + + for part in parts[1:]: + remaining -= 1 + if quote_open: + if remaining == 0: + result = result + "`" + part + quote_open = False + continue + if len(part) == 0: + result = result + items[curr_indx] + continue + try: + idx = int(part) + except ValueError: + idx = None + if idx is not None and str(idx) == part: + curr_indx = idx - 1 + result = result + items[curr_indx] + quote_open = False + continue + else: + result = join_blocks( + result, TEXTBLOCK_BACKQUOTE, part, TEXTBLOCK_BACKQUOTE + ) + quote_open = False + continue + else: + result = join_blocks(result, part) + quote_open = True + + return result + + +expr_to_2d_text_map["System`StringForm"] = stringform_render_2d_text + + +def superscript_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + elements = expr.elements + if len(elements) != 2: + raise _WrongFormattedExpression + if kwargs.get("2d", False): + base, exponent = elements + base_tb, exponent_tb = ( + render_2d_text(item, evaluation, **kwargs) for item in elements + ) + precedence = compare_precedence(base, 590) or 1 + if precedence < 0: + base_tb = parenthesize(base_tb) + return superscript(base_tb, exponent_tb) + infix_form = Expression( + SymbolInfix, + ListExpression(*(expr.elements)), + String("^"), + Integer(590), + SymbolRight, + ) + return render_2d_text(infix_form, evaluation, **kwargs) + + +expr_to_2d_text_map["System`Superscript"] = superscript_render_2d_text + + +def symbol_render_2d_text(symb: Symbol, evaluation: Evaluation, **kwargs): + return TextBlock(evaluation.definitions.shorten_name(symb.name)) + + +expr_to_2d_text_map["System`Symbol"] = symbol_render_2d_text + + +def tableform_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + return grid_render_2d_text(expr, evaluation) + + +expr_to_2d_text_map["System`TableForm"] = tableform_render_2d_text + + +def texform_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + # boxes = format_element(expr.elements[0], evaluation) + boxes = Expression( + Symbol("System`MakeBoxes"), expr.elements[0], SymbolStandardForm + ).evaluate(evaluation) + return TextBlock(boxes.boxes_to_tex()) # type: ignore + + +expr_to_2d_text_map["System`TeXForm"] = texform_render_2d_text + + +def times_render_2d_text( + expr: Expression, evaluation: Evaluation, **kwargs +) -> TextBlock: + elements = expr.elements + num: List[BaseElement] = [] + den: List[BaseElement] = [] + # First, split factors with integer, negative powers: + for elem in elements: + if elem.has_form("Power", 2): + base, exponent = elem.elements + if isinstance(exponent, Integer): + if exponent.value == -1: + den.append(base) + continue + elif exponent.value < 0: + den.append(Expression(SymbolPower, base, Integer(-exponent.value))) + continue + elif isinstance(elem, Rational): + num.append(elem.numerator()) + den.append(elem.denominator()) + continue + elif elem.has_form("Rational", 2): + elem_elements = elem.elements + num.append(elem_elements[0]) + den.append(elem_elements[1]) + continue + + num.append(elem) + + # If there are integer, negative powers, process as a fraction: + if den: + den_expr = den[0] if len(den) == 1 else Expression(SymbolTimes, *den) + num_expr = ( + Expression(SymbolTimes, *num) + if len(num) > 1 + else num[0] + if len(num) == 1 + else Integer1 + ) + return _divide(num_expr, den_expr, evaluation, **kwargs) + + # there are no integer negative powers: + if len(num) == 1: + return render_2d_text(num[0], evaluation, **kwargs) + + prefactor = 1 + result: TextBlock = TEXTBLOCK_NULL + for i, elem in enumerate(num): + if elem is IntegerM1: + prefactor *= -1 + continue + if isinstance(elem, Integer): + prefactor *= -1 + elem = Integer(-elem.value) + + elem_txt = render_2d_text(elem, evaluation, **kwargs) + if compare_precedence(elem, 400): + elem_txt = parenthesize(elem_txt) + if i == 0: + result = elem_txt + else: + result = join_blocks(result, TEXTBLOCK_SPACE, elem_txt) + if str(result) == "": + result = TEXTBLOCK_ONE + if prefactor == -1: + result = join_blocks(TEXTBLOCK_MINUS, result) + return result + + +expr_to_2d_text_map["System`Times"] = times_render_2d_text diff --git a/test/format/test_2d.py b/test/format/test_2d.py new file mode 100644 index 000000000..1165415a2 --- /dev/null +++ b/test/format/test_2d.py @@ -0,0 +1,44 @@ +""" +Test 2d Output form +""" + +from test.helper import session + +import pytest + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + ("$Use2DOutputForm=True;", "Null", "Set the 2D form"), + ( + '"Hola\nCómo estás?"', + ("\n" "Hola \n" "Cómo estás?"), + "String", + ), + ("a^b", ("\n" " b\n" "a "), "power"), + ("(-a)^b", ("\n" " b\n" "(-a) "), "power of negative"), + ("(a+b)^c", ("\n" " c\n" "(a + b) "), "power with composite basis"), + ("Derivative[1][f][x]", "f'[x]", "first derivative"), + ("Derivative[2][f][x]", "f''[x]", "second derivative"), + ("Derivative[3][f][x]", ("\n" " (3) \n" "f [x]"), "Third derivative"), + ( + "Derivative[0,2][f][x]", + ("\n" " (0,2) \n" "f [x]"), + "partial derivative", + ), + ( + "Integrate[f[x]^2,x]", + ("\n" "⌠ 2 \n" "⎮f[x] dx\n" "⌡ "), + "Indefinite integral", + ), + ("$Use2DOutputForm=False;", "Null", "Go back to the standard behavior."), + ], +) +def test_Output2D(str_expr: str, str_expected: str, msg: str): + test_expr = f"OutputForm[{str_expr}]" + result = session.evaluate_as_in_cli(test_expr).result + if msg: + assert result == str_expected, msg + else: + assert result == str_expected