Repository: MaineKuehn/usim

Issue:
Streamline internal notification API
The internal subscribe/unsubscribe API of notifications is partially refactored. ``Notification.__await_notification__`` has a useless parameter and the ``subscribe`` context is not used yet. Probably some more unused/incomplete code.

Discussion:

Patch:
diff --git a/usim/_core/loop.py b/usim/_core/loop.py
index 733de51..90e8752 100644
--- a/usim/_core/loop.py
+++ b/usim/_core/loop.py
@@ -74,15 +74,37 @@ __LOOP_STATE__.LOOP = MissingLoop()  # type: Union[MissingLoop, Loop]
 
 # Commands
 class Hibernate(object):
-    r"""Pause current execution indefinitely"""
+    r"""
+    Pause current execution indefinitely
+
+    When yielding this event to :py:class:`~.Loop`,
+    the current activity is suspended.
+    It can be used both in asynchronous functions
+    and synchronous generators;
+    the latter is intended for ``__await__`` methods.
+
+    This class is stateless and effectively works like a singleton.
+    Use :py:attr:`~.__HIBERNATE__` to avoid creating new instances.
+
+    .. code:: python3
+
+        async def sleep():
+            await __HIBERNATE__
+
+        class Sleep:
+            def __await__():
+                yield from __HIBERNATE__
+    """
     __slots__ = ()
 
     def __await__(self) -> 'Generator[Hibernate, None, None]':
         yield self
 
+    __iter__ = __await__
+
 
 #: reusable instance of :py:class:`Hibernate`
-HIBERNATE = Hibernate
+__HIBERNATE__ = Hibernate()
 
 
 class Loop:
diff --git a/usim/_primitives/condition.py b/usim/_primitives/condition.py
index 5edc095..6e880b8 100644
--- a/usim/_primitives/condition.py
+++ b/usim/_primitives/condition.py
@@ -67,9 +67,13 @@ class Condition(Notification):
         return True  # noqa: B901
 
     def __and__(self, other) -> 'Condition':
+        if isinstance(other, All):
+            return All(self, *other._children)
         return All(self, other)
 
     def __or__(self, other) -> 'Condition':
+        if isinstance(other, Any):
+            return Any(self, *other._children)
         return Any(self, other)
 
     def __invert__(self) -> 'Condition':
diff --git a/usim/_primitives/flag.py b/usim/_primitives/flag.py
index 4fac82d..11572de 100644
--- a/usim/_primitives/flag.py
+++ b/usim/_primitives/flag.py
@@ -11,10 +11,10 @@ class Flag(Condition):
         self._value = False
         self._inverse = InverseFlag(self)
 
-    def __bool__(self):
+    def __bool__(self) -> bool:
         return self._value
 
-    def __invert__(self):
+    def __invert__(self) -> 'InverseFlag':
         return self._inverse
 
     async def set(self, to: bool = True):
@@ -35,10 +35,10 @@ class InverseFlag(Condition):
         super().__init__()
         self._event = flag
 
-    def __bool__(self):
+    def __bool__(self) -> bool:
         return not self._event
 
-    def __invert__(self):
+    def __invert__(self) -> 'Flag':
         return self._event
 
     async def set(self, to: bool = True):
diff --git a/usim/_primitives/notification.py b/usim/_primitives/notification.py
index d6e1aa1..3fd43cd 100644
--- a/usim/_primitives/notification.py
+++ b/usim/_primitives/notification.py
@@ -1,7 +1,7 @@
 from typing import List, Tuple, Coroutine
 from contextlib import contextmanager
 
-from .._core.loop import Hibernate, Interrupt, __LOOP_STATE__
+from .._core.loop import Interrupt, __LOOP_STATE__, __HIBERNATE__
 
 
 # TODO: add protocol for destroying a notification
@@ -22,7 +22,7 @@ async def postpone():
     wake_up = Interrupt('postpone', task)
     __LOOP_STATE__.LOOP.schedule(task, signal=wake_up)
     try:
-        await Hibernate()
+        await __HIBERNATE__
     except Interrupt as err:
         if err is not wake_up:
             assert (
@@ -33,23 +33,6 @@ async def postpone():
         wake_up.revoke()
 
 
-@contextmanager
-def subscribe(notification: 'Notification'):
-    task = __LOOP_STATE__.LOOP.activity
-    wake_up = Interrupt(notification, task)
-    notification.__subscribe__(task, wake_up)
-    try:
-        yield
-    except Interrupt as err:
-        if err is not wake_up:
-            assert (
-                task is __LOOP_STATE__.LOOP.activity
-            ), 'Break points cannot be passed to other coroutines'
-            raise
-    finally:
-        notification.__unsubscribe__(task, wake_up)
-
-
 class Notification:
     """
     Synchronisation point to which activities can subscribe
@@ -67,19 +50,8 @@ class Notification:
         self._waiting = []  # type: List[Tuple[Coroutine, Interrupt]]
 
     def __await__(self):
-        yield from self.__await_notification__().__await__()
-
-    async def __await_notification__(self, interrupt: Interrupt = None):
-        activity = __LOOP_STATE__.LOOP.activity
-        interrupt = interrupt if interrupt is not None else Interrupt(self, activity)
-        self.__subscribe__(activity, interrupt)
-        try:
-            await Hibernate()  # break point - we are resumed when the event is set
-        except Interrupt as err:
-            if err is not interrupt:  # resumed prematurely
-                raise
-        finally:
-            self.__unsubscribe__(activity, interrupt)
+        with self.__subscription__():
+            yield from __HIBERNATE__
 
     def __awake_next__(self) -> Tuple[Coroutine, Interrupt]:
         """Awake the oldest waiter"""
@@ -127,13 +99,14 @@ class Notification:
         finally:
             self.__unsubscribe__(task, wake_up)
 
-    def __del__(self):
-        if self._waiting:
-            raise RuntimeError(
-                '%r collected without releasing %d waiting tasks:\n  %s' % (
-                    self, len(self._waiting), self._waiting
+    if __debug__:
+        def __del__(self):
+            if self._waiting:
+                raise RuntimeError(
+                    '%r collected without releasing %d waiting tasks:\n  %s' % (
+                        self, len(self._waiting), self._waiting
+                    )
                 )
-            )
 
     def __repr__(self):
         return '<%s, waiters=%d>' % (self.__class__.__name__, len(self._waiting))
diff --git a/usim/_primitives/timing.py b/usim/_primitives/timing.py
index a98ebd5..3d7b93a 100644
--- a/usim/_primitives/timing.py
+++ b/usim/_primitives/timing.py
@@ -14,7 +14,7 @@ time can represent more than 285 million years of time accurately.
 """
 from typing import Coroutine, Union, Generator, Any
 
-from .._core.loop import __LOOP_STATE__, Hibernate, Interrupt as CoreInterrupt
+from .._core.loop import __LOOP_STATE__, __HIBERNATE__, Interrupt as CoreInterrupt
 from .notification import postpone, Notification
 from .condition import Condition
 
@@ -107,7 +107,7 @@ class Before(Condition):
         if self:
             yield from postpone().__await__()
         else:
-            yield from Hibernate().__await__()
+            yield from __HIBERNATE__
         return True  # noqa: B901
 
     def __repr__(self):
@@ -157,7 +157,7 @@ class Moment(Condition):
         elif not self._transition:
             yield from self._transition.__await__()
         else:
-            yield from Hibernate().__await__()
+            yield from __HIBERNATE__
         return True  # noqa: B901
 
     def __subscribe__(self, waiter: Coroutine, interrupt: CoreInterrupt):
@@ -192,7 +192,7 @@ class Eternity(Condition):
         return Instant()
 
     def __await__(self) -> Generator[Any, None, bool]:
-        yield from Hibernate().__await__()
+        yield from __HIBERNATE__
         return True  # noqa: B901
 
 


Test patch:
diff --git a/usim_pytest/test_types/test_condition.py b/usim_pytest/test_types/test_condition.py
index 8d0b819..a9f4651 100644
--- a/usim_pytest/test_types/test_condition.py
+++ b/usim_pytest/test_types/test_condition.py
@@ -1,4 +1,4 @@
-from usim import Flag
+from usim import Flag, until, instant
 
 from ..utility import via_usim
 
@@ -65,3 +65,59 @@ class TestChainedCondition:
         assert chain
         assert not ~chain
         assert await chain
+
+    @via_usim
+    async def test_flatten_and(self):
+        a, b, c, d, e, f = Flag(), Flag(), Flag(), Flag(), Flag(), Flag()
+        lexical_chain = a & b & c & d & e & f
+        parenthesised_chain = a & (b & (c & (d & (e & f))))
+        pairwise_chain = (a & b) & (c & d) & (e & f)
+        assert lexical_chain._children == parenthesised_chain._children
+        assert lexical_chain._children == pairwise_chain._children
+
+    @via_usim
+    async def test_flatten_or(self):
+        a, b, c, d, e, f = Flag(), Flag(), Flag(), Flag(), Flag(), Flag()
+        lexical_chain = a | b | c | d | e | f
+        parenthesised_chain = a | (b | (c | (d | (e | f))))
+        pairwise_chain = (a | b) | (c | d) | (e | f)
+        assert lexical_chain._children == parenthesised_chain._children
+        assert lexical_chain._children == pairwise_chain._children
+
+
+class TestContextCondition:
+    @via_usim
+    async def test_release_self(self):
+        """Condition is set synchronously"""
+        flag = Flag()
+        entered, exited = False, False
+        async with until(flag):
+            entered = True
+            await flag.set()  # get interrupted immediately
+            exited = True
+        assert entered and not exited
+
+    @via_usim
+    async def test_release_concurrent(self):
+        """Condition is set concurrently"""
+        flag = Flag()
+        entered, exited = False, False
+        async with until(flag) as scope:
+            scope.do(flag.set())
+            await instant  # start task
+            entered = True
+            await instant
+            exited = True
+        assert entered and not exited
+
+    @via_usim
+    async def test_release_early(self):
+        """Condition is set before entering interrupt scope"""
+        flag = Flag()
+        await flag.set()
+        entered, exited = False, False
+        async with until(flag):
+            entered = True
+            await instant
+            exited = True
+        assert entered and not exited
diff --git a/usim_pytest/test_types/test_flag.py b/usim_pytest/test_types/test_flag.py
new file mode 100644
index 0000000..ed48e59
--- /dev/null
+++ b/usim_pytest/test_types/test_flag.py
@@ -0,0 +1,30 @@
+from usim import Flag
+
+from ..utility import via_usim
+
+
+class TestFlag:
+    @via_usim
+    async def test_set(self):
+        flag = Flag()
+        assert not flag
+        await flag.set()
+        assert flag
+        await flag.set()
+        assert flag
+        await flag.set(to=False)
+        assert not flag
+        await flag.set(to=False)
+        assert not flag
+
+    @via_usim
+    async def test_invert(self):
+        flag = Flag()
+        i_flag = ~flag
+        assert not flag and i_flag
+        assert ~flag is i_flag
+        assert flag is ~i_flag
+        await i_flag.set(to=False)
+        assert flag and not i_flag
+        assert ~flag is i_flag
+        assert flag is ~i_flag