docs: def add_columns(self, layers: Union[np.ndarray, Dict[str, np.ndarray], loompy.LayerManager], col_attrs: Dict[str, np.ndarray], *, row_attrs: Dict[str, np.ndarray] = None, fill_values: Dict[str, np.ndarray] = None) -> None:
  		"""
  		Add columns of data and attribute values to the dataset.
  
  		Args:
  			layers (dict or numpy.ndarray or LayerManager):
  				Either:
  				1) A N-by-M matrix of float32s (N rows, M columns) in this case columns are added at the default layer
  				2) A dict {layer_name : matrix} specified so that the matrix (N, M) will be added to layer `layer_name`
  				3) A LayerManager object (such as what is returned by view.layers)
  			col_attrs (dict):
  				Column attributes, where keys are attribute names and values are numpy arrays (float or string) of length M
  			row_attrs (dict):
  				Optional row attributes, where keys are attribute names and values are numpy arrays (float or string) of length M
  			fill_values: dictionary of values to use if a column attribute is missing, or "auto" to fill with zeros or empty strings
  
  		Returns:
  			Nothing.
  
  		Notes
  		-----
  		- This will modify the underlying HDF5 file, which will interfere with any concurrent readers.
  		- Column attributes in the file that are NOT provided, will be deleted (unless fill value provided).
  		- Array with Nan should not be provided
  
  		"""
  		if self._file.mode != "r+":
  			raise IOError("Cannot add columns when connected in read-only mode")
  
  		# If this is an empty loom file, just assign the provided row and column attributes, and set the shape
  		is_new = self.shape == (0, 0)
  		if is_new:
  			if row_attrs is None:
  				raise ValueError("row_attrs must be provided when adding to an empty (new) Loom file")
  			for k, v in row_attrs.items():
  				self.ra[k] = v
  				self.shape = (self.ra[k].shape[0], self.shape[1])
  			if len(self.ca) == 0:
  				for k, v in col_attrs.items():
  					self.ca[k] = np.zeros(0, v.dtype)
  
  		layers_dict: Dict[str, np.ndarray] = {}
  		if isinstance(layers, np.ndarray):
  			layers_dict = {"": layers}
  		elif isinstance(layers, loompy.LayerManager):
  			layers_dict = {k: v[:, :] for k, v in layers.items()}
  		elif isinstance(layers, dict):
  			layers_dict = layers
  		else:
  			raise ValueError("Invalid type for layers argument")
  		n_cols = 0
  		for layer, matrix in layers_dict.items():
  			if not is_new and layer not in self.layers.keys():
  				raise ValueError(f"Layer {layer} does not exist in the target loom file")
  			if matrix.shape[0] != self.shape[0]:
  				raise ValueError(f"Layer {layer} has {matrix.shape[0]} rows but file has {self.shape[0]}")
  			if n_cols == 0:
  				n_cols = matrix.shape[1]
  			elif matrix.shape[1] != n_cols:
  				raise ValueError(f"Layer {layer} has {matrix.shape[1]} columns but the first layer had {n_cols}")
  
  		did_remove = False
  		todel = []  # type: List[str]
  		for key, vals in col_attrs.items():
  			if key not in self.col_attrs:
  				if fill_values is not None:
  					if fill_values == "auto":
  						fill_with = np.zeros(1, dtype=col_attrs[key].dtype)[0]
  					else:
  						fill_with = fill_values[key]
  					self.ca[key] = np.array([fill_with] * self.shape[1])
  				else:
  					did_remove = True
  					todel.append(key)
  			if len(vals) != n_cols:
  				raise ValueError(f"Each column attribute must have exactly {n_cols} values, but {key} had {len(vals)}")
  		for key in todel:
  			del col_attrs[key]
  		if did_remove:
  			logging.debug("Some column attributes were removed: " + ",".join(todel))
  
  		todel = []
  		did_remove = False
  		for key in self.col_attrs.keys():
  			if key not in col_attrs:
  				if fill_values is not None:
  					if fill_values == "auto":
  						fill_with = np.zeros(1, dtype=self.col_attrs[key].dtype)[0]
  					else:
  						fill_with = fill_values[key]
  					col_attrs[key] = np.array([fill_with] * n_cols)
  				else:
  					did_remove = True
  					todel.append(key)
  		for key in todel:
  			del self.ca[key]  # delete_attr(key, axis=1)
  		if did_remove:
  			logging.debug("Some column attributes were removed: " + ",".join(todel))
  
  		if is_new:
  			for k, v in layers_dict.items():
  				self.layers[k] = v
  			for k, v in col_attrs.items():
  				self.ca[k] = v
  		else:
  			n_cols = n_cols + self.shape[1]
  			old_n_cols = self.shape[1]
  			# Must set new shape here, otherwise the attribute manager will complain
  			self.shape = (self.shape[0], n_cols)
  			todel = []
  			for key, vals in col_attrs.items():
  				if vals.shape[1:] != self.col_attrs[key].shape[1:]:
  					logging.debug(f"Removing attribute {key} because shape {vals.shape} did not match existing shape {self.col_attrs[key].shape} beyond first dimension")
  					todel.append(key)
  				else:
  					self.ca[key] = np.concatenate([self.ca[key], vals])
  			for key in todel:
  				del self.ca[key]
  
  			# Add the columns layerwise
  			for key in self.layers.keys():
  				self.layers[key]._resize(n_cols, axis=1)
  				self.layers[key][:, old_n_cols:n_cols] = layers_dict[key]
  		self._file.flush()