Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to using standalone Outcome library. #503

Merged
merged 15 commits into from
Apr 21, 2018
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def setup(app):

intersphinx_mapping = {
"python": ('https://docs.python.org/3', None),
"outcome": ('https://outcome.readthedocs.org/en/latest/', None),
}

autodoc_member_order = "bysource"
Expand Down
4 changes: 1 addition & 3 deletions docs/source/design.rst
Original file line number Diff line number Diff line change
Expand Up @@ -458,12 +458,10 @@ of our public APIs without having to modify trio internals.
Inside ``trio._core``
~~~~~~~~~~~~~~~~~~~~~

There are three notable sub-modules that are largely independent of
There are two notable sub-modules that are largely independent of
the rest of trio, and could (possibly should?) be extracted into their
own independent packages:

* ``_result.py``: Defines :class:`~trio.hazmat.Result`.

* ``_multierror.py``: Implements :class:`MultiError` and associated
infrastructure.

Expand Down
6 changes: 3 additions & 3 deletions docs/source/history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,9 @@ Upcoming breaking changes with warnings (i.e., stuff that in 0.2.0

* ``trio.Task`` → :class:`trio.hazmat.Task`
* ``trio.current_task`` → :func:`trio.hazmat.current_task`
* ``trio.Result`` → :class:`trio.hazmat.Result`
* ``trio.Value`` → :class:`trio.hazmat.Value`
* ``trio.Error`` → :class:`trio.hazmat.Error`
* ``trio.Result`` → ``trio.hazmat.Result``
* ``trio.Value`` → ``trio.hazmat.Value``
* ``trio.Error`` → ``trio.hazmat.Error``
* ``trio.UnboundedQueue`` → :class:`trio.hazmat.UnboundedQueue`

In addition, several introspection attributes are being renamed:
Expand Down
37 changes: 0 additions & 37 deletions docs/source/reference-hazmat.rst
Original file line number Diff line number Diff line change
Expand Up @@ -358,43 +358,6 @@ These transitions are accomplished using two function decorators:
.. autofunction:: currently_ki_protected


Result objects
==============

Trio provides some simple classes for representing the result of a
Python function call, so that it can be passed around. The basic rule
is::

result = Result.capture(f, *args)
x = result.unwrap()

is the same as::

x = f(*args)

even if ``f`` raises an error. And there's also
:meth:`Result.acapture`, which is like ``await f(*args)``.

There's nothing really dangerous about this system – it's actually
very general and quite handy! But mostly it's used for things like
implementing :func:`trio.run_sync_in_worker_thread`, or for getting
values to pass to :func:`reschedule`, so we put it in
:mod:`trio.hazmat` to avoid cluttering up the main API.

Since :class:`Result` objects are simple immutable data structures
that don't otherwise interact with the trio machinery, it's safe to
create and access :class:`Result` objects from any thread you like.

.. autoclass:: Result
:members:

.. autoclass:: Value
:members:

.. autoclass:: Error
:members:


Sleeping and waking
===================

Expand Down
3 changes: 3 additions & 0 deletions newsfragments/494.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Switch to using standalone
`Outcome <https://github.com/python-trio/outcome>`__ library for Result
objects.
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"sortedcontainers",
"async_generator >= 1.9",
"idna",
"outcome",
# PEP 508 style, but:
# https://bitbucket.org/pypa/wheel/issues/181/bdist_wheel-silently-discards-pep-508
#"cffi; os_name == 'nt'", # "cffi is required on windows"
Expand Down
16 changes: 16 additions & 0 deletions trio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@
from . import abc
from . import ssl
# Not imported by default: testing
_deprecate.enable_attribute_deprecations(hazmat.__name__)

__deprecated_attributes__ = {
"Result":
_deprecate.DeprecatedAttribute(
hazmat.Result, "0.5.0", issue=494, instead="outcome.Outcome"
),
"Value":
_deprecate.DeprecatedAttribute(
hazmat.Value, "0.5.0", issue=494, instead="outcome.Value"
),
"Error":
_deprecate.DeprecatedAttribute(
hazmat.Error, "0.5.0", issue=494, instead="outcome.Error"
)
}

# Having the public path in .__module__ attributes is important for:
# - exception names in printed tracebacks
Expand Down
140 changes: 24 additions & 116 deletions trio/_core/_result.py
Original file line number Diff line number Diff line change
@@ -1,125 +1,33 @@
import abc
import attr
import outcome

__all__ = ["Result", "Value", "Error"]


class Result(metaclass=abc.ABCMeta):
"""An abstract class representing the result of a Python computation.

This class has two concrete subclasses: :class:`Value` representing a
value, and :class:`Error` representing an exception.

In addition to the methods described below, comparison operators on
:class:`Value` and :class:`Error` objects (``==``, ``<``, etc.) check that
the other object is also a :class:`Value` or :class:`Error` object
respectively, and then compare the contained objects.

:class:`Result` objects are hashable if the contained objects are
hashable.

"""
__slots__ = ()

@staticmethod
def capture(sync_fn, *args):
"""Run ``sync_fn(*args)`` and capture the result.

Returns:
Either a :class:`Value` or :class:`Error` as appropriate.

"""
try:
return Value(sync_fn(*args))
except BaseException as exc:
return Error(exc)

@staticmethod
async def acapture(async_fn, *args):
"""Run ``await async_fn(*args)`` and capture the result.

Returns:
Either a :class:`Value` or :class:`Error` as appropriate.

"""
try:
return Value(await async_fn(*args))
except BaseException as exc:
return Error(exc)

@abc.abstractmethod
def unwrap(self):
"""Return or raise the contained value or exception.

These two lines of code are equivalent::

x = fn(*args)
x = Result.capture(fn, *args).unwrap()

"""

@abc.abstractmethod
def send(self, gen):
"""Send or throw the contained value or exception into the given
generator object.

Args:
gen: A generator object supporting ``.send()`` and ``.throw()``
methods.

"""

@abc.abstractmethod
async def asend(self, agen):
"""Send or throw the contained value or exception into the given async
generator object.

Args:
agen: An async generator object supporting ``.asend()`` and
``.athrow()`` methods.

"""


@attr.s(frozen=True, repr=False)
class Value(Result):
"""Concrete :class:`Result` subclass representing a regular value.

"""

value = attr.ib()
"""The contained value."""

def __repr__(self):
return "Value({!r})".format(self.value)

def unwrap(self):
return self.value

def send(self, gen):
return gen.send(self.value)

async def asend(self, agen):
return await agen.asend(self.value)
from .. import _deprecate

__all__ = ["Result", "Value", "Error"]

@attr.s(frozen=True, repr=False)
class Error(Result):
"""Concrete :class:`Result` subclass representing a raised exception.
_deprecate.enable_attribute_deprecations(__name__)

"""

error = attr.ib(validator=attr.validators.instance_of(BaseException))
"""The contained exception object."""
class Result(outcome.Outcome):
@classmethod
@_deprecate.deprecated(
version="0.5.0", issue=494, instead="outcome.capture"
)
def capture(cls, sync_fn, *args):
return outcome.capture(sync_fn, *args)

def __repr__(self):
return "Error({!r})".format(self.error)
@classmethod
@_deprecate.deprecated(
version="0.5.0", issue=494, instead="outcome.acapture"
)
async def acapture(cls, async_fn, *args):
return await outcome.acapture(async_fn, *args)

def unwrap(self):
raise self.error

def send(self, it):
return it.throw(self.error)
# alias these types so they don't mysteriously disappear
Value = outcome.Value
Error = outcome.Error

async def asend(self, agen):
return await agen.athrow(self.error)
# ensure that isinstance(Value(), Result)/issubclass(Value, Result) and etc
# don't break
Result.register(Value)
Result.register(Error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh. Could you please keep the newline?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, missed that. Surprised yapf didn't catch it.

12 changes: 7 additions & 5 deletions trio/_core/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
import threading
from collections import deque
from contextlib import contextmanager, closing

import outcome
from contextvars import copy_context
from math import inf
from time import monotonic

import attr
from async_generator import async_generator, yield_, asynccontextmanager
from sortedcontainers import SortedDict
from outcome import Outcome, Error, Value

from . import _public
from ._entry_queue import EntryQueue, TrioToken
Expand All @@ -23,7 +26,6 @@
enable_ki_protection
)
from ._multierror import MultiError
from ._result import Result, Error, Value
from ._traps import (
Abort,
wait_task_rescheduled,
Expand Down Expand Up @@ -402,7 +404,7 @@ async def _nested_child_finished(self, nested_child_exc):
# KeyboardInterrupt), then save that, but still wait until our
# children finish.
def aborted(raise_cancel):
self._add_exc(Result.capture(raise_cancel).error)
self._add_exc(outcome.capture(raise_cancel).error)
return Abort.FAILED

self._parent_waiting_in_aexit = True
Expand Down Expand Up @@ -542,7 +544,7 @@ def _attempt_abort(self, raise_cancel):
# whether we succeeded or failed.
self._abort_func = None
if success is Abort.SUCCEEDED:
self._runner.reschedule(self, Result.capture(raise_cancel))
self._runner.reschedule(self, outcome.capture(raise_cancel))

def _attempt_delivery_of_any_pending_cancel(self):
if self._abort_func is None:
Expand Down Expand Up @@ -690,7 +692,7 @@ def current_root_task(self):
@_public
def reschedule(self, task, next_send=Value(None)):
"""Reschedule the given task with the given
:class:`~trio.hazmat.Result`.
:class:`outcome.Result`.

See :func:`wait_task_rescheduled` for the gory details.

Expand All @@ -702,7 +704,7 @@ def reschedule(self, task, next_send=Value(None)):
Args:
task (trio.hazmat.Task): the task to be rescheduled. Must be blocked
in a call to :func:`wait_task_rescheduled`.
next_send (trio.hazmat.Result): the value (or error) to return (or
next_send (outcome.Result): the value (or error) to return (or
raise) from :func:`wait_task_rescheduled`.

"""
Expand Down
2 changes: 1 addition & 1 deletion trio/_core/_traps.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def abort_func(raise_cancel):
# Catch the exception from raise_cancel and inject it into the task.
# (This is what trio does automatically for you if you return
# Abort.SUCCEEDED.)
trio.hazmat.reschedule(task, Result.capture(raise_cancel))
trio.hazmat.reschedule(task, outcome.capture(raise_cancel))

# Option 2:
# wait to be woken by "someone", and then decide whether to raise
Expand Down
3 changes: 2 additions & 1 deletion trio/_core/tests/test_ki.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import outcome
import pytest
import sys
import os
Expand Down Expand Up @@ -355,7 +356,7 @@ async def main():
task = _core.current_task()

def abort(raise_cancel):
result = _core.Result.capture(raise_cancel)
result = outcome.capture(raise_cancel)
_core.reschedule(task, result)
return _core.Abort.FAILED

Expand Down
3 changes: 2 additions & 1 deletion trio/_core/tests/test_local.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import outcome
import pytest

import threading
Expand Down Expand Up @@ -110,7 +111,7 @@ async def main(x, in_q, out_q):
assert r.attr == x

def harness(x, in_q, out_q):
result_q.put(_core.Result.capture(_core.run, main, x, in_q, out_q))
result_q.put(outcome.capture(_core.run, main, x, in_q, out_q))

in_q1 = queue.Queue()
out_q1 = queue.Queue()
Expand Down
Loading