docs: def propagate_cols_up(self, cols, target_df_name, source_df_name):
          """
          Take values from source table, compile them into a colon-delimited list,
          and apply them to the target table.
          This method won't overwrite values in the target table, it will only
          supply values where they are missing.
  
          Parameters
          ----------
          cols : list-like
              list of columns to propagate
          target_df_name : str
              name of table to propagate values into
          source_df_name:
              name of table to propagate values from
  
          Returns
          ---------
          target_df : MagicDataFrame
              updated MagicDataFrame with propagated values
          """
          print("-I- Trying to propagate {} columns from {} table into {} table".format(cols,
                                                                                        source_df_name,
                                                                                        target_df_name))
          # make sure target table is read in
          if target_df_name not in self.tables:
              self.add_magic_table(target_df_name)
          if target_df_name not in self.tables:
              print("-W- Couldn't read in {} table".format(target_df_name))
              return
          # make sure source table is read in
          if source_df_name not in self.tables:
              self.add_magic_table(source_df_name)
              print("-W- Couldn't read in {} table".format(source_df_name))
              return
          target_df = self.tables[target_df_name]
          source_df = self.tables[source_df_name]
          target_name = target_df_name[:-1]
          # make sure source_df has relevant columns
          for col in cols:
              if col not in source_df.df.columns:
                  source_df.df[col] = None
          # if target_df has info, propagate that into all rows
          target_df.front_and_backfill(cols)
          # make sure target_name is in source_df for merging
          if target_name not in source_df.df.columns:
              print("-W- You can't merge data from {} table into {} table".format(source_df_name, target_df_name))
              print("    Your {} table is missing {} column".format(source_df_name, target_name))
              self.tables[target_df_name] = target_df
              return target_df
          source_df.front_and_backfill([target_name])
          # group source df by target_name
          grouped = source_df.df.groupby(source_df.df[target_name])
          if not len(grouped):
              print("-W- Couldn't propagate from {} to {}".format(source_df_name, target_df_name))
              return target_df
          # function to generate capitalized, sorted, colon-delimited list
          # of unique, non-null values from a column
          def func(group, col_name):
              lst = group[col_name][group[col_name].notnull()].unique()
              split_lst = [col.split(':') for col in lst if col]
              sorted_lst = sorted(np.unique([item.capitalize() for sublist in split_lst for item in sublist]))
              group_col = ":".join(sorted_lst)
              return group_col
          # apply func to each column
          for col in cols:
              res = grouped.apply(func, col)
              target_df.df['new_' + col] = res
              target_df.df[col] = np.where(target_df.df[col], target_df.df[col], target_df.df['new_' + col])
              target_df.df.drop(['new_' + col], axis='columns', inplace=True)
          # set table
          self.tables[target_df_name] = target_df
          return target_df