diff --git a/CHANGELOG.md b/CHANGELOG.md index a73bc421399e29c08873a9e6902237ae5afde524..d11c11e4300285195e40219f0b544cd33d0ee1ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ # v2.17.4 * warnings for some use cases of Closeable +* added an optional top limit for IDAllocator diff --git a/satella/__init__.py b/satella/__init__.py index 1b1cd0327bf37dc564b76f4d260019b63a2c2878..28312a5e524df9cdff18388cf13c6a68b3e885da 100644 --- a/satella/__init__.py +++ b/satella/__init__.py @@ -1 +1 @@ -__version__ = '2.17.4a2' +__version__ = '2.17.4' diff --git a/satella/coding/concurrent/id_allocator.py b/satella/coding/concurrent/id_allocator.py index c3b974bad03438f6a0eb70a482b3207a4d73d617..a9d2665398b1c9a6e72fa6160380ec91f5be0778 100644 --- a/satella/coding/concurrent/id_allocator.py +++ b/satella/coding/concurrent/id_allocator.py @@ -1,5 +1,7 @@ +import math +import typing as tp from .monitor import Monitor -from ...exceptions import AlreadyAllocated +from ...exceptions import AlreadyAllocated, Empty class SequentialIssuer(Monitor): @@ -57,20 +59,30 @@ class IDAllocator(Monitor): Thread-safe. :param start_at: the lowest integer that the allocator will return + :param top_limit: the maximum value that will not be allocated. If used, + subsequent calls to :meth:`~satella.coding.concurrent.IDAllocator.allocate_int` will + raise :class:`~satella.exceptions.Empty` """ - __slots__ = ('start_at', 'ints_allocated', 'free_ints', 'bound') + __slots__ = 'start_at', 'ints_allocated', 'free_ints', 'bound', 'top_limit' - def __init__(self, start_at: int = 0): + def __init__(self, start_at: int = 0, top_limit: tp.Optional[int] = None): super().__init__() self.start_at = start_at self.ints_allocated = set() self.free_ints = set() + self.top_limit = top_limit or math.inf self.bound = 0 - def _extend_the_bound_to(self, x: int): + def _extend_the_bound_to(self, x: int) -> int: + """Return how many integers added""" + if x > self.top_limit: + x = self.top_limit + how_many = 0 for i in range(self.bound, x): self.free_ints.add(i) + how_many += 1 self.bound = x + return how_many @Monitor.synchronized def mark_as_free(self, x: int): @@ -94,9 +106,11 @@ class IDAllocator(Monitor): Return a previously unallocated int, and mark it as allocated :return: an allocated int + :raises Empty: could not allocate an int due to top limit """ if not self.free_ints: - self._extend_the_bound_to(self.bound + 10) + if self._extend_the_bound_to(self.bound + 10) == 0: + raise Empty('No integers remaining!') x = self.free_ints.pop() self.ints_allocated.add(x) return x + self.start_at @@ -112,6 +126,8 @@ class IDAllocator(Monitor): """ if x < self.start_at: raise ValueError('%s is less than start_at' % (x,)) + if x >= self.top_limit: + raise ValueError('Cannot allocate a value greater or equal than top limit!') x -= self.start_at if x >= self.bound: self._extend_the_bound_to(x + 1) diff --git a/tests/test_coding/test_concurrent.py b/tests/test_coding/test_concurrent.py index cdbff9c64b67cc2a9594b6608e61e09856522ef0..43ba7c65ffc9fa6b5e6cdfed43ca57b185a07b7c 100644 --- a/tests/test_coding/test_concurrent.py +++ b/tests/test_coding/test_concurrent.py @@ -340,6 +340,13 @@ class TestConcurrent(unittest.TestCase): self.assertEqual(fut.result(), 5) self.assertEqual(a['test'], 2) + def test_id_allocator_top_limit(self): + id_alloc = IDAllocator(top_limit=10) + for i in range(10): + id_alloc.allocate_int() + self.assertRaises(Empty, id_alloc.allocate_int) + self.assertRaises(ValueError, lambda: id_alloc.mark_as_allocated(12)) + def test_id_allocator(self): id_alloc = IDAllocator() x = set([id_alloc.allocate_int(), id_alloc.allocate_int(), id_alloc.allocate_int()])