An enhanced list for Python
from .main import listo  # noqa

src/listo/__init__.py
pydanny/listo
import collections
import functools
import random
import typing


class Listo(list):  # type: ignore [type-arg]
    """
    Rules:
        1. Every method must return something
        2. Cached properties must call a function and be prefixed with '_'
    """

    def __init__(self, *args: typing.Any) -> None:
        # If the first argument is an iterable, unpack it
        if len(args) == 1 and isinstance(args[0], collections.abc.Iterable):
            self.extend(list(args[0]))
        else:
            self.extend(list(args))

    @functools.cached_property
    def _length(self) -> int:
        # Returns caached length of the listo
        return len(self)

    def first(self) -> typing.Any:
        return self[0]

    def join(self, seperator: str = " ") -> str:
        # Replicates str.join()
        return seperator.join([str(x) for x in self])

    def last(self) -> typing.Any:
        return self[-1]

    def len(self) -> int:
        # Returns the length of the listo
        return self._length

    def max(self) -> typing.Any:
        return max(self)

    def median(self) -> typing.Any:
        # Provides the median value, rounding up
        index = round((self._length - 1) / 2)
        return self[index]

    def min(self) -> typing.Any:
        return min(self)

    def random_choice(self) -> typing.Any:
        # Randomly returns a value from the listo
        return random.choice(self)

    def reverse(self) -> typing.Any:
        # Overwrites the built-in reverse method so a value is returned
        return self[::-1]

    def shuffle(self) -> list[typing.Any]:
        # Shuffles and resits the listo
        return random.sample(self, self._length)

    reverse.__doc__ = "Overwrites the built-in reverse method so a value is returned"


listo = Listo

src/listo/main.py
pydanny/listo
from listo import listo


def test_first():
    lst = listo(1, 2, 3, 4)
    assert lst.first() == 1


def test_last():
    lst = listo(1, 2, 3, 4)
    assert lst.last() == 4


def test_min():
    lst = listo(1, 2, 3, 4)
    assert lst.min() == 1


def test_max():
    lst = listo(1, 2, 3, 4)
    assert lst.max() == 4


def test_median():
    lst = listo(1, 2, 3, 4)
    assert lst.median() == 3


def test_join():
    lst = listo(1, 2, 3, 4)
    assert lst.join("-") == "1-2-3-4"


def test_shuffle():
    lst = listo(range(100))
    assert lst.shuffle() != lst


def test_type():
    lst = listo(1, 2, 3, 4)
    assert isinstance(lst, list)
    assert isinstance(lst, listo)


def test_unpacking_of_single_list_arg():
    lst = listo([1, 2, 3, 4])
    assert lst == [1, 2, 3, 4]


def test_unpacking_of_single_tuple_arg():
    lst = listo((1, 2, 3, 4))
    assert lst == [1, 2, 3, 4]


def test_assignment_of_multiple_iterator_args():
    lst = listo([1, 2, 3], (4, 5, 6))
    assert lst == [[1, 2, 3], (4, 5, 6)]


def test_len():
    lst = listo(1, 2, 3, 4)
    assert lst.len() == 4


def test_reverse():
    lst = listo(range(10))
    assert lst.reverse() == list(range(9, -1, -1))


def test_random_choice():
    lst = listo(range(10))
    assert lst.random_choice() in lst

tests/test_listo.py
pydanny/listo
import json
import pathlib
import typing


def main() -> None:
    changes: dict[str, typing.Any] = json.loads(
        pathlib.Path("changelog.json").read_text()
    )
    previous_changelog: str = pathlib.Path("CHANGELOG.md").read_text()

    new_changelog: str = f"""
# [{changes["tag_name"]}]({changes['html_url']})

{changes['created_at']} by
[@{changes['author']['login']}]({changes['author']['html_url']})

## {changes["body"]}

---

{previous_changelog}
"""

    pathlib.Path("CHANGELOG.md").write_text(new_changelog)


if __name__ == "__main__":
    main()

utils/update_changelog.py
pydanny/listo