Skip to content

Commit

Permalink
Fix CI, add tests for new code
Browse files Browse the repository at this point in the history
  • Loading branch information
oremanj committed Dec 12, 2019
1 parent e3a6bcd commit b6bf428
Show file tree
Hide file tree
Showing 7 changed files with 467 additions and 11 deletions.
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
16 changes: 9 additions & 7 deletions tricycle/_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
Optional,
Type,
TypeVar,
TYPE_CHECKING,
)
from ._service_nursery import open_service_nursery

Expand Down Expand Up @@ -125,14 +126,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
144 changes: 144 additions & 0 deletions tricycle/_tests/test_meta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import attr
import pytest # type: ignore
import types
import trio
import trio.testing
from async_generator import asynccontextmanager
from typing import Any, AsyncIterator, Coroutine, 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) -> None:
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 b6bf428

Please sign in to comment.