Repository: scverse/scanpy

Issue:
AttributeError: 'Ingest' object has no attribute '_pca_use_hvg'
### Please make sure these conditions are met

- [X] I have checked that this issue has not already been reported.
- [X] I have confirmed this bug exists on the latest version of scanpy.
- [ ] (optional) I have confirmed this bug exists on the main branch of scanpy.

### What happened?

Attempted label transfer using `ingest`, using the example code on the [docs page](https://scanpy.readthedocs.io/en/1.10.x/generated/scanpy.tl.ingest.html)

The fix is trivial (you must run `sc.pp.pca(adata_ref)` as well), but the error message is cryptic so I got stuck for a bit! I'll create the PR too.

### Minimal code sample

```python
import scanpy as sc
import pandas as pd

sc.settings.verbosity = 1  # verbosity: errors (0), warnings (1), info (2), hints (3)
sc.logging.print_versions()
sc.settings.set_figure_params(dpi=80, frameon=False, figsize=(3, 3), facecolor="white")

adata_ref = sc.datasets.pbmc3k_processed()
adata = sc.datasets.pbmc68k_reduced()

var_names = adata_ref.var_names.intersection(adata.var_names)
adata_ref = adata_ref[:, var_names]
adata = adata[:, var_names]

# start from scratch
del adata.obs["louvain"]
adata.uns = {}
adata_ref.uns = {}

# example code for ingest function:
sc.pp.neighbors(adata_ref)
sc.tl.umap(adata_ref)
sc.tl.ingest(adata, adata_ref, obs="louvain")
```


### Error output

```pytb
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[11], line 23
     21 sc.pp.neighbors(adata_ref)
     22 sc.tl.umap(adata_ref)
---> 23 sc.tl.ingest(adata, adata_ref, obs="louvain")

File ~/miniconda3/envs/sc/lib/python3.12/site-packages/legacy_api_wrap/__init__.py:80, in legacy_api.<locals>.wrapper.<locals>.fn_compatible(*args_all, **kw)
     77 @wraps(fn)
     78 def fn_compatible(*args_all: P.args, **kw: P.kwargs) -> R:
     79     if len(args_all) <= n_positional:
---> 80         return fn(*args_all, **kw)
     82     args_pos: P.args
     83     args_pos, args_rest = args_all[:n_positional], args_all[n_positional:]

File ~/miniconda3/envs/sc/lib/python3.12/site-packages/scanpy/tools/_ingest.py:141, in ingest(adata, adata_ref, obs, embedding_method, labeling_method, neighbors_key, inplace, **kwargs)
    138     labeling_method = labeling_method * len(obs)
    140 ing = Ingest(adata_ref, neighbors_key)
--> 141 ing.fit(adata)
    143 for method in embedding_method:
    144     ing.map_embedding(method)

File ~/miniconda3/envs/sc/lib/python3.12/site-packages/scanpy/tools/_ingest.py:404, in Ingest.fit(self, adata_new)
    401 self._obsm = _DimDict(adata_new.n_obs, axis=0)
    403 self._adata_new = adata_new
--> 404 self._obsm["rep"] = self._same_rep()

File ~/miniconda3/envs/sc/lib/python3.12/site-packages/scanpy/tools/_ingest.py:371, in Ingest._same_rep(self)
    369 adata = self._adata_new
    370 if self._n_pcs is not None:
--> 371     return self._pca(self._n_pcs)
    372 if self._use_rep == "X":
    373     return adata.X

File ~/miniconda3/envs/sc/lib/python3.12/site-packages/scanpy/tools/_ingest.py:361, in Ingest._pca(self, n_pcs)
    359 X = self._adata_new.X
    360 X = X.toarray() if issparse(X) else X.copy()
--> 361 if self._pca_use_hvg:
    362     X = X[:, self._adata_ref.var["highly_variable"]]
    363 if self._pca_centered:

AttributeError: 'Ingest' object has no attribute '_pca_use_hvg'
```


### Versions

<details>

```
-----
anndata     0.10.7
scanpy      1.10.1
-----
PIL                         10.3.0
anyio                       NA
arrow                       1.3.0
asttokens                   NA
attr                        23.2.0
attrs                       23.2.0
babel                       2.14.0
brotli                      1.1.0
certifi                     2024.02.02
cffi                        1.16.0
charset_normalizer          3.3.2
colorama                    0.4.6
comm                        0.2.2
cycler                      0.12.1
cython_runtime              NA
dateutil                    2.9.0
debugpy                     1.8.1
decorator                   5.1.1
defusedxml                  0.7.1
executing                   2.0.1
fastjsonschema              NA
fqdn                        NA
h5py                        3.11.0
idna                        3.7
igraph                      0.10.8
ipykernel                   6.29.3
ipywidgets                  8.1.2
isoduration                 NA
jedi                        0.19.1
jinja2                      3.1.3
joblib                      1.4.0
json5                       0.9.25
jsonpointer                 2.4
jsonschema                  4.21.1
jsonschema_specifications   NA
jupyter_events              0.10.0
jupyter_server              2.14.0
jupyterlab_server           2.27.0
kiwisolver                  1.4.5
legacy_api_wrap             NA
leidenalg                   0.10.2
llvmlite                    0.42.0
markupsafe                  2.1.5
matplotlib                  3.8.4
matplotlib_inline           0.1.7
mpl_toolkits                NA
natsort                     8.4.0
nbformat                    5.10.4
numba                       0.59.1
numpy                       1.26.4
overrides                   NA
packaging                   24.0
pandas                      1.5.3
parso                       0.8.4
pickleshare                 0.7.5
platformdirs                4.2.0
prometheus_client           NA
prompt_toolkit              3.0.42
psutil                      5.9.8
pure_eval                   0.2.2
pycparser                   2.22
pydev_ipython               NA
pydevconsole                NA
pydevd                      2.9.5
pydevd_file_utils           NA
pydevd_plugins              NA
pydevd_tracing              NA
pygments                    2.17.2
pynndescent                 0.5.12
pyparsing                   3.1.2
pythonjsonlogger            NA
pytz                        2024.1
referencing                 NA
requests                    2.31.0
rfc3339_validator           0.1.4
rfc3986_validator           0.1.1
rpds                        NA
scipy                       1.13.0
send2trash                  NA
session_info                1.0.0
six                         1.16.0
sklearn                     1.4.2
sniffio                     1.3.1
socks                       1.7.1
stack_data                  0.6.2
texttable                   1.7.0
threadpoolctl               3.4.0
tornado                     6.4
tqdm                        4.66.2
traitlets                   5.14.3
umap                        0.5.5
uri_template                NA
urllib3                     2.2.1
wcwidth                     0.2.13
webcolors                   1.13
websocket                   1.7.0
yaml                        6.0.1
zmq                         26.0.2
-----
IPython             8.22.2
jupyter_client      8.6.1
jupyter_core        5.7.2
jupyterlab          4.1.6
notebook            7.1.3
-----
Python 3.12.3 | packaged by conda-forge | (main, Apr 15 2024, 18:38:13) [GCC 12.3.0]
Linux-5.4.0-150-generic-x86_64-with-glibc2.27
-----
Session information updated at 2024-05-22 17:19


```

</details>



Discussion:

Resolving patch:
diff --git a/scanpy/tools/_utils.py b/scanpy/tools/_utils.py
--- a/scanpy/tools/_utils.py
+++ b/scanpy/tools/_utils.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import warnings
 from typing import TYPE_CHECKING
 
 import numpy as np
@@ -37,14 +38,14 @@ def _choose_representation(
                 X = adata.obsm["X_pca"][:, :n_pcs]
                 logg.info(f"    using 'X_pca' with n_pcs = {X.shape[1]}")
             else:
-                logg.warning(
+                warnings.warn(
                     f"You’re trying to run this on {adata.n_vars} dimensions of `.X`, "
                     "if you really want this, set `use_rep='X'`.\n         "
                     "Falling back to preprocessing with `sc.pp.pca` and default params."
                 )
                 n_pcs_pca = n_pcs if n_pcs is not None else settings.N_PCS
-                X = pca(adata.X, n_comps=n_pcs_pca)
-                adata.obsm["X_pca"] = X
+                pca(adata, n_comps=n_pcs_pca)
+                X = adata.obsm["X_pca"]
         else:
             logg.info("    using data matrix X directly")
             X = adata.X


Test patch:
diff --git a/scanpy/tests/test_neighbors_key_added.py b/scanpy/tests/test_neighbors_key_added.py
--- a/scanpy/tests/test_neighbors_key_added.py
+++ b/scanpy/tests/test_neighbors_key_added.py
@@ -32,6 +32,16 @@ def test_neighbors_key_added(adata):
     )
 
 
+def test_neighbors_pca_keys_added_without_previous_pca_run(adata):
+    assert "pca" not in adata.uns and "X_pca" not in adata.obsm
+    with pytest.warns(
+        UserWarning,
+        match=r".*Falling back to preprocessing with `sc.pp.pca` and default params",
+    ):
+        sc.pp.neighbors(adata, n_neighbors=n_neighbors, random_state=0)
+    assert "pca" in adata.uns
+
+
 # test functions with neighbors_key and obsp
 @needs.igraph
 @needs.leidenalg