diff --git a/mathics/builtin/colors/color_operations.py b/mathics/builtin/colors/color_operations.py index b5cbcc429..2596e162f 100644 --- a/mathics/builtin/colors/color_operations.py +++ b/mathics/builtin/colors/color_operations.py @@ -24,6 +24,7 @@ try: import numpy + import PIL.Image as PILImage import PIL.ImageOps _enabled = True @@ -377,7 +378,7 @@ def eval(self, image, n, prop, evaluation, options): im = ( image.color_convert("RGB") .pil() - .convert("P", palette=PIL.Image.ADAPTIVE, colors=256) + .convert("P", palette=PILImage.ADAPTIVE, colors=256) ) pixels = numpy.array(list(im.getdata())) diff --git a/mathics/builtin/drawing/image.py b/mathics/builtin/drawing/image.py index bfaa64348..6c5f29678 100644 --- a/mathics/builtin/drawing/image.py +++ b/mathics/builtin/drawing/image.py @@ -68,6 +68,7 @@ import numpy import PIL + import PIL.Image as PILImage import PIL.ImageEnhance import PIL.ImageFilter import PIL.ImageOps @@ -158,7 +159,7 @@ class ImageImport(_ImageBuiltin): def eval(self, path: String, evaluation: Evaluation): """ImageImport[path_String]""" - pillow = PIL.Image.open(path.value) + pillow = PILImage.open(path.value) pixels = numpy.asarray(pillow) is_rgb = len(pixels.shape) >= 3 and pixels.shape[2] >= 3 options_from_exif = extract_exif(pillow, evaluation) @@ -644,7 +645,7 @@ def eval(self, image, angle, evaluation: Evaluation): def rotate(im): return im.rotate( - 180 * py_angle / math.pi, resample=PIL.Image.BICUBIC, expand=True + 180 * py_angle / math.pi, resample=PILImage.BICUBIC, expand=True ) return image.filter(rotate) @@ -1308,7 +1309,7 @@ def eval(self, image, n: Integer, evaluation: Evaluation): if converted is None: return pixels = pixels_as_ubyte(converted.pixels) - im = PIL.Image.fromarray(pixels).quantize(py_value) + im = PILImage.fromarray(pixels).quantize(py_value) im = im.convert("RGB") return Image(numpy.array(im), "RGB") @@ -2038,7 +2039,7 @@ def atom_to_boxes(self, form, evaluation: Evaluation) -> ImageBox: pillow = deepcopy(self.pillow) else: pixels_format = "RGBA" if len(shape) >= 3 and shape[2] == 4 else "RGB" - pillow = PIL.Image.fromarray(pixels, pixels_format) + pillow = PILImage.fromarray(pixels, pixels_format) # if the image is very small, scale it up using nearest neighbour. min_size = 128 @@ -2047,7 +2048,7 @@ def atom_to_boxes(self, form, evaluation: Evaluation) -> ImageBox: scaled_width = int(scale * width) scaled_height = int(scale * height) pillow = pillow.resize( - (scaled_height, scaled_width), resample=PIL.Image.NEAREST + (scaled_height, scaled_width), resample=PILImage.NEAREST ) with warnings.catch_warnings(): @@ -2105,7 +2106,7 @@ def filter(self, f): # apply PIL filters component-wise pixels = self.pixels n = pixels.shape[2] channels = [ - f(PIL.Image.fromarray(c, "L")) for c in (pixels[:, :, i] for i in range(n)) + f(PILImage.fromarray(c, "L")) for c in (pixels[:, :, i] for i in range(n)) ] return Image(numpy.dstack(channels), self.color_space) @@ -2171,7 +2172,7 @@ def pil(self): else: raise NotImplementedError - return PIL.Image.fromarray(pixels, mode) + return PILImage.fromarray(pixels, mode) def options(self): return ListExpression( diff --git a/mathics/builtin/layout.py b/mathics/builtin/layout.py index f79125dfb..78565023f 100644 --- a/mathics/builtin/layout.py +++ b/mathics/builtin/layout.py @@ -8,20 +8,32 @@ we can use ``Row`` """ - +from typing import Optional, Union from mathics.builtin.base import BinaryOperator, Builtin, Operator from mathics.builtin.box.layout import GridBox, RowBox, to_boxes from mathics.builtin.lists import list_boxes from mathics.builtin.makeboxes import MakeBoxes from mathics.builtin.options import options_to_rules -from mathics.core.atoms import Real, String +from mathics.core.atoms import Integer, Integer1, Real, String +from mathics.core.convert.op import operator_to_ascii, operator_to_unicode +from mathics.core.element import BaseElement from mathics.core.expression import Expression from mathics.core.list import ListExpression -from mathics.core.symbols import Symbol -from mathics.core.systemsymbols import SymbolMakeBoxes -from mathics.eval.makeboxes import format_element - +from mathics.core.symbols import Atom, Symbol +from mathics.core.systemsymbols import ( + SymbolFullForm, + SymbolInfix, + SymbolInputForm, + SymbolLeft, + SymbolMakeBoxes, + SymbolNone, + SymbolOutputForm, + SymbolRight, +) +from mathics.eval.makeboxes import format_element, make_boxes_infix + +SymbolNonAssociative = Symbol("System`NonAssociative") SymbolSubscriptBox = Symbol("System`SubscriptBox") @@ -145,11 +157,178 @@ class Infix(Builtin): """ messages = { - "normal": "Nonatomic expression expected at position `1`", + "argb": "Infix called with `1` arguments; between 1 and 4 arguments are expected.", + "group": "Infix::group: Grouping specification `1` is not NonAssociative, None, Left, or Right.", "intm": "Machine-sized integer expected at position 3 in `1`", + "normal": "Nonatomic expression expected at position `1`", } summary_text = "infix form" + # the right rule should be + # mbexpression:MakeBoxes[Infix[___], form] + + def eval_infix_1(self, expr: Expression, form: Symbol, evaluation): + """MakeBoxes[Infix[expr_], + form:StandardForm|TraditionalForm|OutputForm|InputForm]""" + return self.do_eval_infix(expr, String("~"), form, evaluation) + + def eval_infix_2( + self, expr: Expression, operator: BaseElement, form: Symbol, evaluation + ): + """MakeBoxes[Infix[expr_, operator_], + form:StandardForm|TraditionalForm|OutputForm|InputForm]""" + return self.do_eval_infix(expr, operator, form, evaluation) + + def eval_infix_3( + self, + expr: Expression, + operator: BaseElement, + precedence: BaseElement, + form: Symbol, + evaluation, + ): + """MakeBoxes[Infix[expr_, operator_, precedence_], + form:StandardForm|TraditionalForm|OutputForm|InputForm]""" + + if not isinstance(precedence, Integer): + evaluation.message( + "Infix", + "intm", + Expression( + SymbolFullForm, Expression(SymbolInfix, expr, operator, precedence) + ), + ) + return + + return self.do_eval_infix(expr, operator, form, evaluation, precedence.value) + + def eval_infix_4( + self, + expr: Expression, + operator: BaseElement, + precedence: BaseElement, + grouping: BaseElement, + form: Symbol, + evaluation, + ): + """MakeBoxes[Infix[expr_, operator_, precedence_, grouping_], + form:StandardForm|TraditionalForm|OutputForm|InputForm]""" + if not isinstance(precedence, Integer): + fullform_expr = Expression( + SymbolFullForm, + Expression(SymbolInfix, expr, operator, precedence, grouping), + ) + evaluation.message( + "Infix", + "intm", + Expression( + SymbolFullForm, + Expression(SymbolInfix, expr, operator, precedence, grouping), + ), + ) + return fullform_expr + + if grouping is SymbolNone: + return self.do_eval_infix( + expr, operator, form, evaluation, precedence.value + ) + if grouping in (SymbolNonAssociative, SymbolLeft, SymbolRight): + return self.do_eval_infix( + expr, operator, form, evaluation, precedence.value, grouping.get_name() + ) + + evaluation.message("Infix", "argb", grouping) + return Expression( + SymbolFullForm, + Expression(SymbolInfix, expr, operator, precedence, grouping), + ) + + def eval_infix_default(self, parms, form: Symbol, evaluation): + """MakeBoxes[Infix[parms___], + form:StandardForm|TraditionalForm|OutputForm|InputForm]""" + evaluation.message("Infix", "argb", Integer(len(parms.get_sequence()))) + return None + + def do_eval_infix( + self, + expr: Expression, + operator: BaseElement, + form: Symbol, + evaluation, + precedence_value: Optional[int] = None, + grouping: Optional[str] = None, + ): + """Implements MakeBoxes[Infix[...]]""" + + ## FIXME: this should go into a some formatter. + def format_operator(operator) -> Union[String, BaseElement]: + """ + Format infix operator `operator`. To do this outside parameter form is used. + Sometimes no changes are made and operator is returned unchanged. + + This function probably should be rewritten be more scalable across other forms + and moved to a module that contiaing similar formatting routines. + """ + if not isinstance(operator, String): + return MakeBoxes(operator, form) + + op_str = operator.value + + # FIXME: performing a check using the operator symbol representation feels a bit + # fragile. The operator name seems more straightforward and more robust. + if form == SymbolInputForm and op_str in ["*", "^", " "]: + return operator + elif ( + form in (SymbolInputForm, SymbolOutputForm) + and not op_str.startswith(" ") + and not op_str.endswith(" ") + ): + # FIXME: Again, testing on specific forms is fragile and not scalable. + op = String(" " + op_str + " ") + return op + return operator + + if isinstance(expr, Atom): + evaluation.message("Infix", "normal", Integer1) + return None + + elements = expr.elements + if len(elements) > 1: + if operator.has_form("List", len(elements) - 1): + operator = [format_operator(op) for op in operator.elements] + return make_boxes_infix( + elements, operator, precedence_value, grouping, form + ) + else: + encoding_rule = evaluation.definitions.get_ownvalue( + "$CharacterEncoding" + ) + encoding = ( + "UTF8" if encoding_rule is None else encoding_rule.replace.value + ) + op_str = ( + operator.value + if isinstance(operator, String) + else operator.short_name + ) + if encoding == "ASCII": + operator = format_operator( + String(operator_to_ascii.get(op_str, op_str)) + ) + else: + operator = format_operator( + String(operator_to_unicode.get(op_str, op_str)) + ) + + return make_boxes_infix( + elements, operator, precedence_value, grouping, form + ) + + elif len(elements) == 1: + return MakeBoxes(elements[0], form) + else: + return MakeBoxes(expr, form) + class Left(Builtin): """ diff --git a/mathics/builtin/makeboxes.py b/mathics/builtin/makeboxes.py index 3817a68e4..942c13606 100644 --- a/mathics/builtin/makeboxes.py +++ b/mathics/builtin/makeboxes.py @@ -11,23 +11,15 @@ from mathics.builtin.base import Builtin, Predefined from mathics.builtin.box.layout import RowBox, to_boxes -from mathics.core.atoms import Integer, Integer1, Real, String +from mathics.core.atoms import Integer, Real, String from mathics.core.attributes import A_HOLD_ALL_COMPLETE, A_READ_PROTECTED -from mathics.core.convert.op import operator_to_ascii, operator_to_unicode -from mathics.core.element import BaseElement, BoxElementMixin +from mathics.core.element import BoxElementMixin 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 -from mathics.core.systemsymbols import ( - SymbolFullForm, - SymbolInfix, - SymbolInputForm, - SymbolNone, - SymbolOutputForm, - SymbolRowBox, -) -from mathics.eval.makeboxes import _boxed_string, format_element +from mathics.core.symbols import Atom +from mathics.core.systemsymbols import SymbolRowBox +from mathics.eval.makeboxes import _boxed_string, format_element, parenthesize def int_to_s_exp(expr, n): @@ -42,56 +34,6 @@ def int_to_s_exp(expr, n): return s, exp, nonnegative -def parenthesize(precedence, element, element_boxes, when_equal): - from mathics.builtin import builtins_precedence - - while element.has_form("HoldForm", 1): - element = element.elements[0] - - if element.has_form(("Infix", "Prefix", "Postfix"), 3, None): - element_prec = element.elements[2].value - elif element.has_form("PrecedenceForm", 2): - element_prec = element.elements[1].value - # For negative values, ensure that the element_precedence is at least the precedence. (Fixes #332) - elif isinstance(element, (Integer, Real)) and element.value < 0: - element_prec = precedence - else: - element_prec = builtins_precedence.get(element.get_head_name()) - if precedence is not None and element_prec is not None: - if precedence > element_prec or (precedence == element_prec and when_equal): - return Expression( - SymbolRowBox, - ListExpression(String("("), element_boxes, String(")")), - ) - return element_boxes - - -# FIXME: op should be a string, so remove the Union. -def make_boxes_infix( - elements, op: Union[String, list], precedence: int, grouping, form: Symbol -): - result = [] - for index, element in enumerate(elements): - if index > 0: - if isinstance(op, list): - result.append(op[index - 1]) - else: - result.append(op) - parenthesized = False - if grouping == "System`NonAssociative": - parenthesized = True - elif grouping == "System`Left" and index > 0: - parenthesized = True - elif grouping == "System`Right" and index == 0: - parenthesized = True - - element_boxes = MakeBoxes(element, form) - element = parenthesize(precedence, element, element_boxes, parenthesized) - - result.append(element) - return Expression(SymbolRowBox, ListExpression(*result)) - - def real_to_s_exp(expr, n): if expr.is_zero: s = "0" @@ -458,107 +400,6 @@ def eval_postprefix(self, p, expr, h, prec, f, evaluation): else: return MakeBoxes(expr, f).evaluate(evaluation) - # FIXME: prec is sometimes not an Integer for a ListExpession. - # And this is recent with respect to PredefinedSymbol revision. - # How does this get set and why? - def eval_infix( - self, expr, operator, precedence: Integer, grouping, form: Symbol, evaluation - ): - """MakeBoxes[Infix[expr_, operator_, precedence_:None, grouping_:None], - form:StandardForm|TraditionalForm|OutputForm|InputForm]""" - - # In WMA, this is covered with two different rules: - # * ```MakeBoxes[Infix[expr_, operator_]``` - # * ```MakeBoxes[Infix[expr_, operator_, precedence_, grouping_:None]``` - # In the second form, precedence must be an Integer. Otherwise, a message is - # shown and fall back to the standard MakeBoxes form. - # Here we allow Precedence to be ```System`None```, to have just one rule. - - if precedence is SymbolNone: - precedence_value = None - elif isinstance(precedence, Integer): - precedence_value = precedence.value - else: - if grouping is not SymbolNone: - # Here I use a String as a head to avoid a circular evaluation. - # TODO: Use SymbolInfix when the MakeBoxes refactor be done. - expr = Expression(String("Infix"), expr, operator, precedence, grouping) - else: - expr = Expression(String("Infix"), expr, operator, precedence) - evaluation.message("Infix", "intm", expr) - return self.eval_general(expr, form, evaluation) - - grouping = grouping.get_name() - - ## FIXME: this should go into a some formatter. - def format_operator(operator) -> Union[String, BaseElement]: - """ - Format infix operator `operator`. To do this outside parameter form is used. - Sometimes no changes are made and operator is returned unchanged. - - This function probably should be rewritten be more scalable across other forms - and moved to a module that contiaing similar formatting routines. - """ - if not isinstance(operator, String): - return MakeBoxes(operator, form) - - op_str = operator.value - - # FIXME: performing a check using the operator symbol representation feels a bit - # fragile. The operator name seems more straightforward and more robust. - if form == SymbolInputForm and op_str in ["*", "^", " "]: - return operator - elif ( - form in (SymbolInputForm, SymbolOutputForm) - and not op_str.startswith(" ") - and not op_str.endswith(" ") - ): - # FIXME: Again, testing on specific forms is fragile and not scalable. - op = String(" " + op_str + " ") - return op - return operator - - if isinstance(expr, Atom): - evaluation.message("Infix", "normal", Integer1) - return None - - elements = expr.elements - if len(elements) > 1: - if operator.has_form("List", len(elements) - 1): - operator = [format_operator(op) for op in operator.elements] - return make_boxes_infix( - elements, operator, precedence_value, grouping, form - ) - else: - encoding_rule = evaluation.definitions.get_ownvalue( - "$CharacterEncoding" - ) - encoding = ( - "UTF8" if encoding_rule is None else encoding_rule.replace.value - ) - op_str = ( - operator.value - if isinstance(operator, String) - else operator.short_name - ) - if encoding == "ASCII": - operator = format_operator( - String(operator_to_ascii.get(op_str, op_str)) - ) - else: - operator = format_operator( - String(operator_to_unicode.get(op_str, op_str)) - ) - - return make_boxes_infix( - elements, operator, precedence_value, grouping, form - ) - - elif len(elements) == 1: - return MakeBoxes(elements[0], form) - else: - return MakeBoxes(expr, form) - class ToBoxes(Builtin): """ diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py index f1bc2431d..597bac531 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -170,6 +170,7 @@ SymbolRepeated = Symbol("System`Repeated") SymbolRepeatedNull = Symbol("System`RepeatedNull") SymbolReturn = Symbol("System`Return") +SymbolRight = Symbol("System`Right") SymbolRound = Symbol("System`Round") SymbolRow = Symbol("System`Row") SymbolRowBox = Symbol("System`RowBox") diff --git a/mathics/eval/image.py b/mathics/eval/image.py index 3f78371bd..023034d3e 100644 --- a/mathics/eval/image.py +++ b/mathics/eval/image.py @@ -9,6 +9,7 @@ import numpy import PIL +import PIL.Image as PILImage from mathics.builtin.base import String from mathics.core.atoms import Rational @@ -36,11 +37,11 @@ } # After Python 3.6 support is dropped, this can be simplified -# to for Pillow 9+ and use PIL.Image.Resampling only. -if hasattr(PIL.Image, "Resampling"): - pil_resize = PIL.Image.Resampling +# to for Pillow 9+ and use PILImage.Resampling only. +if hasattr(PILImage, "Resampling"): + pil_resize = PILImage.Resampling else: - pil_resize = PIL.Image + pil_resize = PILImage # See: # https://pillow.readthedocs.io/en/stable/handbook/concepts.html#filters diff --git a/mathics/eval/makeboxes.py b/mathics/eval/makeboxes.py index 7c51be55f..784d58e5c 100644 --- a/mathics/eval/makeboxes.py +++ b/mathics/eval/makeboxes.py @@ -7,9 +7,9 @@ import typing -from typing import Any +from typing import Any, Union -from mathics.core.atoms import Complex, Integer, Rational, String, SymbolI +from mathics.core.atoms import Complex, Integer, Rational, Real, String, SymbolI from mathics.core.convert.expression import to_expression_with_specialization from mathics.core.definitions import OutputForms from mathics.core.element import BaseElement, BoxElementMixin, EvalMixin @@ -38,6 +38,7 @@ SymbolMinus, SymbolOutputForm, SymbolRational, + SymbolRowBox, SymbolStandardForm, ) @@ -51,6 +52,56 @@ def _boxed_string(string: str, **options): return StyleBox(String(string), **options) +def parenthesize(precedence, element, element_boxes, when_equal): + from mathics.builtin import builtins_precedence + + while element.has_form("HoldForm", 1): + element = element.elements[0] + + if element.has_form(("Infix", "Prefix", "Postfix"), 3, None): + element_prec = element.elements[2].value + elif element.has_form("PrecedenceForm", 2): + element_prec = element.elements[1].value + # For negative values, ensure that the element_precedence is at least the precedence. (Fixes #332) + elif isinstance(element, (Integer, Real)) and element.value < 0: + element_prec = precedence + else: + element_prec = builtins_precedence.get(element.get_head_name()) + if precedence is not None and element_prec is not None: + if precedence > element_prec or (precedence == element_prec and when_equal): + return Expression( + SymbolRowBox, + ListExpression(String("("), element_boxes, String(")")), + ) + return element_boxes + + +# FIXME: op should be a string, so remove the Union. +def make_boxes_infix( + elements, op: Union[String, list], precedence: int, grouping, form: Symbol +): + result = [] + for index, element in enumerate(elements): + if index > 0: + if isinstance(op, list): + result.append(op[index - 1]) + else: + result.append(op) + parenthesized = False + if grouping == "System`NonAssociative": + parenthesized = True + elif grouping == "System`Left" and index > 0: + parenthesized = True + elif grouping == "System`Right" and index == 0: + parenthesized = True + + element_boxes = Expression(SymbolMakeBoxes, element, form) + element = parenthesize(precedence, element, element_boxes, parenthesized) + + result.append(element) + return Expression(SymbolRowBox, ListExpression(*result)) + + def eval_makeboxes(self, expr, evaluation, f=SymbolStandardForm): """ This function takes the definitions prodived by the evaluation