From 5db7aab5f5957413b86efdd546257641f16f4ecf Mon Sep 17 00:00:00 2001 From: rocky Date: Thu, 29 Oct 2026 12:06:34 -0400 Subject: [PATCH 01/12] Add Extended GCD. --- SYMBOLS_MANIFEST.txt | 1 + mathics/builtin/intfns/divlike.py | 106 ++++++++++++++++++------------ mathics/eval/intfns/divlike.py | 95 ++++++++++++++++++++++---- 3 files changed, 149 insertions(+), 53 deletions(-) diff --git a/SYMBOLS_MANIFEST.txt b/SYMBOLS_MANIFEST.txt index 3a8d24e1e..f4b16d8cf 100644 --- a/SYMBOLS_MANIFEST.txt +++ b/SYMBOLS_MANIFEST.txt @@ -439,6 +439,7 @@ System`Exponent System`Export System`ExportString System`Expression +System`ExtendedGCD System`Extract System`FaceForm System`Factor diff --git a/mathics/builtin/intfns/divlike.py b/mathics/builtin/intfns/divlike.py index 1817e4fa9..2de33c725 100644 --- a/mathics/builtin/intfns/divlike.py +++ b/mathics/builtin/intfns/divlike.py @@ -1,11 +1,9 @@ -# -*- coding: utf-8 -*- - """ Division-Related Functions """ import sys -from typing import Callable, Optional, Union +from typing import Final from sympy import Q, ask @@ -33,6 +31,8 @@ SymbolQuotientRemainder, ) from mathics.eval.intfns.divlike import ( + combine_integer_arguments, + eval_ExtendedGCD, eval_GCD, eval_LCM, eval_ModularInverse, @@ -98,44 +98,65 @@ class Divisible(Builtin): attributes = A_LISTABLE | A_PROTECTED | A_READ_PROTECTED eval_error = Builtin.generic_argument_error expected_args = range(2, sys.maxsize) - rules = { + rules: Final[dict] = { "Divisible[n_, m_]": "Mod[n, m] == 0", } summary_text = "test whether one number is divisible by the other" -def combine_integer_arguments(builtin, ns, eval_fn: Callable) -> Optional[Expression]: +class ExtendedGCD(Builtin): """ - Build the result of GCD or LCM from its arguments ns: the integer - arguments are combined by eval_fn into a single Integer. If every - argument is an integer, that Integer is returned; otherwise a new - GCD[...] or LCM[...] expression is returned that holds the combined - integer followed by the remaining arguments. None, meaning no change, - is returned when there are fewer than two integers to combine. - - The rule pattern is ns___ (ns__) and not ns___Integer: the functions are - Flat, and ns___Integer also matches a lone integer among other - arguments. Rewriting it to itself made GCD[1/2, 3] or GCD[2.5, 5] - evaluate until $IterationLimit. + Extended Euclidean algorithm + :https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm( + :SymPy:https://docs.sympy.org/latest/modules/polys/domainsref.html#sympy.polys.domains.IntegerRing.gcdex, + :WMA: + https://reference.wolfram.com/language/ref/GCD.html) + +
+
'ExtendedGCD'[$n_1$, $n_2$, ...] + computes the extended greatest common divisor of integers $n_1$, $n_2$, ... +
+ + Get the GCD and coefficients for the GCD of 2 and 3: + >> {gcd, {a, b}} = ExtendedGCD[2, 3] + = {1, {-1, 1}} + + Now reconstruct the gcd using just the coefficents and original numbers and check it matches 'gcd': + >> 2 a + 3 b == gcd + = True + + Get the GCD and coefficients for the GCD of 6 15, and 30: + >> {g, {a, b, c}} = ExtendedGCD[6, 15, 30] + = {3, {-2, 1, 0}} + + Again, reconstruct the gcd using just the coefficents and original numbers and check it matches 'gcd': + >> 6 a + 15 b + 30 c == g + = True + + + ExtendedGCD[3, {5, 15}] theads element-wise over lists: + >> ExtendedGCD[3, {5, 15}] + = {{1, {2, -1}}, {3, {1, 0}}} """ - ns = ns.get_sequence() - integers = [n for n in ns if isinstance(n, Integer)] - if len(integers) == len(ns): - return eval_fn(ns) - if len(integers) < 2: - return None - combined = eval_fn(integers) - if combined is None: - return None - others = [n for n in ns if not isinstance(n, Integer)] - return Expression(Symbol(builtin.get_name()), combined, *others) + + attributes = A_FLAT | A_LISTABLE | A_ONE_IDENTITY | A_ORDERLESS | A_PROTECTED + eval_error = Builtin.generic_argument_error + expected_args = range(1, sys.maxsize) + sympy_name = "gcdex" + summary_text = "compute the extended Greatest Common Divisor" + + def eval(self, ns, evaluation: Evaluation) -> Expression | None: + "ExtendedGCD[ns__]" + return eval_ExtendedGCD(ns) class GCD(Builtin): """ - - :WMA link: - https://reference.wolfram.com/language/ref/GCD.html + Greatest common divisor + :https://en.wikipedia.org/wiki/Greatest_common_divisor( + :SymPy:https://docs.sympy.org/latest/modules/polys/domainsref.html#sympy.polys.domains.IntegerRing.gcdex, + :WMA: + https://reference.wolfram.com/language/ref/GCD.html)
'GCD'[$n_1$, $n_2$, ...] @@ -158,7 +179,7 @@ class GCD(Builtin): summary_text = "compute the Greatest Common Divisor" sympy_name = "gcd" - def eval(self, ns, evaluation: Evaluation) -> Optional[Expression]: + def eval(self, ns, evaluation: Evaluation) -> Expression | None: "GCD[ns___]" return combine_integer_arguments(self, ns, eval_GCD) @@ -166,6 +187,9 @@ def eval(self, ns, evaluation: Evaluation) -> Optional[Expression]: class LCM(Builtin): """ + Least common multiple + :https://en.wikipedia.org/wiki/Least_common_multiple( + :SymPy:https://docs.sympy.org/latest/modules/polys/domainsref.html#sympy.polys.domains.IntegerRing.lcm, :WMA link:https://reference.wolfram.com/language/ref/LCM.html
@@ -182,13 +206,13 @@ class LCM(Builtin): attributes = A_FLAT | A_LISTABLE | A_ONE_IDENTITY | A_ORDERLESS | A_PROTECTED eval_error = Builtin.generic_argument_error expected_args = range(1, sys.maxsize) - messages = { + messages: Final[dict[str, str]] = { "argm": "LCM called with 0 arguments; 1 or more arguments are expected.", } summary_text = "compute the Least Common Multiple" sympy_name = "lcm" - def eval(self, ns, evaluation: Evaluation) -> Optional[Expression]: + def eval(self, ns, evaluation: Evaluation) -> Expression | None: "LCM[ns__]" # LCM[] is answered by generic_argument_error. @@ -297,7 +321,7 @@ class ModularInverse(SympyFunction): summary_text = "get the modular inverse $k^(-1)$ mod $n$" sympy_name = "mod_inverse" - def eval(self, k: Integer, n: Integer, evaluation: Evaluation) -> Optional[Integer]: + def eval(self, k: Integer, n: Integer, evaluation: Evaluation) -> Integer | None: "ModularInverse[k_Integer, n_Integer]" return eval_ModularInverse(k.value, n.value) @@ -385,17 +409,17 @@ class PowerMod(Builtin): eval_error = Builtin.generic_argument_error expected_args = 3 - messages = { + messages: Final[dict[str, str]] = { "ninv": "`1` is not invertible modulo `2`.", } - rules = { + rules: Final[dict[str, str]] = { "PowerMod[a, l_List, m_?NumberQ]": "Map[PowerMod[a, #, m] &, l]", } summary_text = "compute modular exponentiation" - sympy_names = { + sympy_names: Final[dict[str, str]] = { -1: "mod_inverse", # b = -1 0: "nth_root_mod", # 0 < b < 1 and is an inverse 0.5: "sqrt_mod", # b = 1/2 @@ -458,7 +482,7 @@ class Quotient(Builtin): eval_error = Builtin.generic_argument_error expected_args = (2, 3) - rules = { + rules: Final[dict[str, str]] = { "Quotient[m_Complex, n_?NumberQ]": "Round[m / n]", "Quotient[m_?NumberQ, n_Complex]": "Round[m / n]", "Quotient[l_List, n_?NumberQ]": "Map[Quotient[#, m] &, l]", @@ -467,7 +491,7 @@ class Quotient(Builtin): summary_text = "compute integer quotient" - def eval(self, m, n, evaluation: Evaluation) -> Union[Symbol, Integer]: + def eval(self, m, n, evaluation: Evaluation) -> Symbol | Integer: "Quotient[m_?NumberQ, n_?NumberQ]" py_m = m.value @@ -483,9 +507,7 @@ def eval(self, m, n, evaluation: Evaluation) -> Union[Symbol, Integer]: return result return eval_Quotient(py_m, py_n, 0) - def eval_with_offset( - self, m, n, d, evaluation: Evaluation - ) -> Union[Symbol, Integer]: + def eval_with_offset(self, m, n, d, evaluation: Evaluation) -> Symbol | Integer: "Quotient[m_?NumberQ, n_?NumberQ, d_?NumberQ]" py_m = m.value py_n = n.value diff --git a/mathics/eval/intfns/divlike.py b/mathics/eval/intfns/divlike.py index 637983724..ed733fb7a 100644 --- a/mathics/eval/intfns/divlike.py +++ b/mathics/eval/intfns/divlike.py @@ -2,28 +2,63 @@ Mathics3 builtins from mathics.builtin.intfns.divlike """ -from typing import Optional, Union +from collections.abc import Callable import sympy -import mathics.eval.tracing as tracing +import mathics.eval.tracing as tracing # noqa from mathics.core.atoms import Complex, Integer, Integer0 +from mathics.core.convert.expression import to_mathics_list from mathics.core.convert.python import from_python from mathics.core.convert.sympy import from_sympy from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression +from mathics.core.list import ListExpression +from mathics.core.symbols import Symbol -def get_unit_inverse(f: float) -> Optional[int]: +def combine_integer_arguments(builtin, ns, eval_fn: Callable) -> Expression | None: """ - If `f` is a unit inverse, that is, 1 / some_int, - then some_int. Otherwise return None + Build the result of GCD or LCM from its arguments ns: the integer + arguments are combined by eval_fn into a single Integer. If every + argument is an integer, that Integer is returned; otherwise a new + GCD[...] or LCM[...] expression is returned that holds the combined + integer followed by the remaining arguments. None, meaning no change, + is returned when there are fewer than two integers to combine. + + The rule pattern is ns___ (ns__) and not ns___Integer: the functions are + Flat, and ns___Integer also matches a lone integer among other + arguments. Rewriting it to itself made GCD[1/2, 3] or GCD[2.5, 5] + evaluate until $IterationLimit. """ - if f <= 0: + ns = ns.get_sequence() + integers = [n for n in ns if isinstance(n, Integer)] + if len(integers) == len(ns): + return eval_fn(ns) + if len(integers) < 2: return None - return int(inv) if (inv := 1.0 / f) else None + combined = eval_fn(integers) + if combined is None: + return None + others = [n for n in ns if not isinstance(n, Integer)] + return Expression(Symbol(builtin.get_name()), combined, *others) + +def eval_ExtendedGCD(arguments: tuple) -> Integer | None: + integers: list[int] = [] + for element in arguments.elements: + if isinstance(element, Integer): + integers.append(element.value) + else: + # Handle things other than Integers? + return None + coeffs, current_gcd = gcdex_list(integers) + return ListExpression( + Integer(current_gcd), to_mathics_list(*coeffs, elements_conversion_fn=Integer) + ) -def eval_GCD(ns: tuple) -> Optional[Integer]: + +def eval_GCD(ns: tuple) -> Integer | None: if len(ns) == 0: return Integer0 if (result := ns[0].value) is None: @@ -36,7 +71,7 @@ def eval_GCD(ns: tuple) -> Optional[Integer]: return Integer(abs(result)) -def eval_LCM(ns: tuple) -> Optional[Integer]: +def eval_LCM(ns: tuple) -> Integer | None: if len(ns) == 0: return Integer0 if (result := ns[0].value) is None: @@ -49,7 +84,7 @@ def eval_LCM(ns: tuple) -> Optional[Integer]: return Integer(abs(result)) -def eval_ModularInverse(k: int, n: int) -> Optional[Integer]: +def eval_ModularInverse(k: int, n: int) -> Integer | None: try: r = tracing.run_sympy(sympy.mod_inverse, k, n) except ValueError: @@ -57,7 +92,7 @@ def eval_ModularInverse(k: int, n: int) -> Optional[Integer]: return Integer(r) -def eval_PowerMod(a, b, m, evaluation: Evaluation) -> Union[Integer, Complex]: +def eval_PowerMod(a, b, m, evaluation: Evaluation) -> Integer | Complex: """ All the variaous forms of handling module exponentiation. SymPy offers mod_inverse, nth_root_mod, sqrt_mod, and core.power.Pow. Python's @@ -119,3 +154,41 @@ def eval_PowerMod(a, b, m, evaluation: Evaluation) -> Union[Integer, Complex]: def eval_Quotient(m, n, d) -> Integer: return Integer((m - d) // n) + + +def gcdex_list(numbers: list[int]) -> (list, int): + """ + Return the extended GCD's coefficients and cumulative gcd, for a list of numbers using + SymPy's gcdex. + If the list is empty we return []. + + """ + if not numbers: + return [], 0 + if len(numbers) == 1: + # Degenerate case. + return [1], numbers[0] + + # Start with the first element... + coeffs = [1] + current_gcd = numbers[0] + + # and then chain through the remaining elements. + for num in numbers[1:]: + x, y, current_gcd = tracing.run_sympy(sympy.gcdex, current_gcd, num) + # Scale all previously found coefficients by x + coeffs = [c * x for c in coeffs] + # Append the new coefficient y. + coeffs.append(y) + + return coeffs, current_gcd + + +def get_unit_inverse(f: float) -> int | None: + """ + If `f` is a unit inverse, that is, 1 / some_int, + then some_int. Otherwise return None + """ + if f <= 0: + return None + return int(inv) if (inv := 1.0 / f) else None From 57bfb82ad19f70315e535203f3cc3580ba2b40a2 Mon Sep 17 00:00:00 2001 From: rocky Date: Thu, 29 Oct 2026 12:43:28 -0400 Subject: [PATCH 02/12] Go over divlike docs --- mathics/builtin/intfns/divlike.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/mathics/builtin/intfns/divlike.py b/mathics/builtin/intfns/divlike.py index 2de33c725..6e771226b 100644 --- a/mathics/builtin/intfns/divlike.py +++ b/mathics/builtin/intfns/divlike.py @@ -106,15 +106,15 @@ class Divisible(Builtin): class ExtendedGCD(Builtin): """ - Extended Euclidean algorithm - :https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm( + :Extended Euclidean algorithm: + https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm ( :SymPy:https://docs.sympy.org/latest/modules/polys/domainsref.html#sympy.polys.domains.IntegerRing.gcdex, :WMA: https://reference.wolfram.com/language/ref/GCD.html)
'ExtendedGCD'[$n_1$, $n_2$, ...] - computes the extended greatest common divisor of integers $n_1$, $n_2$, ... +
computes the extended greatest common divisor of integers $n_1$, $n_2$, ...
Get the GCD and coefficients for the GCD of 2 and 3: @@ -152,8 +152,8 @@ def eval(self, ns, evaluation: Evaluation) -> Expression | None: class GCD(Builtin): """ - Greatest common divisor - :https://en.wikipedia.org/wiki/Greatest_common_divisor( + :Greatest common divisor: + https://en.wikipedia.org/wiki/Greatest_common_divisor ( :SymPy:https://docs.sympy.org/latest/modules/polys/domainsref.html#sympy.polys.domains.IntegerRing.gcdex, :WMA: https://reference.wolfram.com/language/ref/GCD.html) @@ -187,10 +187,10 @@ def eval(self, ns, evaluation: Evaluation) -> Expression | None: class LCM(Builtin): """ - Least common multiple - :https://en.wikipedia.org/wiki/Least_common_multiple( + :Least common multiple: + https://en.wikipedia.org/wiki/Least_common_multiple ( :SymPy:https://docs.sympy.org/latest/modules/polys/domainsref.html#sympy.polys.domains.IntegerRing.lcm, - :WMA link:https://reference.wolfram.com/language/ref/LCM.html + :WMA link:https://reference.wolfram.com/language/ref/LCM.html)
'LCM'[$n_1$, $n_2$, ...] @@ -329,7 +329,8 @@ def eval(self, k: Integer, n: Integer, evaluation: Evaluation) -> Integer | None class PowerMod(Builtin): """ :Modular exponentiation: - https://en.wikipedia.org/wiki/Modular_exponentiation SymPy: :mod_inverse:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.mod_inverse, + https://en.wikipedia.org/wiki/Modular_exponentiation (:SymPy: + :mod_inverse:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.mod_inverse, :nth_root_mod:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.nth_root, :sqrt_mod:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.sqrt_mod, and :Pow:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.Pow; :WMA link: @@ -526,7 +527,8 @@ def eval_with_offset(self, m, n, d, evaluation: Evaluation) -> Symbol | Integer: class QuotientRemainder(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/QuotientRemainder.html + :Euclidian Division:https://en.wikipedia.org/wiki/Euclidean_division ( + :WMA link:https://reference.wolfram.com/language/ref/QuotientRemainder.html)
'QuotientRemainder[m, n]' From f92297ae3ac068d69a0115f552389863dceb0283 Mon Sep 17 00:00:00 2001 From: "R. Bernstein" Date: Thu, 29 Oct 2026 17:22:37 -0400 Subject: [PATCH 03/12] Fix typos in comments for clarity --- mathics/builtin/intfns/divlike.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mathics/builtin/intfns/divlike.py b/mathics/builtin/intfns/divlike.py index 6e771226b..29423871b 100644 --- a/mathics/builtin/intfns/divlike.py +++ b/mathics/builtin/intfns/divlike.py @@ -121,15 +121,15 @@ class ExtendedGCD(Builtin): >> {gcd, {a, b}} = ExtendedGCD[2, 3] = {1, {-1, 1}} - Now reconstruct the gcd using just the coefficents and original numbers and check it matches 'gcd': + Now reconstruct the gcd using just the coefficients and original numbers and check it matches 'gcd': >> 2 a + 3 b == gcd = True - Get the GCD and coefficients for the GCD of 6 15, and 30: + Get the GCD and coefficients for the GCD of 6, 15, and 30: >> {g, {a, b, c}} = ExtendedGCD[6, 15, 30] = {3, {-2, 1, 0}} - Again, reconstruct the gcd using just the coefficents and original numbers and check it matches 'gcd': + Again, reconstruct the gcd using just the coefficients and original numbers and check it matches 'gcd': >> 6 a + 15 b + 30 c == g = True From 30517d7e847e292752a00b34b4f476a4a41073fb Mon Sep 17 00:00:00 2001 From: "R. Bernstein" Date: Thu, 29 Oct 2026 17:24:19 -0400 Subject: [PATCH 04/12] Fix typos in divlike.py documentation Corrected typos in docstrings and comments for clarity. --- mathics/eval/intfns/divlike.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mathics/eval/intfns/divlike.py b/mathics/eval/intfns/divlike.py index ed733fb7a..57c83e792 100644 --- a/mathics/eval/intfns/divlike.py +++ b/mathics/eval/intfns/divlike.py @@ -1,4 +1,4 @@ -""" +b""" Mathics3 builtins from mathics.builtin.intfns.divlike """ @@ -94,7 +94,7 @@ def eval_ModularInverse(k: int, n: int) -> Integer | None: def eval_PowerMod(a, b, m, evaluation: Evaluation) -> Integer | Complex: """ - All the variaous forms of handling module exponentiation. + All the various forms of handling modular exponentiation. SymPy offers mod_inverse, nth_root_mod, sqrt_mod, and core.power.Pow. Python's `pow` is good for large ints. """ @@ -143,7 +143,7 @@ def eval_PowerMod(a, b, m, evaluation: Evaluation) -> Integer | Complex: elif all(isinstance(x, int) for x in (a, b, m)) and b >= 1: # Standard Modular Exponentiation (b >= 1) using sympy.pow: # Use Python's builtin function `pow`. - # this handles large int's better than SymPy's core.power.Pow: + # This handles large int's better than SymPy's core.power.Pow: return Integer(pow(a, b, m)) elif b > 1: # Standard Modular Exponentiation (b >= 1) using sympy.pow: @@ -158,9 +158,9 @@ def eval_Quotient(m, n, d) -> Integer: def gcdex_list(numbers: list[int]) -> (list, int): """ - Return the extended GCD's coefficients and cumulative gcd, for a list of numbers using + Return the extended GCD's coefficients and cumulative GCD for a list of numbers using SymPy's gcdex. - If the list is empty we return []. + If the list is empty,bbbbbbb we return []. """ if not numbers: From 6d9e969d091d8c621bf7c610c99df95299e582c2 Mon Sep 17 00:00:00 2001 From: "R. Bernstein" Date: Thu, 29 Oct 2026 17:25:05 -0400 Subject: [PATCH 05/12] Fix typo in gcdex_list docstring --- mathics/eval/intfns/divlike.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/eval/intfns/divlike.py b/mathics/eval/intfns/divlike.py index 57c83e792..838eb4406 100644 --- a/mathics/eval/intfns/divlike.py +++ b/mathics/eval/intfns/divlike.py @@ -160,7 +160,7 @@ def gcdex_list(numbers: list[int]) -> (list, int): """ Return the extended GCD's coefficients and cumulative GCD for a list of numbers using SymPy's gcdex. - If the list is empty,bbbbbbb we return []. + If the list is empty, we return []. """ if not numbers: From ab857670d5e59ea92e65744174275963aad0901a Mon Sep 17 00:00:00 2001 From: rocky Date: Thu, 29 Oct 2026 19:45:32 -0400 Subject: [PATCH 06/12] run ruff over changed files --- mathics/builtin/intfns/divlike.py | 4 ++ mathics/core/expression.py | 101 ++++++++++++++---------------- mathics/eval/intfns/divlike.py | 6 +- 3 files changed, 55 insertions(+), 56 deletions(-) diff --git a/mathics/builtin/intfns/divlike.py b/mathics/builtin/intfns/divlike.py index 6e771226b..757d15da0 100644 --- a/mathics/builtin/intfns/divlike.py +++ b/mathics/builtin/intfns/divlike.py @@ -133,6 +133,10 @@ class ExtendedGCD(Builtin): >> 6 a + 15 b + 30 c == g = True + When one integer argument is given, the answer degenerates to a GCD of the number with coefficient 1: + >> ExtendedGCD[5] + = {5, {1}} + ExtendedGCD[3, {5, 15}] theads element-wise over lists: >> ExtendedGCD[3, {5, 15}] diff --git a/mathics/core/expression.py b/mathics/core/expression.py index 24d468c1e..259eadeae 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -1,11 +1,11 @@ # cython: language_level=3 -# -*- coding: utf-8 -*- import math from bisect import bisect_left +from collections.abc import Callable, Iterable, Sequence from itertools import chain from types import MethodType -from typing import Any, Callable, Iterable, Optional, Sequence, Union +from typing import Any, Optional import sympy from mathics_scanner.location import SourceRange, SourceRange2 @@ -149,7 +149,7 @@ def next_elem(): if len_elements != len(current[1]._elements): return False parents.append(current) - current = tuple((c._head for c in current)) + current = tuple(c._head for c in current) pos.append(0) else: # Atom is not the same than an expression return False @@ -268,7 +268,7 @@ def sliced(self, lower, upper): b = bisect_left(seq, upper) # all(val >= j for val in seq[b:]) new_sequences = tuple(x - lower for x in seq[a:b]) elif seq is not None: - new_sequences = tuple() + new_sequences = () else: new_sequences = None @@ -329,18 +329,18 @@ class Expression(BaseElement, NumericOperators, EvalMixin): _head: BaseElement _elements: tuple[BaseElement, ...] _sequences: Any - _cache: Optional[ExpressionCache] - elements_properties: Optional[ElementsProperties] - options: Optional[dict[str, Any]] + _cache: ExpressionCache | None + elements_properties: ElementsProperties | None + options: dict[str, Any] | None pattern_sequence: bool - location: Optional[Union[SourceRange, SourceRange2, MethodType]] + location: SourceRange | SourceRange2 | MethodType | None def __init__( self, head: BaseElement, *elements: BaseElement, - elements_properties: Optional[ElementsProperties] = None, - literal_values: Optional[tuple] = None, + elements_properties: ElementsProperties | None = None, + literal_values: tuple | None = None, ): self.options = None self.pattern_sequence = False @@ -362,7 +362,7 @@ def __init__( self.location = None # self.copy creates this - self.original: Optional[Expression] = None + self.original: Expression | None = None def __getnewargs__(self): return (self._head, self._elements) @@ -371,14 +371,12 @@ def __hash__(self): return hash(("Expression", self._head) + tuple(self._elements)) def __repr__(self) -> str: - return "" % ( - repr(self.head), + return f"" % ( ", ".join([repr(element) for element in self.elements]), ) def __str__(self) -> str: - return "%s[%s]" % ( - str(self.head), + return f"{self.head}[%s]" % ( ", ".join([str(element) for element in self.elements]), ) @@ -498,24 +496,19 @@ def _does_not_contain_symbol(self, symbol_name: str) -> bool: return False symbols = cache.symbols - if symbols is not None and symbol_name not in symbols: - return True - else: - return False + return bool(symbols is not None and symbol_name not in symbols) def _rebuild_cache(self): cache = self._cache if cache is None: time = None - elif cache.symbols is None: - time = cache.time - elif cache.sequences is None: + elif cache.symbols is None or cache.sequences is None: time = cache.time else: return cache - sym = set((self.get_head_name(),)) + sym = {self.get_head_name()} seq = [] for i, element in enumerate(self._elements): @@ -583,7 +576,7 @@ def elements(self, values: Iterable): self.elements_properties = None self._cache = None - def equal2(self, rhs: Any) -> Optional[bool]: + def equal2(self, rhs: Any) -> bool | None: """Mathics3 two-argument Equal (==) returns True if self and rhs are identical. """ @@ -618,7 +611,7 @@ def equal2(self, rhs: Any) -> Optional[bool]: def evaluate( self, evaluation: Evaluation, - ) -> Optional[BaseElement]: + ) -> BaseElement | None: """ Apply transformation rules and expression evaluation to ``evaluation`` via ``rewrite_apply_eval_step()`` until that method tells us to stop, @@ -629,7 +622,7 @@ def evaluate( if evaluation.timeout: return None - expr: Optional[BaseElement] = self + expr: BaseElement | None = self reevaluate = True limit = None iteration = 1 @@ -691,7 +684,7 @@ def evaluate( evaluation.recursion_depth = recursion_depth # And one more for the finally below evaluation.inc_recursion_depth() - raise ret + raise finally: # Restores the state evaluation.options = old_options @@ -733,7 +726,7 @@ def evaluate_elements(self, evaluation) -> "Expression": # head = head.evaluate_elements(evaluation) return Expression(head, *elements) - def filter(self, head, cond, evaluation: Evaluation, count: Optional[int] = None): + def filter(self, head, cond, evaluation: Evaluation, count: int | None = None): # faster equivalent to: Expression(head, [element in self.elements if cond(element)]) return structure(head, self, evaluation).filter(self, cond, count) @@ -907,7 +900,7 @@ def get_mutable_elements(self) -> list: def get_option_values( self, evaluation: Evaluation, allow_symbols=False, stop_on_error=True - ) -> Optional[dict]: + ) -> dict | None: """ Build a dictionary of options from an expression. For example, Symbol("Integrate").get_option_values(evaluation, allow_symbols=True) @@ -917,7 +910,7 @@ def get_option_values( values = self.flatten_with_respect_to_head(SymbolList).elements else: values = [self] - option_values: dict[str, Union[str, BaseElement]] = {} + option_values: dict[str, str | BaseElement] = {} for option in values: symbol_name = option.get_name() if allow_symbols and symbol_name: @@ -946,7 +939,7 @@ def get_option_values( # test/builtin/atomic/test_assignment.py and # ensuring we never use Expression(SymbolList, ...) # for ListExpression. - def get_rules_list(self) -> Optional[list]: + def get_rules_list(self) -> list | None: """ If the expression is of the form {pat1->expr1,... {pat_2,expr2},...} return a (python) list of rules. @@ -992,7 +985,7 @@ def element_order(self) -> tuple: in Monomial's __cmp() for the reverse ordering when comparing symbols in the monomial. """ - exps: dict[tuple[str, str], Union[float, complex]] = {} + exps: dict[tuple[str, str], float | complex] = {} head = self._head if head is SymbolTimes: @@ -1100,7 +1093,7 @@ def is_uncertain_final_definitions(self, definitions) -> bool: def has_form( self, heads: Iterable[Symbol] | Symbol, - *element_counts: Optional[int], + *element_counts: int | None, ) -> bool: """ element_counts: @@ -1239,13 +1232,12 @@ def eval_range(indices): element = elements[index] if not ( element.is_literal or element.has_form(SymbolUnevaluated, 1) - ): - if isinstance(element, EvalMixin): - new_value = element.evaluate(evaluation) - # We need id() because != by itself is too permissive - if id(element) != id(new_value): - recompute_properties = True - elements[index] = new_value + ) and isinstance(element, EvalMixin): + new_value = element.evaluate(evaluation) + # We need id() because != by itself is too permissive + if id(element) != id(new_value): + recompute_properties = True + elements[index] = new_value # @timeit def rest_range(indices): @@ -1255,24 +1247,25 @@ def rest_range(indices): return for index in indices: element = elements[index] - if not element.is_literal and element.has_form( - SymbolEvaluate, 1 + if ( + not element.is_literal + and element.has_form(SymbolEvaluate, 1) + and isinstance(element, EvalMixin) ): - if isinstance(element, EvalMixin): - new_value = element.evaluate(evaluation) - # We need id() because != by itself is too permissive - if id(new_value) != id(element): - elements[index] = new_value - recompute_properties = True + new_value = element.evaluate(evaluation) + # We need id() because != by itself is too permissive + if id(new_value) != id(element): + elements[index] = new_value + recompute_properties = True if (A_HOLD_ALL | A_HOLD_ALL_COMPLETE) & attributes: # eval_range(range(0, 0)) rest_range(range(len(elements))) elif A_HOLD_FIRST & attributes: - rest_range(range(0, min(1, len(elements)))) + rest_range(range(min(1, len(elements)))) eval_range(range(1, len(elements))) elif A_HOLD_REST & attributes: - eval_range(range(0, min(1, len(elements)))) + eval_range(range(min(1, len(elements)))) rest_range(range(1, len(elements))) else: eval_range(range(len(elements))) @@ -1558,14 +1551,14 @@ def rules(): def round_to_float( self, evaluation=None, permit_complex=False - ) -> Optional[Union[float, complex]]: + ) -> float | complex | None: """ Round to a Python float. Return None if rounding is not possible. This can happen if self or evaluation is NaN. """ if evaluation is None: - value: Optional[BaseElement] = self + value: BaseElement | None = self elif isinstance(evaluation, sympy.core.numbers.NaN): return None else: @@ -1665,7 +1658,7 @@ def to_python(self, *args, **kwargs) -> Any: # If self.value is legitimately None, we'll # catch it further down. if hasattr(self, "value") and self.value is not None: - if self.value == tuple() and kwargs.get("prefer_dictionary"): + if self.value == () and kwargs.get("prefer_dictionary"): return {} return self.value @@ -2105,7 +2098,7 @@ def construct(elements): # Note: this function is called a *lot* so it needs to be fast. def convert_expression_elements( elements: Iterable, conversion_fn: Callable = from_python, is_uniform: bool = True -) -> tuple[tuple, ElementsProperties, Optional[tuple]]: +) -> tuple[tuple, ElementsProperties, tuple | None]: """ Convert and return a tuple of Elements from the Python-like items in `elements`, along with elements properties of the elements tuple, diff --git a/mathics/eval/intfns/divlike.py b/mathics/eval/intfns/divlike.py index ed733fb7a..af606f548 100644 --- a/mathics/eval/intfns/divlike.py +++ b/mathics/eval/intfns/divlike.py @@ -7,7 +7,7 @@ import sympy import mathics.eval.tracing as tracing # noqa -from mathics.core.atoms import Complex, Integer, Integer0 +from mathics.core.atoms import Complex, Integer, Integer0, Integer1 from mathics.core.convert.expression import to_mathics_list from mathics.core.convert.python import from_python from mathics.core.convert.sympy import from_sympy @@ -44,7 +44,9 @@ def combine_integer_arguments(builtin, ns, eval_fn: Callable) -> Expression | No return Expression(Symbol(builtin.get_name()), combined, *others) -def eval_ExtendedGCD(arguments: tuple) -> Integer | None: +def eval_ExtendedGCD(arguments: tuple | Integer) -> Integer | None: + if isinstance(arguments, Integer): + return ListExpression(arguments, ListExpression(Integer1)) integers: list[int] = [] for element in arguments.elements: if isinstance(element, Integer): From 180656b100e0b3963db7f75ef58fcf194f871582 Mon Sep 17 00:00:00 2001 From: rocky Date: Thu, 29 Oct 2026 20:02:30 -0400 Subject: [PATCH 07/12] isort and black --- mathics/core/expression.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/mathics/core/expression.py b/mathics/core/expression.py index 539918854..350fb9553 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -370,14 +370,9 @@ class Expression(BaseElement, NumericOperators, EvalMixin): _sequences: Any _cache: ExpressionCache | None # (head, elements, element_order) of the last element_order built. - _element_order_cache: Optional[tuple[BaseElement, tuple[BaseElement, ...], tuple]] + _element_order_cache: tuple[BaseElement, tuple[BaseElement, ...], tuple] | None elements_properties: ElementsProperties | None options: dict[str, Any] | None -======= - _cache: Optional[ExpressionCache] - elements_properties: Optional[ElementsProperties] - options: Optional[dict[str, Any]] ->>>>>>> 6d9e969d091d8c621bf7c610c99df95299e582c2 pattern_sequence: bool location: SourceRange | SourceRange2 | MethodType | None From 48076f33d770516341d7797cd39a1a4ff57b3d04 Mon Sep 17 00:00:00 2001 From: "R. Bernstein" Date: Fri, 30 Oct 2026 05:05:18 -0400 Subject: [PATCH 08/12] Update mathics/builtin/intfns/divlike.py Co-authored-by: tombouctou --- mathics/builtin/intfns/divlike.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/builtin/intfns/divlike.py b/mathics/builtin/intfns/divlike.py index 5598dcf9c..383981d57 100644 --- a/mathics/builtin/intfns/divlike.py +++ b/mathics/builtin/intfns/divlike.py @@ -143,7 +143,7 @@ class ExtendedGCD(Builtin): = {{1, {2, -1}}, {3, {1, 0}}} """ - attributes = A_FLAT | A_LISTABLE | A_ONE_IDENTITY | A_ORDERLESS | A_PROTECTED + attributes = A_LISTABLE | A_PROTECTED eval_error = Builtin.generic_argument_error expected_args = range(1, sys.maxsize) sympy_name = "gcdex" From 069fede544f46a9c11efdf7601a788bf6492f8c9 Mon Sep 17 00:00:00 2001 From: rocky Date: Fri, 30 Oct 2026 05:44:43 -0400 Subject: [PATCH 09/12] Try to address @tombouctou execellent review --- mathics/builtin/intfns/divlike.py | 32 +++++++++++++++++++++---------- mathics/core/atoms/__init__.py | 7 +++---- mathics/core/expression.py | 2 +- mathics/eval/intfns/divlike.py | 14 +++++++++----- 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/mathics/builtin/intfns/divlike.py b/mathics/builtin/intfns/divlike.py index 383981d57..7f9a45a4b 100644 --- a/mathics/builtin/intfns/divlike.py +++ b/mathics/builtin/intfns/divlike.py @@ -110,7 +110,7 @@ class ExtendedGCD(Builtin): https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm ( :SymPy:https://docs.sympy.org/latest/modules/polys/domainsref.html#sympy.polys.domains.IntegerRing.gcdex, :WMA: - https://reference.wolfram.com/language/ref/GCD.html) + https://reference.wolfram.com/language/ref/ExtendedGCD.html)
'ExtendedGCD'[$n_1$, $n_2$, ...] @@ -133,14 +133,21 @@ class ExtendedGCD(Builtin): >> 6 a + 15 b + 30 c == g = True + ExtendedGCD[3, {5, 15}] theads element-wise over lists: + >> ExtendedGCD[3, {5, 15}] + = {{1, {2, -1}}, {3, {1, 0}}} + When one integer argument is given, the answer degenerates to a GCD of the number with coefficient 1: >> ExtendedGCD[5] = {5, {1}} + And for a single negative integer, the coefficient is -1: + >> ExtendedGCD[-5] + = {-5, {-1}} - ExtendedGCD[3, {5, 15}] theads element-wise over lists: - >> ExtendedGCD[3, {5, 15}] - = {{1, {2, -1}}, {3, {1, 0}}} + See also + :'GCD': + /doc/reference-of-built-in-symbols/integer-functions/division-related-functions/gcd/. """ attributes = A_LISTABLE | A_PROTECTED @@ -151,6 +158,8 @@ class ExtendedGCD(Builtin): def eval(self, ns, evaluation: Evaluation) -> Expression | None: "ExtendedGCD[ns__]" + if not isinstance(ns, (Integer, Expression)): + return return eval_ExtendedGCD(ns) @@ -177,6 +186,10 @@ class GCD(Builtin): = {2, 1, 4, 1, 2} 'GCD' does not work for rational numbers and Gaussian integers yet. + + See also + :'ExtendedGCD': + /doc/reference-of-built-in-symbols/integer-functions/division-related-functions/extendedgcd/. """ attributes = A_FLAT | A_LISTABLE | A_ONE_IDENTITY | A_ORDERLESS | A_PROTECTED @@ -333,12 +346,11 @@ def eval(self, k: Integer, n: Integer, evaluation: Evaluation) -> Integer | None class PowerMod(Builtin): """ :Modular exponentiation: - https://en.wikipedia.org/wiki/Modular_exponentiation (:SymPy: - :mod_inverse:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.mod_inverse, - :nth_root_mod:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.nth_root, - :sqrt_mod:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.sqrt_mod, and - :Pow:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.Pow; :WMA link: - https://reference.wolfram.com/language/ref/PowerMod.html. + https://en.wikipedia.org/wiki/Modular_exponentiation (:SymPy mod_inverse:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.mod_inverse, + :SymPy nth_root_mod:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.nth_root, + :SymPy sqrt_mod:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.sqrt_mod, + :SymPy Pow:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.Pow; :WMA: + https://reference.wolfram.com/language/ref/PowerMod.html).
'PowerMod'[$x$, $y$, $m$] diff --git a/mathics/core/atoms/__init__.py b/mathics/core/atoms/__init__.py index 2b2354d7d..de5ba1c49 100644 --- a/mathics/core/atoms/__init__.py +++ b/mathics/core/atoms/__init__.py @@ -36,23 +36,22 @@ from mathics.core.atoms.strings import String, StringFromPython __all__: Final[list[str]] = [ - "ByteArray", "COMPARE_PREC", - "Complex", "FP_MANTISA_BINARY_DIGITS", "MATHICS3_COMPLEX_I", "MATHICS3_COMPLEX_I_NEG", "NUMERICAL_CONSTANTS", + "ByteArray", + "Complex", "Integer", "Integer0", "Integer1", "Integer2", "Integer3", "Integer4", - "Integer310", "Integer10", + "Integer310", "IntegerM1", - "Integer1", "MachineReal", "MachineReal0", "MachineReal1", diff --git a/mathics/core/expression.py b/mathics/core/expression.py index 350fb9553..0b2df62c4 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -1048,7 +1048,7 @@ def _build_element_order(self) -> tuple: in Monomial's __cmp() for the reverse ordering when comparing symbols in the monomial. """ - exps: dict[tuple[str, str], float | complex] = {} + exps: dict head = self._head numeric = is_numeric(self) diff --git a/mathics/eval/intfns/divlike.py b/mathics/eval/intfns/divlike.py index d8caf2093..e812beca4 100644 --- a/mathics/eval/intfns/divlike.py +++ b/mathics/eval/intfns/divlike.py @@ -1,4 +1,4 @@ -b""" +""" Mathics3 builtins from mathics.builtin.intfns.divlike """ @@ -7,7 +7,7 @@ import sympy import mathics.eval.tracing as tracing # noqa -from mathics.core.atoms import Complex, Integer, Integer0, Integer1 +from mathics.core.atoms import Complex, Integer, Integer0, Integer1, IntegerM1 from mathics.core.convert.expression import to_mathics_list from mathics.core.convert.python import from_python from mathics.core.convert.sympy import from_sympy @@ -44,9 +44,13 @@ def combine_integer_arguments(builtin, ns, eval_fn: Callable) -> Expression | No return Expression(Symbol(builtin.get_name()), combined, *others) -def eval_ExtendedGCD(arguments: tuple | Integer) -> Integer | None: +def eval_ExtendedGCD(arguments: Expression | Integer) -> ListExpression: if isinstance(arguments, Integer): - return ListExpression(arguments, ListExpression(Integer1)) + return ( + ListExpression(arguments, ListExpression(Integer1)) + if arguments.value > 0 + else ListExpression(arguments, ListExpression(IntegerM1)) + ) integers: list[int] = [] for element in arguments.elements: if isinstance(element, Integer): @@ -158,7 +162,7 @@ def eval_Quotient(m, n, d) -> Integer: return Integer((m - d) // n) -def gcdex_list(numbers: list[int]) -> (list, int): +def gcdex_list(numbers: list[int]) -> tuple[list[int], int]: """ Return the extended GCD's coefficients and cumulative GCD for a list of numbers using SymPy's gcdex. From c6e77f3da2a80a35e8c8941e9370bbf3d06c4eee Mon Sep 17 00:00:00 2001 From: rocky Date: Fri, 30 Oct 2026 05:48:00 -0400 Subject: [PATCH 10/12] One more link correction --- mathics/builtin/intfns/divlike.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/builtin/intfns/divlike.py b/mathics/builtin/intfns/divlike.py index 7f9a45a4b..09c7184d4 100644 --- a/mathics/builtin/intfns/divlike.py +++ b/mathics/builtin/intfns/divlike.py @@ -167,7 +167,7 @@ class GCD(Builtin): """ :Greatest common divisor: https://en.wikipedia.org/wiki/Greatest_common_divisor ( - :SymPy:https://docs.sympy.org/latest/modules/polys/domainsref.html#sympy.polys.domains.IntegerRing.gcdex, + :SymPy:https://docs.sympy.org/latest/modules/polys/domainsref.html#sympy.polys.domains.IntegerRing.gcd, :WMA: https://reference.wolfram.com/language/ref/GCD.html) From 703897b539618ecd2a6bf26c22dcb0ce3ebc71b6 Mon Sep 17 00:00:00 2001 From: rocky Date: Fri, 30 Oct 2026 05:53:10 -0400 Subject: [PATCH 11/12] revert Expression.__repr__ change since I am not confident how to fix --- mathics/core/expression.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mathics/core/expression.py b/mathics/core/expression.py index 0b2df62c4..6a3ad919f 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -412,7 +412,8 @@ def __hash__(self): return hash(("Expression", self._head) + tuple(self._elements)) def __repr__(self) -> str: - return f"" % ( + return "" % ( + repr(self.head), ", ".join([repr(element) for element in self.elements]), ) From 6846d3acaa056617adb9b06b3a5404e8796deabe Mon Sep 17 00:00:00 2001 From: rocky Date: Fri, 30 Oct 2026 06:45:14 -0400 Subject: [PATCH 12/12] More corrections from tombouctou --- mathics/builtin/intfns/divlike.py | 12 ++++++++---- mathics/core/expression.py | 11 ++++------- mathics/eval/intfns/divlike.py | 7 +++---- test/builtin/intfns/test_divlike.py | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/mathics/builtin/intfns/divlike.py b/mathics/builtin/intfns/divlike.py index 09c7184d4..0e4d70773 100644 --- a/mathics/builtin/intfns/divlike.py +++ b/mathics/builtin/intfns/divlike.py @@ -121,16 +121,16 @@ class ExtendedGCD(Builtin): >> {gcd, {a, b}} = ExtendedGCD[2, 3] = {1, {-1, 1}} - Now reconstruct the gcd using just the coefficients and original numbers and check it matches 'gcd': + Now reconstruct the GCD using just the coefficients and original numbers and check it matches 'gcd': >> 2 a + 3 b == gcd = True Get the GCD and coefficients for the GCD of 6, 15, and 30: - >> {g, {a, b, c}} = ExtendedGCD[6, 15, 30] + >> {gcd, {a, b, c}} = ExtendedGCD[6, 15, 30] = {3, {-2, 1, 0}} Again, reconstruct the gcd using just the coefficients and original numbers and check it matches 'gcd': - >> 6 a + 15 b + 30 c == g + >> 6 a + 15 b + 30 c == gcd = True ExtendedGCD[3, {5, 15}] theads element-wise over lists: @@ -143,7 +143,11 @@ class ExtendedGCD(Builtin): And for a single negative integer, the coefficient is -1: >> ExtendedGCD[-5] - = {-5, {-1}} + = {5, {-1}} + + The Extended GCD of 0 gives coefficient 0: + >> ExtendedGCD[0] + = {0, {0}} See also :'GCD': diff --git a/mathics/core/expression.py b/mathics/core/expression.py index 6a3ad919f..91eb1684f 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -412,15 +412,12 @@ def __hash__(self): return hash(("Expression", self._head) + tuple(self._elements)) def __repr__(self) -> str: - return "" % ( - repr(self.head), - ", ".join([repr(element) for element in self.elements]), - ) + elements = ", ".join(repr(element) for element in self.elements) + return f"" def __str__(self) -> str: - return f"{self.head}[%s]" % ( - ", ".join([str(element) for element in self.elements]), - ) + elements = ", ".join(str(element) for element in self.elements) + return f"{self.head}[{elements}]" def _as_sympy_function(self, **kwargs): function_name = sympy_name(self.head) diff --git a/mathics/eval/intfns/divlike.py b/mathics/eval/intfns/divlike.py index e812beca4..1e6bbe5e2 100644 --- a/mathics/eval/intfns/divlike.py +++ b/mathics/eval/intfns/divlike.py @@ -46,10 +46,9 @@ def combine_integer_arguments(builtin, ns, eval_fn: Callable) -> Expression | No def eval_ExtendedGCD(arguments: Expression | Integer) -> ListExpression: if isinstance(arguments, Integer): - return ( - ListExpression(arguments, ListExpression(Integer1)) - if arguments.value > 0 - else ListExpression(arguments, ListExpression(IntegerM1)) + n = arguments.value + return ListExpression( + Integer(abs(n)), ListExpression(Integer((n > 0) - (n < 0))) ) integers: list[int] = [] for element in arguments.elements: diff --git a/test/builtin/intfns/test_divlike.py b/test/builtin/intfns/test_divlike.py index 9fe651572..b5bf886a1 100644 --- a/test/builtin/intfns/test_divlike.py +++ b/test/builtin/intfns/test_divlike.py @@ -107,7 +107,7 @@ ], ) def test_divlike(str_expr, msgs, str_expected, fail_msg): - """ """ + """tests functions Quotient, LCM, GCD""" check_evaluation( str_expr, str_expected,