Skip to content

Commit 516280e

Browse files
committed
Change ByteArray to be Atomic.
This is work is in prepration for handling NumericArray which might be similar.
1 parent 647fff8 commit 516280e

File tree

10 files changed

+151
-119
lines changed

10 files changed

+151
-119
lines changed
Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
# -*- coding: utf-8 -*-
22
"""
3-
Byte Arrays
3+
ByteArrays
44
"""
55

6+
from typing import Optional, Union
7+
68
from mathics.core.atoms import ByteArrayAtom, Integer, String
79
from mathics.core.builtin import Builtin
810
from mathics.core.convert.expression import to_mathics_list
9-
from mathics.core.expression import Expression
10-
from mathics.core.systemsymbols import SymbolByteArray, SymbolFailed
11+
from mathics.core.evaluation import Evaluation
12+
from mathics.core.systemsymbols import SymbolFailed
1113

1214

13-
class ByteArray(Builtin):
15+
class ByteArray_(Builtin):
1416
r"""
1517
<url>:WMA link:
1618
https://reference.wolfram.com/language/ref/ByteArray.html</url>
@@ -35,44 +37,51 @@ class ByteArray(Builtin):
3537
>> ByteArray["ARkD"]
3638
= ByteArray[<3>]
3739
>> B=ByteArray["asy"]
38-
: The first argument in Bytearray[asy] should be a B64 encoded string or a vector of integers.
40+
: The argument in ByteArray[asy] should be a vector of unsigned byte values or a Base64-encoded string.
3941
= $Failed
42+
43+
A 'ByteArray" is a kind of Atom:
44+
45+
>> AtomQ[ByteArray[{4, 2}]]
46+
= True
4047
"""
4148

4249
messages = {
43-
"aotd": "Elements in `1` are inconsistent with type Byte",
44-
"lend": "The first argument in Bytearray[`1`] should "
45-
+ "be a B64 encoded string or a vector of integers.",
50+
"batd": "Elements in `1` are not unsigned byte values.",
51+
"lend": (
52+
"The argument in ByteArray[`1`] should "
53+
"be a vector of unsigned byte values or a Base64-encoded string."
54+
),
4655
}
56+
57+
name = "ByteArray"
4758
summary_text = "array of bytes"
4859

49-
def eval_str(self, string, evaluation):
60+
def eval_str(self, string, evaluation) -> Union[ByteArrayAtom, SymbolFailed]:
5061
"ByteArray[string_String]"
5162
try:
5263
atom = ByteArrayAtom(string.value)
53-
except Exception:
64+
except TypeError:
5465
evaluation.message("ByteArray", "lend", string)
5566
return SymbolFailed
56-
return Expression(SymbolByteArray, atom)
67+
return atom
5768

58-
def eval_to_str(self, baa, evaluation):
69+
def eval_to_str(self, baa, evaluation: Evaluation):
5970
"ToString[ByteArray[baa_ByteArrayAtom]]"
6071
return String(f"ByteArray[<{len(baa.value)}>]")
6172

62-
def eval_normal(self, baa, evaluation):
73+
def eval_normal(self, baa, evaluation: Evaluation):
6374
"System`Normal[ByteArray[baa_ByteArrayAtom]]"
6475
return to_mathics_list(*baa.value, elements_conversion_fn=Integer)
6576

66-
def eval_list(self, values, evaluation):
77+
def eval_list(self, values, evaluation) -> Optional[ByteArrayAtom]:
6778
"ByteArray[values_List]"
68-
if not values.has_form("List", None):
69-
return
7079
try:
71-
ba = bytearray([b.get_int_value() for b in values.elements])
80+
ba = ByteArrayAtom(bytearray([b.get_int_value() for b in values.elements]))
7281
except Exception:
73-
evaluation.message("ByteArray", "aotd", values)
74-
return
75-
return Expression(SymbolByteArray, ByteArrayAtom(ba))
82+
evaluation.message("ByteArray", "batd", values)
83+
return None
84+
return ba
7685

7786

7887
# TODO: BaseEncode, BaseDecode, ByteArrayQ, ByteArrayToString, StringToByteArray, ImportByteArray, ExportByteArray

mathics/builtin/list/constructing.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ class Normal(Builtin):
198198
def eval_general(self, expr: Expression, evaluation: Evaluation):
199199
"Normal[expr_]"
200200
if isinstance(expr, Atom):
201+
if hasattr(expr, "elements"):
202+
return ListExpression(*expr.elements)
201203
return expr
202204
if expr.has_form("RootSum", 2):
203205
return from_sympy(expr.to_sympy().doit(roots=True))

mathics/builtin/statistics/orderstats.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,32 @@ class ReverseSort(Builtin):
300300
}
301301

302302

303+
# FIXME: there might be a bug in sorting...
304+
#
305+
# Sort[{
306+
# "a","b", 1,
307+
# ByteArray[{1,2,4,1}],
308+
# 2, 1.2, I, 2I-3, A,
309+
# a+b, a*b, a+1, a*2, b^3, 2/3,
310+
# A[x], F[2], F[x], F[x_], F[x___], F[x,t], F[x__],
311+
# Condition[A,b>2], Pattern[expr, A]
312+
# }]
313+
#
314+
# should be:
315+
#
316+
# {-3 + 2*I, I, 2/3, 1, 1.2, 2,
317+
# "a", "b", 2*a,
318+
# 1 + a, A, a*b, b^3, a + b,
319+
# A[x], A /; b > 2,
320+
# F[2], F[x], F[x_], F[x___], F[x__], F[x, t],
321+
# ByteArray["AQIEAQ=="], expr:A}
322+
#
323+
# But this is too complicated a case to run as a test. It needs
324+
# to be isolated. Break this down to smaller pieces,
325+
# and also use Order[] to check smaller components.
326+
# The problem might also be in boxing-order output.
327+
328+
303329
class Sort(Builtin):
304330
"""
305331
<url>:WMA link:https://reference.wolfram.com/language/ref/Sort.html</url>
@@ -316,7 +342,7 @@ class Sort(Builtin):
316342
>> Sort[{4, 1.0, a, 3+I}]
317343
= {1., 3 + I, 4, a}
318344
319-
Sort uses 'OrderedQ' to determine ordering by default.
345+
Sort uses 'Order' to determine ordering by default.
320346
You can sort patterns according to their precedence using 'PatternsOrderedQ':
321347
>> Sort[{items___, item_, OptionsPattern[], item_symbol, item_?test}, PatternsOrderedQ]
322348
= {item_symbol, item_ ? test, item_, items___, OptionsPattern[]}

mathics/core/atoms.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from mathics.core.element import BoxElementMixin, ImmutableValueMixin
1414
from mathics.core.keycomparable import (
15+
BASIC_ATOM_BYTEARRAY_SORT_KEY,
1516
BASIC_ATOM_NUMBER_SORT_KEY,
1617
BASIC_ATOM_STRING_OR_BYTEARRAY_SORT_KEY,
1718
)
@@ -640,8 +641,10 @@ def to_sympy(self, *args, **kwargs):
640641

641642

642643
class ByteArrayAtom(Atom, ImmutableValueMixin):
643-
value: Union[bytes, bytearray]
644+
_value: Union[bytes, bytearray]
645+
_elements: Optional[tuple] = None
644646
class_head_name = "System`ByteArrayAtom"
647+
hash: int
645648

646649
# We use __new__ here to ensure that two ByteArrayAtom's that have the same value
647650
# return the same object, and to set an object hash value.
@@ -651,18 +654,24 @@ class ByteArrayAtom(Atom, ImmutableValueMixin):
651654
def __new__(cls, value):
652655
self = super().__new__(cls)
653656
if type(value) in (bytes, bytearray):
654-
self.value = value
657+
self._value = value
655658
elif type(value) is list:
656-
self.value = bytearray(list)
659+
self._value = bytearray(value)
657660
elif type(value) is str:
658-
self.value = base64.b64decode(value)
661+
try:
662+
self._value = base64.b64decode(value)
663+
except Exception as e:
664+
raise TypeError(f"base64 string decode failed: {e}")
659665
else:
660-
raise Exception("value does not belongs to a valid type")
666+
raise TypeError("value does not belongs to a valid type")
661667

662668
self.hash = hash(("ByteArrayAtom", str(self.value)))
663669
return self
664670

665-
def __hash__(self):
671+
def __getnewargs__(self):
672+
return (self.value,)
673+
674+
def __hash__(self) -> int:
666675
return self.hash
667676

668677
def __str__(self) -> str:
@@ -675,8 +684,7 @@ def __str__(self) -> str:
675684
# is removed and the form makes decisions, rather than
676685
# have this routine know everything about all forms.
677686
def atom_to_boxes(self, f, evaluation) -> "String":
678-
res = String(f"<{len(self.value)}>")
679-
return res
687+
return String(f"ByteArray[<{len(self.value)}>]")
680688

681689
def do_copy(self) -> "ByteArrayAtom":
682690
return ByteArrayAtom(self.value)
@@ -685,15 +693,24 @@ def default_format(self, evaluation, form) -> str:
685693
value = self.value
686694
return '"' + value.__str__() + '"'
687695

696+
@property
697+
def elements(self) -> Tuple[int, ...]:
698+
"""
699+
Return a tuple value of Mathics3 Inteters for each element of the ByteArray.
700+
"""
701+
if self._elements is None:
702+
self._elements = tuple([Integer(i) for i in self.value])
703+
return self._elements
704+
688705
@property
689706
def element_order(self) -> tuple:
690707
"""
691708
Return a tuple value that is used in ordering elements
692709
of an expression. The tuple is ultimately compared lexicographically.
693710
"""
694711
return (
695-
BASIC_ATOM_STRING_OR_BYTEARRAY_SORT_KEY,
696-
self.value,
712+
BASIC_ATOM_BYTEARRAY_SORT_KEY,
713+
str(self.value, "utf-8"),
697714
0,
698715
1,
699716
)
@@ -734,12 +751,17 @@ def to_python(self, *args, **kwargs) -> Union[bytes, bytearray]:
734751
return self.value
735752

736753
def user_hash(self, update):
737-
# hashing a String is the one case where the user gets the untampered
754+
"""
755+
returned untampered hash value.
756+
757+
hashing a String is the one case where the user gets the untampered
738758
# hash value of the string's text. this corresponds to MMA behavior.
759+
"""
739760
update(self.value)
740761

741-
def __getnewargs__(self):
742-
return (self.value,)
762+
@property
763+
def value(self) -> Union[bytes, bytearray]:
764+
return self._value
743765

744766

745767
class Complex(Number[Tuple[Number[T], Number[T], Optional[int]]]):

mathics/core/keycomparable.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ def __ne__(self, other) -> bool:
263263

264264
BASIC_ATOM_NUMBER_SORT_KEY = 0x00
265265
BASIC_ATOM_STRING_OR_BYTEARRAY_SORT_KEY = 0x01
266+
BASIC_ATOM_BYTEARRAY_SORT_KEY = 0x02
266267
LITERAL_EXPRESSION_SORT_KEY = 0x03
267268

268269
BASIC_NUMERIC_EXPRESSION_SORT_KEY = 0x12

mathics/eval/list/eol.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def list_parts(exprs, selectors, evaluation):
128128

129129
picked = list(list_parts(selected, selectors[1:], evaluation))
130130

131-
if unwrap is None:
131+
if unwrap is None and hasattr(expr, "restructure"):
132132
expr = expr.restructure(expr.head, picked, evaluation)
133133
yield expr
134134
else:
@@ -197,7 +197,7 @@ def parts_sequence_selector(pspec):
197197
raise MessageException("Part", "pspec", pspec)
198198

199199
def select(inner):
200-
if isinstance(inner, Atom):
200+
if not hasattr(inner, "elements"):
201201
raise MessageException("Part", "partd")
202202

203203
elements = inner.elements
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# -*- coding: utf-8 -*-
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from test.helper import check_evaluation
2+
3+
import pytest
4+
5+
from mathics.core.builtin import check_requires_list
6+
7+
8+
def test_canonical_sort():
9+
check_evaluation(
10+
"Sort[{F[2], ByteArray[{2}]}]",
11+
"{ByteArray[<1>], F[2]}",
12+
hold_expected=True,
13+
)
14+
check_evaluation(
15+
r"Sort[Table[IntegerDigits[2^n], {n, 10}]]",
16+
r"{{2}, {4}, {8}, {1, 6}, {3, 2}, {6, 4}, {1, 2, 8}, {2, 5, 6}, {5, 1, 2}, {1, 0, 2, 4}}",
17+
)
18+
check_evaluation(
19+
r"SortBy[Table[IntegerDigits[2^n], {n, 10}], First]",
20+
r"{{1, 6}, {1, 2, 8}, {1, 0, 2, 4}, {2}, {2, 5, 6}, {3, 2}, {4}, {5, 1, 2}, {6, 4}, {8}}",
21+
)
22+
23+
24+
# FIXME: come up with an example that doesn't require skimage.
25+
@pytest.mark.skipif(
26+
not check_requires_list(["skimage"]),
27+
reason="Right now need scikit-image for this to work",
28+
)
29+
def test_canonical_sort_images():
30+
check_evaluation(
31+
r'Sort[{Import["ExampleData/Einstein.jpg"], 5}]',
32+
r'{5, Import["ExampleData/Einstein.jpg"]}',
33+
)
34+
35+
36+
@pytest.mark.parametrize(
37+
("str_expr", "msgs", "str_expected", "fail_msg"),
38+
[
39+
("Sort[{x_, y_}, PatternsOrderedQ]", None, "{x_, y_}", None),
40+
],
41+
)
42+
def test_SortPatterns(str_expr, msgs, str_expected, fail_msg):
43+
""" """
44+
45+
check_evaluation(
46+
str_expr,
47+
str_expected,
48+
to_string_expr=True,
49+
to_string_expected=True,
50+
hold_expected=True,
51+
failure_message=fail_msg,
52+
expected_messages=msgs,
53+
)

test/builtin/test_sort.py

Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,11 @@
11
# -*- coding: utf-8 -*-
22

3-
from test.helper import check_evaluation, evaluate_value
3+
from test.helper import check_evaluation
44

5-
import pytest
6-
7-
from mathics.core.builtin import check_requires_list
85
from mathics.core.expression import Expression
96
from mathics.core.symbols import Symbol, SymbolPlus, SymbolTimes
107

118

12-
def test_canonical_sort():
13-
check_evaluation(
14-
"""
15-
Sort[{
16-
"a","b", 1,
17-
ByteArray[{1,2,4,1}],
18-
2, 1.2, I, 2I-3, A,
19-
a+b, a*b, a+1, a*2, b^3, 2/3,
20-
A[x], F[2], F[x], F[x_], F[x___], F[x,t], F[x__],
21-
Condition[A,b>2], Pattern[expr, A]
22-
}]
23-
""",
24-
"""{ -3 + 2*I, I, 2 / 3, 1, 1.2, 2,
25-
"a", "b", A, 2*a, a*b, b^3,
26-
A[x], F[2], F[x], F[x_], F[x___], F[x__], F[x, t],
27-
ByteArray["AQIEAQ=="], A /; b > 2,
28-
expr:A, 1 + a, a + b}""",
29-
)
30-
# The right canonical order should be, according to WMA:
31-
# -3 + 2*I, I, 2/3, 1, 1.2, 2,
32-
# "a", "b", 2*a,
33-
# 1 + a, A, a*b, b^3, a + b,
34-
# A[x], A /; b > 2,
35-
# F[2], F[x], F[x_], F[x___], F[x__], F[x, t],
36-
# ByteArray["AQIEAQ=="], expr:A
37-
38-
check_evaluation(
39-
r"Sort[Table[IntegerDigits[2^n], {n, 10}]]",
40-
r"{{2}, {4}, {8}, {1, 6}, {3, 2}, {6, 4}, {1, 2, 8}, {2, 5, 6}, {5, 1, 2}, {1, 0, 2, 4}}",
41-
)
42-
check_evaluation(
43-
r"SortBy[Table[IntegerDigits[2^n], {n, 10}], First]",
44-
r"{{1, 6}, {1, 2, 8}, {1, 0, 2, 4}, {2}, {2, 5, 6}, {3, 2}, {4}, {5, 1, 2}, {6, 4}, {8}}",
45-
)
46-
47-
48-
# FIXME: come up with an example that doesn't require skimage.
49-
@pytest.mark.skipif(
50-
not check_requires_list(["skimage"]),
51-
reason="Right now need scikit-image for this to work",
52-
)
53-
def test_canonical_sort_images():
54-
check_evaluation(
55-
r'Sort[{Import["ExampleData/Einstein.jpg"], 5}]',
56-
r'{5, Import["ExampleData/Einstein.jpg"]}',
57-
)
58-
59-
609
def test_Expression_sameQ():
6110
"""
6211
Test Expression.SameQ

0 commit comments

Comments
 (0)