Skip to content

Commit

Permalink
Add matcher for exceptions in asyncio future
Browse files Browse the repository at this point in the history
WIP: Based on raises matcher but adapted to deal with future objects.

Example of use

```
assert_that(
    await resolved(raise_exception()),
    future_exception(AssertionError))
)
```

The resolved helper is used to create resolved future objects in async
code. It takes a "future like" object and waits for it to complete.

Ref hamcrest#155
  • Loading branch information
keis committed Mar 11, 2021
1 parent 3b11537 commit c151c46
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 0 deletions.
122 changes: 122 additions & 0 deletions src/hamcrest/core/core/future.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import re
import sys
import asyncio
from typing import Any, Callable, Mapping, Optional, Tuple, Type, TypeVar, Union, Generator, Awaitable, cast
from weakref import ref

from hamcrest.core.base_matcher import BaseMatcher
from hamcrest.core.description import Description
from hamcrest.core.matcher import Matcher

__author__ = "David Keijser"
__copyright__ = "Copyright 2021 hamcrest.org"
__license__ = "BSD, see License.txt"

T = TypeVar('T')

# Same as used for asyncio.ensure_future
FutureT = Union[asyncio.Future[T], Generator[Any, None, T], Awaitable[T]]


class FutureException(BaseMatcher[asyncio.Future[T]]):
def __init__(
self,
expected: Type[Exception],
pattern: Optional[str] = None,
matching: Optional[Matcher] = None,
) -> None:
self.pattern = pattern
self.matcher = matching
self.expected = expected

def _matches(self, future: asyncio.Future[T]) -> bool:
if not asyncio.isfuture(future):
return False

if not future.done():
return False

exc = future.exception()
if exc is None:
return False

if isinstance(exc, self.expected):
if self.pattern is not None:
if re.search(self.pattern, str(exc)) is None:
return False
if self.matcher is not None:
if not self.matcher.matches(exc):
return False
return True

return False

def describe_to(self, description: Description) -> None:
description.append_text("Expected a future with exception %s" % self.expected)

def describe_mismatch(self, future: asyncio.Future[T], description: Description) -> None:
if not asyncio.isfuture(future):
description.append_text("%s is not a future" % future)
return

if not future.done():
description.append_text("%s is not completed yet" % future)
return

exc = future.exception()
if exc is None:
description.append_text("No exception raised.")
elif isinstance(exc, self.expected):
if self.pattern is not None or self.matcher is not None:
description.append_text("Correct assertion type raised, but ")
if self.pattern is not None:
description.append_text('the expected pattern ("%s") ' % self.pattern)
if self.pattern is not None and self.matcher is not None:
description.append_text("and ")
if self.matcher is not None:
description.append_description_of(self.matcher)
description.append_text(" ")
description.append_text('not found. Exception message was: "%s"' % str(exc))
else:
description.append_text(
"%r of type %s was raised instead" % (exc, type(exc))
)

def describe_match(self, future: asyncio.Future[T], match_description: Description) -> None:
exc = future.exception()
match_description.append_text(
"%r of type %s was raised." % (exc, type(exc))
)


def future_exception(exception: Type[Exception], pattern=None, matching=None) -> Matcher[asyncio.Future[T]]:
"""Matches a future with the expected exception.
:param exception: The class of the expected exception
:param pattern: Optional regular expression to match exception message.
:param matching: Optional Hamcrest matchers to apply to the exception.
Expects the actual to be an already resolved future. Optional argument
pattern should be a string containing a regular expression. If provided,
the string representation of the actual exception - e.g. `str(actual)` -
must match pattern.
Examples::
assert_that(somefuture, future_exception(ValueError))
assert_that(
await resolved(async_http_get()),
future_exception(HTTPError, matching=has_properties(status_code=500)
)
"""
return FutureException(exception, pattern, matching)


async def resolved(obj: FutureT[T]) -> asyncio.Future[T]:
"""Wait for a async operation to finish and return a resolved future object of the result.
:param obj: A future like object, a coroutine, or an awaitable object.
"""
fut = asyncio.ensure_future(obj)
await asyncio.wait([fut])
return fut
33 changes: 33 additions & 0 deletions tests/hamcrest_unit_test/core/future_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import sys
import unittest

import pytest
from hamcrest import has_properties, not_
from hamcrest.core.core.future import resolved, future_exception
from hamcrest_unit_test.matcher_test import MatcherTest, assert_mismatch_description

if __name__ == "__main__":
sys.path.insert(0, "..")
sys.path.insert(0, "../..")


__author__ = "David Keijser"
__copyright__ = "Copyright 2021 hamcrest.org"
__license__ = "BSD, see License.txt"


async def no_exception(*args, **kwargs):
return


async def raise_exception(*args, **kwargs):
raise AssertionError(str(args) + str(kwargs))


class FutureExceptionTest(MatcherTest, unittest.IsolatedAsyncioTestCase):
async def testMatchesIfFutureHasTheExactExceptionExpected(self):
self.assert_matches("Right exception", future_exception(AssertionError), await resolved(raise_exception()))

async def testDoesNotMatchTypeErrorIfActualIsNotAFuture(self):
self.assert_does_not_match("Not a future", future_exception(TypeError), 23)

0 comments on commit c151c46

Please sign in to comment.