docs: def read_root(paths, key=None, columns=None, ignore=None, chunksize=None, where=None, flatten=False, *args, **kwargs):
      """
      Read a ROOT file, or list of ROOT files, into a pandas DataFrame.
      Further *args and *kwargs are passed to root_numpy's root2array.
      If the root file contains a branch matching __index__*, it will become the DataFrame's index.
  
      Parameters
      ----------
      paths: string or list
          The path(s) to the root file(s)
      key: string
          The key of the tree to load.
      columns: str or sequence of str
          A sequence of shell-patterns (can contain *, ?, [] or {}). Matching columns are read.
          The columns beginning with `noexpand:` are not interpreted as shell-patterns,
          allowing formula columns such as `noexpand:2*x`. The column in the returned DataFrame
          will not have the `noexpand:` prefix.
      ignore: str or sequence of str
          A sequence of shell-patterns (can contain *, ?, [] or {}). All matching columns are ignored (overriding the columns argument).
      chunksize: int
          If this parameter is specified, an iterator is returned that yields DataFrames with `chunksize` rows.
      where: str
          Only rows that match the expression will be read.
      flatten: sequence of str
          A sequence of column names. Will use root_numpy.stretch to flatten arrays in the specified columns into
          individual entries. All arrays specified in the columns must have the same length for this to work.
          Be careful if you combine this with chunksize, as chunksize will refer to the number of unflattened entries,
          so you will be iterating over a number of entries that is potentially larger than chunksize.
          The index of each element within its former array will be saved in the __array_index column.
  
      Returns
      -------
          DataFrame created from matching data in the specified TTree
  
      Notes
      -----
  
          >>> df = read_root('test.root', 'MyTree', columns=['A{B,C}*', 'D'], where='ABB > 100')
  
      """
  
      if not isinstance(paths, list):
          paths = [paths]
      # Use a single file to search for trees and branches, ensuring the key exists
      for seed_path in paths:
          trees = list_trees(seed_path)
          if key and key not in trees:
              continue
          break
      else:
          if key:
              raise OSError('{} not found in any of the given paths'.format(key))
          else:
              raise OSError('No trees found in any of the given paths')
  
      if not key:
          if len(trees) == 1:
              key = trees[0]
          elif len(trees) == 0:
              raise ValueError('No trees found in {}'.format(seed_path))
          else:
              raise ValueError('More than one tree found in {}'.format(seed_path))
  
      branches = list_branches(seed_path, key)
  
      if not columns:
          all_vars = branches
      else:
          if isinstance(columns, string_types):
              columns = [columns]
          # __index__* is always loaded if it exists
          # XXX Figure out what should happen with multi-dimensional indices
          index_branches = list(filter(lambda x: x.startswith('__index__'), branches))
          if index_branches:
              columns = columns[:]
              columns.append(index_branches[0])
          columns, noexpand = filter_noexpand_columns(columns)
          columns = list(itertools.chain.from_iterable(list(map(expand_braces, columns))))
          all_vars = get_matching_variables(branches, columns) + noexpand
  
      if ignore:
          if isinstance(ignore, string_types):
              ignore = [ignore]
          ignored = get_matching_variables(branches, ignore, fail=False)
          ignored = list(itertools.chain.from_iterable(list(map(expand_braces, ignored))))
          if any(map(lambda x: x.startswith('__index__'), ignored)):
              raise ValueError('__index__* branch is being ignored!')
          for var in ignored:
              all_vars.remove(var)
  
      if chunksize:
          tchain = ROOT.TChain(key)
          for path in paths:
              tchain.Add(path)
          n_entries = tchain.GetEntries()
          n_chunks = int(ceil(float(n_entries) / chunksize))
          # XXX could explicitly clean up the opened TFiles with TChain::Reset
  
          class genchunk(object):
              def __len__(self):
                  return n_chunks
  
              def __iter__(self):
                  current_index = 0
                  for chunk in range(n_chunks):
                      arr = root2array(paths, key, all_vars, start=chunk * chunksize, stop=(chunk+1) * chunksize, selection=where, *args, **kwargs)
                      if len(arr) == 0:
                          continue
                      if flatten:
                          arr = do_flatten(arr, flatten)
                      yield convert_to_dataframe(arr, start_index=current_index)
                      current_index += len(arr)
  
          return genchunk()
  
      arr = root2array(paths, key, all_vars, selection=where, *args, **kwargs)
      if flatten:
          arr = do_flatten(arr, flatten)
      return convert_to_dataframe(arr)