Skip to content

Commit

Permalink
Allow desc= argument to Matching
Browse files Browse the repository at this point in the history
  • Loading branch information
Xion committed Apr 23, 2016
1 parent d6f7b72 commit c63b1bd
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 21 deletions.
2 changes: 2 additions & 0 deletions callee/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

IS_PY3 = sys.version_info[0] == 3

STRING_TYPES = (str,) if IS_PY3 else (basestring,)


class MetaclassDecorator(object):
"""Decorator for creating a class through a metaclass.
Expand Down
83 changes: 62 additions & 21 deletions callee/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from itertools import starmap
from operator import itemgetter

from callee._compat import IS_PY3, STRING_TYPES
from callee.base import BaseMatcher, Eq


Expand Down Expand Up @@ -32,16 +33,44 @@ def __repr__(self):
class Matching(BaseMatcher):
"""Matches an object that satisfies given predicate."""

def __init__(self, predicate):
MAX_DESC_LENGTH = 32

def __init__(self, predicate, desc=None):
"""
:param predicate: Callable taking a single argument
and returning True or False
:param desc: Optional description of the predicate.
This will be displayed as a part of the error message
on failed assertion.
"""
if not callable(predicate):
raise TypeError(
"Matching requires a predicate, got %r" % (predicate,))

self.predicate = predicate
self.desc = self._validate_desc(desc)

def _validate_desc(self, desc):
"""Validate the predicate description."""
if desc is None:
return desc

if not isinstance(desc, STRING_TYPES):
raise TypeError(
"predicate description for Matching must be a string, "
"got %r" % (type(desc),))

# Python 2 mandates __repr__ to be an ASCII string,
# so if Unicode is passed (usually due to unicode_literals),
# it should be ASCII-encodable.
if not IS_PY3 and isinstance(desc, unicode):
try:
desc = desc.encode('ascii', errors='strict')
except UnicodeEncodeError:
raise TypeError("predicate description must be "
"an ASCII string in Python 2")

return desc

def match(self, value):
# Note that any possible exceptions from ``predicate``
Expand All @@ -54,28 +83,40 @@ def match(self, value):
def __repr__(self):
"""Return a representation of the matcher."""
name = getattr(self.predicate, '__name__', None)

# If not a lambda function, we can probably make the representation
# more readable by showing just the function's own name.
if name and name != '<lambda>':
# Where possible, make it a fully qualified name, including
# the module path. That's either on Python 3.3+ (via __qualname__),
# or when it's a standalone function (not a method).
qualname = getattr(self.predicate, '__qualname__', name)
is_method = inspect.ismethod(self.predicate) or \
isinstance(self.predicate, staticmethod)
if qualname != name or not is_method:
# Note that this shows inner functions (those defined locally
# inside other functions) as if they were global to the module.
# This is why we use colon (:) as separator here, as to not
# suggest this is an evaluatable identifier.
name = '%s:%s' % (self.predicate.__module__, qualname)
desc = self.desc

# When no user-provided description is available,
# use function's own name or even its repr().
if desc is None:
# If not a lambda function, we can probably make the representation
# more readable by showing just the function's own name.
if name and name != '<lambda>':
# Where possible, make it a fully qualified name, including
# the module path. This is either on Python 3.3+
# (via __qualname__), or when the predicate is
# a standalone function (not a method).
qualname = getattr(self.predicate, '__qualname__', name)
is_method = inspect.ismethod(self.predicate) or \
isinstance(self.predicate, staticmethod)
if qualname != name or not is_method:
# Note that this shows inner functions (those defined
# locally inside other functions) as if they were global
# to the module.
# This is why we use colon (:) as separator here, as to not
# suggest this is an evaluatable identifier.
name = '%s:%s' % (self.predicate.__module__, qualname)
else:
# For lambdas and other callable objects,
# we'll just default to the Python repr().
name = None
else:
# For lambdas and other callable objects,
# we'll just default to the Python repr().
name = None
# Quote and possibly ellipsize the provided description.
if len(desc) > self.MAX_DESC_LENGTH:
ellipsis = '...'
desc = desc[:self.MAX_DESC_LENGTH - len(ellipsis)] + ellipsis
desc = '"%s"' % desc

return "<Matching %s>" % (name or repr(self.predicate))
return "<Matching %s>" % (desc or name or repr(self.predicate))

ArgThat = Matching

Expand Down
17 changes: 17 additions & 0 deletions tests/test_general.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,23 @@ def assert_no_match(self, value, predicate):
class MatchingRepr(MatcherTestCase):
"""Tests for the __repr__ method of Matching."""

def tesc_desc__empty(self):
matcher = __unit__.Matching(bool, "")
self.assertIn('""', repr(matcher))

def test_desc__nonempty(self):
desc = "Truthy"
matcher = __unit__.Matching(bool, desc)
self.assertIn(desc, repr(matcher))

def test_desc__trimmed(self):
desc = "Long description with extraneous characters: %s" % (
"x" * __unit__.Matching.MAX_DESC_LENGTH,)
matcher = __unit__.Matching(bool, desc)

self.assertNotIn(desc, repr(matcher))
self.assertIn("...", repr(matcher))

def test_lambda(self):
self.assert_lambda_repr(lambda _: True)

Expand Down

0 comments on commit c63b1bd

Please sign in to comment.