From e32a968dae8eb0dbfb50b7d774202b0ae8e8fd32 Mon Sep 17 00:00:00 2001 From: Karl Kroening Date: Mon, 12 Jun 2017 03:17:29 -0600 Subject: [PATCH 1/5] Add `select_ok` function to detect closed channels during select --- goless/__init__.py | 2 +- goless/selecting.py | 57 ++++++++++++++++++++++++++++++++------------ tests/test_select.py | 27 +++++++++++++++++++++ 3 files changed, 70 insertions(+), 16 deletions(-) diff --git a/goless/__init__.py b/goless/__init__.py index 27ad68f..92d5480 100644 --- a/goless/__init__.py +++ b/goless/__init__.py @@ -19,7 +19,7 @@ # noinspection PyUnresolvedReferences from .channels import chan, ChannelClosed # noinspection PyUnresolvedReferences -from .selecting import dcase, rcase, scase, select +from .selecting import dcase, rcase, scase, select, select_ok version_info = 0, 7, 2 diff --git a/goless/selecting.py b/goless/selecting.py index 29c0114..6b9b4a8 100644 --- a/goless/selecting.py +++ b/goless/selecting.py @@ -1,4 +1,5 @@ from .backends import current as _be, Deadlock as _Deadlock +from .channels import ChannelClosed # noinspection PyPep8Naming,PyShadowingNames @@ -10,7 +11,7 @@ def __init__(self, chan): self.chan = chan def ready(self): - return self.chan.recv_ready() + return self.chan._closed or self.chan.recv_ready() def exec_(self): return self.chan.recv() @@ -25,7 +26,7 @@ def __init__(self, chan, value): self.value = value def ready(self): - return self.chan.send_ready() + return self.chan._closed or self.chan.send_ready() def exec_(self): self.chan.send(self.value) @@ -38,20 +39,15 @@ def ready(self): return False -def select(*cases): +def select_ok(*cases): """ - Select the first case that becomes ready. - If a default case (:class:`goless.dcase`) is present, - return that if no other cases are ready. - If there is no default case and no case is ready, - block until one becomes ready. - - See Go's ``reflect.Select`` method for an analog - (http://golang.org/pkg/reflect/#Select). + Select the first case that becomes ready, including an ``ok`` indication. + This is the same as the ``select`` method except than an ``ok`` indication + is included allowing checks for closed channels. :param cases: List of case instances, such as :class:`goless.rcase`, :class:`goless.scase`, or :class:`goless.dcase`. - :return: ``(chosen case, received value)``. + :return: ``(chosen case, received value, ok indication)``. If the chosen case is not an :class:`goless.rcase`, it will be None. """ if len(cases) == 0: @@ -70,13 +66,16 @@ def select(*cases): default = None for c in cases: if c.ready(): - return c, c.exec_() + try: + return c, c.exec_(), True + except ChannelClosed: + return c, None, False if isinstance(c, dcase): assert default is None, 'Only one default case is allowd.' default = c if default is not None: # noinspection PyCallingNonCallable - return default, None + return default, None, True # We need to check for deadlocks before selecting. # We can't rely on the underlying backend to do it, @@ -89,5 +88,33 @@ def select(*cases): while True: for c in cases: if c.ready(): - return c, c.exec_() + try: + return c, c.exec_(), True + except ChannelClosed: + return c, None, False _be.yield_() + + +def select(*cases): + """ + Select the first case that becomes ready. + If a default case (:class:`goless.dcase`) is present, + return that if no other cases are ready. + If there is no default case and no case is ready, + block until one becomes ready. + + See Go's ``reflect.Select`` method for an analog + (http://golang.org/pkg/reflect/#Select). + + :param cases: List of case instances, such as + :class:`goless.rcase`, :class:`goless.scase`, or :class:`goless.dcase`. + :return: ``(chosen case, received value)``. + If the chosen case is not an :class:`goless.rcase`, it will be None. + """ + result = select_ok(*cases) + if result is not None: + chosen, value, ok = result + if not ok: + raise ChannelClosed() + result = chosen, value + return result diff --git a/tests/test_select.py b/tests/test_select.py index aa1a659..296b2f4 100644 --- a/tests/test_select.py +++ b/tests/test_select.py @@ -93,6 +93,33 @@ def test_select_chooses_ready_selection(self): self.assertIs(result, cases[1]) self.assertEqual(val, 3) + def test_select_ok_default_true(self): + cases = [goless.rcase(self.chan1), goless.dcase()] + result, val, ok = goless.select_ok(cases) + self.assertIs(result, cases[1]) + self.assertTrue(ok) + + def test_select_ok_chooses_closed_over_default(self): + readychan = goless.chan(1) + readychan.send(3) + readychan.close() + cases = [goless.rcase(readychan), goless.dcase()] + + result, val, ok = goless.select_ok(cases) + self.assertIs(result, cases[0]) + self.assertEqual(val, 3) + self.assertTrue(ok) + + result, val, ok = goless.select_ok(cases) + self.assertIs(result, cases[0]) + self.assertIsNone(val) + self.assertFalse(ok) + + result, val, ok = goless.select_ok(cases) + self.assertIs(result, cases[0]) + self.assertIsNone(val) + self.assertFalse(ok) + def test_select_no_default_no_ready_blocks(self): chan1 = goless.chan() chan2 = goless.chan() From d041a314faf6ea8073b07b14212b8f7995b1fce7 Mon Sep 17 00:00:00 2001 From: Karl Kroening Date: Mon, 12 Jun 2017 03:23:27 -0600 Subject: [PATCH 2/5] Ensure ChannelClosed is raised when selecting on a closed channel --- tests/test_select.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_select.py b/tests/test_select.py index 296b2f4..1699480 100644 --- a/tests/test_select.py +++ b/tests/test_select.py @@ -1,5 +1,6 @@ import goless from goless.backends import current as be +from goless.channels import ChannelClosed from . import BaseTests @@ -93,7 +94,7 @@ def test_select_chooses_ready_selection(self): self.assertIs(result, cases[1]) self.assertEqual(val, 3) - def test_select_ok_default_true(self): + def test_select_ok_default_is_ok(self): cases = [goless.rcase(self.chan1), goless.dcase()] result, val, ok = goless.select_ok(cases) self.assertIs(result, cases[1]) @@ -120,6 +121,12 @@ def test_select_ok_chooses_closed_over_default(self): self.assertIsNone(val) self.assertFalse(ok) + def test_select_raises_if_closed(self): + self.chan1.close() + cases = [goless.rcase(self.chan1), goless.dcase()] + with self.assertRaises(ChannelClosed): + goless.select(cases) + def test_select_no_default_no_ready_blocks(self): chan1 = goless.chan() chan2 = goless.chan() From d3ede77f9edec9140e485ec47cc931c795588b62 Mon Sep 17 00:00:00 2001 From: Karl Kroening Date: Mon, 12 Jun 2017 03:37:08 -0600 Subject: [PATCH 3/5] Ignore scase/rcase with chan=None --- goless/selecting.py | 4 ++-- tests/test_select.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/goless/selecting.py b/goless/selecting.py index 6b9b4a8..c2266cf 100644 --- a/goless/selecting.py +++ b/goless/selecting.py @@ -11,7 +11,7 @@ def __init__(self, chan): self.chan = chan def ready(self): - return self.chan._closed or self.chan.recv_ready() + return self.chan is not None and (self.chan._closed or self.chan.recv_ready()) def exec_(self): return self.chan.recv() @@ -26,7 +26,7 @@ def __init__(self, chan, value): self.value = value def ready(self): - return self.chan._closed or self.chan.send_ready() + return self.chan is not None and (self.chan._closed or self.chan.send_ready()) def exec_(self): self.chan.send(self.value) diff --git a/tests/test_select.py b/tests/test_select.py index 1699480..b592ff5 100644 --- a/tests/test_select.py +++ b/tests/test_select.py @@ -100,6 +100,12 @@ def test_select_ok_default_is_ok(self): self.assertIs(result, cases[1]) self.assertTrue(ok) + def test_select_ok_ignores_null_chan(self): + cases = [goless.rcase(None), goless.dcase()] + result, val, ok = goless.select_ok(cases) + self.assertIs(result, cases[1]) + self.assertTrue(ok) + def test_select_ok_chooses_closed_over_default(self): readychan = goless.chan(1) readychan.send(3) From 7e928343e688d6dace995afb1a22a97e98dfc10e Mon Sep 17 00:00:00 2001 From: Karl Kroening Date: Mon, 12 Jun 2017 03:39:25 -0600 Subject: [PATCH 4/5] Update comment --- goless/selecting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/goless/selecting.py b/goless/selecting.py index c2266cf..956e38a 100644 --- a/goless/selecting.py +++ b/goless/selecting.py @@ -43,7 +43,7 @@ def select_ok(*cases): """ Select the first case that becomes ready, including an ``ok`` indication. This is the same as the ``select`` method except than an ``ok`` indication - is included allowing checks for closed channels. + is included, allowing checks for closed channels. :param cases: List of case instances, such as :class:`goless.rcase`, :class:`goless.scase`, or :class:`goless.dcase`. From a3dfc5b8e8961ca614289cf880fa2bfb964ea13a Mon Sep 17 00:00:00 2001 From: Karl Kroening Date: Mon, 19 Jun 2017 18:50:10 -0500 Subject: [PATCH 5/5] Cover selecting from null-channel scase --- tests/test_select.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_select.py b/tests/test_select.py index b592ff5..e717e3d 100644 --- a/tests/test_select.py +++ b/tests/test_select.py @@ -101,9 +101,9 @@ def test_select_ok_default_is_ok(self): self.assertTrue(ok) def test_select_ok_ignores_null_chan(self): - cases = [goless.rcase(None), goless.dcase()] + cases = [goless.scase(None, None), goless.rcase(None), goless.dcase()] result, val, ok = goless.select_ok(cases) - self.assertIs(result, cases[1]) + self.assertIs(result, cases[2]) self.assertTrue(ok) def test_select_ok_chooses_closed_over_default(self):