Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions SYMBOLS_MANIFEST.txt
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ System`Arrow
System`Arrow3DBox
System`ArrowBox
System`Arrowheads
System`AssociateTo
System`Association
System`AssociationQ
System`Assuming
Expand Down Expand Up @@ -634,6 +635,7 @@ System`KelvinBer
System`KelvinKei
System`KelvinKer
System`Key
System`KeyDropFrom
System`KeyExistsQ
System`Keys
System`Khinchin
Expand Down
90 changes: 90 additions & 0 deletions mathics/builtin/associations/modifying.py
Original file line number Diff line number Diff line change
@@ -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):
"""
<url>
:WMA link:
https://reference.wolfram.com/language/ref/AssociateTo.html</url>

<dl>
<dt>'AssociateTo'[$a$, $key$ -> $val$]
<dd>Adds key-value pair $key$ -> $val$ to $a$.

</dl>

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):
"""
<url>
:WMA link:
https://reference.wolfram.com/language/ref/KeyDropFrom.html</url>

<dl>
<dt>'KeyDropFrom'[$a$, $key$]
<dd>Removes key pair from association $a$.

</dl>

>> 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)
2 changes: 2 additions & 0 deletions mathics/builtin/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
69 changes: 42 additions & 27 deletions mathics/core/atoms/associations.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""
Mathics3 implementation of an Association atom
6"""
Mathics3 implementation of an Association atom.
"""

from typing import Any, Iterable, Optional

Expand All @@ -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.
Expand All @@ -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:
Expand All @@ -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."""
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand All @@ -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)))
2 changes: 1 addition & 1 deletion mathics/eval/associations/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""
Evaluation routines and associated code for Built-in functions found under module
mathics.builtins.assications.
mathics.builtins.associations.
"""
81 changes: 81 additions & 0 deletions mathics/eval/associations/modifying.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions mathics/repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading