From cd6cc03968156344c91fbb96765989f67e3e625a Mon Sep 17 00:00:00 2001 From: rocky Date: Sun, 26 Jul 2026 06:46:35 -0400 Subject: [PATCH 1/8] Add AssociationTo --- SYMBOLS_MANIFEST.txt | 1 + mathics/builtin/messages.py | 2 + mathics/eval/associations/modifying.py | 42 +++++++++++++++++++++ mathics/repl.py | 2 +- test/builtin/associations/test_modifying.py | 39 +++++++++++++++++++ 5 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 mathics/eval/associations/modifying.py create mode 100644 test/builtin/associations/test_modifying.py diff --git a/SYMBOLS_MANIFEST.txt b/SYMBOLS_MANIFEST.txt index 5815b2485..4b5827d00 100644 --- a/SYMBOLS_MANIFEST.txt +++ b/SYMBOLS_MANIFEST.txt @@ -131,6 +131,7 @@ System`Arrow System`Arrow3DBox System`ArrowBox System`Arrowheads +System`AssociateTo System`Association System`AssociationQ System`Assuming diff --git a/mathics/builtin/messages.py b/mathics/builtin/messages.py index 4a729ef02..1ec62af48 100644 --- a/mathics/builtin/messages.py +++ b/mathics/builtin/messages.py @@ -215,6 +215,8 @@ class General(Builtin): "intp": "Positive integer expected.", "intnn": "Non-negative integer expected.", "intnm": "Non-negative machine-sized integer expected at position `1` in `2`.", + "invak": "The argument `1` is not a valid Association.", + "invlb": "The argument `1` is not a list, Rule or Association.", "invrl": "The argument `1` is not a valid Association or a list of rules.", "iterb": "Iterator does not have appropriate bounds.", "ivar": "`1` is not a valid variable.", diff --git a/mathics/eval/associations/modifying.py b/mathics/eval/associations/modifying.py new file mode 100644 index 000000000..491960db6 --- /dev/null +++ b/mathics/eval/associations/modifying.py @@ -0,0 +1,42 @@ +""" +Evaluation routines for builtins in mathics.builtin.associations.modifying +""" + +from typing import Optional + +from mathics.core.atoms.associations import Association +from mathics.core.element import BaseElement +from mathics.core.evaluation import Evaluation +from mathics.core.list import ListExpression +from mathics.core.rules import is_rule +from mathics.core.symbols import Symbol + + +def eval_AssociateTo( + a: BaseElement, expr: BaseElement, evaluation: Evaluation +) -> Optional[Association]: + """AssociateTo[a_, expr_]""" + + # FIXME we should check if a is protected. And readible? And Writeable? + if isinstance(a, Symbol): + assoc_value = a.evaluate(evaluation) + else: + assoc_value = expr + + if not isinstance(assoc_value, Association): + evaluation.message("AssociateTo", "invak", assoc_value) + return + + if is_rule(expr): + changed = Association([expr]) + elif isinstance(expr, ListExpression): + changed = Association(expr.elements) + else: + changed = expr + + if not isinstance(changed, Association): + evaluation.message("AssociateTo", "invlb", expr) + return + + assoc_value.update(changed.collection) + return assoc_value diff --git a/mathics/repl.py b/mathics/repl.py index a09748a8e..9432ab414 100644 --- a/mathics/repl.py +++ b/mathics/repl.py @@ -212,7 +212,7 @@ def get_out_prompt(self, form=None) -> str: """ Return a prompt string to be shown before showing output. """ - line_number = self.last_line_number + line_number = self.last_line_number - 1 if form: return "{3}{0}[{4}{1}{5}]//{2}= {6}".format( self.out_prefix, line_number, form, *self.outcolors diff --git a/test/builtin/associations/test_modifying.py b/test/builtin/associations/test_modifying.py new file mode 100644 index 000000000..011848f12 --- /dev/null +++ b/test/builtin/associations/test_modifying.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +""" +Unit tests for mathics.builtins.associations.modifying +""" +from test.helper import check_evaluation + +import pytest + + +@pytest.mark.parametrize( + ("str_expr", "expected_messages", "str_expected", "assert_message"), + [ + ( + "AssociateTo[1, 1->2]", + ["The argument 1 -> 2 is not a valid Association."], + "AssociateTo[1, 1->2]", + "AssociateTo first parameter should be an association not an Integer.", + ), + ( + "Clear[x]; AssociateTo[x, 1->2]", + ["The argument x is not a valid Association."], + "AssociateTo[x, 1->2]", + "AssociateTo first parameter should have a value.", + ), + ( + "assoc = <|a->1|>; AssociateTo[assoc, 1]", + ["The argument 1 is not a list, Rule or Association."], + "AssociateTo[assoc, 1]", + "AssociateTo first parameter wrong type.", + ), + ], +) +def test_AssociateTo(str_expr, expected_messages, str_expected, assert_message): + check_evaluation( + str_expr, + str_expected, + failure_message=assert_message, + expected_messages=expected_messages, + ) From fca7af3d770fd9e66cd0f957456aa7464d2caee7 Mon Sep 17 00:00:00 2001 From: rocky Date: Sun, 26 Jul 2026 07:47:59 -0400 Subject: [PATCH 2/8] Add KeyDropFrom --- SYMBOLS_MANIFEST.txt | 1 + mathics/builtin/associations/modifying.py | 90 +++++++++++++++++++++++ mathics/core/atoms/associations.py | 21 +++++- mathics/eval/associations/modifying.py | 41 ++++++++++- 4 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 mathics/builtin/associations/modifying.py diff --git a/SYMBOLS_MANIFEST.txt b/SYMBOLS_MANIFEST.txt index 4b5827d00..6b3bd147b 100644 --- a/SYMBOLS_MANIFEST.txt +++ b/SYMBOLS_MANIFEST.txt @@ -635,6 +635,7 @@ System`KelvinBer System`KelvinKei System`KelvinKer System`Key +System`KeyDropFrom System`KeyExistsQ System`Keys System`Khinchin diff --git a/mathics/builtin/associations/modifying.py b/mathics/builtin/associations/modifying.py new file mode 100644 index 000000000..929d06943 --- /dev/null +++ b/mathics/builtin/associations/modifying.py @@ -0,0 +1,90 @@ +""" +Modifying Associations +""" + +from typing import Optional + +from mathics.core.atoms.associations import Association +from mathics.core.attributes import A_HOLD_FIRST, A_PROTECTED +from mathics.core.builtin import Builtin +from mathics.core.element import BaseElement +from mathics.core.evaluation import Evaluation +from mathics.eval.associations.modifying import eval_AssociateTo, eval_KeyDropFrom + + +class AssociateTo(Builtin): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/AssociateTo.html + +
+
'AssociateTo'[$a$, $key$ -> $val$] +
Adds key-value pair $key$ -> $val$ to $a$. + +
+ + Create an association + >> assoc = Association[{a -> 1, b -> 2, c -> 3}] + = <|a -> 1, b -> 2, c -> 3|> + + Change the value associated with key $a$: + >> AssociateTo[assoc, a -> 10] + = <|a -> 10, b -> 2, c -> 3|> + + Add key $d$ rather than change it since it is not already in $a$: + >> AssociateTo[assoc, d->4] + = <|a -> 10, b -> 2, c -> 3, d -> 4|> + + Change and add keys using several keys-value pairs: + >> AssociateTo[assoc, {d -> 5, e-> f}] + = <|a -> 10, b -> 2, c -> 3, d -> 5, e -> f|> + """ + + attributes = A_HOLD_FIRST | A_PROTECTED + eval_error = Builtin.generic_argument_error + expected_args = 2 + + summary_text = "update an Association" + + def eval( + self, a: BaseElement, expr: BaseElement, evaluation: Evaluation + ) -> Optional[Association]: + """AssociateTo[a_, expr_]""" + + return eval_AssociateTo(a, expr, evaluation) + + +class KeyDropFrom(Builtin): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/KeyDropFrom.html + +
+
'KeyDropFrom'[$a$, $key$] +
Removes key pair from association $a$. + +
+ + >> assoc = <|a -> 1, b -> 2|> + = <|a -> 1, b -> 2|> + >> KeyDropFrom[assoc, a] + = <|b -> 2|> + + >> KeyDropFrom[assoc, {b, d}] + = <||> + """ + + attributes = A_HOLD_FIRST | A_PROTECTED + eval_error = Builtin.generic_argument_error + expected_args = 2 + + summary_text = "remove a key from an Association" + + def eval( + self, a: BaseElement, key: BaseElement, evaluation: Evaluation + ) -> Optional[Association]: + """KeyDropFrom[a_, key_]""" + + return eval_KeyDropFrom(a, key, evaluation) diff --git a/mathics/core/atoms/associations.py b/mathics/core/atoms/associations.py index 64d90b69c..286d83118 100644 --- a/mathics/core/atoms/associations.py +++ b/mathics/core/atoms/associations.py @@ -60,10 +60,8 @@ def __delitem__(self, key: BaseElement) -> None: Side effects: Updates self.collection """ - if key not in self.collection: - raise KeyError(key) - del self.collection[key] + self._expr = None def __eq__(self, other: Any) -> bool: """Check equality with another Association.""" @@ -199,6 +197,23 @@ def items(self): """ return self.collection.items() + def pop(self, key: BaseElement) -> BaseElement: + """pops a key-value pair from the association. + + Args: + key: The key to remove. + + Raises: + KeyError: If the key is not found in the association. + + Side effects: + Updates self.collection + """ + popped = self.collection.pop(key, None) + if popped: + self._expr = None + return popped + def sameQ(self, other: Any) -> bool: """ Mathics3 SameQ comparison. diff --git a/mathics/eval/associations/modifying.py b/mathics/eval/associations/modifying.py index 491960db6..52b701804 100644 --- a/mathics/eval/associations/modifying.py +++ b/mathics/eval/associations/modifying.py @@ -5,11 +5,14 @@ from typing import Optional from mathics.core.atoms.associations import Association +from mathics.core.attributes import A_PROTECTED from mathics.core.element import BaseElement from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.rules import is_rule from mathics.core.symbols import Symbol +from mathics.core.systemsymbols import SymbolAssociation def eval_AssociateTo( @@ -17,9 +20,11 @@ def eval_AssociateTo( ) -> Optional[Association]: """AssociateTo[a_, expr_]""" - # FIXME we should check if a is protected. And readible? And Writeable? + # FIXME: we should test for Readable? And Writable? + attributes = 0 if isinstance(a, Symbol): assoc_value = a.evaluate(evaluation) + attributes = evaluation.definitions.get_attributes(a.name) else: assoc_value = expr @@ -34,9 +39,43 @@ def eval_AssociateTo( else: changed = expr + if (attributes & A_PROTECTED) or isinstance(a, Expression): + evaluation.message("Set", "write", SymbolAssociation, a) + return + if not isinstance(changed, Association): evaluation.message("AssociateTo", "invlb", expr) return assoc_value.update(changed.collection) return assoc_value + + +def eval_KeyDropFrom( + a: BaseElement, key: BaseElement, evaluation: Evaluation +) -> Optional[Association]: + """KeyDropFrom[a_, key_]""" + + # FIXME: we should test for Readable? And Writable? + attributes = 0 + if isinstance(a, Symbol): + assoc_value = a.evaluate(evaluation) + attributes = evaluation.definitions.get_attributes(a.name) + else: + assoc_value = a + + if (attributes & A_PROTECTED) or isinstance(a, Expression): + evaluation.message("Set", "write", SymbolAssociation, a) + return + + if not isinstance(assoc_value, Association): + # If it's not an association, we might just return it or handle errors. + return a + + if isinstance(key, ListExpression): + keys = key.elements + else: + keys = [key] + for key in keys: + assoc_value.pop(key) + return assoc_value From 563ba4f4e66d30f4c9a251cbcac3d81aa38ce16e Mon Sep 17 00:00:00 2001 From: rocky Date: Sun, 26 Jul 2026 12:28:17 -0400 Subject: [PATCH 3/8] Some small Association atom cleanup --- mathics/core/atoms/associations.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/mathics/core/atoms/associations.py b/mathics/core/atoms/associations.py index 286d83118..ac9d3a290 100644 --- a/mathics/core/atoms/associations.py +++ b/mathics/core/atoms/associations.py @@ -42,7 +42,6 @@ def __init__(self, elements: Iterable, expr: Optional[Expression] = None): raise TypeError(f"Association keys must be Rules, got {rule_expr}") self.collection[rule_expr.elements[0]] = rule_expr.elements[1] - self.update_for_change() return # Add some dictionary like methods so that we can treat an Association object @@ -103,7 +102,15 @@ def __getitem__(self, key: Any) -> Any: return self.collection[key] raise KeyError(key) + # FIXME: We probably shoudn't have this. + # Find out what needs it and adjust that. def __hash__(self) -> int: + hash_elements = [] + for key, value in self.collection.items(): + # Update hash component + hash_elements.append((hash(key), hash(value))) + + self._hash = hash(("Association", tuple(hash_elements))) return self._hash def __setitem__(self, key: BaseElement, value: BaseElement) -> None: @@ -236,8 +243,13 @@ def sameQ(self, other: Any) -> bool: return True def to_python(self, *args, **kwargs) -> Optional[dict]: - # FIXME - return None + result = {} + for key, value in self.collection.items(): + if hasattr(key, "to_python") and hasattr(value, "to_python"): + result[key.to_python()] = value.to_python() + else: + return None + return result def to_sympy(self, **kwargs): return None @@ -254,16 +266,4 @@ def update(self, e: Iterable): value """ self.collection.update(e) - self.update_for_change() self._expr = None - - def update_for_change(self): - """ - Things we have to do when the Association is changed. - """ - hash_elements = [] - for key, value in self.collection.items(): - # Update hash component - hash_elements.append((hash(key), hash(value))) - - self._hash = hash(("Association", tuple(hash_elements))) From 3c747d5149a8b5f22ac88170cf6a4b64a290b536 Mon Sep 17 00:00:00 2001 From: rocky Date: Sun, 26 Jul 2026 13:07:00 -0400 Subject: [PATCH 4/8] Spelling --- mathics/core/atoms/associations.py | 6 +++--- mathics/eval/associations/__init__.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mathics/core/atoms/associations.py b/mathics/core/atoms/associations.py index ac9d3a290..15d2d87b9 100644 --- a/mathics/core/atoms/associations.py +++ b/mathics/core/atoms/associations.py @@ -1,6 +1,6 @@ """ -Mathics3 implementation of an Association atom -6""" +Mathics3 implementation of an Association atom. +""" from typing import Any, Iterable, Optional @@ -102,7 +102,7 @@ def __getitem__(self, key: Any) -> Any: return self.collection[key] raise KeyError(key) - # FIXME: We probably shoudn't have this. + # FIXME: We probably shouldn't have this. # Find out what needs it and adjust that. def __hash__(self) -> int: hash_elements = [] diff --git a/mathics/eval/associations/__init__.py b/mathics/eval/associations/__init__.py index be91ec16e..6494054e0 100644 --- a/mathics/eval/associations/__init__.py +++ b/mathics/eval/associations/__init__.py @@ -1,4 +1,4 @@ """ Evaluation routines and associated code for Built-in functions found under module -mathics.builtins.assications. +mathics.builtins.associations. """ From be7359072b337c6e474e8cc2b8d528fec6824504 Mon Sep 17 00:00:00 2001 From: "R. Bernstein" Date: Sun, 26 Jul 2026 13:09:37 -0400 Subject: [PATCH 5/8] Fix typos in comments and docstrings --- mathics/core/atoms/associations.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mathics/core/atoms/associations.py b/mathics/core/atoms/associations.py index 15d2d87b9..eb1e50573 100644 --- a/mathics/core/atoms/associations.py +++ b/mathics/core/atoms/associations.py @@ -19,7 +19,7 @@ class Association(Atom, BoxElementMixin): """An Association is an Atom collection that maps keys to values, similar to a Python dictionary. - Each Key-Value mappings of an Association is called a Rule; but + Each key-value mapping of an Association is called a Rule; but this kind of Rule is distinct from (or a degenerate form of) the pattern-matching RewriteRules found in DelayedRule and Set builtins. @@ -44,7 +44,7 @@ def __init__(self, elements: Iterable, expr: Optional[Expression] = None): return - # Add some dictionary like methods so that we can treat an Association object + # Add some dictionary-like methods so that we can treat an Association object # as we would a dictionary. def __delitem__(self, key: BaseElement) -> None: @@ -71,8 +71,8 @@ def __eq__(self, other: Any) -> bool: return False # "other" is an Association that is not literal like us, - # and has the same number items in its collection. - # Here, we have compare key-value pairs + # and has the same number of items in its collection. + # Here, we have to compare key-value pairs. return self.collection == other.collection # If for some reason the above does not work: @@ -255,13 +255,13 @@ def to_sympy(self, **kwargs): return None def values(self): - """Return the values of an the association. + """Return the values of the association. Behaves like dict.values(). """ return self.collection.values() def update(self, e: Iterable): - """Return the values of an the association. + """Return the values of the association. Behaves like dict.update() except we return the update object value """ From 6bd58aa925b6c0a40c22769156535e22c2097478 Mon Sep 17 00:00:00 2001 From: "R. Bernstein" Date: Sun, 26 Jul 2026 13:11:38 -0400 Subject: [PATCH 6/8] Fix comment casing and wording for ANSI sequences --- mathics/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/repl.py b/mathics/repl.py index 9432ab414..6afb0cc00 100644 --- a/mathics/repl.py +++ b/mathics/repl.py @@ -123,7 +123,7 @@ def __init__( except ImportError: pass - # Try importing colorama to escape ansi sequences for cross platform + # Try importing colorama to escape ANSI sequences for cross-platform # colors try: from colorama import init as colorama_init From e874fdd1cd7f6b96d82257e5431be4827b3448db Mon Sep 17 00:00:00 2001 From: "R. Bernstein" Date: Sun, 26 Jul 2026 13:12:14 -0400 Subject: [PATCH 7/8] Fix typo in docstring for items method --- mathics/core/atoms/associations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/core/atoms/associations.py b/mathics/core/atoms/associations.py index eb1e50573..3523e9ccc 100644 --- a/mathics/core/atoms/associations.py +++ b/mathics/core/atoms/associations.py @@ -199,7 +199,7 @@ def head(self) -> Symbol: return SymbolRule def items(self): - """Return the values of an the association. + """Return the values of the association. Behaves like dict.items(). """ return self.collection.items() From 29e1434d34f15ccebc05c2f0f7f7042e1486d06b Mon Sep 17 00:00:00 2001 From: rocky Date: Tue, 28 Jul 2026 20:20:53 -0400 Subject: [PATCH 8/8] Track changes due to Encoding changes --- mathics/builtin/associations/modifying.py | 14 +++++++------- test/builtin/associations/test_modifying.py | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/mathics/builtin/associations/modifying.py b/mathics/builtin/associations/modifying.py index 929d06943..2f09158f6 100644 --- a/mathics/builtin/associations/modifying.py +++ b/mathics/builtin/associations/modifying.py @@ -26,19 +26,19 @@ class AssociateTo(Builtin): Create an association >> assoc = Association[{a -> 1, b -> 2, c -> 3}] - = <|a -> 1, b -> 2, c -> 3|> + = <|a ⇾ 1, b ⇾ 2, c ⇾ 3|> Change the value associated with key $a$: >> AssociateTo[assoc, a -> 10] - = <|a -> 10, b -> 2, c -> 3|> + = <|a ⇾ 10, b ⇾ 2, c ⇾ 3|> Add key $d$ rather than change it since it is not already in $a$: >> AssociateTo[assoc, d->4] - = <|a -> 10, b -> 2, c -> 3, d -> 4|> + = <|a ⇾ 10, b ⇾ 2, c ⇾ 3, d ⇾ 4|> Change and add keys using several keys-value pairs: >> AssociateTo[assoc, {d -> 5, e-> f}] - = <|a -> 10, b -> 2, c -> 3, d -> 5, e -> f|> + = <|a ⇾ 10, b ⇾ 2, c ⇾ 3, d ⇾ 5, e ⇾ f|> """ attributes = A_HOLD_FIRST | A_PROTECTED @@ -67,10 +67,10 @@ class KeyDropFrom(Builtin): - >> assoc = <|a -> 1, b -> 2|> - = <|a -> 1, b -> 2|> + >> assoc = <|a ⇾ 1, b ⇾ 2|> + = <|a ⇾ 1, b ⇾ 2|> >> KeyDropFrom[assoc, a] - = <|b -> 2|> + = <|b ⇾ 2|> >> KeyDropFrom[assoc, {b, d}] = <||> diff --git a/test/builtin/associations/test_modifying.py b/test/builtin/associations/test_modifying.py index 011848f12..99a97a777 100644 --- a/test/builtin/associations/test_modifying.py +++ b/test/builtin/associations/test_modifying.py @@ -11,19 +11,19 @@ ("str_expr", "expected_messages", "str_expected", "assert_message"), [ ( - "AssociateTo[1, 1->2]", - ["The argument 1 -> 2 is not a valid Association."], - "AssociateTo[1, 1->2]", + "AssociateTo[1, 1⇾2]", + ["The argument 1 ⇾ 2 is not a valid Association."], + "AssociateTo[1, 1⇾2]", "AssociateTo first parameter should be an association not an Integer.", ), ( - "Clear[x]; AssociateTo[x, 1->2]", + "Clear[x]; AssociateTo[x, 1⇾2]", ["The argument x is not a valid Association."], - "AssociateTo[x, 1->2]", + "AssociateTo[x, 1⇾2]", "AssociateTo first parameter should have a value.", ), ( - "assoc = <|a->1|>; AssociateTo[assoc, 1]", + "assoc = <|a⇾1|>; AssociateTo[assoc, 1]", ["The argument 1 is not a list, Rule or Association."], "AssociateTo[assoc, 1]", "AssociateTo first parameter wrong type.",