Repository: assertpy/assertpy

Issue:
ContainsMixin - missing return of an AssertionBuilder instance if verification fails.
I've found that some methods in `ContainsMixin` (` is_in`, `contains_sequence`,` contains_duplicates`, `does_not_contain_duplicates`) doesn't return the instance of the AssertionBuilder chain if the verification fail.

There is an 'exception' problem when I try to use `asser_warn` with some of these methods:

```python
assert_warn(1).is_in(2).contains_duplicates()
```

```
WARNING [<input>:1]: Expected <1> to be in <2>, but was not.
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'contains_duplicates'
```

Discussion:

Patch:
diff --git a/README.md b/README.md
index a180a80..5c1b782 100644
--- a/README.md
+++ b/README.md
@@ -941,7 +941,7 @@ from assertpy import add_extension
 
 def is_5(self):
     if self.val != 5:
-        self.error(f'{self.val} is NOT 5!')
+        return self.error(f'{self.val} is NOT 5!')
     return self
 
 add_extension(is_5)
@@ -964,7 +964,7 @@ from assertpy import add_extension
 
 def is_5(self):
     if self.val != 5:
-        self.error(f'{self.val} is NOT 5!')
+        return self.error(f'{self.val} is NOT 5!')
     return self
 
 @pytest.fixture(scope='module')
@@ -1014,7 +1014,7 @@ def is_multiple_of(self, other):
     # test the negative (is remainder non-zero?)
     if rem > 0:
         # non-zero remainder, so not multiple -> we fail!
-        self.error('Expected <%s> to be multiple of <%s>, but was not.' % (self.val, other))
+        return self.error('Expected <%s> to be multiple of <%s>, but was not.' % (self.val, other))
 
     # success, and return self to allow chaining
     return self
diff --git a/assertpy/assertpy.py b/assertpy/assertpy.py
index 6a8bc65..39abb20 100644
--- a/assertpy/assertpy.py
+++ b/assertpy/assertpy.py
@@ -294,7 +294,7 @@ def add_extension(func):
 
             def is_5(self):
                 if self.val != 5:
-                    self.error(f'{self.val} is NOT 5!')
+                    return self.error(f'{self.val} is NOT 5!')
                 return self
 
             add_extension(is_5)
@@ -439,12 +439,16 @@ class AssertionBuilder(
             Used to fail an assertion::
 
                 if self.val != other:
-                    self.error('Expected <%s> to be equal to <%s>, but was not.' % (self.val, other))
+                    return self.error('Expected <%s> to be equal to <%s>, but was not.' % (self.val, other))
 
         Raises:
             AssertionError: always raised unless ``kind`` is ``warn`` (as set when using an
                 :meth:`assert_warn` assertion) or ``kind`` is ``soft`` (as set when inside a
                 :meth:`soft_assertions` context).
+
+        Returns:
+            AssertionBuilder: returns this instance to chain to the next assertion, but only when
+                ``AssertionError`` is not raised, as is the case when ``kind`` is ``warn`` or ``soft``.
         """
         out = '%s%s' % ('[%s] ' % self.description if len(self.description) > 0 else '', msg)
         if self.kind == 'warn':
diff --git a/assertpy/base.py b/assertpy/base.py
index 3e08d40..795d8bc 100644
--- a/assertpy/base.py
+++ b/assertpy/base.py
@@ -123,7 +123,7 @@ class BaseMixin(object):
                 self._dict_err(self.val, other, ignore=kwargs.get('ignore'), include=kwargs.get('include'))
         else:
             if self.val != other:
-                self.error('Expected <%s> to be equal to <%s>, but was not.' % (self.val, other))
+                return self.error('Expected <%s> to be equal to <%s>, but was not.' % (self.val, other))
         return self
 
     def is_not_equal_to(self, other):
@@ -153,7 +153,7 @@ class BaseMixin(object):
             AssertionError: if actual **is** equal to expected
         """
         if self.val == other:
-            self.error('Expected <%s> to be not equal to <%s>, but was.' % (self.val, other))
+            return self.error('Expected <%s> to be not equal to <%s>, but was.' % (self.val, other))
         return self
 
     def is_same_as(self, other):
@@ -195,7 +195,7 @@ class BaseMixin(object):
             AssertionError: if actual is **not** identical to expected
         """
         if self.val is not other:
-            self.error('Expected <%s> to be identical to <%s>, but was not.' % (self.val, other))
+            return self.error('Expected <%s> to be identical to <%s>, but was not.' % (self.val, other))
         return self
 
     def is_not_same_as(self, other):
@@ -226,7 +226,7 @@ class BaseMixin(object):
             AssertionError: if actual **is** identical to expected
         """
         if self.val is other:
-            self.error('Expected <%s> to be not identical to <%s>, but was.' % (self.val, other))
+            return self.error('Expected <%s> to be not identical to <%s>, but was.' % (self.val, other))
         return self
 
     def is_true(self):
@@ -251,7 +251,7 @@ class BaseMixin(object):
             AssertionError: if val **is** false
         """
         if not self.val:
-            self.error('Expected <True>, but was not.')
+            return self.error('Expected <True>, but was not.')
         return self
 
     def is_false(self):
@@ -276,7 +276,7 @@ class BaseMixin(object):
             AssertionError: if val **is** true
         """
         if self.val:
-            self.error('Expected <False>, but was not.')
+            return self.error('Expected <False>, but was not.')
         return self
 
     def is_none(self):
@@ -295,7 +295,7 @@ class BaseMixin(object):
             AssertionError: if val is **not** none
         """
         if self.val is not None:
-            self.error('Expected <%s> to be <None>, but was not.' % self.val)
+            return self.error('Expected <%s> to be <None>, but was not.' % self.val)
         return self
 
     def is_not_none(self):
@@ -315,7 +315,7 @@ class BaseMixin(object):
             AssertionError: if val **is** none
         """
         if self.val is None:
-            self.error('Expected not <None>, but was.')
+            return self.error('Expected not <None>, but was.')
         return self
 
     def _type(self, val):
@@ -353,7 +353,7 @@ class BaseMixin(object):
             raise TypeError('given arg must be a type')
         if type(self.val) is not some_type:
             t = self._type(self.val)
-            self.error('Expected <%s:%s> to be of type <%s>, but was not.' % (self.val, t, some_type.__name__))
+            return self.error('Expected <%s:%s> to be of type <%s>, but was not.' % (self.val, t, some_type.__name__))
         return self
 
     def is_instance_of(self, some_class):
@@ -390,7 +390,7 @@ class BaseMixin(object):
         try:
             if not isinstance(self.val, some_class):
                 t = self._type(self.val)
-                self.error('Expected <%s:%s> to be instance of class <%s>, but was not.' % (self.val, t, some_class.__name__))
+                return self.error('Expected <%s:%s> to be instance of class <%s>, but was not.' % (self.val, t, some_class.__name__))
         except TypeError:
             raise TypeError('given arg must be a class')
         return self
@@ -423,5 +423,5 @@ class BaseMixin(object):
         if length < 0:
             raise ValueError('given arg must be a positive int')
         if len(self.val) != length:
-            self.error('Expected <%s> to be of length <%d>, but was <%d>.' % (self.val, length, len(self.val)))
+            return self.error('Expected <%s> to be of length <%d>, but was <%d>.' % (self.val, length, len(self.val)))
         return self
diff --git a/assertpy/collection.py b/assertpy/collection.py
index 5aa537d..3066ee8 100644
--- a/assertpy/collection.py
+++ b/assertpy/collection.py
@@ -57,7 +57,7 @@ class CollectionMixin(object):
             AssertionError: if val is **not** iterable
         """
         if not isinstance(self.val, Iterable):
-            self.error('Expected iterable, but was not.')
+            return self.error('Expected iterable, but was not.')
         return self
 
     def is_not_iterable(self):
@@ -78,7 +78,7 @@ class CollectionMixin(object):
             AssertionError: if val **is** iterable
         """
         if isinstance(self.val, Iterable):
-            self.error('Expected not iterable, but was.')
+            return self.error('Expected not iterable, but was.')
         return self
 
     def is_subset_of(self, *supersets):
@@ -129,7 +129,7 @@ class CollectionMixin(object):
                 elif self.val[i] != superdict[i]:
                     missing.append({i: self.val[i]})  # bad val
             if missing:
-                self.error('Expected <%s> to be subset of %s, but %s %s missing.' % (
+                return self.error('Expected <%s> to be subset of %s, but %s %s missing.' % (
                     self.val, self._fmt_items(superdict), self._fmt_items(missing), 'was' if len(missing) == 1 else 'were'))
         else:
             # flatten supersets
@@ -145,7 +145,7 @@ class CollectionMixin(object):
                 if i not in superset:
                     missing.append(i)
             if missing:
-                self.error('Expected <%s> to be subset of %s, but %s %s missing.' % (
+                return self.error('Expected <%s> to be subset of %s, but %s %s missing.' % (
                     self.val, self._fmt_items(superset), self._fmt_items(missing), 'was' if len(missing) == 1 else 'were'))
 
         return self
@@ -187,10 +187,10 @@ class CollectionMixin(object):
             if i > 0:
                 if reverse:
                     if key(x) > key(prev):
-                        self.error('Expected <%s> to be sorted reverse, but subset %s at index %s is not.' % (self.val, self._fmt_items([prev, x]), i-1))
+                        return self.error('Expected <%s> to be sorted reverse, but subset %s at index %s is not.' % (self.val, self._fmt_items([prev, x]), i-1))
                 else:
                     if key(x) < key(prev):
-                        self.error('Expected <%s> to be sorted, but subset %s at index %s is not.' % (self.val, self._fmt_items([prev, x]), i-1))
+                        return self.error('Expected <%s> to be sorted, but subset %s at index %s is not.' % (self.val, self._fmt_items([prev, x]), i-1))
             prev = x
 
         return self
diff --git a/assertpy/contains.py b/assertpy/contains.py
index 83eae00..0f25b49 100644
--- a/assertpy/contains.py
+++ b/assertpy/contains.py
@@ -78,9 +78,9 @@ class ContainsMixin(object):
         elif len(items) == 1:
             if items[0] not in self.val:
                 if self._check_dict_like(self.val, return_as_bool=True):
-                    self.error('Expected <%s> to contain key <%s>, but did not.' % (self.val, items[0]))
+                    return self.error('Expected <%s> to contain key <%s>, but did not.' % (self.val, items[0]))
                 else:
-                    self.error('Expected <%s> to contain item <%s>, but did not.' % (self.val, items[0]))
+                    return self.error('Expected <%s> to contain item <%s>, but did not.' % (self.val, items[0]))
         else:
             missing = []
             for i in items:
@@ -88,10 +88,10 @@ class ContainsMixin(object):
                     missing.append(i)
             if missing:
                 if self._check_dict_like(self.val, return_as_bool=True):
-                    self.error('Expected <%s> to contain keys %s, but did not contain key%s %s.' % (
+                    return self.error('Expected <%s> to contain keys %s, but did not contain key%s %s.' % (
                         self.val, self._fmt_items(items), '' if len(missing) == 0 else 's', self._fmt_items(missing)))
                 else:
-                    self.error('Expected <%s> to contain items %s, but did not contain %s.' % (self.val, self._fmt_items(items), self._fmt_items(missing)))
+                    return self.error('Expected <%s> to contain items %s, but did not contain %s.' % (self.val, self._fmt_items(items), self._fmt_items(missing)))
         return self
 
     def does_not_contain(self, *items):
@@ -126,14 +126,14 @@ class ContainsMixin(object):
             raise ValueError('one or more args must be given')
         elif len(items) == 1:
             if items[0] in self.val:
-                self.error('Expected <%s> to not contain item <%s>, but did.' % (self.val, items[0]))
+                return self.error('Expected <%s> to not contain item <%s>, but did.' % (self.val, items[0]))
         else:
             found = []
             for i in items:
                 if i in self.val:
                     found.append(i)
             if found:
-                self.error('Expected <%s> to not contain items %s, but did contain %s.' % (self.val, self._fmt_items(items), self._fmt_items(found)))
+                return self.error('Expected <%s> to not contain items %s, but did contain %s.' % (self.val, self._fmt_items(items), self._fmt_items(found)))
         return self
 
     def contains_only(self, *items):
@@ -167,14 +167,14 @@ class ContainsMixin(object):
                 if i not in items:
                     extra.append(i)
             if extra:
-                self.error('Expected <%s> to contain only %s, but did contain %s.' % (self.val, self._fmt_items(items), self._fmt_items(extra)))
+                return self.error('Expected <%s> to contain only %s, but did contain %s.' % (self.val, self._fmt_items(items), self._fmt_items(extra)))
 
             missing = []
             for i in items:
                 if i not in self.val:
                     missing.append(i)
             if missing:
-                self.error('Expected <%s> to contain only %s, but did not contain %s.' % (self.val, self._fmt_items(items), self._fmt_items(missing)))
+                return self.error('Expected <%s> to contain only %s, but did not contain %s.' % (self.val, self._fmt_items(items), self._fmt_items(missing)))
         return self
 
     def contains_sequence(self, *items):
@@ -211,7 +211,7 @@ class ContainsMixin(object):
                         return self
             except TypeError:
                 raise TypeError('val is not iterable')
-        self.error('Expected <%s> to contain sequence %s, but did not.' % (self.val, self._fmt_items(items)))
+        return self.error('Expected <%s> to contain sequence %s, but did not.' % (self.val, self._fmt_items(items)))
 
     def contains_duplicates(self):
         """Asserts that val is iterable and *does* contain duplicates.
@@ -234,7 +234,7 @@ class ContainsMixin(object):
                 return self
         except TypeError:
             raise TypeError('val is not iterable')
-        self.error('Expected <%s> to contain duplicates, but did not.' % self.val)
+        return self.error('Expected <%s> to contain duplicates, but did not.' % self.val)
 
     def does_not_contain_duplicates(self):
         """Asserts that val is iterable and *does not* contain any duplicates.
@@ -257,7 +257,7 @@ class ContainsMixin(object):
                 return self
         except TypeError:
             raise TypeError('val is not iterable')
-        self.error('Expected <%s> to not contain duplicates, but did.' % self.val)
+        return self.error('Expected <%s> to not contain duplicates, but did.' % self.val)
 
     def is_empty(self):
         """Asserts that val is empty.
@@ -279,9 +279,9 @@ class ContainsMixin(object):
         """
         if len(self.val) != 0:
             if isinstance(self.val, str_types):
-                self.error('Expected <%s> to be empty string, but was not.' % self.val)
+                return self.error('Expected <%s> to be empty string, but was not.' % self.val)
             else:
-                self.error('Expected <%s> to be empty, but was not.' % self.val)
+                return self.error('Expected <%s> to be empty, but was not.' % self.val)
         return self
 
     def is_not_empty(self):
@@ -304,9 +304,9 @@ class ContainsMixin(object):
         """
         if len(self.val) == 0:
             if isinstance(self.val, str_types):
-                self.error('Expected not empty string, but was empty.')
+                return self.error('Expected not empty string, but was empty.')
             else:
-                self.error('Expected not empty, but was empty.')
+                return self.error('Expected not empty, but was empty.')
         return self
 
     def is_in(self, *items):
@@ -333,7 +333,7 @@ class ContainsMixin(object):
             for i in items:
                 if self.val == i:
                     return self
-        self.error('Expected <%s> to be in %s, but was not.' % (self.val, self._fmt_items(items)))
+        return self.error('Expected <%s> to be in %s, but was not.' % (self.val, self._fmt_items(items)))
 
     def is_not_in(self, *items):
         """Asserts that val is not equal to one of the given items.
@@ -358,5 +358,5 @@ class ContainsMixin(object):
         else:
             for i in items:
                 if self.val == i:
-                    self.error('Expected <%s> to not be in %s, but was.' % (self.val, self._fmt_items(items)))
+                    return self.error('Expected <%s> to not be in %s, but was.' % (self.val, self._fmt_items(items)))
         return self
diff --git a/assertpy/date.py b/assertpy/date.py
index 5d532e1..0cdcb29 100644
--- a/assertpy/date.py
+++ b/assertpy/date.py
@@ -65,7 +65,7 @@ class DateMixin(object):
         if type(other) is not datetime.datetime:
             raise TypeError('given arg must be datetime, but was type <%s>' % type(other).__name__)
         if self.val >= other:
-            self.error('Expected <%s> to be before <%s>, but was not.' % (self.val.strftime('%Y-%m-%d %H:%M:%S'), other.strftime('%Y-%m-%d %H:%M:%S')))
+            return self.error('Expected <%s> to be before <%s>, but was not.' % (self.val.strftime('%Y-%m-%d %H:%M:%S'), other.strftime('%Y-%m-%d %H:%M:%S')))
         return self
 
     def is_after(self, other):
@@ -99,7 +99,7 @@ class DateMixin(object):
         if type(other) is not datetime.datetime:
             raise TypeError('given arg must be datetime, but was type <%s>' % type(other).__name__)
         if self.val <= other:
-            self.error('Expected <%s> to be after <%s>, but was not.' % (self.val.strftime('%Y-%m-%d %H:%M:%S'), other.strftime('%Y-%m-%d %H:%M:%S')))
+            return self.error('Expected <%s> to be after <%s>, but was not.' % (self.val.strftime('%Y-%m-%d %H:%M:%S'), other.strftime('%Y-%m-%d %H:%M:%S')))
         return self
 
     def is_equal_to_ignoring_milliseconds(self, other):
@@ -129,7 +129,7 @@ class DateMixin(object):
         if type(other) is not datetime.datetime:
             raise TypeError('given arg must be datetime, but was type <%s>' % type(other).__name__)
         if self.val.date() != other.date() or self.val.hour != other.hour or self.val.minute != other.minute or self.val.second != other.second:
-            self.error('Expected <%s> to be equal to <%s>, but was not.' % (self.val.strftime('%Y-%m-%d %H:%M:%S'), other.strftime('%Y-%m-%d %H:%M:%S')))
+            return self.error('Expected <%s> to be equal to <%s>, but was not.' % (self.val.strftime('%Y-%m-%d %H:%M:%S'), other.strftime('%Y-%m-%d %H:%M:%S')))
         return self
 
     def is_equal_to_ignoring_seconds(self, other):
@@ -159,7 +159,7 @@ class DateMixin(object):
         if type(other) is not datetime.datetime:
             raise TypeError('given arg must be datetime, but was type <%s>' % type(other).__name__)
         if self.val.date() != other.date() or self.val.hour != other.hour or self.val.minute != other.minute:
-            self.error('Expected <%s> to be equal to <%s>, but was not.' % (self.val.strftime('%Y-%m-%d %H:%M'), other.strftime('%Y-%m-%d %H:%M')))
+            return self.error('Expected <%s> to be equal to <%s>, but was not.' % (self.val.strftime('%Y-%m-%d %H:%M'), other.strftime('%Y-%m-%d %H:%M')))
         return self
 
     def is_equal_to_ignoring_time(self, other):
@@ -189,5 +189,5 @@ class DateMixin(object):
         if type(other) is not datetime.datetime:
             raise TypeError('given arg must be datetime, but was type <%s>' % type(other).__name__)
         if self.val.date() != other.date():
-            self.error('Expected <%s> to be equal to <%s>, but was not.' % (self.val.strftime('%Y-%m-%d'), other.strftime('%Y-%m-%d')))
+            return self.error('Expected <%s> to be equal to <%s>, but was not.' % (self.val.strftime('%Y-%m-%d'), other.strftime('%Y-%m-%d')))
         return self
diff --git a/assertpy/dict.py b/assertpy/dict.py
index 921dd88..e01dce7 100644
--- a/assertpy/dict.py
+++ b/assertpy/dict.py
@@ -106,7 +106,7 @@ class DictMixin(object):
             if v not in self.val.values():
                 missing.append(v)
         if missing:
-            self.error('Expected <%s> to contain values %s, but did not contain %s.' % (self.val, self._fmt_items(values), self._fmt_items(missing)))
+            return self.error('Expected <%s> to contain values %s, but did not contain %s.' % (self.val, self._fmt_items(values), self._fmt_items(missing)))
         return self
 
     def does_not_contain_value(self, *values):
@@ -138,7 +138,7 @@ class DictMixin(object):
                 if v in self.val.values():
                     found.append(v)
             if found:
-                self.error('Expected <%s> to not contain values %s, but did contain %s.' % (self.val, self._fmt_items(values), self._fmt_items(found)))
+                return self.error('Expected <%s> to not contain values %s, but did contain %s.' % (self.val, self._fmt_items(values), self._fmt_items(found)))
         return self
 
     def contains_entry(self, *args, **kwargs):
@@ -189,7 +189,7 @@ class DictMixin(object):
             elif self.val[k] != e[k]:
                 missing.append(e)  # bad val
         if missing:
-            self.error('Expected <%s> to contain entries %s, but did not contain %s.' % (self.val, self._fmt_items(entries), self._fmt_items(missing)))
+            return self.error('Expected <%s> to contain entries %s, but did not contain %s.' % (self.val, self._fmt_items(entries), self._fmt_items(missing)))
         return self
 
     def does_not_contain_entry(self, *args, **kwargs):
@@ -232,5 +232,5 @@ class DictMixin(object):
             if k in self.val and e[k] == self.val[k]:
                 found.append(e)
         if found:
-            self.error('Expected <%s> to not contain entries %s, but did contain %s.' % (self.val, self._fmt_items(entries), self._fmt_items(found)))
+            return self.error('Expected <%s> to not contain entries %s, but did contain %s.' % (self.val, self._fmt_items(entries), self._fmt_items(found)))
         return self
diff --git a/assertpy/dynamic.py b/assertpy/dynamic.py
index e09d146..b2d1c2e 100644
--- a/assertpy/dynamic.py
+++ b/assertpy/dynamic.py
@@ -91,7 +91,7 @@ class DynamicMixin(object):
 
         def _wrapper(*args, **kwargs):
             if err_msg:
-                self.error(err_msg)  # ok to raise AssertionError now that we are inside wrapper
+                return self.error(err_msg)  # ok to raise AssertionError now that we are inside wrapper
             else:
                 if len(args) != 1:
                     raise TypeError('assertion <%s()> takes exactly 1 argument (%d given)' % (attr, len(args)))
@@ -111,7 +111,7 @@ class DynamicMixin(object):
 
                 expected = args[0]
                 if actual != expected:
-                    self.error('Expected <%s> to be equal to <%s> on %s <%s>, but was not.' % (actual, expected, 'key' if is_dict else 'attribute', attr_name))
+                    return self.error('Expected <%s> to be equal to <%s> on %s <%s>, but was not.' % (actual, expected, 'key' if is_dict else 'attribute', attr_name))
             return self
 
         return _wrapper
diff --git a/assertpy/exception.py b/assertpy/exception.py
index 198026f..1649985 100644
--- a/assertpy/exception.py
+++ b/assertpy/exception.py
@@ -100,14 +100,14 @@ class ExceptionMixin(object):
                 return self.builder(str(e), self.description, self.kind)
             else:
                 # got exception, but wrong type, so raise
-                self.error('Expected <%s> to raise <%s> when called with (%s), but raised <%s>.' % (
+                return self.error('Expected <%s> to raise <%s> when called with (%s), but raised <%s>.' % (
                     self.val.__name__,
                     self.expected.__name__,
                     self._fmt_args_kwargs(*some_args, **some_kwargs),
                     type(e).__name__))
 
         # didn't fail as expected, so raise
-        self.error('Expected <%s> to raise <%s> when called with (%s).' % (
+        return self.error('Expected <%s> to raise <%s> when called with (%s).' % (
             self.val.__name__,
             self.expected.__name__,
             self._fmt_args_kwargs(*some_args, **some_kwargs)))
diff --git a/assertpy/extracting.py b/assertpy/extracting.py
index 73aead2..98d4ad8 100644
--- a/assertpy/extracting.py
+++ b/assertpy/extracting.py
@@ -182,7 +182,7 @@ class ExtractingMixin(object):
                     return getattr(x, name)
                 else: #val has no attribute <foo>
                     raise ValueError('item attributes %s did no contain attribute <%s>' % (x._fields, name))
-            elif isinstance(x, Iterable):
+            elif isinstance(x, Iterable): # FIXME, this does __getitem__, but doesn't check for it...
                 self._check_iterable(x, name='item')
                 return x[name]
             elif hasattr(x, name):
diff --git a/assertpy/file.py b/assertpy/file.py
index c98fd44..2c3de9a 100644
--- a/assertpy/file.py
+++ b/assertpy/file.py
@@ -109,7 +109,7 @@ class FileMixin(object):
         if not isinstance(self.val, str_types):
             raise TypeError('val is not a path')
         if not os.path.exists(self.val):
-            self.error('Expected <%s> to exist, but was not found.' % self.val)
+            return self.error('Expected <%s> to exist, but was not found.' % self.val)
         return self
 
     def does_not_exist(self):
@@ -130,7 +130,7 @@ class FileMixin(object):
         if not isinstance(self.val, str_types):
             raise TypeError('val is not a path')
         if os.path.exists(self.val):
-            self.error('Expected <%s> to not exist, but was found.' % self.val)
+            return self.error('Expected <%s> to not exist, but was found.' % self.val)
         return self
 
     def is_file(self):
@@ -149,7 +149,7 @@ class FileMixin(object):
         """
         self.exists()
         if not os.path.isfile(self.val):
-            self.error('Expected <%s> to be a file, but was not.' % self.val)
+            return self.error('Expected <%s> to be a file, but was not.' % self.val)
         return self
 
     def is_directory(self):
@@ -168,7 +168,7 @@ class FileMixin(object):
         """
         self.exists()
         if not os.path.isdir(self.val):
-            self.error('Expected <%s> to be a directory, but was not.' % self.val)
+            return self.error('Expected <%s> to be a directory, but was not.' % self.val)
         return self
 
     def is_named(self, filename):
@@ -193,7 +193,7 @@ class FileMixin(object):
             raise TypeError('given filename arg must be a path')
         val_filename = os.path.basename(os.path.abspath(self.val))
         if val_filename != filename:
-            self.error('Expected filename <%s> to be equal to <%s>, but was not.' % (val_filename, filename))
+            return self.error('Expected filename <%s> to be equal to <%s>, but was not.' % (val_filename, filename))
         return self
 
     def is_child_of(self, parent):
@@ -221,5 +221,5 @@ class FileMixin(object):
         val_abspath = os.path.abspath(self.val)
         parent_abspath = os.path.abspath(parent)
         if not val_abspath.startswith(parent_abspath):
-            self.error('Expected file <%s> to be a child of <%s>, but was not.' % (val_abspath, parent_abspath))
+            return self.error('Expected file <%s> to be a child of <%s>, but was not.' % (val_abspath, parent_abspath))
         return self
diff --git a/assertpy/helpers.py b/assertpy/helpers.py
index f3e4ffb..43ee784 100644
--- a/assertpy/helpers.py
+++ b/assertpy/helpers.py
@@ -162,7 +162,7 @@ class HelpersMixin(object):
                     if i not in val:
                         missing.append(i)
                 if missing:
-                    self.error('Expected <%s> to include key%s %s, but did not include key%s %s.' % (
+                    return self.error('Expected <%s> to include key%s %s, but did not include key%s %s.' % (
                         val,
                         '' if len(includes) == 1 else 's',
                         self._fmt_items(['.'.join([str(s) for s in i]) if type(i) is tuple else i for i in includes]),
@@ -242,7 +242,7 @@ class HelpersMixin(object):
             includes = self._dict_ignore(include)
             include_err = ' including keys %s' % self._fmt_items(['.'.join([str(s) for s in i]) if type(i) is tuple else i for i in includes])
 
-        self.error('Expected <%s> to be equal to <%s>%s%s, but was not.' % (
+        return self.error('Expected <%s> to be equal to <%s>%s%s, but was not.' % (
             _dict_repr(val, other),
             _dict_repr(other, val),
             ignore_err if ignore else '',
diff --git a/assertpy/numeric.py b/assertpy/numeric.py
index a62b77e..0e21936 100644
--- a/assertpy/numeric.py
+++ b/assertpy/numeric.py
@@ -120,7 +120,7 @@ class NumericMixin(object):
         self._validate_number()
         self._validate_real()
         if not math.isnan(self.val):
-            self.error('Expected <%s> to be <NaN>, but was not.' % self.val)
+            return self.error('Expected <%s> to be <NaN>, but was not.' % self.val)
         return self
 
     def is_not_nan(self):
@@ -142,7 +142,7 @@ class NumericMixin(object):
         self._validate_number()
         self._validate_real()
         if math.isnan(self.val):
-            self.error('Expected not <NaN>, but was.')
+            return self.error('Expected not <NaN>, but was.')
         return self
 
     def is_inf(self):
@@ -163,7 +163,7 @@ class NumericMixin(object):
         self._validate_number()
         self._validate_real()
         if not math.isinf(self.val):
-            self.error('Expected <%s> to be <Inf>, but was not.' % self.val)
+            return self.error('Expected <%s> to be <Inf>, but was not.' % self.val)
         return self
 
     def is_not_inf(self):
@@ -185,7 +185,7 @@ class NumericMixin(object):
         self._validate_number()
         self._validate_real()
         if math.isinf(self.val):
-            self.error('Expected not <Inf>, but was.')
+            return self.error('Expected not <Inf>, but was.')
         return self
 
     def is_greater_than(self, other):
@@ -218,10 +218,10 @@ class NumericMixin(object):
         self._validate_compareable(other)
         if self.val <= other:
             if type(self.val) is datetime.datetime:
-                self.error('Expected <%s> to be greater than <%s>, but was not.' % (
+                return self.error('Expected <%s> to be greater than <%s>, but was not.' % (
                     self.val.strftime('%Y-%m-%d %H:%M:%S'), other.strftime('%Y-%m-%d %H:%M:%S')))
             else:
-                self.error('Expected <%s> to be greater than <%s>, but was not.' % (self.val, other))
+                return self.error('Expected <%s> to be greater than <%s>, but was not.' % (self.val, other))
         return self
 
     def is_greater_than_or_equal_to(self, other):
@@ -256,10 +256,10 @@ class NumericMixin(object):
         self._validate_compareable(other)
         if self.val < other:
             if type(self.val) is datetime.datetime:
-                self.error('Expected <%s> to be greater than or equal to <%s>, but was not.' % (
+                return self.error('Expected <%s> to be greater than or equal to <%s>, but was not.' % (
                     self.val.strftime('%Y-%m-%d %H:%M:%S'), other.strftime('%Y-%m-%d %H:%M:%S')))
             else:
-                self.error('Expected <%s> to be greater than or equal to <%s>, but was not.' % (self.val, other))
+                return self.error('Expected <%s> to be greater than or equal to <%s>, but was not.' % (self.val, other))
         return self
 
     def is_less_than(self, other):
@@ -292,9 +292,9 @@ class NumericMixin(object):
         self._validate_compareable(other)
         if self.val >= other:
             if type(self.val) is datetime.datetime:
-                self.error('Expected <%s> to be less than <%s>, but was not.' % (self.val.strftime('%Y-%m-%d %H:%M:%S'), other.strftime('%Y-%m-%d %H:%M:%S')))
+                return self.error('Expected <%s> to be less than <%s>, but was not.' % (self.val.strftime('%Y-%m-%d %H:%M:%S'), other.strftime('%Y-%m-%d %H:%M:%S')))
             else:
-                self.error('Expected <%s> to be less than <%s>, but was not.' % (self.val, other))
+                return self.error('Expected <%s> to be less than <%s>, but was not.' % (self.val, other))
         return self
 
     def is_less_than_or_equal_to(self, other):
@@ -329,10 +329,10 @@ class NumericMixin(object):
         self._validate_compareable(other)
         if self.val > other:
             if type(self.val) is datetime.datetime:
-                self.error('Expected <%s> to be less than or equal to <%s>, but was not.' % (
+                return self.error('Expected <%s> to be less than or equal to <%s>, but was not.' % (
                     self.val.strftime('%Y-%m-%d %H:%M:%S'), other.strftime('%Y-%m-%d %H:%M:%S')))
             else:
-                self.error('Expected <%s> to be less than or equal to <%s>, but was not.' % (self.val, other))
+                return self.error('Expected <%s> to be less than or equal to <%s>, but was not.' % (self.val, other))
         return self
 
     def is_positive(self):
@@ -403,10 +403,10 @@ class NumericMixin(object):
 
         if self.val < low or self.val > high:
             if val_type is datetime.datetime:
-                self.error('Expected <%s> to be between <%s> and <%s>, but was not.' % (
+                return self.error('Expected <%s> to be between <%s> and <%s>, but was not.' % (
                     self.val.strftime('%Y-%m-%d %H:%M:%S'), low.strftime('%Y-%m-%d %H:%M:%S'), high.strftime('%Y-%m-%d %H:%M:%S')))
             else:
-                self.error('Expected <%s> to be between <%s> and <%s>, but was not.' % (self.val, low, high))
+                return self.error('Expected <%s> to be between <%s> and <%s>, but was not.' % (self.val, low, high))
         return self
 
     def is_not_between(self, low, high):
@@ -433,10 +433,10 @@ class NumericMixin(object):
 
         if self.val >= low and self.val <= high:
             if val_type is datetime.datetime:
-                self.error('Expected <%s> to not be between <%s> and <%s>, but was.' % (
+                return self.error('Expected <%s> to not be between <%s> and <%s>, but was.' % (
                     self.val.strftime('%Y-%m-%d %H:%M:%S'), low.strftime('%Y-%m-%d %H:%M:%S'), high.strftime('%Y-%m-%d %H:%M:%S')))
             else:
-                self.error('Expected <%s> to not be between <%s> and <%s>, but was.' % (self.val, low, high))
+                return self.error('Expected <%s> to not be between <%s> and <%s>, but was.' % (self.val, low, high))
         return self
 
     def is_close_to(self, other, tolerance):
@@ -474,10 +474,10 @@ class NumericMixin(object):
                 tolerance_seconds = tolerance.days * 86400 + tolerance.seconds + tolerance.microseconds / 1000000
                 h, rem = divmod(tolerance_seconds, 3600)
                 m, s = divmod(rem, 60)
-                self.error('Expected <%s> to be close to <%s> within tolerance <%d:%02d:%02d>, but was not.' % (
+                return self.error('Expected <%s> to be close to <%s> within tolerance <%d:%02d:%02d>, but was not.' % (
                     self.val.strftime('%Y-%m-%d %H:%M:%S'), other.strftime('%Y-%m-%d %H:%M:%S'), h, m, s))
             else:
-                self.error('Expected <%s> to be close to <%s> within tolerance <%s>, but was not.' % (self.val, other, tolerance))
+                return self.error('Expected <%s> to be close to <%s> within tolerance <%s>, but was not.' % (self.val, other, tolerance))
         return self
 
     def is_not_close_to(self, other, tolerance):
@@ -506,8 +506,8 @@ class NumericMixin(object):
                 tolerance_seconds = tolerance.days * 86400 + tolerance.seconds + tolerance.microseconds / 1000000
                 h, rem = divmod(tolerance_seconds, 3600)
                 m, s = divmod(rem, 60)
-                self.error('Expected <%s> to not be close to <%s> within tolerance <%d:%02d:%02d>, but was.' % (
+                return self.error('Expected <%s> to not be close to <%s> within tolerance <%d:%02d:%02d>, but was.' % (
                     self.val.strftime('%Y-%m-%d %H:%M:%S'), other.strftime('%Y-%m-%d %H:%M:%S'), h, m, s))
             else:
-                self.error('Expected <%s> to not be close to <%s> within tolerance <%s>, but was.' % (self.val, other, tolerance))
+                return self.error('Expected <%s> to not be close to <%s> within tolerance <%s>, but was.' % (self.val, other, tolerance))
         return self
diff --git a/assertpy/string.py b/assertpy/string.py
index 49b505d..be9c866 100644
--- a/assertpy/string.py
+++ b/assertpy/string.py
@@ -71,7 +71,7 @@ class StringMixin(object):
         if not isinstance(other, str_types):
             raise TypeError('given arg must be a string')
         if self.val.lower() != other.lower():
-            self.error('Expected <%s> to be case-insensitive equal to <%s>, but was not.' % (self.val, other))
+            return self.error('Expected <%s> to be case-insensitive equal to <%s>, but was not.' % (self.val, other))
         return self
 
     def contains_ignoring_case(self, *items):
@@ -103,7 +103,7 @@ class StringMixin(object):
                 if not isinstance(items[0], str_types):
                     raise TypeError('given arg must be a string')
                 if items[0].lower() not in self.val.lower():
-                    self.error('Expected <%s> to case-insensitive contain item <%s>, but did not.' % (self.val, items[0]))
+                    return self.error('Expected <%s> to case-insensitive contain item <%s>, but did not.' % (self.val, items[0]))
             else:
                 missing = []
                 for i in items:
@@ -112,7 +112,7 @@ class StringMixin(object):
                     if i.lower() not in self.val.lower():
                         missing.append(i)
                 if missing:
-                    self.error('Expected <%s> to case-insensitive contain items %s, but did not contain %s.' % (
+                    return self.error('Expected <%s> to case-insensitive contain items %s, but did not contain %s.' % (
                         self.val, self._fmt_items(items), self._fmt_items(missing)))
         elif isinstance(self.val, Iterable):
             missing = []
@@ -129,7 +129,7 @@ class StringMixin(object):
                 if not found:
                     missing.append(i)
             if missing:
-                self.error('Expected <%s> to case-insensitive contain items %s, but did not contain %s.' % (
+                return self.error('Expected <%s> to case-insensitive contain items %s, but did not contain %s.' % (
                     self.val, self._fmt_items(items), self._fmt_items(missing)))
         else:
             raise TypeError('val is not a string or iterable')
@@ -163,13 +163,13 @@ class StringMixin(object):
             if len(prefix) == 0:
                 raise ValueError('given prefix arg must not be empty')
             if not self.val.startswith(prefix):
-                self.error('Expected <%s> to start with <%s>, but did not.' % (self.val, prefix))
+                return self.error('Expected <%s> to start with <%s>, but did not.' % (self.val, prefix))
         elif isinstance(self.val, Iterable):
             if len(self.val) == 0:
                 raise ValueError('val must not be empty')
             first = next(iter(self.val))
             if first != prefix:
-                self.error('Expected %s to start with <%s>, but did not.' % (self.val, prefix))
+                return self.error('Expected %s to start with <%s>, but did not.' % (self.val, prefix))
         else:
             raise TypeError('val is not a string or iterable')
         return self
@@ -202,7 +202,7 @@ class StringMixin(object):
             if len(suffix) == 0:
                 raise ValueError('given suffix arg must not be empty')
             if not self.val.endswith(suffix):
-                self.error('Expected <%s> to end with <%s>, but did not.' % (self.val, suffix))
+                return self.error('Expected <%s> to end with <%s>, but did not.' % (self.val, suffix))
         elif isinstance(self.val, Iterable):
             if len(self.val) == 0:
                 raise ValueError('val must not be empty')
@@ -210,7 +210,7 @@ class StringMixin(object):
             for last in self.val:
                 pass
             if last != suffix:
-                self.error('Expected %s to end with <%s>, but did not.' % (self.val, suffix))
+                return self.error('Expected %s to end with <%s>, but did not.' % (self.val, suffix))
         else:
             raise TypeError('val is not a string or iterable')
         return self
@@ -271,7 +271,7 @@ class StringMixin(object):
         if len(pattern) == 0:
             raise ValueError('given pattern arg must not be empty')
         if re.search(pattern, self.val) is None:
-            self.error('Expected <%s> to match pattern <%s>, but did not.' % (self.val, pattern))
+            return self.error('Expected <%s> to match pattern <%s>, but did not.' % (self.val, pattern))
         return self
 
     def does_not_match(self, pattern):
@@ -302,7 +302,7 @@ class StringMixin(object):
         if len(pattern) == 0:
             raise ValueError('given pattern arg must not be empty')
         if re.search(pattern, self.val) is not None:
-            self.error('Expected <%s> to not match pattern <%s>, but did.' % (self.val, pattern))
+            return self.error('Expected <%s> to not match pattern <%s>, but did.' % (self.val, pattern))
         return self
 
     def is_alpha(self):
@@ -324,7 +324,7 @@ class StringMixin(object):
         if len(self.val) == 0:
             raise ValueError('val is empty')
         if not self.val.isalpha():
-            self.error('Expected <%s> to contain only alphabetic chars, but did not.' % self.val)
+            return self.error('Expected <%s> to contain only alphabetic chars, but did not.' % self.val)
         return self
 
     def is_digit(self):
@@ -346,7 +346,7 @@ class StringMixin(object):
         if len(self.val) == 0:
             raise ValueError('val is empty')
         if not self.val.isdigit():
-            self.error('Expected <%s> to contain only digits, but did not.' % self.val)
+            return self.error('Expected <%s> to contain only digits, but did not.' % self.val)
         return self
 
     def is_lower(self):
@@ -368,7 +368,7 @@ class StringMixin(object):
         if len(self.val) == 0:
             raise ValueError('val is empty')
         if self.val != self.val.lower():
-            self.error('Expected <%s> to contain only lowercase chars, but did not.' % self.val)
+            return self.error('Expected <%s> to contain only lowercase chars, but did not.' % self.val)
         return self
 
     def is_upper(self):
@@ -390,7 +390,7 @@ class StringMixin(object):
         if len(self.val) == 0:
             raise ValueError('val is empty')
         if self.val != self.val.upper():
-            self.error('Expected <%s> to contain only uppercase chars, but did not.' % self.val)
+            return self.error('Expected <%s> to contain only uppercase chars, but did not.' % self.val)
         return self
 
     def is_unicode(self):
@@ -409,5 +409,5 @@ class StringMixin(object):
             AssertionError: if val is **not** a unicode string
         """
         if type(self.val) is not unicode:
-            self.error('Expected <%s> to be unicode, but was <%s>.' % (self.val, type(self.val).__name__))
+            return self.error('Expected <%s> to be unicode, but was <%s>.' % (self.val, type(self.val).__name__))
         return self
diff --git a/setup.cfg b/setup.cfg
index 4293e27..d90ec63 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,8 +1,5 @@
 [metadata]
 description-file = README.md
 
-[tool:pytest]
-pep8maxlinelength = 160
-
 [pep8]
 max-line-length = 160
\ No newline at end of file


Test patch:
diff --git a/tests/test_extensions.py b/tests/test_extensions.py
index d884085..fb1430d 100644
--- a/tests/test_extensions.py
+++ b/tests/test_extensions.py
@@ -34,7 +34,7 @@ def is_even(self):
     if isinstance(self.val, numbers.Integral) is False:
         raise TypeError('val must be an integer')
     if self.val % 2 != 0:
-        self.error('Expected <%s> to be even, but was not.' % (self.val))
+        return self.error('Expected <%s> to be even, but was not.' % (self.val))
     return self
 
 
@@ -47,7 +47,7 @@ def is_multiple_of(self, other):
 
     _, rem = divmod(self.val, other)
     if rem > 0:
-        self.error('Expected <%s> to be multiple of <%s>, but was not.' % (self.val, other))
+        return self.error('Expected <%s> to be multiple of <%s>, but was not.' % (self.val, other))
 
     return self
 
@@ -61,7 +61,7 @@ def is_factor_of(self, other):
 
     _, rem = divmod(other, self.val)
     if rem > 0:
-        self.error('Expected <%s> to be factor of <%s>, but was not.' % (self.val, other))
+        return self.error('Expected <%s> to be factor of <%s>, but was not.' % (self.val, other))
 
     return self
 
@@ -195,7 +195,7 @@ def test_remove_bad_extension():
 
 def is_foo(self):
     if self.val != 'foo':
-        self.error('Expected <%s> to be foo, but was not.' % (self.val))
+        return self.error('Expected <%s> to be foo, but was not.' % (self.val))
     return self
 
 
@@ -212,7 +212,7 @@ def dupe1():
 def dupe2():
     def is_foo(self):
         if self.val != 'FOO':
-            self.error('Expected <%s> to be FOO, but was not.' % (self.val))
+            return self.error('Expected <%s> to be FOO, but was not.' % (self.val))
         return self
 
     add_extension(is_foo)
diff --git a/tests/test_warn.py b/tests/test_warn.py
index 68618cd..76906d7 100644
--- a/tests/test_warn.py
+++ b/tests/test_warn.py
@@ -84,9 +84,8 @@ def test_failures():
     assert_that(out).contains('[test_warn.py:69]: Expected <foo> to be not equal to <foo>, but was.')
     assert_that(out).contains('[test_warn.py:70]: Expected <foo> to be case-insensitive equal to <BAR>, but was not.')
 
-def test_failures_with_renamed_import():
-    from assertpy import assert_warn as warn
 
+def test_chained_failure():
     # capture log
     capture2 = StringIO()
     logger = logging.getLogger('capture2')
@@ -94,6 +93,27 @@ def test_failures_with_renamed_import():
     logger.addHandler(handler)
     adapted = WarningLoggingAdapter(logger, None)
 
+    assert_warn('foo', logger=adapted).is_length(4).is_in('bar').does_not_contain_duplicates()
+
+    # dump log to string
+    out = capture2.getvalue()
+    capture2.close()
+
+    assert_that(out).contains('[test_warn.py:96]: Expected <foo> to be of length <4>, but was <3>.')
+    assert_that(out).contains('[test_warn.py:96]: Expected <foo> to be in <bar>, but was not.')
+    assert_that(out).contains('[test_warn.py:96]: Expected <foo> to not contain duplicates, but did.')
+
+
+def test_failures_with_renamed_import():
+    from assertpy import assert_warn as warn
+
+    # capture log
+    capture3 = StringIO()
+    logger = logging.getLogger('capture3')
+    handler = logging.StreamHandler(capture3)
+    logger.addHandler(handler)
+    adapted = WarningLoggingAdapter(logger, None)
+
     warn('foo', logger=adapted).is_length(4)
     warn('foo', logger=adapted).is_empty()
     warn('foo', logger=adapted).is_false()
@@ -106,16 +126,16 @@ def test_failures_with_renamed_import():
     warn('foo', logger=adapted).is_equal_to_ignoring_case('BAR')
 
     # dump log to string
-    out = capture2.getvalue()
-    capture2.close()
+    out = capture3.getvalue()
+    capture3.close()
 
-    assert_that(out).contains('[test_warn.py:97]: Expected <foo> to be of length <4>, but was <3>.')
-    assert_that(out).contains('[test_warn.py:98]: Expected <foo> to be empty string, but was not.')
-    assert_that(out).contains('[test_warn.py:99]: Expected <False>, but was not.')
-    assert_that(out).contains('[test_warn.py:100]: Expected <foo> to contain only digits, but did not.')
-    assert_that(out).contains('[test_warn.py:101]: Expected <123> to contain only alphabetic chars, but did not.')
-    assert_that(out).contains('[test_warn.py:102]: Expected <foo> to contain only uppercase chars, but did not.')
-    assert_that(out).contains('[test_warn.py:103]: Expected <FOO> to contain only lowercase chars, but did not.')
-    assert_that(out).contains('[test_warn.py:104]: Expected <foo> to be equal to <bar>, but was not.')
-    assert_that(out).contains('[test_warn.py:105]: Expected <foo> to be not equal to <foo>, but was.')
-    assert_that(out).contains('[test_warn.py:106]: Expected <foo> to be case-insensitive equal to <BAR>, but was not.')
+    assert_that(out).contains('[test_warn.py:117]: Expected <foo> to be of length <4>, but was <3>.')
+    assert_that(out).contains('[test_warn.py:118]: Expected <foo> to be empty string, but was not.')
+    assert_that(out).contains('[test_warn.py:119]: Expected <False>, but was not.')
+    assert_that(out).contains('[test_warn.py:120]: Expected <foo> to contain only digits, but did not.')
+    assert_that(out).contains('[test_warn.py:121]: Expected <123> to contain only alphabetic chars, but did not.')
+    assert_that(out).contains('[test_warn.py:122]: Expected <foo> to contain only uppercase chars, but did not.')
+    assert_that(out).contains('[test_warn.py:123]: Expected <FOO> to contain only lowercase chars, but did not.')
+    assert_that(out).contains('[test_warn.py:124]: Expected <foo> to be equal to <bar>, but was not.')
+    assert_that(out).contains('[test_warn.py:125]: Expected <foo> to be not equal to <foo>, but was.')
+    assert_that(out).contains('[test_warn.py:126]: Expected <foo> to be case-insensitive equal to <BAR>, but was not.')