-
Notifications
You must be signed in to change notification settings - Fork 113
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
Proposal for improved safety of assert_that #149
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ | |
__tracebackhide__ = True | ||
|
||
T = TypeVar("T") | ||
_unassigned = object() | ||
|
||
|
||
@overload | ||
|
@@ -25,7 +26,7 @@ def assert_that(assertion: bool, reason: str = "") -> None: | |
... | ||
|
||
|
||
def assert_that(actual, matcher=None, reason=""): | ||
def assert_that(actual, matcher=_unassigned, reason=""): | ||
"""Asserts that actual value satisfies matcher. (Can also assert plain | ||
boolean condition.) | ||
|
||
|
@@ -52,14 +53,43 @@ def assert_that(actual, matcher=None, reason=""): | |
This is equivalent to the :py:meth:`~unittest.TestCase.assertTrue` method | ||
of :py:class:`unittest.TestCase`, but offers greater flexibility in test | ||
writing by being a standalone function. | ||
|
||
""" | ||
if isinstance(matcher, Matcher): | ||
_assert_match(actual=actual, matcher=matcher, reason=reason) | ||
else: | ||
if isinstance(actual, Matcher): | ||
warnings.warn("arg1 should be boolean, but was {}".format(type(actual))) | ||
_assert_bool(assertion=cast(bool, actual), reason=cast(str, matcher)) | ||
return _assert_match(actual=actual, matcher=matcher, reason=reason) | ||
|
||
# Warn if we got a Matcher first | ||
if isinstance(actual, Matcher): | ||
warnings.warn("arg1 should be boolean, but was {}".format(type(actual))) | ||
# Or if actual and matcher are both truthy strings | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You aren't checking if matcher is truthy. |
||
if isinstance(actual, str) and isinstance(matcher, str) and actual: | ||
warnings.warn("assert_that(<str>, <str>) only evaluates whether the " | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps we could suggest resolving the ambiguity by passing the reason as a named parameter, in the event that they truly intended to do a truthiness assertion? |
||
"first argument is True, so the second string is always " | ||
"ignored. Make sure to use an explicit matcher for the " | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It isn't always ignored. It is used as the reason string in the event that the first string is falsey. |
||
"second term") | ||
# If matcher is not a string (and so not a 'reason'), and is the same | ||
# type as the actual, it's almost certain that assert_that was invoked | ||
# with the intention that actual == matcher be evaluated. Since this will | ||
# always appear to "pass" if "actual" itself evaluates to boolean True | ||
# (which happens far more often than not), make this a hard error. This | ||
# situation will almost always (if not always) be a buggy test. | ||
if not isinstance(matcher, str) and type(matcher) is type(actual): | ||
raise TypeError('matcher should be a Matcher instance, or simply ' | ||
'provide a reason if you wish to evaluate the actual ' | ||
'value as a boolean.') | ||
# It really only makes sense for matcher to be a Matcher instance or a | ||
# string (actually the 'reason'). Anything besides that is more likely to | ||
# be a mistake than anything else, so properly warn. | ||
if not isinstance(matcher, str): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't warn here if |
||
warnings.warn('matcher was not an explicit Matcher instance, so the ' | ||
'assert_that check will pass if the first argument ' | ||
'simply evaluates to boolean True. This is unusual.') | ||
# If someone provided an explicit 'reason', and passed a matcher that was | ||
# not a Matcher instance, this is almost certainly a mistake. | ||
if reason and matcher is not _unassigned: | ||
raise TypeError('matcher should be a Matcher instance, not {}'.format( | ||
type(matcher))) | ||
|
||
return _assert_bool(assertion=cast(bool, actual), reason=cast(str, matcher)) | ||
|
||
|
||
def _assert_match(actual: T, matcher: Matcher[T], reason: str) -> None: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this perhaps be simplified? (Forgive my somewhat pseudocode.)
I think we should avoid introducing a
TypeError
at this moment, because that is a breaking change. It would be nicer to have an intermediate release that warns about suspicious usages, allowing people to fix these issues without having to fix everything at once.