diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e0818f4..c6d7c73 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 @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index c3fb0d4..311a5a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ### New Features +- Implemented the `plus` function that concatenates iterables and single elements as well as other sequences - 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 diff --git a/README.md b/README.md index f6c0e32..44d6757 100644 --- a/README.md +++ b/README.md @@ -366,6 +366,8 @@ complete documentation reference | `tail()` | Returns sequence without the first element | transformation | | `inits()` | Returns consecutive inits of sequence | transformation | | `tails()` | Returns consecutive tails of sequence | transformation | +| `plus(other)` | Concatenates this sequence with `other` | transformation | +| `append(other)` | Synonym for `plus()` | transformation | | `zip(other)` | Zips the sequence with `other` | transformation | | `zip_with_index(start=0)` | Zips the sequence with the index starting at `start` on the right side | transformation | | `enumerate(start=0)` | Zips the sequence with the index starting at `start` on the left side | transformation | diff --git a/functional/pipeline.py b/functional/pipeline.py index a6ef89f..b590493 100644 --- a/functional/pipeline.py +++ b/functional/pipeline.py @@ -427,6 +427,36 @@ def tails(self) -> Sequence[Self]: """ return self._transform(transformations.tails_t(_wrap)) + def plus(self, other: Sequence[Any] | Iterable[Any] | Any) -> Sequence[Any]: + """ + Concatenates sequence with other. + + >>> seq([1, 2, 3]).plus(4) + [1, 2, 3, 4] + + >>> seq([1, 2, 3]).plus([4, 5, 6]) + [1, 2, 3, 4, 5, 6] + + :param other: single element or sequence to concatenate + :return: concatenated sequence with other + """ + return self._transform(transformations.plus_t(other)) + + def append(self, other: Sequence[Any] | Iterable[Any] | Any) -> Sequence[Any]: + """ + Concatenates sequence with other. + + >>> seq([1, 2, 3]).plus(4) + [1, 2, 3, 4] + + >>> seq([1, 2, 3]).plus([4, 5, 6]) + [1, 2, 3, 4, 5, 6] + + :param other: single element or sequence to concatenate + :return: concatenated sequence with other + """ + return self._transform(transformations.plus_t(other, "append")) + @overload def cartesian(self, /) -> Sequence[tuple[_T_co]]: ... diff --git a/functional/test/test_functional.py b/functional/test/test_functional.py index eb28f72..5296ba8 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") @@ -262,6 +264,22 @@ def test_tails(self): self.assertIteratorEqual(l.tails(), expect) self.assertIteratorEqual(l.tails().map(lambda s: s.sum()), [6, 5, 3, 0]) + @parametrize( + "sequence, other, expected", + [ + ([1, 2, 3], 4, [1, 2, 3, 4]), + ([1, 2], [3, 4], [1, 2, 3, 4]), + ([1, 2], seq(3, 4), [1, 2, 3, 4]), + ([1, 2], [[3, 4]], [1, 2, [3, 4]]), + ([1, 2], [], [1, 2]), + ([], [], []), + ], + ) + def test_plus(self, sequence, other, expected): + result = self.seq(sequence).plus(other) + self.assertIteratorEqual(expected, result) + self.assert_type(result) + def test_drop(self): s = self.seq([1, 2, 3, 4, 5, 6]) expect = [5, 6] diff --git a/functional/test/test_type.py b/functional/test/test_type.py index c9d0644..d79ffae 100644 --- a/functional/test/test_type.py +++ b/functional/test/test_type.py @@ -44,6 +44,10 @@ def type_checking() -> None: t_tails: Sequence[Sequence[int]] = seq([1, 2, 3]).tails() + t_plus: Sequence[int] = seq([1, 2, 3]).plus(seq([4, 5, 6])) + + t_append: Sequence[int] = seq([1, 2, 3]).append(seq([4, 5, 6])) + t_cartesian: Sequence[tuple[int, int]] = seq.range(2).cartesian(range(2)) t_drop: Sequence[int] = seq([1, 2, 3, 4, 5]).drop(2) diff --git a/functional/transformations.py b/functional/transformations.py index 22ac477..39f9fbd 100644 --- a/functional/transformations.py +++ b/functional/transformations.py @@ -11,7 +11,7 @@ ) import collections import types -from collections.abc import Callable +from collections.abc import Callable, Iterable from functional.execution import ExecutionStrategies @@ -376,6 +376,29 @@ def tails_t(wrap): ) +def plus_t(other, function_name="plus"): + """ + Transformation for Sequence.plus + :param other: single element or sequence to concatenate + :param function_name: name of pipeline function + :return: transformation + """ + + def plus(sequence): + class_name = f"{other.__class__.__module__}.{other.__class__.__name__}" + if class_name == "functional.pipeline.Sequence": + return sequence + other.to_list() + if isinstance(other, Iterable): + return sequence + other + return sequence + [other] + + return Transformation( + f"{function_name}({other})", + plus, + None, + ) + + def union_t(other): """ Transformation for Sequence.union diff --git a/run-tests.sh b/run-tests.sh index c9dc520..83e3796 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//./ }) @@ -18,47 +20,91 @@ 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() { + local word=$1 + 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" +} + +# 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} -pipx_version=$(pipx --version) -if [[ -z "$pipx_version" ]]; then - echo "Pipx is not installed" - exit 1 -else - echo "Pipx version: $pipx_version" -fi + 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 +} + +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 -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" -echo + install_package "pre-commit" false -if ! poetry install; then - poetry lock - poetry install -fi + echo -echo + if ! poetry install; then + poetry lock + poetry install + 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 + if [[ $(compare_versions "$python_version" "3.12.0") -lt 0 ]]; then + poetry run pylint functional + else + poetry run ruff check functional + fi -poetry run black --diff --color --check functional + echo -echo + poetry run black --diff --color --check functional -poetry run mypy functional + echo -echo + poetry run mypy functional + + echo + + poetry run pytest +} -poetry run pytest \ No newline at end of file +main "$@" \ No newline at end of file