Repository: mne-tools/mne-python

Issue:
Verbose not working with the Epochs.drop_bad method
#### Describe the bug
The `verbose` argument in not working in the [`drop_bad()`](https://mne.tools/dev/generated/mne.Epochs.html?highlight=drop_bad#mne.Epochs.drop_bad) method.


#### Steps to reproduce
```python
import os
import mne


sample_data_folder = mne.datasets.sample.data_path()
sample_data_raw_file = os.path.join(sample_data_folder, 'MEG', 'sample',
                                    'sample_audvis_raw.fif')
raw = mne.io.read_raw_fif(sample_data_raw_file, verbose=False).crop(tmax=60)
events = mne.find_events(raw, stim_channel='STI 014')
epochs = mne.Epochs(raw, events, tmin=-0.3, tmax=0.7)

epochs.verbose = False

epochs.drop_bad(verbose=True)
```


#### Expected results
The `verbose` argument set to `True` overwrites the `Epochs.verbose` attribute and prints the number of bad epochs dropped.
```
86 events found
Event IDs: [ 1  2  3  4  5 32]
Not setting metadata
Not setting metadata
86 matching events found
Applying baseline correction (mode: mean)
Created an SSP operator (subspace dimension = 3)
3 projection items activated
Loading data for 86 events and 601 original time points ...
1 bad epochs dropped
```

#### Actual results
The number of bad epochs dropped is not printed.
```
86 events found
Event IDs: [ 1  2  3  4  5 32]
Not setting metadata
Not setting metadata
86 matching events found
Applying baseline correction (mode: mean)
Created an SSP operator (subspace dimension = 3)
3 projection items activated
```


#### Additional information
It seems that in the source code of [`drop_bad()`](https://github.com/mne-tools/mne-python/blob/main/mne/epochs.py#L1184-L1224) the argument `verbose` which defaults to `None` is not used.



Discussion:
I confirm.  @HanBnrd are you up for making a PR to fix this?
Yes, I'll have a look at what needs to be modified to fix this.
It seems that this issue is also affecting other methods, such as [`Raw.load_data()`](https://mne.tools/dev/generated/mne.io.Raw.html#mne.io.Raw.load_data). The `verbose` argument is not taken into account with the following code:
```python
import os
import mne


sample_data_folder = mne.datasets.sample.data_path()
sample_data_raw_file = os.path.join(sample_data_folder, 'MEG', 'sample',
                                    'sample_audvis_raw.fif')
raw = mne.io.read_raw_fif(sample_data_raw_file, verbose=False).crop(tmax=60)

raw.load_data(verbose=True)
```
While this code prints info:
```python
import os
import mne


sample_data_folder = mne.datasets.sample.data_path()
sample_data_raw_file = os.path.join(sample_data_folder, 'MEG', 'sample',
                                    'sample_audvis_raw.fif')
raw = mne.io.read_raw_fif(sample_data_raw_file, verbose=False).crop(tmax=60)

raw.verbose = True

raw.load_data()
```
```
Reading 0 ... 36037  =      0.000 ...    60.000 secs...
```
---
Also this following code doesn't print info:
```python
import os
import mne


sample_data_folder = mne.datasets.sample.data_path()
sample_data_raw_file = os.path.join(sample_data_folder, 'MEG', 'sample',
                                    'sample_audvis_raw.fif')
raw = mne.io.read_raw_fif(sample_data_raw_file, verbose=False).crop(tmax=60)

mne.set_log_level(verbose=True)

raw.load_data()
```
Is the [`mne.set_log_level()`](https://mne.tools/dev/generated/mne.set_log_level.html#mne.set_log_level) function supposed to work in this case?
This issue seems to be related to the [`@verbose`](https://github.com/mne-tools/mne-python/blob/main/mne/utils/_logging.py#L58-L135) decorator not working in some cases (maybe working on functions or methods calling functions but not on proper methods?)...

An easy quick fix would be to explicitly use the `verbose` arguments in the methods instead of relying on `@verbose` but it is probably not a good long term solution to solve the issue on all the methods affected.
> Also this following code doesn't print info:

That one at least shouldn't based on how the code is designed. The precedence/order is meant to be:

- `verbose` arg is used if not None
- `verbose` attribute of a class is used if not None
- global verbose level is used

And in that last example, the `raw` gets initialized with `verbose=False`, so `raw.load_data()` should always be in non-verbose mode regardless of `set_log_level`.
(I can take a look at fixing the `verbose` decorator if it would help, since it does seem like the first/top-comment example is problematic)
> 
> 
> (I can take a look at fixing the `verbose` decorator if it would help, since it does seem like the first/top-comment example is problematic)

Thanks @larsoner, that would be nice if you have time as I'm not very familiar with it and it appears to be used as a decorator for a lot of functions/methods in MNE. Please let me know if I can help.

Resolving patch:
diff --git a/mne/epochs.py b/mne/epochs.py
--- a/mne/epochs.py
+++ b/mne/epochs.py
@@ -804,7 +804,7 @@ def _reject_setup(self, reject, flat):
                 reject_imax = idxs[-1]
             self._reject_time = slice(reject_imin, reject_imax)
 
-    @verbose
+    @verbose  # verbose is used by mne-realtime
     def _is_good_epoch(self, data, verbose=None):
         """Determine if epoch is good."""
         if isinstance(data, str):
@@ -1220,7 +1220,7 @@ def drop_bad(self, reject='existing', flat='existing', verbose=None):
                rej in (reject, flat)):
             raise ValueError('reject and flat, if strings, must be "existing"')
         self._reject_setup(reject, flat)
-        self._get_data(out=False)
+        self._get_data(out=False, verbose=verbose)
         return self
 
     def drop_log_stats(self, ignore=('IGNORED',)):
@@ -1414,7 +1414,8 @@ def _get_data(self, out=True, picks=None, item=None, verbose=None):
                     epoch = self._project_epoch(epoch_noproj)
 
                 epoch_out = epoch_noproj if self._do_delayed_proj else epoch
-                is_good, bad_tuple = self._is_good_epoch(epoch)
+                is_good, bad_tuple = self._is_good_epoch(
+                    epoch, verbose=verbose)
                 if not is_good:
                     assert isinstance(bad_tuple, tuple)
                     assert all(isinstance(x, str) for x in bad_tuple)
diff --git a/mne/io/base.py b/mne/io/base.py
--- a/mne/io/base.py
+++ b/mne/io/base.py
@@ -523,8 +523,7 @@ def load_data(self, verbose=None):
             self._preload_data(True)
         return self
 
-    @verbose
-    def _preload_data(self, preload, verbose=None):
+    def _preload_data(self, preload):
         """Actually preload the data."""
         data_buffer = preload
         if isinstance(preload, (bool, np.bool_)) and not preload:
diff --git a/mne/preprocessing/maxwell.py b/mne/preprocessing/maxwell.py
--- a/mne/preprocessing/maxwell.py
+++ b/mne/preprocessing/maxwell.py
@@ -771,7 +771,8 @@ def _copy_preload_add_channels(raw, add_channels, copy, info):
             raw._data = out_data
         else:
             logger.info(msg + 'loading raw data from disk')
-            raw._preload_data(out_data[:len(raw.ch_names)], verbose=False)
+            with use_log_level(False):
+                raw._preload_data(out_data[:len(raw.ch_names)])
             raw._data = out_data
         assert raw.preload is True
         off = len(raw.ch_names)


Test patch:
diff --git a/mne/utils/tests/test_logging.py b/mne/utils/tests/test_logging.py
--- a/mne/utils/tests/test_logging.py
+++ b/mne/utils/tests/test_logging.py
@@ -3,15 +3,18 @@
 import re
 import warnings
 
+import numpy as np
 import pytest
 
-from mne import read_evokeds
+from mne import read_evokeds, Epochs
+from mne.io import read_raw_fif
 from mne.utils import (warn, set_log_level, set_log_file, filter_out_warnings,
                        verbose, _get_call_line, use_log_level, catch_logging,
                        logger)
 from mne.utils._logging import _frame_info
 
 base_dir = op.join(op.dirname(__file__), '..', '..', 'io', 'tests', 'data')
+fname_raw = op.join(base_dir, 'test_raw.fif')
 fname_evoked = op.join(base_dir, 'test-ave.fif')
 fname_log = op.join(base_dir, 'test-ave.log')
 fname_log_2 = op.join(base_dir, 'test-ave-2.log')
@@ -139,6 +142,33 @@ def test_logging_options(tmpdir):
     assert log.getvalue() == ''
 
 
+@pytest.mark.parametrize('verbose', (True, False))
+def test_verbose_method(verbose):
+    """Test for gh-8772."""
+    # raw
+    raw = read_raw_fif(fname_raw, verbose=verbose)
+    with catch_logging() as log:
+        raw.load_data(verbose=True)
+    log = log.getvalue()
+    assert 'Reading 0 ... 14399' in log
+    with catch_logging() as log:
+        raw.load_data(verbose=False)
+    log = log.getvalue()
+    assert log == ''
+    # epochs
+    events = np.array([[raw.first_samp + 200, 0, 1]], int)
+    epochs = Epochs(raw, events, verbose=verbose)
+    with catch_logging() as log:
+        epochs.drop_bad(verbose=True)
+    log = log.getvalue()
+    assert '0 bad epochs dropped' in log
+    epochs = Epochs(raw, events, verbose=verbose)
+    with catch_logging() as log:
+        epochs.drop_bad(verbose=False)
+    log = log.getvalue()
+    assert log == ''
+
+
 def test_warn(capsys):
     """Test the smart warn() function."""
     with pytest.warns(RuntimeWarning, match='foo'):
@@ -177,11 +207,34 @@ def bad_verbose():
     class Okay:
 
         @verbose
-        def meth(self):  # allowed because it should just use self.verbose
-            pass
+        def meth_1(self):  # allowed because it should just use self.verbose
+            logger.info('meth_1')
+
+        @verbose
+        def meth_2(self, verbose=None):
+            logger.info('meth_2')
 
     o = Okay()
     with pytest.raises(RuntimeError, match=r'does not have self\.verbose'):
-        o.meth()  # should raise, no verbose attr yet
-    o.verbose = None
-    o.meth()
+        o.meth_1()  # should raise, no verbose attr yet
+    o.verbose = False
+    with catch_logging() as log:
+        o.meth_1()
+        o.meth_2()
+    log = log.getvalue()
+    assert log == ''
+    with catch_logging() as log:
+        o.meth_2(verbose=True)
+    log = log.getvalue()
+    assert 'meth_2' in log
+    o.verbose = True
+    with catch_logging() as log:
+        o.meth_1()
+        o.meth_2()
+    log = log.getvalue()
+    assert 'meth_1' in log
+    assert 'meth_2' in log
+    with catch_logging() as log:
+        o.meth_2(verbose=False)
+    log = log.getvalue()
+    assert log == ''