Repository: scverse/scanpy

Issue:
`FutureWarning` for the 1.11 to begin the process of switching leiden backend
See: https://github.com/scverse/scanpy/issues/2865


Discussion:
I think we sorta already did this:

```python
sc.tl.leiden(adata)
/mnt/workspace/repos/scanpy/scanpy/tools/_leiden.py:144: FutureWarning: Use of leidenalg is discouraged and will be deprecated in the future.  Please use `flavor="igraph"` `n_iterations=2` to achieve similar results.  `directed` must also be `False` to work with `igraph`'s implementation.
  warnings.warn(msg, FutureWarning)
```

However, I do think the warning could be reworked

- [ ] Stack level should be set so the right line is shown
- [ ] Message should be more like:

> FutureWarning: In the future, the default backend for `leiden` will be `igraph` instead of `leidenalg`. To achieve the future defaults please pass: `flavor="igraph"` and `n_iterations=2`.  `directed` must also be `False` to work with `igraph`'s implementation.
>
> Note that results will be slightly different with the changed backend.

- [ ] We should only warn once, instead of every time leiden is called. Here's a util we defined in anndata for this (I think stolen from pandas?)

```python
def warn_once(msg: str, category: type[Warning], stacklevel: int = 1):
    warnings.warn(msg, category, stacklevel=stacklevel)
    # Prevent from showing up every time an awkward array is used
    # You'd think `'once'` works, but it doesn't at the repl and in notebooks
    warnings.filterwarnings("ignore", category=category, message=re.escape(msg))
```

Resolving patch:
diff --git a/scanpy/_utils/__init__.py b/scanpy/_utils/__init__.py
--- a/scanpy/_utils/__init__.py
+++ b/scanpy/_utils/__init__.py
@@ -9,6 +9,7 @@
 import importlib.util
 import inspect
 import random
+import re
 import sys
 import warnings
 from collections import namedtuple
@@ -857,6 +858,12 @@ def warn_with_traceback(message, category, filename, lineno, file=None, line=Non
     settings.write(warnings.formatwarning(message, category, filename, lineno, line))
 
 
+def warn_once(msg: str, category: type[Warning], stacklevel: int = 1):
+    warnings.warn(msg, category, stacklevel=stacklevel)
+    # You'd think `'once'` works, but it doesn't at the repl and in notebooks
+    warnings.filterwarnings("ignore", category=category, message=re.escape(msg))
+
+
 def subsample(
     X: np.ndarray,
     subsample: int = 1,
diff --git a/scanpy/tools/_leiden.py b/scanpy/tools/_leiden.py
--- a/scanpy/tools/_leiden.py
+++ b/scanpy/tools/_leiden.py
@@ -1,7 +1,6 @@
 from __future__ import annotations
 
 import importlib
-import warnings
 from typing import TYPE_CHECKING, Literal
 
 import numpy as np
@@ -140,8 +139,8 @@ def leiden(
         try:
             import leidenalg
 
-            msg = 'Use of leidenalg is discouraged and will be deprecated in the future.  Please use `flavor="igraph"` `n_iterations=2` to achieve similar results.  `directed` must also be `False` to work with `igraph`\'s implementation.'
-            warnings.warn(msg, FutureWarning)
+            msg = 'In the future, the default backend for leiden will be igraph instead of leidenalg.\n\n To achieve the future defaults please pass: flavor="igraph" and n_iterations=2.  directed must also be False to work with igraph\'s implementation.'
+            _utils.warn_once(msg, FutureWarning, stacklevel=3)
         except ImportError:
             raise ImportError(
                 "Please install the leiden algorithm: `conda install -c conda-forge leidenalg` or `pip3 install leidenalg`."


Test patch: