diff --git a/CHANGES/666.feature.rst b/CHANGES/666.feature.rst new file mode 100644 index 00000000..6a5b7f3e --- /dev/null +++ b/CHANGES/666.feature.rst @@ -0,0 +1 @@ +Added faster exception check for frozenlist, faster sequence check and new sort method -- by ``@Vizonex``. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 0abae2ea..7685b777 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -6,3 +6,4 @@ Marcin Konowalczyk Martijn Pieters Pau Freixes Paul Colomiets +Vizonex diff --git a/frozenlist/__init__.py b/frozenlist/__init__.py index 1b35881c..d7157b7f 100644 --- a/frozenlist/__init__.py +++ b/frozenlist/__init__.py @@ -31,6 +31,10 @@ def frozen(self): def freeze(self): self._frozen = True + def sort(self, key: object = None, reverse: bool = False): + self._check_frozen() + self._items.sort(key=key, reverse=reverse) + def __getitem__(self, index): return self._items[index] diff --git a/frozenlist/__init__.pyi b/frozenlist/__init__.pyi index ae803ef6..7ae3307c 100644 --- a/frozenlist/__init__.pyi +++ b/frozenlist/__init__.pyi @@ -1,10 +1,12 @@ from typing import ( + Callable, Generic, Iterable, Iterator, List, MutableSequence, Optional, + Protocol, TypeVar, Union, overload, @@ -42,6 +44,7 @@ class FrozenList(MutableSequence[_T], Generic[_T]): def insert(self, pos: int, item: _T) -> None: ... def __repr__(self) -> str: ... def __hash__(self) -> int: ... + def sort(self, *, key: object = None, reverse: bool = False) -> None: ... # types for C accelerators are the same CFrozenList = PyFrozenList = FrozenList diff --git a/frozenlist/_frozenlist.pyx b/frozenlist/_frozenlist.pyx index a82d8c8f..0191136b 100644 --- a/frozenlist/_frozenlist.pyx +++ b/frozenlist/_frozenlist.pyx @@ -2,6 +2,11 @@ # distutils: language = c++ from cpython.bool cimport PyBool_FromLong +from cpython.exc cimport PyErr_SetObject +from cpython.list cimport PyList_Append, PyList_GET_SIZE, PyList_New +from cpython.long cimport PyLong_FromSsize_t +from cpython.object cimport PyObject_GetIter +from cpython.sequence cimport PySequence_Contains, PySequence_Count, PySequence_List from libcpp.atomic cimport atomic import copy @@ -15,30 +20,32 @@ cdef class FrozenList: cdef atomic[bint] _frozen cdef list _items - def __init__(self, items=None): + def __init__(self, object items=None): self._frozen.store(False) if items is not None: - items = list(items) + items = PySequence_List(items) else: - items = [] + items = PyList_New(0) self._items = items @property def frozen(self): return PyBool_FromLong(self._frozen.load()) - cdef object _check_frozen(self): + cdef int _check_frozen(self) except -1: if self._frozen.load(): - raise RuntimeError("Cannot modify frozen list.") + PyErr_SetObject(RuntimeError, "Cannot modify frozen list.") + return -1 + return 0 - cdef inline object _fast_len(self): - return len(self._items) + cdef inline Py_ssize_t _fast_len(self): + return PyList_GET_SIZE(self._items) def freeze(self): self._frozen.store(True) def __getitem__(self, index): - return self._items[index] + return self._items.__getitem__(index) def __setitem__(self, index, value): self._check_frozen() @@ -49,10 +56,10 @@ cdef class FrozenList: del self._items[index] def __len__(self): - return self._fast_len() + return PyLong_FromSsize_t(self._fast_len()) def __iter__(self): - return self._items.__iter__() + return PyObject_GetIter(self._items) def __reversed__(self): return self._items.__reversed__() @@ -76,13 +83,17 @@ cdef class FrozenList: self._items.insert(pos, item) def __contains__(self, item): - return item in self._items + return PySequence_Contains(self._items, item) def __iadd__(self, items): self._check_frozen() - self._items += list(items) + self._items.extend(items) return self + def sort(self, object key = None, bint reverse = False): + self._check_frozen() + self._items.sort(key, reverse) + def index(self, item): return self._items.index(item) @@ -96,7 +107,7 @@ cdef class FrozenList: def extend(self, items): self._check_frozen() - self._items += list(items) + self._items.extend(items) def reverse(self): self._check_frozen() @@ -108,10 +119,10 @@ cdef class FrozenList: def append(self, item): self._check_frozen() - return self._items.append(item) + PyList_Append(self._items, item) def count(self, item): - return self._items.count(item) + return PySequence_Count(self._items, item) def __repr__(self): return ''.format(self._frozen.load(), @@ -128,7 +139,7 @@ cdef class FrozenList: obj_id = id(self) # Return existing copy if already processed (circular reference) - if obj_id in memo: + if PySequence_Contains(memo, obj_id): return memo[obj_id] # Create new instance and register immediately diff --git a/tests/test_frozenlist.py b/tests/test_frozenlist.py index c37f5c0d..c54a8764 100644 --- a/tests/test_frozenlist.py +++ b/tests/test_frozenlist.py @@ -198,6 +198,15 @@ def test_reverse_frozen(self) -> None: _list.reverse() assert _list == [1, 2] + def test_sort(self) -> None: + _list = self.FrozenList([3, 1, 4, 2]) + _list.sort() + assert _list == [1, 2, 3, 4] + _list.freeze() + # ensure we can't sort when frozen + with pytest.raises(RuntimeError): + _list.sort() + def test_pop(self) -> None: _list = self.FrozenList([1, 2]) assert _list.pop(0) == 1