def with_roles(obj=None, rw=None, call=None, read=None, write=None):
    """
    Convenience function and decorator to define roles on an attribute. Only
    works with :class:`RoleMixin`, which reads the annotations made by this
    function and populates :attr:`~RoleMixin.__roles__`.

    Examples::

        id = db.Column(Integer, primary_key=True)
        with_roles(id, read={'all'})

        @with_roles(read={'all'})
        @hybrid_property
        def url_id(self):
            return str(self.id)

    When used with properties, with_roles must always be applied after the
    property is fully described::

        @property
        def title(self):
            return self._title

        @title.setter
        def title(self, value):
            self._title = value

        # Either of the following is fine, since with_roles annotates objects
        # instead of wrapping them. The return value can be discarded if it's
        # already present on the host object:

        with_roles(title, read={'all'}, write={'owner', 'editor'})
        title = with_roles(title, read={'all'}, write={'owner', 'editor'})


    :param set rw: Roles which get read and write access to the decorated
        attribute
    :param set call: Roles which get call access to the decorated method
    :param set read: Roles which get read access to the decorated attribute
    :param set write: Roles which get write access to the decorated attribute
    """
    # Convert lists and None values to sets
    rw = set(rw) if rw else set()
    call = set(call) if call else set()
    read = set(read) if read else set()
    write = set(write) if write else set()
    # `rw` is shorthand for read+write
    read.update(rw)
    write.update(rw)

    def inner(attr):
        __cache__[attr] = {'call': call, 'read': read, 'write': write}
        try:
            attr._coaster_roles = {'call': call, 'read': read, 'write': write}
            # If the attr has a restrictive __slots__, we'll get an attribute error.
            # Unfortunately, because of the way SQLAlchemy works, by copying objects
            # into subclasses, the cache alone is not a reliable mechanism. We need both.
        except AttributeError:
            pass
        return attr

    if is_collection(obj):
        # Protect against accidental specification of roles instead of an object
        raise TypeError('Roles must be specified as named parameters')
    elif obj is not None:
        return inner(obj)
    else:
        return inner