From 751adba7e2ae96da0613732aa0efec4a1e9732de Mon Sep 17 00:00:00 2001 From: Thanos <111999343+Sachaa-Thanasius@users.noreply.github.com> Date: Wed, 29 Nov 2023 20:49:59 -0500 Subject: [PATCH 1/4] Start of attempted Queue refactor. --- wavelink/queue.py | 330 +++++++++++++++++++++++++--------------------- 1 file changed, 183 insertions(+), 147 deletions(-) diff --git a/wavelink/queue.py b/wavelink/queue.py index 3c04307f..0c874ff7 100644 --- a/wavelink/queue.py +++ b/wavelink/queue.py @@ -26,7 +26,7 @@ import asyncio import random from collections import deque -from collections.abc import Iterator +from collections.abc import Iterable, Iterator from typing import overload from .enums import QueueMode @@ -36,89 +36,7 @@ __all__ = ("Queue",) -class _Queue: - def __init__(self) -> None: - self._queue: deque[Playable] = deque() - - def __str__(self) -> str: - return ", ".join([f'"{p}"' for p in self]) - - def __repr__(self) -> str: - return f"BaseQueue(items={len(self._queue)})" - - def __bool__(self) -> bool: - return bool(self._queue) - - def __call__(self, item: Playable | Playlist) -> None: - self.put(item) - - def __len__(self) -> int: - return len(self._queue) - - @overload - def __getitem__(self, index: int) -> Playable: - ... - - @overload - def __getitem__(self, index: slice) -> list[Playable]: - ... - - def __getitem__(self, index: int | slice) -> Playable | list[Playable]: - if isinstance(index, slice): - return list(self._queue)[index] - - return self._queue[index] - - def __iter__(self) -> Iterator[Playable]: - return self._queue.__iter__() - - def __contains__(self, item: object) -> bool: - return item in self._queue - - @staticmethod - def _check_compatability(item: object) -> None: - if not isinstance(item, Playable): - raise TypeError("This queue is restricted to Playable objects.") - - def _get(self) -> Playable: - if not self: - raise QueueEmpty("There are no items currently in this queue.") - - return self._queue.popleft() - - def get(self) -> Playable: - return self._get() - - def _check_atomic(self, item: list[Playable] | Playlist) -> None: - for track in item: - self._check_compatability(track) - - def _put(self, item: Playable) -> None: - self._check_compatability(item) - self._queue.append(item) - - def put(self, item: Playable | Playlist, /, *, atomic: bool = True) -> int: - added: int = 0 - - if isinstance(item, Playlist): - if atomic: - self._check_atomic(item) - - for track in item: - try: - self._put(track) - added += 1 - except TypeError: - pass - - else: - self._put(item) - added += 1 - - return added - - -class Queue(_Queue): +class Queue: """The default custom wavelink Queue designed specifically for :class:`wavelink.Player`. .. container:: operations @@ -155,30 +73,41 @@ class Queue(_Queue): Check whether a specific track is in the queue. - Attributes ---------- history: :class:`wavelink.Queue` A queue of tracks that have been added to history. - - Even though the history queue is the same class as this Queue some differences apply. - Mainly you can not set the ``mode``. """ - def __init__(self, history: bool = True) -> None: - super().__init__() - self.history: Queue | None = None - - if history: - self.history = Queue(history=False) + def __init__(self, *, max_retention: int = 500, history: bool = True) -> None: + self.__items: list[Playable] = [] + self.max_retention = max_retention - self._loaded: Playable | None = None + self._history: Queue | None = None if not history else Queue(history=False) self._mode: QueueMode = QueueMode.normal + self._loaded: Playable | None = None + self._waiters: deque[asyncio.Future[None]] = deque() - self._finished: asyncio.Event = asyncio.Event() - self._finished.set() + self._lock = asyncio.Lock() + + @property + def mode(self) -> QueueMode: + """Property which returns a :class:`~wavelink.QueueMode` indicating which mode the + :class:`~wavelink.Queue` is in. + + This property can be set with any :class:`~wavelink.QueueMode`. + + .. versionadded:: 3.0.0 + """ + return self._mode + + @mode.setter + def mode(self, value: QueueMode) -> None: + self._mode = value - self._lock: asyncio.Lock = asyncio.Lock() + @property + def history(self) -> Queue | None: + return self._history def __str__(self) -> str: return ", ".join([f'"{p}"' for p in self]) @@ -186,6 +115,51 @@ def __str__(self) -> str: def __repr__(self) -> str: return f"Queue(items={len(self)}, history={self.history!r})" + def __call__(self, item: Playable) -> None: + self.put(item) + + def __bool__(self) -> bool: + return bool(self.__items) + + @overload + def __getitem__(self, __index: int) -> Playable: + ... + + @overload + def __getitem__(self, __index: slice) -> list[Playable]: + ... + + def __getitem__(self, __index: int | slice) -> Playable | list[Playable]: + return self.__items[__index] + + @overload + def __setitem__(self, __index: int, __value: Playable) -> None: + ... + + @overload + def __setitem__(self, __index: slice, __value: Playlist | Iterable[Playable]) -> None: + ... + + def __setitem__(self, __index: int | slice, __value: Playable | Playlist | Iterable[Playable], /) -> None: + if isinstance(__value, Playlist): + __value = __value.tracks + self.__items[__index] = __value + + def __delitem__(self, __index: int | slice, /) -> None: + del self.__items[__index] + + def __contains__(self, __other: Playable) -> bool: + return __other in self.__items + + def __len__(self) -> int: + return len(self.__items) + + def __reversed__(self) -> Iterator[Playable]: + return reversed(self.__items) + + def __iter__(self) -> Iterator[Playable]: + return iter(self.__items) + def _wakeup_next(self) -> None: while self._waiters: waiter = self._waiters.popleft() @@ -193,21 +167,15 @@ def _wakeup_next(self) -> None: if not waiter.done(): waiter.set_result(None) break + + @staticmethod + def _check_compatability(item: object) -> None: + if not isinstance(item, Playable): + raise TypeError("This queue is restricted to Playable objects.") - def _get(self) -> Playable: - if self.mode is QueueMode.loop and self._loaded: - return self._loaded - - if self.mode is QueueMode.loop_all and not self: - assert self.history is not None - - self._queue.extend(self.history._queue) - self.history.clear() - - track: Playable = super()._get() - self._loaded = track - - return track + def _check_atomic(self, item: list[Playable] | Playlist) -> None: + for track in item: + self._check_compatability(track) def get(self) -> Playable: """Retrieve a track from the left side of the queue. E.g. the first. @@ -219,13 +187,37 @@ def get(self) -> Playable: :class:`wavelink.Playable` The track retrieved from the queue. - Raises ------ QueueEmpty The queue was empty when retrieving a track. """ - return self._get() + + if self.mode is QueueMode.loop and self._loaded: + return self._loaded + + if self.mode is QueueMode.loop_all and not self: + assert self.history is not None + + self.__items.extend(self.history.__items) + self.history.clear() + + if not self: + raise QueueEmpty("There are no items currently in this queue.") + + track: Playable = self.__items.pop(0) + self._loaded = track + + return track + + def get_at(self, index: int, /) -> Playable: + if not self: + raise QueueEmpty("There are no items currently in this queue.") + + return self.__items.pop(index) + + def put_at(self, index: int, value: Playable, /) -> None: + self[index] = value async def get_wait(self) -> Playable: """This method returns the first :class:`wavelink.Playable` if one is present or @@ -237,8 +229,8 @@ async def get_wait(self) -> Playable: ------- :class:`wavelink.Playable` The track retrieved from the queue. - """ + while not self: loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() waiter: asyncio.Future[None] = loop.create_future() @@ -275,15 +267,33 @@ def put(self, item: Playable | Playlist, /, *, atomic: bool = True) -> int: Whether the items should be inserted atomically. If set to ``True`` this method won't enter any tracks if it encounters an error. Defaults to ``True`` - Returns ------- int The amount of tracks added to the queue. """ - added: int = super().put(item, atomic=atomic) + + added = 0 + + if isinstance(item, Iterable): + if atomic: + self._check_atomic(item) + + for track in item: + try: + self._check_compatability(track) + except TypeError: + pass + else: + self.__items.append(track) + added += 1 + else: + self._check_compatability(item) + self.__items.append(item) + added += 1 self._wakeup_next() + return added async def put_wait(self, item: list[Playable] | Playable | Playlist, /, *, atomic: bool = True) -> int: @@ -303,57 +313,75 @@ async def put_wait(self, item: list[Playable] | Playable | Playlist, /, *, atomi Whether the items should be inserted atomically. If set to ``True`` this method won't enter any tracks if it encounters an error. Defaults to ``True`` - Returns ------- int The amount of tracks added to the queue. """ + added: int = 0 async with self._lock: - if isinstance(item, list | Playlist): + if isinstance(item, Iterable): if atomic: - super()._check_atomic(item) + self._check_atomic(item) for track in item: try: - super()._put(track) - added += 1 + self._check_compatability(track) except TypeError: pass + else: + self.__items.append(track) + added += 1 await asyncio.sleep(0) else: - super()._put(item) + self._check_compatability(item) + self.__items.append(item) added += 1 await asyncio.sleep(0) self._wakeup_next() return added - async def delete(self, index: int, /) -> None: + def delete(self, index: int, /) -> None: """Method to delete an item in the queue by index. - This method is asynchronous and implements/waits for a lock. - Raises ------ IndexError No track exists at this index. - Examples -------- .. code:: python3 - await queue.delete(1) + queue.delete(1) # Deletes the track at index 1 (The second track). """ - async with self._lock: - self._queue.__delitem__(index) + + del self.__items[index] + + def count(self) -> int: + return len(self) + + def is_empty(self) -> bool: + return bool(self) + + def peek(self, index: int, /) -> Playable: + if not self: + raise QueueEmpty("There are no items currently in this queue.") + + return self[index] + + def swap(self, first: int, second: int, /) -> None: + self[first], self[second] = self[second], self[first] + + def index(self, item: Playable, /) -> int: + return self.__items.index(item) def shuffle(self) -> None: """Shuffles the queue in place. This does **not** return anything. @@ -366,40 +394,48 @@ def shuffle(self) -> None: player.queue.shuffle() # Your queue has now been shuffled... """ - random.shuffle(self._queue) + + random.shuffle(self.__items) def clear(self) -> None: """Remove all items from the queue. - .. note:: This does not reset the queue. Example ------- - .. code:: python3 player.queue.clear() # Your queue is now empty... """ - self._queue.clear() - @property - def mode(self) -> QueueMode: - """Property which returns a :class:`~wavelink.QueueMode` indicating which mode the - :class:`~wavelink.Queue` is in. + self.__items.clear() + + def copy(self) -> Queue: + return Queue(max_retention=self.max_retention, history=self.history is not None) - This property can be set with any :class:`~wavelink.QueueMode`. + def reset(self) -> None: + self.clear() + if self.history is not None: + self.history.clear() - .. versionadded:: 3.0.0 - """ - return self._mode + for waiter in self._waiters: + waiter.cancel() - @mode.setter - def mode(self, value: QueueMode) -> None: - if not hasattr(self, "_mode"): - raise AttributeError("This queues mode can not be set.") + self._waiters.clear() - self._mode = value + self._mode: QueueMode = QueueMode.normal + self._loaded = None + + def remove(self, item: Playable, /, count: int = 1) -> int: + deleted_count = 0 + for track in reversed(self): + if deleted_count >= count: + break + if item == track: + del track + deleted_count += 1 + return deleted_count From cde0cf638e251aa0520b86c200e010062955fb52 Mon Sep 17 00:00:00 2001 From: Thanos <111999343+Sachaa-Thanasius@users.noreply.github.com> Date: Wed, 29 Nov 2023 20:57:33 -0500 Subject: [PATCH 2/4] Run black. --- wavelink/queue.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wavelink/queue.py b/wavelink/queue.py index 0c874ff7..027c7710 100644 --- a/wavelink/queue.py +++ b/wavelink/queue.py @@ -167,7 +167,7 @@ def _wakeup_next(self) -> None: if not waiter.done(): waiter.set_result(None) break - + @staticmethod def _check_compatability(item: object) -> None: if not isinstance(item, Playable): @@ -209,7 +209,7 @@ def get(self) -> Playable: self._loaded = track return track - + def get_at(self, index: int, /) -> Playable: if not self: raise QueueEmpty("There are no items currently in this queue.") @@ -362,7 +362,7 @@ def delete(self, index: int, /) -> None: queue.delete(1) # Deletes the track at index 1 (The second track). """ - + del self.__items[index] def count(self) -> int: @@ -413,7 +413,7 @@ def clear(self) -> None: """ self.__items.clear() - + def copy(self) -> Queue: return Queue(max_retention=self.max_retention, history=self.history is not None) From 779844cf88c09ab54a5ae0dd42f9ad1565da41fe Mon Sep 17 00:00:00 2001 From: Thanos <111999343+Sachaa-Thanasius@users.noreply.github.com> Date: Wed, 29 Nov 2023 20:59:03 -0500 Subject: [PATCH 3/4] Adjust underlying Queue list access in player.py. --- wavelink/player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wavelink/player.py b/wavelink/player.py index 6d0fb59a..9150cbf2 100644 --- a/wavelink/player.py +++ b/wavelink/player.py @@ -299,7 +299,7 @@ async def _search(query: str | None) -> T_a: track._recommended = True added += await self.auto_queue.put_wait(track) - random.shuffle(self.auto_queue._queue) + random.shuffle(self.auto_queue.__items) logger.debug(f'Player "{self.guild.id}" added "{added}" tracks to the auto_queue via AutoPlay.') @property From 253adc41a5948af189ff161451739e82adf8c0fa Mon Sep 17 00:00:00 2001 From: Thanos <111999343+Sachaa-Thanasius@users.noreply.github.com> Date: Sun, 17 Dec 2023 14:39:03 -0500 Subject: [PATCH 4/4] Add a few things. - Add more docstrings. - Change the Queue's internal list to a single-underscore-prefixed name. - Add some "fast" paths when putting stuff in the queue? - Experiment with TypeGuard. Will probably be removed later. --- wavelink/player.py | 22 +++++-- wavelink/queue.py | 159 ++++++++++++++++++++++++++++++--------------- 2 files changed, 123 insertions(+), 58 deletions(-) diff --git a/wavelink/player.py b/wavelink/player.py index 9cb2e601..b557f963 100644 --- a/wavelink/player.py +++ b/wavelink/player.py @@ -91,7 +91,11 @@ def __call__(self, client: discord.Client, channel: VocalGuildChannel) -> Self: return self def __init__( - self, client: discord.Client = MISSING, channel: Connectable = MISSING, *, nodes: list[Node] | None = None + self, + client: discord.Client = MISSING, + channel: Connectable = MISSING, + *, + nodes: list[Node] | None = None, ) -> None: super().__init__(client, channel) @@ -203,7 +207,12 @@ async def _do_recommendation(self): weighted_history: list[Playable] = self.queue.history[::-1][: max(5, 5 * self._auto_weight)] weighted_upcoming: list[Playable] = self.auto_queue[: max(3, int((5 * self._auto_weight) / 3))] - choices: list[Playable | None] = [*weighted_history, *weighted_upcoming, self._current, self._previous] + choices: list[Playable | None] = [ + *weighted_history, + *weighted_upcoming, + self._current, + self._previous, + ] # Filter out tracks which are None... _previous: deque[str] = self.__previous_seeds._queue # type: ignore @@ -299,7 +308,7 @@ async def _search(query: str | None) -> T_a: track._recommended = True added += await self.auto_queue.put_wait(track) - random.shuffle(self.auto_queue.__items) + random.shuffle(self.auto_queue._items) logger.debug(f'Player "{self.guild.id}" added "{added}" tracks to the auto_queue via AutoPlay.') @property @@ -488,7 +497,12 @@ async def _dispatch_voice_update(self) -> None: logger.debug(f"Player {self.guild.id} is dispatching VOICE_UPDATE.") async def connect( - self, *, timeout: float = 10.0, reconnect: bool, self_deaf: bool = False, self_mute: bool = False + self, + *, + timeout: float = 10.0, + reconnect: bool, + self_deaf: bool = False, + self_mute: bool = False, ) -> None: """ diff --git a/wavelink/queue.py b/wavelink/queue.py index 027c7710..c5bb13ee 100644 --- a/wavelink/queue.py +++ b/wavelink/queue.py @@ -27,7 +27,7 @@ import random from collections import deque from collections.abc import Iterable, Iterator -from typing import overload +from typing import SupportsIndex, TypeGuard, overload from .enums import QueueMode from .exceptions import QueueEmpty @@ -80,7 +80,7 @@ class Queue: """ def __init__(self, *, max_retention: int = 500, history: bool = True) -> None: - self.__items: list[Playable] = [] + self._items: list[Playable] = [] self.max_retention = max_retention self._history: Queue | None = None if not history else Queue(history=False) @@ -109,6 +109,18 @@ def mode(self, value: QueueMode) -> None: def history(self) -> Queue | None: return self._history + @property + def count(self) -> int: + """The queue member count.""" + + return len(self) + + @property + def is_empty(self) -> bool: + """Whether the queue has no members.""" + + return not bool(self) + def __str__(self) -> str: return ", ".join([f'"{p}"' for p in self]) @@ -119,46 +131,44 @@ def __call__(self, item: Playable) -> None: self.put(item) def __bool__(self) -> bool: - return bool(self.__items) + return bool(self._items) @overload - def __getitem__(self, __index: int) -> Playable: + def __getitem__(self, __index: SupportsIndex, /) -> Playable: ... @overload - def __getitem__(self, __index: slice) -> list[Playable]: + def __getitem__(self, __index: slice, /) -> list[Playable]: ... - def __getitem__(self, __index: int | slice) -> Playable | list[Playable]: - return self.__items[__index] + def __getitem__(self, __index: SupportsIndex | slice, /) -> Playable | list[Playable]: + return self._items[__index] @overload - def __setitem__(self, __index: int, __value: Playable) -> None: + def __setitem__(self, __index: SupportsIndex, __value: Playable, /) -> None: ... @overload - def __setitem__(self, __index: slice, __value: Playlist | Iterable[Playable]) -> None: + def __setitem__(self, __index: slice, __value: Iterable[Playable], /) -> None: ... - def __setitem__(self, __index: int | slice, __value: Playable | Playlist | Iterable[Playable], /) -> None: - if isinstance(__value, Playlist): - __value = __value.tracks - self.__items[__index] = __value + def __setitem__(self, __index: SupportsIndex | slice, __value: Playable | Iterable[Playable], /) -> None: + self._items[__index] = __value def __delitem__(self, __index: int | slice, /) -> None: - del self.__items[__index] + del self._items[__index] def __contains__(self, __other: Playable) -> bool: - return __other in self.__items + return __other in self._items def __len__(self) -> int: - return len(self.__items) + return len(self._items) def __reversed__(self) -> Iterator[Playable]: - return reversed(self.__items) + return reversed(self._items) def __iter__(self) -> Iterator[Playable]: - return iter(self.__items) + return iter(self._items) def _wakeup_next(self) -> None: while self._waiters: @@ -169,13 +179,16 @@ def _wakeup_next(self) -> None: break @staticmethod - def _check_compatability(item: object) -> None: + def _check_compatability(item: object) -> TypeGuard[Playable]: if not isinstance(item, Playable): raise TypeError("This queue is restricted to Playable objects.") + return True - def _check_atomic(self, item: list[Playable] | Playlist) -> None: + @classmethod + def _check_atomic(cls, item: Iterable[object]) -> TypeGuard[Iterable[Playable]]: for track in item: - self._check_compatability(track) + cls._check_compatability(track) + return True def get(self) -> Playable: """Retrieve a track from the left side of the queue. E.g. the first. @@ -199,25 +212,59 @@ def get(self) -> Playable: if self.mode is QueueMode.loop_all and not self: assert self.history is not None - self.__items.extend(self.history.__items) + self._items.extend(self.history._items) self.history.clear() if not self: raise QueueEmpty("There are no items currently in this queue.") - track: Playable = self.__items.pop(0) + track: Playable = self._items.pop(0) self._loaded = track return track def get_at(self, index: int, /) -> Playable: + """Retrieve a track from the queue at a given index. + + Parameters + ---------- + index: int + The index of the track to get. + + Returns + ------- + :class:`wavelink.Playable` + The track retrieved from the queue. + + Raises + ------ + QueueEmpty + The queue was empty when retrieving a track. + IndexError + The index was out of range for the current queue. + """ + if not self: raise QueueEmpty("There are no items currently in this queue.") - return self.__items.pop(index) + return self._items.pop(index) def put_at(self, index: int, value: Playable, /) -> None: - self[index] = value + """Put a track into the queue at a given index. + + .. note:: + + This method doesn't replace the track at the index but rather inserts one there. + + Parameters + ---------- + index: int + The index to put the track at. + value: :class:`wavelink.Playable` + The track to put. + """ + + self._items.insert(index, value) async def get_wait(self) -> Playable: """This method returns the first :class:`wavelink.Playable` if one is present or @@ -257,7 +304,7 @@ async def get_wait(self) -> Playable: def put(self, item: Playable | Playlist, /, *, atomic: bool = True) -> int: """Put an item into the end of the queue. - Accepts a :class:`wavelink.Playable` or :class:`wavelink.Playlist` + Accepts a :class:`wavelink.Playable` or :class:`wavelink.Playlist`. Parameters ---------- @@ -265,12 +312,12 @@ def put(self, item: Playable | Playlist, /, *, atomic: bool = True) -> int: The item to enter into the queue. atomic: bool Whether the items should be inserted atomically. If set to ``True`` this method won't enter any tracks if - it encounters an error. Defaults to ``True`` + it encounters an error. Defaults to ``True``. Returns ------- int - The amount of tracks added to the queue. + The number of tracks added to the queue. """ added = 0 @@ -278,19 +325,23 @@ def put(self, item: Playable | Playlist, /, *, atomic: bool = True) -> int: if isinstance(item, Iterable): if atomic: self._check_atomic(item) + self._items.extend(item) + added = len(item) + else: - for track in item: - try: - self._check_compatability(track) - except TypeError: - pass - else: - self.__items.append(track) - added += 1 + def try_compatibility(track: object) -> bool: + try: + return self._check_compatability(track) + except TypeError: + return False + + passing_items = [track for track in item if try_compatibility(track)] + self._items.extend(passing_items) + added = len(passing_items) else: self._check_compatability(item) - self.__items.append(item) - added += 1 + self._items.append(item) + added = 1 self._wakeup_next() @@ -299,7 +350,7 @@ def put(self, item: Playable | Playlist, /, *, atomic: bool = True) -> int: async def put_wait(self, item: list[Playable] | Playable | Playlist, /, *, atomic: bool = True) -> int: """Put an item or items into the end of the queue asynchronously. - Accepts a :class:`wavelink.Playable` or :class:`wavelink.Playlist` or list[:class:`wavelink.Playable`] + Accepts a :class:`wavelink.Playable` or :class:`wavelink.Playlist` or list[:class:`wavelink.Playable`]. .. note:: @@ -311,12 +362,12 @@ async def put_wait(self, item: list[Playable] | Playable | Playlist, /, *, atomi The item or items to enter into the queue. atomic: bool Whether the items should be inserted atomically. If set to ``True`` this method won't enter any tracks if - it encounters an error. Defaults to ``True`` + it encounters an error. Defaults to ``True``. Returns ------- int - The amount of tracks added to the queue. + The number of tracks added to the queue. """ added: int = 0 @@ -325,6 +376,8 @@ async def put_wait(self, item: list[Playable] | Playable | Playlist, /, *, atomi if isinstance(item, Iterable): if atomic: self._check_atomic(item) + self._items.extend(item) + return len(item) for track in item: try: @@ -332,14 +385,14 @@ async def put_wait(self, item: list[Playable] | Playable | Playlist, /, *, atomi except TypeError: pass else: - self.__items.append(track) + self._items.append(track) added += 1 await asyncio.sleep(0) else: self._check_compatability(item) - self.__items.append(item) + self._items.append(item) added += 1 await asyncio.sleep(0) @@ -363,13 +416,7 @@ def delete(self, index: int, /) -> None: # Deletes the track at index 1 (The second track). """ - del self.__items[index] - - def count(self) -> int: - return len(self) - - def is_empty(self) -> bool: - return bool(self) + del self._items[index] def peek(self, index: int, /) -> Playable: if not self: @@ -381,7 +428,7 @@ def swap(self, first: int, second: int, /) -> None: self[first], self[second] = self[second], self[first] def index(self, item: Playable, /) -> int: - return self.__items.index(item) + return self._items.index(item) def shuffle(self) -> None: """Shuffles the queue in place. This does **not** return anything. @@ -395,7 +442,7 @@ def shuffle(self) -> None: # Your queue has now been shuffled... """ - random.shuffle(self.__items) + random.shuffle(self._items) def clear(self) -> None: """Remove all items from the queue. @@ -412,10 +459,14 @@ def clear(self) -> None: # Your queue is now empty... """ - self.__items.clear() + self._items.clear() def copy(self) -> Queue: - return Queue(max_retention=self.max_retention, history=self.history is not None) + """Return a shallow copy of the queue.""" + + copy_queue = Queue(max_retention=self.max_retention, history=self.history is not None) + copy_queue._items = self._items.copy() + return copy_queue def reset(self) -> None: self.clear()