Skip to content

Commit f63f1cd

Browse files
authored
Try to lint atoms, start Order tests... (#1518)
Also, canonicalize color_directives in conversions, although this doesn't solve a problem I'll open an issue for.
1 parent 90d6767 commit f63f1cd

File tree

3 files changed

+82
-38
lines changed

3 files changed

+82
-38
lines changed

mathics/builtin/colors/color_directives.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,12 @@ def to_js(self):
204204
return self.to_rgba()
205205

206206
def to_expr(self):
207-
return to_expression(self.get_name(), *self.components)
207+
"""Convert components to MachineReal consistently so that colors with
208+
numerically-equal components but different numeric atom types compare equal.
209+
"""
210+
return to_expression(
211+
self.get_name(), *self.components, elements_conversion_fn=MachineReal
212+
)
208213

209214
def to_rgba(self):
210215
return self.to_color_space("RGB")

mathics/core/atoms.py

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import base64
55
import math
66
import re
7+
from functools import cache
78
from typing import Any, Dict, Generic, Optional, Tuple, TypeVar, Union
89

910
import mpmath
@@ -56,10 +57,16 @@ class Number(Atom, ImmutableValueMixin, NumericOperators, Generic[T]):
5657
being: Integer, Rational, Real, Complex.
5758
"""
5859

59-
_value: T
60+
_value: Any
6061
hash: int
6162

62-
def __getnewargs__(self):
63+
def __eq__(self, other):
64+
if isinstance(other, Number):
65+
return self.element_order == other.element_order
66+
else:
67+
return False
68+
69+
def __getnewargs__(self) -> tuple:
6370
"""
6471
__getnewargs__ is used in pickle loading to ensure __new__ is
6572
called with the right value.
@@ -71,12 +78,6 @@ def __getnewargs__(self):
7178
"""
7279
return (self._value,)
7380

74-
def __eq__(self, other):
75-
if isinstance(other, Number):
76-
return self.element_order == other.element_order
77-
else:
78-
return False
79-
8081
def __str__(self) -> str:
8182
return str(self.value)
8283

@@ -208,6 +209,7 @@ class Integer(Number[int]):
208209
# The key is the Integer's Python `int` value, and the
209210
# dictionary's value is the corresponding Mathics Integer object.
210211
_integers: Dict[Any, "Integer"] = {}
212+
_value: int
211213

212214
_sympy: sympy_numbers.Integer
213215

@@ -448,6 +450,7 @@ class MachineReal(Real[float]):
448450
# The key is the MachineReal's Python `float` value, and the
449451
# dictionary's value is the corresponding Mathics MachineReal object.
450452
_machine_reals: Dict[Any, "MachineReal"] = {}
453+
_value: float
451454

452455
def __new__(cls, value) -> "MachineReal":
453456
n = float(value)
@@ -487,7 +490,7 @@ def get_precision(self) -> int:
487490
return FP_MANTISA_BINARY_DIGITS
488491

489492
def get_float_value(self, permit_complex=False) -> float:
490-
return self.value
493+
return self._value
491494

492495
@property
493496
def is_approx_zero(self) -> bool:
@@ -561,7 +564,10 @@ class PrecisionReal(Real[sympy.Float]):
561564
# The key is the PrecisionReal's sympy.Float, and the
562565
# dictionary's value is the corresponding Mathics PrecisionReal object.
563566
_precision_reals: Dict[Any, "PrecisionReal"] = {}
564-
_sympy: Number
567+
_sympy: sympy.Float
568+
569+
# Note: We have no _value attribute or value property .
570+
# value attribute comes from Number.value
565571

566572
def __new__(cls, value) -> "PrecisionReal":
567573
n = sympy.Float(value)
@@ -635,10 +641,10 @@ def sameQ(self, rhs) -> bool:
635641
diff = abs(value - other_value)
636642
return diff < 0.5**prec
637643

638-
def to_python(self, *args, **kwargs):
644+
def to_python(self, *args, **kwargs) -> float:
639645
return float(self.value)
640646

641-
def to_sympy(self, *args, **kwargs):
647+
def to_sympy(self, *args, **kwargs) -> sympy.Float:
642648
return self.value
643649

644650

@@ -683,7 +689,7 @@ def __getitem__(self, index: int) -> int:
683689
"""
684690
return self.value[index]
685691

686-
def __getnewargs__(self):
692+
def __getnewargs__(self) -> tuple:
687693
return (self.value,)
688694

689695
def __hash__(self) -> int:
@@ -795,11 +801,12 @@ class Complex(Number[Tuple[Number[T], Number[T], Optional[int]]]):
795801
# dictionary's value is the corresponding Mathics Complex object.
796802
_complex_numbers: Dict[Any, "Complex"] = {}
797803

798-
# We use __new__ here to ensure that two Integer's that have the same value
799-
# return the same object, and to set an object hash value.
800-
# Consider also @lru_cache, and mechanisms for limiting and
801-
# clearing the cache and the object store which might be useful in implementing
802-
# Builtin Share[].
804+
# We use __new__ here to ensure that two Complex number that have
805+
# down to the type on the imaginary and real parts and precision of those --
806+
# the same value return the same object, and to set an object hash
807+
# value. Consider also @lru_cache, and mechanisms for limiting
808+
# and clearing the cache and the object store which might be
809+
# useful in implementing Builtin Share[].
803810
def __new__(cls, real, imag):
804811
if not isinstance(real, (Integer, Real, Rational)):
805812
raise ValueError(
@@ -852,9 +859,16 @@ def __new__(cls, real, imag):
852859

853860
return self
854861

862+
def __getnewargs__(self) -> tuple:
863+
return (self.real, self.imag)
864+
855865
def __hash__(self):
856866
return self.hash
857867

868+
@cache
869+
def __neg__(self):
870+
return Complex(-self.real, -self.imag)
871+
858872
def __str__(self) -> str:
859873
return str(self.to_sympy())
860874

@@ -955,12 +969,6 @@ def __eq__(self, other) -> bool:
955969
else:
956970
return super().__eq__(other)
957971

958-
def __getnewargs__(self):
959-
return (self.real, self.imag)
960-
961-
def __neg__(self):
962-
return Complex(-self.real, -self.imag)
963-
964972
@property
965973
def is_zero(self) -> bool:
966974
return self.real.is_zero and self.imag.is_zero
@@ -985,6 +993,9 @@ class Rational(Number[sympy.Rational]):
985993

986994
# Collection of integers defined so far.
987995
_rationals: Dict[Any, "Rational"] = {}
996+
_value: Union[
997+
sympy.Rational, sympy.core.numbers.NaN, sympy.core.numbers.ComplexInfinity
998+
]
988999

9891000
# We use __new__ here to ensure that two Rationals's that have the same value
9901001
# return the same object, and to set an object hash value.
@@ -1008,16 +1019,28 @@ def __new__(cls, numerator, denominator=1) -> "Rational":
10081019
self.hash = hash(key)
10091020
return self
10101021

1022+
def __getnewargs__(self) -> tuple:
1023+
return (self.numerator().value, self.denominator().value)
1024+
10111025
# __hash__ is defined so that we can store Number-derived objects
10121026
# in a set or dictionary.
10131027
def __hash__(self):
10141028
return self.hash
10151029

1030+
def __neg__(self) -> "Rational":
1031+
return Rational(-self.numerator().value, self.denominator().value)
1032+
10161033
def atom_to_boxes(self, f, evaluation):
10171034
from mathics.eval.makeboxes import format_element
10181035

10191036
return format_element(self, evaluation, f)
10201037

1038+
@property
1039+
def is_zero(self) -> bool:
1040+
return (
1041+
self.numerator().is_zero
1042+
) # (implicit) and not (self.denominator().is_zero)
1043+
10211044
def to_sympy(self, **kwargs):
10221045
return self.value
10231046

@@ -1034,9 +1057,11 @@ def sameQ(self, rhs) -> bool:
10341057
"""Mathics SameQ"""
10351058
return isinstance(rhs, Rational) and self.value == rhs.value
10361059

1060+
@cache
10371061
def numerator(self) -> "Integer":
10381062
return Integer(self.value.as_numer_denom()[0])
10391063

1064+
@cache
10401065
def denominator(self) -> "Integer":
10411066
return Integer(self.value.as_numer_denom()[1])
10421067

@@ -1073,18 +1098,6 @@ def user_hash(self, update) -> None:
10731098
b"System`Rational>" + ("%s>%s" % self.value.as_numer_denom()).encode("utf8")
10741099
)
10751100

1076-
def __getnewargs__(self):
1077-
return (self.numerator().value, self.denominator().value)
1078-
1079-
def __neg__(self) -> "Rational":
1080-
return Rational(-self.numerator().value, self.denominator().value)
1081-
1082-
@property
1083-
def is_zero(self) -> bool:
1084-
return (
1085-
self.numerator().is_zero
1086-
) # (implicit) and not (self.denominator().is_zero)
1087-
10881101

10891102
RationalOneHalf = Rational(1, 2)
10901103
RationalMinusOneHalf = Rational(-1, 2)
@@ -1299,7 +1312,7 @@ def user_hash(self, update):
12991312
# hash value of the string's text. this corresponds to MMA behavior.
13001313
update(self.value.encode("utf8"))
13011314

1302-
def __getnewargs__(self):
1315+
def __getnewargs__(self) -> tuple:
13031316
return (self.value,)
13041317

13051318

test/core/test_keycomparable.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import pytest
2+
3+
from mathics.core.atoms import Complex, Integer0, Integer1, Real, String
4+
5+
6+
# Tests
7+
@pytest.mark.parametrize(
8+
("atom1", "atom2", "assert_msg"),
9+
[
10+
(
11+
Complex(Integer0, Integer1),
12+
Integer1,
13+
"Complex numbers come before Integers when numerically incompable",
14+
),
15+
(
16+
Complex(Integer0, Integer1),
17+
Real(1.0),
18+
"Complex numbers come before Real Numbers when numerically incomparable",
19+
),
20+
# FIXME: Fixing this breaks ColorNegate since RGBColor[1, 0, 0] != RGBColor[1.0, 0, 0]
21+
# (Integer1, Real(1.0), "Integers come before Real Numbers when equal value"),
22+
(Integer1, String("abc"), "Integers come before Real Numbers when equal value"),
23+
],
24+
)
25+
def test_element_ordering_lt(atom1, atom2, assert_msg: str):
26+
assert atom1 < atom2, assert_msg

0 commit comments

Comments
 (0)