Skip to content

Exception & callable assertions

Assertions for callables: expected exceptions and the value returned by a call.

Expected exception mixin.

raises

raises(ex: type) -> Self

Asserts that val is callable and set the expected exception.

Just sets the expected exception, but never calls val, and therefore never fails. You must chain to when_called_with() to invoke val().

Parameters:

Name Type Description Default
ex type

the expected exception

required

Examples:

Usage:

assert_that(some_func).raises(RuntimeError).when_called_with('foo')

Returns:

Name Type Description
AssertionBuilder Self

returns a new instance (with the expected exception) to chain the next assertion

Source code in assertpy2/exception.py
def raises(self, ex: type) -> Self:
    """Asserts that val is callable and set the expected exception.

    Just sets the expected exception, but never calls val, and therefore never fails. You must
    chain to [`when_called_with()`][assertpy2.exception.ExceptionMixin.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 (with the expected exception) to chain the next assertion
    """
    if not callable(self.val):
        refuse(self.val, "callable")
    expected = _require_exception_type(ex)

    return self.builder(self.val, self.description, self.kind, expected, self.logger)

when_called_with

when_called_with(
    *some_args: object, **some_kwargs: object
) -> Self

Asserts that val, when invoked with the given args and kwargs, meets the set expectation.

Invokes val() with the given args and kwargs. You must first set an expectation with raises() or does_not_raise() (expected exception), or with warns() or does_not_warn() (expected warning).

Only what happens before the call returns is judged. A call that hands back an awaitable or an async generator is refused with TypeError, since what it does when awaited or iterated comes later. A plain generator is judged as returned: its body runs only when it is consumed.

Parameters:

Name Type Description Default
*some_args object

the args to call val()

()
**some_kwargs object

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:

Name Type Description
AssertionBuilder Self

returns a new instance (now with the captured exception or warning message as the val) to chain to the next assertion

Raises:

Type Description
AssertionError

if val does not meet the set expectation

TypeError

if no expectation set first

Source code in assertpy2/exception.py
def when_called_with(self, *some_args: object, **some_kwargs: object) -> Self:
    """Asserts that val, when invoked with the given args and kwargs, meets the set expectation.

    Invokes ``val()`` with the given args and kwargs.  You must first set an expectation with
    [`raises()`][assertpy2.exception.ExceptionMixin.raises] or
    [`does_not_raise()`][assertpy2.exception.ExceptionMixin.does_not_raise] (expected exception),
    or with
    [`warns()`][assertpy2.warning.WarningMixin.warns] or
    [`does_not_warn()`][assertpy2.warning.WarningMixin.does_not_warn] (expected warning).

    Only what happens before the call returns is judged.  A call that hands back an awaitable or an
    async generator is refused with ``TypeError``, since what it does when awaited or iterated comes
    later.  A plain generator is judged as returned: its body runs only when it is consumed.

    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 or warning
            message as the val) to chain to the next assertion

    Raises:
        AssertionError: if val does **not** meet the set expectation
        TypeError: if no expectation set first
    """
    if self._expected_warning is not None:
        if self._not_expected:
            return self._when_called_with_not_warning(self._expected_warning, *some_args, **some_kwargs)
        return self._when_called_with_warning(self._expected_warning, *some_args, **some_kwargs)

    if not self.expected:
        raise TypeError("no expectation set; call raises(), warns() or a does_not_* method first")

    if getattr(self, "_not_expected", False):
        return self._when_called_with_not_expected(*some_args, **some_kwargs)

    try:
        result = self.val(*some_args, **some_kwargs)
    except BaseException as e:
        if issubclass(type(e), self.expected):
            captured = self.builder(_safe_str(e), self.description, self.kind, logger=self.logger)
            captured._raised_exception = e
            return captured
        elif _escaped(e):
            raise
        else:
            self.error(
                f"Expected <{_callable_name(self.val)}> to raise <{self.expected.__name__}>"
                f" when called with ({self._fmt_args_kwargs(*some_args, **some_kwargs)}),"
                f" but raised <{type(e).__name__}>.",
                expected=self.expected,
            )
            return cast("Self", _InertBuilder(self._value_taint_reason))

    _require_synchronous_result(result, self.val)
    self.error(
        f"Expected <{_callable_name(self.val)}> to raise <{self.expected.__name__}>"
        f" when called with ({self._fmt_args_kwargs(*some_args, **some_kwargs)}).",
        expected=self.expected,
    )
    return cast("Self", _InertBuilder(self._value_taint_reason))

returned

returned() -> Self

Pivots the chain to the value val() returned during when_called_with().

Use after a call that completed normally (warns(), does_not_warn(), or does_not_raise()) to assert on the return value in the same chain.

Examples:

Usage:

assert_that(make_client).warns(DeprecationWarning).when_called_with().returned().is_instance_of(Client)
assert_that(adder).does_not_raise(TypeError).when_called_with(1, 2).returned().is_equal_to(3)

Returns:

Name Type Description
AssertionBuilder Self

a new instance wrapping the captured return value

Raises:

Type Description
TypeError

if no return value was captured (the call raised, or when_called_with() was not invoked first)

Source code in assertpy2/exception.py
def returned(self) -> Self:
    """Pivots the chain to the value ``val()`` returned during
    [`when_called_with()`][assertpy2.exception.ExceptionMixin.when_called_with].

    Use after a call that completed normally ([`warns()`][assertpy2.warning.WarningMixin.warns],
    [`does_not_warn()`][assertpy2.warning.WarningMixin.does_not_warn], or
    [`does_not_raise()`][assertpy2.exception.ExceptionMixin.does_not_raise]) to assert
    on the return value in the same chain.

    Examples:
        Usage:

            assert_that(make_client).warns(DeprecationWarning).when_called_with().returned().is_instance_of(Client)
            assert_that(adder).does_not_raise(TypeError).when_called_with(1, 2).returned().is_equal_to(3)

    Returns:
        AssertionBuilder: a new instance wrapping the captured return value

    Raises:
        TypeError: if no return value was captured (the call raised, or
            [`when_called_with()`][assertpy2.exception.ExceptionMixin.when_called_with]
            was not invoked first)
    """
    if self._return_value is _UNSET:
        raise TypeError("no return value captured; returned() is only valid after a call that completed normally")
    return self.builder(self._return_value, self.description, self.kind, logger=self.logger)

raised

raised() -> Self

Pivots the chain to the exception object caught by when_called_with(), to assert on its type, args, or custom attributes - not only its message string.

Examples:

Usage:

err = assert_that(load).raises(ConfigError).when_called_with("bad").raised().value
assert_that(err.code).is_equal_to(42)

Returns:

Name Type Description
AssertionBuilder Self

a new instance wrapping the caught exception object

Raises:

Type Description
TypeError

if no exception was captured (the call did not raise, or when_called_with() was not invoked first)

Source code in assertpy2/exception.py
def raised(self) -> Self:
    """Pivots the chain to the exception object caught by
    [`when_called_with()`][assertpy2.exception.ExceptionMixin.when_called_with], to assert on its type,
    ``args``, or custom attributes - not only its message string.

    Examples:
        Usage:

            err = assert_that(load).raises(ConfigError).when_called_with("bad").raised().value
            assert_that(err.code).is_equal_to(42)

    Returns:
        AssertionBuilder: a new instance wrapping the caught exception object

    Raises:
        TypeError: if no exception was captured (the call did not raise, or
            [`when_called_with()`][assertpy2.exception.ExceptionMixin.when_called_with] was not invoked first)
    """
    exc = self._require_raised("raised")
    return self.builder(exc, self.description, self.kind, logger=self.logger)

caused_by

caused_by(ex: ClassInfo) -> Self

Asserts the caught exception was chained from a cause of type ex (raise ... from, or an exception raised during handling), then pivots the chain to that cause's message.

Examples:

Usage:

assert_that(save).raises(ServiceError).when_called_with(row).caused_by(TimeoutError)

Parameters:

Name Type Description Default
ex ClassInfo

the expected cause type, or a union or tuple of them as isinstance takes

required

Returns:

Name Type Description
AssertionBuilder Self

a new instance wrapping the cause's message (chain on it, or walk deeper)

Raises:

Type Description
TypeError

if ex holds anything isinstance cannot take, checked member by member

Source code in assertpy2/exception.py
def caused_by(self, ex: ClassInfo) -> Self:
    """Asserts the caught exception was chained from a cause of type ``ex`` (``raise ... from``, or an
    exception raised during handling), then pivots the chain to that cause's message.

    Examples:
        Usage:

            assert_that(save).raises(ServiceError).when_called_with(row).caused_by(TimeoutError)

    Args:
        ex: the expected cause type, or a union or tuple of them as `isinstance` takes

    Returns:
        AssertionBuilder: a new instance wrapping the cause's message (chain on it, or walk deeper)

    Raises:
        TypeError: if ``ex`` holds anything `isinstance` cannot take, checked member by member
    """
    exc = self._require_raised("caused_by")
    cause = _effective_cause(exc)
    _require_class_info(ex, name="exception", probe=cause)
    if cause is None or not isinstance(cause, ex):
        found = "no cause" if cause is None else f"<{type(cause).__name__}>"
        expected_name = _type_expression_name(ex)
        self.error(
            f"Expected <{type(exc).__name__}> to be caused by <{expected_name}>, but the cause was {found}.",
            expected=ex,
        )
        return cast("Self", _InertBuilder(self._value_taint_reason))
    pivoted = self.builder(_safe_str(cause), self.description, self.kind, logger=self.logger)
    pivoted._raised_exception = cause
    return pivoted

has_root_cause

has_root_cause(ex: ClassInfo) -> Self

Asserts the root of the caught exception's cause chain is of type ex, then pivots the chain to that root cause's message.

Parameters:

Name Type Description Default
ex ClassInfo

the expected root-cause type, or a union or tuple of them as isinstance takes

required

Returns:

Name Type Description
AssertionBuilder Self

a new instance wrapping the root cause's message

Raises:

Type Description
TypeError

if ex holds anything isinstance cannot take, checked member by member

Source code in assertpy2/exception.py
def has_root_cause(self, ex: ClassInfo) -> Self:
    """Asserts the *root* of the caught exception's cause chain is of type ``ex``, then pivots the chain
    to that root cause's message.

    Args:
        ex: the expected root-cause type, or a union or tuple of them as `isinstance` takes

    Returns:
        AssertionBuilder: a new instance wrapping the root cause's message

    Raises:
        TypeError: if ``ex`` holds anything `isinstance` cannot take, checked member by member
    """
    exc = self._require_raised("has_root_cause")
    root = exc
    seen = {id(root)}
    while (nxt := _effective_cause(root)) is not None and id(nxt) not in seen:
        root = nxt
        seen.add(id(root))
    _require_class_info(ex, name="exception", probe=root)
    if not isinstance(root, ex):
        self.error(
            f"Expected <{type(exc).__name__}> to have root cause <{_type_expression_name(ex)}>,"
            f" but the root cause was <{type(root).__name__}>.",
            expected=ex,
        )
        return cast("Self", _InertBuilder(self._value_taint_reason))
    pivoted = self.builder(_safe_str(root), self.description, self.kind, logger=self.logger)
    pivoted._raised_exception = root
    return pivoted

contains_error

contains_error(*ex_types: type) -> Self

Asserts the caught exception is an exception group that contains, recursively, an exception of each given type (for raises(ExceptionGroup)).

A caught exception that is not a group fails with not_ as well: negation inverts what the group holds, never whether there is a group.

Examples:

Usage:

assert_that(run_tasks).raises(ExceptionGroup).when_called_with().contains_error(ValueError, KeyError)

Parameters:

Name Type Description Default
*ex_types type

the exception types the group must contain

()

Returns:

Name Type Description
AssertionBuilder Self

this instance, to chain further assertions on the group

Raises:

Type Description
ValueError

if called with no types at all, which nothing could fail

TypeError

if given anything but an exception class

Source code in assertpy2/exception.py
def contains_error(self, *ex_types: type) -> Self:
    """Asserts the caught exception is an exception group that contains, recursively, an exception of
    each given type (for [`raises(ExceptionGroup)`][assertpy2.exception.ExceptionMixin.raises]).

    A caught exception that is not a group fails with ``not_`` as well: negation inverts what the group
    holds, never whether there is a group.

    Examples:
        Usage:

            assert_that(run_tasks).raises(ExceptionGroup).when_called_with().contains_error(ValueError, KeyError)

    Args:
        *ex_types: the exception types the group must contain

    Returns:
        AssertionBuilder: this instance, to chain further assertions on the group

    Raises:
        ValueError: if called with no types at all, which nothing could fail
        TypeError: if given anything but an exception class
    """
    if len(ex_types) == 0:
        raise ValueError("one or more args must be given")
    for ex in ex_types:
        _require_exception_type(ex)
    exc = self._require_group("contains_error")
    if exc is None:
        return cast("Self", _InertBuilder(self._value_taint_reason))
    for ex in ex_types:
        if _first_of(exc, ex) is None:
            self.error(
                f"Expected the raised exception group to contain <{ex.__name__}>, but it did not.", expected=ex
            )
            return cast("Self", _InertBuilder(self._value_taint_reason))
    return self

does_not_contain_error

does_not_contain_error(*ex_types: type) -> Self

Asserts the caught exception group holds none of the given types, at any depth.

The none-of counterpart to contains_error() rather than its negation: that one asks for every type given, this one refuses every type given. With several arguments both can fail on the same group, which is what "some but not all" means. A caught exception that is not a group fails it with or without not_.

Examples:

Usage:

assert_that(run_tasks).raises(ExceptionGroup).when_called_with().does_not_contain_error(KeyError)

Parameters:

Name Type Description Default
*ex_types type

the exception types the group must not contain

()

Returns:

Name Type Description
AssertionBuilder Self

this instance, to chain further assertions on the group

Raises:

Type Description
ValueError

if called with no types at all, which nothing could fail

TypeError

if given anything but an exception class

Source code in assertpy2/exception.py
def does_not_contain_error(self, *ex_types: type) -> Self:
    """Asserts the caught exception group holds none of the given types, at any depth.

    The none-of counterpart to [`contains_error()`][assertpy2.exception.ExceptionMixin.contains_error]
    rather than its negation: that one asks for every type given, this one refuses every type given.
    With several arguments both can fail on the same group, which is what "some but not all" means.
    A caught exception that is not a group fails it with or without ``not_``.

    Examples:
        Usage:

            assert_that(run_tasks).raises(ExceptionGroup).when_called_with().does_not_contain_error(KeyError)

    Args:
        *ex_types: the exception types the group must not contain

    Returns:
        AssertionBuilder: this instance, to chain further assertions on the group

    Raises:
        ValueError: if called with no types at all, which nothing could fail
        TypeError: if given anything but an exception class
    """
    if len(ex_types) == 0:
        raise ValueError("one or more args must be given")
    for ex in ex_types:
        _require_exception_type(ex)
    exc = self._require_group("does_not_contain_error")
    if exc is None:
        return cast("Self", _InertBuilder(self._value_taint_reason))
    for ex in ex_types:
        if _first_of(exc, ex) is not None:
            self.error(f"Expected the raised exception group to not contain <{ex.__name__}>, but it did.")
            return cast("Self", _InertBuilder(self._value_taint_reason))
    return self

errors

errors() -> Self

Pivots the chain to the list of leaf exceptions in the caught group, nested ones flattened.

Flattened rather than one level deep, because that is what contains_error() already searches: a group holding a group is an implementation detail of whoever raised it, and a suite asking "what failed" means the leaves. The view this was reached from still holds the group, so raised() on that answers with the whole tree when the shape itself is the point. It is not offered on the leaves, which are a collection.

Examples:

Usage:

assert_that(run_tasks).raises(ExceptionGroup).when_called_with().errors().is_length(2)
assert_that(run_tasks).raises(ExceptionGroup).when_called_with().errors().extracting(
    "args"
).contains(("bad id",))

Returns:

Name Type Description
AssertionBuilder Self

a new instance wrapping the leaves as a list, to ask a collection anything

Raises:

Type Description
TypeError

if no exception was captured

Source code in assertpy2/exception.py
def errors(self) -> Self:
    """Pivots the chain to the list of leaf exceptions in the caught group, nested ones flattened.

    Flattened rather than one level deep, because that is what
    [`contains_error()`][assertpy2.exception.ExceptionMixin.contains_error] already searches: a group
    holding a group is an implementation detail of whoever raised it, and a suite asking "what
    failed" means the leaves.  The view this was reached from still holds the group, so
    [`raised()`][assertpy2.exception.ExceptionMixin.raised] on *that* answers with the whole tree when
    the shape itself is the point.  It is not offered on the leaves, which are a collection.

    Examples:
        Usage:

            assert_that(run_tasks).raises(ExceptionGroup).when_called_with().errors().is_length(2)
            assert_that(run_tasks).raises(ExceptionGroup).when_called_with().errors().extracting(
                "args"
            ).contains(("bad id",))

    Returns:
        AssertionBuilder: a new instance wrapping the leaves as a list, to ask a collection anything

    Raises:
        TypeError: if no exception was captured
    """
    exc = self._require_group("errors")
    if exc is None:
        return cast("Self", _InertBuilder(self._value_taint_reason))
    return self.builder(_leaves(exc), self.description, self.kind, logger=self.logger)

error_of

error_of(ex: type) -> Self

Asserts the caught group holds an exception of type ex, then pivots to that one's message.

The step contains_error() cannot take: after it the chain still holds the group's message, so asking what one failure said meant reaching into the tree by hand.

Both search the same exceptions, groups included, so whatever one finds the other pivots to. Asking for a group type therefore answers with that group, and for the outermost one that is the message the chain already held. A caught exception that is not a group fails it with or without not_.

Examples:

Usage:

assert_that(run_tasks).raises(ExceptionGroup).when_called_with().error_of(ValueError).contains("bad id")

Parameters:

Name Type Description Default
ex type

the type to pivot to, the first one found, the group itself before its members

required

Returns:

Name Type Description
AssertionBuilder Self

a new instance wrapping that exception's message (chain on it, or raised() to reach the object itself)

Raises:

Type Description
TypeError

if given anything but an exception class

Source code in assertpy2/exception.py
def error_of(self, ex: type) -> Self:
    """Asserts the caught group holds an exception of type ``ex``, then pivots to that one's message.

    The step [`contains_error()`][assertpy2.exception.ExceptionMixin.contains_error] cannot take: after
    it the chain still holds the *group's* message, so asking what one failure said meant reaching into
    the tree by hand.

    Both search the same exceptions, groups included, so whatever one finds the other pivots to.  Asking
    for a group type therefore answers with that group, and for the outermost one that is the message
    the chain already held.  A caught exception that is not a group fails it with or without ``not_``.

    Examples:
        Usage:

            assert_that(run_tasks).raises(ExceptionGroup).when_called_with().error_of(ValueError).contains("bad id")

    Args:
        ex: the type to pivot to, the first one found, the group itself before its members

    Returns:
        AssertionBuilder: a new instance wrapping that exception's message (chain on it, or
            `raised()` to reach the object itself)

    Raises:
        TypeError: if given anything but an exception class
    """
    _require_exception_type(ex)
    exc = self._require_group("error_of")
    if exc is None:
        return cast("Self", _InertBuilder(self._value_taint_reason))
    found = _first_of(exc, ex)
    if found is None:
        self.error(f"Expected the raised exception group to contain <{ex.__name__}>, but it did not.", expected=ex)
        return cast("Self", _InertBuilder(self._value_taint_reason))
    pivoted = self.builder(_safe_str(found), self.description, self.kind, logger=self.logger)
    pivoted._raised_exception = found
    return pivoted

matches_error_tree

matches_error_tree(*expected: type | list[Any]) -> Self

Asserts the caught group holds exactly these exceptions, in exactly this nesting.

The question contains_error() does not ask. That one searches the whole tree for a type and says nothing about how many others there are or how deep it sat, so a group that grew an extra failure, or one whose members moved into a subgroup, still passes it. This one reads the shape.

A list is a subgroup and a type is one exception, so (ValueError, [KeyError]) means a group of two members whose second is a subgroup holding one KeyError. A type matches by isinstance, the way the rest of the family does, so Exception matches a subgroup node as readily as a leaf, and stops there: only a nested list looks inside one, and what a matched subgroup holds is unsaid.

Order is not part of the shape. Whoever raised the group usually did not choose it: an asyncio.TaskGroup reports its failures in the order the tasks finished. A caught exception that is not a group fails it with or without not_.

Examples:

Usage:

assert_that(run_tasks).raises(ExceptionGroup).when_called_with().matches_error_tree(
    ValueError, [KeyError, KeyError]
)

Parameters:

Name Type Description Default
*expected type | list[Any]

one entry per member of the group, a type for an exception and a list for a subgroup

()

Returns:

Name Type Description
AssertionBuilder Self

this instance, to chain further assertions on the group

Raises:

Type Description
ValueError

if called with no entries at all, as the rest of the family does

TypeError

if an entry is neither an exception class nor a list, or is an empty list, which is a group with no members and cannot be raised

Source code in assertpy2/exception.py
def matches_error_tree(self, *expected: type | list[Any]) -> Self:
    """Asserts the caught group holds exactly these exceptions, in exactly this nesting.

    The question [`contains_error()`][assertpy2.exception.ExceptionMixin.contains_error] does not ask.
    That one searches the whole tree for a type and says nothing about how many others there are or
    how deep it sat, so a group that grew an extra failure, or one whose members moved into a
    subgroup, still passes it.  This one reads the shape.

    A `list` is a subgroup and a type is one exception, so `(ValueError, [KeyError])` means a group of
    two members whose second is a subgroup holding one `KeyError`.  A type matches by `isinstance`,
    the way the rest of the family does, so `Exception` matches a subgroup node as readily as a leaf,
    and stops there: only a nested list looks inside one, and what a matched subgroup holds is unsaid.

    Order is not part of the shape.  Whoever raised the group usually did not choose it: an
    `asyncio.TaskGroup` reports its failures in the order the tasks finished.  A caught exception that
    is not a group fails it with or without ``not_``.

    Examples:
        Usage:

            assert_that(run_tasks).raises(ExceptionGroup).when_called_with().matches_error_tree(
                ValueError, [KeyError, KeyError]
            )

    Args:
        *expected: one entry per member of the group, a type for an exception and a list for a
            subgroup

    Returns:
        AssertionBuilder: this instance, to chain further assertions on the group

    Raises:
        ValueError: if called with no entries at all, as the rest of the family does
        TypeError: if an entry is neither an exception class nor a list, or is an empty list, which
            is a group with no members and cannot be raised
    """
    if not expected:
        raise ValueError("one or more args must be given")
    spec = [_shaped(entry, f"[{index}]") for index, entry in enumerate(expected)]
    exc = self._require_group("matches_error_tree")
    if exc is None:
        return cast("Self", _InertBuilder(self._value_taint_reason))
    if not _matches_shape(spec, exc.exceptions):
        names = _naming(spec, exc)
        self.error(
            f"Expected the raised exception group to match <{_spec_shape(spec, names)}>,"
            f" but it was <{_shape_of(exc, names)}>.",
            expected=spec,
        )
        return cast("Self", _InertBuilder(self._value_taint_reason))
    return self

does_not_raise

does_not_raise(ex: type) -> Self

Asserts that val is callable and sets the not-expected exception.

Just sets the not-expected exception, but never calls val. You must chain to when_called_with() to invoke val().

Parameters:

Name Type Description Default
ex type

the exception that should not be raised

required

Examples:

Usage:

assert_that(some_func).does_not_raise(RuntimeError).when_called_with('foo')

Returns:

Name Type Description
AssertionBuilder Self

returns a new instance to chain to the next assertion

Source code in assertpy2/exception.py
def does_not_raise(self, ex: type) -> Self:
    """Asserts that val is callable and sets the not-expected exception.

    Just sets the not-expected exception, but never calls val. You must
    chain to [`when_called_with()`][assertpy2.exception.ExceptionMixin.when_called_with] to invoke ``val()``.

    Args:
        ex: the exception that should **not** be raised

    Examples:
        Usage:

            assert_that(some_func).does_not_raise(RuntimeError).when_called_with('foo')

    Returns:
        AssertionBuilder: returns a new instance to chain to the next assertion
    """
    if not callable(self.val):
        refuse(self.val, "callable")
    unwanted = _require_exception_type(ex)

    new_builder = self.builder(self.val, self.description, self.kind, unwanted, self.logger)
    new_builder._not_expected = True
    return new_builder