docs: def resolve_path(path, expected=None, multi_projects=False, allow_empty_string=True):
      '''
      :param path: A path to a data object to attempt to resolve
      :type path: string
      :param expected: one of the following: "folder", "entity", or None
              to indicate whether the expected path is a folder, a data
              object, or either
      :type expected: string or None
      :returns: A tuple of 3 values: container_ID, folderpath, entity_name
      :rtype: string, string, string
      :raises: exc:`ResolutionError` if the project cannot be resolved by
              name or the path is malformed
      :param allow_empty_string: If false, a ResolutionError will be
              raised if *path* is an empty string. Use this when resolving
              the empty string could result in unexpected behavior.
      :type allow_empty_string: boolean
  
      Attempts to resolve *path* to a project or container ID, a folder
      path, and a data object or folder name.  This method will NOT
      raise an exception if the specified folder or object does not
      exist.  This method is primarily for parsing purposes.
  
      Returns one of the following:
  
        (project, folder, maybe_name)
        where
          project is a container ID (non-null)
          folder is a folder path
          maybe_name is a string if the path could represent a folder or an object, or
          maybe_name is None if the path could only represent a folder
  
      OR
  
        (maybe_project, None, object_id)
        where
          maybe_project is a container ID or None
          object_id is a dataobject, app, or execution (specified by ID, not name)
  
      OR
  
        (job_id, None, output_name)
        where
          job_id and output_name are both non-null
  
      '''
      # TODO: callers that intend to obtain a data object probably won't be happy
      # with an app or execution ID. Callers should probably have to specify
      # whether they are okay with getting an execution ID or not.
  
      # TODO: callers that are looking for a place to write data, rather than
      # read it, probably won't be happy with receiving an object ID, or a
      # JBOR. Callers should probably specify whether they are looking for an
      # "LHS" expression or not.
  
      if '_DX_FUSE' in os.environ:
          from xattr import xattr
          path = xattr(path)['project'] + ":" + xattr(path)['id']
  
      if path == '' and not allow_empty_string:
          raise ResolutionError('Cannot parse ""; expected the path to be a non-empty string')
      path = _maybe_convert_stringified_dxlink(path)
  
      # Easy case: ":"
      if path == ':':
          if dxpy.WORKSPACE_ID is None:
              raise ResolutionError("Cannot resolve \":\": expected a project name or ID "
                                    "to the left of the colon, or for a current project to be set")
          return ([dxpy.WORKSPACE_ID] if multi_projects else dxpy.WORKSPACE_ID), '/', None
      # Second easy case: empty string
      if path == '':
          if dxpy.WORKSPACE_ID is None:
              raise ResolutionError('Expected a project name or ID to the left of a colon, '
                                    'or for a current project to be set')
          return ([dxpy.WORKSPACE_ID] if multi_projects else dxpy.WORKSPACE_ID), dxpy.config.get('DX_CLI_WD', '/'), None
      # Third easy case: hash ID
      if is_container_id(path):
          return ([path] if multi_projects else path), '/', None
      elif is_hashid(path):
          return ([dxpy.WORKSPACE_ID] if multi_projects else dxpy.WORKSPACE_ID), None, path
  
      # using a numerical sentinel value to indicate that it hasn't been
      # set in case dxpy.WORKSPACE_ID is actually None
      project = 0
      folderpath = None
      entity_name = None
      wd = dxpy.config.get('DX_CLI_WD', u'/')
  
      # Test for multiple colons
      last_colon = get_last_pos_of_char(':', path)
      if last_colon >= 0:
          last_last_colon = get_last_pos_of_char(':', path[:last_colon])
          if last_last_colon >= 0:
              raise ResolutionError('Cannot parse "' + path + '" as a path; at most one unescaped colon can be present')
  
      substrings = split_unescaped(':', path)
  
      if len(substrings) == 2:
          # One of the following:
          # 1) job-id:fieldname
          # 2) project-name-or-id:folderpath/to/possible/entity
          if is_job_id(substrings[0]):
              return ([substrings[0]] if multi_projects else substrings[0]), None, substrings[1]
  
          if multi_projects:
              project_ids = resolve_container_id_or_name(substrings[0], is_error=True, multi=True)
          else:
              project = resolve_container_id_or_name(substrings[0], is_error=True)
          wd = '/'
      elif get_last_pos_of_char(':', path) >= 0:
          # :folderpath/to/possible/entity OR project-name-or-id:
          # Colon is either at the beginning or at the end
          wd = '/'
          if path.startswith(':'):
              if dxpy.WORKSPACE_ID is None:
                  raise ResolutionError('Cannot resolve "%s": expected a project name or ID to the left of the '
                                        'colon, or for a current project to be set' % (path,))
              project = dxpy.WORKSPACE_ID
          else:
              # One nonempty string to the left of a colon
              project = resolve_container_id_or_name(substrings[0], is_error=True)
              folderpath = '/'
      else:
          # One nonempty string, no colon present, do NOT interpret as
          # project
          project = dxpy.WORKSPACE_ID
          if project is None:
              raise ResolutionError('Cannot resolve "%s": expected the path to be qualified with a project name or ID, '
                                    'and a colon; or for a current project to be set' % (path,))
  
      # Determine folderpath and entity_name if necessary
      if folderpath is None:
          folderpath = substrings[-1]
          folderpath, entity_name = clean_folder_path(('' if folderpath.startswith('/') else wd + '/') + folderpath, expected)
  
      if multi_projects:
          return (project_ids if project == 0 else [project]), folderpath, entity_name
      else:
          return project, folderpath, entity_name