docs: def get_sub_node(dsp, path, node_attr='auto', solution=NONE, _level=0,
                   _dsp_name=NONE):
      """
      Returns a sub node of a dispatcher.
  
      :param dsp:
           A dispatcher object or a sub dispatch function.
      :type dsp: schedula.Dispatcher | SubDispatch
  
      :param path:
          A sequence of node ids or a single node id. Each id identifies a
          sub-level node.
      :type path: tuple, str
  
      :param node_attr:
          Output node attr.
  
          If the searched node does not have this attribute, all its attributes
          are returned.
  
          When 'auto', returns the "default" attributes of the searched node,
          which are:
  
            - for data node: its output, and if not exists, all its attributes.
            - for function and sub-dispatcher nodes: the 'function' attribute.
      :type node_attr: str | None
  
      :param solution:
          Parent Solution.
      :type solution: schedula.utils.Solution
  
      :param _level:
          Path level.
      :type _level: int
  
      :param _dsp_name:
          dsp name to show when the function raise a value error.
      :type _dsp_name: str
  
      :return:
          A sub node of a dispatcher and its path.
      :rtype: dict | object, tuple[str]
  
      **Example**:
  
      .. dispatcher:: o
         :opt: graph_attr={'ratio': '1'}, depth=-1
         :code:
  
          >>> from schedula import Dispatcher
          >>> s_dsp = Dispatcher(name='Sub-dispatcher')
          >>> def fun(a, b):
          ...     return a + b
          ...
          >>> s_dsp.add_function('a + b', fun, ['a', 'b'], ['c'])
          'a + b'
          >>> dispatch = SubDispatch(s_dsp, ['c'], output_type='dict')
          >>> dsp = Dispatcher(name='Dispatcher')
          >>> dsp.add_function('Sub-dispatcher', dispatch, ['a'], ['b'])
          'Sub-dispatcher'
  
          >>> o = dsp.dispatch(inputs={'a': {'a': 3, 'b': 1}})
          ...
  
      Get the sub node 'c' output or type::
  
          >>> get_sub_node(dsp, ('Sub-dispatcher', 'c'))
          (4, ('Sub-dispatcher', 'c'))
          >>> get_sub_node(dsp, ('Sub-dispatcher', 'c'), node_attr='type')
          ('data', ('Sub-dispatcher', 'c'))
  
      Get the sub-dispatcher output:
  
      .. dispatcher:: sol
         :opt: graph_attr={'ratio': '1'}, depth=-1
         :code:
  
          >>> sol, p = get_sub_node(dsp, ('Sub-dispatcher',), node_attr='output')
          >>> sol, p
          (Solution([('a', 3), ('b', 1), ('c', 4)]), ('Sub-dispatcher',))
      """
  
      path = list(path)
  
      if isinstance(dsp, SubDispatch):  # Take the dispatcher obj.
          dsp = dsp.dsp
  
      if _dsp_name is NONE:  # Set origin dispatcher name for warning purpose.
          _dsp_name = dsp.name
  
      if solution is NONE:  # Set origin dispatcher name for warning purpose.
          solution = dsp.solution
  
      node_id = path[_level]  # Node id at given level.
  
      try:
          node_id, node = _get_node(dsp.nodes, node_id)  # Get dispatcher node.
          path[_level] = node_id
      except KeyError:
          if _level == len(path) - 1 and node_attr in ('auto', 'output') \
                  and solution is not EMPTY:
              try:
                  # Get dispatcher node.
                  node_id, node = _get_node(solution, node_id, False)
                  path[_level] = node_id
                  return node, tuple(path)
              except KeyError:
                  pass
          msg = 'Path %s does not exist in %s dispatcher.' % (path, _dsp_name)
          raise ValueError(msg)
  
      _level += 1  # Next level.
  
      if _level < len(path):  # Is not path leaf?.
  
          try:
              if node['type'] in ('function', 'dispatcher'):
                  try:
                      solution = solution.workflow.node[node_id]['solution']
                  except (KeyError, AttributeError):
                      solution = EMPTY
                  dsp = parent_func(node['function'])  # Get parent function.
              else:
                  raise KeyError
  
          except KeyError:
              msg = 'Node of path %s at level %i is not a function or ' \
                    'sub-dispatcher node of %s ' \
                    'dispatcher.' % (path, _level, _dsp_name)
              raise ValueError(msg)
  
          # Continue the node search.
          return get_sub_node(dsp, path, node_attr, solution, _level, _dsp_name)
      else:
          data, sol = EMPTY, solution
          # Return the sub node.
          if node_attr == 'auto' and node['type'] != 'data':  # Auto: function.
              node_attr = 'function'
          elif node_attr == 'auto' and sol is not EMPTY and node_id in sol:
              data = sol[node_id]  # Auto: data output.
          elif node_attr == 'output' and node['type'] != 'data':
              data = sol.workflow.nodes[node_id]['solution']
          elif node_attr == 'output' and node['type'] == 'data':
              data = sol[node_id]
          elif node_attr == 'description':  # Search and return node description.
              data = dsp.search_node_description(node_id)[0]
          elif node_attr == 'value_type' and node['type'] == 'data':
              # Search and return data node value's type.
              data = dsp.search_node_description(node_id, node_attr)[0]
          elif node_attr == 'default_value':
              data = dsp.default_values[node_id]
          elif node_attr == 'dsp':
              data = dsp
          elif node_attr == 'sol':
              data = sol
  
          if data is EMPTY:
              data = node.get(node_attr, node)
  
          return data, tuple(path)