From 120fabc559ac27770f1accda6a3c078657a77621 Mon Sep 17 00:00:00 2001 From: Idan Miara Date: Tue, 14 Feb 2023 12:18:33 +0200 Subject: [PATCH 01/18] .gitignore - add `.python-version`, `build`, `.idea` --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b54cd70..c9940c1 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ dist/ htmlcov .eggs .venv +.python-version +build +.idea \ No newline at end of file From 1f714807db0c45e3ce3e00e023fcefe6124c1162 Mon Sep 17 00:00:00 2001 From: Idan Miara Date: Mon, 13 Feb 2023 10:40:03 +0200 Subject: [PATCH 02/18] .pre-commit-config.yaml added --- .pre-commit-config.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..6d9c14a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +files: 'ordered_set/' +repos: +- repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black +- repo: https://github.com/pycqa/flake8 + rev: 5.0.4 + hooks: + - id: flake8 +- repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + args: ["--profile", "black"] From 71d603083aa50802691b60989c6bd6c636a7820e Mon Sep 17 00:00:00 2001 From: Idan Miara Date: Tue, 14 Feb 2023 12:34:01 +0200 Subject: [PATCH 03/18] requirements-dev.txt - added --- requirements-dev.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 requirements-dev.txt diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..07fa1e8 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +pytest +black +mypy +pre-commit From 7660654635a38c739c940c88d967ea57c57d8863 Mon Sep 17 00:00:00 2001 From: Idan Miara Date: Tue, 14 Feb 2023 12:21:45 +0200 Subject: [PATCH 04/18] setup.py, pyproject.toml, tox.ini - update to drop support for Python 3.7 (Python 3.8 is the new minimum) --- pyproject.toml | 3 +-- setup.py | 2 +- tox.ini | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a15e706..eeaddce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -21,7 +20,7 @@ classifiers = [ "Programming Language :: Python :: Implementation :: PyPy", ] dynamic = ["version", "description"] -requires-python = ">=3.7" +requires-python = ">=3.8" [tool.flit.module] name = "ordered_set" diff --git a/setup.py b/setup.py index c38f2e9..8d5d903 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ 'author_email': 'gh@arborelia.net', 'url': 'https://github.com/rspeer/ordered-set', 'packages': packages, - 'python_requires': '>=3.7', + 'python_requires': '>=3.8', } diff --git a/tox.ini b/tox.ini index ea2f87d..fdcff24 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = pypy3, py37, py38, py39, py310 +envlist = pypy3, py38, py39, py310 [testenv] deps = pytest From 0ed6b3c7927dfea856a16c791c58a5f6f4147e9e Mon Sep 17 00:00:00 2001 From: Idan Miara Date: Tue, 14 Feb 2023 12:15:05 +0200 Subject: [PATCH 05/18] v5.2.0 Major refactor - adding `StableSet` implementation and many features - Added a StableSet implementation, as a base class for OrderedSet. - Added Many functions to OrderedSet, to be more complete and more compatible with other implementations. - popitem(last: bool = True), similar to `dict.popitem` (note minor incompatibility with another implementation (`orderedset`) that have the `last` keyword in the `pop` function) - move_to_end(key), similar to `dict.move_to_end` - __le__, __lt__, __ge__, __gt__ - to improve subset/superset testing - Minimum Python version is 3.8 (because __reversed__) - Fix: OrderedSet.update now raised a TypeError instead of a ValueError when the type of the input is incorrect --- MIT-LICENSE | 2 +- ordered_set/__init__.py | 908 ++++++++++++++++++++++++++++----------- pyproject.toml | 5 +- test/test_ordered_set.py | 4 +- 4 files changed, 654 insertions(+), 265 deletions(-) diff --git a/MIT-LICENSE b/MIT-LICENSE index 02185ee..47f4daf 100644 --- a/MIT-LICENSE +++ b/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012-2022 Elia Robyn Lake +Copyright (c) 2012-2022 Elia Robyn Lake, 2023 Idan Miara Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), diff --git a/ordered_set/__init__.py b/ordered_set/__init__.py index bdd593a..c869c3b 100644 --- a/ordered_set/__init__.py +++ b/ordered_set/__init__.py @@ -1,19 +1,39 @@ -""" -An OrderedSet is a custom MutableSet that remembers its order, so that every -entry has an index that can be looked up. It can also act like a Sequence. +"""A StableSet is a mutable set that remembers its insertion order. +Featuring: Fast O(1) insertion, deletion, iteration and membership testing. +But slow O(N) Index Lookup. -Based on a recipe originally posted to ActiveState Recipes by Raymond Hettiger, -and released under the MIT license. -""" +An OrderedSet is a mutable data structure that is a hybrid of a list and a set. +It remembers its insertion order so that every entry has an index that can be looked up. +Featuring: O(1) Index lookup, insertion, iteration and membership testing. +But slow O(N) Deletion. + +Both have similar interfaces but differ in respect of their implementation and performance. + +OrderedSet Based on a recipe originally posted to ActiveState Recipes by Raymond Hettiger, +and released under the MIT license.""" + +__version__ = "5.2.0" +__description__ = ( + "StableSet and OrderedSet are sets that remembers their order, " + "and allows looking up its items by their index in that order." +) +__long_description__ = __doc__ +__author__ = "Elia Robyn Lake, Idan Miara" +__author_email__ = "gh@arborelia.net, idan@miara.com" +__url__ = "https://github.com/rspeer/ordered-set" +__python_requires__ = ">=3.8" + +import itertools import itertools as it from typing import ( + AbstractSet, Any, Dict, Iterable, Iterator, List, MutableSet, - AbstractSet, + Optional, Sequence, Set, TypeVar, @@ -22,21 +42,19 @@ ) SLICE_ALL = slice(None) -__version__ = "4.1.0" - T = TypeVar("T") # SetLike[T] is either a set of elements of type T, or a sequence, which -# we will convert to an OrderedSet by adding its elements in order. +# we will convert to a StableSet or to an OrderedSet by adding its elements in order. SetLike = Union[AbstractSet[T], Sequence[T]] -OrderedSetInitializer = Union[AbstractSet[T], Sequence[T], Iterable[T]] +SetInitializer = Union[AbstractSet[T], Sequence[T], Iterable[T]] def _is_atomic(obj: object) -> bool: """ Returns True for objects which are iterable but should not be iterated in - the context of indexing an OrderedSet. + the context of indexing a StableSet or an OrderedSet. When we index by an iterable, usually that means we're being asked to look up a list of things. @@ -46,45 +64,61 @@ def _is_atomic(obj: object) -> bool: up, they're the single, atomic thing we're trying to find. As an example, oset.index('hello') should give the index of 'hello' in an - OrderedSet of strings. It shouldn't give the indexes of each individual + StableSet of strings. It shouldn't give the indexes of each individual character. """ return isinstance(obj, (str, tuple)) -class OrderedSet(MutableSet[T], Sequence[T]): +class StableSet(MutableSet[T], Sequence[T]): """ - An OrderedSet is a custom MutableSet that remembers its order, so that - every entry has an index that can be looked up. + A StableSet is a custom MutableSet that remembers its insertion order. + Featuring: Fast O(1) insertion, deletion, iteration and membership testing. + But slow O(N) Index Lookup. + + StableSet is meant to be a drop-in replacement for `set` when iteration in insertion order + is the only additional requirement over the built-in `set`. + + Equality: StableSet, like `set` and `dict_keys` [dict.keys()], and unlike OrderdSet, + disregards the items order when checking equality. + Like `set` it may be equal only to other instances of AbstractSet + (like `set`, `dict_keys` or StableSet). + + This implementation of StableSet is based on the built-in dict type. + In Python 3.6 and later, the built-in dict type is inherently ordered. + If you ignore the dictionary values, that also gives you a simple ordered set, + with fast O(1) insertion, deletion, iteration and membership testing. + However, dict does not provide the list-like random access features of StableSet. + So we have to convert it to a list in O(N) to look up the index of an entry + or look up an entry by its index. Example: - >>> OrderedSet([1, 1, 2, 3, 2]) - OrderedSet([1, 2, 3]) + >>> StableSet([1, 1, 2, 3, 2]) + StableSet([1, 2, 3]) """ - def __init__(self, initial: OrderedSetInitializer[T] = None): - self.items: List[T] = [] - self.map: Dict[T, int] = {} - if initial is not None: - # In terms of duck-typing, the default __ior__ is compatible with - # the types we use, but it doesn't expect all the types we - # support as values for `initial`. - self |= initial # type: ignore + __slots__ = ("_map",) + + _map: Dict[T, Any] + + def __init__(self, initial: Optional[SetInitializer[T]] = None): + self._map = dict.fromkeys(initial) if initial else {} def __len__(self) -> int: """ Returns the number of unique elements in the ordered set Example: - >>> len(OrderedSet([])) + >>> len(StableSet([])) 0 - >>> len(OrderedSet([1, 2])) + >>> len(StableSet([1, 2])) 2 """ - return len(self.items) + # return len(self._map) + return self._map.__len__() @overload - def __getitem__(self, index: slice) -> "OrderedSet[T]": + def __getitem__(self, index: slice) -> "StableSet[T]": ... @overload @@ -101,49 +135,41 @@ def __getitem__(self, index): Get the item at a given index. If `index` is a slice, you will get back that slice of items, as a - new OrderedSet. + new StableSet. If `index` is a list or a similar iterable, you'll get a list of items corresponding to those indices. This is similar to NumPy's - "fancy indexing". The result is not an OrderedSet because you may ask + "fancy indexing". The result is not a StableSet because you may ask for duplicate indices, and the number of elements returned should be the number of elements asked for. Example: - >>> oset = OrderedSet([1, 2, 3]) + >>> oset = StableSet([1, 2, 3]) >>> oset[1] 2 """ - if isinstance(index, slice) and index == SLICE_ALL: + if isinstance(index, int): + try: + return next(itertools.islice(self._map.keys(), index, index + 1)) + except StopIteration: + raise IndexError(f"index {index} out of range") + elif isinstance(index, slice) and index == SLICE_ALL: return self.copy() - elif isinstance(index, Iterable): - return [self.items[i] for i in index] + items = list(self._map.keys()) + if isinstance(index, Iterable): + return [items[i] for i in index] elif isinstance(index, slice) or hasattr(index, "__index__"): - result = self.items[index] + result = items[index] if isinstance(result, list): return self.__class__(result) else: return result else: - raise TypeError("Don't know how to index an OrderedSet by %r" % index) + raise TypeError(f"Don't know how to index a StableSet by {index}") - def copy(self) -> "OrderedSet[T]": - """ - Return a shallow copy of this object. - - Example: - >>> this = OrderedSet([1, 2, 3]) - >>> other = this.copy() - >>> this == other - True - >>> this is other - False - """ - return self.__class__(self) - - # Define the gritty details of how an OrderedSet is serialized as a pickle. + # Define the gritty details of how a StableSet is serialized as a pickle. # We leave off type annotations, because the only code that should interact - # with these is a generalized tool such as pickle. + # with this is a generalized tool such as pickle. def __getstate__(self): if len(self) == 0: # In pickle, the state can't be an empty list. @@ -151,7 +177,7 @@ def __getstate__(self): # # This could have been done more gracefully by always putting the state # in a tuple, but this way is backwards- and forwards- compatible with - # previous versions of OrderedSet. + # previous versions of StableSet. return (None,) else: return list(self) @@ -162,39 +188,152 @@ def __setstate__(self, state): else: self.__init__(state) - def __contains__(self, key: object) -> bool: + def __contains__(self, key: Any) -> bool: """ Test if the item is in this ordered set. Example: - >>> 1 in OrderedSet([1, 3, 2]) + >>> 1 in StableSet([1, 3, 2]) + True + >>> 5 in StableSet([1, 3, 2]) + False + """ + # return key in self._map + return self._map.__contains__(key) + + def __iter__(self) -> Iterator[T]: + """ + Example: + >>> list(iter(StableSet([1, 2, 3]))) + [1, 2, 3] + """ + # return iter(self._map.keys()) + return self._map.keys().__iter__() + + def __reversed__(self) -> Iterator[T]: + """ + Supported from Python >= 3.8 + Example: + >>> list(reversed(StableSet([1, 2, 3]))) + [3, 2, 1] + """ + return reversed(self._map.keys()) + + def __repr__(self) -> str: + if not self: + return f"{self.__class__.__name__}()" + return f"{self.__class__.__name__}({list(self)!r})" + + def __and__(self, other: SetLike[T]) -> "StableSet[T]": + # the parent implementation of this is backwards + return self.intersection(other) + + # sub, or, xor that support ordering + # (left hand and right hand - as the operands order does matter) + # based on the implementations of the super class (Set(Collection)), + # see _collections_abc.py + def __sub__(self, other): + cls = type( + self + if isinstance(self, StableSet) + else other + if isinstance(other, StableSet) + else StableSet + ) + if not isinstance(other, Set): + if not isinstance(other, Iterable): + return NotImplemented + other = cls(other) + return cls(value for value in self if value not in other) + + def __rsub__(self, other): + cls = type( + self + if isinstance(self, StableSet) + else other + if isinstance(other, StableSet) + else StableSet + ) + if not isinstance(other, Set): + if not isinstance(other, Iterable): + return NotImplemented + other = cls(other) + return cls(value for value in other if value not in self) + + def __or__(self, other): + cls = type( + self + if isinstance(self, StableSet) + else other + if isinstance(other, StableSet) + else StableSet + ) + if not isinstance(other, Iterable): + return NotImplemented + chain = (e for s in (self, other) for e in s) + return cls(chain) + + def __ror__(self, other): + cls = type( + self + if isinstance(self, StableSet) + else other + if isinstance(other, StableSet) + else StableSet + ) + if not isinstance(other, Iterable): + return NotImplemented + chain = (e for s in (other, self) for e in s) + return cls(chain) + + def __xor__(self, other): + if not isinstance(other, Iterable): + return NotImplemented + return (self - other) | (other - self) + + def __rxor__(self, other): + if not isinstance(other, Iterable): + return NotImplemented + return (other - self) | (self - other) + + def clear(self) -> None: + """ + Remove all items from this StableSet. + """ + self._map.clear() + + def copy(self) -> "StableSet[T]": + """ + Return a shallow copy of this object. + + Example: + >>> this = StableSet([1, 2, 3]) + >>> other = this.copy() + >>> this == other True - >>> 5 in OrderedSet([1, 3, 2]) + >>> this is other False """ - return key in self.map + return self.__class__(self) # Technically type-incompatible with MutableSet, because we return an # int instead of nothing. This is also one of the things that makes - # OrderedSet convenient to use. + # StableSet convenient to use. def add(self, key: T) -> int: """ - Add `key` as an item to this OrderedSet, then return its index. + Add `key` as an item to this StableSet, then return its index. - If `key` is already in the OrderedSet, return the index it already + If `key` is already in the StableSet, return the index it already had. Example: - >>> oset = OrderedSet() + >>> oset = StableSet() >>> oset.append(3) - 0 >>> print(oset) - OrderedSet([3]) + StableSet([3]) """ - if key not in self.map: - self.map[key] = len(self.items) - self.items.append(key) - return self.map[key] + self._map[key] = None + return len(self._map) - 1 append = add @@ -204,45 +343,46 @@ def update(self, sequence: SetLike[T]) -> int: of the last element inserted. Example: - >>> oset = OrderedSet([1, 2, 3]) + >>> oset = StableSet([1, 2, 3]) >>> oset.update([3, 1, 5, 1, 4]) - 4 >>> print(oset) - OrderedSet([1, 2, 3, 5, 4]) + StableSet([1, 2, 3, 5, 4]) """ - item_index = 0 - try: - for item in sequence: - item_index = self.add(item) - except TypeError: - raise ValueError(f"Argument needs to be an iterable, got {type(sequence}") - return item_index + other_map = dict.fromkeys(sequence) + self._map.update(other_map) + return len(self._map) - 1 @overload - def index(self, key: Sequence[T]) -> List[int]: + def index(self, key: Sequence[T]) -> List[int]: # NOQA ... @overload - def index(self, key: T) -> int: + def index(self, key: T) -> int: # NOQA ... # concrete implementation - def index(self, key): + def index(self, key): # NOQA """ - Get the index of a given entry, raising an IndexError if it's not - present. + Get the index of a given entry, raising an IndexError if it's not present `key` can be an iterable of entries that is not a string, in which case this returns a list of indices. Example: - >>> oset = OrderedSet([1, 2, 3]) + >>> oset = StableSet([1, 2, 3]) >>> oset.index(2) 1 """ - if isinstance(key, Iterable) and not _is_atomic(key): - return [self.index(subkey) for subkey in key] - return self.map[key] + try: + if isinstance(key, Iterable) and not _is_atomic(key): + return [self.index(subkey) for subkey in key] + for index, item in enumerate(self._map.keys()): + if item == key: + return index + raise KeyError(key) + # return list(self._map.keys()).index(key) + except ValueError: + raise KeyError(key) # Provide some compatibility with pd.Index get_loc = index @@ -256,18 +396,42 @@ def pop(self, index: int = -1) -> T: Raises IndexError if index is out of range. Example: - >>> oset = OrderedSet([1, 2, 3]) + >>> oset = StableSet([1, 2, 3]) >>> oset.pop() 3 """ - if not self.items: + if not self._map: raise KeyError("Set is empty") + if index == -1: + elem, _ = self._map.popitem() + return elem + elif index == 0: + elem = next(iter(self._map.keys())) + else: + elem = next(itertools.islice(self._map.keys(), index, index + 1)) + self._map.pop(elem) + return elem - elem = self.items[index] - del self.items[index] - del self.map[elem] + def popitem(self, last: bool = True): + """Remove and return an item from the set. + Items are returned in LIFO order if last is true or FIFO order if false. + """ + if not self._map: + raise KeyError("Set is empty") + if last: + elem, _ = self._map.popitem() + return elem + elem = next(iter(self._map.keys())) + self._map.pop(elem) return elem + def move_to_end(self, key) -> None: + """Move an existing element to the end. + Raise KeyError if the element does not exist. + """ + self._map.pop(key) + self._map[key] = None + def discard(self, key: T) -> None: """ Remove an element. Do not raise an exception if absent. @@ -276,157 +440,171 @@ def discard(self, key: T) -> None: *does* raise an error when asked to remove a non-existent item. Example: - >>> oset = OrderedSet([1, 2, 3]) + >>> oset = StableSet([1, 2, 3]) >>> oset.discard(2) >>> print(oset) - OrderedSet([1, 3]) + StableSet([1, 3]) >>> oset.discard(2) >>> print(oset) - OrderedSet([1, 3]) - """ - if key in self: - i = self.map[key] - del self.items[i] - del self.map[key] - for k, v in self.map.items(): - if v >= i: - self.map[k] = v - 1 - - def clear(self) -> None: - """ - Remove all items from this OrderedSet. - """ - del self.items[:] - self.map.clear() - - def __iter__(self) -> Iterator[T]: - """ - Example: - >>> list(iter(OrderedSet([1, 2, 3]))) - [1, 2, 3] - """ - return iter(self.items) - - def __reversed__(self) -> Iterator[T]: - """ - Example: - >>> list(reversed(OrderedSet([1, 2, 3]))) - [3, 2, 1] - """ - return reversed(self.items) - - def __repr__(self) -> str: - if not self: - return f"{self.__class__.__name__}()" - return f"{self.__class__.__name__}({list(self)!r})" - - def __eq__(self, other: object) -> bool: - """ - Returns true if the containers have the same items. If `other` is a - Sequence, then order is checked, otherwise it is ignored. - - Example: - >>> oset = OrderedSet([1, 3, 2]) - >>> oset == [1, 3, 2] - True - >>> oset == [1, 2, 3] - False - >>> oset == [2, 3] - False - >>> oset == OrderedSet([3, 2, 1]) - False + StableSet([1, 3]) """ - if isinstance(other, Sequence): - # Check that this OrderedSet contains the same elements, in the - # same order, as the other object. - return list(self) == list(other) - try: - other_as_set = set(other) - except TypeError: - # If `other` can't be converted into a set, it's not equal. - return False - else: - return set(self) == other_as_set + # if key in self: + # del self._map[key] + self._map.pop(key, None) - def union(self, *sets: SetLike[T]) -> "OrderedSet[T]": + def union(self, *sets: SetLike[T]) -> "StableSet[T]": """ Combines all unique items. - Each items order is defined by its first appearance. + Each item order is defined by its first appearance. Example: - >>> oset = OrderedSet.union(OrderedSet([3, 1, 4, 1, 5]), [1, 3], [2, 0]) + >>> oset = StableSet.union(StableSet([3, 1, 4, 1, 5]), [1, 3], [2, 0]) >>> print(oset) - OrderedSet([3, 1, 4, 5, 2, 0]) + StableSet([3, 1, 4, 5, 2, 0]) >>> oset.union([8, 9]) - OrderedSet([3, 1, 4, 5, 2, 0, 8, 9]) + StableSet([3, 1, 4, 5, 2, 0, 8, 9]) >>> oset | {10} - OrderedSet([3, 1, 4, 5, 2, 0, 10]) + StableSet([3, 1, 4, 5, 2, 0, 10]) """ - cls: type = OrderedSet - if isinstance(self, OrderedSet): - cls = self.__class__ - containers = map(list, it.chain([self], sets)) + cls = type(self if isinstance(self, StableSet) else StableSet) + containers = map(list, it.chain([self], sets)) # type: ignore items = it.chain.from_iterable(containers) - return cls(items) - - def __and__(self, other: SetLike[T]) -> "OrderedSet[T]": - # the parent implementation of this is backwards - return self.intersection(other) + return cls(items) # type: ignore - def intersection(self, *sets: SetLike[T]) -> "OrderedSet[T]": + def intersection(self, *sets: SetLike[T]) -> "StableSet[T]": """ Returns elements in common between all sets. Order is defined only by the first set. Example: - >>> oset = OrderedSet.intersection(OrderedSet([0, 1, 2, 3]), [1, 2, 3]) + >>> oset = StableSet.intersection(StableSet([0, 1, 2, 3]), [1, 2, 3]) >>> print(oset) - OrderedSet([1, 2, 3]) + StableSet([1, 2, 3]) >>> oset.intersection([2, 4, 5], [1, 2, 3, 4]) - OrderedSet([2]) + StableSet([2]) >>> oset.intersection() - OrderedSet([1, 2, 3]) + StableSet([1, 2, 3]) """ - cls: type = OrderedSet - items: OrderedSetInitializer[T] = self - if isinstance(self, OrderedSet): - cls = self.__class__ + cls = type(self if isinstance(self, StableSet) else StableSet) + items: SetInitializer[T] = self if sets: - common = set.intersection(*map(set, sets)) + common = set.intersection(*map(set, sets)) # type: ignore items = (item for item in self if item in common) return cls(items) - def difference(self, *sets: SetLike[T]) -> "OrderedSet[T]": + def difference(self, *sets: SetLike[T]) -> "StableSet[T]": """ Returns all elements that are in this set but not the others. Example: - >>> OrderedSet([1, 2, 3]).difference(OrderedSet([2])) - OrderedSet([1, 3]) - >>> OrderedSet([1, 2, 3]).difference(OrderedSet([2]), OrderedSet([3])) - OrderedSet([1]) - >>> OrderedSet([1, 2, 3]) - OrderedSet([2]) - OrderedSet([1, 3]) - >>> OrderedSet([1, 2, 3]).difference() - OrderedSet([1, 2, 3]) - """ - cls = self.__class__ - items: OrderedSetInitializer[T] = self + >>> StableSet([1, 2, 3]).difference(StableSet([2])) + StableSet([1, 3]) + >>> StableSet([1, 2, 3]).difference(StableSet([2]), StableSet([3])) + StableSet([1]) + >>> StableSet([1, 2, 3]) - StableSet([2]) + StableSet([1, 3]) + >>> StableSet([1, 2, 3]).difference() + StableSet([1, 2, 3]) + """ + cls = type(self if isinstance(self, StableSet) else StableSet) + items: SetInitializer[T] = self if sets: - other = set.union(*map(set, sets)) + other = set.union(*map(set, sets)) # type: ignore items = (item for item in self if item not in other) return cls(items) + def symmetric_difference(self, other: SetLike[T]) -> "StableSet[T]": + """ + Return the symmetric difference of two StableSets as a new set. + That is, the new set will contain all elements that are in exactly + one of the sets. + + Their order will be preserved, with elements from `self` preceding + elements from `other`. + + Example: + >>> this = StableSet([1, 4, 3, 5, 7]) + >>> other = StableSet([9, 7, 1, 3, 2]) + >>> this.symmetric_difference(other) + StableSet([4, 5, 9, 2]) + """ + cls = type( + self + if isinstance(self, StableSet) + else other + if isinstance(other, StableSet) + else StableSet + ) + diff1 = cls(self).difference(other) + diff2 = cls(other).difference(self) + return diff1.union(diff2) + + def difference_update(self, *sets: SetLike[T]) -> None: + """ + Update this StableSet to remove items from one or more other sets. + + Example: + >>> this = StableSet([1, 2, 3]) + >>> this.difference_update(StableSet([2, 4])) + >>> print(this) + StableSet([1, 3]) + + >>> this = StableSet([1, 2, 3, 4, 5]) + >>> this.difference_update(StableSet([2, 4]), StableSet([1, 4, 6])) + >>> print(this) + StableSet([3, 5]) + """ + items_to_remove = set() # type: Set[T] + for other in sets: + items_as_set = set(other) # type: Set[T] + items_to_remove |= items_as_set + self._map = dict.fromkeys( + [item for item in self._map if item not in items_to_remove] + ) + + def intersection_update(self, other: SetLike[T]) -> None: + """ + Update this StableSet to keep only items in another set, preserving + their order in this set. + + Example: + >>> this = StableSet([1, 4, 3, 5, 7]) + >>> other = StableSet([9, 7, 1, 3, 2]) + >>> this.intersection_update(other) + >>> print(this) + StableSet([1, 3, 7]) + """ + other = set(other) + self._map = dict.fromkeys([item for item in self._map if item in other]) + + def symmetric_difference_update(self, other: SetLike[T]) -> None: + """ + Update this StableSet to remove items from another set, then + add items from the other set that were not present in this set. + + Example: + >>> this = StableSet([1, 4, 3, 5, 7]) + >>> other = StableSet([9, 7, 1, 3, 2]) + >>> this.symmetric_difference_update(other) + >>> print(this) + StableSet([4, 5, 9, 2]) + """ + items_to_add = [item for item in other if item not in self] + items_to_remove = set(other) + self._map = dict.fromkeys( + [item for item in self._map if item not in items_to_remove] + items_to_add + ) + def issubset(self, other: SetLike[T]) -> bool: """ Report whether another set contains this set. Example: - >>> OrderedSet([1, 2, 3]).issubset({1, 2}) + >>> StableSet([1, 2, 3]).issubset({1, 2}) False - >>> OrderedSet([1, 2, 3]).issubset({1, 2, 3, 4}) + >>> StableSet([1, 2, 3]).issubset({1, 2, 3, 4}) True - >>> OrderedSet([1, 2, 3]).issubset({1, 4, 3, 5}) + >>> StableSet([1, 2, 3]).issubset({1, 4, 3, 5}) False """ if len(self) > len(other): # Fast check for obvious cases @@ -438,97 +616,305 @@ def issuperset(self, other: SetLike[T]) -> bool: Report whether this set contains another set. Example: - >>> OrderedSet([1, 2]).issuperset([1, 2, 3]) + >>> StableSet([1, 2]).issuperset([1, 2, 3]) False - >>> OrderedSet([1, 2, 3, 4]).issuperset({1, 2, 3}) + >>> StableSet([1, 2, 3, 4]).issuperset({1, 2, 3}) True - >>> OrderedSet([1, 4, 3, 5]).issuperset({1, 2, 3}) + >>> StableSet([1, 4, 3, 5]).issuperset({1, 2, 3}) False """ if len(self) < len(other): # Fast check for obvious cases return False return all(item in self for item in other) - def symmetric_difference(self, other: SetLike[T]) -> "OrderedSet[T]": + def isorderedsubset(self: SetLike, other: SetLike, non_consecutive: bool = False): + if len(self) > len(other): + return False + if non_consecutive: + i = 0 + self_len = len(self) + for other_item in other: + if other_item == self[i]: + i += 1 + if i == self_len: + return True + return False + else: + for self_item, other_item in zip(self, other): + if not self_item == other_item: + return False + return True + + def isorderedsuperset(self, other: SetLike, non_consecutive: bool = False): + return StableSet.isorderedsubset(other, self, non_consecutive) + + +class StableSetEq(StableSet[T]): + """ + StableSetEq is a StableSet with a modified quality operator. + + StableSetEq, like `set` and `dict_keys` [dict.keys()], and unlike OrderdSet, + disregards the items order when checking equality. + Unlike StableSet, `set`, or `dict_keys` - A StableSetEq can also equal be equal to a Sequence: + `StableSet([1, 2]) == [1, 2]` and `StableSet([1, 2]) == [2, 1]`; but `set([1, 2]) != [1, 2]` + """ + + def __eq__(self, other: Any) -> bool: """ - Return the symmetric difference of two OrderedSets as a new set. - That is, the new set will contain all elements that are in exactly - one of the sets. + Returns true if the containers have the same items. + Items order is ignored. - Their order will be preserved, with elements from `self` preceding - elements from `other`. + Example: + >>> oset = StableSet([1, 3, 2]) + >>> oset == [1, 3, 2] + True + >>> oset == [1, 2, 3] + True + >>> oset == [2, 3] + False + >>> oset == StableSet([3, 2, 1]) + True + """ + + if not isinstance(other, AbstractSet): + try: + other = set(other) + except TypeError: + # If `other` can't be converted into a set, it's not equal. + return False + return self._map.keys() == other + + def __le__(self, other: SetLike[T]): + return len(self) <= len(other) and ( + self._map.keys() <= other + if isinstance(other, AbstractSet) + else self._map.keys() <= set(other) + ) + + def __lt__(self, other: SetLike[T]): + return len(self) < len(other) and ( + self._map.keys() < other + if isinstance(other, AbstractSet) + else self._map.keys() < set(other) + ) + + def __ge__(self, other: SetLike[T]): + return len(self) >= len(other) and ( + self._map.keys() >= other + if isinstance(other, AbstractSet) + else self._map.keys() >= set(other) + ) + + def __gt__(self, other: SetLike[T]): + return len(self) > len(other) and ( + self._map.keys() > other + if isinstance(other, AbstractSet) + else self._map.keys() > set(other) + ) + + +class OrderedSet(StableSet[T]): + """ + An OrderedSet is a mutable data structure that is a hybrid of a list and a set. + It remembers its insertion order so that every entry has an index that can be looked up. + Featuring: O(1) Index lookup, insertion, iteration and membership testing. + But slow O(N) Deletion. + Using OrderedSet over StableSet is advised only if you require fast Index lookup - + Otherwise using StableSet is advised as it is much faster and has a smaller memory footprint. + + In some aspects OrderedSet behaves like a `set` and in other aspects it behaves like a list. + + Equality: OrderedSet, like `list` and `odict_keys` [OrderdDict.keys()], and unlike OrderdSet, + regards the items order when checking equality. + Unlike `set`, An OrderedSet can also equal be equal to a Sequence: + `StableSet([1, 2]) == [1, 2]` and `StableSet([1, 2]) != [2, 1]`; but `set([1, 2]) != [1, 2]` + + The original implementation of OrderedSet was a recipe posted by Raymond Hettiger, + https://code.activestate.com/recipes/576694-orderedset/ + Released under the MIT license. + Hettiger's implementation kept its content in a doubly-linked list referenced by a dict. + As a result, looking up an item by its index was an O(N) operation, while deletion was O(1). + This version makes different trade-offs for the sake of efficient lookups. + Its content is a standard Python list instead of a doubly-linked list. + This provides O(1) lookups by index at the expense of O(N) deletion, + as well as slightly faster iteration. + + Example: + >>> OrderedSet([1, 1, 2, 3, 2]) + OrderedSet([1, 2, 3]) + """ + + __slots__ = ("_items",) + + _items: List[T] + + def __init__(self, initial: Optional[SetInitializer[T]] = None): + self._items = [] + self._map = {} + if initial is not None: + # In terms of duck-typing, the default __ior__ is compatible with + # the types we use, but it doesn't expect all the types we + # support as values for `initial`. + self |= initial # type: ignore + + def __getitem__(self, index): + if isinstance(index, int): + return self._items[index] + elif isinstance(index, slice) and index == SLICE_ALL: + return self.copy() + elif isinstance(index, Iterable): + return [self._items[i] for i in index] + elif isinstance(index, slice) or hasattr(index, "__index__"): + result = self._items[index] + if isinstance(result, list): + return self.__class__(result) + else: + return result + else: + raise TypeError("Don't know how to index an OrderedSet by %r" % index) + + def __eq__(self, other: Any) -> bool: + """ + Returns true if the containers have the same items. + If `other` is a Sequence, then order is checked, otherwise it is ignored. Example: - >>> this = OrderedSet([1, 4, 3, 5, 7]) - >>> other = OrderedSet([9, 7, 1, 3, 2]) - >>> this.symmetric_difference(other) - OrderedSet([4, 5, 9, 2]) + >>> oset = OrderedSet([1, 3, 2]) + >>> oset == [1, 3, 2] + True + >>> oset == [1, 2, 3] + False + >>> oset == [2, 3] + False + >>> oset == OrderedSet([3, 2, 1]) + False """ - cls: type = OrderedSet - if isinstance(self, OrderedSet): - cls = self.__class__ - diff1 = cls(self).difference(other) - diff2 = cls(other).difference(self) - return diff1.union(diff2) + if isinstance(other, Sequence): + # Check that this OrderedSet contains the same elements, in the + # same order, as the other object. + return len(self) == len(other) and self._items == list(other) + try: + other_as_set = set(other) + except TypeError: + # If `other` can't be converted into a set, it's not equal. + return False + else: + return self._map.keys() == other_as_set + + def __le__(self, other: SetLike[T]): + return len(self) <= len(other) and ( + self._map.keys() <= other + if isinstance(other, AbstractSet) + else self._items <= other + if isinstance(other, list) + else self._items <= list(other) + ) + + def __lt__(self, other: SetLike[T]): + return len(self) < len(other) and ( + self._map.keys() < other + if isinstance(other, AbstractSet) + else self._items < other + if isinstance(other, list) + else self._items < list(other) + ) + + def __ge__(self, other: SetLike[T]): + return len(self) >= len(other) and ( + self._map.keys() >= other + if isinstance(other, AbstractSet) + else self._items >= other + if isinstance(other, list) + else self._items >= list(other) + ) + + def __gt__(self, other: SetLike[T]): + return len(self) > len(other) and ( + self._map.keys() > other + if isinstance(other, AbstractSet) + else self._items > other + if isinstance(other, list) + else self._items > list(other) + ) + + def clear(self) -> None: + del self._items[:] + self._map.clear() + + def add(self, key: T) -> int: + if key not in self._map: + self._map[key] = len(self._items) + self._items.append(key) + return self._map[key] + + def update(self, sequence: SetLike[T]) -> int: + item_index = 0 + for item in sequence: + item_index = self.add(item) + return item_index + + def index(self, key): + if isinstance(key, Iterable) and not _is_atomic(key): + return [self.index(subkey) for subkey in key] + return self._map[key] + + def pop(self, index: int = -1) -> T: + if not self._items: + raise KeyError("Set is empty") + elem = self._items[index] + del self._items[index] + del self._map[elem] + return elem + + def popitem(self, last: bool = True): + if not self._items: + raise KeyError("Set is empty") + index = -1 if last else 0 + elem = self._items[index] + del self._items[index] + del self._map[elem] + return elem + + def move_to_end(self, key): + if key in self: + self.discard(key) + self.add(key) + else: + raise KeyError(key) + + def discard(self, key: T) -> None: + if key in self: + i = self._map[key] + del self._items[i] + del self._map[key] + for k, v in self._map.items(): + if v >= i: + self._map[k] = v - 1 def _update_items(self, items: list) -> None: """ Replace the 'items' list of this OrderedSet with a new one, updating - self.map accordingly. + self._map accordingly. """ - self.items = items - self.map = {item: idx for (idx, item) in enumerate(items)} + self._items = items + self._map = {item: idx for (idx, item) in enumerate(items)} def difference_update(self, *sets: SetLike[T]) -> None: - """ - Update this OrderedSet to remove items from one or more other sets. - - Example: - >>> this = OrderedSet([1, 2, 3]) - >>> this.difference_update(OrderedSet([2, 4])) - >>> print(this) - OrderedSet([1, 3]) - - >>> this = OrderedSet([1, 2, 3, 4, 5]) - >>> this.difference_update(OrderedSet([2, 4]), OrderedSet([1, 4, 6])) - >>> print(this) - OrderedSet([3, 5]) - """ items_to_remove = set() # type: Set[T] for other in sets: items_as_set = set(other) # type: Set[T] items_to_remove |= items_as_set - self._update_items([item for item in self.items if item not in items_to_remove]) + self._update_items( + [item for item in self._items if item not in items_to_remove] + ) def intersection_update(self, other: SetLike[T]) -> None: - """ - Update this OrderedSet to keep only items in another set, preserving - their order in this set. - - Example: - >>> this = OrderedSet([1, 4, 3, 5, 7]) - >>> other = OrderedSet([9, 7, 1, 3, 2]) - >>> this.intersection_update(other) - >>> print(this) - OrderedSet([1, 3, 7]) - """ other = set(other) - self._update_items([item for item in self.items if item in other]) + self._update_items([item for item in self._items if item in other]) def symmetric_difference_update(self, other: SetLike[T]) -> None: - """ - Update this OrderedSet to remove items from another set, then - add items from the other set that were not present in this set. - - Example: - >>> this = OrderedSet([1, 4, 3, 5, 7]) - >>> other = OrderedSet([9, 7, 1, 3, 2]) - >>> this.symmetric_difference_update(other) - >>> print(this) - OrderedSet([4, 5, 9, 2]) - """ items_to_add = [item for item in other if item not in self] items_to_remove = set(other) self._update_items( - [item for item in self.items if item not in items_to_remove] + items_to_add + [item for item in self._items if item not in items_to_remove] + items_to_add ) diff --git a/pyproject.toml b/pyproject.toml index eeaddce..64c73f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,10 @@ build-backend = "flit_core.buildapi" [project] name = "ordered-set" -authors = [{name = "Elia Robyn Lake", email = "gh@arborelia.net"}] +authors = [ + {name = "Elia Robyn Lake", email = "gh@arborelia.net"}, + {name = "Idan Miara", email = "idan@miara.com"}, +] readme = "README.md" license = {file = "MIT-LICENSE"} classifiers = [ diff --git a/test/test_ordered_set.py b/test/test_ordered_set.py index 6efe0a9..df30b1f 100644 --- a/test/test_ordered_set.py +++ b/test/test_ordered_set.py @@ -168,9 +168,9 @@ def test_getitem_type_error(): set1["a"] -def test_update_value_error(): +def test_update_type_error(): set1 = OrderedSet("ab") - with pytest.raises(ValueError): + with pytest.raises(TypeError): # noinspection PyTypeChecker set1.update(3) From 8997dbcc8d4e4497b3cea331bc1c95e64c633e7e Mon Sep 17 00:00:00 2001 From: Idan Miara Date: Tue, 14 Feb 2023 14:13:25 +0200 Subject: [PATCH 06/18] setup.py - use a general purpose setup.py that takes the package details from __init__.py --- setup.py | 121 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 108 insertions(+), 13 deletions(-) diff --git a/setup.py b/setup.py index 8d5d903..5ca0199 100644 --- a/setup.py +++ b/setup.py @@ -1,24 +1,119 @@ +""" +This is a generic setup file for creating a package. +It assumes that all the package details, like __version__, present in module's __init__.py +If it finds more than one module to pack then the first module's details would be present in the package metadata +In most cases, setup.cfg and MANIFEST.in are not required. +""" + # This is a wrapper for environments that require a `setup.py`-based installation. # This is not the primary way of installing ordered-set. # # The primary setup is in pyproject.toml. You can install ordered-set as a # dependency using `poetry` or `pip`. -from setuptools import setup +__author__ = 'Idan Miara' + +import glob +import importlib +import os +import re +from typing import Dict, Any +from warnings import warn +from setuptools import setup, find_packages + +replaces_underscores_with_dashes_in_package_name = True + + +def get_requirements() -> Dict[str, Any]: + """ + parsing the main requirement file `requirements.txt` and any extra requirements files, like `requirements-dev.txt + and returns a requirements dict to use with setup + """ + requirements = {re.match(r'requirements-(.*).txt', x).group(1): x for x in glob.glob('requirements-*.txt')} + if os.path.isfile('requirements.txt'): + requirements[''] = 'requirements.txt' + + # read requirements from files + requirements = { + r: [req.strip() for req in open(filename).readlines()] + for r, filename in requirements.items() + } + # remove emptry lines, remarks and skip extra-index-url lines + requirements = { + # read requirements from files + r: [req for req in reqs if req and not req.startswith('--') and not req.startswith('#')] + for r, reqs in requirements.items() + } + # remove empty requirements + requirements = {r: req for r, req in requirements.items() if req} + + result = {} + if requirements.get(''): + result['install_requires'] = requirements.pop('') + + if requirements: + result['extras_require'] = requirements + return result + + +def get_setup_kwargs(m: object, attrs=None) -> Dict[str, str]: + if attrs is None: + attrs = [ + 'name', + 'version', + 'author', + 'author_email', + 'maintainer', + 'maintainer_email', + 'license', + 'url', + 'description', + 'long_description', + 'classifiers', + 'python_requires', + ] + + # result = {k: v for k in attrs if (v := getattr(m, f'__{k}__', None)) is not None} + result = {k: getattr(m, f'__{k}__', None) for k in attrs} + result = {k: v for k, v in result.items() if v is not None} + + # if python_minimum_version := getattr(m, '__python_minimum_version__', None): + python_minimum_version = getattr(m, '__python_minimum_version__', None) + if python_minimum_version is not None: + result.setdefault('python_requires', f'>={python_minimum_version}') + + for filename, fmt in ('README.md', 'text/markdown'), ('README.rst', 'text/x-rst'): + if os.path.exists(filename): + result['long_description'] = open(filename, encoding="utf-8").read() + result['long_description_content_type'] = fmt + break + + result.update(get_requirements()) + return result -packages = ['ordered_set'] +package_root = '.' # package sources are under this dir +packages = find_packages(package_root) # include all packages under package_root +package_dir = {'': package_root} # packages sources are under package_root -setup_kwargs = { - 'name': 'ordered-set', - 'version': '4.1.0', - 'description': 'A set that remembers its order, and allows looking up its items by their index in that order.', - 'author': 'Elia Robyn Lake', - 'author_email': 'gh@arborelia.net', - 'url': 'https://github.com/rspeer/ordered-set', - 'packages': packages, - 'python_requires': '>=3.8', -} +module_names = sorted(set(p.split('.')[0] for p in packages)) +if len(module_names) == 0: + raise Exception(f'Could not find any module_names in {package_root}') +elif len(module_names) > 1: + warn(f'There is more than 1 module in this package - {module_names}. ' + f'Assuming this is not a mistake - ' + f'All these modules would be packaged inside a single package ' + f'with the package name and metadata would taken from the first module: ' + f'{module_names[0]}') +module_name = module_names[0] +module = importlib.import_module(module_name) +kwargs = get_setup_kwargs(module) -setup(**setup_kwargs) +if replaces_underscores_with_dashes_in_package_name: + kwargs['name'] = kwargs['name'].replace('_', '-') +setup( + **kwargs, + packages=packages, + package_dir=package_dir, +) From a1f6a862fa4b13ae2d51563a4b540a4cf0114534 Mon Sep 17 00:00:00 2001 From: Idan Miara Date: Tue, 14 Feb 2023 13:29:04 +0200 Subject: [PATCH 07/18] test_ordered_set.py moved to test/test_ordered_set_1.py test/__init__.py added --- ordered_set/test/__init__.py | 0 .../test_ordered_set.py => ordered_set/test/test_ordered_set_1.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 ordered_set/test/__init__.py rename test/test_ordered_set.py => ordered_set/test/test_ordered_set_1.py (100%) diff --git a/ordered_set/test/__init__.py b/ordered_set/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_ordered_set.py b/ordered_set/test/test_ordered_set_1.py similarity index 100% rename from test/test_ordered_set.py rename to ordered_set/test/test_ordered_set_1.py From 58ce4a5d35c09f6742db3dc6de5cd9bd654d92f2 Mon Sep 17 00:00:00 2001 From: Idan Miara Date: Tue, 14 Feb 2023 12:31:27 +0200 Subject: [PATCH 08/18] test/__init__.py - add test types groups to be used across the different tests --- ordered_set/test/__init__.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ordered_set/test/__init__.py b/ordered_set/test/__init__.py index e69de29..4d475b0 100644 --- a/ordered_set/test/__init__.py +++ b/ordered_set/test/__init__.py @@ -0,0 +1,14 @@ +from ordered_set import OrderedSet, StableSet, StableSetEq + +stable_sets = [StableSet] +stableeq_sets = [StableSetEq] +ordered_sets = [OrderedSet] + +# from orderedset import OrderedSet as OrderedSet2 +# ordered_sets += [OrderedSet2] + +set_and_stable_sets = [set] + stable_sets +stableeq_and_ordered_sets = stableeq_sets + ordered_sets +stable_and_ordered_sets = stable_sets + stableeq_sets + ordered_sets +sets_and_stable_sets = [set] + stable_sets + stableeq_sets +all_sets = [set] + stable_sets + stableeq_sets + ordered_sets From 0c482dceacfc8eb3b22f9842dd51450afacba033 Mon Sep 17 00:00:00 2001 From: Idan Miara Date: Tue, 14 Feb 2023 12:17:31 +0200 Subject: [PATCH 09/18] test/test_ordered_set_1.py - add many tests and parameterized the tests to test multiple set types --- ordered_set/test/test_ordered_set_1.py | 476 ++++++++++++++++--------- 1 file changed, 308 insertions(+), 168 deletions(-) diff --git a/ordered_set/test/test_ordered_set_1.py b/ordered_set/test/test_ordered_set_1.py index df30b1f..14515ba 100644 --- a/ordered_set/test/test_ordered_set_1.py +++ b/ordered_set/test/test_ordered_set_1.py @@ -1,59 +1,80 @@ -import collections import itertools as it +import json import operator import pickle import random -import sys +import tempfile +from collections import OrderedDict, deque +from pathlib import Path import pytest -from ordered_set import OrderedSet +from ordered_set.test import ( + all_sets, + ordered_sets, + sets_and_stable_sets, + stable_and_ordered_sets, + stableeq_and_ordered_sets, +) -def test_pickle(): - set1 = OrderedSet("abracadabra") +@pytest.mark.parametrize("set_t", all_sets) +def test_pickle(set_t): + set1 = set_t("abracadabra") roundtrip = pickle.loads(pickle.dumps(set1)) assert roundtrip == set1 -def test_empty_pickle(): - empty_oset = OrderedSet() +@pytest.mark.parametrize("set_t", all_sets) +def test_empty_pickle(set_t): + empty_oset = set_t() empty_roundtrip = pickle.loads(pickle.dumps(empty_oset)) assert empty_roundtrip == empty_oset -def test_order(): - set1 = OrderedSet("abracadabra") +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_order(set_t): + set1 = set_t("abracadabra") assert len(set1) == 5 - assert set1 == OrderedSet(["a", "b", "r", "c", "d"]) + set2 = set_t(["a", "b", "r", "c", "d"]) + assert set1 == set2 assert list(reversed(set1)) == ["d", "c", "r", "b", "a"] -def test_binary_operations(): - set1 = OrderedSet("abracadabra") - set2 = OrderedSet("simsalabim") +@pytest.mark.parametrize("set_t", all_sets) +def test_binary_operations(set_t): + set1 = set_t("abracadabra") + set2 = set_t("simsalabim") assert set1 != set2 - assert set1 & set2 == OrderedSet(["a", "b"]) - assert set1 | set2 == OrderedSet(["a", "b", "r", "c", "d", "s", "i", "m", "l"]) - assert set1 - set2 == OrderedSet(["r", "c", "d"]) + assert set1 & set2 == set_t(["a", "b"]) + assert set1 | set2 == set_t(["a", "b", "r", "c", "d", "s", "i", "m", "l"]) + assert set1 - set2 == set_t(["r", "c", "d"]) -def test_indexing(): - set1 = OrderedSet("abracadabra") +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_indexing(set_t): + set1 = set_t("abracadabra") + assert set1[0] == "a" + assert set1[3] == "c" assert set1[:] == set1 assert set1.copy() == set1 assert set1 is set1 assert set1[:] is not set1 assert set1.copy() is not set1 - assert set1[[1, 2]] == OrderedSet(["b", "r"]) - assert set1[1:3] == OrderedSet(["b", "r"]) + assert set1[[1, 2]] == ["b", "r"] + assert set1[1:3] == set_t(["b", "r"]) assert set1.index("b") == 1 assert set1.index(["b", "r"]) == [1, 2] with pytest.raises(KeyError): set1.index("br") + with pytest.raises(IndexError): + set1[100] + set1[[100, 0]] + set1.index("br") + class FancyIndexTester: """ @@ -75,33 +96,37 @@ def __eq__(self, other): raise TypeError -def test_fancy_index_class(): - set1 = OrderedSet("abracadabra") +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_fancy_index_class(set_t): + set1 = set_t("abracadabra") indexer = FancyIndexTester([1, 0, 4, 3, 0, 2]) assert "".join(set1[indexer]) == "badcar" -def test_pandas_compat(): - set1 = OrderedSet("abracadabra") +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_pandas_compat(set_t): + set1 = set_t("abracadabra") assert set1.get_loc("b") == 1 assert set1.get_indexer(["b", "r"]) == [1, 2] -def test_tuples(): - set1 = OrderedSet() +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_tuples(set_t): + set1 = set_t() tup = ("tuple", 1) set1.add(tup) assert set1.index(tup) == 0 assert set1[0] == tup -def test_remove(): - set1 = OrderedSet("abracadabra") +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_remove(set_t): + set1 = set_t("abracadabra") set1.remove("a") set1.remove("b") - assert set1 == OrderedSet("rcd") + assert set1 == set_t("rcd") assert set1[0] == "r" assert set1[1] == "c" assert set1[2] == "d" @@ -120,136 +145,217 @@ def test_remove(): set1.discard("a") -def test_remove_error(): +@pytest.mark.parametrize("set_t", all_sets) +def test_remove_error(set_t): # If we .remove() an element that's not there, we get a KeyError - set1 = OrderedSet("abracadabra") + set1 = set_t("abracadabra") with pytest.raises(KeyError): set1.remove("z") -def test_clear(): - set1 = OrderedSet("abracadabra") +@pytest.mark.parametrize("set_t", all_sets) +def test_clear(set_t): + set1 = set_t("abracadabra") set1.clear() assert len(set1) == 0 - assert set1 == OrderedSet() + assert set1 == set_t() -def test_update(): - set1 = OrderedSet("abcd") +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_update(set_t): + set1 = set_t("abcd") result = set1.update("efgh") assert result == 7 assert len(set1) == 8 assert "".join(set1) == "abcdefgh" - set2 = OrderedSet("abcd") + set2 = set_t("abcd") result = set2.update("cdef") assert result == 5 assert len(set2) == 6 assert "".join(set2) == "abcdef" -def test_pop(): - set1 = OrderedSet("ab") +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_pop(set_t): + set1 = set_t("ab") elem = set1.pop() - assert elem == "b" + elem = set1.pop() + assert elem == "a" + + pytest.raises(KeyError, set1.pop) + + +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_pop_by_index(set_t): + set1 = set_t("abcde") + elem = set1.pop(1) + assert elem == "b" + + elem = set1.pop(1) + assert elem == "c" + elem = set1.pop(0) assert elem == "a" + elem = set1.pop(-1) + assert elem == "e" + + elem = set1.pop(-1) + assert elem == "d" + pytest.raises(KeyError, set1.pop) -def test_getitem_type_error(): - set1 = OrderedSet("ab") +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_popitem(set_t): + set1 = set_t("abcd") + elem = set1.popitem() + assert elem == "d" + + elem = set1.popitem(last=True) + assert elem == "c" + + elem = set1.popitem(last=False) + assert elem == "a" + + elem = set1.popitem() + assert elem == "b" + + pytest.raises(KeyError, set1.popitem) + + +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_move_to_end(set_t): + set1 = set_t("abcd") + set1.move_to_end("a") + assert list(set1) == ["b", "c", "d", "a"] + + with pytest.raises(KeyError): + set1.move_to_end("z") + + +@pytest.mark.parametrize("set_t", all_sets) +def test_getitem_type_error(set_t): + set1 = set_t("ab") with pytest.raises(TypeError): set1["a"] -def test_update_type_error(): - set1 = OrderedSet("ab") - with pytest.raises(TypeError): +@pytest.mark.parametrize("set_t", all_sets) +def test_update_type_error(set_t): + set1 = set_t("ab") + with pytest.raises(TypeError): # not ValueError # noinspection PyTypeChecker set1.update(3) -def test_empty_repr(): - set1 = OrderedSet() - assert repr(set1) == "OrderedSet()" +@pytest.mark.parametrize("set_t", all_sets) +def test_empty_repr(set_t): + set1 = set_t() + set_class_name = set_t.__name__ + assert repr(set1) == f"{set_class_name}()" -def test_eq_wrong_type(): - set1 = OrderedSet() +@pytest.mark.parametrize("set_t", all_sets) +def test_eq_wrong_type(set_t): + set1 = set_t() assert set1 != 2 -def test_ordered_equality(): +@pytest.mark.parametrize("set_t", all_sets) +def test_ordered_equality(set_t): # Ordered set checks order against sequences. - assert OrderedSet([1, 2]) == OrderedSet([1, 2]) - assert OrderedSet([1, 2]) == [1, 2] - assert OrderedSet([1, 2]) == (1, 2) - assert OrderedSet([1, 2]) == collections.deque([1, 2]) + set1 = set_t([1, 2]) + assert set1 == set_t([1, 2]) + assert set1 == {1, 2} + assert set1 == {2, 1} -def test_ordered_inequality(): - # Ordered set checks order against sequences. - assert OrderedSet([1, 2]) != OrderedSet([2, 1]) +@pytest.mark.parametrize("set_t", stableeq_and_ordered_sets) +def test_ordered_equality_non_set(set_t): + set1 = set_t([1, 2]) + assert set1 == [1, 2] + assert set1 == (1, 2) + assert set1 == deque([1, 2]) - assert OrderedSet([1, 2]) != [2, 1] - assert OrderedSet([1, 2]) != [2, 1, 1] - assert OrderedSet([1, 2]) != (2, 1) - assert OrderedSet([1, 2]) != (2, 1, 1) +@pytest.mark.parametrize("set_t", sets_and_stable_sets) +def test_ordered_equality_unordered_sets(set_t): + set1 = set_t([1, 2]) + assert set1 == set_t([2, 1]) + assert set1 == {2, 1} - # Note: in Python 2.7 deque does not inherit from Sequence, but __eq__ - # contains an explicit check for this case for python 2/3 compatibility. - assert OrderedSet([1, 2]) != collections.deque([2, 1]) - assert OrderedSet([1, 2]) != collections.deque([2, 2, 1]) +@pytest.mark.parametrize("set_t", ordered_sets) +def test_ordered_inequality(set_t): + # Equal Ordered set checks order against sequences. + set1 = set_t([1, 2]) + assert set1 != set_t([2, 1]) -def test_comparisons(): + assert set1 != [2, 1] + assert set1 != [2, 1, 1] + + assert set1 != (2, 1) + assert set1 != (2, 1, 1) + + assert set1 != deque([2, 1]) + assert set1 != deque([2, 2, 1]) + + +@pytest.mark.parametrize("set_t", all_sets) +def test_comparisons(set_t): # Comparison operators on sets actually test for subset and superset. - assert OrderedSet([1, 2]) < OrderedSet([1, 2, 3]) - assert OrderedSet([1, 2]) > OrderedSet([1]) + assert set_t([1, 2]) < set_t([1, 2, 3]) + assert set_t([1, 2]) > set_t([1]) - # MutableSet subclasses aren't comparable to set on 3.3. - if tuple(sys.version_info) >= (3, 4): - assert OrderedSet([1, 2]) > {1} + assert set_t([1, 2]) > {1} -def test_unordered_equality(): +@pytest.mark.parametrize("set_t", all_sets) +def test_unordered_equality(set_t): # Unordered set checks order against non-sequences. - assert OrderedSet([1, 2]) == {1, 2} - assert OrderedSet([1, 2]) == frozenset([2, 1]) + assert set_t([1, 2]) == {1, 2} + assert set_t([1, 2]) == frozenset([2, 1]) + + assert set_t([1, 2]) == {1: 1, 2: 2}.keys() + assert set_t([1, 2]) == {2: 2, 1: 1}.keys() - assert OrderedSet([1, 2]) == {1: "a", 2: "b"} - assert OrderedSet([1, 2]) == {1: 1, 2: 2}.keys() - assert OrderedSet([1, 2]) == {1: 1, 2: 2}.values() + assert set_t([1, 2]) == OrderedDict([(2, 2), (1, 1)]).keys() + + +@pytest.mark.parametrize("set_t", ordered_sets) +def test_unordered_equality_ordered_set(set_t): + assert set_t([1, 2]) == {1: 1, 2: 2}.values() + assert set_t([1, 2]) == {1: "a", 2: "b"} # Corner case: OrderedDict is not a Sequence, so we don't check for order, # even though it does have the concept of order. - assert OrderedSet([1, 2]) == collections.OrderedDict([(2, 2), (1, 1)]) + assert set_t([1, 2]) == OrderedDict([(2, 2), (1, 1)]) # Corner case: We have to treat iterators as unordered because there # is nothing to distinguish an ordered and unordered iterator - assert OrderedSet([1, 2]) == iter([1, 2]) - assert OrderedSet([1, 2]) == iter([2, 1]) - assert OrderedSet([1, 2]) == iter([2, 1, 1]) + assert set_t([1, 2]) == iter([1, 2]) + assert set_t([1, 2]) == iter([2, 1]) + assert set_t([1, 2]) == iter([2, 1, 1]) -def test_unordered_inequality(): - assert OrderedSet([1, 2]) != set([]) - assert OrderedSet([1, 2]) != frozenset([2, 1, 3]) +@pytest.mark.parametrize("set_t", all_sets) +def test_unordered_inequality(set_t): + assert set_t([1, 2]) != set([]) + assert set_t([1, 2]) != frozenset([2, 1, 3]) - assert OrderedSet([1, 2]) != {2: "b"} - assert OrderedSet([1, 2]) != {1: 1, 4: 2}.keys() - assert OrderedSet([1, 2]) != {1: 1, 2: 3}.values() + assert set_t([1, 2]) != {2: "b"} + assert set_t([1, 2]) != {1: 1, 4: 2}.keys() + assert set_t([1, 2]) != {1: 1, 2: 3}.values() # Corner case: OrderedDict is not a Sequence, so we don't check for order, # even though it does have the concept of order. - assert OrderedSet([1, 2]) != collections.OrderedDict([(2, 2), (3, 1)]) + assert set_t([1, 2]) != OrderedDict([(2, 2), (3, 1)]) def allsame_(iterable, eq=operator.eq): @@ -278,110 +384,144 @@ def check_results_(results, datas, name): assert a is not b, name + " should all be different items" -def _operator_consistency_testdata(): +def _operator_consistency_testdata(set_t): """ Predefined and random data used to test operator consistency. """ # test case 1 - data1 = OrderedSet([5, 3, 1, 4]) - data2 = OrderedSet([1, 4]) - yield data1, data2 + a = set_t([5, 3, 1, 4]) + b = set_t([1, 4]) + yield a, b # first set is empty - data1 = OrderedSet([]) - data2 = OrderedSet([3, 1, 2]) - yield data1, data2 + a = set_t([]) + b = set_t([3, 1, 2]) + yield a, b # second set is empty - data1 = OrderedSet([3, 1, 2]) - data2 = OrderedSet([]) - yield data1, data2 + a = set_t([3, 1, 2]) + b = set_t([]) + yield a, b # both sets are empty - data1 = OrderedSet([]) - data2 = OrderedSet([]) - yield data1, data2 + a = set_t([]) + b = set_t([]) + yield a, b # random test cases rng = random.Random(0) - a, b = 20, 20 + x, y = 20, 20 for _ in range(10): - data1 = OrderedSet(rng.randint(0, a) for _ in range(b)) - data2 = OrderedSet(rng.randint(0, a) for _ in range(b)) - yield data1, data2 - yield data2, data1 - - -def test_operator_consistency_isect(): - for data1, data2 in _operator_consistency_testdata(): - result1 = data1.copy() - result1.intersection_update(data2) - result2 = data1 & data2 - result3 = data1.intersection(data2) - check_results_([result1, result2, result3], datas=(data1, data2), name="isect") - - -def test_operator_consistency_difference(): - for data1, data2 in _operator_consistency_testdata(): - result1 = data1.copy() - result1.difference_update(data2) - result2 = data1 - data2 - result3 = data1.difference(data2) - check_results_( - [result1, result2, result3], datas=(data1, data2), name="difference" - ) + a = set_t(rng.randint(0, x) for _ in range(y)) + b = set_t(rng.randint(0, x) for _ in range(y)) + yield a, b + yield b, a -def test_operator_consistency_xor(): - for data1, data2 in _operator_consistency_testdata(): - result1 = data1.copy() - result1.symmetric_difference_update(data2) - result2 = data1 ^ data2 - result3 = data1.symmetric_difference(data2) - check_results_([result1, result2, result3], datas=(data1, data2), name="xor") +datasets_t = list(_operator_consistency_testdata(set_t) for set_t in all_sets) +datasets = [item for sublist in datasets_t for item in sublist] -def test_operator_consistency_union(): - for data1, data2 in _operator_consistency_testdata(): - result1 = data1.copy() - result1.update(data2) - result2 = data1 | data2 - result3 = data1.union(data2) - check_results_([result1, result2, result3], datas=(data1, data2), name="union") +@pytest.mark.parametrize("a, b", datasets) +def test_operator_consistency_isect(a, b): + result1 = a.copy() + result1.intersection_update(b) + result2 = a & b + result3 = a.intersection(b) + check_results_([result1, result2, result3], datas=(a, b), name="isect") -def test_operator_consistency_subset(): - for data1, data2 in _operator_consistency_testdata(): - result1 = data1 <= data2 - result2 = data1.issubset(data2) - result3 = set(data1).issubset(set(data2)) - check_results_([result1, result2, result3], datas=(data1, data2), name="subset") +@pytest.mark.parametrize("a, b", datasets) +def test_operator_consistency_difference(a, b): + result1 = a.copy() + result1.difference_update(b) + result2 = a - b + result3 = a.difference(b) + check_results_([result1, result2, result3], datas=(a, b), name="difference") -def test_operator_consistency_superset(): - for data1, data2 in _operator_consistency_testdata(): - result1 = data1 >= data2 - result2 = data1.issuperset(data2) - result3 = set(data1).issuperset(set(data2)) - check_results_( - [result1, result2, result3], datas=(data1, data2), name="superset" - ) +@pytest.mark.parametrize("a, b", datasets) +def test_operator_consistency_xor(a, b): + result1 = a.copy() + result1.symmetric_difference_update(b) + result2 = a ^ b + result3 = a.symmetric_difference(b) + check_results_([result1, result2, result3], datas=(a, b), name="xor") + + +@pytest.mark.parametrize("a, b", datasets) +def test_operator_consistency_union(a, b): + result1 = a.copy() + result1.update(b) + result2 = a | b + result3 = a.union(b) + check_results_([result1, result2, result3], datas=(a, b), name="union") + + +@pytest.mark.parametrize("a, b", datasets) +def test_operator_consistency_subset(a, b): + result1 = a <= b + result2 = a.issubset(b) + result3 = set(a).issubset(set(b)) + check_results_([result1, result2, result3], datas=(a, b), name="subset") + + +@pytest.mark.parametrize("a, b", datasets) +def test_operator_consistency_superset(a, b): + result1 = a >= b + result2 = a.issuperset(b) + result3 = set(a).issuperset(set(b)) + check_results_([result1, result2, result3], datas=(a, b), name="superset") -def test_operator_consistency_disjoint(): - for data1, data2 in _operator_consistency_testdata(): - result1 = data1.isdisjoint(data2) - result2 = len(data1.intersection(data2)) == 0 - check_results_([result1, result2], datas=(data1, data2), name="disjoint") +@pytest.mark.parametrize("a, b", datasets) +def test_operator_consistency_disjoint(a, b): + result1 = a.isdisjoint(b) + result2 = len(a.intersection(b)) == 0 + check_results_([result1, result2], datas=(a, b), name="disjoint") -def test_bitwise_and_consistency(): +@pytest.mark.parametrize("set_t", all_sets) +def test_bitwise_and_consistency(set_t): # Specific case that was failing without explicit __and__ definition - data1 = OrderedSet([12, 13, 1, 8, 16, 15, 9, 11, 18, 6, 4, 3, 19, 17]) - data2 = OrderedSet([19, 4, 9, 3, 2, 10, 15, 17, 11, 13, 20, 6, 14, 16, 8]) - result1 = data1.copy() - result1.intersection_update(data2) + a = set_t([12, 13, 1, 8, 16, 15, 9, 11, 18, 6, 4, 3, 19, 17]) + b = set_t([19, 4, 9, 3, 2, 10, 15, 17, 11, 13, 20, 6, 14, 16, 8]) + result1 = a.copy() + result1.intersection_update(b) # This requires a custom & operation apparently - result2 = data1 & data2 - result3 = data1.intersection(data2) - check_results_([result1, result2, result3], datas=(data1, data2), name="isect") + result2 = a & b + result3 = a.intersection(b) + check_results_([result1, result2, result3], datas=(a, b), name="isect") + + +@pytest.mark.skip("Not implemented yet") +@pytest.mark.parametrize("set_t", all_sets) +def test_json(set_t): + a = set_t([1, 2, 3]) + json_data = json.dumps(a) + b = json.loads(json_data) + assert a == b + + +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_stability(set_t, override=False): + """if you run this test twice it should fail with `set` and succeed with `StableSet`""" + items = "abcdabcd" + + filename = Path(tempfile.gettempdir()) / f"data_{set_t.__name__}.pickle" + if override or not filename.is_file(): + sitems0 = list(set_t(items)) + print(f"saving {filename}") + print(sitems0) + with open(filename, "wb") as f: + pickle.dump(sitems0, f, pickle.HIGHEST_PROTOCOL) + + print(f"loading {filename}") + with open(filename, "rb") as f: + sitems = pickle.load(f) + print(sitems) + + for i in range(100): + new_sitems = list(set_t(items)) + print(new_sitems) + assert sitems == new_sitems, f"{sitems} != {new_sitems}" From 596840ef9c024d7d91436cec2c90bd9cb3ea959e Mon Sep 17 00:00:00 2001 From: Idan Miara Date: Tue, 14 Feb 2023 12:39:31 +0200 Subject: [PATCH 10/18] test/test_ordered_set_2.py - added from https://github.com/simonpercivall/orderedset/blob/master/tests/test_orderedset.py --- ordered_set/test/test_ordered_set_2.py | 376 +++++++++++++++++++++++++ 1 file changed, 376 insertions(+) create mode 100644 ordered_set/test/test_ordered_set_2.py diff --git a/ordered_set/test/test_ordered_set_2.py b/ordered_set/test/test_ordered_set_2.py new file mode 100644 index 0000000..5fa87f5 --- /dev/null +++ b/ordered_set/test/test_ordered_set_2.py @@ -0,0 +1,376 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +test_orderedset +---------------------------------- + +Tests for `orderedset` module. +""" +import sys + +if sys.version_info < (2, 7): + import unittest2 as unittest +else: + import unittest +import weakref +import gc +import copy +import pickle + +from ordered_set import OrderedSet + + +class TestOrderedset(unittest.TestCase): + + def setUp(self): + self.lst = list(range(10)) + + def test_add_new(self): + oset = OrderedSet(self.lst) + lst = self.lst + + item = 10 + lst.append(item) + oset.add(item) + + self.assertEqual(list(oset), lst) + + def test_add_existing(self): + oset = OrderedSet(self.lst) + lst = self.lst + + oset.add(1) + oset.add(3) + self.assertEqual(list(oset), lst) + + def test_discard(self): + oset = OrderedSet([1, 2, 3]) + + oset.discard(1) + self.assertNotIn(1, oset) + + oset.discard(4) + + def test_pop(self): + oset = OrderedSet([1, 2, 3]) + + v = oset.pop() + self.assertEqual(v, 3) + self.assertNotIn(v, oset) + + v = oset.pop(last=False) + self.assertEqual(v, 1) + self.assertNotIn(v, oset) + + def test_remove(self): + oset = OrderedSet(self.lst) + lst = self.lst + + oset.remove(3) + lst.remove(3) + + self.assertEqual(list(oset), lst) + + def test_clear(self): + val = frozenset([1]) + + oset = OrderedSet() + ws = weakref.WeakKeyDictionary() + + oset.add(val) + ws[val] = 1 + oset.clear() + + self.assertEqual(list(oset), []) + + del val + gc.collect() + self.assertEqual(list(ws), []) + + def test_copy(self): + oset1 = OrderedSet(self.lst) + oset2 = oset1.copy() + + self.assertIsNot(oset1, oset2) + self.assertEqual(oset1, oset2) + + oset1.clear() + self.assertNotEqual(oset1, oset2) + + def test_reduce(self): + oset = OrderedSet(self.lst) + oset2 = copy.copy(oset) + self.assertEqual(oset, oset2) + + oset3 = pickle.loads(pickle.dumps(oset)) + self.assertEqual(oset, oset3) + + oset.add(-1) + self.assertNotEqual(oset, oset2) + + def test_difference_and_update(self): + oset1 = OrderedSet([1, 2, 3]) + oset2 = OrderedSet([3, 4, 5]) + + oset3 = oset1 - oset2 + self.assertEqual(oset3, OrderedSet([1, 2])) + + self.assertEqual(oset1.difference(oset2), oset3) + + oset4 = oset1.copy() + oset4 -= oset2 + self.assertEqual(oset4, oset3) + + oset5 = oset1.copy() + oset5.difference_update(oset2) + self.assertEqual(oset5, oset3) + + def test_intersection_and_update(self): + oset1 = OrderedSet([1, 2, 3]) + oset2 = OrderedSet([3, 4, 5]) + + oset3 = oset1 & oset2 + self.assertEqual(oset3, OrderedSet([3])) + + oset4 = oset1.copy() + oset4 &= oset2 + + self.assertEqual(oset4, oset3) + + oset5 = oset1.copy() + oset5.intersection_update(oset2) + self.assertEqual(oset5, oset3) + + def test_issubset(self): + oset1 = OrderedSet([1, 2, 3]) + oset2 = OrderedSet([1, 2]) + + self.assertTrue(oset2 < oset1) + self.assertTrue(oset2.issubset(oset1)) + + oset2 = OrderedSet([1, 2, 3]) + self.assertTrue(oset2 <= oset1) + self.assertTrue(oset1 <= oset2) + self.assertTrue(oset2.issubset(oset1)) + + oset2 = OrderedSet([1, 2, 3, 4]) + self.assertFalse(oset2 < oset1) + self.assertFalse(oset2.issubset(oset1)) + self.assertTrue(oset1 < oset2) + + # issubset compares underordered for all sets + oset2 = OrderedSet([4, 3, 2, 1]) + self.assertTrue(oset1 < oset2) + + def test_issuperset(self): + oset1 = OrderedSet([1, 2, 3]) + oset2 = OrderedSet([1, 2]) + + self.assertTrue(oset1 > oset2) + self.assertTrue(oset1.issuperset(oset2)) + + oset2 = OrderedSet([1, 2, 3]) + self.assertTrue(oset1 >= oset2) + self.assertTrue(oset2 >= oset1) + self.assertTrue(oset1.issubset(oset2)) + + oset2 = OrderedSet([1, 2, 3, 4]) + self.assertFalse(oset1 > oset2) + self.assertFalse(oset1.issuperset(oset2)) + self.assertTrue(oset2 > oset1) + + # issubset compares underordered for all sets + oset2 = OrderedSet([4, 3, 2, 1]) + self.assertTrue(oset2 > oset1) + + def test_orderedsubset(self): + oset1 = OrderedSet([1, 2, 3]) + oset2 = OrderedSet([1, 2, 3, 4]) + oset3 = OrderedSet([1, 2, 4, 3]) + + self.assertTrue(oset1.isorderedsubset(oset2)) + self.assertFalse(oset1.isorderedsubset(oset3)) + self.assertFalse(oset2.isorderedsubset(oset3)) + + def test_orderedsuperset(self): + oset1 = OrderedSet([1, 2, 3]) + oset2 = OrderedSet([1, 2, 3, 4]) + oset3 = OrderedSet([1, 2, 4, 3]) + + self.assertTrue(oset2.isorderedsuperset(oset1)) + self.assertFalse(oset3.isorderedsuperset(oset1)) + self.assertFalse(oset3.isorderedsuperset(oset2)) + + def test_symmetric_difference_and_update(self): + oset1 = OrderedSet([1, 2, 3]) + oset2 = OrderedSet([2, 3, 4]) + + oset3 = oset1 ^ oset2 + self.assertEqual(oset3, OrderedSet([1, 4])) + + oset4 = oset1.copy() + self.assertEqual(oset4.symmetric_difference(oset2), oset3) + + oset4 ^= oset2 + self.assertEqual(oset4, oset3) + + oset5 = oset1.copy() + oset5.symmetric_difference_update(oset2) + self.assertEqual(oset5, oset3) + + def test_union_and_update(self): + oset = OrderedSet(self.lst) + lst = self.lst + + oset2 = oset | [3, 9, 27] + self.assertEqual(oset2, lst + [27]) + + # make sure original oset isn't changed + self.assertEqual(oset, lst) + + oset1 = OrderedSet(self.lst) + oset2 = OrderedSet(self.lst) + + oset3 = oset1 | oset2 + self.assertEqual(oset3, oset1) + + self.assertEqual(oset3, oset1.union(oset2)) + + oset1 |= OrderedSet("abc") + self.assertEqual(oset1, oset2 | "abc") + + oset1 = OrderedSet(self.lst) + oset1.update("abc") + self.assertEqual(oset1, oset2 | "abc") + + def test_union_with_iterable(self): + oset1 = OrderedSet([1]) + + self.assertEqual(oset1 | [2, 1], OrderedSet([1, 2])) + self.assertEqual([2] | oset1, OrderedSet([2, 1])) + self.assertEqual([1, 2] | OrderedSet([3, 1, 2, 4]), OrderedSet([1, 2, 3, 4])) + + # union with unordered set should work, though the order will be arbitrary + self.assertEqual(oset1 | set([2]), OrderedSet([1, 2])) + self.assertEqual(set([2]) | oset1, OrderedSet([2, 1])) + + def test_symmetric_difference_with_iterable(self): + oset1 = OrderedSet([1]) + + self.assertEqual(oset1 ^ [1], OrderedSet([])) + self.assertEqual([1] ^ oset1, OrderedSet([])) + + self.assertEqual(OrderedSet([3, 1, 4, 2]) ^ [3, 4], OrderedSet([1, 2])) + self.assertEqual([3, 1, 4, 2] ^ OrderedSet([3, 4]), OrderedSet([1, 2])) + + self.assertEqual(OrderedSet([3, 1, 4, 2]) ^ set([3, 4]), OrderedSet([1, 2])) + self.assertEqual(set([3, 1, 4]) ^ OrderedSet([3, 4, 2]), OrderedSet([1, 2])) + + def test_intersection_with_iterable(self): + self.assertEqual([1, 2, 3] & OrderedSet([3, 2]), OrderedSet([2, 3])) + self.assertEqual(OrderedSet([3, 2] & OrderedSet([1, 2, 3])), OrderedSet([3, 2])) + + def test_difference_with_iterable(self): + self.assertEqual(OrderedSet([1, 2, 3, 4]) - [3, 2], OrderedSet([1, 4])) + self.assertEqual([3, 2, 4, 1] - OrderedSet([2, 4]), OrderedSet([3, 1])) + + def test_isdisjoint(self): + self.assertTrue(OrderedSet().isdisjoint(OrderedSet())) + self.assertTrue(OrderedSet([1]).isdisjoint(OrderedSet([2]))) + self.assertFalse(OrderedSet([1, 2]).isdisjoint(OrderedSet([2, 3]))) + + def test_index(self): + oset = OrderedSet("abcd") + self.assertEqual(oset.index("b"), 1) + + def test_getitem(self): + oset = OrderedSet("abcd") + self.assertEqual(oset[2], "c") + + def test_getitem_slice(self): + oset = OrderedSet("abcdef") + self.assertEqual(oset[:2], OrderedSet("ab")) + self.assertEqual(oset[2:], OrderedSet("cdef")) + self.assertEqual(oset[::-1], OrderedSet("fedcba")) + self.assertEqual(oset[1:-1:2], OrderedSet("bd")) + self.assertEqual(oset[1::2], OrderedSet("bdf")) + + def test_len(self): + oset = OrderedSet(self.lst) + self.assertEqual(len(oset), len(self.lst)) + + oset.remove(0) + self.assertEqual(len(oset), len(self.lst) - 1) + + def test_contains(self): + oset = OrderedSet(self.lst) + self.assertTrue(1 in oset) + + def test_iter_mutated(self): + oset = OrderedSet(self.lst) + it = iter(oset) + oset.add('a') + + with self.assertRaises(RuntimeError): + next(it) + + it = reversed(oset) + oset.add('b') + + with self.assertRaises(RuntimeError): + next(it) + + def test_iter_and_valid_order(self): + oset = OrderedSet(self.lst) + self.assertEqual(list(oset), self.lst) + + oset = OrderedSet(self.lst + self.lst) + self.assertEqual(list(oset), self.lst) + + def test_reverse_order(self): + oset = OrderedSet(self.lst) + self.assertEqual(list(reversed(oset)), list(reversed(self.lst))) + + def test_repr(self): + oset = OrderedSet([1]) + self.assertEqual(repr(oset), "OrderedSet([1])") + + def test_eq(self): + oset1 = OrderedSet(self.lst) + oset2 = OrderedSet(self.lst) + + self.assertNotEqual(oset1, None) + + self.assertEqual(oset1, oset2) + self.assertEqual(oset1, set(self.lst)) + self.assertEqual(oset1, list(self.lst)) + + def test_ordering(self): + oset1 = OrderedSet(self.lst) + oset2 = OrderedSet(self.lst) + + if sys.version_info < (3, 0): + self.assertFalse(oset1 <= None) + + self.assertLessEqual(oset2, oset1) + self.assertLessEqual(oset2, set(oset1)) + self.assertLessEqual(oset2, list(oset1)) + + self.assertGreaterEqual(oset1, oset2) + self.assertGreaterEqual(oset1, set(oset2)) + self.assertGreaterEqual(oset1, list(oset2)) + + oset3 = OrderedSet(self.lst[:-1]) + + self.assertLess(oset3, oset1) + self.assertLess(oset3, set(oset1)) + self.assertLess(oset3, list(oset1)) + + self.assertGreater(oset1, oset3) + self.assertGreater(oset1, set(oset3)) + self.assertGreater(oset1, list(oset3)) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 12f92bc67c25e20070e0ebed13a4cadd8f3ee1ba Mon Sep 17 00:00:00 2001 From: Idan Miara Date: Tue, 14 Feb 2023 12:26:44 +0200 Subject: [PATCH 11/18] test/pytest_util.py - auxiliary module to help transition from `unittest` to `pytest` --- ordered_set/test/pytest_util.py | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 ordered_set/test/pytest_util.py diff --git a/ordered_set/test/pytest_util.py b/ordered_set/test/pytest_util.py new file mode 100644 index 0000000..4998c85 --- /dev/null +++ b/ordered_set/test/pytest_util.py @@ -0,0 +1,42 @@ +def assertEqual(a, b): + assert a == b + + +def assertNotEqual(a, b): + assert a != b + + +def assertTrue(a): + assert a is True + + +def assertFalse(a): + assert a is False + + +def assertLessEqual(a, b): + assert a <= b + + +def assertLess(a, b): + assert a < b + + +def assertGreaterEqual(a, b): + assert a >= b + + +def assertGreater(a, b): + assert a > b + + +def assertIn(a, b): + assert a in b + + +def assertNotIn(a, b): + assert a not in b + + +def assertIsNot(a, b): + assert a is not b From c7735603e0c8e5e2f595f56ea2ad509d2fb1b1e4 Mon Sep 17 00:00:00 2001 From: Idan Miara Date: Tue, 14 Feb 2023 12:22:48 +0200 Subject: [PATCH 12/18] test/test_ordered_set_2.py - updated the tests from https://github.com/simonpercivall/orderedset/blob/master/tests/test_orderedset.py to test against multiple set types --- ordered_set/test/test_ordered_set_2.py | 752 ++++++++++++++++--------- 1 file changed, 481 insertions(+), 271 deletions(-) diff --git a/ordered_set/test/test_ordered_set_2.py b/ordered_set/test/test_ordered_set_2.py index 5fa87f5..631b0c6 100644 --- a/ordered_set/test/test_ordered_set_2.py +++ b/ordered_set/test/test_ordered_set_2.py @@ -1,376 +1,586 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +# The following tests are based on +# https://github.com/simonpercivall/orderedset/blob/master/tests/test_orderedset.py +# Thus shall be under the license: +# https://github.com/simonpercivall/orderedset/blob/master/LICENSE -""" -test_orderedset ----------------------------------- - -Tests for `orderedset` module. -""" -import sys - -if sys.version_info < (2, 7): - import unittest2 as unittest -else: - import unittest -import weakref -import gc import copy +import gc import pickle +import weakref + +import pytest + +from ordered_set.test import ( + all_sets, + ordered_sets, + stable_and_ordered_sets, + stableeq_and_ordered_sets, + stableeq_sets, +) +from ordered_set.test.pytest_util import ( + assertEqual, + assertFalse, + assertGreater, + assertGreaterEqual, + assertIsNot, + assertLess, + assertLessEqual, + assertNotEqual, + assertNotIn, + assertTrue, +) + +datasets = [list(range(10))] + + +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", all_sets) +def test_add_new(set_t, lst: list): + oset = set_t(lst) + lst = copy.copy(lst) + + item = 10 + lst.append(item) + oset.add(item) + + assertEqual(list(oset), lst) + + +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", all_sets) +def test_add_existing(set_t, lst: list): + oset = set_t(lst) + lst = copy.copy(lst) + + oset.add(1) + oset.add(3) + assertEqual(list(oset), lst) + + +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", all_sets) +def test_discard(set_t, lst: list): + oset = set_t([1, 2, 3]) + + oset.discard(1) + assertNotIn(1, oset) + + oset.discard(4) + + +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_pop(set_t): + oset = set_t([1, 2, 3]) + + v = oset.pop() + assertEqual(v, 3) + assertNotIn(v, oset) + + v = oset.popitem(last=False) + assertEqual(v, 1) + assertNotIn(v, oset) + + +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", all_sets) +def test_remove(set_t, lst: list): + oset = set_t(lst) + lst = copy.copy(lst) + + oset.remove(3) + lst.remove(3) + + assertEqual(list(oset), lst) + + +@pytest.mark.parametrize("set_t", all_sets) +def test_clear(set_t): + val = frozenset([1]) + + oset = set_t() + ws = weakref.WeakKeyDictionary() + + oset.add(val) + ws[val] = 1 + oset.clear() + + assertEqual(list(oset), []) + + del val + gc.collect() + assertEqual(list(ws), []) + + +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", all_sets) +def test_copy(set_t, lst: list): + oset1 = set_t(lst) + oset2 = oset1.copy() + + assertIsNot(oset1, oset2) + assertEqual(oset1, oset2) + + oset1.clear() + assertNotEqual(oset1, oset2) + + +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", all_sets) +def test_reduce(set_t, lst: list): + oset = set_t(lst) + oset2 = copy.copy(oset) + assertEqual(oset, oset2) + + oset3 = pickle.loads(pickle.dumps(oset)) + assertEqual(oset, oset3) + + oset.add(-1) + assertNotEqual(oset, oset2) + + +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_difference_and_update(set_t, lst: list): + list1 = [1, 2, 3] + list2 = [3, 4, 5] + oset1 = set_t(list1) + oset2 = set_t(list2) + + # right hand + oset3 = oset2 - list1 + assertEqual(oset3, set_t([4, 5])) + assertEqual(oset2.difference(oset1), oset3) + + oset3 = list2 - oset1 + assertEqual(oset3, set_t([4, 5])) + assertEqual(oset2.difference(oset1), oset3) + + oset3 = oset1 - oset2 + assertEqual(oset3, set_t([1, 2])) + assertEqual(oset1.difference(oset2), oset3) + + # left hand + oset3 = oset1 - list2 + assertEqual(oset3, set_t([1, 2])) + assertEqual(oset1.difference(oset2), oset3) + + oset3 = list1 - oset2 + assertEqual(oset3, set_t([1, 2])) + assertEqual(oset1.difference(oset2), oset3) + + oset4 = oset1.copy() + oset4 -= oset2 + assertEqual(oset4, oset3) + + oset5 = oset1.copy() + oset5.difference_update(oset2) + assertEqual(oset5, oset3) + + +@pytest.mark.parametrize("set_t", all_sets) +def test_intersection_and_update(set_t): + oset1 = set_t([1, 2, 3]) + oset2 = set_t([3, 4, 5]) + + oset3 = oset1 & oset2 + assertEqual(oset3, set_t([3])) + + oset4 = oset1.copy() + oset4 &= oset2 + + assertEqual(oset4, oset3) + + oset5 = oset1.copy() + oset5.intersection_update(oset2) + assertEqual(oset5, oset3) + + +@pytest.mark.parametrize("set_t", all_sets) +def test_issubset(set_t): + oset1 = set_t([1, 2, 3]) + oset2 = set_t([1, 2]) + + assertTrue(oset2 < oset1) + assertTrue(oset2.issubset(oset1)) -from ordered_set import OrderedSet + oset2 = set_t([1, 2, 3]) + assertTrue(oset2 <= oset1) + assertTrue(oset1 <= oset2) + assertTrue(oset2.issubset(oset1)) + oset2 = set_t([1, 2, 3, 4]) + assertFalse(oset2 < oset1) + assertFalse(oset2.issubset(oset1)) + assertTrue(oset1 < oset2) -class TestOrderedset(unittest.TestCase): + # issubset compares unordered for all sets + oset2 = set_t([4, 3, 2, 1]) + assertTrue(oset1 < oset2) - def setUp(self): - self.lst = list(range(10)) - def test_add_new(self): - oset = OrderedSet(self.lst) - lst = self.lst +@pytest.mark.parametrize("set_t", all_sets) +def test_issuperset(set_t): + oset1 = set_t([1, 2, 3]) + oset2 = set_t([1, 2]) - item = 10 - lst.append(item) - oset.add(item) + assertTrue(oset1 > oset2) + assertTrue(oset1.issuperset(oset2)) - self.assertEqual(list(oset), lst) + oset2 = set_t([1, 2, 3]) + assertTrue(oset1 >= oset2) + assertTrue(oset2 >= oset1) + assertTrue(oset1.issubset(oset2)) - def test_add_existing(self): - oset = OrderedSet(self.lst) - lst = self.lst + oset2 = set_t([1, 2, 3, 4]) + assertFalse(oset1 > oset2) + assertFalse(oset1.issuperset(oset2)) + assertTrue(oset2 > oset1) - oset.add(1) - oset.add(3) - self.assertEqual(list(oset), lst) + # issubset compares underordered for all sets + oset2 = set_t([4, 3, 2, 1]) + assertTrue(oset2 > oset1) - def test_discard(self): - oset = OrderedSet([1, 2, 3]) - oset.discard(1) - self.assertNotIn(1, oset) +@pytest.mark.parametrize("set_t", all_sets) +def test_symmetric_difference_and_update(set_t): + oset1 = set_t([1, 2, 3]) + oset2 = set_t([2, 3, 4]) - oset.discard(4) + oset3 = oset1 ^ oset2 + assertEqual(oset3, set_t([1, 4])) - def test_pop(self): - oset = OrderedSet([1, 2, 3]) + oset4 = oset1.copy() + assertEqual(oset4.symmetric_difference(oset2), oset3) - v = oset.pop() - self.assertEqual(v, 3) - self.assertNotIn(v, oset) + oset4 ^= oset2 + assertEqual(oset4, oset3) - v = oset.pop(last=False) - self.assertEqual(v, 1) - self.assertNotIn(v, oset) + oset5 = oset1.copy() + oset5.symmetric_difference_update(oset2) + assertEqual(oset5, oset3) - def test_remove(self): - oset = OrderedSet(self.lst) - lst = self.lst - oset.remove(3) - lst.remove(3) +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", stableeq_and_ordered_sets) +def test_union_and_update(set_t, lst: list): + oset = set_t(lst) + lst = copy.copy(lst) - self.assertEqual(list(oset), lst) + oset2 = oset | [3, 9, 27] + assertEqual(oset2, lst + [27]) - def test_clear(self): - val = frozenset([1]) + oset2 = [3, 9, 27] | oset + assertEqual(oset2, [3, 9, 27, 0, 1, 2, 4, 5, 6, 7, 8]) - oset = OrderedSet() - ws = weakref.WeakKeyDictionary() + # make sure original oset isn't changed + assertEqual(oset, lst) - oset.add(val) - ws[val] = 1 - oset.clear() + oset1 = set_t(lst) + oset2 = set_t(lst) - self.assertEqual(list(oset), []) + oset3 = oset1 | oset2 + assertEqual(oset3, oset1) - del val - gc.collect() - self.assertEqual(list(ws), []) + assertEqual(oset3, oset1.union(oset2)) - def test_copy(self): - oset1 = OrderedSet(self.lst) - oset2 = oset1.copy() + oset1 |= set_t("abc") + assertEqual(oset1, oset2 | "abc") - self.assertIsNot(oset1, oset2) - self.assertEqual(oset1, oset2) + oset1 = set_t(lst) + oset1.update("abc") + assertEqual(oset1, oset2 | "abc") - oset1.clear() - self.assertNotEqual(oset1, oset2) - def test_reduce(self): - oset = OrderedSet(self.lst) - oset2 = copy.copy(oset) - self.assertEqual(oset, oset2) +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_union_with_iterable(set_t): + oset1 = set_t([1]) - oset3 = pickle.loads(pickle.dumps(oset)) - self.assertEqual(oset, oset3) + assertEqual(oset1 | [2, 1], set_t([1, 2])) + oset2 = [2] | oset1 + assertEqual(oset2, set_t([2, 1])) + oset2 = [1, 2] | set_t([3, 1, 2, 4]) + assertEqual(oset2, set_t([1, 2, 3, 4])) - oset.add(-1) - self.assertNotEqual(oset, oset2) + # union with unordered set should work, though the order will be arbitrary + oset2 = oset1 | set([2]) + assertEqual(oset2, set_t([1, 2])) + oset2 = set([2]) | oset1 + assertEqual(oset2, set_t([2, 1])) - def test_difference_and_update(self): - oset1 = OrderedSet([1, 2, 3]) - oset2 = OrderedSet([3, 4, 5]) - oset3 = oset1 - oset2 - self.assertEqual(oset3, OrderedSet([1, 2])) +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_symmetric_difference_with_iterable(set_t): + oset1 = set_t([1]) - self.assertEqual(oset1.difference(oset2), oset3) + assertEqual(oset1 ^ [1], set_t([])) + assertEqual([1] ^ oset1, set_t([])) - oset4 = oset1.copy() - oset4 -= oset2 - self.assertEqual(oset4, oset3) + assertEqual(set_t([3, 1, 4, 2]) ^ [3, 4], set_t([1, 2])) + assertEqual([3, 1, 4, 2] ^ set_t([3, 4]), set_t([1, 2])) - oset5 = oset1.copy() - oset5.difference_update(oset2) - self.assertEqual(oset5, oset3) + assertEqual(set_t([3, 1, 4, 2]) ^ set([3, 4]), set_t([1, 2])) + oset2 = set([3, 1, 4]) ^ set_t([3, 4, 2]) + assertEqual(oset2, set_t([1, 2])) - def test_intersection_and_update(self): - oset1 = OrderedSet([1, 2, 3]) - oset2 = OrderedSet([3, 4, 5]) - oset3 = oset1 & oset2 - self.assertEqual(oset3, OrderedSet([3])) +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_intersection_with_iterable(set_t): + assertEqual([1, 2, 3] & set_t([3, 2]), set_t([2, 3])) + assertEqual(set_t([3, 2] & set_t([1, 2, 3])), set_t([3, 2])) - oset4 = oset1.copy() - oset4 &= oset2 - self.assertEqual(oset4, oset3) +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_difference_with_iterable(set_t): + assertEqual(set_t([1, 2, 3, 4]) - [3, 2], set_t([1, 4])) + assertEqual([3, 2, 4, 1] - set_t([2, 4]), set_t([3, 1])) - oset5 = oset1.copy() - oset5.intersection_update(oset2) - self.assertEqual(oset5, oset3) - def test_issubset(self): - oset1 = OrderedSet([1, 2, 3]) - oset2 = OrderedSet([1, 2]) +@pytest.mark.parametrize("set_t", all_sets) +def test_isdisjoint(set_t): + assertTrue(set_t().isdisjoint(set_t())) + assertTrue(set_t([1]).isdisjoint(set_t([2]))) + assertFalse(set_t([1, 2]).isdisjoint(set_t([2, 3]))) - self.assertTrue(oset2 < oset1) - self.assertTrue(oset2.issubset(oset1)) - oset2 = OrderedSet([1, 2, 3]) - self.assertTrue(oset2 <= oset1) - self.assertTrue(oset1 <= oset2) - self.assertTrue(oset2.issubset(oset1)) +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_index(set_t): + oset = set_t("abcd") + assertEqual(oset.index("b"), 1) - oset2 = OrderedSet([1, 2, 3, 4]) - self.assertFalse(oset2 < oset1) - self.assertFalse(oset2.issubset(oset1)) - self.assertTrue(oset1 < oset2) - # issubset compares underordered for all sets - oset2 = OrderedSet([4, 3, 2, 1]) - self.assertTrue(oset1 < oset2) +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_getitem(set_t): + oset = set_t("abcd") + assertEqual(oset[2], "c") - def test_issuperset(self): - oset1 = OrderedSet([1, 2, 3]) - oset2 = OrderedSet([1, 2]) - self.assertTrue(oset1 > oset2) - self.assertTrue(oset1.issuperset(oset2)) +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_getitem_slice(set_t): + oset = set_t("abcdef") + assertEqual(oset[:2], set_t("ab")) + assertEqual(oset[2:], set_t("cdef")) + assertEqual(oset[::-1], set_t("fedcba")) + assertEqual(oset[1:-1:2], set_t("bd")) + assertEqual(oset[1::2], set_t("bdf")) - oset2 = OrderedSet([1, 2, 3]) - self.assertTrue(oset1 >= oset2) - self.assertTrue(oset2 >= oset1) - self.assertTrue(oset1.issubset(oset2)) - oset2 = OrderedSet([1, 2, 3, 4]) - self.assertFalse(oset1 > oset2) - self.assertFalse(oset1.issuperset(oset2)) - self.assertTrue(oset2 > oset1) +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", all_sets) +def test_len(set_t, lst: list): + oset = set_t(lst) + assertEqual(len(oset), len(lst)) - # issubset compares underordered for all sets - oset2 = OrderedSet([4, 3, 2, 1]) - self.assertTrue(oset2 > oset1) + oset.remove(0) + assertEqual(len(oset), len(lst) - 1) - def test_orderedsubset(self): - oset1 = OrderedSet([1, 2, 3]) - oset2 = OrderedSet([1, 2, 3, 4]) - oset3 = OrderedSet([1, 2, 4, 3]) - self.assertTrue(oset1.isorderedsubset(oset2)) - self.assertFalse(oset1.isorderedsubset(oset3)) - self.assertFalse(oset2.isorderedsubset(oset3)) +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", all_sets) +def test_contains(set_t, lst: list): + oset = set_t(lst) + assertTrue(1 in oset) - def test_orderedsuperset(self): - oset1 = OrderedSet([1, 2, 3]) - oset2 = OrderedSet([1, 2, 3, 4]) - oset3 = OrderedSet([1, 2, 4, 3]) - self.assertTrue(oset2.isorderedsuperset(oset1)) - self.assertFalse(oset3.isorderedsuperset(oset1)) - self.assertFalse(oset3.isorderedsuperset(oset2)) +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_iter_mutated(set_t, lst: list): + oset = set_t(lst) + it = iter(oset) + oset.add("a") - def test_symmetric_difference_and_update(self): - oset1 = OrderedSet([1, 2, 3]) - oset2 = OrderedSet([2, 3, 4]) + with pytest.raises(RuntimeError): + next(it) - oset3 = oset1 ^ oset2 - self.assertEqual(oset3, OrderedSet([1, 4])) + it = reversed(oset) + oset.add("b") - oset4 = oset1.copy() - self.assertEqual(oset4.symmetric_difference(oset2), oset3) + with pytest.raises(RuntimeError): + next(it) - oset4 ^= oset2 - self.assertEqual(oset4, oset3) - oset5 = oset1.copy() - oset5.symmetric_difference_update(oset2) - self.assertEqual(oset5, oset3) +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", all_sets) +def test_iter_and_valid_order(set_t, lst: list): + oset = set_t(lst) + assertEqual(list(oset), lst) - def test_union_and_update(self): - oset = OrderedSet(self.lst) - lst = self.lst + oset = set_t(lst + lst) + assertEqual(list(oset), lst) - oset2 = oset | [3, 9, 27] - self.assertEqual(oset2, lst + [27]) - # make sure original oset isn't changed - self.assertEqual(oset, lst) +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_reverse_order(set_t, lst: list): + oset = set_t(lst) + assertEqual(list(reversed(oset)), list(reversed(lst))) - oset1 = OrderedSet(self.lst) - oset2 = OrderedSet(self.lst) - oset3 = oset1 | oset2 - self.assertEqual(oset3, oset1) +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", all_sets) +def test_eq(set_t, lst: list): + oset1 = set_t(lst) + oset2 = set_t(lst) - self.assertEqual(oset3, oset1.union(oset2)) + assertNotEqual(oset1, None) - oset1 |= OrderedSet("abc") - self.assertEqual(oset1, oset2 | "abc") + assertEqual(oset1, oset2) + assertEqual(oset1, set(lst)) - oset1 = OrderedSet(self.lst) - oset1.update("abc") - self.assertEqual(oset1, oset2 | "abc") - def test_union_with_iterable(self): - oset1 = OrderedSet([1]) +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", ordered_sets) +def test_eq_list(set_t, lst: list): + assertEqual(set_t(lst), list(lst)) - self.assertEqual(oset1 | [2, 1], OrderedSet([1, 2])) - self.assertEqual([2] | oset1, OrderedSet([2, 1])) - self.assertEqual([1, 2] | OrderedSet([3, 1, 2, 4]), OrderedSet([1, 2, 3, 4])) - # union with unordered set should work, though the order will be arbitrary - self.assertEqual(oset1 | set([2]), OrderedSet([1, 2])) - self.assertEqual(set([2]) | oset1, OrderedSet([2, 1])) +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_repr(set_t): + oset = set_t([1]) + set_class_name = set_t.__name__ + assertEqual(repr(oset), f"{set_class_name}([1])") - def test_symmetric_difference_with_iterable(self): - oset1 = OrderedSet([1]) - self.assertEqual(oset1 ^ [1], OrderedSet([])) - self.assertEqual([1] ^ oset1, OrderedSet([])) +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_subset(set_t, lst: list): + oset1 = set_t([1, 2, 3]) + oset2 = set_t([1, 2, 3, 4]) + oset3 = set_t([1, 2, 4, 3]) + oset4 = set_t([1, 3, 2, 4]) - self.assertEqual(OrderedSet([3, 1, 4, 2]) ^ [3, 4], OrderedSet([1, 2])) - self.assertEqual([3, 1, 4, 2] ^ OrderedSet([3, 4]), OrderedSet([1, 2])) + assertTrue(oset1.isorderedsubset(oset2)) + assertFalse(oset1.isorderedsubset(oset3)) + assertFalse(oset2.isorderedsubset(oset3)) + assertFalse(oset1.isorderedsubset(oset4)) - self.assertEqual(OrderedSet([3, 1, 4, 2]) ^ set([3, 4]), OrderedSet([1, 2])) - self.assertEqual(set([3, 1, 4]) ^ OrderedSet([3, 4, 2]), OrderedSet([1, 2])) + assertTrue(oset1 < oset2) + assertTrue(oset1 < oset3) + assertFalse(oset2 < oset3) + assertTrue(oset1 < oset4) - def test_intersection_with_iterable(self): - self.assertEqual([1, 2, 3] & OrderedSet([3, 2]), OrderedSet([2, 3])) - self.assertEqual(OrderedSet([3, 2] & OrderedSet([1, 2, 3])), OrderedSet([3, 2])) - def test_difference_with_iterable(self): - self.assertEqual(OrderedSet([1, 2, 3, 4]) - [3, 2], OrderedSet([1, 4])) - self.assertEqual([3, 2, 4, 1] - OrderedSet([2, 4]), OrderedSet([3, 1])) +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_subset_non_consecutive(set_t, lst: list): + oset1 = set_t([1, 2, 3]) + oset2 = set_t([6, 1, 2, 5, 3, 4]) + oset3 = set_t([6, 1, 2, 5, 4, 3]) + oset4 = set_t([6, 1, 3, 5, 2, 4]) - def test_isdisjoint(self): - self.assertTrue(OrderedSet().isdisjoint(OrderedSet())) - self.assertTrue(OrderedSet([1]).isdisjoint(OrderedSet([2]))) - self.assertFalse(OrderedSet([1, 2]).isdisjoint(OrderedSet([2, 3]))) + assertTrue(oset1.isorderedsubset(oset2, non_consecutive=True)) + assertTrue(oset1.isorderedsubset(oset3, non_consecutive=True)) + assertFalse(oset2.isorderedsubset(oset3, non_consecutive=True)) + assertFalse(oset1.isorderedsubset(oset4, non_consecutive=True)) - def test_index(self): - oset = OrderedSet("abcd") - self.assertEqual(oset.index("b"), 1) + assertTrue(oset1 < oset2) + assertTrue(oset1 < oset3) + assertFalse(oset2 < oset3) + assertTrue(oset1 < oset4) - def test_getitem(self): - oset = OrderedSet("abcd") - self.assertEqual(oset[2], "c") - def test_getitem_slice(self): - oset = OrderedSet("abcdef") - self.assertEqual(oset[:2], OrderedSet("ab")) - self.assertEqual(oset[2:], OrderedSet("cdef")) - self.assertEqual(oset[::-1], OrderedSet("fedcba")) - self.assertEqual(oset[1:-1:2], OrderedSet("bd")) - self.assertEqual(oset[1::2], OrderedSet("bdf")) +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_superset(set_t, lst: list): + oset1 = set_t([1, 2, 3]) + oset2 = set_t([1, 2, 3, 4]) + oset3 = set_t([1, 2, 4, 3]) + oset4 = set_t([1, 3, 2, 4]) - def test_len(self): - oset = OrderedSet(self.lst) - self.assertEqual(len(oset), len(self.lst)) + assertTrue(oset2.isorderedsuperset(oset1)) + assertFalse(oset3.isorderedsuperset(oset1)) + assertFalse(oset3.isorderedsuperset(oset2)) + assertFalse(oset4.isorderedsuperset(oset1)) - oset.remove(0) - self.assertEqual(len(oset), len(self.lst) - 1) + assertTrue(oset2 > oset1) + assertTrue(oset3 > oset1) + assertFalse(oset3 > oset2) + assertTrue(oset4 > oset1) - def test_contains(self): - oset = OrderedSet(self.lst) - self.assertTrue(1 in oset) - def test_iter_mutated(self): - oset = OrderedSet(self.lst) - it = iter(oset) - oset.add('a') +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_superset_non_consecutive(set_t, lst: list): + oset1 = set_t([1, 2, 3]) + oset2 = set_t([6, 1, 2, 5, 3, 4]) + oset3 = set_t([6, 1, 2, 5, 4, 3]) + oset4 = set_t([6, 1, 3, 5, 2, 4]) - with self.assertRaises(RuntimeError): - next(it) + assertTrue(oset2.isorderedsuperset(oset1, non_consecutive=True)) + assertTrue(oset3.isorderedsuperset(oset1, non_consecutive=True)) + assertFalse(oset3.isorderedsuperset(oset2, non_consecutive=True)) + assertFalse(oset4.isorderedsuperset(oset1, non_consecutive=True)) - it = reversed(oset) - oset.add('b') + assertTrue(oset2 > oset1) + assertTrue(oset3 > oset1) + assertFalse(oset3 > oset2) + assertTrue(oset4 > oset1) - with self.assertRaises(RuntimeError): - next(it) - def test_iter_and_valid_order(self): - oset = OrderedSet(self.lst) - self.assertEqual(list(oset), self.lst) +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", all_sets) +def test_ordering(set_t, lst: list): + oset1 = set_t(lst) + oset2 = set_t(lst) - oset = OrderedSet(self.lst + self.lst) - self.assertEqual(list(oset), self.lst) + assertLessEqual(oset2, oset1) + assertLessEqual(oset2, set(oset1)) - def test_reverse_order(self): - oset = OrderedSet(self.lst) - self.assertEqual(list(reversed(oset)), list(reversed(self.lst))) + assertGreaterEqual(oset1, oset2) + assertGreaterEqual(oset1, set(oset2)) + + oset3 = set_t(lst[:-1]) - def test_repr(self): - oset = OrderedSet([1]) - self.assertEqual(repr(oset), "OrderedSet([1])") + assertLess(oset3, oset1) + assertLess(oset3, set(oset1)) + + assertGreater(oset1, oset3) + assertGreater(oset1, set(oset3)) + + oset4 = set_t(lst[1:]) - def test_eq(self): - oset1 = OrderedSet(self.lst) - oset2 = OrderedSet(self.lst) + assertFalse(oset3 < oset4) + assertFalse(oset3 < set(oset4)) + assertFalse(oset3 >= oset4) + assertFalse(oset3 >= set(oset4)) + assertFalse(oset3 < oset4) + assertFalse(oset3 < set(oset4)) + assertFalse(oset3 >= oset4) + assertFalse(oset3 >= set(oset4)) - self.assertNotEqual(oset1, None) - self.assertEqual(oset1, oset2) - self.assertEqual(oset1, set(self.lst)) - self.assertEqual(oset1, list(self.lst)) +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", stableeq_and_ordered_sets) +def test_ordering_with_lists(set_t, lst: list): + oset1 = set_t(lst) + oset2 = set_t(lst) - def test_ordering(self): - oset1 = OrderedSet(self.lst) - oset2 = OrderedSet(self.lst) + assertLessEqual(oset2, list(oset1)) + assertGreaterEqual(oset1, list(oset2)) - if sys.version_info < (3, 0): - self.assertFalse(oset1 <= None) + oset3 = set_t(lst[:-1]) - self.assertLessEqual(oset2, oset1) - self.assertLessEqual(oset2, set(oset1)) - self.assertLessEqual(oset2, list(oset1)) + assertLess(oset3, list(oset1)) + assertGreater(oset1, list(oset3)) - self.assertGreaterEqual(oset1, oset2) - self.assertGreaterEqual(oset1, set(oset2)) - self.assertGreaterEqual(oset1, list(oset2)) + oset4 = set_t(lst[1:]) - oset3 = OrderedSet(self.lst[:-1]) + assertFalse(oset3 < list(oset4)) + assertFalse(oset3 >= list(oset4)) + assertFalse(oset3 < list(oset4)) + assertFalse(oset3 >= list(oset4)) - self.assertLess(oset3, oset1) - self.assertLess(oset3, set(oset1)) - self.assertLess(oset3, list(oset1)) - self.assertGreater(oset1, oset3) - self.assertGreater(oset1, set(oset3)) - self.assertGreater(oset1, list(oset3)) +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", ordered_sets) +def test_eq_reversed_orderedset(set_t, lst: list): + oset1 = set_t(lst) + oset2 = set_t(reversed(lst)) + assertNotEqual(oset1, oset2) -if __name__ == '__main__': - unittest.main() \ No newline at end of file +@pytest.mark.parametrize("lst", datasets) +@pytest.mark.parametrize("set_t", stableeq_sets) +def test_eq_reversed_stableset(set_t, lst: list): + oset1 = set_t(lst) + oset2 = set_t(reversed(lst)) + assertEqual(oset1, oset2) From 13e1a53251e756f22284309d39ea4b0a2abdfbf9 Mon Sep 17 00:00:00 2001 From: Idan Miara Date: Mon, 13 Feb 2023 11:04:45 +0200 Subject: [PATCH 13/18] test/test_ordered_set_3.py - added from https://github.com/bustawin/ordered-set-37/blob/master/test/test_testing.py _test_mypy.py - added from https://github.com/bustawin/ordered-set-37/blob/master/test/_test_mypy.py --- ordered_set/test/_test_mypy.py | 5 ++ ordered_set/test/test_ordered_set_3.py | 88 ++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 ordered_set/test/_test_mypy.py create mode 100644 ordered_set/test/test_ordered_set_3.py diff --git a/ordered_set/test/_test_mypy.py b/ordered_set/test/_test_mypy.py new file mode 100644 index 0000000..be7dc48 --- /dev/null +++ b/ordered_set/test/_test_mypy.py @@ -0,0 +1,5 @@ +from ordered_set import OrderedSet + +x = OrderedSet([1, 2, 3]) +x.add(5) +z = 5 + x[2] diff --git a/ordered_set/test/test_ordered_set_3.py b/ordered_set/test/test_ordered_set_3.py new file mode 100644 index 0000000..e6c4753 --- /dev/null +++ b/ordered_set/test/test_ordered_set_3.py @@ -0,0 +1,88 @@ +from pathlib import Path + +import mypy.api +import pytest + +from ordered_set import OrderedSet + +TESTS = Path(__file__).parent + + +def test_add(): + x = OrderedSet([1, 2, -1, "bar"]) + x.add(0) + assert list(x) == [1, 2, -1, "bar", 0] + + +def test_discard(): + x = OrderedSet([1, 2, -1]) + x.discard(2) + assert list(x) == [1, -1] + + +def test_discard_ignores_missing_element(): + x = OrderedSet() + x.discard(1) # This does not raise + + +def test_remove(): + x = OrderedSet([1]) + x.remove(1) + assert not x + + +def test_remove_raises_missing_element(): + x = OrderedSet() + with pytest.raises(KeyError): + x.remove(1) + + +def test_getitem(): + x = OrderedSet([1, 2, -1]) + assert x[0] == 1 + assert x[1] == 2 + assert x[2] == -1 + with pytest.raises(IndexError): + x[3] + + +def test_len(): + x = OrderedSet([1]) + assert len(x) == 1 + + +def test_iter(): + for x in OrderedSet([1]): + assert x == 1 + + +def test_str(): + x = OrderedSet([1, 2, 3]) + assert str(x) == "{1, 2, 3}" + + +def test_repr(): + x = OrderedSet([1, 2, 3]) + assert repr(x) == "" + + +def test_eq(): + x = OrderedSet([1, 2, 3]) + y = OrderedSet([1, 2, 3]) + assert x == y + assert x is not y + + +def test_init_empty(): + x = OrderedSet() + assert len(x) == 0 + x.add(2) + assert len(x) == 1 + + +def test_typing_mypy(): + """Checks the typing values with mypy.""" + fixture = TESTS / "_test_mypy.py" + module = TESTS.parent / "ordered_set_37" + *_, error = mypy.api.run([str(module), str(fixture)]) + assert not error From c5bcb10a6445c35016acd7538eb31349323455f3 Mon Sep 17 00:00:00 2001 From: Idan Miara Date: Tue, 14 Feb 2023 12:23:52 +0200 Subject: [PATCH 14/18] test/test_ordered_set_3.py - update the tests from https://github.com/bustawin/ordered-set-37/blob/master/test/test_testing.py to test against multiple set types --- ordered_set/test/test_ordered_set_3.py | 88 +++++++++++++++++--------- 1 file changed, 57 insertions(+), 31 deletions(-) diff --git a/ordered_set/test/test_ordered_set_3.py b/ordered_set/test/test_ordered_set_3.py index e6c4753..07fcba1 100644 --- a/ordered_set/test/test_ordered_set_3.py +++ b/ordered_set/test/test_ordered_set_3.py @@ -1,44 +1,55 @@ +# The following tests are based on +# https://github.com/bustawin/ordered-set-37/blob/master/test/test_testing.py +# Thus shall be under the license: +# https://github.com/bustawin/ordered-set-37/blob/master/LICENSE.md + from pathlib import Path import mypy.api import pytest -from ordered_set import OrderedSet +from ordered_set.test import all_sets, stable_and_ordered_sets TESTS = Path(__file__).parent -def test_add(): - x = OrderedSet([1, 2, -1, "bar"]) +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_add(set_t): + x = set_t([1, 2, -1, "bar"]) x.add(0) assert list(x) == [1, 2, -1, "bar", 0] -def test_discard(): - x = OrderedSet([1, 2, -1]) +@pytest.mark.parametrize("set_t", all_sets) +def test_discard(set_t): + x = set_t([1, 2, -1]) x.discard(2) assert list(x) == [1, -1] -def test_discard_ignores_missing_element(): - x = OrderedSet() +@pytest.mark.parametrize("set_t", all_sets) +def test_discard_ignores_missing_element(set_t): + x = set_t() x.discard(1) # This does not raise -def test_remove(): - x = OrderedSet([1]) +@pytest.mark.parametrize("set_t", all_sets) +def test_remove(set_t): + x = set_t([1]) x.remove(1) assert not x -def test_remove_raises_missing_element(): - x = OrderedSet() +@pytest.mark.parametrize("set_t", all_sets) +def test_remove_raises_missing_element(set_t): + x = set_t() with pytest.raises(KeyError): x.remove(1) -def test_getitem(): - x = OrderedSet([1, 2, -1]) +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_getitem(set_t): + x = set_t([1, 2, -1]) assert x[0] == 1 assert x[1] == 2 assert x[2] == -1 @@ -46,43 +57,58 @@ def test_getitem(): x[3] -def test_len(): - x = OrderedSet([1]) +@pytest.mark.parametrize("set_t", all_sets) +def test_len(set_t): + x = set_t([1]) assert len(x) == 1 -def test_iter(): - for x in OrderedSet([1]): +@pytest.mark.parametrize("set_t", all_sets) +def test_iter(set_t): + for x in set_t([1]): assert x == 1 -def test_str(): - x = OrderedSet([1, 2, 3]) - assert str(x) == "{1, 2, 3}" +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_str(set_t): + x = set_t([1, 2, 3]) + assert str(x) == f"{set_t.__name__}([1, 2, 3])" -def test_repr(): - x = OrderedSet([1, 2, 3]) - assert repr(x) == "" +@pytest.mark.parametrize("set_t", stable_and_ordered_sets) +def test_repr(set_t): + x = set_t([1, 2, 3]) + assert str(x) == f"{set_t.__name__}([1, 2, 3])" -def test_eq(): - x = OrderedSet([1, 2, 3]) - y = OrderedSet([1, 2, 3]) +@pytest.mark.parametrize("set_t", all_sets) +def test_eq(set_t): + x = set_t([1, 2, 3]) + y = set_t([1, 2, 3]) assert x == y assert x is not y -def test_init_empty(): - x = OrderedSet() +@pytest.mark.parametrize("set_t", all_sets) +def test_init_empty(set_t): + x = set_t() assert len(x) == 0 x.add(2) assert len(x) == 1 -def test_typing_mypy(): +@pytest.mark.skip("Not ready yet") +@pytest.mark.parametrize("set_t", all_sets) +def test_typing_mypy(set_t, do_assert: bool = True): """Checks the typing values with mypy.""" fixture = TESTS / "_test_mypy.py" - module = TESTS.parent / "ordered_set_37" + module = TESTS.parent / "ordered_sets" *_, error = mypy.api.run([str(module), str(fixture)]) - assert not error + if do_assert: + assert not error + else: + print(*_) + + +if __name__ == "__main__": + test_typing_mypy(False) From 11b4e5d0d908f60cfe0ccacfbecfc6b7a16eb5a2 Mon Sep 17 00:00:00 2001 From: Idan Miara Date: Tue, 14 Feb 2023 12:34:43 +0200 Subject: [PATCH 15/18] _test/_test_ordered_set_all.py - added to ease running all the tests at once --- ordered_set/test/_test_ordered_set_all.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 ordered_set/test/_test_ordered_set_all.py diff --git a/ordered_set/test/_test_ordered_set_all.py b/ordered_set/test/_test_ordered_set_all.py new file mode 100644 index 0000000..5559d5d --- /dev/null +++ b/ordered_set/test/_test_ordered_set_all.py @@ -0,0 +1,3 @@ +from ordered_set.test.test_ordered_set_1 import * # NOQA +from ordered_set.test.test_ordered_set_2 import * # NOQA +from ordered_set.test.test_ordered_set_3 import * # NOQA From c92c21931fc65711ab537c30a40cf27ecb0e37fe Mon Sep 17 00:00:00 2001 From: Idan Miara Date: Tue, 14 Feb 2023 12:35:16 +0200 Subject: [PATCH 16/18] ordered_set_benchmark.py - added to test the performance of different implementations --- benchmarks/ordered_set_benchmark.py | 110 ++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 benchmarks/ordered_set_benchmark.py diff --git a/benchmarks/ordered_set_benchmark.py b/benchmarks/ordered_set_benchmark.py new file mode 100644 index 0000000..43ae192 --- /dev/null +++ b/benchmarks/ordered_set_benchmark.py @@ -0,0 +1,110 @@ +import timeit +from functools import partial +from random import randint + +from ordered_set import OrderedSet as OS2 + +try: + from orderedset import OrderedSet as OS1 +except ImportError: + # currently orderedset fails to install on Python 3.10, 3.11 + # https://github.com/simonpercivall/orderedset/issues/36#issuecomment-1424309665 + print("orderedset is not installed, using ordered_set twice") + OS1 = OS2 +from ordered_set import StableSet as OS3 +try: + from sortedcollections import OrderedSet as OS4 +except ImportError: + print("sortedcollections is not installed, using ordered_set twice") + OS4 = OS2 + + +item_count = 10_000 +item_range = item_count * 2 +items = [randint(0, item_range) for _ in range(item_count)] +items_b = [randint(0, item_range) for _ in range(item_count)] + +oset1a = OS1(items) +oset2a = OS2(items) +oset1b = OS1(items_b) +oset2b = OS2(items_b) +assert oset1a.difference(oset1b) == oset2a.difference(oset2b) +assert oset1a.intersection(oset1b) == oset2a.intersection(oset2b) + +oset1c = OS1(items) +oset2c = OS2(items) +oset1c.add(item_range + 1) +oset2c.add(item_range + 1) +assert oset1c == oset2c + +for i in range(item_range): + assert (i in oset1a) == (i in oset2a) + if i in oset1a: + assert oset1a.index(i) == oset2a.index(i) + + +def init_set(T, items) -> set: + return T(items) + + +def init_set_list(T, items) -> list: + return list(T(items)) + + +def init_set_d(items) -> dict: + return dict.fromkeys(items) + + +def init_set_d_list(items) -> list: + return list(dict.fromkeys(items)) + + +def update(s: set, items) -> set: + s.update(items) + return s + + +def update_d(s: dict, items) -> dict: + d2 = dict.fromkeys(items) + s.update(d2) + return s + + +ordered_sets_types = [OS1, OS2, OS3, OS4] +set_types = [set] + ordered_sets_types + +oss = [init_set(T, items) for T in set_types] +od = init_set_d(items) + +osls = [init_set_list(T, items) for T in set_types[1:]] + [init_set_d_list(items)] +for x in osls: + assert osls[0] == x + +osls = [update(init_set(T, items), items_b) for T in ordered_sets_types[:-1]] + [ + update_d(init_set_d(items), items_b) +] +osls = [list(x) for x in osls] +for x in osls: + assert osls[0] == x + +number = 10000 +repeats = 4 +for i in range(repeats): + print(f"----- {i} ------") + + print("-- init set like --") + print(f"d: {timeit.timeit(partial(init_set_d, items),number=number)=}") + for idx, T in enumerate(set_types): + print(f"{idx}: {timeit.timeit(partial(init_set, T, items),number=number)=}") + + print("-- unique list --") + print(f"d: {timeit.timeit(partial(init_set_d, items),number=number)=}") + for idx, T in enumerate(set_types): + print( + f"{idx}: {timeit.timeit(partial(init_set_list, T, items),number=number)=}" + ) + + print("-- update set like --") + print(f"d: {timeit.timeit(partial(update_d, od, items_b),number=number)=}") + for idx, os in enumerate(oss[:-1]): + print(f"{idx}: {timeit.timeit(partial(update, os, items_b),number=number)=}") From c6860e36c846b0330991d3faca3ade13412b2350 Mon Sep 17 00:00:00 2001 From: Idan Miara Date: Tue, 14 Feb 2023 12:33:20 +0200 Subject: [PATCH 17/18] README.md - updated with new features --- README.md | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f66fadd..0e48f2f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,15 @@ [![Pypi](https://img.shields.io/pypi/v/ordered-set.svg)](https://pypi.python.org/pypi/ordered-set) +A StableSet is a mutable set that remembers its insertion order. +Featuring: Fast O(1) insertion, deletion, iteration and membership testing. +But slow O(N) Index Lookup. + An OrderedSet is a mutable data structure that is a hybrid of a list and a set. -It remembers the order of its entries, and every entry has an index number that -can be looked up. +It remembers its insertion order so that every entry has an index that can be looked up. +Featuring: O(1) Index lookup, insertion, iteration and membership testing. +But slow O(N) Deletion. + +Both have similar interfaces but differ in respect of their implementation and performance. ## Installation @@ -105,12 +112,23 @@ in OrderedSet). ## Authors OrderedSet was implemented by Elia Robyn Lake (maiden name: Robyn Speer). +StableSet was implemented by Idan Miara, built upon the foundations of OrderedSet. Jon Crall contributed changes and tests to make it fit the Python set API. Roman Inflianskas added the original type annotations. - ## Comparisons +A StableSet is a mutable set that remembers its insertion order. +Featuring: Fast O(1) insertion, deletion, iteration and membership testing. +But slow O(N) Index Lookup. + +An OrderedSet is a mutable data structure that is a hybrid of a list and a set. +It remembers its insertion order so that every entry has an index that can be looked up. +Featuring: O(1) Index lookup, insertion, iteration and membership testing. +But slow O(N) Deletion. + +Both have similar interfaces but differ in respect of their implementation and performance. + The original implementation of OrderedSet was a [recipe posted to ActiveState Recipes][recipe] by Raymond Hettiger, released under the MIT license. @@ -120,14 +138,15 @@ Hettiger's implementation kept its content in a doubly-linked list referenced by dict. As a result, looking up an item by its index was an O(N) operation, while deletion was O(1). -This version makes different trade-offs for the sake of efficient lookups. Its -content is a standard Python list instead of a doubly-linked list. This +This version of OrderedSet makes different trade-offs for the sake of efficient lookups. +Its content is a standard Python list instead of a doubly-linked list. This provides O(1) lookups by index at the expense of O(N) deletion, as well as slightly faster iteration. -In Python 3.6 and later, the built-in `dict` type is inherently ordered. If you -ignore the dictionary values, that also gives you a simple ordered set, with -fast O(1) insertion, deletion, iteration and membership testing. However, `dict` -does not provide the list-like random access features of OrderedSet. You -would have to convert it to a list in O(N) to look up the index of an entry or -look up an entry by its index. +## Other implementations + +The included implementation of OrderedSet is fully compatible with the following implementation: +* https://pypi.org/project/orderedset/ - by Simon Percivall (faster implementation of `OrderedSet` using Cython, which currently only works for Python<3.9) + +The included implementation of StableSet is fully compatible with the following implementation: +* https://pypi.org/project/Ordered-set-37/ - by Xavier Bustamante Talavera (Similar basic implementation for `StableSet`, but named `OrderedSet`) From c1b4f8fef836a9792e4f08de384860cd0a144ea3 Mon Sep 17 00:00:00 2001 From: Idan Miara Date: Tue, 14 Feb 2023 12:30:24 +0200 Subject: [PATCH 18/18] CHANGELOG.md - updated for v5.2.0 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4eb738..314aebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ Significant changes in major and minor releases of this library: +## Version 5.2 (February 2022) + +- Major refactor +- Added a StableSet implementation, as a base class for OrderedSet. +- Added Many functions to OrderedSet, to be more complete and more compatible with other implementations. + - popitem(last: bool = True), similar to `dict.popitem` (note minor incompatibility with another implementation (`orderedset`) that have the `last` keyword in the `pop` function) + - move_to_end(key), similar to `dict.move_to_end` + - __le__, __lt__, __ge__, __gt__ - to improve subset/superset testing +- Minimum Python version is 3.8 (because __reversed__) +- Fix: OrderedSet.update now raised a TypeError instead of a ValueError when the type of the input is incorrect +- Added many new tests, and all the tests from 2 other implementations. + ## Version 4.1 (January 2022) - Packaged using flit. Wheels now exist, and setuptools is no longer required.