Skip to content

Commit

Permalink
Add module-level factory methods for all collections
Browse files Browse the repository at this point in the history
Closes #12
  • Loading branch information
Ryan Gabbard committed Feb 7, 2019
1 parent d861bd6 commit 21e9a02
Show file tree
Hide file tree
Showing 14 changed files with 436 additions and 260 deletions.
1 change: 0 additions & 1 deletion benchmarks/test_benchmark_immutablelist.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import random
import pytest
from immutablecollections import ImmutableList
from immutablecollections import ImmutableDict, ImmutableList

empty_tuple = ()
Expand Down
50 changes: 36 additions & 14 deletions benchmarks/test_benchmark_immutableset.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,67 @@
import pytest
import random

from immutablecollections import ImmutableDict, ImmutableSet
from immutablecollections import ImmutableDict, immutableset, ImmutableSet

print(type(immutableset))

empty_tuple = ()
empty_list = []
empty_set = set()
empty_frozenset = frozenset()

small_tuple = (3, 1, 2)
small_list = [3, 1, 2]
small_set = set(small_tuple)
small_frozen_set = set(small_list)

rand = random.Random(0)

big_list = list(range(10000))
rand.shuffle(big_list)
big_tuple = tuple(big_list)
big_set = set(big_list)
big_frozen_set = frozenset(big_list)

input_types = ImmutableDict.of(
(
("empty set", empty_set),
("empty frozenset", empty_frozenset),
("empty tuple", empty_tuple),
("empty list", empty_list),
("small set", small_set),
("small frozenset", small_frozen_set),
("small tuple", small_tuple),
("small list", small_list),
("big set", big_set),
("big frozenset", big_frozen_set),
("big tuple", big_tuple),
("big list", big_list),
)
)


def function_call_overhead_no_arg():
pass


def function_call_overhead_arg(iterable):
pass


no_arg_constructors = ImmutableDict.of(
(
("set", set),
("frozenset", frozenset),
("immutableset", immutableset),
("function_call", function_call_overhead_no_arg),
)
)


@pytest.mark.parametrize("constructor", no_arg_constructors.items())
def test_create_empty(constructor, benchmark):
benchmark.name = constructor[0]
benchmark.group = f"Create empty"
benchmark(constructor[1])


constructors = ImmutableDict.of(
(("set", set), ("frozenset", frozenset), ("ImmutableSet", ImmutableSet.of))
(
("set", set),
("frozenset", frozenset),
("ImmutableSet", ImmutableSet.of),
("immutableset", immutableset),
("function_call", function_call_overhead_arg),
)
)


Expand Down
13 changes: 9 additions & 4 deletions immutablecollections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@
from attr.exceptions import FrozenInstanceError

from immutablecollections.immutablecollection import ImmutableCollection
from immutablecollections.immutabledict import ImmutableDict
from immutablecollections.immutablelist import ImmutableList
from immutablecollections.immutableset import ImmutableSet
from immutablecollections.utils import ImmutableMixin
from immutablecollections._immutabledict import immutabledict, ImmutableDict
from immutablecollections._immutablelist import immutablelist, ImmutableList
from immutablecollections._immutableset import immutableset, ImmutableSet
from immutablecollections._immutablemultidict import (
ImmutableSetMultiDict,
ImmutableListMultiDict,
immutablelistmultidict,
immutablesetmultidict,
)

from immutablecollections.version import version as __version__
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from abc import ABCMeta
from typing import (
Dict,
Iterable,
Mapping,
TypeVar,
Tuple,
Iterator,
Optional,
Union,
Generic,
Callable,
Expand All @@ -15,6 +17,7 @@
from frozendict import frozendict

from immutablecollections.immutablecollection import ImmutableCollection
from immutablecollections._utils import DICT_ITERATION_IS_DETERMINISTIC

KT = TypeVar("KT")
VT = TypeVar("VT")
Expand All @@ -27,6 +30,47 @@

SelfType = TypeVar("SelfType") # pylint:disable=invalid-name

AllowableDictType = Union[ # pylint:disable=invalid-name
"ImmutableDict[KT, VT]", Dict[KT, VT]
]


def immutabledict(
iterable: Optional[Union[Iterable[Tuple[KT, VT]], AllowableDictType]] = None
) -> "ImmutableDict[KT, VT]":
"""
Create an immutable dictionary with the given mappings.
Mappings may be specified as a sequence of key-value pairs or as another ``ImmutableDict`` or
(on Python 3.7+ and CPython 3.6+) as a built-in ``dict``.
The iteration order of the created keys, values, and items of the resulting ``ImmutableDict``
will match *iterable*.
If *iterable* is already an ``ImmutableDict``, *iterable* itself will be returned.
"""
# immutabledict() should return an empty set
if iterable is None:
return _EMPTY

if isinstance(iterable, ImmutableDict):
# if an ImmutableDict is input, we can safely just return it,
# since the object can safely be shared
return iterable

if isinstance(iterable, Dict) and not DICT_ITERATION_IS_DETERMINISTIC:
raise ValueError(
"ImmutableDicts can only be initialized from built-in dicts when the "
"iteration of built-in dicts is guaranteed to be deterministic (Python "
"3.7+; CPython 3.6+)"
)

ret: ImmutableDict[KT, VT] = _FrozenDictBackedImmutableDict(iterable)
if ret:
return ret
else:
return _EMPTY


class ImmutableDict(ImmutableCollection[KT], Mapping[KT, VT], metaclass=ABCMeta):
__slots__ = ()
Expand All @@ -35,17 +79,27 @@ class ImmutableDict(ImmutableCollection[KT], Mapping[KT, VT], metaclass=ABCMeta)
# pylint: disable = arguments-differ
@staticmethod
def of(dict_: Union[Mapping[KT, VT], Iterable[IT]]) -> "ImmutableDict[KT, VT]":
"""
Deprecated - prefer ``immutabledict`` module-level factory
"""
if isinstance(dict_, ImmutableDict):
return dict_
else:
return ImmutableDict.builder().put_all(dict_).build() # type:ignore

@staticmethod
def empty() -> "ImmutableDict[KT, VT]":
"""
Deprecated - prefer the ``immutabledict`` module-level factory with no arguments.
"""
return _EMPTY

@staticmethod
def builder() -> "ImmutableDict.Builder[KT, VT]":
"""
Deprecated - prefer to build a list of tuples and pass them to the ``immutabledict``
module-level factory
"""
return ImmutableDict.Builder()

@staticmethod
Expand All @@ -58,12 +112,7 @@ def index(
The result is an `ImmutableDict` where each given item appears as a value which
is mapped to from the result of applying `key_function` to the value.
"""
ret: ImmutableDict.Builder[KT, VT] = ImmutableDict.builder()

for item in items:
ret.put(key_function(item), item)

return ret.build()
return immutabledict((key_function(item), item) for item in items)

def modified_copy_builder(self) -> "ImmutableDict.Builder[KT, VT]":
return ImmutableDict.Builder(source=self)
Expand All @@ -79,14 +128,11 @@ def filter_keys(self, predicate: Callable[[KT], bool]) -> "ImmutableDict[KT, VT]
ImmutableDict in general is updated to maintain order) and allows us not to do any
copying if all keys pass the filter
"""
retained_keys = [key for key in self.keys() if predicate(key)]
if len(retained_keys) == len(self.keys()):
new_items = [item for item in self.items() if predicate(item[0])]
if len(new_items) == len(self):
return self
else:
ret: ImmutableDict.Builder[KT, VT] = ImmutableDict.builder()
for key in retained_keys:
ret.put(key, self[key])
return ret.build()
return immutabledict(new_items)

def __repr__(self):
return "i" + str(self)
Expand Down Expand Up @@ -144,13 +190,13 @@ def build(self) -> "ImmutableDict[KT2, VT2]":
# objects can be safely shared
return self.source
if self._dict:
return FrozenDictBackedImmutableDict(self._dict)
return _FrozenDictBackedImmutableDict(self._dict)
else:
return _EMPTY


@attrs(frozen=True, slots=True, repr=False)
class FrozenDictBackedImmutableDict(ImmutableDict[KT, VT]):
class _FrozenDictBackedImmutableDict(ImmutableDict[KT, VT]):

# Mypy does not believe this is a valid converter, but it is
_dict = attrib(converter=frozendict) # type:ignore
Expand All @@ -170,4 +216,4 @@ def __contains__(self, x: object) -> bool:


# Singleton instance for empty
_EMPTY: ImmutableDict = FrozenDictBackedImmutableDict({})
_EMPTY: ImmutableDict = _FrozenDictBackedImmutableDict({})
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Iterable,
Iterator,
List,
Optional,
Sequence,
Sized,
Tuple,
Expand All @@ -20,6 +21,32 @@
T2 = TypeVar("T2")


def immutablelist(iterable: Optional[Iterable[T]] = None) -> "ImmutableList[T]":
"""
Create an immutable list with the given contents.
The iteration order of the created list will match *iterable*. If *iterable* is `None`, an
empty `ImmutableList` will be returned.
If *iterable* is already an ``ImmutableList``, *iterable* itself will be returned.
"""
# immutablelist() should return an empty set
if iterable is None:
return _EMPTY_IMMUTABLE_LIST

if isinstance(iterable, ImmutableList):
# if an ImmutableList is input, we can safely just return it,
# since the object can safely be shared
return iterable

values_as_tuple = tuple(iterable)

if values_as_tuple:
return _TupleBackedImmutableList(iterable)
else:
return _EMPTY_IMMUTABLE_LIST


class ImmutableList(
Generic[T], ImmutableCollection[T], Sequence[T], Iterable[T], metaclass=ABCMeta
):
Expand All @@ -29,20 +56,27 @@ class ImmutableList(
# pylint: disable = arguments-differ
@staticmethod
def of(seq: Iterable[T]) -> "ImmutableList[T]":
"""
Use of this method is deprecated. Prefer the module-level ``immutablelist``.
"""
if isinstance(seq, ImmutableList):
return seq
else:
return ImmutableList.builder().add_all(seq).build() # type: ignore

@staticmethod
def empty() -> "ImmutableList[T]":
return EMPTY_IMMUTABLE_LIST
return _EMPTY_IMMUTABLE_LIST

@staticmethod
def builder() -> "ImmutableList.Builder[T]":
return ImmutableList.Builder()

class Builder(Generic[T2], Sized):
"""
Use of this builder is deprecated. Prefer to pass a regular ``list`` to ``immutablelist``
"""

def __init__(self):
self._list: List[T2] = []

Expand All @@ -61,7 +95,7 @@ def build(self) -> "ImmutableList[T2]":
if self._list:
return _TupleBackedImmutableList(self._list)
else:
return EMPTY_IMMUTABLE_LIST
return _EMPTY_IMMUTABLE_LIST

def __repr__(self):
return "i" + str(self)
Expand Down Expand Up @@ -99,4 +133,4 @@ def __len__(self):


# Singleton instance for empty
EMPTY_IMMUTABLE_LIST: ImmutableList = _TupleBackedImmutableList(())
_EMPTY_IMMUTABLE_LIST: ImmutableList = _TupleBackedImmutableList(())
Loading

0 comments on commit 21e9a02

Please sign in to comment.