docs: def lift(obj, memo=None):
      """Make a promise out of object `obj`, where `obj` may contain promises
      internally.
  
      :param obj: Any object.
      :param memo: used for internal caching (similar to :func:`deepcopy`).
  
      If the object is a :class:`PromisedObject`, or *pass-by-value*
      (:class:`str`, :class:`int`, :class:`float`, :class:`complex`) it is
      returned as is.
  
      If the object's `id` has an entry in `memo`, the value from `memo` is
      returned.
  
      If the object has a method `__lift__`, it is used to get the promise.
      `__lift__` should take one additional argument for the `memo` dictionary,
      entirely analogous to :func:`deepcopy`.
  
      If the object is an instance of one of the basic container types (list,
      dictionary, tuple and set), we use the analogous function
      (:func:`make_list`, :func:`make_dict`, :func:`make_tuple`, and
      :func:`make_set`) to promise their counterparts should these objects
      contain any promises.  First, we map all items in the container through
      :func:`lift`, then check the result for any promises.  Note that in the
      case of dictionaries, we lift all the items (i.e. the list of key/value
      tuples) and then construct a new dictionary.
  
      If the object is an instance of a subclass of any of the basic container
      types, the `__dict__` of the object is lifted as well as the object cast
      to its base type. We then use :func:`set_dict` to set the `__dict__` of
      the new promise. Again, if the object did not contain any promises,
      we return it without change.
  
      Otherwise, we lift the `__dict__` and create a promise of a new object of
      the same class as the input, using :func:`create_object`. This works fine
      for what we call *reasonable* objects. Since calling :func:`lift` is an
      explicit action, we do not require reasonable objects to be derived from
      :class:`Reasonable` as we do with serialisation, where such a default
      behaviour could lead to unexplicable bugs."""
      if memo is None:
          memo = {}
  
      if isinstance(obj, (PromisedObject, str, int, float, complex)):
          return obj
  
      if id(obj) in memo:
          return memo[id(obj)]
  
      if hasattr(obj, '__lift__'):
          rv = obj.__lift__(memo)
          memo[id(obj)] = rv
          return rv
  
      actions = {
          list:  (lambda x: x, make_list),
          dict:  (lambda x: list(x.items()), make_dict),
          tuple: (lambda x: x, make_tuple),
          set:   (lambda x: x, make_set)
      }
  
      if obj.__class__ in actions:
          items, construct = actions[obj.__class__]
          tmp = [lift(a, memo) for a in items(obj)]
          if any(isinstance(a, PromisedObject) for a in tmp):
              rv = construct(*tmp)
              memo[id(obj)] = rv
              return rv
          else:
              memo[id(obj)] = obj
              return obj
  
      subclass = next(filter(
          lambda x: issubclass(obj.__class__, x),
          actions.keys()), None)
  
      if subclass:
          members = lift(obj.__dict__, memo)
          internal = lift(subclass(obj), memo)
  
          if isinstance(internal, PromisedObject):
              internal = construct_object(obj.__class__, internal)
              rv = set_dict(internal, members)
          elif isinstance(members, PromisedObject):
              rv = set_dict(obj.__class__(internal), members)
          else:
              rv = obj
  
          memo[id(obj)] = rv
          return rv
  
      try:
          members = lift(obj.__dict__, memo)
          if isinstance(members, PromisedObject):
              rv = create_object(obj.__class__, members)
          else:
              rv = obj
      except AttributeError:
          memo[id(obj)] = obj
          return obj
  
      memo[id(obj)] = rv
      return rv