From a3c425925f3ca25d36e7a8c7700d959a06a42aa3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Ma=C5=9Blanka?= <piotr.maslanka@henrietta.com.pl>
Date: Mon, 27 Jan 2020 19:10:34 +0100
Subject: [PATCH] v2.2.17 - added coding.sequences.choose (#38)

* v2.2.17 - added coding.sequences.choose

* link to GitHub at primary page of docs

* tests for python 3.5 + code reformat

* moved requirements to requirements.txt instead of .travis.yml
---
 .travis.yml                          |  1 -
 CHANGELOG.md                         |  2 +-
 docs/coding/sequences.rst            |  6 ++++++
 docs/index.rst                       |  5 +++++
 requirements.txt                     |  2 ++
 satella/__init__.py                  |  2 +-
 satella/coding/sequences/__init__.py |  4 ++++
 satella/coding/sequences/choose.py   | 30 ++++++++++++++++++++++++++++
 tests/test_coding/test_sequences.py  | 14 +++++++++++++
 9 files changed, 63 insertions(+), 3 deletions(-)
 create mode 100644 docs/coding/sequences.rst
 create mode 100644 satella/coding/sequences/__init__.py
 create mode 100644 satella/coding/sequences/choose.py
 create mode 100644 tests/test_coding/test_sequences.py

diff --git a/.travis.yml b/.travis.yml
index 64cf253c..0835bafe 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,7 +9,6 @@ python:
 cache: pip
 install:
   - pip install -r requirements.txt
-  - pip install pyyaml toml
   - pip install --force-reinstall "coverage>=4.0,<4.4" codeclimate-test-reporter # for codeclimate-test-reporter
 script:
   - bash tests/test_posix/test_hang_until_sig.sh
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff642575..49fd0d32 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
 # v2.2.17
 
-* _TBA_
+* added [choose](satella/coding/sequences/choose.py)
 
 # v2.2.16
 
diff --git a/docs/coding/sequences.rst b/docs/coding/sequences.rst
new file mode 100644
index 00000000..b08ecbf6
--- /dev/null
+++ b/docs/coding/sequences.rst
@@ -0,0 +1,6 @@
+choose
+======
+
+To return the single element that returns true on given callable, use the following function:
+
+.. autofunction:: satella.coding.sequences.choose
diff --git a/docs/index.rst b/docs/index.rst
index 3168e26a..222e5ca4 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -2,6 +2,10 @@
 Welcome to satella's documentation!
 ===================================
 
+Visit the project's page at GitHub_!
+
+.. _GitHub: https://github.com/piotrmaslanka/satella
+
 .. toctree::
            :maxdepth: 2
            :caption: Contents
@@ -11,6 +15,7 @@ Welcome to satella's documentation!
            coding/functions
            coding/structures
            coding/concurrent
+           coding/sequences
            instrumentation/traceback
            instrumentation/metrics
            exception_handling
diff --git a/requirements.txt b/requirements.txt
index a4d92cc0..d36e77a2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,3 @@
 psutil
+pyyaml
+toml
diff --git a/satella/__init__.py b/satella/__init__.py
index 2b8a694a..342df280 100644
--- a/satella/__init__.py
+++ b/satella/__init__.py
@@ -1,2 +1,2 @@
 # coding=UTF-8
-__version__ = '2.2.17a1'
+__version__ = '2.2.17'
diff --git a/satella/coding/sequences/__init__.py b/satella/coding/sequences/__init__.py
new file mode 100644
index 00000000..7dfca4cc
--- /dev/null
+++ b/satella/coding/sequences/__init__.py
@@ -0,0 +1,4 @@
+from .choose import choose
+
+
+__all__ = ['choose']
diff --git a/satella/coding/sequences/choose.py b/satella/coding/sequences/choose.py
new file mode 100644
index 00000000..f323506c
--- /dev/null
+++ b/satella/coding/sequences/choose.py
@@ -0,0 +1,30 @@
+import typing as tp
+
+T = tp.TypeVar('T')
+
+__all__ = ['choose']
+
+
+def choose(filter_fun: tp.Callable[[T], bool], iterable: tp.Iterable[T]) -> T:
+    """
+    Return a single value that exists in given iterable
+
+    :param filter_fun: function that returns bool on the single value
+    :param iterable: iterable to examine
+    :return: single element in the iterable that matches given input
+    :raises ValueError: on multiple elements matching, or none at all
+    """
+    elem_candidate = None
+    found = False
+    for elem in iterable:
+        if filter_fun(elem):
+            if found:
+                raise ValueError(
+                    'Multiple values (%s, %s) seen' % (repr(elem_candidate), repr(elem)))
+            elem_candidate = elem
+            found = True
+
+    if not found:
+        raise ValueError('No elements matching given filter seen')
+
+    return elem_candidate
diff --git a/tests/test_coding/test_sequences.py b/tests/test_coding/test_sequences.py
new file mode 100644
index 00000000..307b756b
--- /dev/null
+++ b/tests/test_coding/test_sequences.py
@@ -0,0 +1,14 @@
+import logging
+import typing as tp
+import unittest
+
+from satella.coding.sequences import choose
+
+logger = logging.getLogger(__name__)
+
+
+class TestSequences(unittest.TestCase):
+    def test_choose(self):
+        self.assertEqual(choose(lambda x: x == 2, [1, 2, 3, 4, 5]), 2)
+        self.assertRaises(ValueError, lambda: choose(lambda x: x % 2 == 0, [1, 2, 3, 4, 5]))
+        self.assertRaises(ValueError, lambda: choose(lambda x: x == 0, [1, 2, 3, 4, 5]))
-- 
GitLab