From be3775b761d31978b3501f9ebe60740b1bd5c31f Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Tue, 9 Apr 2019 17:15:13 +0200 Subject: [PATCH 01/20] whitespace --- docs/source/tutorial/03_scopes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/tutorial/03_scopes.rst b/docs/source/tutorial/03_scopes.rst index 1f9fa92..bbc57b2 100644 --- a/docs/source/tutorial/03_scopes.rst +++ b/docs/source/tutorial/03_scopes.rst @@ -36,7 +36,7 @@ We again use ``usim.time`` to track and influence the progression of our simulat ... await (time + 1) # 3 ... drivers.do(deliver_one(3)) ... print('Sent deliveries at', time.now) # 4.1 - ... print('-- Done deliveries at', time.now) # 4.2 + ... print('-- Done deliveries at', time.now) # 4.2 Scopes can be difficult because they are inherently about doing several things at once. It helps to step through individual points of notice: From 0adbbbb73aa055fbaa8d8e46d5ce1545fcd9d626 Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Tue, 9 Apr 2019 17:15:31 +0200 Subject: [PATCH 02/20] cancellation template --- docs/source/tutorial/04_cancel_scope.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 docs/source/tutorial/04_cancel_scope.rst diff --git a/docs/source/tutorial/04_cancel_scope.rst b/docs/source/tutorial/04_cancel_scope.rst new file mode 100644 index 0000000..fde8e43 --- /dev/null +++ b/docs/source/tutorial/04_cancel_scope.rst @@ -0,0 +1,21 @@ + +Interlude 01: Interrupting Scopes +--------------------------------- + +.. code:: python3 + + >>> from usim import time, until as out + >>> + >>> async def deliver_one(which): + ... print('Delivering', which, 'at', time.now) + ... await (time + 5) + ... print('Delivered', which, 'at', time.now) + >>> + >>> async def deliver_all(count=3): + ... print('-- Start deliveries at', time.now) + ... async with out(time + 10) as deliveries: # 1 + ... for delivery in range(count): # 2 + ... deliveries.do(deliver_one(delivery)) + ... await (time + 3) + ... print('Sent deliveries at', time.now) # 4.1 + ... print('-- Done deliveries at', time.now) # 4.2 From 3749c2ff890fda4215f4fe3fce14b6b9cefdf74d Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Wed, 10 Apr 2019 15:23:57 +0200 Subject: [PATCH 03/20] Activity API is closer to native coroutines --- usim/_primitives/activity.py | 69 +++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/usim/_primitives/activity.py b/usim/_primitives/activity.py index 9dcffac..5ab7c77 100644 --- a/usim/_primitives/activity.py +++ b/usim/_primitives/activity.py @@ -1,6 +1,6 @@ from functools import wraps import enum -from typing import Coroutine, TypeVar, Generic, Optional, Tuple, Any, List +from typing import Coroutine, TypeVar, Awaitable, Optional, Tuple, Any, List from .._core.loop import __LOOP_STATE__, Interrupt from .condition import Condition @@ -54,13 +54,13 @@ class ActivityExit(BaseException): ... -class Activity(Condition, Generic[RT]): +class Activity(Awaitable[RT]): """ Active coroutine that allows others to listen for its completion :note: Simulation code should never instantiate this class directly. """ - __slots__ = ('payload', '_result', '_execution', '_cancellations') + __slots__ = ('payload', '_result', '_execution', '_cancellations', '_done') def __init__(self, payload: Coroutine[Any, Any, RT]): @wraps(payload) @@ -78,28 +78,26 @@ async def payload_wrapper(): self._result = result, None for cancellation in self._cancellations: cancellation.revoke() - self.__trigger__() + self._done.__set_done__() super().__init__() self._cancellations = [] # type: List[CancelActivity] self._result = None # type: Optional[Tuple[RT, BaseException]] self.payload = payload + self._done = Done(self) self._execution = payload_wrapper() - @property - async def result(self) -> RT: - """ - Wait for the completion of this :py:class:`Activity` and return its result - - :returns: the result of the activity - :raises: :py:exc:`CancelActivity` if the activity was cancelled - """ - await self + def __await__(self): + yield from self._done.__await__() result, error = self._result if error is not None: raise error else: return result + @property + def done(self) -> 'Done': + return self._done + @property def status(self) -> ActivityState: """The current status of this activity""" @@ -113,12 +111,6 @@ def status(self) -> ActivityState: return ActivityState.CREATED return ActivityState.RUNNING - def __bool__(self): - return self._result is not None - - def __invert__(self): - return NotDone(self) - def __runner__(self): return self._execution @@ -133,7 +125,7 @@ def cancel(self, *token) -> None: if self._result is None: if self.status is ActivityState.CREATED: self._result = None, ActivityCancelled(self, *token) - self.__trigger__() + self._done.__set_done__() else: cancellation = CancelActivity(self, *token) self._cancellations.append(cancellation) @@ -161,16 +153,43 @@ def __del__(self): self._execution.close() -class NotDone(Condition): +class Done(Condition): + __slots__ = ('_activity', '_value', '_inverse') + def __init__(self, activity: Activity): super().__init__() - self.activity = activity + self._activity = activity + self._value = False + self._inverse = NotDone(self) + + def __bool__(self): + return self._value + + def __invert__(self): + return self._inverse + + def __set_done__(self): + """Set the boolean value of this condition""" + assert not self._value + self._value = True + self.__trigger__() + + def __repr__(self): + return '<%s for %r>' % (self.__class__.__name__, self._activity) + + +class NotDone(Condition): + __slots__ = ('_done',) + + def __init__(self, done: Done): + super().__init__() + self._done = done def __bool__(self): - return not self.activity + return not self._done def __invert__(self): - return self.activity + return self._done def __repr__(self): - return '<%s for %r>' % (self.__class__.__name__, self.activity) + return '<%s for %r>' % (self.__class__.__name__, self._done._activity) From 165bd2c92d252306860e6b8683fae4c52f26fc60 Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Wed, 10 Apr 2019 16:19:05 +0200 Subject: [PATCH 04/20] adjusted tests for direct await API --- usim_pytest/test_scopes.py | 16 ++++++------ usim_pytest/test_types/test_activity.py | 33 +++++++++++++------------ 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/usim_pytest/test_scopes.py b/usim_pytest/test_scopes.py index a84f4d6..cc54b44 100644 --- a/usim_pytest/test_scopes.py +++ b/usim_pytest/test_scopes.py @@ -15,7 +15,7 @@ async def payload(): async with Scope() as scope: activity = scope.do(payload()) - assert await activity.result == 2 + assert await activity == 2 @via_usim async def test_negative(self): @@ -42,7 +42,7 @@ async def payload(): assert activity.status == ActivityState.CREATED await (time + 3) assert activity.status == ActivityState.RUNNING - await activity.result + await activity.done assert time.now == 15 assert activity.status == ActivityState.SUCCESS @@ -54,9 +54,9 @@ async def payload(duration): async with Scope() as scope: activity_one = scope.do(payload(10), at=5) activity_two = scope.do(payload(15), at=5) - await (activity_one | activity_two) + await (activity_one.done | activity_two.done) assert time.now == 15 - await (activity_one & activity_two) + await (activity_one.done & activity_two.done) assert time.now == 20 @via_usim @@ -68,7 +68,7 @@ async def payload(): async with Scope() as scope: activity = scope.do(payload(), volatile=True) with pytest.raises(VolatileActivityExit): - assert await activity.result + assert await activity assert activity.status == ActivityState.FAILED @via_usim @@ -154,7 +154,7 @@ async def run_job(): assert time.now == 500 with pytest.raises(ActivityCancelled): - await activity.result + await activity @via_usim @@ -164,7 +164,7 @@ async def make_job(): async with Scope() as scope: running_job = scope.do(run_job()) running_job.cancel() - await running_job.result + await running_job async def run_job(): await (time + 100) @@ -180,7 +180,7 @@ async def make_job(): async with Scope() as scope: running_job = scope.do(run_job()) running_job.cancel() - await running_job + await running_job.done async def run_job(): await (time + 100) diff --git a/usim_pytest/test_types/test_activity.py b/usim_pytest/test_types/test_activity.py index 17a48e3..6ff975e 100644 --- a/usim_pytest/test_types/test_activity.py +++ b/usim_pytest/test_types/test_activity.py @@ -15,10 +15,10 @@ async def test_await(self): activity = scope.do(sleep(20)) assert time.now == 0 # await inside scope - await activity + await activity.done assert time.now == 20 # await outside scope - await activity + await activity.done assert time.now == 20 @via_usim @@ -27,11 +27,11 @@ async def test_result(self): activity = scope.do(sleep(20)) assert time.now == 0 # await result inside scope - assert await activity.result == 20 + assert await activity == 20 # await result delayed us assert time.now == 20 # await outside scope - assert await activity.result == 20 + assert await activity == 20 assert time.now == 20 @via_usim @@ -41,7 +41,7 @@ async def test_state_success(self): assert activity.status == ActivityState.CREATED await instant assert activity.status == ActivityState.RUNNING - await activity + await activity.done assert activity.status == ActivityState.SUCCESS assert activity.status & ActivityState.FINISHED @@ -55,7 +55,7 @@ async def test_state_cancel_created(self): assert activity.status == ActivityState.CANCELLED await instant assert activity.status == ActivityState.CANCELLED - await activity + await activity.done assert activity.status == ActivityState.CANCELLED assert activity.status & ActivityState.FINISHED @@ -69,7 +69,7 @@ async def test_state_cancel_running(self): activity.cancel() # running cancellation is graceful assert activity.status == ActivityState.RUNNING - await activity + await activity.done assert activity.status == ActivityState.CANCELLED assert activity.status & ActivityState.FINISHED @@ -77,18 +77,19 @@ async def test_state_cancel_running(self): async def test_condition(self): async with Scope() as scope: activity = scope.do(sleep(20)) - assert bool(activity) is False - assert bool(~activity) is True + activity_done = activity.done + assert bool(activity_done) is False + assert bool(~activity_done) is True # waiting for inverted, unfinished activity does not delay - assert await (~activity) is True + assert await (~activity_done) is True assert time.now == 0 await (time + 10) - assert bool(activity) is False - assert bool(~activity) is True - assert await (~activity) is True + assert bool(activity_done) is False + assert bool(~activity_done) is True + assert await (~activity_done) is True assert time.now == 10 await (time + 10) - assert bool(activity) is True - assert bool(~activity) is False - assert await activity is True + assert bool(activity_done) is True + assert bool(~activity_done) is False + assert await activity_done is True assert time.now == 20 From cfd493dfee375764d315673870f16d9c0e08f997 Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Wed, 10 Apr 2019 16:19:19 +0200 Subject: [PATCH 05/20] scope properly waits for completion of activities --- usim/_primitives/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usim/_primitives/context.py b/usim/_primitives/context.py index 478a190..c5badae 100644 --- a/usim/_primitives/context.py +++ b/usim/_primitives/context.py @@ -77,7 +77,7 @@ async def graceful(containing_scope: Scope): async def _await_children(self): for child in self._children: - await child + await child.done def _cancel_children(self): for child in self._children: From 64449956f973d8d91ca92ff827c2be0b114f8493 Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Wed, 10 Apr 2019 22:13:46 +0200 Subject: [PATCH 06/20] removed activity indirection --- usim/_primitives/activity.py | 16 ++++++---------- usim/_primitives/context.py | 2 +- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/usim/_primitives/activity.py b/usim/_primitives/activity.py index 5ab7c77..4ae4ba6 100644 --- a/usim/_primitives/activity.py +++ b/usim/_primitives/activity.py @@ -60,7 +60,7 @@ class Activity(Awaitable[RT]): :note: Simulation code should never instantiate this class directly. """ - __slots__ = ('payload', '_result', '_execution', '_cancellations', '_done') + __slots__ = ('payload', '_result', '__runner__', '_cancellations', '_done') def __init__(self, payload: Coroutine[Any, Any, RT]): @wraps(payload) @@ -79,12 +79,11 @@ async def payload_wrapper(): for cancellation in self._cancellations: cancellation.revoke() self._done.__set_done__() - super().__init__() self._cancellations = [] # type: List[CancelActivity] self._result = None # type: Optional[Tuple[RT, BaseException]] self.payload = payload self._done = Done(self) - self._execution = payload_wrapper() + self.__runner__ = payload_wrapper() # type: Coroutine[Any, Any, RT] def __await__(self): yield from self._done.__await__() @@ -107,17 +106,14 @@ def status(self) -> ActivityState: return ActivityState.CANCELLED if isinstance(error, ActivityCancelled) else ActivityState.FAILED return ActivityState.SUCCESS # a stripped-down version of `inspect.getcoroutinestate` - if self._execution.cr_frame.f_lasti == -1: + if self.__runner__.cr_frame.f_lasti == -1: return ActivityState.CREATED return ActivityState.RUNNING - def __runner__(self): - return self._execution - def __close__(self, reason=ActivityExit('activity closed')): """Close the underlying coroutine""" if self._result is None: - self._execution.close() + self.__runner__.close() self._result = None, reason def cancel(self, *token) -> None: @@ -130,7 +126,7 @@ def cancel(self, *token) -> None: cancellation = CancelActivity(self, *token) self._cancellations.append(cancellation) cancellation.scheduled = True - __LOOP_STATE__.LOOP.schedule(self._execution, signal=cancellation) + __LOOP_STATE__.LOOP.schedule(self.__runner__, signal=cancellation) def __repr__(self): return '<%s of %s (%s)>' % ( @@ -150,7 +146,7 @@ def __del__(self): # error message or traceback. # In order not to detract with auxiliary, useless resource # warnings, we clean up silently to hide our abstraction. - self._execution.close() + self.__runner__.close() class Done(Condition): diff --git a/usim/_primitives/context.py b/usim/_primitives/context.py index c5badae..45c0fee 100644 --- a/usim/_primitives/context.py +++ b/usim/_primitives/context.py @@ -66,7 +66,7 @@ async def graceful(containing_scope: Scope): """ child_activity = Activity(payload) __LOOP_STATE__.LOOP.schedule( - child_activity.__runner__(), + child_activity.__runner__, delay=after, at=at ) if not volatile: From aaaa61e6372920a2495ac8e7797bbddd54023f41 Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Tue, 23 Apr 2019 10:43:31 +0200 Subject: [PATCH 07/20] closing an activity notifies waiting activities --- usim/_primitives/activity.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/usim/_primitives/activity.py b/usim/_primitives/activity.py index 4ae4ba6..7128b17 100644 --- a/usim/_primitives/activity.py +++ b/usim/_primitives/activity.py @@ -56,9 +56,30 @@ class ActivityExit(BaseException): class Activity(Awaitable[RT]): """ - Active coroutine that allows others to listen for its completion + Concurrently running activity that allows multiple activities to await its completion - :note: Simulation code should never instantiate this class directly. + A :py:class:`Activity` represents an activity that is concurrently run in a :py:class:`~.Scope`. + This allows to store or pass an an :py:class:`Activity`, in order to check its progress. + Other activities can ``await`` a :py:class:`Activity`, + which returns any results or exceptions on completion, similar to a regular activity. + + .. code:: python3 + + await my_activity() # await a bare activity + + async with Scope() as scope: + activity = scope.do(my_activity()) + await activity # await a rich activity + + In contrast to a regular activity, it is possible to + + * :py:meth:`~.Activity.cancel` an :py:class:`Activity` before completion, + * ``await`` the result of an :py:class:`Activity` multiple times, + and + * ``await`` that an is an :py:class:`Activity` is :py:meth:`~.Activity.cancel`. + + :note: This class should not be instantiated directly. + Always use a :py:class:`~.Scope` to create it. """ __slots__ = ('payload', '_result', '__runner__', '_cancellations', '_done') @@ -95,6 +116,10 @@ def __await__(self): @property def done(self) -> 'Done': + """ + :py:class:`~.Condition` whether the :py:class:`~.Activity` has stopped running. + This includes completion, cancellation and failure. + """ return self._done @property @@ -115,6 +140,7 @@ def __close__(self, reason=ActivityExit('activity closed')): if self._result is None: self.__runner__.close() self._result = None, reason + self._done.__set_done__() def cancel(self, *token) -> None: """Cancel this activity during the current time step""" @@ -150,6 +176,7 @@ def __del__(self): class Done(Condition): + """Whether a :py:class:`Activity` has stopped running""" __slots__ = ('_activity', '_value', '_inverse') def __init__(self, activity: Activity): @@ -175,6 +202,7 @@ def __repr__(self): class NotDone(Condition): + """Whether a :py:class:`Activity` has not stopped running""" __slots__ = ('_done',) def __init__(self, done: Done): From a22928b2e2843dc03c67c1edfa3647fd37267ae9 Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Tue, 23 Apr 2019 13:52:02 +0200 Subject: [PATCH 08/20] updated glossary --- docs/source/glossary.rst | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index db1a252..00ca225 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -2,18 +2,23 @@ Glossary of Terms ================= +.. Using references in the glossary itself: + When mentioning other items, always reference them. + When mention the current item, never reference it. + .. glossary:: Activity - Ongoing action that drives forward a simulation - either through time or events. - Activities may be suspended and resumed as desired, or interrupted involuntarily. + Ongoing action that drives forward a simulation - either through :term:`time` or :term:`events `. + Activities may be :term:`suspended ` and resumed as desired, or interrupted involuntarily. Time Representation of the progression of a simulation. Whereas the unit of time is arbitrary, its value always grows. - Time may only pass while all :term:`activities ` are *suspended*. + Time may only pass while all :term:`activities ` + are :term:`postponed ` until a later time, not :term:`turn`. An :term:`activity` may actively wait for the progression of time, or implicitly delay until an event happens at a future point in time. @@ -23,10 +28,31 @@ Glossary of Terms Event A well-defined occurrence at a specific point in :term:`time`. Events may occur - as result of activities ("dinner is done"), + as result of activities ("when dinner is done"), as time passes ("after 20 time units"), or at predefined points in time ("at 2000 time units"), Notification Information sent to an :term:`activity`, usually in response to an :term:`event`. + Notifications can only be received when an :term:`activity` is :term:`suspended `. + + Postponement + :term:`Suspension` of an :term:`activity` until a later :term:`turn` at the same :term:`time`. + When an :term:`activity` is postponed, + other :term:`activities ` may run but :term:`time` does not advance. + If there are no other :term:`activities ` to resume, + a postponed :term:`activity` is resumed immediately. + + :note: μSim guarantees that all its primitives postpone on asynchronous operations. + This ensures that activities are reliably and deterministically interwoven. + + Suspension + Pause in the execution of an :term:`activity`, + allowing other :term:`activities ` or :term:`time` to advance. + A suspended activity is only resumed when it receives a :term:`notification`. + + Suspension can only occur as part of asynchronous statements: + waiting for the target of an ``await`` statement, + fetching the next item of an ``async for`` statement, + and entering/exiting an ``async with`` block. From 3778c579ae30c8507796a31cf3b714f7c9875707 Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Tue, 23 Apr 2019 14:00:53 +0200 Subject: [PATCH 09/20] Activity docs --- usim/_primitives/activity.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/usim/_primitives/activity.py b/usim/_primitives/activity.py index 7128b17..be35b35 100644 --- a/usim/_primitives/activity.py +++ b/usim/_primitives/activity.py @@ -76,7 +76,7 @@ class Activity(Awaitable[RT]): * :py:meth:`~.Activity.cancel` an :py:class:`Activity` before completion, * ``await`` the result of an :py:class:`Activity` multiple times, and - * ``await`` that an is an :py:class:`Activity` is :py:meth:`~.Activity.cancel`. + * ``await`` that an is an :py:class:`Activity` is :py:meth:`~.Activity.done`. :note: This class should not be instantiated directly. Always use a :py:class:`~.Scope` to create it. @@ -136,14 +136,36 @@ def status(self) -> ActivityState: return ActivityState.RUNNING def __close__(self, reason=ActivityExit('activity closed')): - """Close the underlying coroutine""" + """ + Close the underlying coroutine + + This is similar to calling :py:meth:`Coroutine.close`, + but ensures that waiting activities are properly notified. + """ if self._result is None: self.__runner__.close() self._result = None, reason self._done.__set_done__() def cancel(self, *token) -> None: - """Cancel this activity during the current time step""" + """ + Cancel this activity during the current time step + + If the :py:class:`~.Activity` is running, + a :py:class:`~.CancelActivity` is raised once the activity suspends. + The activity may catch and react to :py:class:`~.CancelActivity`, + but should not suppress it. + + If the :py:class:`~.Activity` is :py:meth:`~.Activity.done` before :py:class:`~.CancelActivity` is raised, + the cancellation is ignored. + This also means that cancelling an activity multiple is allowed, + but only the first successful cancellation is stored as the cancellation cause. + + If the :py:class:`~.Activity` has not started running, it is cancelled immediately. + This prevents any code execution, even before the first suspension. + + :warning: The timing of cancelling an Activity before it started running may change in the future. + """ if self._result is None: if self.status is ActivityState.CREATED: self._result = None, ActivityCancelled(self, *token) From dc563de440a72cfe022b9a75825a59033daeb242 Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Tue, 23 Apr 2019 14:05:06 +0200 Subject: [PATCH 10/20] fixed docs reference --- usim/_primitives/activity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/usim/_primitives/activity.py b/usim/_primitives/activity.py index be35b35..c024cad 100644 --- a/usim/_primitives/activity.py +++ b/usim/_primitives/activity.py @@ -76,7 +76,7 @@ class Activity(Awaitable[RT]): * :py:meth:`~.Activity.cancel` an :py:class:`Activity` before completion, * ``await`` the result of an :py:class:`Activity` multiple times, and - * ``await`` that an is an :py:class:`Activity` is :py:meth:`~.Activity.done`. + * ``await`` that an is an :py:class:`Activity` is :py:attr:`~.Activity.done`. :note: This class should not be instantiated directly. Always use a :py:class:`~.Scope` to create it. @@ -156,7 +156,7 @@ def cancel(self, *token) -> None: The activity may catch and react to :py:class:`~.CancelActivity`, but should not suppress it. - If the :py:class:`~.Activity` is :py:meth:`~.Activity.done` before :py:class:`~.CancelActivity` is raised, + If the :py:class:`~.Activity` is :py:attr:`~.Activity.done` before :py:class:`~.CancelActivity` is raised, the cancellation is ignored. This also means that cancelling an activity multiple is allowed, but only the first successful cancellation is stored as the cancellation cause. From 4d12b14b9655be297cb263aee80532be62c0ff2b Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Tue, 23 Apr 2019 17:33:10 +0200 Subject: [PATCH 11/20] Condition docs --- usim/_primitives/condition.py | 50 ++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/usim/_primitives/condition.py b/usim/_primitives/condition.py index f3de160..80d686f 100644 --- a/usim/_primitives/condition.py +++ b/usim/_primitives/condition.py @@ -10,14 +10,48 @@ class Condition(Notification): """ - A logical condition that triggers when ``True`` + An asynchronous logical condition + + Every :py:class:`~.Condition` can be used both in a + asynchronous *and* boolean context. + In an asynchronous context, + such as ``await``, + a :py:class:`~.Condition` triggers when :py:const:`True`. + In a boolean context, + such as ``if``, + a :py:class:`~.Condition` provides its current boolean value. .. code:: python + if condition: # resume with current value + print(condition, 'is met') + else: + print(condition, 'is not met') + await condition # resume when condition is True - async with until(condition): # abort if condition becomes False + async with out(condition): # interrupt when condition is True ... + + Every :py:class:`~.Condition` supports the bitwise operators + ``~a`` (not), + ``a & b`` (and), and + ``a | b`` (or) + to derive a new :py:class:`~.Condition`. + While it is possible to use the boolean operators + ``not``, ``and``, and ``or``, + they immediately evaluate any :py:class:`~.Condition` in a boolean context. + + .. code:: python + + await (a & b) # resume when both a and b are True + await (a | b) # resume when one of a or b are True + await (a & ~b) # resume when a is True and b is False + + c = a & b # derive new Condition... + await c # that can be awaited + + d = a and b # force boolean evaluation """ __slots__ = () @@ -88,7 +122,11 @@ def __repr__(self): class All(Connective): - """Logical AND of all sub-conditions""" + """ + Logical AND of all sub-conditions + + The expression ``a & b & c`` is equivalent to ``All(a, b, c)``. + """ __slots__ = () def __bool__(self): @@ -102,7 +140,11 @@ def __str__(self): class Any(Connective): - """Logical OR of all sub-conditions""" + """ + Logical OR of all sub-conditions + + The expression ``a | b | c`` is equivalent to ``Any(a, b, c)``. + """ __slots__ = () def __bool__(self): From 6606b4b75eb29b905e47060ac2f7409bd7b4395d Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Wed, 24 Apr 2019 15:56:58 +0200 Subject: [PATCH 12/20] exception docs --- usim/_primitives/activity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usim/_primitives/activity.py b/usim/_primitives/activity.py index c024cad..0466f6b 100644 --- a/usim/_primitives/activity.py +++ b/usim/_primitives/activity.py @@ -51,7 +51,7 @@ def __transcript__(self) -> ActivityCancelled: class ActivityExit(BaseException): - ... + """A :py:class:`~.Activity` forcefully exited""" class Activity(Awaitable[RT]): From a7fd32368b8a93fe734192806a98524de79e96e1 Mon Sep 17 00:00:00 2001 From: Eileen Kuehn Date: Thu, 25 Apr 2019 14:36:12 +0200 Subject: [PATCH 13/20] Activity docstring clarification Co-Authored-By: maxfischer2781 --- usim/_primitives/activity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usim/_primitives/activity.py b/usim/_primitives/activity.py index 0466f6b..dd6b261 100644 --- a/usim/_primitives/activity.py +++ b/usim/_primitives/activity.py @@ -56,7 +56,7 @@ class ActivityExit(BaseException): class Activity(Awaitable[RT]): """ - Concurrently running activity that allows multiple activities to await its completion + Concurrently running activity that allows multiple objects including activities to await its completion A :py:class:`Activity` represents an activity that is concurrently run in a :py:class:`~.Scope`. This allows to store or pass an an :py:class:`Activity`, in order to check its progress. From f2c06b2220c9dc5ec9b1eed71dd50d0f032e0615 Mon Sep 17 00:00:00 2001 From: Eileen Kuehn Date: Thu, 25 Apr 2019 15:01:32 +0200 Subject: [PATCH 14/20] Apply suggestions from code review Co-Authored-By: maxfischer2781 --- usim/_primitives/activity.py | 10 +++++----- usim/_primitives/condition.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/usim/_primitives/activity.py b/usim/_primitives/activity.py index dd6b261..f18b670 100644 --- a/usim/_primitives/activity.py +++ b/usim/_primitives/activity.py @@ -58,9 +58,9 @@ class Activity(Awaitable[RT]): """ Concurrently running activity that allows multiple objects including activities to await its completion - A :py:class:`Activity` represents an activity that is concurrently run in a :py:class:`~.Scope`. + An :py:class:`Activity` represents an activity that is concurrently run in a :py:class:`~.Scope`. This allows to store or pass an an :py:class:`Activity`, in order to check its progress. - Other activities can ``await`` a :py:class:`Activity`, + Other activities can ``await`` an :py:class:`Activity`, which returns any results or exceptions on completion, similar to a regular activity. .. code:: python3 @@ -158,7 +158,7 @@ def cancel(self, *token) -> None: If the :py:class:`~.Activity` is :py:attr:`~.Activity.done` before :py:class:`~.CancelActivity` is raised, the cancellation is ignored. - This also means that cancelling an activity multiple is allowed, + This also means that cancelling an activity multiple times is allowed, but only the first successful cancellation is stored as the cancellation cause. If the :py:class:`~.Activity` has not started running, it is cancelled immediately. @@ -198,7 +198,7 @@ def __del__(self): class Done(Condition): - """Whether a :py:class:`Activity` has stopped running""" + """Whether an :py:class:`Activity` has stopped running""" __slots__ = ('_activity', '_value', '_inverse') def __init__(self, activity: Activity): @@ -224,7 +224,7 @@ def __repr__(self): class NotDone(Condition): - """Whether a :py:class:`Activity` has not stopped running""" + """Whether an :py:class:`Activity` has not stopped running""" __slots__ = ('_done',) def __init__(self, done: Done): diff --git a/usim/_primitives/condition.py b/usim/_primitives/condition.py index 80d686f..66d86c6 100644 --- a/usim/_primitives/condition.py +++ b/usim/_primitives/condition.py @@ -12,11 +12,11 @@ class Condition(Notification): """ An asynchronous logical condition - Every :py:class:`~.Condition` can be used both in a + Every :py:class:`~.Condition` can be used both in an asynchronous *and* boolean context. In an asynchronous context, such as ``await``, - a :py:class:`~.Condition` triggers when :py:const:`True`. + a :py:class:`~.Condition` triggers when the :py:class:`~.Condition` becomes :py:const:`True`. In a boolean context, such as ``if``, a :py:class:`~.Condition` provides its current boolean value. From 77d9c0e6df5746556c25781aa5a9c7b3480ea2bf Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Thu, 25 Apr 2019 12:51:25 +0200 Subject: [PATCH 15/20] clarified glossary --- docs/source/glossary.rst | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index 00ca225..d2647db 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -18,7 +18,7 @@ Glossary of Terms Whereas the unit of time is arbitrary, its value always grows. Time may only pass while all :term:`activities ` - are :term:`postponed ` until a later time, not :term:`turn`. + are :term:`suspended ` until a later time, not :term:`turn`. An :term:`activity` may actively wait for the progression of time, or implicitly delay until an event happens at a future point in time. @@ -35,20 +35,19 @@ Glossary of Terms Notification Information sent to an :term:`activity`, usually in response to an :term:`event`. - Notifications can only be received when an :term:`activity` is :term:`suspended `. + Notifications are only received when the :term:`activity` + is :term:`suspended `, i.e. at an ``await``, ``async for`` or ``async with``. Postponement :term:`Suspension` of an :term:`activity` until a later :term:`turn` at the same :term:`time`. - When an :term:`activity` is postponed, - other :term:`activities ` may run but :term:`time` does not advance. - If there are no other :term:`activities ` to resume, - a postponed :term:`activity` is resumed immediately. + When an :term:`activity` is postponed, :term:`notifications ` may be received + and other :term:`activities ` may run but :term:`time` does not advance. :note: μSim guarantees that all its primitives postpone on asynchronous operations. This ensures that activities are reliably and deterministically interwoven. Suspension - Pause in the execution of an :term:`activity`, + Pause the execution of an :term:`activity`, allowing other :term:`activities ` or :term:`time` to advance. A suspended activity is only resumed when it receives a :term:`notification`. From 034e84be35058e67cdc2189e43bcbf4e42ae73da Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Thu, 25 Apr 2019 15:07:33 +0200 Subject: [PATCH 16/20] removed proposed 'async with out' from docs (see issue #1) --- docs/index.rst | 2 +- docs/source/tutorial/04_cancel_scope.rst | 2 +- usim/_primitives/condition.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index f26f637..54faa03 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -63,7 +63,7 @@ All hard-to-use functionality is automatically scoped and managed, making it nat .. code:: python3 # scoping is a builtin concept of usim - async with out(time >= 3000) as scope: + async with until(time >= 3000) as scope: # complex tools are automatically managed async for message in stream: scope.do(handle(message)) diff --git a/docs/source/tutorial/04_cancel_scope.rst b/docs/source/tutorial/04_cancel_scope.rst index fde8e43..f2457a6 100644 --- a/docs/source/tutorial/04_cancel_scope.rst +++ b/docs/source/tutorial/04_cancel_scope.rst @@ -13,7 +13,7 @@ Interlude 01: Interrupting Scopes >>> >>> async def deliver_all(count=3): ... print('-- Start deliveries at', time.now) - ... async with out(time + 10) as deliveries: # 1 + ... async with until(time + 10) as deliveries: # 1 ... for delivery in range(count): # 2 ... deliveries.do(deliver_one(delivery)) ... await (time + 3) diff --git a/usim/_primitives/condition.py b/usim/_primitives/condition.py index 66d86c6..3bfa5a8 100644 --- a/usim/_primitives/condition.py +++ b/usim/_primitives/condition.py @@ -30,7 +30,7 @@ class Condition(Notification): await condition # resume when condition is True - async with out(condition): # interrupt when condition is True + async with until(condition): # interrupt when condition is True ... Every :py:class:`~.Condition` supports the bitwise operators From 0adc21df075e3274c3c9e0ed6569e1afc8afd96e Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Thu, 25 Apr 2019 15:13:57 +0200 Subject: [PATCH 17/20] changed wording of Activity to wrap an activity instead of representing it --- usim/_primitives/activity.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/usim/_primitives/activity.py b/usim/_primitives/activity.py index f18b670..5ed2ba8 100644 --- a/usim/_primitives/activity.py +++ b/usim/_primitives/activity.py @@ -58,25 +58,29 @@ class Activity(Awaitable[RT]): """ Concurrently running activity that allows multiple objects including activities to await its completion - An :py:class:`Activity` represents an activity that is concurrently run in a :py:class:`~.Scope`. - This allows to store or pass an an :py:class:`Activity`, in order to check its progress. + An :py:class:`Activity` wraps an activity that is concurrently run in a :py:class:`~.Scope`. + This allows to store or pass around the :py:class:`Activity`, in order to check its progress. Other activities can ``await`` an :py:class:`Activity`, which returns any results or exceptions on completion, similar to a regular activity. .. code:: python3 - await my_activity() # await a bare activity + async def my_activity(delay): + await (time + delay) + return delay + + await my_activity() # await an unwrapped activity async with Scope() as scope: activity = scope.do(my_activity()) - await activity # await a rich activity + await activity # await a wrapping Activity - In contrast to a regular activity, it is possible to + In contrast to a bare activity, it is possible to * :py:meth:`~.Activity.cancel` an :py:class:`Activity` before completion, * ``await`` the result of an :py:class:`Activity` multiple times, and - * ``await`` that an is an :py:class:`Activity` is :py:attr:`~.Activity.done`. + * ``await`` that an :py:class:`Activity` is :py:attr:`~.Activity.done`. :note: This class should not be instantiated directly. Always use a :py:class:`~.Scope` to create it. From 5cca28c2605cadb12ae8e05b0b5e1a7e590a63c3 Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Thu, 25 Apr 2019 15:14:45 +0200 Subject: [PATCH 18/20] added reference to glossary for Activity --- usim/_primitives/activity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usim/_primitives/activity.py b/usim/_primitives/activity.py index 5ed2ba8..e071092 100644 --- a/usim/_primitives/activity.py +++ b/usim/_primitives/activity.py @@ -58,7 +58,7 @@ class Activity(Awaitable[RT]): """ Concurrently running activity that allows multiple objects including activities to await its completion - An :py:class:`Activity` wraps an activity that is concurrently run in a :py:class:`~.Scope`. + An :py:class:`Activity` wraps an :term:`activity` that is concurrently run in a :py:class:`~.Scope`. This allows to store or pass around the :py:class:`Activity`, in order to check its progress. Other activities can ``await`` an :py:class:`Activity`, which returns any results or exceptions on completion, similar to a regular activity. From 3637960948a1f9143d8ac8c477eedfdc91e3785c Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Thu, 25 Apr 2019 22:04:36 +0200 Subject: [PATCH 19/20] renamed Activity and related classes to Task --- usim/__init__.py | 6 +- usim/_primitives/context.py | 22 ++--- usim/_primitives/locks.py | 2 +- usim/_primitives/{activity.py => task.py} | 104 +++++++++++----------- usim/typing.py | 4 +- usim_pytest/test_scopes.py | 18 ++-- usim_pytest/test_types/test_activity.py | 30 +++---- 7 files changed, 94 insertions(+), 92 deletions(-) rename usim/_primitives/{activity.py => task.py} (62%) diff --git a/usim/__init__.py b/usim/__init__.py index 0684c78..86043a1 100644 --- a/usim/__init__.py +++ b/usim/__init__.py @@ -5,14 +5,14 @@ from ._primitives.timing import Time, Eternity, Instant, each from ._primitives.flag import Flag from ._primitives.locks import Lock -from ._primitives.context import until, Scope, VolatileActivityExit -from ._primitives.activity import ActivityCancelled, ActivityState +from ._primitives.context import until, Scope, VolatileTaskExit +from ._primitives.task import TaskCancelled, TaskState __all__ = [ 'run', 'time', 'eternity', 'instant', 'each', - 'until', 'Scope', 'ActivityCancelled', 'VolatileActivityExit', 'ActivityState', + 'until', 'Scope', 'TaskCancelled', 'VolatileTaskExit', 'TaskState', 'Flag', 'Lock', ] diff --git a/usim/_primitives/context.py b/usim/_primitives/context.py index 45c0fee..5047e86 100644 --- a/usim/_primitives/context.py +++ b/usim/_primitives/context.py @@ -3,13 +3,13 @@ from .._core.loop import __LOOP_STATE__, Interrupt as CoreInterrupt from .notification import Notification from .flag import Flag -from .activity import Activity, ActivityExit +from .task import Task, TaskExit RT = TypeVar('RT') -class VolatileActivityExit(ActivityExit): +class VolatileTaskExit(TaskExit): ... @@ -25,15 +25,15 @@ class Scope: __slots__ = ('_children', '_done', '_activity', '_volatile_children') def __init__(self): - self._children = [] # type: List[Activity] - self._volatile_children = [] # type: List[Activity] + self._children = [] # type: List[Task] + self._volatile_children = [] # type: List[Task] self._done = Flag() self._activity = None def __await__(self): yield from self._done.__await__() - def do(self, payload: Coroutine[Any, Any, RT], *, after: float = None, at: float = None, volatile: bool = False) -> Activity[RT]: + def do(self, payload: Coroutine[Any, Any, RT], *, after: float = None, at: float = None, volatile: bool = False) -> Task[RT]: r""" Concurrently perform an activity in this scope @@ -64,16 +64,16 @@ async def graceful(containing_scope: Scope): :py:class:`GeneratorExit` is raised in the activity, and must exit without `await`\ ing or `yield`\ ing anything. """ - child_activity = Activity(payload) + child_task = Task(payload) __LOOP_STATE__.LOOP.schedule( - child_activity.__runner__, + child_task.__runner__, delay=after, at=at ) if not volatile: - self._children.append(child_activity) + self._children.append(child_task) else: - self._volatile_children.append(child_activity) - return child_activity + self._volatile_children.append(child_task) + return child_task async def _await_children(self): for child in self._children: @@ -84,7 +84,7 @@ def _cancel_children(self): child.cancel(self) def _close_volatile(self): - reason = VolatileActivityExit("closed at end of scope '%s'" % self) + reason = VolatileTaskExit("closed at end of scope '%s'" % self) for child in self._volatile_children: child.__close__(reason=reason) diff --git a/usim/_primitives/locks.py b/usim/_primitives/locks.py index 9c4bd87..07e7bc1 100644 --- a/usim/_primitives/locks.py +++ b/usim/_primitives/locks.py @@ -31,7 +31,7 @@ def __init__(self): @property def available(self): """ - Check whether the current Activity can acquire this lock + Check whether the current Task can acquire this lock """ if self._owner is None: return True diff --git a/usim/_primitives/activity.py b/usim/_primitives/task.py similarity index 62% rename from usim/_primitives/activity.py rename to usim/_primitives/task.py index e071092..0ac401a 100644 --- a/usim/_primitives/activity.py +++ b/usim/_primitives/task.py @@ -10,8 +10,8 @@ # enum.Flag is Py3.6+ -class ActivityState(enum.Flag if hasattr(enum, 'Flag') else enum.IntEnum): - """State of a :py:class:`~.Activity`""" +class TaskState(enum.Flag if hasattr(enum, 'Flag') else enum.IntEnum): + """State of a :py:class:`~.Task`""" #: created but not running yet CREATED = 2 ** 0 #: being executed at the moment @@ -26,42 +26,44 @@ class ActivityState(enum.Flag if hasattr(enum, 'Flag') else enum.IntEnum): FINISHED = CANCELLED | FAILED | SUCCESS -class ActivityCancelled(Exception): - """An Activity has been cancelled""" +class TaskCancelled(Exception): + """A :py:class:`~.Task` has been cancelled""" __slots__ = ('subject',) - def __init__(self, subject: 'Activity', *token): + def __init__(self, subject: 'Task', *token): super().__init__(*token) + #: the cancelled Task self.subject = subject -class CancelActivity(Interrupt): - """An Activity is being cancelled""" +class CancelTask(Interrupt): + """A :py:class:`~.Task` is being cancelled""" __slots__ = ('subject',) - def __init__(self, subject: 'Activity', *token): + def __init__(self, subject: 'Task', *token): super().__init__(*token) + #: the Task being cancelled self.subject = subject @property - def __transcript__(self) -> ActivityCancelled: - result = ActivityCancelled(self.subject, *self.token) + def __transcript__(self) -> TaskCancelled: + result = TaskCancelled(self.subject, *self.token) result.__cause__ = self return result -class ActivityExit(BaseException): - """A :py:class:`~.Activity` forcefully exited""" +class TaskExit(BaseException): + """A :py:class:`~.Task` forcefully exited""" -class Activity(Awaitable[RT]): +class Task(Awaitable[RT]): """ - Concurrently running activity that allows multiple objects including activities to await its completion + Concurrently running activity - An :py:class:`Activity` wraps an :term:`activity` that is concurrently run in a :py:class:`~.Scope`. - This allows to store or pass around the :py:class:`Activity`, in order to check its progress. - Other activities can ``await`` an :py:class:`Activity`, - which returns any results or exceptions on completion, similar to a regular activity. + A :py:class:`Task` wraps an :term:`activity` that is concurrently run in a :py:class:`~.Scope`. + This allows to store or pass on the :py:class:`Task` in order to control the underlying activity. + Other activities can ``await`` a :py:class:`Task` + to receive any results or exceptions on completion, similar to a regular activity. .. code:: python3 @@ -69,18 +71,18 @@ async def my_activity(delay): await (time + delay) return delay - await my_activity() # await an unwrapped activity + await my_activity() # await an activity async with Scope() as scope: - activity = scope.do(my_activity()) - await activity # await a wrapping Activity + task = scope.do(my_activity()) + await task # await Task of an activity In contrast to a bare activity, it is possible to - * :py:meth:`~.Activity.cancel` an :py:class:`Activity` before completion, - * ``await`` the result of an :py:class:`Activity` multiple times, + * :py:meth:`~.Task.cancel` a :py:class:`Task` before completion, + * ``await`` the result of a :py:class:`Task` multiple times, and - * ``await`` that an :py:class:`Activity` is :py:attr:`~.Activity.done`. + * ``await`` that a :py:class:`Task` is :py:attr:`~.Task.done`. :note: This class should not be instantiated directly. Always use a :py:class:`~.Scope` to create it. @@ -96,15 +98,15 @@ async def payload_wrapper(): return try: result = await self.payload - except CancelActivity as err: - assert err.subject is self, "activity %r received cancellation of %r" % (self, err.subject) + except CancelTask as err: + assert err.subject is self, "task for activity %r received cancellation of %r" % (self, err.subject) self._result = None, err.__transcript__ else: self._result = result, None for cancellation in self._cancellations: cancellation.revoke() self._done.__set_done__() - self._cancellations = [] # type: List[CancelActivity] + self._cancellations = [] # type: List[CancelTask] self._result = None # type: Optional[Tuple[RT, BaseException]] self.payload = payload self._done = Done(self) @@ -121,25 +123,25 @@ def __await__(self): @property def done(self) -> 'Done': """ - :py:class:`~.Condition` whether the :py:class:`~.Activity` has stopped running. + :py:class:`~.Condition` whether the :py:class:`~.Task` has stopped running. This includes completion, cancellation and failure. """ return self._done @property - def status(self) -> ActivityState: + def status(self) -> TaskState: """The current status of this activity""" if self._result is not None: result, error = self._result if error is not None: - return ActivityState.CANCELLED if isinstance(error, ActivityCancelled) else ActivityState.FAILED - return ActivityState.SUCCESS + return TaskState.CANCELLED if isinstance(error, TaskCancelled) else TaskState.FAILED + return TaskState.SUCCESS # a stripped-down version of `inspect.getcoroutinestate` if self.__runner__.cr_frame.f_lasti == -1: - return ActivityState.CREATED - return ActivityState.RUNNING + return TaskState.CREATED + return TaskState.RUNNING - def __close__(self, reason=ActivityExit('activity closed')): + def __close__(self, reason=TaskExit('activity closed')): """ Close the underlying coroutine @@ -153,29 +155,29 @@ def __close__(self, reason=ActivityExit('activity closed')): def cancel(self, *token) -> None: """ - Cancel this activity during the current time step + Cancel this task during the current time step - If the :py:class:`~.Activity` is running, - a :py:class:`~.CancelActivity` is raised once the activity suspends. + If the :py:class:`~.Task` is running, + a :py:class:`~.CancelTask` is raised once the activity suspends. The activity may catch and react to :py:class:`~.CancelActivity`, but should not suppress it. - If the :py:class:`~.Activity` is :py:attr:`~.Activity.done` before :py:class:`~.CancelActivity` is raised, + If the :py:class:`~.Task` is :py:attr:`~.Task.done` before :py:class:`~.CancelTask` is raised, the cancellation is ignored. This also means that cancelling an activity multiple times is allowed, but only the first successful cancellation is stored as the cancellation cause. - If the :py:class:`~.Activity` has not started running, it is cancelled immediately. + If the :py:class:`~.Task` has not started running, it is cancelled immediately. This prevents any code execution, even before the first suspension. - :warning: The timing of cancelling an Activity before it started running may change in the future. + :warning: The timing of cancelling a Task before it started running may change in the future. """ if self._result is None: - if self.status is ActivityState.CREATED: - self._result = None, ActivityCancelled(self, *token) + if self.status is TaskState.CREATED: + self._result = None, TaskCancelled(self, *token) self._done.__set_done__() else: - cancellation = CancelActivity(self, *token) + cancellation = CancelTask(self, *token) self._cancellations.append(cancellation) cancellation.scheduled = True __LOOP_STATE__.LOOP.schedule(self.__runner__, signal=cancellation) @@ -192,7 +194,7 @@ def __repr__(self): ) def __del__(self): - # Since an Activity is only meant for use in a controlled + # Since a Task is only meant for use in a controlled # fashion, going out of scope unexpectedly means there is # a bug/error somewhere. This should be accompanied by an # error message or traceback. @@ -202,12 +204,12 @@ def __del__(self): class Done(Condition): - """Whether an :py:class:`Activity` has stopped running""" - __slots__ = ('_activity', '_value', '_inverse') + """Whether a :py:class:`Task` has stopped running""" + __slots__ = ('_task', '_value', '_inverse') - def __init__(self, activity: Activity): + def __init__(self, task: Task): super().__init__() - self._activity = activity + self._task = task self._value = False self._inverse = NotDone(self) @@ -224,11 +226,11 @@ def __set_done__(self): self.__trigger__() def __repr__(self): - return '<%s for %r>' % (self.__class__.__name__, self._activity) + return '<%s for %r>' % (self.__class__.__name__, self._task) class NotDone(Condition): - """Whether an :py:class:`Activity` has not stopped running""" + """Whether a :py:class:`Task` has not stopped running""" __slots__ = ('_done',) def __init__(self, done: Done): @@ -242,4 +244,4 @@ def __invert__(self): return self._done def __repr__(self): - return '<%s for %r>' % (self.__class__.__name__, self._done._activity) + return '<%s for %r>' % (self.__class__.__name__, self._done._task) diff --git a/usim/typing.py b/usim/typing.py index 7a9636f..d37ce3a 100644 --- a/usim/typing.py +++ b/usim/typing.py @@ -1,11 +1,11 @@ from ._primitives.notification import Notification from ._primitives.condition import Condition -from ._primitives.activity import Activity +from ._primitives.task import Task from ._basics.streams import Stream, StreamAsyncIterator __all__ = [ 'Notification', 'Condition', 'Stream', 'StreamAsyncIterator', - 'Activity', + 'Task', ] diff --git a/usim_pytest/test_scopes.py b/usim_pytest/test_scopes.py index cc54b44..9c22fbe 100644 --- a/usim_pytest/test_scopes.py +++ b/usim_pytest/test_scopes.py @@ -1,6 +1,6 @@ import pytest -from usim import Scope, time, eternity, VolatileActivityExit, ActivityState, ActivityCancelled, until, each +from usim import Scope, time, eternity, VolatileTaskExit, TaskState, TaskCancelled, until, each from .utility import via_usim @@ -37,14 +37,14 @@ async def payload(): async with Scope() as scope: activity = scope.do(payload(), after=5) - assert activity.status == ActivityState.CREATED + assert activity.status == TaskState.CREATED await (time + 4) - assert activity.status == ActivityState.CREATED + assert activity.status == TaskState.CREATED await (time + 3) - assert activity.status == ActivityState.RUNNING + assert activity.status == TaskState.RUNNING await activity.done assert time.now == 15 - assert activity.status == ActivityState.SUCCESS + assert activity.status == TaskState.SUCCESS @via_usim async def test_at(self): @@ -67,9 +67,9 @@ async def payload(): async with Scope() as scope: activity = scope.do(payload(), volatile=True) - with pytest.raises(VolatileActivityExit): + with pytest.raises(VolatileTaskExit): assert await activity - assert activity.status == ActivityState.FAILED + assert activity.status == TaskState.FAILED @via_usim async def test_after_and_at(self): @@ -153,12 +153,12 @@ async def run_job(): activity = running.do(scheduler()) assert time.now == 500 - with pytest.raises(ActivityCancelled): + with pytest.raises(TaskCancelled): await activity @via_usim -@pytest.mark.xfail(raises=ActivityCancelled, strict=True) +@pytest.mark.xfail(raises=TaskCancelled, strict=True) async def test_result(): async def make_job(): async with Scope() as scope: diff --git a/usim_pytest/test_types/test_activity.py b/usim_pytest/test_types/test_activity.py index 6ff975e..f4105ae 100644 --- a/usim_pytest/test_types/test_activity.py +++ b/usim_pytest/test_types/test_activity.py @@ -1,4 +1,4 @@ -from usim import time, Scope, ActivityState, instant +from usim import time, Scope, TaskState, instant from ..utility import via_usim @@ -38,40 +38,40 @@ async def test_result(self): async def test_state_success(self): async with Scope() as scope: activity = scope.do(sleep(20)) - assert activity.status == ActivityState.CREATED + assert activity.status == TaskState.CREATED await instant - assert activity.status == ActivityState.RUNNING + assert activity.status == TaskState.RUNNING await activity.done - assert activity.status == ActivityState.SUCCESS - assert activity.status & ActivityState.FINISHED + assert activity.status == TaskState.SUCCESS + assert activity.status & TaskState.FINISHED @via_usim async def test_state_cancel_created(self): async with Scope() as scope: activity = scope.do(sleep(20)) - assert activity.status == ActivityState.CREATED + assert activity.status == TaskState.CREATED activity.cancel() # early cancellation does not run - assert activity.status == ActivityState.CANCELLED + assert activity.status == TaskState.CANCELLED await instant - assert activity.status == ActivityState.CANCELLED + assert activity.status == TaskState.CANCELLED await activity.done - assert activity.status == ActivityState.CANCELLED - assert activity.status & ActivityState.FINISHED + assert activity.status == TaskState.CANCELLED + assert activity.status & TaskState.FINISHED @via_usim async def test_state_cancel_running(self): async with Scope() as scope: activity = scope.do(sleep(20)) - assert activity.status == ActivityState.CREATED + assert activity.status == TaskState.CREATED await instant - assert activity.status == ActivityState.RUNNING + assert activity.status == TaskState.RUNNING activity.cancel() # running cancellation is graceful - assert activity.status == ActivityState.RUNNING + assert activity.status == TaskState.RUNNING await activity.done - assert activity.status == ActivityState.CANCELLED - assert activity.status & ActivityState.FINISHED + assert activity.status == TaskState.CANCELLED + assert activity.status & TaskState.FINISHED @via_usim async def test_condition(self): From ca531ad5358b47d015e0ff3599192c8d7661d2bc Mon Sep 17 00:00:00 2001 From: Eileen Kuehn Date: Thu, 25 Apr 2019 22:42:51 +0200 Subject: [PATCH 20/20] Apply suggestions from code review Co-Authored-By: maxfischer2781 --- docs/source/glossary.rst | 2 +- docs/source/tutorial/04_cancel_scope.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index d2647db..c493d2a 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -4,7 +4,7 @@ Glossary of Terms .. Using references in the glossary itself: When mentioning other items, always reference them. - When mention the current item, never reference it. + When mentioning the current item, never reference it. .. glossary:: diff --git a/docs/source/tutorial/04_cancel_scope.rst b/docs/source/tutorial/04_cancel_scope.rst index f2457a6..de60811 100644 --- a/docs/source/tutorial/04_cancel_scope.rst +++ b/docs/source/tutorial/04_cancel_scope.rst @@ -4,7 +4,7 @@ Interlude 01: Interrupting Scopes .. code:: python3 - >>> from usim import time, until as out + >>> from usim import time, until >>> >>> async def deliver_one(which): ... print('Delivering', which, 'at', time.now)