docs: def run_cell(self, cell):
          """Run a notebook cell and update the output of that cell in-place."""
          logging.info('Running cell:\n%s\n', cell.input)
          self.kc.execute(cell.input)
          reply = self.kc.get_shell_msg()
          status = reply['content']['status']
          traceback_text = ''
          if status == 'error':
              traceback_text = 'Cell raised uncaught exception: \n' + \
                  '\n'.join(reply['content']['traceback'])
              logging.info(traceback_text)
          else:
              logging.info('Cell returned')
  
          outs = list()
          while True:
              try:
                  msg = self.kc.get_iopub_msg(timeout=1)
                  if msg['msg_type'] == 'status':
                      if msg['content']['execution_state'] == 'idle':
                          break
              except Empty:
                  # execution state should return to idle
                  # before the queue becomes empty,
                  # if it doesn't, something bad has happened
                  raise
  
              content = msg['content']
              msg_type = msg['msg_type']
  
              # IPython 3.0.0-dev writes pyerr/pyout in the notebook format
              # but uses error/execute_result in the message spec. This does the
              # translation needed for tests to pass with IPython 3.0.0-dev
              notebook3_format_conversions = {
                  'error': 'pyerr',
                  'execute_result': 'pyout'
              }
              msg_type = notebook3_format_conversions.get(msg_type, msg_type)
  
              out = NotebookNode(output_type=msg_type)
  
              if 'execution_count' in content:
                  cell['prompt_number'] = content['execution_count']
                  out.prompt_number = content['execution_count']
  
              if msg_type in ('status', 'pyin', 'execute_input'):
                  continue
              elif msg_type == 'stream':
                  out.stream = content['name']
                  # in msgspec 5, this is name, text
                  # in msgspec 4, this is name, data
                  if 'text' in content:
                      out.text = content['text']
                  else:
                      out.text = content['data']
              elif msg_type in ('display_data', 'pyout'):
                  for mime, data in content['data'].items():
                      try:
                          attr = self.MIME_MAP[mime]
                      except KeyError:
                          raise NotImplementedError(
                              'unhandled mime type: %s' % mime
                          )
  
                      # In notebook version <= 3 JSON data is stored as a string
                      # Evaluation of IPython2's JSON gives strings directly
                      # Therefore do not encode for IPython versions prior to 3
                      json_encode = (
                              IPython.version_info[0] >= 3 and
                              mime == "application/json")
  
                      data_out = data if not json_encode else json.dumps(data)
                      setattr(out, attr, data_out)
              elif msg_type == 'pyerr':
                  out.ename = content['ename']
                  out.evalue = content['evalue']
                  out.traceback = content['traceback']
              elif msg_type == 'clear_output':
                  outs = list()
                  continue
              else:
                  raise NotImplementedError(
                      'unhandled iopub message: %s' % msg_type
                  )
              outs.append(out)
          cell['outputs'] = outs
  
          if status == 'error':
              raise NotebookError(traceback_text)