docs: def read(self, **keys):
          """
          read a data from an ascii table HDU
  
          By default, all rows are read.  Send rows= to select subsets of the
          data.  Table data are read into a recarray for multiple columns,
          plain array for a single column.
  
          parameters
          ----------
          columns: list/array
              An optional set of columns to read from table HDUs.  Can be string
              or number. If a sequence, a recarray is always returned.  If a
              scalar, an ordinary array is returned.
          rows: list/array, optional
              An optional list of rows to read from table HDUS.  Default is to
              read all.
          vstorage: string, optional
              Over-ride the default method to store variable length columns.  Can
              be 'fixed' or 'object'.  See docs on fitsio.FITS for details.
          lower: bool, optional
              If True, force all columns names to lower case in output. Will over
              ride the lower= keyword from construction.
          upper: bool, optional
              If True, force all columns names to upper case in output. Will over
              ride the lower= keyword from construction.
          """
  
          rows = keys.get('rows', None)
          columns = keys.get('columns', None)
  
          # if columns is None, returns all.  Guaranteed to be unique and sorted
          colnums = self._extract_colnums(columns)
          if isinstance(colnums, int):
              # scalar sent, don't read as a recarray
              return self.read_column(columns, **keys)
  
          rows = self._extract_rows(rows)
          if rows is None:
              nrows = self._info['nrows']
          else:
              nrows = rows.size
  
          # if rows is None still returns None, and is correctly interpreted
          # by the reader to mean all
          rows = self._extract_rows(rows)
  
          # this is the full dtype for all columns
          dtype, offsets, isvar = self.get_rec_dtype(colnums=colnums, **keys)
          array = numpy.zeros(nrows, dtype=dtype)
  
          # note reading into existing data
          wnotvar, = numpy.where(isvar == False)  # noqa
          if wnotvar.size > 0:
              for i in wnotvar:
                  colnum = colnums[i]
                  name = array.dtype.names[i]
                  a = array[name].copy()
                  self._FITS.read_column(self._ext+1, colnum+1, a, rows)
                  array[name] = a
                  del a
  
          array = self._maybe_decode_fits_ascii_strings_to_unicode_py3(array)
  
          wvar, = numpy.where(isvar == True)  # noqa
          if wvar.size > 0:
              for i in wvar:
                  colnum = colnums[i]
                  name = array.dtype.names[i]
                  dlist = self._FITS.read_var_column_as_list(
                      self._ext+1, colnum+1, rows)
                  if (isinstance(dlist[0], str) or
                          (IS_PY3 and isinstance(dlist[0], bytes))):
                      is_string = True
                  else:
                      is_string = False
  
                  if array[name].dtype.descr[0][1][1] == 'O':
                      # storing in object array
                      # get references to each, no copy made
                      for irow, item in enumerate(dlist):
                          if IS_PY3 and isinstance(item, bytes):
                              item = item.decode('ascii')
                          array[name][irow] = item
                  else:
                      for irow, item in enumerate(dlist):
                          if IS_PY3 and isinstance(item, bytes):
                              item = item.decode('ascii')
                          if is_string:
                              array[name][irow] = item
                          else:
                              ncopy = len(item)
                              array[name][irow][0:ncopy] = item[:]
  
          lower = keys.get('lower', False)
          upper = keys.get('upper', False)
          if self.lower or lower:
              _names_to_lower_if_recarray(array)
          elif self.upper or upper:
              _names_to_upper_if_recarray(array)
  
          self._maybe_trim_strings(array, **keys)
  
          return array