diff --git a/SYMBOLS_MANIFEST.txt b/SYMBOLS_MANIFEST.txt
index 5815b2485..6b3bd147b 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
@@ -634,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..2f09158f6
--- /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/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/core/atoms/associations.py b/mathics/core/atoms/associations.py
index 64d90b69c..3523e9ccc 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
@@ -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.
@@ -42,10 +42,9 @@ 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
+ # Add some dictionary-like methods so that we can treat an Association object
# as we would a dictionary.
def __delitem__(self, key: BaseElement) -> None:
@@ -60,10 +59,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."""
@@ -74,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:
@@ -105,7 +102,15 @@ def __getitem__(self, key: Any) -> Any:
return self.collection[key]
raise KeyError(key)
+ # FIXME: We probably shouldn'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:
@@ -194,11 +199,28 @@ 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()
+ 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.
@@ -221,34 +243,27 @@ 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
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
"""
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)))
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.
"""
diff --git a/mathics/eval/associations/modifying.py b/mathics/eval/associations/modifying.py
new file mode 100644
index 000000000..52b701804
--- /dev/null
+++ b/mathics/eval/associations/modifying.py
@@ -0,0 +1,81 @@
+"""
+Evaluation routines for builtins in mathics.builtin.associations.modifying
+"""
+
+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(
+ a: BaseElement, expr: BaseElement, evaluation: Evaluation
+) -> Optional[Association]:
+ """AssociateTo[a_, expr_]"""
+
+ # 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
+
+ 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 (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
diff --git a/mathics/repl.py b/mathics/repl.py
index a09748a8e..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
@@ -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..99a97a777
--- /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,
+ )