diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..bcaad605f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,87 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + + +## Description + + + +## How to Reproduce + + + +## Output Given + + + + +## Expected behavior + + + +## Your Environment + + + +## Workarounds + + + +## Priority + + + +## Additional context + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..c51ecfbc0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,21 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- +*Note: If the feature is about adding or filling out an existing deficiency in the Mathics3 language, please file this as an [issue](https://github.com/Mathics3/mathics-core/issues/new?assignees=&labels=&projects=&template=bug_report.md&title=).* + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/SYMBOLS_MANIFEST.txt b/SYMBOLS_MANIFEST.txt index 6a4797982..861113891 100644 --- a/SYMBOLS_MANIFEST.txt +++ b/SYMBOLS_MANIFEST.txt @@ -845,6 +845,7 @@ System`NumberQ System`NumberString System`Numerator System`NumericFunction +System`NumericArray System`NumericQ System`O System`Octahedron diff --git a/mathics/builtin/binary/bytearray.py b/mathics/builtin/binary/bytearray.py index ae3a4e947..d0c3b5975 100644 --- a/mathics/builtin/binary/bytearray.py +++ b/mathics/builtin/binary/bytearray.py @@ -1,18 +1,16 @@ # -*- coding: utf-8 -*- """ -ByteArrays +Byte Arrays """ -from typing import Optional - -from mathics.core.atoms import ByteArray, Integer, String +from mathics.core.atoms import ByteArrayAtom, Integer, String from mathics.core.builtin import Builtin from mathics.core.convert.expression import to_mathics_list -from mathics.core.evaluation import Evaluation -from mathics.core.list import ListExpression +from mathics.core.expression import Expression +from mathics.core.systemsymbols import SymbolByteArray, SymbolFailed -class ByteArray_(Builtin): +class ByteArray(Builtin): r""" :WMA link: https://reference.wolfram.com/language/ref/ByteArray.html @@ -37,55 +35,44 @@ class ByteArray_(Builtin): >> ByteArray["ARkD"] = ByteArray[<3>] >> B=ByteArray["asy"] - : The argument at position 1 in ByteArray[asy] should be a vector of unsigned byte values or a Base64-encoded string. - = ByteArray[asy] - - A 'ByteArray" is a kind of Atom: - - >> AtomQ[ByteArray[{4, 2}]] - = True + : The first argument in Bytearray[asy] should be a B64 encoded string or a vector of integers. + = $Failed """ messages = { - "batd": "Elements in `1` are not unsigned byte values.", - "lend": ( - "The argument at position 1 in ByteArray[`1`] should " - "be a vector of unsigned byte values or a Base64-encoded string." - ), + "aotd": "Elements in `1` are inconsistent with type Byte", + "lend": "The first argument in Bytearray[`1`] should " + + "be a B64 encoded string or a vector of integers.", } - - name = "ByteArray" summary_text = "array of bytes" - def eval_str(self, string, evaluation: Evaluation) -> Optional[ByteArray]: + def eval_str(self, string, evaluation): "ByteArray[string_String]" try: - atom = ByteArray(string.value) - except TypeError: + atom = ByteArrayAtom(string.value) + except Exception: evaluation.message("ByteArray", "lend", string) - return None - return atom + return SymbolFailed + return Expression(SymbolByteArray, atom) - def eval_to_str(self, baa, evaluation: Evaluation): - "ToString[baa_ByteArray]" + def eval_to_str(self, baa, evaluation): + "ToString[ByteArray[baa_ByteArrayAtom]]" return String(f"ByteArray[<{len(baa.value)}>]") - def eval_normal(self, baa, evaluation: Evaluation): - "System`Normal[baa_ByteArray]" + def eval_normal(self, baa, evaluation): + "System`Normal[ByteArray[baa_ByteArrayAtom]]" return to_mathics_list(*baa.value, elements_conversion_fn=Integer) - def eval_list(self, values, evaluation) -> Optional[ByteArray]: - "ByteArray[values_]" - if not isinstance(values, ListExpression): - evaluation.message("ByteArray", "lend", values) - return None - + def eval_list(self, values, evaluation): + "ByteArray[values_List]" + if not values.has_form("List", None): + return try: - ba = ByteArray(bytearray([b.value for b in values.elements])) + ba = bytearray([b.get_int_value() for b in values.elements]) except Exception: - evaluation.message("ByteArray", "batd", values) - return None - return ba + evaluation.message("ByteArray", "aotd", values) + return + return Expression(SymbolByteArray, ByteArrayAtom(ba)) # TODO: BaseEncode, BaseDecode, ByteArrayQ, ByteArrayToString, StringToByteArray, ImportByteArray, ExportByteArray diff --git a/mathics/builtin/compilation.py b/mathics/builtin/compilation.py index 09b56fa3e..d8af4368f 100644 --- a/mathics/builtin/compilation.py +++ b/mathics/builtin/compilation.py @@ -25,7 +25,7 @@ from mathics.core.element import ImmutableValueMixin from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression -from mathics.core.keycomparable import LITERAL_EXPRESSION_ELT_ORDER +from mathics.core.keycomparable import LITERAL_EXPRESSION_SORT_KEY from mathics.core.symbols import Atom, Symbol, SymbolFalse, SymbolTrue from mathics.core.systemsymbols import SymbolCompiledFunction @@ -146,7 +146,7 @@ def element_order(self) -> tuple: lexicographically. """ - return (LITERAL_EXPRESSION_ELT_ORDER, hex(id(self))) + return (LITERAL_EXPRESSION_SORT_KEY, hex(id(self))) @property def pattern_precedence(self) -> tuple: diff --git a/mathics/builtin/exp_structure/size_and_sig.py b/mathics/builtin/exp_structure/size_and_sig.py index b06cede7f..0d6babb9f 100644 --- a/mathics/builtin/exp_structure/size_and_sig.py +++ b/mathics/builtin/exp_structure/size_and_sig.py @@ -5,7 +5,7 @@ import platform import zlib -from mathics.core.atoms import ByteArray, Integer, String +from mathics.core.atoms import ByteArrayAtom, Integer, String from mathics.core.attributes import A_PROTECTED, A_READ_PROTECTED from mathics.core.builtin import Builtin from mathics.core.evaluation import Evaluation @@ -132,7 +132,7 @@ def compute(user_hash, py_hashtype, py_format): if py_format == "DecimalString": return String(str(res)) elif py_format == "ByteArray": - return Expression(SymbolByteArray, ByteArray(res)) + return Expression(SymbolByteArray, ByteArrayAtom(res)) return Integer(res) def eval(self, expr, hashtype: String, outformat: String, evaluation: Evaluation): diff --git a/mathics/builtin/files_io/importexport.py b/mathics/builtin/files_io/importexport.py index 6db2700e7..9831814b7 100644 --- a/mathics/builtin/files_io/importexport.py +++ b/mathics/builtin/files_io/importexport.py @@ -26,7 +26,7 @@ from urllib.error import HTTPError, URLError from mathics.builtin.pymimesniffer import magic -from mathics.core.atoms import ByteArray +from mathics.core.atoms import ByteArrayAtom from mathics.core.attributes import A_NO_ATTRIBUTES, A_PROTECTED, A_READ_PROTECTED from mathics.core.builtin import Builtin, Integer, Predefined, String, get_option from mathics.core.convert.expression import to_mathics_list @@ -2005,7 +2005,7 @@ def eval_elements(self, expr, elems, evaluation: Evaluation, **options): evaluation.predetermined_out = current_predetermined_out return SymbolFailed if is_binary: - res = Expression(SymbolByteArray, ByteArray(res)) + res = Expression(SymbolByteArray, ByteArrayAtom(res)) else: res = String(str(res)) elif function_channels == ListExpression(String("Streams")): @@ -2030,7 +2030,9 @@ def eval_elements(self, expr, elems, evaluation: Evaluation, **options): res = exporter_function.evaluate(evaluation) if res is SymbolNull: if is_binary: - res = Expression(SymbolByteArray, ByteArray(pystream.getvalue())) + res = Expression( + SymbolByteArray, ByteArrayAtom(pystream.getvalue()) + ) else: res = String(str(pystream.getvalue())) else: diff --git a/mathics/builtin/image/base.py b/mathics/builtin/image/base.py index 2d8ce7756..cca3c8a95 100644 --- a/mathics/builtin/image/base.py +++ b/mathics/builtin/image/base.py @@ -12,7 +12,7 @@ from mathics.core.builtin import AtomBuiltin, String from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression -from mathics.core.keycomparable import IMAGE_EXPRESSION_ELT_ORDER +from mathics.core.keycomparable import IMAGE_EXPRESSION_SORT_KEY from mathics.core.list import ListExpression from mathics.core.systemsymbols import SymbolImage, SymbolRule from mathics.eval.image import image_pixels, pixels_as_float, pixels_as_ubyte @@ -122,7 +122,7 @@ def element_order(self) -> tuple: # and adding two extra fields: the length in the 5th position, # and a hash in the 6th place. return ( - IMAGE_EXPRESSION_ELT_ORDER, + IMAGE_EXPRESSION_SORT_KEY, SymbolImage, len(self.pixels), tuple(), diff --git a/mathics/builtin/layout.py b/mathics/builtin/layout.py index c5b4eb6cc..f35c4c979 100644 --- a/mathics/builtin/layout.py +++ b/mathics/builtin/layout.py @@ -60,7 +60,7 @@ class Format(Builtin): Raw objects cannot be formatted: >> Format[3] = "three"; - : Tag Integer in 3 is Protected. + : Cannot assign to raw object 3. Format types must be symbols: >> Format[r, a + b] = "r"; diff --git a/mathics/builtin/list/constructing.py b/mathics/builtin/list/constructing.py index 73aa8fb4d..bef62e321 100644 --- a/mathics/builtin/list/constructing.py +++ b/mathics/builtin/list/constructing.py @@ -13,10 +13,11 @@ from typing import Optional, Tuple from mathics.builtin.box.layout import RowBox -from mathics.core.atoms import ByteArray, Integer, Integer1, is_integer_rational_or_real +from mathics.core.atoms import Integer, Integer1, is_integer_rational_or_real, NumericArray from mathics.core.attributes import A_HOLD_FIRST, A_LISTABLE, A_LOCKED, A_PROTECTED from mathics.core.builtin import BasePattern, Builtin, IterationFunction from mathics.core.convert.expression import to_expression +from mathics.core.convert.python import from_python from mathics.core.convert.sympy import from_sympy from mathics.core.element import ElementsProperties from mathics.core.evaluation import Evaluation @@ -197,9 +198,9 @@ class Normal(Builtin): def eval_general(self, expr: Expression, evaluation: Evaluation): "Normal[expr_]" + if isinstance(expr, NumericArray): + return from_python(expr.to_python()) if isinstance(expr, Atom): - if isinstance(expr, ByteArray): - return ListExpression(*expr.items) return expr if expr.has_form("RootSum", 2): return from_sympy(expr.to_sympy().doit(roots=True)) diff --git a/mathics/builtin/list/eol.py b/mathics/builtin/list/eol.py index 89c061144..ce355d0ab 100644 --- a/mathics/builtin/list/eol.py +++ b/mathics/builtin/list/eol.py @@ -10,16 +10,7 @@ from itertools import chain from mathics.builtin.box.layout import RowBox -from mathics.core.atoms import ( - ByteArray, - Integer, - Integer0, - Integer1, - Integer2, - Integer3, - Integer4, - String, -) +from mathics.core.atoms import Integer, Integer0, Integer1, Integer3, Integer4, String from mathics.core.attributes import ( A_HOLD_FIRST, A_HOLD_REST, @@ -49,10 +40,8 @@ SymbolByteArray, SymbolDrop, SymbolFailed, - SymbolFirst, SymbolInfinity, SymbolKey, - SymbolLast, SymbolMakeBoxes, SymbolMissing, SymbolSelect, @@ -686,6 +675,7 @@ class First(Builtin): attributes = A_HOLD_REST | A_PROTECTED messages = { + "argt": "First called with `1` arguments; 1 or 2 arguments are expected.", "nofirst": "`1` has zero length and no first element.", } summary_text = "first element of a list or expression" @@ -695,23 +685,14 @@ def eval(self, expr, evaluation: Evaluation, expression: Expression): "expression: First[expr__]" if isinstance(expr, Atom): - if not hasattr(expr, "items"): - evaluation.message("First", "normal", Integer1, expression) - return - expr_len = len(expr.items) - else: - expr_len = len(expr.elements) + evaluation.message("First", "normal", Integer1, expression) + return + expr_len = len(expr.elements) if expr_len == 0: evaluation.message("First", "nofirst", expr) return - - if isinstance(expr, ByteArray): - return expr.items[0] - if expr_len > 2 and expr.head is SymbolSequence: - evaluation.message( - "First", "argt", SymbolFirst, Integer(expr_len), Integer1, Integer2 - ) + evaluation.message("First", "argt", expr_len) return first_elem = expr.elements[0] @@ -968,6 +949,7 @@ class Last(Builtin): attributes = A_HOLD_REST | A_PROTECTED messages = { + "argt": "Last called with `1` arguments; 1 or 2 arguments are expected.", "nolast": "`1` has zero length and no last element.", } summary_text = "last element of a list or expression" @@ -977,24 +959,14 @@ def eval(self, expression: Expression, expr, evaluation: Evaluation): "expression: Last[expr__]" if isinstance(expr, Atom): - if not hasattr(expr, "items"): - evaluation.message("First", "normal", Integer1, expression) - return - expr_len = len(expr.items) - else: - expr_len = len(expr.elements) + evaluation.message("Last", "normal", Integer1, expression) + return + expr_len = len(expr.elements) if expr_len == 0: evaluation.message("Last", "nolast", expr) return - - if isinstance(expr, ByteArray): - # ByteArray or NumericArray, ... - return expr.items[-1] - if expr_len > 2 and expr.head is SymbolSequence: - evaluation.message( - "Last", "argt", SymbolLast, Integer(expr_len), Integer1, Integer2 - ) + evaluation.message("Last", "argt", expr_len) return return expr.elements[-1] @@ -1188,6 +1160,7 @@ def eval(self, list, i, evaluation): indices = i.get_sequence() # How to deal with ByteArrays if list.get_head() is SymbolByteArray: + list = list.evaluate(evaluation) if len(indices) > 1: print( "Part::partd1: Depth of object ByteArray[<3>] " @@ -1199,18 +1172,19 @@ def eval(self, list, i, evaluation): idx = idx.value if idx == 0: return SymbolByteArray - n = len(list.value) + data = list.elements[0].value + lendata = len(data) if idx < 0: - idx = n - idx + idx = data - idx if idx < 0: evaluation.message("Part", "partw", i, list) return else: idx = idx - 1 - if idx > n: + if idx > lendata: evaluation.message("Part", "partw", i, list) return - return Integer(list[idx]) + return Integer(data[idx]) if idx is Symbol("System`All"): return list # TODO: handling ranges and lists... diff --git a/mathics/builtin/numericarray.py b/mathics/builtin/numericarray.py new file mode 100644 index 000000000..a2ff8bab5 --- /dev/null +++ b/mathics/builtin/numericarray.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +"""Rules for working with NumericArray atoms.""" + +from typing import Optional, Tuple + +try: # pragma: no cover - numpy is optional at runtime + import numpy +except ImportError: # pragma: no cover - handled via requires attribute + numpy = None + +from mathics.core.atoms import NumericArray, NUMERIC_ARRAY_TYPE_MAP, String +from mathics.core.builtin import Builtin +from mathics.core.convert.python import from_python +from mathics.core.symbols import Symbol, strip_context +from mathics.core.systemsymbols import SymbolAutomatic, SymbolFailed, SymbolNumericArray + + +# class name modeled on Complex_ to avoid collision with NumericArray atom +class NumericArray_(Builtin): + + summary_text = "construct NumericArray" + name = "NumericArray" + rules = { + "NumericArray[list_List]": "NumericArray[list, Automatic]" + } + messages = { + "type": "The type specification `1` is not supported in NumericArray.", + } + + # rule to convert NumericArray[...nested list...] expression to NumericArray atom + def eval_list(self, data, typespec, evaluation): + "System`NumericArray[data_List, typespec_]" + + # get a string key from the typespec + if isinstance(typespec, Symbol): + key = strip_context(typespec.get_name()) + elif isinstance(typespec, String): + key = typespec.value + else: + evaluation.message("NumericArray", "type", typespec) + return SymbolFailed + + # compute numpy dtype from key + if key == "Automatic": + dtype = None + else: + dtype = NUMERIC_ARRAY_TYPE_MAP.get(key, None) + if not dtype: + evaluation.message("NumericArray", "type", typespec) + return SymbolFailed + + # compute array from data and dtype and wrap it in a NumericArray atom + python_value = data.to_python() + array = numpy.array(python_value, dtype=dtype) + atom = NumericArray(array, dtype) + + return atom + + # this doesn't work + # instead see Normal builtin in mathics/builtin/list/constructing.py + #def eval_normal(self, array, evaluation): + # "System`Normal[array_NumericArray]" + # return from_python(array.value.tolist()) diff --git a/mathics/builtin/statistics/orderstats.py b/mathics/builtin/statistics/orderstats.py index ff75c6a0c..12b0314f7 100644 --- a/mathics/builtin/statistics/orderstats.py +++ b/mathics/builtin/statistics/orderstats.py @@ -300,32 +300,6 @@ class ReverseSort(Builtin): } -# FIXME: there might be a bug in sorting... -# -# Sort[{ -# "a","b", 1, -# ByteArray[{1,2,4,1}], -# 2, 1.2, I, 2I-3, A, -# a+b, a*b, a+1, a*2, b^3, 2/3, -# A[x], F[2], F[x], F[x_], F[x___], F[x,t], F[x__], -# Condition[A,b>2], Pattern[expr, A] -# }] -# -# should be: -# -# {-3 + 2*I, I, 2/3, 1, 1.2, 2, -# "a", "b", 2*a, -# 1 + a, A, a*b, b^3, a + b, -# A[x], A /; b > 2, -# F[2], F[x], F[x_], F[x___], F[x__], F[x, t], -# ByteArray["AQIEAQ=="], expr:A} -# -# But this is too complicated a case to run as a test. It needs -# to be isolated. Break this down to smaller pieces, -# and also use Order[] to check smaller components. -# The problem might also be in boxing-order output. - - class Sort(Builtin): """ :WMA link:https://reference.wolfram.com/language/ref/Sort.html @@ -342,7 +316,7 @@ class Sort(Builtin): >> Sort[{4, 1.0, a, 3+I}] = {1., 3 + I, 4, a} - Sort uses 'Order' to determine ordering by default. + Sort uses 'OrderedQ' to determine ordering by default. You can sort patterns according to their precedence using 'PatternsOrderedQ': >> Sort[{items___, item_, OptionsPattern[], item_symbol, item_?test}, PatternsOrderedQ] = {item_symbol, item_ ? test, item_, items___, OptionsPattern[]} diff --git a/mathics/core/assignment.py b/mathics/core/assignment.py index adbf8123b..ae12aa99d 100644 --- a/mathics/core/assignment.py +++ b/mathics/core/assignment.py @@ -102,10 +102,10 @@ def get_symbol_values( A list of rules. None if `symbol` is not a Symbol. """ - if not isinstance(symbol, Symbol): + name = symbol.get_name() + if not name: evaluation.message(func_name, "sym", symbol, 1) return None - name = symbol.get_name() definitions = evaluation.definitions try: definition = ( diff --git a/mathics/core/atoms.py b/mathics/core/atoms.py index 17e5062ce..69cab6be2 100644 --- a/mathics/core/atoms.py +++ b/mathics/core/atoms.py @@ -10,11 +10,15 @@ import sympy from sympy.core import numbers as sympy_numbers +try: # pragma: no cover - optional dependency handled at runtime + import numpy +except ImportError: # pragma: no cover - numpy is optional at import time + numpy = None + from mathics.core.element import BoxElementMixin, ImmutableValueMixin from mathics.core.keycomparable import ( - BASIC_ATOM_BYTEARRAY_ELT_ORDER, - BASIC_ATOM_NUMBER_ELT_ORDER, - BASIC_ATOM_STRING_ELT_ORDER, + BASIC_ATOM_NUMBER_SORT_KEY, + BASIC_ATOM_STRING_OR_BYTEARRAY_SORT_KEY, ) from mathics.core.number import ( FP_MANTISA_BINARY_DIGITS, @@ -91,7 +95,7 @@ def element_order(self) -> tuple: of an expression. The tuple is ultimately compared lexicographically. """ return ( - BASIC_ATOM_NUMBER_ELT_ORDER, + BASIC_ATOM_NUMBER_SORT_KEY, self.value, 0, 1, @@ -640,51 +644,30 @@ def to_sympy(self, *args, **kwargs): return self.value -class ByteArray(Atom, ImmutableValueMixin): - _value: Union[bytes, bytearray] - - # Items is analogous to "elements" in Lists. - # However the name is different because there is a concern - # having these be distinct names may catch mistakes in coding - # where an expanded or Normal[]'d value is used when it should - # not be used. - _items: Optional[tuple] = None - - class_head_name = "System`ByteArray" - hash: int +class ByteArrayAtom(Atom, ImmutableValueMixin): + value: Union[bytes, bytearray] + class_head_name = "System`ByteArrayAtom" - # We use __new__ here to ensure that two ByteArray's that have the same value + # We use __new__ here to ensure that two ByteArrayAtom's that have the same value # return the same object, and to set an object hash value. # Consider also @lru_cache, and mechanisms for limiting and # clearing the cache and the object store which might be useful in implementing # Builtin Share[]. def __new__(cls, value): self = super().__new__(cls) - if isinstance(value, (bytes, bytearray)): - self._value = value - elif isinstance(value, list): - self._value = bytearray(value) - elif isinstance(value, str): - try: - self._value = base64.b64decode(value) - except Exception as e: - raise TypeError(f"base64 string decode failed: {e}") + if type(value) in (bytes, bytearray): + self.value = value + elif type(value) is list: + self.value = bytearray(list) + elif type(value) is str: + self.value = base64.b64decode(value) else: - raise TypeError("value does not belongs to a valid type") + raise Exception("value does not belongs to a valid type") - self.hash = hash(("ByteArray", str(self.value))) + self.hash = hash(("ByteArrayAtom", str(self.value))) return self - def __getitem__(self, index: int) -> int: - """ - Support List index lookup without having to expand the entire bytearray into a Mathics3 list. - """ - return self.value[index] - - def __getnewargs__(self): - return (self.value,) - - def __hash__(self) -> int: + def __hash__(self): return self.hash def __str__(self) -> str: @@ -697,24 +680,16 @@ def __str__(self) -> str: # is removed and the form makes decisions, rather than # have this routine know everything about all forms. def atom_to_boxes(self, f, evaluation) -> "String": - return String(f"ByteArray[<{len(self.value)}>]") + res = String(f"<{len(self.value)}>") + return res - def do_copy(self) -> "ByteArray": - return ByteArray(self.value) + def do_copy(self) -> "ByteArrayAtom": + return ByteArrayAtom(self.value) def default_format(self, evaluation, form) -> str: value = self.value return '"' + value.__str__() + '"' - @property - def items(self) -> Tuple[Integer, ...]: - """ - Return a tuple value of Mathics3 Integers for each element of the ByteArray. - """ - if self._items is None: - self._items = tuple([Integer(i) for i in self.value]) - return self._items - @property def element_order(self) -> tuple: """ @@ -722,9 +697,8 @@ def element_order(self) -> tuple: of an expression. The tuple is ultimately compared lexicographically. """ return ( - BASIC_ATOM_BYTEARRAY_ELT_ORDER, + BASIC_ATOM_STRING_OR_BYTEARRAY_SORT_KEY, self.value, - "utf-8", 0, 1, ) @@ -739,16 +713,16 @@ def pattern_precedence(self) -> tuple: @property def is_literal(self) -> bool: - """For a ByteArray, the value can't change and has a Python representation, + """For an ByteArrayAtom, the value can't change and has a Python representation, i.e. a value is set and it does not depend on definition bindings. So we say it is a literal. """ return True def sameQ(self, rhs) -> bool: - """Mathics3 SameQ""" + """Mathics SameQ""" # FIX: check - if isinstance(rhs, ByteArray): + if isinstance(rhs, ByteArrayAtom): return self.value == rhs.value return False @@ -765,17 +739,12 @@ def to_python(self, *args, **kwargs) -> Union[bytes, bytearray]: return self.value def user_hash(self, update): - """ - returned untampered hash value. - - hashing a String is the one case where the user gets the untampered + # hashing a String is the one case where the user gets the untampered # hash value of the string's text. this corresponds to MMA behavior. - """ update(self.value) - @property - def value(self) -> Union[bytes, bytearray]: - return self._value + def __getnewargs__(self): + return (self.value,) class Complex(Number[Tuple[Number[T], Number[T], Optional[int]]]): @@ -887,7 +856,7 @@ def element_order(self) -> tuple: of an expression. The tuple is ultimately compared lexicographically. """ return ( - BASIC_ATOM_NUMBER_ELT_ORDER, + BASIC_ATOM_NUMBER_SORT_KEY, self.real.element_order[1], self.imag.element_order[1], 1, @@ -1049,7 +1018,7 @@ def element_order(self) -> tuple: """ # HACK: otherwise "Bus error" when comparing 1==1. return ( - BASIC_ATOM_NUMBER_ELT_ORDER, + BASIC_ATOM_NUMBER_SORT_KEY, sympy.Float(self.value), 0, 1, @@ -1098,10 +1067,117 @@ def is_zero(self) -> bool: } +# +# NumericArray +# + +if numpy is not None: + NUMERIC_ARRAY_TYPE_MAP = { + "UnsignedInteger8": numpy.dtype("uint8"), + "UnsignedInteger16": numpy.dtype("uint16"), + "UnsignedInteger32": numpy.dtype("uint32"), + "UnsignedInteger64": numpy.dtype("uint64"), + "Integer8": numpy.dtype("int8"), + "Integer16": numpy.dtype("int16"), + "Integer32": numpy.dtype("int32"), + "Integer64": numpy.dtype("int64"), + "Real32": numpy.dtype("float32"), + "Real64": numpy.dtype("float64"), + "ComplexReal32": numpy.dtype("complex64"), + "ComplexReal64": numpy.dtype("complex128"), + } + NUMERIC_ARRAY_DTYPE_TO_NAME = { + dtype: name for name, dtype in NUMERIC_ARRAY_TYPE_MAP.items() + } +else: # pragma: no cover - executed only when numpy is absent + NUMERIC_ARRAY_TYPE_MAP = {} + NUMERIC_ARRAY_DTYPE_TO_NAME = {} + + +# TODO: would it be useful to follow the example of Complex and parameterize by type? +# would that be array.dtype or the MMA type from the map above? +class NumericArray(Atom, ImmutableValueMixin): + """ + NumericArray provides compact storage and efficient access for machine-precision numeric arrays, + backed by NumPy arrays. + """ + + class_head_name = "NumericArray" + + def __init__(self, value, dtype=None): + if numpy is None: + raise ImportError("numpy is required for NumericArray") + + # compute value + if not isinstance(value, numpy.ndarray): + value = numpy.asarray(value, dtype=dtype) + elif dtype is not None: + value = value.astype(dtype) + self.value = value + + # check type + self._type_name = NUMERIC_ARRAY_DTYPE_TO_NAME.get(self.value.dtype, None) + if not self._type_name: + allowed = ", ".join(str(dtype) for dtype in NUMERIC_ARRAY_TYPE_MAP.values()) + message = f"Argument 'value' must be one of {allowed}; is {str(self.value.dtype)}." + raise ValueError(message) + + # summary and hash + self._summary = (self._type_name, self.value.shape, self.value.tobytes()) + shape_string = "×".join(str(dim) for dim in self.value.shape) or "0" + self._summary_string = f"{self._type_name}, {shape_string}" + self._hash = None + + # TODO: this is potentially expensive - what if we left it unimplemented? is hashing a numpy array reasonable? + # TODO: or maybe make self._summary included only some of the bytes, since it's just a hash? + def __hash__(self): + if not self._hash: + print("HASHING NUMERICARRAY") + self._hash = hash(("NumericArray", self._summary)) + return self._hash + + def __str__(self) -> str: + return f"NumericArray[{self._summary_string}]" + + def atom_to_boxes(self, f, evaluation): + return String(f"<{self._summary_string}>") + + def do_copy(self) -> "NumericArray": + return NumericArray(self.value.copy()) + + def default_format(self, evaluation, form) -> str: + return f"NumericArray[<{self._summary_string}>]" + + @property + def element_order(self) -> tuple: + return (BASIC_ATOM_STRING_OR_BYTEARRAY_SORT_KEY, *self._summary) + + @property + def pattern_precedence(self) -> tuple: + return super().pattern_precedence + + def sameQ(self, rhs) -> bool: + return isinstance(rhs, NumericArray) and numpy.array_equal(self.value, rhs.value) + + def to_sympy(self, **kwargs): + return None + + # TODO: note that this returns a simple python list (of lists), + # not the numpy array - ok? + def to_python(self, *args, **kwargs): + return self.value.tolist() + + # TODO: what is this? is it right? + def user_hash(self, update): + update(self._summary[2]) + + def __getnewargs__(self): + return (self.value, self.value.dtype) + + class String(Atom, BoxElementMixin): value: str class_head_name = "System`String" - hash: int def __new__(cls, value): self = super().__new__(cls) @@ -1111,7 +1187,7 @@ def __new__(cls, value): self.hash = hash(("String", self.value)) return self - def __hash__(self) -> int: + def __hash__(self): return self.hash def __str__(self) -> str: @@ -1140,7 +1216,7 @@ def element_order(self) -> tuple: of an expression. The tuple is ultimately compared lexicographically. """ return ( - BASIC_ATOM_STRING_ELT_ORDER, + BASIC_ATOM_STRING_OR_BYTEARRAY_SORT_KEY, self.value, 0, 1, diff --git a/mathics/core/builtin.py b/mathics/core/builtin.py index 3822b1013..e91b50c21 100644 --- a/mathics/core/builtin.py +++ b/mathics/core/builtin.py @@ -444,7 +444,7 @@ def contextify_form_name(f): # for Sqrt[a, b] (one argument expected) or Subtract[a] (two # arguments expected) It assumes each builtin defines # "expected_args" for the correct number of arguments to give. - # See class mathics.builtin.arithfns.basic.Sqrt for how to set up. + # See class mathics.builtins.basic.Sqrt for how to set up. def generic_argument_error(self, invalid, evaluation: Evaluation): "%(name)s[invalid___]" diff --git a/mathics/core/convert/python.py b/mathics/core/convert/python.py index 70340f3a0..c61941edd 100644 --- a/mathics/core/convert/python.py +++ b/mathics/core/convert/python.py @@ -5,7 +5,12 @@ from typing import Any -from mathics.core.atoms import Complex, Integer, Rational, Real, String +try: # pragma: no cover - numpy is optional + import numpy +except ImportError: # pragma: no cover - optional dependency missing + numpy = None + +from mathics.core.atoms import Complex, Integer, NumericArray, Rational, Real, String from mathics.core.number import get_type from mathics.core.symbols import ( BaseElement, @@ -113,5 +118,7 @@ def from_python(arg: Any) -> BaseElement: from mathics.builtin.binary.bytearray import ByteArray return Expression(SymbolByteArray, ByteArray(arg)) + elif numpy is not None and isinstance(arg, numpy.ndarray): + return NumericArray(arg) else: raise NotImplementedError diff --git a/mathics/core/element.py b/mathics/core/element.py index fb2ef7a2a..f087ad2e5 100644 --- a/mathics/core/element.py +++ b/mathics/core/element.py @@ -77,9 +77,6 @@ class ElementsProperties: # this True elements are not sorted can cause evaluation differences. is_ordered: bool = False - # Uniform expressions have all their elements with the same Head. - is_uniform: bool = False - class ImmutableValueMixin: @property diff --git a/mathics/core/expression.py b/mathics/core/expression.py index 41883e650..2a6801532 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -40,10 +40,10 @@ from mathics.core.evaluation import Evaluation from mathics.core.interrupt import ReturnInterrupt from mathics.core.keycomparable import ( - BASIC_EXPRESSION_ELT_ORDER, - BASIC_NUMERIC_EXPRESSION_ELT_ORDER, - GENERAL_EXPRESSION_ELT_ORDER, - GENERAL_NUMERIC_EXPRESSION_ELT_ORDER, + BASIC_EXPRESSION_SORT_KEY, + BASIC_NUMERIC_EXPRESSION_SORT_KEY, + GENERAL_EXPRESSION_SORT_KEY, + GENERAL_NUMERIC_EXPRESSION_SORT_KEY, Monomial, ) from mathics.core.structure import LinkedStructure @@ -355,25 +355,15 @@ def _build_elements_properties(self): """ # All of the properties start out optimistic (True) and are reset when that proves wrong. - self.elements_properties = ElementsProperties(True, True, True, True) + self.elements_properties = ElementsProperties(True, True, True) last_element = None values = [] - last_lookup_name = "" - uniform = True for element in self._elements: # Test for the literalness, and the three properties mentioned above if not element.is_literal: self.elements_properties.elements_fully_evaluated = False - if uniform: - lookup_name = element.get_lookup_name() - if last_lookup_name: - if lookup_name != last_lookup_name: - uniform = self.elements_properties.is_uniform = False - else: - last_lookup_name = lookup_name - if isinstance(element, Expression): # "self" can't be flat. self.elements_properties.is_flat = False @@ -897,7 +887,7 @@ def element_order(self) -> tuple: of an expression. The tuple is ultimately compared lexicographically. """ """ - General element order key structure: + General sort key structure: 0: 1/2: Numeric / General Expression 1: 2/3 Special arithmetic (Times / Power) / General Expression 2: Element: Head @@ -932,9 +922,9 @@ def element_order(self) -> tuple: if exps: return ( ( - BASIC_NUMERIC_EXPRESSION_ELT_ORDER + BASIC_NUMERIC_EXPRESSION_SORT_KEY if self.is_numeric() - else BASIC_EXPRESSION_ELT_ORDER + else BASIC_EXPRESSION_SORT_KEY ), Monomial(exps), 1, @@ -945,9 +935,9 @@ def element_order(self) -> tuple: else: return ( ( - GENERAL_NUMERIC_EXPRESSION_ELT_ORDER + GENERAL_NUMERIC_EXPRESSION_SORT_KEY if self.is_numeric() - else GENERAL_EXPRESSION_ELT_ORDER + else GENERAL_EXPRESSION_SORT_KEY ), head, len(self._elements), @@ -1306,7 +1296,6 @@ def flatten_callback(new_elements, old): else: return threaded, True - elements_properties = new.elements_properties # Step 6: # Look at the rules associated with: # 1. the upvalues of each element @@ -1346,17 +1335,15 @@ def flatten_callback(new_elements, old): def rules(): rules_names = set() if not A_HOLD_ALL_COMPLETE & attributes: - sample_elements = ( - (elements[0],) - if elements and elements_properties.is_uniform - else elements - ) - for element in sample_elements: + for element in elements: + if not isinstance(element, EvalMixin): + continue name = element.get_lookup_name() - if name and name not in rules_names: - rules_names.add(name) - for rule in evaluation.definitions.get_upvalues(name): - yield rule + if len(name) > 0: # only lookup rules if this is a symbol + if name not in rules_names: + rules_names.add(name) + for rule in evaluation.definitions.get_upvalues(name): + yield rule lookup_name = new.get_lookup_name() if lookup_name == new.get_head_name(): for rule in evaluation.definitions.get_downvalues(lookup_name): diff --git a/mathics/core/keycomparable.py b/mathics/core/keycomparable.py index c8bd5a771..f07a4fce1 100644 --- a/mathics/core/keycomparable.py +++ b/mathics/core/keycomparable.py @@ -5,39 +5,26 @@ class KeyComparable: - """Mathics3/WL defines a "canonical ordering" between elements, - even where they would not otherwise be comparable either - numerically, lexicographically, etc. - - For example, there is an ordering defined between a Function - call with some number of arguments, and a ByteArray Atom. - - Symbols that have an "Orderless" attribute use this canonic - ordering in arranging elements. + """ - Also, there are builtin-functions like "Order[]", "OrderQ[]", and - "Ordering[]", that use this canonic ordering in their computation. + Some Mathics3/WL Symbols have an "OrderLess" attribute + which is used in the evaluation process to arrange items in a list. - To support the WL-predefined canonic order elements types, we need - a way to compare arbitrary elements. That is what this class is for. + To do that, we need a way to compare Symbols, and that is what + this class is for. - This class adds the boilerplate Python comparison operators, like - __lt__, __eq__, etc. that Python provides for comparing Python - objects. + This class adds the boilerplate Python comparison operators that + you expect in Python programs for comparing Python objects. - This class is not complete in of itself; it is intended to be - mixed into other classes, and is used as a fallback when Python - object's comparison does not apply due to type mismatch. + This class is not complete in of itself, it is intended to be + mixed into other classes. Each class should provide a `element_order` property which is the primitive from which all other comparisons are based on. - The class also contains a `pattern_precedence` property that - provides the sort key used to order a list of rules according to - the precedence they have in the evaluation loop. Note that pattern - precedence and element ordering are separate concepts, although - they both have a similar feel. - + The class also contains a `pattern_precedence` property that provides + the sort key used to order a list of rules according to the + precedence they have in the evaluation loop. """ @property @@ -184,7 +171,7 @@ def __ne__(self, other) -> bool: # finished with ``END_OF_LIST_PATTERN_SORT_KEY`` to ensure that the longest # list of patterns always come first. -# Let's start by defining the basic magic numbers: +# Let' s start by defining the basic magic numbers: # EXPRESSION BIT PATTERN_SORT_KEY_IS_EXPRESSION = 0x00020000 @@ -272,17 +259,15 @@ def __ne__(self, other) -> bool: ) # Used as the last element in the third field. -### _ELT_ORDER suffixes are used in element ordering and -### Expression.element_order(). +### SORT_KEYS prefix for expression_order -BASIC_ATOM_NUMBER_ELT_ORDER = 0x00 -BASIC_ATOM_STRING_ELT_ORDER = 0x01 -BASIC_ATOM_BYTEARRAY_ELT_ORDER = 0x02 -LITERAL_EXPRESSION_ELT_ORDER = 0x03 +BASIC_ATOM_NUMBER_SORT_KEY = 0x00 +BASIC_ATOM_STRING_OR_BYTEARRAY_SORT_KEY = 0x01 +LITERAL_EXPRESSION_SORT_KEY = 0x03 -BASIC_NUMERIC_EXPRESSION_ELT_ORDER = 0x12 -GENERAL_NUMERIC_EXPRESSION_ELT_ORDER = 0x13 -IMAGE_EXPRESSION_ELT_ORDER = 0x13 +BASIC_NUMERIC_EXPRESSION_SORT_KEY = 0x12 +GENERAL_NUMERIC_EXPRESSION_SORT_KEY = 0x13 +IMAGE_EXPRESSION_SORT_KEY = 0x13 -BASIC_EXPRESSION_ELT_ORDER = 0x22 -GENERAL_EXPRESSION_ELT_ORDER = 0x23 +BASIC_EXPRESSION_SORT_KEY = 0x22 +GENERAL_EXPRESSION_SORT_KEY = 0x23 diff --git a/mathics/core/symbols.py b/mathics/core/symbols.py index 1394d06e8..83fa837ce 100644 --- a/mathics/core/symbols.py +++ b/mathics/core/symbols.py @@ -16,8 +16,8 @@ from mathics.core.keycomparable import ( BASIC_ATOM_PATTERN_SORT_KEY, - BASIC_EXPRESSION_ELT_ORDER, - BASIC_NUMERIC_EXPRESSION_ELT_ORDER, + BASIC_EXPRESSION_SORT_KEY, + BASIC_NUMERIC_EXPRESSION_SORT_KEY, Monomial, ) from mathics.eval.tracing import trace_evaluate @@ -216,13 +216,6 @@ def get_head_name(self) -> "str": # 1/0 # return None if stop_on_error else {} - def get_lookup_name(self) -> str: - """ - By default, atoms that are not symbols - have their class head_names as their lookup names. - """ - return self.class_head_name - @property def element_order(self) -> tuple: """ @@ -468,12 +461,6 @@ def get_head(self) -> "Symbol": def get_head_name(self) -> str: return "System`Symbol" - def get_lookup_name(self) -> str: - """ - The lookup name of a Symbol is its name. - """ - return self.get_name() - def get_option_values(self, evaluation, allow_symbols=False, stop_on_error=True): """ Build a dictionary of options from an expression. @@ -558,9 +545,9 @@ def element_order(self) -> tuple: """ return ( ( - BASIC_NUMERIC_EXPRESSION_ELT_ORDER + BASIC_NUMERIC_EXPRESSION_SORT_KEY if self.is_numeric() - else BASIC_EXPRESSION_ELT_ORDER + else BASIC_EXPRESSION_SORT_KEY ), Monomial({self.name: 1}), 0, diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py index a6b014081..c46fc227e 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -100,7 +100,6 @@ SymbolFactorial = Symbol("System`Factorial") SymbolFailed = Symbol("System`$Failed") SymbolFindClusters = Symbol("System`FindClusters") -SymbolFirst = Symbol("System`First") SymbolFloor = Symbol("System`Floor") SymbolFormat = Symbol("System`Format") SymbolFractionBox = Symbol("System`FractionBox") @@ -135,7 +134,6 @@ SymbolInputStream = Symbol("System`InputStream") SymbolInteger = Symbol("System`Integer") SymbolIntegrate = Symbol("System`Integrate") -SymbolLast = Symbol("System`Last") SymbolLeft = Symbol("System`Left") SymbolLength = Symbol("System`Length") SymbolLess = Symbol("System`Less") @@ -179,6 +177,7 @@ SymbolNumberForm = Symbol("System`NumberForm") SymbolNumberString = Symbol("System`NumberString") SymbolNumberQ = Symbol("System`NumberQ") +SymbolNumericArray = Symbol("System`NumericArray") SymbolNumericQ = Symbol("System`NumericQ") SymbolO = Symbol("System`O") SymbolOpacity = Symbol("System`Opacity") diff --git a/mathics/eval/assignments/assignment.py b/mathics/eval/assignments/assignment.py index 752928f15..1c00b1d23 100644 --- a/mathics/eval/assignments/assignment.py +++ b/mathics/eval/assignments/assignment.py @@ -508,7 +508,7 @@ def eval_assign_format( lhs_reference = get_reference_expression(lhs) lhs_reference = ( lhs_reference.get_head() - if not isinstance(lhs_reference, Symbol) + if isinstance(lhs_reference, Expression) else lhs_reference ) tags = process_tags_and_upset_dont_allow_custom( diff --git a/mathics/eval/list/eol.py b/mathics/eval/list/eol.py index a20326293..8387bdb47 100644 --- a/mathics/eval/list/eol.py +++ b/mathics/eval/list/eol.py @@ -128,7 +128,7 @@ def list_parts(exprs, selectors, evaluation): picked = list(list_parts(selected, selectors[1:], evaluation)) - if unwrap is None and hasattr(expr, "restructure"): + if unwrap is None: expr = expr.restructure(expr.head, picked, evaluation) yield expr else: @@ -197,7 +197,7 @@ def parts_sequence_selector(pspec): raise MessageException("Part", "pspec", pspec) def select(inner): - if not hasattr(inner, "elements"): + if isinstance(inner, Atom): raise MessageException("Part", "partd") elements = inner.elements diff --git a/pyproject.toml b/pyproject.toml index 81d96749d..254eacde4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ # ExampleData image hedy.tif is in this format. # Pillow 9.2 handles sunflowers.jpg "pillow >= 9.2", - "pint >=0.24", # Earlier pint has problems with numpy 2.2.6 + "pint", "python-dateutil", # Pympler is used in ByteCount[] and MemoryInUse[]. "Pympler", diff --git a/test/builtin/list/test_eol.py b/test/builtin/list/test_eol.py index 7941903f9..0b7780ee4 100644 --- a/test/builtin/list/test_eol.py +++ b/test/builtin/list/test_eol.py @@ -87,6 +87,12 @@ "Drop[{1, 2, 3, 4, 5, 6}, {-5, -2, -2}]", None, ), + ( + "First[a, b, c]", + ("First called with 3 arguments; 1 or 2 arguments are expected.",), + "First[a, b, c]", + None, + ), ('FirstPosition[{1, 2, 3}, _?StringQ, "NoStrings"]', None, "NoStrings", None), ("FirstPosition[a, a]", None, "{}", None), ( @@ -161,6 +167,12 @@ ## Negative step ("{1,2,3,4,5}[[3;;1;;-1]]", None, "{3, 2, 1}", None), ("ClearAll[a]", None, "Null", None), + ( + "Last[a, b, c]", + ("Last called with 3 arguments; 1 or 2 arguments are expected.",), + "Last[a, b, c]", + None, + ), ("Range[11][[-3 ;; 2 ;; -2]]", None, "{9, 7, 5, 3}", None), ("Range[11][[-3 ;; -7 ;; -3]]", None, "{9, 6}", None), ("Range[11][[7 ;; -7;; -2]]", None, "{7, 5}", None), @@ -257,46 +269,9 @@ ), ], ) -def test_eol_edicates(str_expr, expected_messages, str_expected, assert_message): - check_evaluation( - str_expr, - str_expected, - failure_message=assert_message, - expected_messages=expected_messages, - hold_expected=True, - ) - - -@pytest.mark.parametrize( - ("str_expr", "expected_messages", "str_expected", "assert_message"), - [ - ( - "First[a, b, c]", - ("First called with 3 arguments; 1 or 2 arguments are expected.",), - "First[a, b, c]", - None, - ), - ( - "First[ByteArray[{5}]]", - None, - "5", - None, - ), - ( - "Last[c, d, e]", - ("Last called with 3 arguments; 1 or 2 arguments are expected.",), - "Last[c, d, e]", - None, - ), - ( - "Last[ByteArray[{6}]]", - None, - "6", - None, - ), - ], -) -def test_First_and_Last(str_expr, expected_messages, str_expected, assert_message): +def test_eol_edicates_private_doctests( + str_expr, expected_messages, str_expected, assert_message +): check_evaluation( str_expr, str_expected, diff --git a/test/builtin/statistics/__init__.py b/test/builtin/statistics/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/test/builtin/statistics/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/test/builtin/statistics/test_orderstats.py b/test/builtin/statistics/test_orderstats.py deleted file mode 100644 index 52cd46169..000000000 --- a/test/builtin/statistics/test_orderstats.py +++ /dev/null @@ -1,53 +0,0 @@ -from test.helper import check_evaluation - -import pytest - -from mathics.core.builtin import check_requires_list - - -def test_canonical_sort(): - check_evaluation( - "Sort[{F[2], ByteArray[{2}]}]", - "{ByteArray[<1>], F[2]}", - hold_expected=True, - ) - check_evaluation( - r"Sort[Table[IntegerDigits[2^n], {n, 10}]]", - r"{{2}, {4}, {8}, {1, 6}, {3, 2}, {6, 4}, {1, 2, 8}, {2, 5, 6}, {5, 1, 2}, {1, 0, 2, 4}}", - ) - check_evaluation( - r"SortBy[Table[IntegerDigits[2^n], {n, 10}], First]", - r"{{1, 6}, {1, 2, 8}, {1, 0, 2, 4}, {2}, {2, 5, 6}, {3, 2}, {4}, {5, 1, 2}, {6, 4}, {8}}", - ) - - -# FIXME: come up with an example that doesn't require skimage. -@pytest.mark.skipif( - not check_requires_list(["skimage"]), - reason="Right now need scikit-image for this to work", -) -def test_canonical_sort_images(): - check_evaluation( - r'Sort[{Import["ExampleData/Einstein.jpg"], 5}]', - r'{5, Import["ExampleData/Einstein.jpg"]}', - ) - - -@pytest.mark.parametrize( - ("str_expr", "msgs", "str_expected", "fail_msg"), - [ - ("Sort[{x_, y_}, PatternsOrderedQ]", None, "{x_, y_}", None), - ], -) -def test_SortPatterns(str_expr, msgs, str_expected, fail_msg): - """ """ - - check_evaluation( - str_expr, - str_expected, - to_string_expr=True, - to_string_expected=True, - hold_expected=True, - failure_message=fail_msg, - expected_messages=msgs, - ) diff --git a/test/builtin/test_binary.py b/test/builtin/test_binary.py index e39e55905..0791bb254 100644 --- a/test/builtin/test_binary.py +++ b/test/builtin/test_binary.py @@ -376,24 +376,6 @@ def test_private_doctests_io(str_expr, str_expected, fail_msg): ) -@pytest.mark.parametrize( - ("str_expr", "str_expected", "fail_msg"), - [ - ("Head[ByteArray[{1}]]", "ByteArray", None), - ], -) -def test_ByteArray(str_expr, str_expected, fail_msg): - """ """ - check_evaluation( - str_expr, - str_expected, - to_string_expr=True, - to_string_expected=True, - hold_expected=True, - failure_message=fail_msg, - ) - - @pytest.mark.parametrize( ("str_expr", "str_expected", "fail_msg"), [ @@ -411,7 +393,7 @@ def test_ByteArray(str_expr, str_expected, fail_msg): ), ], ) -def test_ByteOrdering(str_expr, str_expected, fail_msg): +def test_private_doctests_system(str_expr, str_expected, fail_msg): """ """ check_evaluation( str_expr, diff --git a/test/builtin/test_numericarray.py b/test/builtin/test_numericarray.py new file mode 100644 index 000000000..b539aae7a --- /dev/null +++ b/test/builtin/test_numericarray.py @@ -0,0 +1,54 @@ +from mathics.core.atoms import Integer, NumericArray, String +from mathics.core.convert.python import from_python +from test.helper import check_evaluation, evaluate + +import numpy as np +import pytest + +# +# Python API tests +# + +def test_numericarray_atom_preserves_array_reference(): + array = np.array([1, 2, 3], dtype=np.int64) + atom = NumericArray(array) + assert atom.value is array + +def test_numericarray_atom_preserves_equality(): + array = np.array([1, 2, 3], dtype=np.int64) + atom = NumericArray(array, dtype=np.float64) + np.testing.assert_array_equal(atom.value, array) + + +def test_numericarray_expression_from_python_array(): + array = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float32) + atom = from_python(array) + assert isinstance(atom, NumericArray) + assert atom.value is array + + +# +# WL tests +# + +@pytest.mark.parametrize( + ("str_expr", "str_expected"), + [ + ("NumericArray[{{1,2},{3,4}}]", ""), + ("ToString[NumericArray[{{1,2},{3,4}}]]", ""), + ("Head[NumericArray[{1,2}]]", "NumericArray"), + ("AtomQ[NumericArray[{1,2}]]", "True"), + ("Normal[NumericArray[{{1,2}, {3,4}}]]", "{{1, 2}, {3, 4}}"), + ] +) +def test_basics(str_expr, str_expected): + check_evaluation(str_expr, str_expected, hold_expected=True) + +def test_type_conversion(): + expr = evaluate("NumericArray[{1,2}]") + assert isinstance(expr, NumericArray) + assert expr.value.dtype == np.int64 + expr = evaluate('NumericArray[{1,2}, "ComplexReal32"]') + assert expr.value.dtype == np.complex64 + + diff --git a/test/builtin/test_sort.py b/test/builtin/test_sort.py index 4c851ed95..78daf0dc0 100644 --- a/test/builtin/test_sort.py +++ b/test/builtin/test_sort.py @@ -1,11 +1,62 @@ # -*- coding: utf-8 -*- -from test.helper import check_evaluation +from test.helper import check_evaluation, evaluate_value +import pytest + +from mathics.core.builtin import check_requires_list from mathics.core.expression import Expression from mathics.core.symbols import Symbol, SymbolPlus, SymbolTimes +def test_canonical_sort(): + check_evaluation( + """ + Sort[{ + "a","b", 1, + ByteArray[{1,2,4,1}], + 2, 1.2, I, 2I-3, A, + a+b, a*b, a+1, a*2, b^3, 2/3, + A[x], F[2], F[x], F[x_], F[x___], F[x,t], F[x__], + Condition[A,b>2], Pattern[expr, A] + }] + """, + """{ -3 + 2*I, I, 2 / 3, 1, 1.2, 2, + "a", "b", A, 2*a, a*b, b^3, + A[x], F[2], F[x], F[x_], F[x___], F[x__], F[x, t], + ByteArray["AQIEAQ=="], A /; b > 2, + expr:A, 1 + a, a + b}""", + ) + # The right canonical order should be, according to WMA: + # -3 + 2*I, I, 2/3, 1, 1.2, 2, + # "a", "b", 2*a, + # 1 + a, A, a*b, b^3, a + b, + # A[x], A /; b > 2, + # F[2], F[x], F[x_], F[x___], F[x__], F[x, t], + # ByteArray["AQIEAQ=="], expr:A + + check_evaluation( + r"Sort[Table[IntegerDigits[2^n], {n, 10}]]", + r"{{2}, {4}, {8}, {1, 6}, {3, 2}, {6, 4}, {1, 2, 8}, {2, 5, 6}, {5, 1, 2}, {1, 0, 2, 4}}", + ) + check_evaluation( + r"SortBy[Table[IntegerDigits[2^n], {n, 10}], First]", + r"{{1, 6}, {1, 2, 8}, {1, 0, 2, 4}, {2}, {2, 5, 6}, {3, 2}, {4}, {5, 1, 2}, {6, 4}, {8}}", + ) + + +# FIXME: come up with an example that doesn't require skimage. +@pytest.mark.skipif( + not check_requires_list(["skimage"]), + reason="Right now need scikit-image for this to work", +) +def test_canonical_sort_images(): + check_evaluation( + r'Sort[{Import["ExampleData/Einstein.jpg"], 5}]', + r'{5, Import["ExampleData/Einstein.jpg"]}', + ) + + def test_Expression_sameQ(): """ Test Expression.SameQ diff --git a/test/builtin/test_statistics.py b/test/builtin/test_statistics.py new file mode 100644 index 000000000..ff92e5725 --- /dev/null +++ b/test/builtin/test_statistics.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +""" +Unit tests from mathics.builtin.statistics. +""" + +import sys +import time +from test.helper import check_evaluation, evaluate + +import pytest + + +@pytest.mark.parametrize( + ("str_expr", "msgs", "str_expected", "fail_msg"), + [ + ("Sort[{x_, y_}, PatternsOrderedQ]", None, "{x_, y_}", None), + ], +) +def test_private_doctests_statistics_orderstatistics( + str_expr, msgs, str_expected, fail_msg +): + """ """ + check_evaluation( + str_expr, + str_expected, + to_string_expr=True, + to_string_expected=True, + hold_expected=True, + failure_message=fail_msg, + expected_messages=msgs, + ) diff --git a/test/builtin/test_testing_expressions.py b/test/builtin/test_testing_expressions.py index a1ced6d6b..8efe40db5 100644 --- a/test/builtin/test_testing_expressions.py +++ b/test/builtin/test_testing_expressions.py @@ -3,7 +3,9 @@ Unit tests for mathics.builtin.testing_expressions """ -from test.helper import check_evaluation +import sys +import time +from test.helper import check_evaluation, evaluate import pytest @@ -23,7 +25,7 @@ ("Xor[a, b]", None, "a \\[Xor] b", None), ], ) -def test_logic(str_expr, msgs, str_expected, fail_msg): +def test_private_doctests_logic(str_expr, msgs, str_expected, fail_msg): """text_expressions.logic""" check_evaluation( str_expr, @@ -68,7 +70,7 @@ def test_logic(str_expr, msgs, str_expected, fail_msg): ("SubsetQ[f[a, b, c], f[a]]", None, "True", None), ], ) -def test_list_oriented(str_expr, msgs, str_expected, fail_msg): +def test_private_doctests_list_oriented(str_expr, msgs, str_expected, fail_msg): """text_expressions.logic""" check_evaluation( str_expr, @@ -95,7 +97,7 @@ def test_list_oriented(str_expr, msgs, str_expected, fail_msg): ("a != b != a", None, "a != b != a", "Reproduce strange MMA behaviour"), ], ) -def test_equality_inequality(str_expr, msgs, str_expected, fail_msg): +def test_private_doctests_equality_inequality(str_expr, msgs, str_expected, fail_msg): """text_expressions.logic""" check_evaluation( str_expr, @@ -122,7 +124,7 @@ def test_equality_inequality(str_expr, msgs, str_expected, fail_msg): ("PrimeQ[2 ^ 255 - 1]", None, "False", None), ], ) -def test_numerical_properties(str_expr, msgs, str_expected, fail_msg): +def test_private_doctests_numerical_properties(str_expr, msgs, str_expected, fail_msg): """text_expressions.numerical_properties""" check_evaluation( str_expr, @@ -175,52 +177,3 @@ def test_matchq(str_expr, msgs, str_expected, fail_msg): failure_message=fail_msg, expected_messages=msgs, ) - - -@pytest.mark.parametrize( - ("str_expr", "str_expected", "assert_fail_msg"), - [ - # Clean the definitions, because - # a previous definition of `A` or `F` could make - # the test to fail. - (None, None, None), - ('Order["c", "d"]', "1", "Alphabetic order: 'c' comes before 'd'"), - ('Order["d", "c"]', "-1", "Alphabetic order: 'd' comes after 'c'"), - ('Order["c", ByteArray[{99}]]', "1", "String comes before ByteArray"), - ('Order[ByteArray[{1, 99}], "ZZZZZ"]', "-1", "ByteArray comes after String"), - ('Order["xyzzy", "xyzzy"]', "0", "Equal strings"), - ( - "Order[ByteArray[{1, 99}], ByteArray[{2, 0}]]", - "1", - "Numeric ordering within a ByteArray", - ), - ('Order["a", 1000]', "-1", "String comes after Integer"), - ("Order[0.9, 1]", "1", "Numeric less-than comparison between Real and Integer"), - ( - "Order[1.2, 1]", - "-1", - "Numeric greater than comparison between Real and Integer", - ), - ("Order[F[2], A[2]]", "-1", "Function ordering in function name"), - ( - "Order[F[2], F[3]]", - "1", - "Function ordering in function with a single parameter", - ), - ( - "Order[F[2, 3], F[2]]", - "-1", - "Function ordering in function with mixed-length parameters", - ), - ], -) -def test_order(str_expr: str, str_expected: str, assert_fail_msg: str): - """text_expressions.matchq""" - check_evaluation( - str_expr, - str_expected, - to_string_expr=False, - to_string_expected=False, - hold_expected=False, - failure_message=assert_fail_msg, - ) diff --git a/test/test_evaluation.py b/test/test_evaluation.py index d21b7ac07..14a6c4e10 100644 --- a/test/test_evaluation.py +++ b/test/test_evaluation.py @@ -289,20 +289,6 @@ def test_system_specific_long_integer(): # check_evaluation_with_err(str_expr, str_expected, message) -def test_eval_atom_upvalues(): - """Check that upvalues of atoms are taken into account in evaluations""" - # Clear definitions - check_evaluation(None, None, None) - check_evaluation( - "Unprotect[Real]; Real/:F[x_Real]:=x; DownValues[F]", - "{}", - "F does not have downvalues", - ) - check_evaluation("F[3.]", "3.", "Upvalue of Real is taken into account.") - # Clear definitions again. - check_evaluation(None, None, None) - - def test_exit(): global session try: