from collections import abc
from ....common.graft import client
from ...cereal import serializable
from ..core import (
ProxyTypeError,
GenericProxytype,
typecheck_promote,
assert_is_proxytype,
merge_params,
)
from ..primitives import Str, Bool, Int
from .list_ import List
from .tuple_ import Tuple
class BaseDict(GenericProxytype):
@property
def key_type(self):
"Type of the keys in the dictionary"
return self._type_params[-2]
@property
def value_type(self):
"Type of the values in the dictionary"
return self._type_params[-1]
def _promote_key(self, key):
try:
return self.key_type._promote(key)
except ProxyTypeError:
raise ProxyTypeError(
"Dict keys are of type {}, but indexed with {!r}".format(
self.key_type.__name__, key
)
)
def _promote_default(self, default):
try:
return self.value_type._promote(default)
except ProxyTypeError:
raise ProxyTypeError(
"The `default` must be the same type as the Dict values."
" Expected something of type {}, but got type {}.".format(
self.value_type.__name__, type(default)
)
)
def __getitem__(self, key):
return self.value_type._from_apply("wf.get", self, self._promote_key(key))
def get(self, key, default):
"""
Return the value for `key` if `key` is in the dictionary, else `default`.
Parameters
----------
key:
Key to look up. Must be the same type as `key_type`.
default:
Value returned if `key` does not exist. Must be the same type as `value_type`.
Example
-------
>>> from descarteslabs.workflows import Dict, Str, Int
>>> my_dict = Dict[Str, Int]({"foo": 1, "bar": 2, "baz": 3})
>>> my_dict.get("baz").compute() # doctest: +SKIP
3
"""
return self.value_type._from_apply(
"wf.get", self, self._promote_key(key), self._promote_default(default)
)
def __iter__(self):
raise TypeError(
"Proxy {} is not iterable. Consider .keys().map(...) "
"to achieve something similar.".format(type(self).__name__)
)
@typecheck_promote(lambda self: self.key_type)
def contains(self, key):
"""
Whether the dictionary contains the given key.
Parameters
----------
key:
Key to look up. Must be the same type as ``self.key_type``.
Returns
-------
Bool
Example
-------
>>> from descarteslabs.workflows import Dict, Str, Int
>>> my_dict = Dict[Str, Int]({"foo": 1, "bar": 2, "baz": 3})
>>> my_dict.contains("foo").compute() # doctest: +SKIP
True
>>> my_dict.contains("hello").compute() # doctest: +SKIP
False
"""
return Bool._from_apply("wf.contains", self, key)
def length(self):
"""
The number of items in the dictionary
Returns
-------
Int
Example
-------
>>> from descarteslabs.workflows import Dict, Str, Int
>>> my_dict = Dict[Str, Int]({"foo": 1, "bar": 2, "baz": 3})
>>> my_dict.length().compute() # doctest: +SKIP
3
"""
return Int._from_apply("wf.length", self)
def keys(self):
"""
List of all the dictionary keys.
Returns
-------
List
Example
-------
>>> from descarteslabs.workflows import Dict, Str, Int
>>> my_dict = Dict[Str, Int]({"foo": 1, "bar": 2, "baz": 3})
>>> my_dict.keys().compute() # doctest: +SKIP
['foo', 'bar', 'baz']
"""
return List[self.key_type]._from_apply("wf.dict.keys", self)
def values(self):
"""
List of all the dictionary values.
Returns
-------
List
Example
-------
>>> from descarteslabs.workflows import Dict, Str, Int
>>> my_dict = Dict[Str, Int]({"foo": 1, "bar": 2, "baz": 3})
>>> my_dict.values().compute() # doctest: +SKIP
[1, 2, 3]
"""
return List[self.value_type]._from_apply("wf.dict.values", self)
def items(self):
"""
List of tuples of key-value pairs in the dictionary.
Returns
-------
List[Tuple[KeyType, ValueType]]
Example
-------
>>> from descarteslabs.workflows import Dict, Str, Int
>>> my_dict = Dict[Str, Int]({"foo": 1, "bar": 2, "baz": 3})
>>> my_dict.items().compute() # doctest: +SKIP
[('foo', 1), ('bar', 2), ('baz', 3)]
"""
return List[Tuple[self.key_type, self.value_type]]._from_apply(
"wf.dict.items", self
)
[docs]@serializable()
class Dict(BaseDict):
"""
``Dict[KeyType, ValueType]``: Proxy mapping, from keys of a specific type to values of a specific type.
Can be instantiated from a Python mapping and/or keyword arguments.
NOTE: Proxy types are not hashable and thus can not be used inside regular old Python dicts. In general,
prefer the `Dict.from_pairs` constructor when dealing with proxy types directly.
Examples
--------
>>> from descarteslabs.workflows import Dict, List, Str, Int, Float
>>> Dict[Str, Int](a=1, b=2) # dict of Str to Int
<descarteslabs.workflows.types.containers.dict_.Dict[Str, Int] object at 0x...>
>>> Dict[Str, Int]({'a': 1, 'b': 2}, b=100, c=3) # dict of Str to Int
<descarteslabs.workflows.types.containers.dict_.Dict[Str, Int] object at 0x...>
>>> Dict[Str, List[Float]](a=[1.1, 2.2], b=[3.3]) # dict of Str to List of Floats
<descarteslabs.workflows.types.containers.dict_.Dict[Str, List[Float]] object at 0x...>
>>> from descarteslabs.workflows import Dict, Str, Float
>>> my_dict = Dict[Str, Float]({"red": 100.5, "blue": 67.6})
>>> my_dict
<descarteslabs.workflows.types.containers.dict_.Dict[Str, Float] object at 0x...>
>>> my_dict.compute() # doctest: +SKIP
{"red": 100.5, "blue": 67.6}
>>> my_dict.keys().compute() # doctest: +SKIP
['red', 'blue']
>>> my_dict["red"].compute() # doctest: +SKIP
100.5
"""
def __init__(self, *dct, **kwargs):
if self._type_params is None:
raise TypeError(
"Cannot instantiate a generic Dict; the key and value types must be specified (like `Dict[Str, Bool]`)"
)
if len(dct) > 1:
raise TypeError(
"Dict expected at most 1 arguments, got {}".format(len(dct))
)
if len(dct) == 0:
dct = kwargs
kwargs = {}
else:
dct = dct[0]
kt, vt = self._type_params
if isinstance(dct, BaseDict):
other_kt, other_vt = dct.key_type, dct.value_type
if not (issubclass(other_kt, kt) and issubclass(other_vt, vt)):
raise ProxyTypeError(
"Cannot convert {} to {}, their element types are different".format(
type(dct).__name__, type(self).__name__
)
)
self.graft = dct.graft
self.params = dct.params
if len(kwargs) > 0:
raise NotImplementedError(
"Don't have key merging onto a proxy dict yet."
)
else:
if not isinstance(dct, abc.Mapping):
raise ProxyTypeError("Expected a mapping, got {}".format(dct))
dct = dct.copy()
dct.update(kwargs)
# TODO(gabe): numer of copies of source dict could definitely be reduced here
is_str_dict = issubclass(kt, Str)
promoted = {} if is_str_dict else []
for key, val in dct.items():
try:
promoted_key = kt._promote(key)
except ProxyTypeError:
raise ProxyTypeError(
"Expected Dict keys of type {}, but got {}".format(kt, key)
)
try:
promoted_val = vt._promote(val)
except ProxyTypeError:
raise ProxyTypeError(
"Expected Dict values of type {}, but got {}".format(vt, val)
)
if is_str_dict:
promoted[key] = promoted_val
# note we use the unpromoted key, which should be a string
# this is an optimization that produces a cleaner graph for the case of string-keyed dicts
# FIXME this logic would break on Str proxytype keys, if proxytypes every become hashable
else:
promoted += [promoted_key, promoted_val]
# for non-string dicts, we just give varargs of key, value, key, value, ...
# since that's a much simpler graft representation than constructing a list
# of tuples
if is_str_dict:
self.graft = client.apply_graft("wf.dict.create", **promoted)
self.params = merge_params(*dct.values())
else:
self.graft = client.apply_graft("wf.dict.create", *promoted)
self.params = merge_params(*(x for kv in dct.items() for x in kv))
@classmethod
def _validate_params(cls, type_params):
assert len(type_params) == 2, "Both Dict key and value types must be specified"
for i, type_param in enumerate(type_params):
error_message = "Dict key and value types must be Proxytypes but for parameter {}, got {}".format(
i, type_param
)
assert_is_proxytype(type_param, error_message=error_message)
[docs] @classmethod
@typecheck_promote(lambda cls: List[Tuple[cls._type_params]])
def from_pairs(cls, pairs):
"""
Construct a Dict from a list of key-value pairs.
Parameters
----------
List[Tuple]
Returns
-------
Dict
Example
-------
>>> from descarteslabs.workflows import Dict, Str, Int
>>> pairs = [("foo", 1), ("bar", 2), ("baz", 3)]
>>> my_dict = Dict[Str, Int].from_pairs(pairs)
>>> my_dict
<descarteslabs.workflows.types.containers.dict_.Dict[Str, Int] object at 0x...>
>>> my_dict.compute() # doctest: +SKIP
{'foo': 1, 'bar': 2, 'baz': 3}
"""
return cls._from_apply("wf.dict.from_pairs", pairs)