diff --git a/.gitignore b/.gitignore
index fe51c01..a4ceecc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
*.py[cod]
.cache
.pytest_cache
+__pycache__
# testing output
*.xml
@@ -22,3 +23,7 @@ dist
# IDE
/.idea
.vscode
+
+# docs output
+docs/build
+docs/out
diff --git a/.travis.yml b/.travis.yml
index d54bbef..127e82b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,6 +8,7 @@ python:
- 3.8
before_install:
- pip install pytest-cov
+ - pip install requests==2.21.0 # last version that still supports py 3.4
- pip install coveralls
install:
- pip install .
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index dd67a7b..05a808b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,10 +9,10 @@ Here's how:
1. Create a branch (`git checkout -b my_branch`)
1. Commit your changes (`git commit -am "added some cool feature"`)
1. Push your branch to your fork (`git push origin my_branch`)
-1. Open a [Pull Request](http://github.com/ActivisionGameScience/assertpy/pulls)
+1. Open a [Pull Request](http://github.com/assertpy/assertpy/pulls)
1. Respond to any questions during our review process
-Read more about how pulls work on GitHub's [Using Pull Requests](https://help.github.com/articles/using-pull-requests/) page.
+Read more about how pulls work on GitHub's [About pull requests](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) page.
## Running the Tests
@@ -31,11 +31,11 @@ Should produce output something like this:
```
===== test session starts =====
-collected 373 items
+collected 589 items
-tests/test_bool.py::TestBool::test_is_true PASSED
+tests/test_bool.py::test_is_true PASSED
..
-tests/test_type.py::TestType::test_is_instance_of_bad_arg_failure PASSED
+tests/test_warn.py::test_failures PASSED
-===== 373 passed in 0.52 seconds =====
+===== 589 passed in 1.91s =====
```
diff --git a/README.md b/README.md
index cfb8845..4cec359 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,8 @@
Simple assertions library for unit testing in Python with a nice fluent API. Supports both Python 2 and 3.
-[](https://travis-ci.org/ActivisionGameScience/assertpy)
-[](https://coveralls.io/github/ActivisionGameScience/assertpy)
+[](https://travis-ci.org/assertpy/assertpy)
+[](https://coveralls.io/github/assertpy/assertpy?branch=main)
## Usage
@@ -19,15 +19,15 @@ def test_something():
assert_that(['a', 'b', 'c']).contains('a').does_not_contain('x')
```
-Of course, `assertpy` works best with a python test runner like [pytest](http://pytest.org/latest/contents.html) (our favorite) or [Nose](http://nose.readthedocs.org/).
+Of course, `assertpy` works best with a python test runner like [pytest](http://pytest.org/) (our favorite) or [Nose](http://nose.readthedocs.org/).
## Installation
### Install via pip
-[](https://pypi.python.org/pypi/assertpy)
+[](https://pypi.org/project/assertpy/)
-The `assertpy` library is available via [PyPI](https://pypi.python.org/pypi/assertpy).
+The `assertpy` library is available via [PyPI](https://pypi.org/project/assertpy/).
Just install with:
```
@@ -46,12 +46,16 @@ conda install assertpy --channel conda-forge
```
-## The API
+## Docs
The fluent API of `assertpy` is designed to create compact, yet readable tests.
The API has been modeled after other fluent testing APIs, especially the awesome
-[AssertJ](http://joel-costigliola.github.io/assertj/) assertion library for Java. Of course, in the `assertpy` library everything is fully pythonic and designed to take full advantage of the dynamism in the Python runtime.
+[AssertJ](https://assertj.github.io/doc/) assertion library for Java. Of course, in the `assertpy` library everything is fully pythonic and designed to take full advantage of the dynamism in the Python runtime.
+All assertions, with usage examples, are documented here:
+https://assertpy.github.io/docs.html
+
+And there are hundreds of examples below. Read on...
### Strings
@@ -76,8 +80,8 @@ assert_that('foo').is_equal_to('foo')
assert_that('foo').is_not_equal_to('bar')
assert_that('foo').is_equal_to_ignoring_case('FOO')
-assert_that(u'foo').is_unicode() # on python 2
-assert_that('foo').is_unicode() # on python 3
+assert_that(u'foo').is_unicode() # on python 2
+assert_that('foo').is_unicode() # on python 3
assert_that('foo').contains('f')
assert_that('foo').contains('f','oo')
@@ -210,6 +214,8 @@ assert_that(['a','b']).contains_only('a','b')
assert_that(['a','a']).contains_only('a')
assert_that(['a','b','c']).contains_sequence('b','c')
assert_that(['a','b']).is_subset_of(['a','b','c'])
+assert_that(['a','b','c']).is_sorted()
+assert_that(['c','b','a']).is_sorted(reverse=True)
assert_that(['a','x','x']).contains_duplicates()
assert_that(['a','b','c']).does_not_contain_duplicates()
@@ -252,6 +258,8 @@ assert_that((1,2,3)).contains_only(1,2,3)
assert_that((1,1,1)).contains_only(1)
assert_that((1,2,3)).contains_sequence(2,3)
assert_that((1,2,3)).is_subset_of((1,2,3,4))
+assert_that((1,2,3)).is_sorted()
+assert_that((3,2,1)).is_sorted(reverse=True)
assert_that((1,2,2)).contains_duplicates()
assert_that((1,2,3)).does_not_contain_duplicates()
@@ -863,7 +871,7 @@ forgiving behavior, you can use `soft_fail()` which is collected like any other
### Snapshot Testing
-Take a snapshot of a python data structure, store it on disk in JSON format, and automatically compare the latest data to the stored data on every test run. The snapshot testing features of `assertpy` are borrowed from [Jest](https://facebook.github.io/jest/), a well-kwown and powerful Javascript testing framework. Snapshots require Python 3.
+Take a snapshot of a python data structure, store it on disk in JSON format, and automatically compare the latest data to the stored data on every test run. The snapshot testing features of `assertpy` are borrowed from [Jest](https://facebook.github.io/jest/), a well-known and powerful Javascript testing framework. Snapshots require Python 3.
For example, snapshot the following dict:
@@ -933,7 +941,7 @@ from assertpy import add_extension
def is_5(self):
if self.val != 5:
- self._err(f'{self.val} is NOT 5!')
+ return self.error(f'{self.val} is NOT 5!')
return self
add_extension(is_5)
@@ -943,7 +951,7 @@ Once registered with `assertpy`, we can use our new assertion as expected:
```py
assert_that(5).is_5()
-assert_that(6).is_5() # fails!
+assert_that(6).is_5() # fails!
```
Of course, `is_5()` is only available in the test file where `add_extension()` is called. If you want better control of scope of your custom extensions, such as writing extensions once and using them in any test file, you'll need to use the test setup functionality of your test runner. With [pytest](http://pytest.org/latest/contents.html), you can just use a `conftest.py` file and a _fixture_.
@@ -956,7 +964,7 @@ from assertpy import add_extension
def is_5(self):
if self.val != 5:
- self._err(f'{self.val} is NOT 5!')
+ return self.error(f'{self.val} is NOT 5!')
return self
@pytest.fixture(scope='module')
@@ -971,7 +979,7 @@ from assertpy import assert_that
def test_foo(my_extensions):
assert_that(5).is_5()
- assert_that(6).is_5() # fails!
+ assert_that(6).is_5() # fails!
```
where the `my_extensions` parameter must be the name of your fixture function in `conftest.py`. See the [fixture docs](https://docs.pytest.org/en/latest/fixture.html) for details.
@@ -982,10 +990,10 @@ Here are some useful tips to help you write your own custom assertions:
1. Use `self` as first param (as if your function was an instance method).
2. Use `self.val` to get the _actual_ value to be tested.
-2. It's better to test the negative, and then fail if true.
-3. Fail by raising an `AssertionError`.
-4. Always use the `self._err()` helper to fail (and print your failure message).
-5. Always `return self` to allow for chaining.
+3. It's better to test the negative, and then fail if true.
+4. Fail by raising an `AssertionError` (the `self.error()` helper does this for you).
+5. Always use the `self.error()` helper to fail (and print your failure message).
+6. Always `return self` to allow for chaining.
Putting it all together, here is another custom assertion example, but annotated with comments:
@@ -1000,13 +1008,13 @@ def is_multiple_of(self, other):
if isinstance(other, numbers.Integral) is False or other <= 0:
raise TypeError('given arg must be a positive integer')
- # divide and compute remainder using divmod() built-in
+ # calc remainder using divmod() built-in
_, rem = divmod(self.val, other)
# test the negative (is remainder non-zero?)
if rem > 0:
# non-zero remainder, so not multiple -> we fail!
- self._err('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
@@ -1021,24 +1029,18 @@ Here are just a few examples:
```py
assert_that('foo').is_length(3).starts_with('f').ends_with('oo')
-```
-```py
assert_that([1,2,3]).is_type_of(list).contains(1,2).does_not_contain(4,5)
-```
-```py
assert_that(fred).has_first_name('Fred').has_last_name('Smith').has_shoe_size(12)
-```
-```py
assert_that(people).is_length(2).extracting('first_name').contains('Fred','Joe')
```
## Future
-There are always a few new features in the works...if you'd like to help, check out the [open issues](https://github.com/ActivisionGameScience/assertpy/issues?q=is%3Aopen+is%3Aissue) and see our [Contributing](CONTRIBUTING.md) doc.
+There are always a few new features in the works...if you'd like to help, check out the [open issues](https://github.com/assertpy/assertpy/issues) and see our [Contributing](CONTRIBUTING.md) doc.
## License
diff --git a/assertpy/assertpy.py b/assertpy/assertpy.py
index 13bfe42..39abb20 100644
--- a/assertpy/assertpy.py
+++ b/assertpy/assertpy.py
@@ -36,31 +36,84 @@
import sys
import types
from .base import BaseMixin
-from .contains import ContainsMixin
-from .numeric import NumericMixin
-from .string import StringMixin
from .collection import CollectionMixin
-from .dict import DictMixin
+from .contains import ContainsMixin
from .date import DateMixin
-from .file import FileMixin
+from .dict import DictMixin
+from .dynamic import DynamicMixin
from .extracting import ExtractingMixin
-from .snapshot import SnapshotMixin
from .exception import ExceptionMixin
-from .dynamic import DynamicMixin
+from .file import FileMixin
from .helpers import HelpersMixin
+from .numeric import NumericMixin
+from .snapshot import SnapshotMixin
+from .string import StringMixin
-__version__ = '0.15'
+__version__ = '1.1'
-__tracebackhide__ = True # clean tracebacks via py.test integration
-contextlib.__tracebackhide__ = True # monkey patch contextlib with clean py.test tracebacks
+__tracebackhide__ = True # clean tracebacks via py.test integration
+contextlib.__tracebackhide__ = True # monkey patch contextlib with clean py.test tracebacks
+# assertpy files
+ASSERTPY_FILES = [os.path.join('assertpy', file) for file in [
+ 'assertpy.py',
+ 'base.py',
+ 'collection.py',
+ 'contains.py',
+ 'date.py',
+ 'dict.py',
+ 'dynamic.py',
+ 'exception.py',
+ 'extracting.py',
+ 'file.py',
+ 'helpers.py',
+ 'numeric.py',
+ 'snapshot.py',
+ 'string.py'
+]]
# soft assertions
_soft_ctx = 0
_soft_err = []
+
@contextlib.contextmanager
def soft_assertions():
+ """Create a soft assertion context.
+
+ Normally, any assertion failure will halt test execution immediately by raising an error.
+ Soft assertions are way to collect assertion failures (and failure messages) together, to be
+ raised all at once at the end, without halting your test.
+
+ Examples:
+ Create a soft assertion context, and some failing tests::
+
+ from assertpy import assert_that, soft_assertions
+
+ with soft_assertions():
+ assert_that('foo').is_length(4)
+ assert_that('foo').is_empty()
+ assert_that('foo').is_false()
+ assert_that('foo').is_digit()
+ assert_that('123').is_alpha()
+
+ When the context ends, any assertion failures are collected together and a single
+ ``AssertionError`` is raised::
+
+ AssertionError: soft assertion failures:
+ 1. Expected to be of length <4>, but was <3>.
+ 2. Expected to be empty string, but was not.
+ 3. Expected , but was not.
+ 4. Expected to contain only digits, but did not.
+ 5. Expected <123> to contain only alphabetic chars, but did not.
+
+ Note:
+ The soft assertion context only collects *assertion* failures, other errors such as
+ ``TypeError`` or ``ValueError`` are always raised immediately. Triggering an explicit test
+ failure with :meth:`fail` will similarly halt execution immediately. If you need more
+ forgiving behavior, use :meth:`soft_fail` to add a failure message without halting test
+ execution.
+ """
global _soft_ctx
global _soft_err
@@ -77,32 +130,142 @@ def soft_assertions():
if _soft_err and _soft_ctx == 0:
out = 'soft assertion failures:'
- for i,msg in enumerate(_soft_err):
+ for i, msg in enumerate(_soft_err):
out += '\n%d. %s' % (i+1, msg)
# reset msg, then raise
_soft_err = []
raise AssertionError(out)
+
# factory methods
def assert_that(val, description=''):
- """Factory method for the assertion builder with value to be tested and optional description."""
+ """Set the value to be tested, plus an optional description, and allow assertions to be called.
+
+ This is a factory method for the :class:`AssertionBuilder`, and the single most important
+ method in all of assertpy.
+
+ Args:
+ val: the value to be tested (aka the actual value)
+ description (str, optional): the extra error message description. Defaults to ``''``
+ (aka empty string)
+
+ Examples:
+ Just import it once at the top of your test file, and away you go...::
+
+ from assertpy import assert_that
+
+ def test_something():
+ assert_that(1 + 2).is_equal_to(3)
+ assert_that('foobar').is_length(6).starts_with('foo').ends_with('bar')
+ assert_that(['a', 'b', 'c']).contains('a').does_not_contain('x')
+ """
global _soft_ctx
if _soft_ctx:
- return builder(val, description, 'soft')
- return builder(val, description)
+ return _builder(val, description, 'soft')
+ return _builder(val, description)
+
def assert_warn(val, description='', logger=None):
- """Factory method for the assertion builder with value to be tested, optional description, and
- just warn on assertion failures instead of raisings exceptions."""
- return builder(val, description, 'warn', logger=logger)
+ """Set the value to be tested, and optional description and logger, and allow assertions to be
+ called, but never fail, only log warnings.
+
+ This is a factory method for the :class:`AssertionBuilder`, but unlike :meth:`assert_that` an
+ `AssertionError` is never raised, and execution is never halted. Instead, any assertion failures
+ results in a warning message being logged. Uses the given logger, or defaults to a simple logger
+ that prints warnings to ``stdout``.
+
+
+ Args:
+ val: the value to be tested (aka the actual value)
+ description (str, optional): the extra error message description. Defaults to ``''``
+ (aka empty string)
+ logger (Logger, optional): the logger for warning message on assertion failure. Defaults to ``None``
+ (aka use the default simple logger that prints warnings to ``stdout``)
+
+ Examples:
+ Usage::
+
+ from assertpy import assert_warn
+
+ assert_warn('foo').is_length(4)
+ assert_warn('foo').is_empty()
+ assert_warn('foo').is_false()
+ assert_warn('foo').is_digit()
+ assert_warn('123').is_alpha()
+
+ Even though all of the above assertions fail, ``AssertionError`` is never raised and
+ test execution is never halted. Instead, the failed assertions merely log the following
+ warning messages to ``stdout``::
+
+ 2019-10-27 20:00:35 WARNING [test_foo.py:23]: Expected to be of length <4>, but was <3>.
+ 2019-10-27 20:00:35 WARNING [test_foo.py:24]: Expected to be empty string, but was not.
+ 2019-10-27 20:00:35 WARNING [test_foo.py:25]: Expected , but was not.
+ 2019-10-27 20:00:35 WARNING [test_foo.py:26]: Expected to contain only digits, but did not.
+ 2019-10-27 20:00:35 WARNING [test_foo.py:27]: Expected <123> to contain only alphabetic chars, but did not.
+
+ Tip:
+ Use :meth:`assert_warn` if and only if you have a *really* good reason to log assertion
+ failures instead of failing.
+ """
+ return _builder(val, description, 'warn', logger=logger)
+
def fail(msg=''):
- """Force test failure with the given message."""
+ """Force immediate test failure with the given message.
+
+ Args:
+ msg (str, optional): the failure message. Defaults to ``''``
+
+ Examples:
+ Fail a test::
+
+ from assertpy import assert_that, fail
+
+ def test_fail():
+ fail('forced fail!')
+
+ If you wanted to test for a known failure, here is a useful pattern::
+
+ import operator
+
+ def test_adder_bad_arg():
+ try:
+ operator.add(1, 'bad arg')
+ fail('should have raised error')
+ except TypeError as e:
+ assert_that(str(e)).contains('unsupported operand')
+ """
raise AssertionError('Fail: %s!' % msg if msg else 'Fail!')
+
def soft_fail(msg=''):
- """Adds error message to soft errors list if within soft assertions context.
- Either just force test failure with the given message."""
+ """Within a :meth:`soft_assertions` context, append the failure message to the soft error list,
+ but do not halt test execution.
+
+ Otherwise, outside the context, acts identical to :meth:`fail` and forces immediate test
+ failure with the given message.
+
+ Args:
+ msg (str, optional): the failure message. Defaults to ``''``
+
+ Examples:
+ Failing soft assertions::
+
+ from assertpy import assert_that, soft_assertions, soft_fail
+
+ with soft_assertions():
+ assert_that(1).is_equal_to(2)
+ soft_fail('my message')
+ assert_that('foo').is_equal_to('bar')
+
+ Fails, and outputs the following soft error list::
+
+ AssertionError: soft assertion failures:
+ 1. Expected <1> to be equal to <2>, but was not.
+ 2. Fail: my message!
+ 3. Expected to be equal to , but was not.
+
+ """
global _soft_ctx
if _soft_ctx:
global _soft_err
@@ -110,41 +273,97 @@ def soft_fail(msg=''):
return
fail(msg)
+
# assertion extensions
_extensions = {}
+
+
def add_extension(func):
+ """Add a new user-defined custom assertion to assertpy.
+
+ Once the assertion is registered with assertpy, use it like any other assertion. Pass val to
+ :meth:`assert_that`, and then call it.
+
+ Args:
+ func: the assertion function (to be added)
+
+ Examples:
+ Usage::
+
+ from assertpy import add_extension
+
+ def is_5(self):
+ if self.val != 5:
+ return self.error(f'{self.val} is NOT 5!')
+ return self
+
+ add_extension(is_5)
+
+ def test_5():
+ assert_that(5).is_5()
+
+ def test_6():
+ assert_that(6).is_5() # fails
+ # 6 is NOT 5!
+ """
if not callable(func):
raise TypeError('func must be callable')
_extensions[func.__name__] = func
+
def remove_extension(func):
+ """Remove a user-defined custom assertion.
+
+ Args:
+ func: the assertion function (to be removed)
+
+ Examples:
+ Usage::
+
+ from assertpy import remove_extension
+
+ remove_extension(is_5)
+ """
if not callable(func):
raise TypeError('func must be callable')
if func.__name__ in _extensions:
del _extensions[func.__name__]
-def builder(val, description='', kind=None, expected=None, logger=None):
+
+def _builder(val, description='', kind=None, expected=None, logger=None):
+ """Internal helper to build a new :class:`AssertionBuilder` instance and glue on any extension methods."""
ab = AssertionBuilder(val, description, kind, expected, logger)
if _extensions:
# glue extension method onto new builder instance
- for name,func in _extensions.items():
+ for name, func in _extensions.items():
meth = types.MethodType(func, ab)
setattr(ab, name, meth)
return ab
+
# warnings
class WarningLoggingAdapter(logging.LoggerAdapter):
"""Logging adapter to unwind the stack to get the correct callee filename and line number."""
+
def process(self, msg, kwargs):
- def _unwind(frame, fn='assert_warn'):
- if frame and fn in frame.f_code.co_names:
- return frame
- return _unwind(frame.f_back, fn)
+ def _unwind(frame):
+ # walk all the frames
+ frames = []
+ while frame:
+ frames.append((frame.f_code.co_filename, frame.f_lineno))
+ frame = frame.f_back
+
+ # in reverse, find the first assertpy frame (and return the previous one)
+ prev = None
+ for frame in reversed(frames):
+ for f in ASSERTPY_FILES:
+ if frame[0].endswith(f):
+ return prev
+ prev = frame
+
+ filename, lineno = _unwind(inspect.currentframe())
+ return '[%s:%d]: %s' % (os.path.basename(filename), lineno, msg), kwargs
- frame = _unwind(inspect.currentframe())
- lineno = frame.f_lineno
- filename = os.path.basename(frame.f_code.co_filename)
- return '[%s:%d]: %s' % (filename, lineno, msg), kwargs
_logger = logging.getLogger('assertpy')
_handler = logging.StreamHandler(sys.stdout)
@@ -155,25 +374,82 @@ def _unwind(frame, fn='assert_warn'):
_default_logger = WarningLoggingAdapter(_logger, None)
-class AssertionBuilder(DynamicMixin, ExceptionMixin, SnapshotMixin, ExtractingMixin,
- FileMixin, DateMixin, DictMixin, CollectionMixin, StringMixin, NumericMixin,
- ContainsMixin, HelpersMixin, BaseMixin, object):
- """Assertion builder."""
+class AssertionBuilder(
+ StringMixin,
+ SnapshotMixin,
+ NumericMixin,
+ HelpersMixin,
+ FileMixin,
+ ExtractingMixin,
+ ExceptionMixin,
+ DynamicMixin,
+ DictMixin,
+ DateMixin,
+ ContainsMixin,
+ CollectionMixin,
+ BaseMixin,
+ object
+):
+ """The main assertion class. Never call the constructor directly, always use the
+ :meth:`assert_that` helper instead. Or if you just want warning messages, use the
+ :meth:`assert_warn` helper.
+
+ Args:
+ val: the value to be tested (aka the actual value)
+ description (str, optional): the extra error message description. Defaults to ``''``
+ (aka empty string)
+ kind (str, optional): the kind of assertions, one of ``None``, ``soft``, or ``warn``.
+ Defaults to ``None``
+ expected (Error, optional): the expected exception. Defaults to ``None``
+ logger (Logger, optional): the logger for warning messages. Defaults to ``None``
+ """
def __init__(self, val, description='', kind=None, expected=None, logger=None):
- """Construct the assertion builder."""
+ """Never call this constructor directly."""
self.val = val
self.description = description
self.kind = kind
self.expected = expected
self.logger = logger if logger else _default_logger
- def _builder(self, val, description='', kind=None, expected=None, logger=None):
- """Helper to build a new Builder. Only used when we don't want to chain."""
- return builder(val, description, kind, expected, logger)
+ def builder(self, val, description='', kind=None, expected=None, logger=None):
+ """Helper to build a new :class:`AssertionBuilder` instance. Use this only if not chaining to ``self``.
+
+ Args:
+ val: the value to be tested (aka the actual value)
+ description (str, optional): the extra error message description. Defaults to ``''``
+ (aka empty string)
+ kind (str, optional): the kind of assertions, one of ``None``, ``soft``, or ``warn``.
+ Defaults to ``None``
+ expected (Error, optional): the expected exception. Defaults to ``None``
+ logger (Logger, optional): the logger for warning messages. Defaults to ``None``
+ """
+ return _builder(val, description, kind, expected, logger)
+
+ def error(self, msg):
+ """Helper to raise an ``AssertionError`` with the given message.
+
+ If an error description is set by :meth:`~assertpy.base.BaseMixin.described_as`, then that
+ description is prepended to the error message.
+
+ Args:
+ msg: the error message
+
+ Examples:
+ Used to fail an assertion::
+
+ if 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).
- def _err(self, msg):
- """Helper to raise an AssertionError, and optionally prepend custom description."""
+ 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':
self.logger.warning(out)
diff --git a/assertpy/base.py b/assertpy/base.py
index 847a6cc..b572274 100644
--- a/assertpy/base.py
+++ b/assertpy/base.py
@@ -26,65 +26,296 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+__tracebackhide__ = True
+
+
class BaseMixin(object):
"""Base mixin."""
def described_as(self, description):
- """Describes the assertion. On failure, the description is included in the error message."""
+ """Describes the assertion. On failure, the description is included in the error message.
+
+ This is not an assertion itself. But if the any of the following chained assertions fail,
+ the description will be included in addition to the regular error message.
+
+ Args:
+ description: the error message description
+
+ Examples:
+ Usage::
+
+ assert_that(1).described_as('error msg desc').is_equal_to(2) # fails
+ # [error msg desc] Expected <1> to be equal to <2>, but was not.
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+ """
self.description = str(description)
return self
def is_equal_to(self, other, **kwargs):
- """Asserts that val is equal to other."""
+ """Asserts that val is equal to other.
+
+ Checks actual is equal to expected using the ``==`` operator. When val is *dict-like*,
+ optionally ignore or include keys when checking equality.
+
+ Args:
+ other: the expected value
+ **kwargs: see below
+
+ Keyword Args:
+ ignore: the dict key (or list of keys) to ignore
+ include: the dict key (of list of keys) to include
+
+ Examples:
+ Usage::
+
+ assert_that(1 + 2).is_equal_to(3)
+ assert_that('foo').is_equal_to('foo')
+ assert_that(123).is_equal_to(123)
+ assert_that(123.4).is_equal_to(123.4)
+ assert_that(['a', 'b']).is_equal_to(['a', 'b'])
+ assert_that((1, 2, 3)).is_equal_to((1, 2, 3))
+ assert_that({'a': 1, 'b': 2}).is_equal_to({'a': 1, 'b': 2})
+ assert_that({'a', 'b'}).is_equal_to({'a', 'b'})
+
+ When the val is *dict-like*, keys can optionally be *ignored* when checking equality::
+
+ # ignore a single key
+ assert_that({'a': 1, 'b': 2}).is_equal_to({'a': 1}, ignore='b')
+
+ # ignore multiple keys
+ assert_that({'a': 1, 'b': 2, 'c': 3}).is_equal_to({'a': 1}, ignore=['b', 'c'])
+
+ # ignore nested keys
+ assert_that({'a': {'b': 2, 'c': 3, 'd': 4}}).is_equal_to({'a': {'d': 4}}, ignore=[('a', 'b'), ('a', 'c')])
+
+ When the val is *dict-like*, only certain keys can be *included* when checking equality::
+
+ # include a single key
+ assert_that({'a': 1, 'b': 2}).is_equal_to({'a': 1}, include='a')
+
+ # include multiple keys
+ assert_that({'a': 1, 'b': 2, 'c': 3}).is_equal_to({'a': 1, 'b': 2}, include=['a', 'b'])
+
+ Failure produces a nice error message::
+
+ assert_that(1).is_equal_to(2) # fails
+ # Expected <1> to be equal to <2>, but was not.
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if actual is **not** equal to expected
+
+ Tip:
+ Using :meth:`is_equal_to` with a ``float`` val is just asking for trouble. Instead, you'll
+ always want to use *fuzzy* numeric assertions like :meth:`~assertpy.numeric.NumericMixin.is_close_to`
+ or :meth:`~assertpy.numeric.NumericMixin.is_between`.
+
+ See Also:
+ :meth:`~assertpy.string.StringMixin.is_equal_to_ignoring_case` - for case-insensitive string equality
+ """
if self._check_dict_like(self.val, check_values=False, return_as_bool=True) and \
self._check_dict_like(other, check_values=False, return_as_bool=True):
if self._dict_not_equal(self.val, other, ignore=kwargs.get('ignore'), include=kwargs.get('include')):
self._dict_err(self.val, other, ignore=kwargs.get('ignore'), include=kwargs.get('include'))
else:
if self.val != other:
- self._err('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):
- """Asserts that val is not equal to other."""
+ """Asserts that val is not equal to other.
+
+ Checks actual is not equal to expected using the ``!=`` operator.
+
+ Args:
+ other: the expected value
+
+ Examples:
+ Usage::
+
+ assert_that(1 + 2).is_not_equal_to(4)
+ assert_that('foo').is_not_equal_to('bar')
+ assert_that(123).is_not_equal_to(456)
+ assert_that(123.4).is_not_equal_to(567.8)
+ assert_that(['a', 'b']).is_not_equal_to(['c', 'd'])
+ assert_that((1, 2, 3)).is_not_equal_to((1, 2, 4))
+ assert_that({'a': 1, 'b': 2}).is_not_equal_to({'a': 1, 'b': 3})
+ assert_that({'a', 'b'}).is_not_equal_to({'a', 'x'})
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if actual **is** equal to expected
+ """
if self.val == other:
- self._err('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):
- """Asserts that the val is identical to other, via 'is' compare."""
+ """Asserts that val is identical to other.
+
+ Checks actual is identical to expected using the ``is`` operator.
+
+ Args:
+ other: the expected value
+
+ Examples:
+ Basic types are identical::
+
+ assert_that(1).is_same_as(1)
+ assert_that('foo').is_same_as('foo')
+ assert_that(123.4).is_same_as(123.4)
+
+ As are immutables like ``tuple``::
+
+ assert_that((1, 2, 3)).is_same_as((1, 2, 3))
+
+ But mutable collections like ``list``, ``dict``, and ``set`` are not::
+
+ # these all fail...
+ assert_that(['a', 'b']).is_same_as(['a', 'b']) # fails
+ assert_that({'a': 1, 'b': 2}).is_same_as({'a': 1, 'b': 2}) # fails
+ assert_that({'a', 'b'}).is_same_as({'a', 'b'}) # fails
+
+ Unless they are the same object::
+
+ x = {'a': 1, 'b': 2}
+ y = x
+ assert_that(x).is_same_as(y)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if actual is **not** identical to expected
+ """
if self.val is not other:
- self._err('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):
- """Asserts that the val is not identical to other, via 'is' compare."""
+ """Asserts that val is not identical to other.
+
+ Checks actual is not identical to expected using the ``is`` operator.
+
+ Args:
+ other: the expected value
+
+ Examples:
+ Usage::
+
+ assert_that(1).is_not_same_as(2)
+ assert_that('foo').is_not_same_as('bar')
+ assert_that(123.4).is_not_same_as(567.8)
+ assert_that((1, 2, 3)).is_not_same_as((1, 2, 4))
+
+ # mutable collections, even if equal, are not identical...
+ assert_that(['a', 'b']).is_not_same_as(['a', 'b'])
+ assert_that({'a': 1, 'b': 2}).is_not_same_as({'a': 1, 'b': 2})
+ assert_that({'a', 'b'}).is_not_same_as({'a', 'b'})
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if actual **is** identical to expected
+ """
if self.val is other:
- self._err('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):
- """Asserts that val is true."""
+ """Asserts that val is true.
+
+ Examples:
+ Usage::
+
+ assert_that(True).is_true()
+ assert_that(1).is_true()
+ assert_that('foo').is_true()
+ assert_that(1.0).is_true()
+ assert_that(['a', 'b']).is_true()
+ assert_that((1, 2, 3)).is_true()
+ assert_that({'a': 1, 'b': 2}).is_true()
+ assert_that({'a', 'b'}).is_true()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val **is** false
+ """
if not self.val:
- self._err('Expected , but was not.')
+ return self.error('Expected <%s> to be , but was not.' % self.val)
return self
def is_false(self):
- """Asserts that val is false."""
+ """Asserts that val is false.
+
+ Examples:
+ Usage::
+
+ assert_that(False).is_false()
+ assert_that(0).is_false()
+ assert_that('').is_false()
+ assert_that(0.0).is_false()
+ assert_that([]).is_false()
+ assert_that(()).is_false()
+ assert_that({}).is_false()
+ assert_that(set()).is_false()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val **is** true
+ """
if self.val:
- self._err('Expected , but was not.')
+ return self.error('Expected <%s> to be , but was not.' % self.val)
return self
def is_none(self):
- """Asserts that val is none."""
+ """Asserts that val is none.
+
+ Examples:
+ Usage::
+
+ assert_that(None).is_none()
+ assert_that(print('hello world')).is_none()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** none
+ """
if self.val is not None:
- self._err('Expected <%s> to be , but was not.' % self.val)
+ return self.error('Expected <%s> to be , but was not.' % self.val)
return self
def is_not_none(self):
- """Asserts that val is not none."""
+ """Asserts that val is not none.
+
+ Examples:
+ Usage::
+
+ assert_that(0).is_not_none()
+ assert_that('foo').is_not_none()
+ assert_that(False).is_not_none()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val **is** none
+ """
if self.val is None:
- self._err('Expected not , but was.')
+ return self.error('Expected not , but was.')
return self
def _type(self, val):
@@ -95,30 +326,102 @@ def _type(self, val):
return 'unknown'
def is_type_of(self, some_type):
- """Asserts that val is of the given type."""
+ """Asserts that val is of the given type.
+
+ Args:
+ some_type (type): the expected type
+
+ Examples:
+ Usage::
+
+ assert_that(1).is_type_of(int)
+ assert_that('foo').is_type_of(str)
+ assert_that(123.4).is_type_of(float)
+ assert_that(['a', 'b']).is_type_of(list)
+ assert_that((1, 2, 3)).is_type_of(tuple)
+ assert_that({'a': 1, 'b': 2}).is_type_of(dict)
+ assert_that({'a', 'b'}).is_type_of(set)
+ assert_that(True).is_type_of(bool)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** of the given type
+ """
if type(some_type) is not type and not issubclass(type(some_type), type):
raise TypeError('given arg must be a type')
if type(self.val) is not some_type:
t = self._type(self.val)
- self._err('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):
- """Asserts that val is an instance of the given class."""
+ """Asserts that val is an instance of the given class.
+
+ Args:
+ some_class: the expected class
+
+ Examples:
+ Usage::
+
+ assert_that(1).is_instance_of(int)
+ assert_that('foo').is_instance_of(str)
+ assert_that(123.4).is_instance_of(float)
+ assert_that(['a', 'b']).is_instance_of(list)
+ assert_that((1, 2, 3)).is_instance_of(tuple)
+ assert_that({'a': 1, 'b': 2}).is_instance_of(dict)
+ assert_that({'a', 'b'}).is_instance_of(set)
+ assert_that(True).is_instance_of(bool)
+
+ With a user-defined class::
+
+ class Foo: pass
+ f = Foo()
+ assert_that(f).is_instance_of(Foo)
+ assert_that(f).is_instance_of(object)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** an instance of the given class
+ """
try:
if not isinstance(self.val, some_class):
t = self._type(self.val)
- self._err('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
def is_length(self, length):
- """Asserts that val is the given length."""
+ """Asserts that val is the given length.
+
+ Checks val is the given length using the ``len()`` built-in.
+
+ Args:
+ length (int): the expected length
+
+ Examples:
+ Usage::
+
+ assert_that('foo').is_length(3)
+ assert_that(['a', 'b']).is_length(2)
+ assert_that((1, 2, 3)).is_length(3)
+ assert_that({'a': 1, 'b': 2}).is_length(2)
+ assert_that({'a', 'b'}).is_length(2)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** the given length
+ """
if type(length) is not int:
raise TypeError('given arg must be an int')
if length < 0:
raise ValueError('given arg must be a positive int')
if len(self.val) != length:
- self._err('Expected <%s> to be of length <%d>, but was <%d>.' % (self.val, length, len(self.val)))
- return self
\ No newline at end of file
+ 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 5f9d906..3066ee8 100644
--- a/assertpy/collection.py
+++ b/assertpy/collection.py
@@ -34,24 +34,81 @@
else:
Iterable = collections.Iterable
+__tracebackhide__ = True
+
class CollectionMixin(object):
"""Collection assertions mixin."""
def is_iterable(self):
- """Asserts that val is iterable collection."""
+ """Asserts that val is iterable collection.
+
+ Examples:
+ Usage::
+
+ assert_that('foo').is_iterable()
+ assert_that(['a', 'b']).is_iterable()
+ assert_that((1, 2, 3)).is_iterable()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** iterable
+ """
if not isinstance(self.val, Iterable):
- self._err('Expected iterable, but was not.')
+ return self.error('Expected iterable, but was not.')
return self
def is_not_iterable(self):
- """Asserts that val is not iterable collection."""
+ """Asserts that val is not iterable collection.
+
+ Examples:
+ Usage::
+
+ assert_that(1).is_not_iterable()
+ assert_that(123.4).is_not_iterable()
+ assert_that(True).is_not_iterable()
+ assert_that(None).is_not_iterable()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val **is** iterable
+ """
if isinstance(self.val, Iterable):
- self._err('Expected not iterable, but was.')
+ return self.error('Expected not iterable, but was.')
return self
def is_subset_of(self, *supersets):
- """Asserts that val is iterable and a subset of the given superset or flattened superset if multiple supersets are given."""
+ """Asserts that val is iterable and a subset of the given superset (or supersets).
+
+ Args:
+ *supersets: the expected superset (or supersets)
+
+ Examples:
+ Usage::
+
+ assert_that('foo').is_subset_of('abcdefghijklmnopqrstuvwxyz')
+ assert_that(['a', 'b']).is_subset_of(['a', 'b', 'c'])
+ assert_that((1, 2, 3)).is_subset_of([1, 2, 3, 4])
+ assert_that({'a': 1, 'b': 2}).is_subset_of({'a': 1, 'b': 2, 'c': 3})
+ assert_that({'a', 'b'}).is_subset_of({'a', 'b', 'c'})
+
+ # or multiple supersets (as comma-separated args)
+ assert_that('aBc').is_subset_of('abc', 'ABC')
+ assert_that((1, 2, 3)).is_subset_of([1, 3, 5], [2, 4, 6])
+
+ assert_that({'a': 1, 'b': 2}).is_subset_of({'a': 1, 'c': 3}) # fails
+ # Expected <{'a': 1, 'b': 2}> to be subset of <{'a': 1, 'c': 3}>, but <{'b': 2}> was missing.
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** subset of given superset (or supersets)
+ """
if not isinstance(self.val, Iterable):
raise TypeError('val is not iterable')
if len(supersets) == 0:
@@ -61,18 +118,19 @@ def is_subset_of(self, *supersets):
if hasattr(self.val, 'keys') and callable(getattr(self.val, 'keys')) and hasattr(self.val, '__getitem__'):
# flatten superset dicts
superdict = {}
- for l,j in enumerate(supersets):
+ for l, j in enumerate(supersets):
self._check_dict_like(j, check_values=False, name='arg #%d' % (l+1))
for k in j.keys():
superdict.update({k: j[k]})
for i in self.val.keys():
if i not in superdict:
- missing.append({i: self.val[i]}) # bad key
+ missing.append({i: self.val[i]}) # bad key
elif self.val[i] != superdict[i]:
- missing.append({i: self.val[i]}) # bad val
+ missing.append({i: self.val[i]}) # bad val
if missing:
- self._err('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'))
+ 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
superset = set()
@@ -87,6 +145,52 @@ def is_subset_of(self, *supersets):
if i not in superset:
missing.append(i)
if missing:
- self._err('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.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
+
+ def is_sorted(self, key=lambda x: x, reverse=False):
+ """Asserts that val is iterable and is sorted.
+
+ Args:
+ key (function): the one-arg function to extract the sort comparison key. Defaults to
+ ``lambda x: x`` to just compare items directly.
+ reverse (bool): if ``True``, then comparison key is reversed. Defaults to ``False``.
+
+ Examples:
+ Usage::
+
+ assert_that(['a', 'b', 'c']).is_sorted()
+ assert_that((1, 2, 3)).is_sorted()
+
+ # with a key function
+ assert_that('aBc').is_sorted(key=str.lower)
+
+ # reverse order
+ assert_that(['c', 'b', 'a']).is_sorted(reverse=True)
+ assert_that((3, 2, 1)).is_sorted(reverse=True)
+
+ assert_that((1, 2, 3, 4, -5, 6)).is_sorted() # fails
+ # Expected <(1, 2, 3, 4, -5, 6)> to be sorted, but subset <4, -5> at index 3 is not.
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** sorted
+ """
+ if not isinstance(self.val, Iterable):
+ raise TypeError('val is not iterable')
+
+ for i, x in enumerate(self.val):
+ if i > 0:
+ if reverse:
+ if key(x) > key(prev):
+ 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):
+ 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 1fd87d5..0f25b49 100644
--- a/assertpy/contains.py
+++ b/assertpy/contains.py
@@ -35,20 +35,52 @@
str_types = (basestring,)
xrange = xrange
+__tracebackhide__ = True
+
class ContainsMixin(object):
"""Containment assertions mixin."""
def contains(self, *items):
- """Asserts that val contains the given item or items."""
+ """Asserts that val contains the given item or items.
+
+ Checks if the collection contains the given item or items using ``in`` operator.
+
+ Args:
+ *items: the item or items expected to be contained
+
+ Examples:
+ Usage::
+
+ assert_that('foo').contains('f')
+ assert_that('foo').contains('f', 'oo')
+ assert_that(['a', 'b']).contains('b', 'a')
+ assert_that((1, 2, 3)).contains(3, 2, 1)
+ assert_that({'a': 1, 'b': 2}).contains('b', 'a') # checks keys
+ assert_that({'a', 'b'}).contains('b', 'a')
+ assert_that([1, 2, 3]).is_type_of(list).contains(1, 2).does_not_contain(4, 5)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val does **not** contain the item or items
+
+ Tip:
+ Use the :meth:`~assertpy.dict.DictMixin.contains_key` alias when working with
+ *dict-like* objects to be self-documenting.
+
+ See Also:
+ :meth:`~assertpy.string.StringMixin.contains_ignoring_case` - for case-insensitive string contains
+ """
if len(items) == 0:
raise ValueError('one or more args must be given')
elif len(items) == 1:
if items[0] not in self.val:
if self._check_dict_like(self.val, return_as_bool=True):
- self._err('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._err('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:
@@ -56,29 +88,77 @@ def contains(self, *items):
missing.append(i)
if missing:
if self._check_dict_like(self.val, return_as_bool=True):
- self._err('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)))
+ 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._err('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):
- """Asserts that val does not contain the given item or items."""
+ """Asserts that val does not contain the given item or items.
+
+ Checks if the collection excludes the given item or items using ``in`` operator.
+
+ Args:
+ *items: the item or items expected to be excluded
+
+ Examples:
+ Usage::
+
+ assert_that('foo').does_not_contain('x')
+ assert_that(['a', 'b']).does_not_contain('x', 'y')
+ assert_that((1, 2, 3)).does_not_contain(4, 5)
+ assert_that({'a': 1, 'b': 2}).does_not_contain('x', 'y') # checks keys
+ assert_that({'a', 'b'}).does_not_contain('x', 'y')
+ assert_that([1, 2, 3]).is_type_of(list).contains(1, 2).does_not_contain(4, 5)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val **does** contain the item or items
+
+ Tip:
+ Use the :meth:`~assertpy.dict.DictMixin.does_not_contain_key` alias when working with
+ *dict-like* objects to be self-documenting.
+ """
if len(items) == 0:
raise ValueError('one or more args must be given')
elif len(items) == 1:
if items[0] in self.val:
- self._err('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._err('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):
- """Asserts that val contains only the given item or items."""
+ """Asserts that val contains *only* the given item or items.
+
+ Checks if the collection contains only the given item or items using ``in`` operator.
+
+ Args:
+ *items: the *only* item or items expected to be contained
+
+ Examples:
+ Usage::
+
+ assert_that('foo').contains_only('f', 'o')
+ assert_that(['a', 'a', 'b']).contains_only('a', 'b')
+ assert_that((1, 1, 2)).contains_only(1, 2)
+ assert_that({'a': 1, 'a': 2, 'b': 3}).contains_only('a', 'b')
+ assert_that({'a', 'a', 'b'}).contains_only('a', 'b')
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val contains anything **not** item or items
+ """
if len(items) == 0:
raise ValueError('one or more args must be given')
else:
@@ -87,18 +167,38 @@ def contains_only(self, *items):
if i not in items:
extra.append(i)
if extra:
- self._err('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._err('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):
- """Asserts that val contains the given sequence of items in order."""
+ """Asserts that val contains the given ordered sequence of items.
+
+ Checks if the collection contains the given sequence of items using ``in`` operator.
+
+ Args:
+ *items: the sequence of items expected to be contained
+
+ Examples:
+ Usage::
+
+ assert_that('foo').contains_sequence('f', 'o')
+ assert_that('foo').contains_sequence('o', 'o')
+ assert_that(['a', 'b', 'c']).contains_sequence('b', 'c')
+ assert_that((1, 2, 3)).contains_sequence(1, 2)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val does **not** contains the given sequence of items
+ """
if len(items) == 0:
raise ValueError('one or more args must be given')
else:
@@ -111,60 +211,152 @@ def contains_sequence(self, *items):
return self
except TypeError:
raise TypeError('val is not iterable')
- self._err('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 contains duplicate items."""
+ """Asserts that val is iterable and *does* contain duplicates.
+
+ Examples:
+ Usage::
+
+ assert_that('foo').contains_duplicates()
+ assert_that(['a', 'a', 'b']).contains_duplicates()
+ assert_that((1, 1, 2)).contains_duplicates()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val does **not** contain any duplicates
+ """
try:
if len(self.val) != len(set(self.val)):
return self
except TypeError:
raise TypeError('val is not iterable')
- self._err('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 duplicate items."""
+ """Asserts that val is iterable and *does not* contain any duplicates.
+
+ Examples:
+ Usage::
+
+ assert_that('fox').does_not_contain_duplicates()
+ assert_that(['a', 'b', 'c']).does_not_contain_duplicates()
+ assert_that((1, 2, 3)).does_not_contain_duplicates()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val **does** contain duplicates
+ """
try:
if len(self.val) == len(set(self.val)):
return self
except TypeError:
raise TypeError('val is not iterable')
- self._err('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."""
+ """Asserts that val is empty.
+
+ Examples:
+ Usage::
+
+ assert_that('').is_empty()
+ assert_that([]).is_empty()
+ assert_that(()).is_empty()
+ assert_that({}).is_empty()
+ assert_that(set()).is_empty()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** empty
+ """
if len(self.val) != 0:
if isinstance(self.val, str_types):
- self._err('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._err('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):
- """Asserts that val is not empty."""
+ """Asserts that val is *not* empty.
+
+ Examples:
+ Usage::
+
+ assert_that('foo').is_not_empty()
+ assert_that(['a', 'b']).is_not_empty()
+ assert_that((1, 2, 3)).is_not_empty()
+ assert_that({'a': 1, 'b': 2}).is_not_empty()
+ assert_that({'a', 'b'}).is_not_empty()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val **is** empty
+ """
if len(self.val) == 0:
if isinstance(self.val, str_types):
- self._err('Expected not empty string, but was empty.')
+ return self.error('Expected not empty string, but was empty.')
else:
- self._err('Expected not empty, but was empty.')
+ return self.error('Expected not empty, but was empty.')
return self
def is_in(self, *items):
- """Asserts that val is equal to one of the given items."""
+ """Asserts that val is equal to one of the given items.
+
+ Args:
+ *items: the items expected to contain val
+
+ Examples:
+ Usage::
+
+ assert_that('foo').is_in('foo', 'bar', 'baz')
+ assert_that(1).is_in(0, 1, 2, 3)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** in the given items
+ """
if len(items) == 0:
raise ValueError('one or more args must be given')
else:
for i in items:
if self.val == i:
return self
- self._err('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."""
+ """Asserts that val is not equal to one of the given items.
+
+ Args:
+ *items: the items expected to exclude val
+
+ Examples:
+ Usage::
+
+ assert_that('foo').is_not_in('bar', 'baz', 'box')
+ assert_that(1).is_not_in(-1, -2, -3)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val **is** in the given items
+ """
if len(items) == 0:
raise ValueError('one or more args must be given')
else:
for i in items:
if self.val == i:
- self._err('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 2dbafbb..0cdcb29 100644
--- a/assertpy/date.py
+++ b/assertpy/date.py
@@ -28,54 +28,166 @@
import datetime
+__tracebackhide__ = True
+
class DateMixin(object):
"""Date and time assertions mixin."""
-### datetime assertions ###
def is_before(self, other):
- """Asserts that val is a date and is before other date."""
+ """Asserts that val is a date and is before other date.
+
+ Args:
+ other: the other date, expected to be after val
+
+ Examples:
+ Usage::
+
+ import datetime
+
+ today = datetime.datetime.now()
+ yesterday = today - datetime.timedelta(days=1)
+
+ assert_that(yesterday).is_before(today)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** before the given date
+
+ See Also:
+ :meth:`~assertpy.string.NumericMixin.is_less_than` - numeric assertion, but also works with datetime BR
+ :meth:`~assertpy.string.NumericMixin.is_less_than_or_equal_to` - numeric assertion, but also works with datetime
+ """
if type(self.val) is not datetime.datetime:
raise TypeError('val must be datetime, but was type <%s>' % type(self.val).__name__)
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._err('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):
- """Asserts that val is a date and is after other date."""
+ """Asserts that val is a date and is after other date.
+
+ Args:
+ other: the other date, expected to be before val
+
+ Examples:
+ Usage::
+
+ import datetime
+
+ today = datetime.datetime.now()
+ yesterday = today - datetime.timedelta(days=1)
+
+ assert_that(today).is_after(yesterday)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** after the given date
+
+ See Also:
+ :meth:`~assertpy.string.NumericMixin.is_greater_than` - numeric assertion, but also works with datetime BR
+ :meth:`~assertpy.string.NumericMixin.is_greater_than_or_equal_to` - numeric assertion, but also works with datetime
+ """
if type(self.val) is not datetime.datetime:
raise TypeError('val must be datetime, but was type <%s>' % type(self.val).__name__)
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._err('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):
+ """Asserts that val is a date and is equal to other date to the second.
+
+ Args:
+ other: the other date, expected to be equal to the second
+
+ Examples:
+ Usage::
+
+ import datetime
+
+ d1 = datetime.datetime(2020, 1, 2, 3, 4, 5, 6) # 2020-01-02 03:04:05.000006
+ d2 = datetime.datetime(2020, 1, 2, 3, 4, 5, 777777) # 2020-01-02 03:04:05.777777
+
+ assert_that(d1).is_equal_to_ignoring_milliseconds(d2)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** equal to the given date to the second
+ """
if type(self.val) is not datetime.datetime:
raise TypeError('val must be datetime, but was type <%s>' % type(self.val).__name__)
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._err('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):
+ """Asserts that val is a date and is equal to other date to the minute.
+
+ Args:
+ other: the other date, expected to be equal to the minute
+
+ Examples:
+ Usage::
+
+ import datetime
+
+ d1 = datetime.datetime(2020, 1, 2, 3, 4, 5) # 2020-01-02 03:04:05
+ d2 = datetime.datetime(2020, 1, 2, 3, 4, 55) # 2020-01-02 03:04:55
+
+ assert_that(d1).is_equal_to_ignoring_seconds(d2)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** equal to the given date to the minute
+ """
if type(self.val) is not datetime.datetime:
raise TypeError('val must be datetime, but was type <%s>' % type(self.val).__name__)
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._err('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):
+ """Asserts that val is a date and is equal to other date ignoring time.
+
+ Args:
+ other: the other date, expected to be equal ignoring time
+
+ Examples:
+ Usage::
+
+ import datetime
+
+ d1 = datetime.datetime(2020, 1, 2, 3, 4, 5) # 2020-01-02 03:04:05
+ d2 = datetime.datetime(2020, 1, 2, 13, 44, 55) # 2020-01-02 13:44:55
+
+ assert_that(d1).is_equal_to_ignoring_time(d2)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** equal to the given date ignoring time
+ """
if type(self.val) is not datetime.datetime:
raise TypeError('val must be datetime, but was type <%s>' % type(self.val).__name__)
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._err('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 d1f9379..e01dce7 100644
--- a/assertpy/dict.py
+++ b/assertpy/dict.py
@@ -26,22 +26,78 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+__tracebackhide__ = True
+
class DictMixin(object):
"""Dict assertions mixin."""
-
+
def contains_key(self, *keys):
- """Asserts the val is a dict and contains the given key or keys. Alias for contains()."""
+ """Asserts the val is a dict and contains the given key or keys. Alias for :meth:`~assertpy.contains.ContainsMixin.contains`.
+
+ Checks if the dict contains the given key or keys using ``in`` operator.
+
+ Args:
+ *keys: the key or keys expected to be contained
+
+ Examples:
+ Usage::
+
+ assert_that({'a': 1, 'b': 2}).contains_key('a')
+ assert_that({'a': 1, 'b': 2}).contains_key('a', 'b')
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val does **not** contain the key or keys
+ """
self._check_dict_like(self.val, check_values=False, check_getitem=False)
return self.contains(*keys)
def does_not_contain_key(self, *keys):
- """Asserts the val is a dict and does not contain the given key or keys. Alias for does_not_contain()."""
+ """Asserts the val is a dict and does not contain the given key or keys. Alias for :meth:`~assertpy.contains.ContainsMixin.does_not_contain`.
+
+ Checks if the dict excludes the given key or keys using ``in`` operator.
+
+ Args:
+ *keys: the key or keys expected to be excluded
+
+ Examples:
+ Usage::
+
+ assert_that({'a': 1, 'b': 2}).does_not_contain_key('x')
+ assert_that({'a': 1, 'b': 2}).does_not_contain_key('x', 'y')
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val **does** contain the key or keys
+ """
self._check_dict_like(self.val, check_values=False, check_getitem=False)
return self.does_not_contain(*keys)
def contains_value(self, *values):
- """Asserts that val is a dict and contains the given value or values."""
+ """Asserts that val is a dict and contains the given value or values.
+
+ Checks if the dict contains the given value or values in *any* key.
+
+ Args:
+ *values: the value or values expected to be contained
+
+ Examples:
+ Usage::
+
+ assert_that({'a': 1, 'b': 2}).contains_value(1)
+ assert_that({'a': 1, 'b': 2}).contains_value(1, 2)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val does **not** contain the value or values
+ """
self._check_dict_like(self.val, check_getitem=False)
if len(values) == 0:
raise ValueError('one or more value args must be given')
@@ -50,11 +106,29 @@ def contains_value(self, *values):
if v not in self.val.values():
missing.append(v)
if missing:
- self._err('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):
- """Asserts that val is a dict and does not contain the given value or values."""
+ """Asserts that val is a dict and does not contain the given value or values.
+
+ Checks if the dict excludes the given value or values across *all* keys.
+
+ Args:
+ *values: the value or values expected to be excluded
+
+ Examples:
+ Usage::
+
+ assert_that({'a': 1, 'b': 2}).does_not_contain_value(3)
+ assert_that({'a': 1, 'b': 2}).does_not_contain_value(3, 4)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val **does** contain the value or values
+ """
self._check_dict_like(self.val, check_getitem=False)
if len(values) == 0:
raise ValueError('one or more value args must be given')
@@ -64,13 +138,43 @@ def does_not_contain_value(self, *values):
if v in self.val.values():
found.append(v)
if found:
- self._err('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):
- """Asserts that val is a dict and contains the given entry or entries."""
+ """Asserts that val is a dict and contains the given entry or entries.
+
+ Checks if the dict contains the given key-value pair or pairs.
+
+ Args:
+ *args: the entry or entries expected to be contained (as ``{k: v}`` args)
+ **kwargs: the entry or entries expected to be contained (as ``k=v`` kwargs)
+
+ Examples:
+ Usage::
+
+ # using args
+ assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry({'a': 1})
+ assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry({'a': 1}, {'b': 2})
+ assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry({'a': 1}, {'b': 2}, {'c': 3})
+
+ # using kwargs
+ assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry(a=1)
+ assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry(a=1, b=2)
+ assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry(a=1, b=2, c=3)
+
+ # or args and kwargs
+ assert_that({'a': 1, 'b': 2, 'c': 3}).contains_entry({'c': 3}, a=1, b=2)
+
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val does **not** contain the entry or entries
+ """
self._check_dict_like(self.val, check_values=False)
- entries = list(args) + [{k:v} for k,v in kwargs.items()]
+ entries = list(args) + [{k: v} for k, v in kwargs.items()]
if len(entries) == 0:
raise ValueError('one or more entry args must be given')
missing = []
@@ -81,17 +185,41 @@ def contains_entry(self, *args, **kwargs):
raise ValueError('given entry args must contain exactly one key-value pair')
k = next(iter(e))
if k not in self.val:
- missing.append(e) # bad key
+ missing.append(e) # bad key
elif self.val[k] != e[k]:
- missing.append(e) # bad val
+ missing.append(e) # bad val
if missing:
- self._err('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):
- """Asserts that val is a dict and does not contain the given entry or entries."""
+ """Asserts that val is a dict and does not contain the given entry or entries.
+
+ Checks if the dict excludes the given key-value pair or pairs.
+
+ Args:
+ *args: the entry or entries expected to be excluded (as ``{k: v}`` args)
+ **kwargs: the entry or entries expected to be excluded (as ``k=v`` kwargs)
+
+ Examples:
+ Usage::
+
+ # using args
+ assert_that({'a': 1, 'b': 2, 'c': 3}).does_not_contain_entry({'a': 2})
+ assert_that({'a': 1, 'b': 2, 'c': 3}).does_not_contain_entry({'a': 2}, {'x': 4})
+
+ # using kwargs
+ assert_that({'a': 1, 'b': 2, 'c': 3}).does_not_contain_entry(a=2)
+ assert_that({'a': 1, 'b': 2, 'c': 3}).does_not_contain_entry(a=2, x=4)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val **does** contain the entry or entries
+ """
self._check_dict_like(self.val, check_values=False)
- entries = list(args) + [{k:v} for k,v in kwargs.items()]
+ entries = list(args) + [{k: v} for k, v in kwargs.items()]
if len(entries) == 0:
raise ValueError('one or more entry args must be given')
found = []
@@ -104,5 +232,5 @@ def does_not_contain_entry(self, *args, **kwargs):
if k in self.val and e[k] == self.val[k]:
found.append(e)
if found:
- self._err('Expected <%s> to not contain entries %s, but did contain %s.' % (self.val, self._fmt_items(entries), self._fmt_items(found)))
- return self
\ No newline at end of file
+ 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 edaa963..b2d1c2e 100644
--- a/assertpy/dynamic.py
+++ b/assertpy/dynamic.py
@@ -34,21 +34,56 @@
else:
Iterable = collections.Iterable
+__tracebackhide__ = True
+
class DynamicMixin(object):
- """Dynamic assertions mixin."""
+ """Dynamic assertions mixin.
+
+ When testing attributes of an object (or the contents of a dict), the
+ :meth:`~assertpy.base.BaseMixin.is_equal_to` assertion can be a bit verbose::
+
+ fred = Person('Fred', 'Smith')
+
+ assert_that(fred.first_name).is_equal_to('Fred')
+ assert_that(fred.name).is_equal_to('Fred Smith')
+ assert_that(fred.say_hello()).is_equal_to('Hello, Fred!')
+
+ Instead, use dynamic assertions in the form of ``has_()`` where ```` is the name of
+ any attribute, property, or zero-argument method on the given object. Dynamic equality
+ assertions test if actual is equal to expected using the ``==`` operator. Using dynamic
+ assertions, we can rewrite the above example as::
+
+ assert_that(fred).has_first_name('Fred')
+ assert_that(fred).has_name('Fred Smith')
+ assert_that(fred).has_say_hello('Hello, Fred!')
+
+ Similarly, dynamic assertions also work on any *dict-like* object::
+
+ fred = {
+ 'first_name': 'Fred',
+ 'last_name': 'Smith',
+ 'shoe_size': 12
+ }
+
+ assert_that(fred).has_first_name('Fred')
+ assert_that(fred).has_last_name('Smith')
+ assert_that(fred).has_shoe_size(12)
+ """
def __getattr__(self, attr):
- """Asserts that val has attribute attr and that attribute's value is equal to other via a dynamic assertion of the form: has_()."""
+ """Asserts that val has attribute attr and that its value is equal to other via a dynamic
+ assertion of the form ``has_()``."""
if not attr.startswith('has_'):
raise AttributeError('assertpy has no assertion <%s()>' % attr)
attr_name = attr[4:]
err_msg = False
+ is_namedtuple = isinstance(self.val, tuple) and hasattr(self.val, '_fields')
is_dict = isinstance(self.val, Iterable) and hasattr(self.val, '__getitem__')
if not hasattr(self.val, attr_name):
- if is_dict:
+ if is_dict and not is_namedtuple:
if attr_name not in self.val:
err_msg = 'Expected key <%s>, but val has no key <%s>.' % (attr_name, attr_name)
else:
@@ -56,15 +91,15 @@ def __getattr__(self, attr):
def _wrapper(*args, **kwargs):
if err_msg:
- self._err(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)))
- try:
- val_attr = getattr(self.val, attr_name)
- except AttributeError:
+ if is_dict and not is_namedtuple:
val_attr = self.val[attr_name]
+ else:
+ val_attr = getattr(self.val, attr_name)
if callable(val_attr):
try:
@@ -76,7 +111,7 @@ def _wrapper(*args, **kwargs):
expected = args[0]
if actual != expected:
- self._err('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 6093ecf..1649985 100644
--- a/assertpy/exception.py
+++ b/assertpy/exception.py
@@ -34,38 +34,80 @@
else:
Iterable = collections.Iterable
+__tracebackhide__ = True
+
class ExceptionMixin(object):
"""Expected exception mixin."""
def raises(self, ex):
- """Asserts that val is callable and that when called raises the given error."""
+ """Asserts that val is callable and set the expected exception.
+
+ Just sets the expected exception, but never calls val, and therefore never failes. You must
+ chain to :meth:`~when_called_with` to invoke ``val()``.
+
+ Args:
+ ex: the expected exception
+
+ Examples:
+ Usage::
+
+ assert_that(some_func).raises(RuntimeError).when_called_with('foo')
+
+ Returns:
+ AssertionBuilder: returns a new instance (now with the given expected exception) to chain to the next assertion
+ """
if not callable(self.val):
raise TypeError('val must be callable')
if not issubclass(ex, BaseException):
raise TypeError('given arg must be exception')
- return self._builder(self.val, self.description, self.kind, ex) # don't chain!
+
+ # chain on with ex as the expected exception
+ return self.builder(self.val, self.description, self.kind, ex)
def when_called_with(self, *some_args, **some_kwargs):
- """Asserts the val callable when invoked with the given args and kwargs raises the expected exception."""
+ """Asserts that val, when invoked with the given args and kwargs, raises the expected exception.
+
+ Invokes ``val()`` with the given args and kwargs. You must first set the expected
+ exception with :meth:`~raises`.
+
+ Args:
+ *some_args: the args to call ``val()``
+ **some_kwargs: the kwargs to call ``val()``
+
+ Examples:
+ Usage::
+
+ def some_func(a):
+ raise RuntimeError('some error!')
+
+ assert_that(some_func).raises(RuntimeError).when_called_with('foo')
+
+ Returns:
+ AssertionBuilder: returns a new instance (now with the captured exception error message as the val) to chain to the next assertion
+
+ Raises:
+ AssertionError: if val does **not** raise the expected exception
+ TypeError: if expected exception not set via :meth:`raises`
+ """
if not self.expected:
raise TypeError('expected exception not set, raises() must be called first')
try:
self.val(*some_args, **some_kwargs)
except BaseException as e:
if issubclass(type(e), self.expected):
- # chain on with _message_ (don't chain to self!)
- return self._builder(str(e), self.description, self.kind)
+ # chain on with error message
+ return self.builder(str(e), self.description, self.kind)
else:
# got exception, but wrong type, so raise
- self._err('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._err('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 f0a8b81..98d4ad8 100644
--- a/assertpy/extracting.py
+++ b/assertpy/extracting.py
@@ -36,13 +36,134 @@
str_types = (basestring,)
Iterable = collections.Iterable
+__tracebackhide__ = True
+
class ExtractingMixin(object):
- """Collection flattening mixin."""
+ """Collection flattening mixin.
+
+ It is often necessary to test collections of objects. Use the ``extracting()`` helper to
+ reduce the collection on a given attribute. Reduce a list of objects::
+
+ alice = Person('Alice', 'Alpha')
+ bob = Person('Bob', 'Bravo')
+ people = [alice, bob]
+
+ assert_that(people).extracting('first_name').is_equal_to(['Alice', 'Bob'])
+ assert_that(people).extracting('first_name').contains('Alice', 'Bob')
+ assert_that(people).extracting('first_name').does_not_contain('Charlie')
+
+ Additionally, the ``extracting()`` helper can accept a list of attributes to be extracted, and
+ will flatten them into a list of tuples. Reduce a list of objects on multiple attributes::
+
+ assert_that(people).extracting('first_name', 'last_name').contains(('Alice', 'Alpha'), ('Bob', 'Bravo'))
+
+ Also, ``extracting()`` works on not just attributes, but also properties, and even
+ zero-argument methods. Reduce a list of object on properties and zero-arg methods::
+
+ assert_that(people).extracting('name').contains('Alice Alpha', 'Bob Bravo')
+ assert_that(people).extracting('say_hello').contains('Hello, Alice!', 'Hello, Bob!')
+
+ And ``extracting()`` even works on *dict-like* objects. Reduce a list of dicts on key::
+
+ alice = {'first_name': 'Alice', 'last_name': 'Alpha'}
+ bob = {'first_name': 'Bob', 'last_name': 'Bravo'}
+ people = [alice, bob]
+
+ assert_that(people).extracting('first_name').contains('Alice', 'Bob')
+
+ **Filtering**
+
+ The ``extracting()`` helper can include a *filter* to keep only those items for which the given
+ *filter* is truthy. For example::
+
+ users = [
+ {'user': 'Alice', 'age': 36, 'active': True},
+ {'user': 'Bob', 'age': 40, 'active': False},
+ {'user': 'Charlie', 'age': 13, 'active': True}
+ ]
+
+ # filter the active users
+ assert_that(users).extracting('user', filter='active').is_equal_to(['Alice', 'Charlie'])
+
+ The *filter* can be a *dict-like* object and the extracted items are kept if and only if all
+ corresponding key-value pairs are equal::
+
+ assert_that(users).extracting('user', filter={'active': False}).is_equal_to(['Bob'])
+ assert_that(users).extracting('user', filter={'age': 36, 'active': True}).is_equal_to(['Alice'])
+
+ Or a *filter* can be any function (including an in-line ``lambda``) that accepts as its single
+ argument each item in the collection, and the extracted items are kept if the function
+ evaluates to ``True``::
+
+ assert_that(users).extracting('user', filter=lambda x: x['age'] > 20)
+ .is_equal_to(['Alice', 'Bob'])
+
+ **Sorting**
+
+ The ``extracting()`` helper can include a *sort* to enforce order on the extracted items.
+
+ The *sort* can be the name of a key (or attribute, or property, or zero-argument method) and
+ the extracted items are ordered by the corresponding values::
+
+ assert_that(users).extracting('user', sort='age').is_equal_to(['Charlie', 'Alice', 'Bob'])
+
+ The *sort* can be an ``iterable`` of names and the extracted items are ordered by
+ corresponding value of the first name, ties are broken by the corresponding values of the
+ second name, and so on::
+
+ assert_that(users).extracting('user', sort=['active', 'age']).is_equal_to(['Bob', 'Charlie', 'Alice'])
+
+ The *sort* can be any function (including an in-line ``lambda``) that accepts as its single
+ argument each item in the collection, and the extracted items are ordered by the corresponding
+ function return values::
+
+ assert_that(users).extracting('user', sort=lambda x: -x['age']).is_equal_to(['Bob', 'Alice', 'Charlie'])
+ """
-### collection of objects assertions ###
def extracting(self, *names, **kwargs):
- """Asserts that val is collection, then extracts the named properties or named zero-arg methods into a list (or list of tuples if multiple names are given)."""
+ """Asserts that val is iterable, then extracts the named attributes, properties, or
+ zero-arg methods into a list (or list of tuples if multiple names are given).
+
+ Args:
+ *names: the attribute to be extracted (or property or zero-arg method)
+ **kwargs: see below
+
+ Keyword Args:
+ filter: extract only those items where filter is truthy
+ sort: order the extracted items by the sort key
+
+ Examples:
+ Usage::
+
+ alice = User('Alice', 20, True)
+ bob = User('Bob', 30, False)
+ charlie = User('Charlie', 10, True)
+ users = [alice, bob, charlie]
+
+ assert_that(users).extracting('user').contains('Alice', 'Bob', 'Charlie')
+
+ Works with *dict-like* objects too::
+
+ users = [
+ {'user': 'Alice', 'age': 20, 'active': True},
+ {'user': 'Bob', 'age': 30, 'active': False},
+ {'user': 'Charlie', 'age': 10, 'active': True}
+ ]
+
+ assert_that(people).extracting('user').contains('Alice', 'Bob', 'Charlie')
+
+ Filter::
+
+ assert_that(users).extracting('user', filter='active').is_equal_to(['Alice', 'Charlie'])
+
+ Sort::
+
+ assert_that(users).extracting('user', sort='age').is_equal_to(['Charlie', 'Alice', 'Bob'])
+
+ Returns:
+ AssertionBuilder: returns a new instance (now with the extracted list as the val) to chain to the next assertion
+ """
if not isinstance(self.val, Iterable):
raise TypeError('val is not iterable')
if isinstance(self.val, str_types):
@@ -56,7 +177,12 @@ def _extract(x, name):
return x[name]
else:
raise ValueError('item keys %s did not contain key <%s>' % (list(x.keys()), name))
- elif isinstance(x, Iterable):
+ elif isinstance(x, tuple) and hasattr(x, '_fields') and type(name) is str:
+ if name in x._fields:
+ return getattr(x, name)
+ else: #val has no attribute
+ raise ValueError('item attributes %s did no contain attribute <%s>' % (x._fields, name))
+ 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):
@@ -65,11 +191,11 @@ def _extract(x, name):
try:
return attr()
except TypeError:
- raise ValueError('val method <%s()> exists, but is not zero-arg method' % name)
+ raise ValueError('item method <%s()> exists, but is not zero-arg method' % name)
else:
return attr
else:
- raise ValueError('val does not have property or zero-arg method <%s>' % name)
+ raise ValueError('item does not have property or zero-arg method <%s>' % name)
def _filter(x):
if 'filter' in kwargs:
@@ -105,6 +231,6 @@ def _sort(x):
if _filter(i):
items = [_extract(i, name) for name in names]
extracted.append(tuple(items) if len(items) > 1 else items[0])
-
+
# chain on with _extracted_ list (don't chain to self!)
- return self._builder(extracted, self.description, self.kind)
+ return self.builder(extracted, self.description, self.kind)
diff --git a/assertpy/file.py b/assertpy/file.py
index c881ee9..2c3de9a 100644
--- a/assertpy/file.py
+++ b/assertpy/file.py
@@ -34,22 +34,42 @@
else:
str_types = (basestring,)
+__tracebackhide__ = True
-def contents_of(f, encoding='utf-8'):
+
+def contents_of(file, encoding='utf-8'):
"""Helper to read the contents of the given file or path into a string with the given encoding.
- Encoding defaults to 'utf-8', other useful encodings are 'ascii' and 'latin-1'."""
+ Args:
+ file: a *path-like object* (aka a file name) or a *file-like object* (aka a file)
+ encoding (str): the target encoding. Defaults to ``utf-8``, other useful encodings are ``ascii`` and ``latin-1``.
+
+ Examples:
+ Usage::
+
+ from assertpy import assert_that, contents_of
+
+ contents = contents_of('foo.txt')
+ assert_that(contents).starts_with('foo').ends_with('bar').contains('oob')
+
+ Returns:
+ str: returns the file contents as a string
+
+ Raises:
+ IOError: if file not found
+ TypeError: if file is not a *path-like object* or a *file-like object*
+ """
try:
- contents = f.read()
+ contents = file.read()
except AttributeError:
try:
- with open(f, 'r') as fp:
+ with open(file, 'r') as fp:
contents = fp.read()
except TypeError:
- raise ValueError('val must be file or path, but was type <%s>' % type(f).__name__)
+ raise ValueError('val must be file or path, but was type <%s>' % type(file).__name__)
except OSError:
- if not isinstance(f, str_types):
- raise ValueError('val must be file or path, but was type <%s>' % type(f).__name__)
+ if not isinstance(file, str_types):
+ raise ValueError('val must be file or path, but was type <%s>' % type(file).__name__)
raise
if sys.version_info[0] == 3 and type(contents) is bytes:
@@ -72,52 +92,134 @@ class FileMixin(object):
"""File assertions mixin."""
def exists(self):
- """Asserts that val is a path and that it exists."""
+ """Asserts that val is a path and that it exists.
+
+ Examples:
+ Usage::
+
+ assert_that('myfile.txt').exists()
+ assert_that('mydir').exists()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val does **not** exist
+ """
if not isinstance(self.val, str_types):
raise TypeError('val is not a path')
if not os.path.exists(self.val):
- self._err('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):
- """Asserts that val is a path and that it does not exist."""
+ """Asserts that val is a path and that it does *not* exist.
+
+ Examples:
+ Usage::
+
+ assert_that('missing.txt').does_not_exist()
+ assert_that('missing_dir').does_not_exist()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val **does** exist
+ """
if not isinstance(self.val, str_types):
raise TypeError('val is not a path')
if os.path.exists(self.val):
- self._err('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):
- """Asserts that val is an existing path to a file."""
+ """Asserts that val is a *file* and that it exists.
+
+ Examples:
+ Usage::
+
+ assert_that('myfile.txt').is_file()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val does **not** exist, or is **not** a file
+ """
self.exists()
if not os.path.isfile(self.val):
- self._err('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):
- """Asserts that val is an existing path to a directory."""
+ """Asserts that val is a *directory* and that it exists.
+
+ Examples:
+ Usage::
+
+ assert_that('mydir').is_directory()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val does **not** exist, or is **not** a directory
+ """
self.exists()
if not os.path.isdir(self.val):
- self._err('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):
- """Asserts that val is an existing path to a file and that file is named filename."""
+ """Asserts that val is an existing path to a file and that file is named filename.
+
+ Args:
+ filename: the expected filename
+
+ Examples:
+ Usage::
+
+ assert_that('/path/to/mydir/myfile.txt').is_named('myfile.txt')
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val does **not** exist, or is **not** a file, or is **not** named the given filename
+ """
self.is_file()
if not isinstance(filename, str_types):
raise TypeError('given filename arg must be a path')
val_filename = os.path.basename(os.path.abspath(self.val))
if val_filename != filename:
- self._err('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):
- """Asserts that val is an existing path to a file and that file is a child of parent."""
+ """Asserts that val is an existing path to a file and that file is a child of parent.
+
+ Args:
+ parent: the expected parent directory
+
+ Examples:
+ Usage::
+
+ assert_that('/path/to/mydir/myfile.txt').is_child_of('mydir')
+ assert_that('/path/to/mydir/myfile.txt').is_child_of('to')
+ assert_that('/path/to/mydir/myfile.txt').is_child_of('path')
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val does **not** exist, or is **not** a file, or is **not** a child of the given directory
+ """
self.is_file()
if not isinstance(parent, str_types):
raise TypeError('given parent directory arg must be a path')
val_abspath = os.path.abspath(self.val)
parent_abspath = os.path.abspath(parent)
if not val_abspath.startswith(parent_abspath):
- self._err('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 e5099a1..582a626 100644
--- a/assertpy/helpers.py
+++ b/assertpy/helpers.py
@@ -36,15 +36,18 @@
else:
Iterable = collections.Iterable
+__tracebackhide__ = True
+
class HelpersMixin(object):
- """Helpers mixin."""
+ """Helpers mixin. For internal use only."""
def _fmt_items(self, i):
+ """Helper to format the given items."""
if len(i) == 0:
return '<>'
elif len(i) == 1 and hasattr(i, '__getitem__'):
- return '<%s>' % i[0]
+ return '<%s>' % (i[0],)
else:
return '<%s>' % str(i).lstrip('([').rstrip(',])')
@@ -53,8 +56,8 @@ def _fmt_args_kwargs(self, *some_args, **some_kwargs):
if some_args:
out_args = str(some_args).lstrip('(').rstrip(',)')
if some_kwargs:
- out_kwargs = ', '.join([str(i).lstrip('(').rstrip(')').replace(', ',': ') for i in [
- (k,some_kwargs[k]) for k in sorted(some_kwargs.keys())]])
+ out_kwargs = ', '.join([str(i).lstrip('(').rstrip(')').replace(', ', ': ') for i in [
+ (k, some_kwargs[k]) for k in sorted(some_kwargs.keys())]])
if some_args and some_kwargs:
return out_args + ', ' + out_kwargs
@@ -66,13 +69,14 @@ def _fmt_args_kwargs(self, *some_args, **some_kwargs):
return ''
def _validate_between_args(self, val_type, low, high):
+ """Helper to validate given range args."""
low_type = type(low)
high_type = type(high)
- if val_type in self.NUMERIC_NON_COMPAREABLE:
+ if val_type in self._NUMERIC_NON_COMPAREABLE:
raise TypeError('ordering is not defined for type <%s>' % val_type.__name__)
- if val_type in self.NUMERIC_COMPAREABLE:
+ if val_type in self._NUMERIC_COMPAREABLE:
if low_type is not val_type:
raise TypeError('given low arg must be <%s>, but was <%s>' % (val_type.__name__, low_type.__name__))
if high_type is not val_type:
@@ -89,6 +93,7 @@ def _validate_between_args(self, val_type, low, high):
raise ValueError('given low arg must be less than given high arg')
def _validate_close_to_args(self, val, other, tolerance):
+ """Helper for validate given arg and delta."""
if type(val) is complex or type(other) is complex or type(tolerance) is complex:
raise TypeError('ordering is not defined for complex numbers')
@@ -109,6 +114,7 @@ def _validate_close_to_args(self, val, other, tolerance):
raise ValueError('given tolerance arg must be positive')
def _check_dict_like(self, d, check_keys=True, check_values=True, check_getitem=True, name='val', return_as_bool=False):
+ """Helper to check if given val has various dict-like attributes."""
if not isinstance(d, Iterable):
if return_as_bool:
return False
@@ -136,6 +142,7 @@ def _check_dict_like(self, d, check_keys=True, check_values=True, check_getitem=
return True
def _check_iterable(self, l, check_getitem=True, name='val'):
+ """Helper to check if given val has various iterable attributes."""
if not isinstance(l, Iterable):
raise TypeError('%s <%s> is not iterable' % (name, type(l).__name__))
if check_getitem:
@@ -143,6 +150,7 @@ def _check_iterable(self, l, check_getitem=True, name='val'):
raise TypeError('%s <%s> does not have [] accessor' % (name, type(l).__name__))
def _dict_not_equal(self, val, other, ignore=None, include=None):
+ """Helper to compare dicts."""
if ignore or include:
ignores = self._dict_ignore(ignore)
includes = self._dict_include(include)
@@ -154,59 +162,74 @@ def _dict_not_equal(self, val, other, ignore=None, include=None):
if i not in val:
missing.append(i)
if missing:
- self._err('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]),
'' if len(missing) == 1 else 's',
self._fmt_items(missing)))
+ # calc val keys given ignores and includes
if ignore and include:
k1 = set([k for k in val if k not in ignores and k in includes])
elif ignore:
k1 = set([k for k in val if k not in ignores])
- else: # include
+ else: # include
k1 = set([k for k in val if k in includes])
+ # calc other keys given ignores and includes
if ignore and include:
k2 = set([k for k in other if k not in ignores and k in includes])
elif ignore:
k2 = set([k for k in other if k not in ignores])
- else: # include
+ else: # include
k2 = set([k for k in other if k in includes])
if k1 != k2:
+ # different set of keys, so not equal
return True
else:
for k in k1:
- if self._check_dict_like(val[k], check_values=False, return_as_bool=True) and self._check_dict_like(other[k], check_values=False, return_as_bool=True):
- return self._dict_not_equal(val[k], other[k],
+ if self._check_dict_like(val[k], check_values=False, return_as_bool=True) and \
+ self._check_dict_like(other[k], check_values=False, return_as_bool=True):
+ subdicts_not_equal = self._dict_not_equal(
+ val[k],
+ other[k],
ignore=[i[1:] for i in ignores if type(i) is tuple and i[0] == k] if ignore else None,
include=[i[1:] for i in self._dict_ignore(include) if type(i) is tuple and i[0] == k] if include else None)
+ if subdicts_not_equal:
+ # fast fail inside the loop since sub-dicts are not equal
+ return True
elif val[k] != other[k]:
+ # fast fail inside the loop since values are not equal
return True
return False
else:
return val != other
def _dict_ignore(self, ignore):
- return [i[0] if type(i) is tuple and len(i) == 1 else i \
- for i in (ignore if type(ignore) is list else [ignore])]
+ """Helper to make list for given ignore kwarg values."""
+ return [i[0] if type(i) is tuple and len(i) == 1 else i for i in (ignore if type(ignore) is list else [ignore])]
def _dict_include(self, include):
- return [i[0] if type(i) is tuple else i \
- for i in (include if type(include) is list else [include])]
+ """Helper to make a list from given include kwarg values."""
+ return [i[0] if type(i) is tuple else i for i in (include if type(include) is list else [include])]
def _dict_err(self, val, other, ignore=None, include=None):
+ """Helper to construct error message for dict comparison."""
def _dict_repr(d, other):
out = ''
ellip = False
- for k,v in d.items():
+ for k, v in sorted(d.items()):
if k not in other:
out += '%s%s: %s' % (', ' if len(out) > 0 else '', repr(k), repr(v))
elif v != other[k]:
- out += '%s%s: %s' % (', ' if len(out) > 0 else '', repr(k),
- _dict_repr(v, other[k]) if self._check_dict_like(v, check_values=False, return_as_bool=True) and self._check_dict_like(other[k], check_values=False, return_as_bool=True) else repr(v)
+ out += '%s%s: %s' % (
+ ', ' if len(out) > 0 else '',
+ repr(k),
+ _dict_repr(v, other[k]) if self._check_dict_like(
+ v, check_values=False, return_as_bool=True) and self._check_dict_like(
+ other[k], check_values=False, return_as_bool=True) else repr(v)
)
else:
ellip = True
@@ -219,7 +242,7 @@ def _dict_repr(d, other):
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._err('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 e8721bc..0e21936 100644
--- a/assertpy/numeric.py
+++ b/assertpy/numeric.py
@@ -32,20 +32,22 @@
import numbers
import datetime
+__tracebackhide__ = True
+
class NumericMixin(object):
"""Numeric assertions mixin."""
- NUMERIC_COMPAREABLE = set([datetime.datetime, datetime.timedelta, datetime.date, datetime.time])
- NUMERIC_NON_COMPAREABLE = set([complex])
+ _NUMERIC_COMPAREABLE = set([datetime.datetime, datetime.timedelta, datetime.date, datetime.time])
+ _NUMERIC_NON_COMPAREABLE = set([complex])
def _validate_compareable(self, other):
self_type = type(self.val)
other_type = type(other)
- if self_type in self.NUMERIC_NON_COMPAREABLE:
+ if self_type in self._NUMERIC_NON_COMPAREABLE:
raise TypeError('ordering is not defined for type <%s>' % self_type.__name__)
- if self_type in self.NUMERIC_COMPAREABLE:
+ if self_type in self._NUMERIC_COMPAREABLE:
if other_type is not self_type:
raise TypeError('given arg must be <%s>, but was <%s>' % (self_type.__name__, other_type.__name__))
return
@@ -66,121 +68,405 @@ def _validate_real(self):
raise TypeError('val is not real number')
def is_zero(self):
- """Asserts that val is numeric and equal to zero."""
+ """Asserts that val is numeric and is zero.
+
+ Examples:
+ Usage::
+
+ assert_that(0).is_zero()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** zero
+ """
self._validate_number()
return self.is_equal_to(0)
def is_not_zero(self):
- """Asserts that val is numeric and not equal to zero."""
+ """Asserts that val is numeric and is *not* zero.
+
+ Examples:
+ Usage::
+
+ assert_that(1).is_not_zero()
+ assert_that(123.4).is_not_zero()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val **is** zero
+ """
self._validate_number()
return self.is_not_equal_to(0)
def is_nan(self):
- """Asserts that val is real number and NaN (not a number)."""
+ """Asserts that val is real number and is ``NaN`` (not a number).
+
+ Examples:
+ Usage::
+
+ assert_that(float('nan')).is_nan()
+ assert_that(float('inf') * 0).is_nan()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** NaN
+ """
self._validate_number()
self._validate_real()
if not math.isnan(self.val):
- self._err('Expected <%s> to be , but was not.' % self.val)
+ return self.error('Expected <%s> to be , but was not.' % self.val)
return self
def is_not_nan(self):
- """Asserts that val is real number and not NaN (not a number)."""
+ """Asserts that val is real number and is *not* ``NaN`` (not a number).
+
+ Examples:
+ Usage::
+
+ assert_that(0).is_not_nan()
+ assert_that(123.4).is_not_nan()
+ assert_that(float('inf')).is_not_nan()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val **is** NaN
+ """
self._validate_number()
self._validate_real()
if math.isnan(self.val):
- self._err('Expected not , but was.')
+ return self.error('Expected not , but was.')
return self
def is_inf(self):
- """Asserts that val is real number and Inf (infinity)."""
+ """Asserts that val is real number and is ``Inf`` (infinity).
+
+ Examples:
+ Usage::
+
+ assert_that(float('inf')).is_inf()
+ assert_that(float('inf') * 1).is_inf()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** Inf
+ """
self._validate_number()
self._validate_real()
if not math.isinf(self.val):
- self._err('Expected <%s> to be , but was not.' % self.val)
+ return self.error('Expected <%s> to be , but was not.' % self.val)
return self
def is_not_inf(self):
- """Asserts that val is real number and not Inf (infinity)."""
+ """Asserts that val is real number and is *not* ``Inf`` (infinity).
+
+ Examples:
+ Usage::
+
+ assert_that(0).is_not_inf()
+ assert_that(123.4).is_not_inf()
+ assert_that(float('nan')).is_not_inf()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val **is** Inf
+ """
self._validate_number()
self._validate_real()
if math.isinf(self.val):
- self._err('Expected not , but was.')
+ return self.error('Expected not , but was.')
return self
def is_greater_than(self, other):
- """Asserts that val is numeric and is greater than other."""
+ """Asserts that val is numeric and is greater than other.
+
+ Args:
+ other: the other date, expected to be less than val
+
+ Examples:
+ Usage::
+
+ assert_that(1).is_greater_than(0)
+ assert_that(123.4).is_greater_than(111.1)
+
+ For dates, behavior is identical to :meth:`~assertpy.date.DateMixin.is_after`::
+
+ import datetime
+
+ today = datetime.datetime.now()
+ yesterday = today - datetime.timedelta(days=1)
+
+ assert_that(today).is_greater_than(yesterday)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** greater than other
+ """
self._validate_compareable(other)
if self.val <= other:
if type(self.val) is datetime.datetime:
- self._err('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')))
+ 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._err('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):
- """Asserts that val is numeric and is greater than or equal to other."""
+ """Asserts that val is numeric and is greater than or equal to other.
+
+ Args:
+ other: the other date, expected to be less than or equal to val
+
+ Examples:
+ Usage::
+
+ assert_that(1).is_greater_than_or_equal_to(0)
+ assert_that(1).is_greater_than_or_equal_to(1)
+ assert_that(123.4).is_greater_than_or_equal_to(111.1)
+
+ For dates, behavior is identical to :meth:`~assertpy.date.DateMixin.is_after` *except* when equal::
+
+ import datetime
+
+ today = datetime.datetime.now()
+ yesterday = today - datetime.timedelta(days=1)
+
+ assert_that(today).is_greater_than_or_equal_to(yesterday)
+ assert_that(today).is_greater_than_or_equal_to(today)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** greater than or equal to other
+ """
self._validate_compareable(other)
if self.val < other:
if type(self.val) is datetime.datetime:
- self._err('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')))
+ 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._err('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):
- """Asserts that val is numeric and is less than other."""
+ """Asserts that val is numeric and is less than other.
+
+ Args:
+ other: the other date, expected to be greater than val
+
+ Examples:
+ Usage::
+
+ assert_that(0).is_less_than(1)
+ assert_that(123.4).is_less_than(555.5)
+
+ For dates, behavior is identical to :meth:`~assertpy.date.DateMixin.is_before`::
+
+ import datetime
+
+ today = datetime.datetime.now()
+ yesterday = today - datetime.timedelta(days=1)
+
+ assert_that(yesterday).is_less_than(today)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** less than other
+ """
self._validate_compareable(other)
if self.val >= other:
if type(self.val) is datetime.datetime:
- self._err('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._err('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):
- """Asserts that val is numeric and is less than or equal to other."""
+ """Asserts that val is numeric and is less than or equal to other.
+
+ Args:
+ other: the other date, expected to be greater than or equal to val
+
+ Examples:
+ Usage::
+
+ assert_that(1).is_less_than_or_equal_to(0)
+ assert_that(1).is_less_than_or_equal_to(1)
+ assert_that(123.4).is_less_than_or_equal_to(100.0)
+
+ For dates, behavior is identical to :meth:`~assertpy.date.DateMixin.is_before` *except* when equal::
+
+ import datetime
+
+ today = datetime.datetime.now()
+ yesterday = today - datetime.timedelta(days=1)
+
+ assert_that(yesterday).is_less_than_or_equal_to(today)
+ assert_that(today).is_less_than_or_equal_to(today)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** less than or equal to other
+ """
self._validate_compareable(other)
if self.val > other:
if type(self.val) is datetime.datetime:
- self._err('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')))
+ 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._err('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):
- """Asserts that val is numeric and greater than zero."""
+ """Asserts that val is numeric and is greater than zero.
+
+ Examples:
+ Usage::
+
+ assert_that(1).is_positive()
+ assert_that(123.4).is_positive()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** positive
+ """
return self.is_greater_than(0)
def is_negative(self):
- """Asserts that val is numeric and less than zero."""
+ """Asserts that val is numeric and is less than zero.
+
+ Examples:
+ Usage::
+
+ assert_that(-1).is_negative()
+ assert_that(-123.4).is_negative()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** negative
+ """
return self.is_less_than(0)
def is_between(self, low, high):
- """Asserts that val is numeric and is between low and high."""
+ """Asserts that val is numeric and is between low and high.
+
+ Args:
+ low: the low value
+ high: the high value
+
+ Examples:
+ Usage::
+
+ assert_that(1).is_between(0, 2)
+ assert_that(123.4).is_between(111.1, 222.2)
+
+ For dates, works as expected::
+
+ import datetime
+
+ today = datetime.datetime.now()
+ middle = today - datetime.timedelta(hours=12)
+ yesterday = today - datetime.timedelta(days=1)
+
+ assert_that(middle).is_between(yesterday, today)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** between low and high
+ """
val_type = type(self.val)
self._validate_between_args(val_type, low, high)
if self.val < low or self.val > high:
if val_type is datetime.datetime:
- self._err('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')))
+ 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._err('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):
- """Asserts that val is numeric and is between low and high."""
+ """Asserts that val is numeric and is *not* between low and high.
+
+ Args:
+ low: the low value
+ high: the high value
+
+ Examples:
+ Usage::
+
+ assert_that(1).is_not_between(2, 3)
+ assert_that(1.1).is_not_between(2.2, 3.3)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val **is** between low and high
+ """
val_type = type(self.val)
self._validate_between_args(val_type, low, high)
if self.val >= low and self.val <= high:
if val_type is datetime.datetime:
- self._err('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')))
+ 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._err('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):
- """Asserts that val is numeric and is close to other within tolerance."""
+ """Asserts that val is numeric and is close to other within tolerance.
+
+ Args:
+ other: the other value, expected to be close to val within tolerance
+ tolerance: the tolerance
+
+ Examples:
+ Usage::
+
+ assert_that(123).is_close_to(100, 25)
+ assert_that(123.4).is_close_to(123, 0.5)
+
+ For dates, works as expected::
+
+ import datetime
+
+ today = datetime.datetime.now()
+ yesterday = today - datetime.timedelta(days=1)
+
+ assert_that(today).is_close_to(yesterday, datetime.timedelta(hours=36))
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** close to other within tolerance
+ """
self._validate_close_to_args(self.val, other, tolerance)
if self.val < (other-tolerance) or self.val > (other+tolerance):
@@ -188,13 +474,31 @@ def is_close_to(self, other, tolerance):
tolerance_seconds = tolerance.days * 86400 + tolerance.seconds + tolerance.microseconds / 1000000
h, rem = divmod(tolerance_seconds, 3600)
m, s = divmod(rem, 60)
- self._err('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))
+ 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._err('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):
- """Asserts that val is numeric and is not close to other within tolerance."""
+ """Asserts that val is numeric and is *not* close to other within tolerance.
+
+ Args:
+ other: the other value
+ tolerance: the tolerance
+
+ Examples:
+ Usage::
+
+ assert_that(123).is_not_close_to(100, 22)
+ assert_that(123.4).is_not_close_to(123, 0.1)
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val **is** close to other within tolerance
+ """
self._validate_close_to_args(self.val, other, tolerance)
if self.val >= (other-tolerance) and self.val <= (other+tolerance):
@@ -202,7 +506,8 @@ def is_not_close_to(self, other, tolerance):
tolerance_seconds = tolerance.days * 86400 + tolerance.seconds + tolerance.microseconds / 1000000
h, rem = divmod(tolerance_seconds, 3600)
m, s = divmod(rem, 60)
- self._err('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))
+ 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._err('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/snapshot.py b/assertpy/snapshot.py
index 6c3ef9f..da331a8 100644
--- a/assertpy/snapshot.py
+++ b/assertpy/snapshot.py
@@ -32,11 +32,92 @@
import inspect
import json
+__tracebackhide__ = True
+
class SnapshotMixin(object):
- """Snapshot mixin."""
+ """Snapshot mixin.
+
+ Take a snapshot of a python data structure, store it on disk in JSON format, and automatically
+ compare the latest data to the stored data on every test run.
+
+ Functional testing (which snapshot testing falls under) is very much blackbox testing. When
+ something goes wrong, it's hard to pinpoint the issue, because functional tests typically
+ provide minimal *isolation* as compared to unit tests. On the plus side, snapshots typically
+ do provide enormous *leverage* as a few well-placed snapshot tests can strongly verify that an
+ application is working. Similar coverage would otherwise require dozens if not hundreds of
+ unit tests.
+
+ **On-disk Format**
+
+ Snapshots are stored in a readable JSON format. For example::
+
+ assert_that({'a': 1, 'b': 2, 'c': 3}).snapshot()
+
+ Would be stored as::
+
+ {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ }
+
+ The JSON formatting support most python data structures (dict, list, object, etc), but not custom
+ binary data.
+
+ **Updating**
+
+ It's easy to update your snapshots...just delete them all and re-run the test suite to regenerate all snapshots.
+
+ Note:
+ Snapshots require Python 3.x
+ """
def snapshot(self, id=None, path='__snapshots'):
+ """Asserts that val is identical to the on-disk snapshot stored previously.
+
+ On the first run of a test before the snapshot file has been saved, a snapshot is created,
+ stored to disk, and the test *always* passes. But on all subsequent runs, val is compared
+ to the on-disk snapshot, and the test fails if they don't match.
+
+ Snapshot artifacts are stored in the ``__snapshots`` directory by default, and should be
+ committed to source control alongside any code changes.
+
+ Snapshots are identified by test filename plus line number by default.
+
+ Args:
+ id: the item or items expected to be contained
+ path: the item or items expected to be contained
+
+ Examples:
+ Usage::
+
+ assert_that(None).snapshot()
+ assert_that(True).snapshot()
+ assert_that(1).snapshot()
+ assert_that(123.4).snapshot()
+ assert_that('foo').snapshot()
+ assert_that([1, 2, 3]).snapshot()
+ assert_that({'a': 1, 'b': 2, 'c': 3}).snapshot()
+ assert_that({'a', 'b', 'c'}).snapshot()
+ assert_that(1 + 2j).snapshot()
+ assert_that(someobj).snapshot()
+
+ By default, snapshots are identified by test filename plus line number. Alternately, you can specify a custom identifier using the ``id`` arg::
+
+ assert_that({'a': 1, 'b': 2, 'c': 3}).snapshot(id='foo-id')
+
+
+ By default, snapshots are stored in the ``__snapshots`` directory. Alternately, you can specify a custom path using the ``path`` arg::
+
+ assert_that({'a': 1, 'b': 2, 'c': 3}).snapshot(path='my-custom-folder')
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val does **not** equal to on-disk snapshot
+ """
if sys.version_info[0] < 3:
raise NotImplementedError('snapshot testing requires Python 3')
@@ -87,7 +168,7 @@ def _load(name):
def _name(path, name):
try:
- return os.path.join(path, 'snap-%s.json' % name.replace(' ','_').lower())
+ return os.path.join(path, 'snap-%s.json' % name.replace(' ', '_').lower())
except Exception:
raise ValueError('failed to create snapshot filename, either bad path or bad name')
diff --git a/assertpy/string.py b/assertpy/string.py
index 7a340e2..be9c866 100644
--- a/assertpy/string.py
+++ b/assertpy/string.py
@@ -39,22 +39,63 @@
unicode = unicode
Iterable = collections.Iterable
+__tracebackhide__ = True
+
class StringMixin(object):
"""String assertions mixin."""
def is_equal_to_ignoring_case(self, other):
- """Asserts that val is case-insensitive equal to other."""
+ """Asserts that val is a string and is case-insensitive equal to other.
+
+ Checks actual is equal to expected using the ``==`` operator and ``str.lower()``.
+
+ Args:
+ other: the expected value
+
+ Examples:
+ Usage::
+
+ assert_that('foo').is_equal_to_ignoring_case('FOO')
+ assert_that('FOO').is_equal_to_ignoring_case('foo')
+ assert_that('fOo').is_equal_to_ignoring_case('FoO')
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if actual is **not** case-insensitive equal to expected
+ """
if not isinstance(self.val, str_types):
raise TypeError('val is not a string')
if not isinstance(other, str_types):
raise TypeError('given arg must be a string')
if self.val.lower() != other.lower():
- self._err('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):
- """Asserts that val is string and contains the given item or items."""
+ """Asserts that val is string and contains the given item or items.
+
+ Walks val and checks for item or items using the ``==`` operator and ``str.lower()``.
+
+ Args:
+ *items: the item or items expected to be contained
+
+ Examples:
+ Usage::
+
+ assert_that('foo').contains_ignoring_case('F', 'oO')
+ assert_that(['a', 'B']).contains_ignoring_case('A', 'b')
+ assert_that({'a': 1, 'B': 2}).contains_ignoring_case('A', 'b')
+ assert_that({'a', 'B'}).contains_ignoring_case('A', 'b')
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val does **not** contain the case-insensitive item or items
+ """
if len(items) == 0:
raise ValueError('one or more args must be given')
if isinstance(self.val, str_types):
@@ -62,7 +103,7 @@ def contains_ignoring_case(self, *items):
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._err('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:
@@ -71,7 +112,8 @@ def contains_ignoring_case(self, *items):
if i.lower() not in self.val.lower():
missing.append(i)
if missing:
- self._err('Expected <%s> to case-insensitive contain items %s, but did not contain %s.' % (self.val, self._fmt_items(items), self._fmt_items(missing)))
+ 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 = []
for i in items:
@@ -87,13 +129,32 @@ def contains_ignoring_case(self, *items):
if not found:
missing.append(i)
if missing:
- self._err('Expected <%s> to case-insensitive contain items %s, but did not contain %s.' % (self.val, self._fmt_items(items), self._fmt_items(missing)))
+ 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')
return self
def starts_with(self, prefix):
- """Asserts that val is string or iterable and starts with prefix."""
+ """Asserts that val is string or iterable and starts with prefix.
+
+ Args:
+ prefix: the prefix
+
+ Examples:
+ Usage::
+
+ assert_that('foo').starts_with('fo')
+ assert_that(['a', 'b', 'c']).starts_with('a')
+ assert_that((1, 2, 3)).starts_with(1)
+ assert_that(((1, 2), (3, 4), (5, 6))).starts_with((1, 2))
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val does **not** start with prefix
+ """
if prefix is None:
raise TypeError('given prefix arg must not be none')
if isinstance(self.val, str_types):
@@ -102,19 +163,37 @@ def starts_with(self, prefix):
if len(prefix) == 0:
raise ValueError('given prefix arg must not be empty')
if not self.val.startswith(prefix):
- self._err('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._err('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
def ends_with(self, suffix):
- """Asserts that val is string or iterable and ends with suffix."""
+ """Asserts that val is string or iterable and ends with suffix.
+
+ Args:
+ suffix: the suffix
+
+ Examples:
+ Usage::
+
+ assert_that('foo').ends_with('oo')
+ assert_that(['a', 'b', 'c']).ends_with('c')
+ assert_that((1, 2, 3)).ends_with(3)
+ assert_that(((1, 2), (3, 4), (5, 6))).ends_with((5, 6))
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val does **not** end with suffix
+ """
if suffix is None:
raise TypeError('given suffix arg must not be none')
if isinstance(self.val, str_types):
@@ -123,7 +202,7 @@ def ends_with(self, suffix):
if len(suffix) == 0:
raise ValueError('given suffix arg must not be empty')
if not self.val.endswith(suffix):
- self._err('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')
@@ -131,13 +210,60 @@ def ends_with(self, suffix):
for last in self.val:
pass
if last != suffix:
- self._err('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
def matches(self, pattern):
- """Asserts that val is string and matches regex pattern."""
+ """Asserts that val is string and matches the given regex pattern.
+
+ Args:
+ pattern (str): the regular expression pattern, as raw string (aka prefixed with ``r``)
+
+ Examples:
+ Usage::
+
+ assert_that('foo').matches(r'\\w')
+ assert_that('123-456-7890').matches(r'\\d{3}-\\d{3}-\\d{4}')
+
+ Match is partial unless anchored, so these assertion pass::
+
+ assert_that('foo').matches(r'\\w')
+ assert_that('foo').matches(r'oo')
+ assert_that('foo').matches(r'\\w{2}')
+
+ To match the entire string, just use an anchored regex pattern where ``^`` and ``$``
+ match the start and end of line and ``\\A`` and ``\\Z`` match the start and end of string::
+
+ assert_that('foo').matches(r'^\\w{3}$')
+ assert_that('foo').matches(r'\\A\\w{3}\\Z')
+
+ And regex flags, such as ``re.MULTILINE`` and ``re.DOTALL``, can only be applied via
+ *inline modifiers*, such as ``(?m)`` and ``(?s)``::
+
+ s = '''bar
+ foo
+ baz'''
+
+ # using multiline (?m)
+ assert_that(s).matches(r'(?m)^foo$')
+
+ # using dotall (?s)
+ assert_that(s).matches(r'(?s)b(.*)z')
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val does **not** match pattern
+
+ Tip:
+ Regular expressions are tricky. Be sure to use raw strings (aka prefixed with ``r``).
+ Also, note that the :meth:`matches` assertion passes for partial matches (as does the
+ underlying ``re.match`` method). So, if you need to match the entire string, you must
+ include anchors in the regex pattern.
+ """
if not isinstance(self.val, str_types):
raise TypeError('val is not a string')
if not isinstance(pattern, str_types):
@@ -145,11 +271,30 @@ def matches(self, pattern):
if len(pattern) == 0:
raise ValueError('given pattern arg must not be empty')
if re.search(pattern, self.val) is None:
- self._err('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):
- """Asserts that val is string and does not match regex pattern."""
+ """Asserts that val is string and does not match the given regex pattern.
+
+ Args:
+ pattern (str): the regular expression pattern, as raw string (aka prefixed with ``r``)
+
+ Examples:
+ Usage::
+
+ assert_that('foo').does_not_match(r'\\d+')
+ assert_that('123').does_not_match(r'\\w+')
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val **does** match pattern
+
+ See Also:
+ :meth:`matches` - for more about regex patterns
+ """
if not isinstance(self.val, str_types):
raise TypeError('val is not a string')
if not isinstance(pattern, str_types):
@@ -157,51 +302,112 @@ def does_not_match(self, pattern):
if len(pattern) == 0:
raise ValueError('given pattern arg must not be empty')
if re.search(pattern, self.val) is not None:
- self._err('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):
- """Asserts that val is non-empty string and all characters are alphabetic."""
+ """Asserts that val is non-empty string and all characters are alphabetic (using ``str.isalpha()``).
+
+ Examples:
+ Usage::
+
+ assert_that('foo').is_alpha()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** alphabetic
+ """
if not isinstance(self.val, str_types):
raise TypeError('val is not a string')
if len(self.val) == 0:
raise ValueError('val is empty')
if not self.val.isalpha():
- self._err('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):
- """Asserts that val is non-empty string and all characters are digits."""
+ """Asserts that val is non-empty string and all characters are digits (using ``str.isdigit()``).
+
+ Examples:
+ Usage::
+
+ assert_that('1234567890').is_digit()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** digits
+ """
if not isinstance(self.val, str_types):
raise TypeError('val is not a string')
if len(self.val) == 0:
raise ValueError('val is empty')
if not self.val.isdigit():
- self._err('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):
- """Asserts that val is non-empty string and all characters are lowercase."""
+ """Asserts that val is non-empty string and all characters are lowercase (using ``str.lower()``).
+
+ Examples:
+ Usage::
+
+ assert_that('foo').is_lower()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** lowercase
+ """
if not isinstance(self.val, str_types):
raise TypeError('val is not a string')
if len(self.val) == 0:
raise ValueError('val is empty')
if self.val != self.val.lower():
- self._err('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):
- """Asserts that val is non-empty string and all characters are uppercase."""
+ """Asserts that val is non-empty string and all characters are uppercase (using ``str.upper()``).
+
+ Examples:
+ Usage::
+
+ assert_that('FOO').is_upper()
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** uppercase
+ """
if not isinstance(self.val, str_types):
raise TypeError('val is not a string')
if len(self.val) == 0:
raise ValueError('val is empty')
if self.val != self.val.upper():
- self._err('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):
- """Asserts that val is a unicode string."""
+ """Asserts that val is a unicode string.
+
+ Examples:
+ Usage::
+
+ assert_that(u'foo').is_unicode() # python 2
+ assert_that('foo').is_unicode() # python 3
+
+ Returns:
+ AssertionBuilder: returns this instance to chain to the next assertion
+
+ Raises:
+ AssertionError: if val is **not** a unicode string
+ """
if type(self.val) is not unicode:
- self._err('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/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..d7ce395
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,37 @@
+# assertpy docs
+
+Documentation is mostly written inline using Python docstrings that are in
+[Google Python Style Guide](https://google.github.io/styleguide/pyguide.html) format.
+[Sphinx](https://www.sphinx-doc.org/) and the
+[Napoleon extension](https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html)
+are used to generate html.
+
+## Setup
+
+Install `sphinx` via pip...
+
+```
+pip install sphinx
+pip install sphinxcontrib-napoleon
+```
+
+And checkout the [assertpy.github.io](https://github.com/assertpy/assertpy.github.io)
+repo as a sibling to `assertpy`.
+
+```
+cd ..
+git clone git@github.com:assertpy/assertpy.github.io.git
+```
+
+## Build
+
+To build the docs, run:
+
+```
+cd docs/
+./build.sh
+```
+
+This generates `docs.html` and copies is directly into the sibling
+`assertpy.github.io` repo.
+
diff --git a/docs/assertpy.rst b/docs/assertpy.rst
new file mode 100644
index 0000000..983ac11
--- /dev/null
+++ b/docs/assertpy.rst
@@ -0,0 +1,112 @@
+assertpy
+--------
+
+.. automodule:: assertpy.assertpy
+ :members:
+ :undoc-members:
+ :show-inheritance:
+ :exclude-members: WarningLoggingAdapter
+
+base
+----
+
+.. automodule:: assertpy.base
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+collection
+----------
+
+.. automodule:: assertpy.collection
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+contains
+--------
+
+.. automodule:: assertpy.contains
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+date
+----
+
+.. automodule:: assertpy.date
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+dict
+----
+
+.. automodule:: assertpy.dict
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+dynamic
+-------
+
+.. automodule:: assertpy.dynamic
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+exception
+---------
+
+.. automodule:: assertpy.exception
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+extracting
+----------
+
+.. automodule:: assertpy.extracting
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+file
+----
+
+.. automodule:: assertpy.file
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+helpers
+-------
+
+.. automodule:: assertpy.helpers
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+numeric
+-------
+
+.. automodule:: assertpy.numeric
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+snapshot
+--------
+
+.. automodule:: assertpy.snapshot
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+string
+------
+
+.. automodule:: assertpy.string
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/build.sh b/docs/build.sh
new file mode 100755
index 0000000..17ead2a
--- /dev/null
+++ b/docs/build.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+sphinx-build -b html . build
+python fixup.py
+cp out/docs.html ../../assertpy.github.io
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..5447989
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,24 @@
+# fixup path
+import os
+import sys
+sys.path.insert(0, os.path.abspath('..'))
+print('SYS.PATH=', sys.path)
+
+# proj info
+project = 'assertpy'
+copyright = '2015-2019 Activision Publishing, Inc.'
+author = 'Activision'
+
+# extensions (for Google-style doc strings)
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.napoleon'
+]
+
+templates_path = ['templates']
+
+exclude_patterns = ['build', '.DS_Store']
+
+# html_theme = 'sphinx_rtd_theme'
+
+add_module_names = False
diff --git a/docs/fixup.py b/docs/fixup.py
new file mode 100644
index 0000000..433c791
--- /dev/null
+++ b/docs/fixup.py
@@ -0,0 +1,70 @@
+import os
+import re
+
+
+# conf
+PROJECT = 'assertpy'
+SRC_DIR = os.path.join(os.getcwd(), 'build')
+SRC = os.path.join(SRC_DIR, 'assertpy.html')
+OUT_DIR = os.path.join(os.getcwd(), 'out')
+OUT = os.path.join(OUT_DIR, 'docs.html')
+
+
+def load(filename):
+ with open(filename, 'r') as fp:
+ return fp.read()
+
+
+def save(target, contents):
+ with open(target, 'w') as fp:
+ fp.write(contents)
+
+
+if __name__ == '__main__':
+ print('\nFIXUP')
+ print(f' src={SRC}')
+ print(f' out={OUT}')
+
+ if not os.path.exists(OUT_DIR):
+ os.makedirs(OUT_DIR)
+
+ if not os.path.isfile(SRC):
+ print(f'bad src filename {SRC}')
+
+ html = load(SRC)
+ html = html.replace('
', '
')
+
+ html = html.replace('"admonition-title">Tip
\n
', '"message-header">Tip
\n
')
+ html = html.replace('"admonition-title">Note
\n
', '"message-header">Note
\n
')
+ html = html.replace('"admonition-title">See also
\n
', '"message-header">See Also
\n
')
+ html = html.replace('"admonition tip"', '"message is-primary"')
+ html = html.replace('"admonition note"', '"message is-info"')
+ html = html.replace('"admonition seealso"', '"message is-link"')
+
+ html = html.replace('
', '
')
+ html = html.replace('
\n
', '')
+
+ html = html.replace('class="code">2019',
+ 'class="code">2019')
+ html = html.replace('class="code">AssertionError:',
+ 'class="code">AssertionError:')
+ html = html.replace('class="code">AssertionError: soft assertion failures',
+ 'class="code">AssertionError: soft assertion failures')
+
+ html = html.replace('
Usage:', '')
+ html = html.replace('BR', ' ')
+
+ margin = 'style="margin:0.2em 0;"'
+ html = html.replace('