<SYSTEM_TASK:>
Solve the following problem using Python, implementing the functions described below, one line at a time
<END_TASK>
<USER_TASK:>
Description:
 def filter_genes( data: AnnData, min_counts: Optional[int] = None, min_cells: Optional[int] = None, max_counts: Optional[int] = None, max_cells: Optional[int] = None, inplace: bool = True, copy: bool = False, ) -> Union[AnnData, None, Tuple[np.ndarray, np.ndarray]]: """Filter genes based on number of cells or counts. Keep genes that have at least ``min_counts`` counts or are expressed in at least ``min_cells`` cells or have at most ``max_counts`` counts or are expressed in at most ``max_cells`` cells. Only provide one of the optional parameters ``min_counts``, ``min_cells``, ``max_counts``, ``max_cells`` per call. Parameters data An annotated data matrix of shape `n_obs` × `n_vars`. Rows correspond to cells and columns to genes. min_counts Minimum number of counts required for a gene to pass filtering. min_cells Minimum number of cells expressed required for a gene to pass filtering. max_counts Maximum number of counts required for a gene to pass filtering. max_cells Maximum number of cells expressed required for a gene to pass filtering. inplace Perform computation inplace or return result. Returns ------- Depending on `inplace`, returns the following arrays or directly subsets and annotates the data matrix gene_subset : numpy.ndarray Boolean index mask that does filtering. `True` means that the gene is kept. `False` means the gene is removed. number_per_gene : numpy.ndarray Depending on what was tresholded (`counts` or `cells`), the array stores `n_counts` or `n_cells` per gene. """

    if copy:
       logg.warn('`copy` is deprecated, use `inplace` instead.')
    n_given_options = sum(
        option is not None for option in
        [min_cells, min_counts, max_cells, max_counts])
    if n_given_options != 1:
        raise ValueError(
            'Only provide one of the optional parameters `min_counts`,'
            '`min_cells`, `max_counts`, `max_cells` per call.')

    if isinstance(data, AnnData):
        adata = data.copy() if copy else data
        gene_subset, number = materialize_as_ndarray(
            filter_genes(adata.X, min_cells=min_cells,
                         min_counts=min_counts, max_cells=max_cells,
                         max_counts=max_counts))
        if not inplace:
            return gene_subset, number
        if min_cells is None and max_cells is None:
            adata.var['n_counts'] = number
        else:
            adata.var['n_cells'] = number
        adata._inplace_subset_var(gene_subset)
        return adata if copy else None

    X = data  # proceed with processing the data matrix
    min_number = min_counts if min_cells is None else min_cells
    max_number = max_counts if max_cells is None else max_cells
    number_per_gene = np.sum(X if min_cells is None and max_cells is None
                             else X > 0, axis=0)
    if issparse(X):
        number_per_gene = number_per_gene.A1
    if min_number is not None:
        gene_subset = number_per_gene >= min_number
    if max_number is not None:
        gene_subset = number_per_gene <= max_number

    s = np.sum(~gene_subset)
    if s > 0:
        logg.info('filtered out {} genes that are detected'.format(s), end=' ')
        if min_cells is not None or min_counts is not None:
            logg.info('in less than',
                   str(min_cells) + ' cells'
                   if min_counts is None else str(min_counts) + ' counts', no_indent=True)
        if max_cells is not None or max_counts is not None:
            logg.info('in more than ',
                   str(max_cells) + ' cells'
                   if max_counts is None else str(max_counts) + ' counts', no_indent=True)
    return gene_subset, number_per_gene