docs: 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