Skip to content

Commit

Permalink
Merge pull request oremanj#1 from oremanj/tests
Browse files Browse the repository at this point in the history
Fix CI, add tests for new code
  • Loading branch information
oremanj authored Dec 12, 2019
2 parents e3a6bcd + f29e985 commit 010559c
Show file tree
Hide file tree
Showing 12 changed files with 499 additions and 20 deletions.
8 changes: 5 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ matrix:
- python: 3.6
env: CHECK_LINT=1
# The pypy tests are slow, so we list them first
# 'pypy3' on travis is currently 7.1-beta, which has an __init_subclass__ bug
# that we'd run into (fixed on 7.2)
#- python: pypy3
- language: generic
env: PYPY_NIGHTLY_BRANCH=py3.6
- python: 3.6
- python: 3.7
- python: 3.6-dev
- python: 3.7-dev
- python: 3.8-dev
- python: 3.8
- python: nightly

script:
- ./ci.sh
Expand Down
5 changes: 5 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ Currently we have:
* a readers-writer lock (``tricycle.RWLock``)
* slightly higher-level stream wrappers (``tricycle.BufferedReceiveStream``
and ``tricycle.TextReceiveStream``)
* some tools for managing cancellation (``tricycle.open_service_nursery()``
and ``tricycle.MultiCancelScope``)
* a way to make objects that want to keep background tasks running during the
object's lifetime (``tricycle.BackgroundObject`` and the more general
``tricycle.ScopedObject``)
* [watch this space!]


Expand Down
2 changes: 1 addition & 1 deletion check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ flake8 tricycle/ \
|| EXIT_STATUS=$?

# Run mypy
mypy --strict -p tricycle || EXIT_STATUS=$?
mypy --strict --implicit-reexport -p tricycle || EXIT_STATUS=$?

# Finally, leave a really clear warning of any issues and exit
if [ $EXIT_STATUS -ne 0 ]; then
Expand Down
2 changes: 1 addition & 1 deletion ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ else

INSTALLDIR=$(python -c "import os, tricycle; print(os.path.dirname(tricycle.__file__))")
cp ../setup.cfg $INSTALLDIR
pytest -W error -ra --junitxml=../test-results.xml --faulthandler-timeout=60 ${INSTALLDIR} --cov="$INSTALLDIR" --cov-config=../.coveragerc --verbose
pytest -W error -ra --junitxml=../test-results.xml -o faulthandler_timeout=60 ${INSTALLDIR} --cov="$INSTALLDIR" --cov-config=../.coveragerc --verbose

# Disable coverage on 3.8 until we run 3.8 on Windows CI too
# https://github.com/python-trio/trio/pull/784#issuecomment-446438407
Expand Down
4 changes: 2 additions & 2 deletions docs/source/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ disconnects::
stream: trio.abc.HalfCloseableStream
) -> AsyncIterator[trio.abc.ReceiveChannel[str], trio.abc.SendChannel[str]]:
async with trio.open_nursery() as nursery:
incoming_w, incoming_r = trio.open_memory_channel[Msg](0)
outgoing_w, outgoing_r = trio.open_memory_channel[Msg](0)
incoming_w, incoming_r = trio.open_memory_channel[str](0)
outgoing_w, outgoing_r = trio.open_memory_channel[str](0)
nursery.start_soon(receive_messages, stream, incoming_w)
nursery.start_soon(send_messages, outgoing_r, stream)
try:
Expand Down
4 changes: 2 additions & 2 deletions test-requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pytest-trio
pytest-faulthandler

# Tools
black == 19.10b0
black == 19.10b0; implementation_name == "cpython"
mypy >= 0.750; implementation_name == "cpython"
flake8

Expand All @@ -15,7 +15,7 @@ async_generator >= 1.9
trio >= 0.12.0
trio-typing >= 0.3.0

# typed-ast is required by mypy and doesn't build on PyPy;
# typed-ast is required by black + mypy and doesn't build on PyPy;
# it will be unconstrained in requirements.txt if we don't
# constrain it here
typed-ast; implementation_name == "cpython"
2 changes: 1 addition & 1 deletion test-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
appdirs==1.4.3 # via black
async-generator==1.10
attrs==19.3.0
black==19.10b0
black==19.10b0 ; implementation_name == "cpython"
click==7.0 # via black
contextvars==2.4 # via sniffio, trio
coverage==4.5.4 # via pytest-cov
Expand Down
17 changes: 9 additions & 8 deletions tricycle/_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
Callable,
ClassVar,
Dict,
GenericMeta,
Optional,
Type,
TypeVar,
TYPE_CHECKING,
)
from ._service_nursery import open_service_nursery

Expand Down Expand Up @@ -125,14 +125,15 @@ async def __wrap__(self):
async def __wrap__(self) -> AsyncIterator[None]:
yield

# These are necessary to placate mypy, which doesn't understand
# the asynccontextmanager metaclass __call__. They should never
# actually get called.
async def __aenter__(self: T) -> T:
raise AssertionError
if TYPE_CHECKING:
# These are necessary to placate mypy, which doesn't understand
# the asynccontextmanager metaclass __call__. They should never
# actually get called.
async def __aenter__(self: T) -> T:
raise AssertionError

async def __aexit__(self, *exc: object) -> None:
raise AssertionError
async def __aexit__(self, *exc: object) -> None:
raise AssertionError


class BackgroundObject(ScopedObject):
Expand Down
12 changes: 10 additions & 2 deletions tricycle/_multi_cancel.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import attr
import trio
import weakref
from typing import MutableSet, Optional
from typing import Iterator, MutableSet, Optional


@attr.s(eq=False)
@attr.s(eq=False, repr=False)
class MultiCancelScope:
r"""Manages a dynamic set of :class:`trio.CancelScope`\s that can be
shielded and cancelled as a unit.
Expand All @@ -30,6 +30,14 @@ class MultiCancelScope:
_shield: bool = attr.ib(default=False, kw_only=True)
_cancel_called: bool = attr.ib(default=False, kw_only=True)

def __repr__(self) -> str:
descr = ["MultiCancelScope"]
if self._shield:
descr.append(" shielded")
if self._cancel_called:
descr.append(" cancelled")
return f"<{''.join(descr)}: {list(self._child_scopes)}>"

@property
def cancel_called(self) -> bool:
"""Returns true if :meth:`cancel` has been called."""
Expand Down
160 changes: 160 additions & 0 deletions tricycle/_tests/test_meta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import attr
import pytest # type: ignore
import types
import trio
import trio.testing
from async_generator import asynccontextmanager
from typing import Any, AsyncIterator, Coroutine, Iterator, List
from trio_typing import TaskStatus

from .. import ScopedObject, BackgroundObject


def test_too_much_magic() -> None:
with pytest.raises(TypeError) as info:

class TooMuchMagic(ScopedObject): # pragma: no cover
async def __open__(self) -> None:
pass

@asynccontextmanager
async def __wrap__(self) -> AsyncIterator[None]:
yield

assert str(info.value) == (
"ScopedObjects can define __open__/__close__, or __wrap__, but not both"
)


@types.coroutine
def async_yield(value: str) -> Iterator[str]:
yield value


def test_mro() -> None:
class A(ScopedObject):
async def __open__(self) -> None:
await async_yield("open A")

class B(A):
async def __open__(self) -> None:
await async_yield("open B")

async def __close__(self) -> None:
await async_yield("close B")

class C(A):
async def __open__(self) -> None:
await async_yield("open C")

async def __close__(self) -> None:
await async_yield("close C")

class D(B, C):
def __init__(self, value: int):
self.value = value

async def __close__(self) -> None:
await async_yield("close D")

assert D.__mro__ == (D, B, C, A, ScopedObject, object)
d_mgr = D(42)
assert not isinstance(d_mgr, D)
assert not hasattr(d_mgr, "value")
assert hasattr(d_mgr, "__aenter__")

async def use_it() -> None:
async with d_mgr as d:
assert isinstance(d, D)
assert d.value == 42
await async_yield("body")

coro: Coroutine[str, None, None] = use_it()
record = []
while True:
try:
record.append(coro.send(None))
except StopIteration:
break
assert record == [
"open A",
"open C",
"open B",
"body",
"close D",
"close B",
"close C",
]


@attr.s(auto_attribs=True)
class Example(BackgroundObject):
ticks: int = 0
record: List[str] = attr.Factory(list)
exiting: bool = False

def __attrs_post_init__(self) -> None:
assert not hasattr(self, "nursery")
self.record.append("attrs_post_init")

async def __open__(self) -> None:
self.record.append("open")
await self.nursery.start(self._background_task)
self.record.append("started")

async def __close__(self) -> None:
assert len(self.nursery.child_tasks) != 0
# Make sure this doesn't raise AttributeError in aexit:
del self.nursery
self.record.append("close")
self.exiting = True

async def _background_task(self, *, task_status: TaskStatus[None]) -> None:
self.record.append("background")
await trio.sleep(1)
self.record.append("starting")
task_status.started()
self.record.append("running")
while not self.exiting:
await trio.sleep(1)
self.ticks += 1
self.record.append("stopping")


class DaemonExample(Example, daemon=True):
pass


async def test_background(autojump_clock: trio.testing.MockClock) -> None:
async with Example(ticks=100) as obj:
assert obj.record == [
"attrs_post_init",
"open",
"background",
"starting",
"running",
"started",
]
del obj.record[:]
await trio.sleep(5.5)
assert obj.record == ["close", "stopping"]
# 1 sec start + 6 ticks
assert trio.current_time() == 7.0
assert obj.ticks == 106
assert not hasattr(obj, "nursery")

# With daemon=True, the background tasks are cancelled when the parent exits
async with DaemonExample() as obj2:
assert obj2.record == [
"attrs_post_init",
"open",
"background",
"starting",
"running",
"started",
]
del obj2.record[:]
await trio.sleep(5.5)
assert obj2.record == ["close"]
assert trio.current_time() == 13.5
assert obj2.ticks == 5
Loading

0 comments on commit 010559c

Please sign in to comment.