Repository: nilearn/nilearn

Issue:
LGTM error: Non-iterable used in for loop
I don't fully grasp the details of [this LGTM.com alert](https://lgtm.com/projects/g/nilearn/nilearn/snapshot/0cd925e33787e277990b524efeba0c8125d04816/files/nilearn/decoding/space_net.py?sort=name&dir=ASC&mode=heatmap#x4b41318e0293a848:1):
> `[ 3 Values ]` of class '`[ 3 Values ]`' may be used in for-loop.

What are these "3 Values" about?

Nevertheless, it is true that in `l1_ratios` [might be a float, hence not iterable](https://github.com/nilearn/nilearn/blob/bbbc4ba/nilearn/decoding/space_net.py#L685-L687):
```python
            if isinstance(l1_ratios, numbers.Number):
                l1_ratios = [l1_ratios]
            for l1_ratio in l1_ratios:s.Number):
```
Indeed, `l1_ratios` is initialised to be a float, so why check whether it is an instance of `numbers.Number`? Am I missing something here?


Discussion:
`l1_ratios` can be either a float or a sequence  of floats (in that case the best value will be selected by a nested cross-validation loop). this checks which it is, and turns it into a (length 1) sequence of floats if it is a single float. `0.` and `1.` are valid values for `l1_ratio`, so there is a chance that a user might pass an int instead of a float, so it is safer to check if it is a `numbers.Number` rather than just `float`.

I don't know what [ 3 Values ] of class '[ 3 Values ]' means
Ah, you mean a `float` **is** a `numbers.Number`? Didn't even try to test that, sorry:
```python
>>> x = 0.5
>>> isinstance(x, numbers.Number)
True
>>> 
```
I have reported this as a false positive:
https://github.com/github/codeql/issues/6519
But since what we want is to change a non-iterable into a list, we could as well test:
```python
            try:
                iter(l1_ratios)
            except TypeError:
                l1_ratios = [l1_ratios]
```
instead of:
```python
            if isinstance(l1_ratios, numbers.Number):
                l1_ratios = [l1_ratios]
```

yes that would also work, and we could also check if it is an instance of `collections.Iterable`. I think the existing solution is also fine, though
The current solution is fine, of course. I just want to work around the LGTM.com warning.
I see, then yes if it silences the warning we can change it
Cannot be 100% certain, but I think this will silence the warning.

Resolving patch:
diff --git a/nilearn/decoding/space_net.py b/nilearn/decoding/space_net.py
--- a/nilearn/decoding/space_net.py
+++ b/nilearn/decoding/space_net.py
@@ -12,7 +12,7 @@
 # License: simplified BSD
 
 import warnings
-import numbers
+import collections
 import time
 import sys
 from functools import partial
@@ -313,9 +313,10 @@ def path_scores(solver, X, y, mask, alphas, l1_ratios, train, test,
     test : array or list of integers
         List of indices for the test samples.
 
-    l1_ratio : float in the interval [0, 1]; optional (default .5)
+    l1_ratios : float or list of floats in the interval [0, 1];
+    optional (default .5)
         Constant that mixes L1 and TV (resp. Graph-Net) penalization.
-        l1_ratio == 0: just smooth. l1_ratio == 1: just lasso.
+        l1_ratios == 0: just smooth. l1_ratios == 1: just lasso.
 
     eps : float, optional (default 1e-3)
         Length of the path. For example, ``eps=1e-3`` means that
@@ -382,7 +383,7 @@ def path_scores(solver, X, y, mask, alphas, l1_ratios, train, test,
         copy=False)
 
     # misc
-    if isinstance(l1_ratios, numbers.Number):
+    if not isinstance(l1_ratios, collections.abc.Iterable):
         l1_ratios = [l1_ratios]
     l1_ratios = sorted(l1_ratios)[::-1]  # from large to small l1_ratios
     best_score = -np.inf
@@ -502,7 +503,7 @@ class BaseSpaceNet(LinearRegression, CacheMixin):
     l1_ratios : float or list of floats in the interval [0, 1];
     optional (default .5)
         Constant that mixes L1 and spatial prior terms in penalization.
-        l1_ratio == 1 corresponds to pure LASSO. The larger the value of this
+        l1_ratios == 1 corresponds to pure LASSO. The larger the value of this
         parameter, the sparser the estimated weights map. If list is provided,
         then the best value will be selected by cross-validation.
 
@@ -682,7 +683,7 @@ def check_params(self):
         """Makes sure parameters are sane"""
         if self.l1_ratios is not None:
             l1_ratios = self.l1_ratios
-            if isinstance(l1_ratios, numbers.Number):
+            if not isinstance(l1_ratios, collections.abc.Iterable):
                 l1_ratios = [l1_ratios]
             for l1_ratio in l1_ratios:
                 if not 0 <= l1_ratio <= 1.:
@@ -777,11 +778,12 @@ def fit(self, X, y):
         n_samples, _ = X.shape
         y = np.array(y).copy()
         l1_ratios = self.l1_ratios
-        if isinstance(l1_ratios, numbers.Number):
+        if not isinstance(l1_ratios, collections.abc.Iterable):
             l1_ratios = [l1_ratios]
         alphas = self.alphas
-        if isinstance(alphas, numbers.Number):
-            alphas = [alphas]
+        if alphas is not None:
+            if not isinstance(alphas, collections.abc.Iterable):
+                alphas = [alphas]
         if self.loss is not None:
             loss = self.loss
         elif self.is_classif:
@@ -972,9 +974,10 @@ class SpaceNetClassifier(BaseSpaceNet):
     loss : string, optional (default "logistic")
         Loss to be used in the classifier. Must be one of "mse", or "logistic".
 
-    l1_ratios : float or list of floats in the interval [0, 1]; optional (default .5)
+    l1_ratios : float or list of floats in the interval [0, 1];
+    optional (default .5)
         Constant that mixes L1 and spatial prior terms in penalization.
-        l1_ratio == 1 corresponds to pure LASSO. The larger the value of this
+        l1_ratios == 1 corresponds to pure LASSO. The larger the value of this
         parameter, the sparser the estimated weights map. If list is provided,
         then the best value will be selected by cross-validation.
 
@@ -1178,9 +1181,10 @@ class SpaceNetRegressor(BaseSpaceNet):
     penalty : string, optional (default 'graph-net')
         Penalty to used in the model. Can be 'graph-net' or 'tv-l1'.
 
-    l1_ratios : float or list of floats in the interval [0, 1]; optional (default .5)
+    l1_ratios : float or list of floats in the interval [0, 1];
+    optional (default .5)
         Constant that mixes L1 and spatial prior terms in penalization.
-        l1_ratio == 1 corresponds to pure LASSO. The larger the value of this
+        l1_ratios == 1 corresponds to pure LASSO. The larger the value of this
         parameter, the sparser the estimated weights map. If list is provided,
         then the best value will be selected by cross-validation.
 


Test patch: