From 0eb806efdff550057d316eb3023ab6a95ec528b5 Mon Sep 17 00:00:00 2001 From: Samer Hamood Date: Sat, 18 Jan 2025 21:50:19 +0000 Subject: [PATCH 01/15] Add parametrize to test_functional.py --- functional/test/test_functional.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/functional/test/test_functional.py b/functional/test/test_functional.py index eb28f72..f35e7d2 100644 --- a/functional/test/test_functional.py +++ b/functional/test/test_functional.py @@ -8,6 +8,8 @@ from functional.transformations import name from functional import seq, pseq +from parametrize import parametrize # type: ignore + Data = namedtuple("Data", "x y") From dbaf5249cc605c3b7cd8914fe4b81f8eec9a4383 Mon Sep 17 00:00:00 2001 From: Samer Hamood Date: Sun, 19 Jan 2025 18:03:13 +0000 Subject: [PATCH 02/15] Add doc to function --- run-tests.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/run-tests.sh b/run-tests.sh index c9dc520..8aa775b 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -1,4 +1,6 @@ +# campare_versions(v1, v2) +# Compares two 3-part sematic versions, returning -1 if v1 is less than v2, 1 if v1 is greater than v2 or 0 if v1 and v2 are equal. compare_versions() { local v1=(${1//./ }) local v2=(${2//./ }) From b6e03a45363ee3a1b8fce1f438b1898e6cbd5cda Mon Sep 17 00:00:00 2001 From: Samer Hamood Date: Sun, 19 Jan 2025 17:50:39 +0000 Subject: [PATCH 03/15] Add function to capitalize words --- run-tests.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/run-tests.sh b/run-tests.sh index 8aa775b..1e08746 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -22,6 +22,13 @@ compare_versions() { python_version=$(python --version | grep -Eo \[0-9\]\.\[0-9\]+\.\[0-9\]+) echo "Python version: $python_version" +# capitalise(word) +# Capitalizes a word. +capitalize() { + local word=$1 + echo "$(tr '[:lower:]' '[:upper:]' <<< ${word:0:1})${word:1}" +} + pipx_version=$(pipx --version) if [[ -z "$pipx_version" ]]; then From e8c43829eb8eaa0fccbf55a7ff5a01b5fe2db4ae Mon Sep 17 00:00:00 2001 From: Samer Hamood Date: Sun, 19 Jan 2025 17:52:06 +0000 Subject: [PATCH 04/15] Add function to get sematic version of a package installed in Pipx --- run-tests.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/run-tests.sh b/run-tests.sh index 1e08746..7077a90 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -20,8 +20,15 @@ compare_versions() { echo 0 } -python_version=$(python --version | grep -Eo \[0-9\]\.\[0-9\]+\.\[0-9\]+) -echo "Python version: $python_version" +# get_version_in_pipx(package_name) +# Gets the standard semantic version of a package installed in Pipx if installed. +get_version_in_pipx() { + local package_name=$1 + local version + version=$(pipx list | grep -oP "$package_name"\\s+\\K\[0-9\]+\.\[0-9\]+\.\[0-9\]+) + echo "$version" +} + # capitalise(word) # Capitalizes a word. capitalize() { From 7ca869ba21baa6864bbd659bff34ec99c4240f96 Mon Sep 17 00:00:00 2001 From: Samer Hamood Date: Sun, 19 Jan 2025 17:54:56 +0000 Subject: [PATCH 05/15] Add function to print software and version --- run-tests.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/run-tests.sh b/run-tests.sh index 7077a90..e31a5d6 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -36,6 +36,17 @@ capitalize() { echo "$(tr '[:lower:]' '[:upper:]' <<< ${word:0:1})${word:1}" } +# print_version(name, version, capitalize, width) +# Prints the version of the software with option to capitalize name and change left-aligned padding. +print_version() { + local name=$1 + local version=$2 + local capitalize=${3:-true} + local width=${4:-19} + name=$([[ $capitalize == 'true' ]] && capitalize "$name" || echo "$name") + printf "%-${width}s %s\n" "$name version:" "$version" +} + pipx_version=$(pipx --version) if [[ -z "$pipx_version" ]]; then From 18c041c9c4f3a26db338e455569bfb146dc0da3d Mon Sep 17 00:00:00 2001 From: Samer Hamood Date: Sun, 19 Jan 2025 17:56:02 +0000 Subject: [PATCH 06/15] Add function to install a package --- run-tests.sh | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/run-tests.sh b/run-tests.sh index e31a5d6..c323e65 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -47,6 +47,22 @@ print_version() { printf "%-${width}s %s\n" "$name version:" "$version" } +# install_package(package_name) +# Installs specified package with Pipx or displays the its version if it's already installed. +install_package() { + local package_name=$1 + local capitalize=${2:-true} + + local version + version=$(get_version_in_pipx "$package_name") + if [[ -n $version ]]; then + print_version "$package_name" "$version" "$capitalize" + else + pipx install "$package_name" + pipx ensurepath + fi +} + pipx_version=$(pipx --version) if [[ -z "$pipx_version" ]]; then From 8aac16ccb2a933ca7da6ecf183af6723248d6c5e Mon Sep 17 00:00:00 2001 From: Samer Hamood Date: Sun, 19 Jan 2025 17:58:42 +0000 Subject: [PATCH 07/15] Improve Pipx installation message --- run-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run-tests.sh b/run-tests.sh index c323e65..5942bc3 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -66,7 +66,7 @@ install_package() { pipx_version=$(pipx --version) if [[ -z "$pipx_version" ]]; then - echo "Pipx is not installed" + echo "Please install Pipx before running this script." exit 1 else echo "Pipx version: $pipx_version" From 0392a1c935cf871391f3ba0fa34314afae5f00be Mon Sep 17 00:00:00 2001 From: Samer Hamood Date: Sun, 19 Jan 2025 17:59:49 +0000 Subject: [PATCH 08/15] Print correctly aligned Pipx version --- run-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run-tests.sh b/run-tests.sh index 5942bc3..f720d63 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -69,7 +69,7 @@ if [[ -z "$pipx_version" ]]; then echo "Please install Pipx before running this script." exit 1 else - echo "Pipx version: $pipx_version" + print_version "Pipx" "$pipx_version" fi poetry_version=$(pipx list | grep -oP poetry\\s+\\K\[0-9\]\.\[0-9\]+\.\[0-9\]+) From 194b8ae401a4629b57f7eaabf043c9d91bb1f4ca Mon Sep 17 00:00:00 2001 From: Samer Hamood Date: Sun, 19 Jan 2025 18:00:52 +0000 Subject: [PATCH 09/15] Use new function to install packages if missing or print version --- run-tests.sh | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/run-tests.sh b/run-tests.sh index f720d63..1dc9230 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -72,12 +72,9 @@ else print_version "Pipx" "$pipx_version" fi -poetry_version=$(pipx list | grep -oP poetry\\s+\\K\[0-9\]\.\[0-9\]+\.\[0-9\]+) -if [[ -n $poetry_version ]]; then - echo "Poetry version: $poetry_version" -else - pipx install poetry -fi +install_package "poetry" + +install_package "pre-commit" false echo From 293317e54b304ef1566df1d6d291c7f009628132 Mon Sep 17 00:00:00 2001 From: Samer Hamood Date: Sun, 19 Jan 2025 18:02:45 +0000 Subject: [PATCH 10/15] Put remaining standalone commands in a main function --- run-tests.sh | 58 +++++++++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/run-tests.sh b/run-tests.sh index 1dc9230..83e3796 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -63,42 +63,48 @@ install_package() { fi } +main() { + python_version=$(python --version | grep -Eo \[0-9\]\.\[0-9\]+\.\[0-9\]+) + print_version "Python" "$python_version" + + pipx_version=$(pipx --version) + if [[ -z "$pipx_version" ]]; then + echo "Please install Pipx before running this script." + exit 1 + else + print_version "Pipx" "$pipx_version" + fi -pipx_version=$(pipx --version) -if [[ -z "$pipx_version" ]]; then - echo "Please install Pipx before running this script." - exit 1 -else - print_version "Pipx" "$pipx_version" -fi + install_package "poetry" -install_package "poetry" + install_package "pre-commit" false -install_package "pre-commit" false + echo -echo + if ! poetry install; then + poetry lock + poetry install + fi -if ! poetry install; then - poetry lock - poetry install -fi + echo -echo + if [[ $(compare_versions "$python_version" "3.12.0") -lt 0 ]]; then + poetry run pylint functional + else + poetry run ruff check functional + fi -if [[ $(compare_versions "$python_version" "3.12.0") -lt 0 ]]; then - poetry run pylint functional -else - poetry run ruff check functional -fi + echo -echo + poetry run black --diff --color --check functional -poetry run black --diff --color --check functional + echo -echo + poetry run mypy functional -poetry run mypy functional + echo -echo + poetry run pytest +} -poetry run pytest \ No newline at end of file +main "$@" \ No newline at end of file From f20bd3590c6f548e69f368d6bd46ce8904e3a64f Mon Sep 17 00:00:00 2001 From: Samer Hamood Date: Sun, 19 Jan 2025 18:03:58 +0000 Subject: [PATCH 11/15] Upgrade pre-commit-hooks to latest version --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e0818f4..373d7a4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-yaml - id: check-added-large-files From b8f3ad520e631f79a9233f2847f957d69698d436 Mon Sep 17 00:00:00 2001 From: Samer Hamood Date: Sun, 19 Jan 2025 18:05:24 +0000 Subject: [PATCH 12/15] Upgrade ruff-pre-commit to the highest version before failing --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 373d7a4..c6d7c73 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,6 +23,6 @@ repos: pass_filenames: false - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.2 + rev: v0.6.0 hooks: - id: ruff From bb741d387879301d6b8a4d5792ac1b7e4743cc21 Mon Sep 17 00:00:00 2001 From: Samer Hamood Date: Tue, 14 Jan 2025 18:27:48 +0000 Subject: [PATCH 13/15] Add head_option/first_option and last_option functions to return the first and last elements respectively wrapped in an Option to handle the case when the Sequence is empty --- functional/pipeline.py | 175 ++++++++++++++++++++++++++++- functional/test/test_functional.py | 80 ++++++++++++- functional/test/test_type.py | 6 +- 3 files changed, 255 insertions(+), 6 deletions(-) diff --git a/functional/pipeline.py b/functional/pipeline.py index a6ef89f..f9a21ef 100644 --- a/functional/pipeline.py +++ b/functional/pipeline.py @@ -3,6 +3,7 @@ """ from __future__ import annotations +from dataclasses import dataclass from operator import mul, add import collections from functools import reduce, wraps, partial @@ -12,9 +13,8 @@ import sqlite3 import re -from typing import TYPE_CHECKING import typing -from typing import NamedTuple, TypeVar, Generic, overload +from typing import NamedTuple, TypeVar, Generic, overload, TYPE_CHECKING from tabulate import tabulate @@ -329,6 +329,21 @@ def head_or_none(self, no_wrap: bool | None = None) -> _T_co | None: return None return self.head(no_wrap=no_wrap) + def head_option(self): + """ + Returns an Option with the head of the sequence, + or an empty Option if sequence is empty. + + >>> seq([1, 2, 3]).head_option() + Option(1) + + >>> seq([]).head_option() + Option(None) + + :return: Option containing the head or None + """ + return Option(None if self.empty() else self.head_or_none(True)) + def first_or_none(self, no_wrap: bool | None = None) -> _T_co | None: """ Returns the first element of the sequence or None, if the sequence is empty. @@ -344,6 +359,21 @@ def first_or_none(self, no_wrap: bool | None = None) -> _T_co | None: """ return self.head_or_none(no_wrap=no_wrap) + def first_option(self): + """ + Returns an Option with the first element of the sequence, + or an empty Option if sequence is empty. + + >>> seq([1, 2, 3]).head_option() + Option(1) + + >>> seq([]).head_option() + Option(None) + + :return: Option containing the first element or None + """ + return self.head_option() + def last(self, no_wrap: bool | None = None) -> _T_co: """ Returns the last element of the sequence. @@ -383,6 +413,21 @@ def last_or_none(self, no_wrap: bool | None = None) -> _T_co | None: return None return self.last(no_wrap=no_wrap) + def last_option(self): + """ + Returns an Option with the last element of the sequence, + or an empty Option if the sequence is empty. + + >>> seq([1, 2, 3]).head_option() + Option(3) + + >>> seq([]).head_option() + Option(None) + + :return: Option containing the last element or None + """ + return Option(None if self.empty() else self.last_or_none(True)) + def init(self) -> Sequence[_T_co]: """ Returns the sequence, without its last element. @@ -2090,6 +2135,132 @@ def tabulate( ) +@dataclass +class Option(Generic[_T_co]): + """ + An Option is a container for a single value, or None if no value is available. + When an Option contains a value, it provides a number of functions from Sequence + that can be executed on that value. + """ + + value: _T_co + seq: Sequence[_T_co] + + def __init__(self, value: _T_co): + self.value = value + if value is None: + self.seq = Sequence[_T_co]([]) + elif isinstance(value, list): + self.seq = Sequence(value) + elif isinstance(value, Sequence): + self.seq = value + else: + self.seq = Sequence[_T_co]([value]) + + def __str__(self): + return f"Option({self.value})" + + def __repr__(self): + return self.__str__() + + def map(self, func: Callable[[_T_co], _T]) -> Option[_T]: + """ + Maps func onto this Option's value, or returns this Option if empty. + + >>> Option(1).map(lambda x: x * -1) + Option(-1) + + >>> Option(None).map(lambda x: x * -1) + Option(None) + + :param func: function to map with + :return: Option with func mapped onto its value, or this Option if empty + """ + return self.seq.map(func).head_option() + + def flat_map(self, func: Callable[[_T_co], Iterable[_T]]) -> Sequence[_T]: + """ + Applies func on each element of this Option's value then flattens the value, + returning a Sequence or raising an error if the Option's value is not a list or Sequence. + If Option is empty, it's returned instead. + + >>> Option([[1, 2], [3, 4], [5, 6]]).flat_map(lambda x: x) + [1, 2, 3, 4, 5, 6] + + >>> Option(["a", "bc", "def"]).flat_map(list) + ['a', 'b', 'c', 'd', 'e', 'f'] + + >>> Option([[1], [2], [3]]).flat_map(lambda x: x * 2) + [1, 1, 2, 2, 3, 3] + + >>> Option(None).flat_map(lambda x: x * -1) + Option(None) + + :param func: function to apply to the value of this Option + :return: sequence resulting from value mapped to func then flattered, + or empty Sequence if Option is empty + :raise ValueError: if the value of Option is not a list or Sequence + """ + if not isinstance(self.value, (list, Sequence)): + raise ValueError("Single values cannot be converted to a Sequence") + return self.seq.flat_map(func) + + def flatten(self) -> Sequence[_T]: + """ + Flattens the value of this Option by taking the elements of its element + and putting them into a new Sequence. + + :return: value of Option as a sequence + :raise ValueError: if the value of Option is not a list or Sequence + """ + return self.flat_map(identity) + + def plus(self, sequence: list | Sequence) -> Sequence: + if isinstance(sequence, Sequence): + seq = sequence + elif isinstance(sequence, list): + seq = Sequence(sequence) + else: + raise ValueError("sequence should be o list or Sequence") + return self.seq + seq + + def non_empty(self): + """ + Returns True if this Option contains a value, or False if no value is contained. + + :return: True if Option has a value, or False if empty + """ + return self.seq.non_empty() + + def empty(self): + """ + Returns True if this Option does not contain a value, or False if a value is contained. + + :return: True if Option is empty, or False if value is present + """ + return self.seq.empty() + + def or_else(self, other: Any) -> _T_co: + """ + Returns this Option's value, or `other` if Option is empty. + + :param other: the value to return if no value is present + :return: Option's value if present, else `other` + """ + return self.value if self.non_empty() else other + + def or_raise_error(self) -> _T_co: + """ + Returns this Option's value, or raises an Error. + + :return: Option's value if present + :raise ValueError: if Option is empty + """ + if self.empty(): + raise ValueError("Option is empty") + return self.value + + _PrimitiveT = TypeVar("_PrimitiveT", str, bool, float, complex, bytes, int) _NamedTupleT = TypeVar("_NamedTupleT", bound=NamedTuple) _DictT = TypeVar("_DictT", bound=dict) diff --git a/functional/test/test_functional.py b/functional/test/test_functional.py index f35e7d2..0df2ca1 100644 --- a/functional/test/test_functional.py +++ b/functional/test/test_functional.py @@ -4,7 +4,9 @@ from collections import namedtuple, deque from itertools import product -from functional.pipeline import Sequence, is_iterable, _wrap, extend +from parametrize import parametrize # type: ignore + +from functional.pipeline import Sequence, is_iterable, _wrap, extend, Option from functional.transformations import name from functional import seq, pseq @@ -27,8 +29,8 @@ class TestPipeline(unittest.TestCase): def setUp(self): self.seq = seq - def assert_type(self, s): - self.assertTrue(isinstance(s, Sequence)) + def assert_type(self, s, t: type = Sequence): + self.assertTrue(isinstance(s, t)) def assert_not_type(self, s): self.assertFalse(isinstance(s, Sequence)) @@ -201,6 +203,42 @@ def test_head_or_none(self): l = self.seq([deque(), deque()], no_wrap=True).head_or_none(no_wrap=False) self.assert_type(l) + @parametrize( + "sequence, present, head, mapped_value, flat_mapped_value", + [ + ([], False, None, None, []), + ([1, 2, 3, 4, 5], True, 1, 0, None), + ([[[1, 2]], 3, 4, 5], True, [[1, 2]], 0, [1, 2]), + ], + ) + def test_head_option( + self, sequence, present, head, mapped_value, flat_mapped_value + ): + head_option = self.seq(sequence).head_option() + self.assert_type(head_option, Option) + self.assertEqual(present, head_option.non_empty()) + self.assertEqual(not present, head_option.empty()) + self.assertEqual(head, head_option.or_else(None)) + if head is None: + h = [] + elif isinstance(head, list): + h = head + else: + h = [head] + s = [2, 3, 4] + self.assertEqual(self.seq(h + s), head_option.plus(s)) + self.assert_type(head_option.plus(s)) + self.assertEqual(mapped_value, head_option.map(lambda x: 0).or_else(None)) + self.assert_type(head_option.map(lambda x: 0), Option) + if flat_mapped_value: + self.assertEqual(self.seq(flat_mapped_value), head_option.flatten()) + self.assert_type(head_option.flat_map(lambda x: x)) + if sequence: + self.assertEqual(head, head_option.or_raise_error()) + else: + with self.assertRaises(ValueError): + head_option.or_raise_error() + def test_last(self): l = self.seq([1, 2, 3]).map(lambda x: x) self.assertEqual(l.last(), 3) @@ -242,6 +280,42 @@ def test_last_or_none(self): l = self.seq([deque(), deque()], no_wrap=True).last_or_none(no_wrap=False) self.assert_type(l) + @parametrize( + "sequence, present, last, mapped_value, flat_mapped_value", + [ + ([], False, None, None, []), + ([1, 2, 3, 4, 5], True, 5, 0, None), + ([1, 2, 3, [[4, 5]]], True, [[4, 5]], 0, [4, 5]), + ], + ) + def test_last_option( + self, sequence, present, last, mapped_value, flat_mapped_value + ): + last_option = self.seq(sequence).last_option() + self.assert_type(last_option, Option) + self.assertEqual(present, last_option.non_empty()) + self.assertEqual(not present, last_option.empty()) + self.assertEqual(last, last_option.or_else(None)) + if last is None: + l = [] + elif isinstance(last, list): + l = last + else: + l = [last] + s = [2, 3, 4] + self.assertEqual(self.seq(l + s), last_option.plus(s)) + self.assert_type(last_option.plus(s)) + self.assertEqual(mapped_value, last_option.map(lambda x: 0).or_else(None)) + self.assert_type(last_option.map(lambda x: 0), Option) + if flat_mapped_value: + self.assertEqual(self.seq(flat_mapped_value), last_option.flatten()) + self.assert_type(last_option.flat_map(lambda x: x)) + if sequence: + self.assertEqual(last, last_option.or_raise_error()) + else: + with self.assertRaises(ValueError): + last_option.or_raise_error() + def test_init(self): result = self.seq([1, 2, 3, 4]).map(lambda x: x).init() expect = [1, 2, 3] diff --git a/functional/test/test_type.py b/functional/test/test_type.py index c9d0644..52364ae 100644 --- a/functional/test/test_type.py +++ b/functional/test/test_type.py @@ -6,7 +6,7 @@ if TYPE_CHECKING: from typing import Any, Iterator from functional import seq - from functional.pipeline import Sequence + from functional.pipeline import Sequence, Option from pandas import DataFrame def type_checking() -> None: @@ -30,12 +30,16 @@ def type_checking() -> None: t_head_or_none: int | None = seq([1, 2, 3]).head_or_none() + t_head_option: Option[int] = seq([1, 2, 3]).head_option() + t_first_or_none: int | None = seq([1, 2, 3]).first_or_none() t_last: int = seq([1, 2, 3]).last() t_last_or_none: int | None = seq([1, 2, 3]).last_or_none() + t_last_option: Option[int] = seq([1, 2, 3]).last_option() + t_init: Sequence[int] = seq([1, 2, 3]).init() t_tail: Sequence[int] = seq([1, 2, 3]).tail() From a16ed315e8574885535fdda37ac8c11cd933c3d1 Mon Sep 17 00:00:00 2001 From: Samer Hamood Date: Tue, 14 Jan 2025 18:46:19 +0000 Subject: [PATCH 14/15] Add head_option/first_option and last_option functions to Transformations and Actions APIs table --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f6c0e32..ba5e782 100644 --- a/README.md +++ b/README.md @@ -382,8 +382,10 @@ complete documentation reference | `slice(start, until)` | Sequence starting at `start` and including elements up to `until` | transformation | | `head(no_wrap=None)` / `first(no_wrap=None)` | Returns first element in sequence (if `no_wrap=True`, the result will never be wrapped with `Sequence`) | action | | `head_or_none(no_wrap=None)` / `first_or_none(no_wrap=None)` | Returns first element in sequence or `None` if it's empty (if `no_wrap=True`, the result will never be wrapped with `Sequence`) | action | +| `head_option()` / `first_option()` | Returns an `Option` containing the first element in the sequence or `None` if it's empty | action | | `last(no_wrap=None)` | Returns last element in sequence (if `no_wrap=True`, the result will never be wrapped with `Sequence`) | action | | `last_or_none(no_wrap=None)` | Returns last element in sequence or `None` if it's empty (if `no_wrap=True`, the result will never be wrapped with `Sequence`) | action | +| `last_option()` | Returns an `Option` containing the last element in the sequence or `None` if it's empty | action | | `len()` / `size()` | Returns length of sequence | action | | `count(func)` | Returns count of elements in sequence where `func(element)` is True | action | | `empty()` | Returns `True` if the sequence has zero length | action | From a5e90ea5b360116a57fa8a6be66e43a287b925dd Mon Sep 17 00:00:00 2001 From: Samer Hamood Date: Mon, 20 Jan 2025 21:38:28 +0000 Subject: [PATCH 15/15] Add new option functions to CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3fb0d4..da6737b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ### New Features +- Reimplemented `head_option`/`first_option` and `last_option` to return an `Option` instead of `None` - Added `first_or_none`, a function to match `head_or_none` - Added run_test.sh script - Added [parametrize](https://pypi.org/project/parametrize/) for parameterized unit tests