Repository: mne-tools/mne-python

Issue:
sys_info fails to find mne-qt-browser
It runs on my laptop but it took about a minute to print.

Also I get this

```
❯ mne sys_info
Platform:         macOS-13.2.1-arm64-arm-64bit
Python:           3.10.8 | packaged by conda-forge | (main, Nov 22 2022, 08:25:29) [Clang 14.0.6 ]
Executable:       /Users/agramfort/miniconda3/envs/mne/bin/python3.10
CPU:              arm (10 cores)
Memory:           32.0 GB

Core
├☑ mne:              1.4.dev0
├☑ numpy:            1.23.5 (OpenBLAS 0.3.21 with 10 threads)
├☑ scipy:            1.10.0
├☑ matplotlib:       3.7.0 (backend=MacOSX)
├☑ pooch:            1.6.0
└☑ jinja2:           3.1.2

Numerical (optional)
├☑ sklearn:          1.2.0
├☑ numba:            0.56.4
├☑ nibabel:          5.0.0
├☑ nilearn:          0.10.0
├☑ dipy:             1.5.0
├☑ openmeeg:         2.5.5
├☑ pandas:           1.5.3
└☐ unavailable:      cupy

Visualization (optional)
├☑ pyvista:          0.37.0 (OpenGL 4.1 Metal - 83 via Apple M1 Pro)
├☑ pyvistaqt:        0.9.0
├☑ ipyvtklink:       0.2.2
├☑ vtk:              9.2.5
├☑ qtpy:             2.3.0 (PyQt5=5.15.6)
├☑ ipympl:           0.9.2
├☑ pyqtgraph:        0.13.1
└☐ unavailable:      mne-qt-browser

Ecosystem (optional)
├☑ mne_bids:         0.13.dev0
├☑ mne_nirs:         0.6.dev0
└☐ unavailable:      mne_features, mne_connectivity, mne_icalabel
❯ which mne
/Users/agramfort/miniconda3/envs/mne/bin/mne
❯ mne browse_raw ~/mne_data/MNE-sample-data/MEG/sample/sample_audvis_raw.fif
❯ conda list | grep mne-qt
mne-qt-browser            0.4.0           py310hbe9552e_0    conda-forge
```

you can see that mne-qt-browser is installed and works but the new `mne sys_info` does not detect this. To me it's a bug...

_Originally posted by @agramfort in https://github.com/mne-tools/mne-python/issues/11568#issuecomment-1475991673_
            

Discussion:
agramfort: thx @cbrnr 

can we test this?
cbrnr: > can we test this?

Sure. Should I add a `requires_qt_browser` decorator (or whatever we want to name it)? We have some workflows that do include `mne-qt-browser`, so this should get tested at least once.

Patch:
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 51a65fc0d..bedd00825 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -123,7 +123,7 @@ stages:
     - bash: |
         set -e
         mne sys_info -pd
-        mne sys_info -pd | grep "qtpy: .*(PySide6=.*)$"
+        mne sys_info -pd | grep "qtpy .*(PySide6=.*)$"
       displayName: Print config
     # Uncomment if "xcb not found" Qt errors/segfaults come up again
     # - bash: |
@@ -198,7 +198,7 @@ stages:
     - bash: |
         set -e
         mne sys_info -pd
-        mne sys_info -pd | grep "qtpy: .* (PySide6=.*)$"
+        mne sys_info -pd | grep "qtpy .* (PySide6=.*)$"
         pytest -m "not slowtest" ${TEST_OPTIONS}
         python -m pip uninstall -yq PySide6
       displayName: 'PySide6'
@@ -206,7 +206,7 @@ stages:
         set -e
         python -m pip install PyQt6
         mne sys_info -pd
-        mne sys_info -pd | grep "qtpy: .* (PyQt6=.*)$"
+        mne sys_info -pd | grep "qtpy .* (PyQt6=.*)$"
         pytest -m "not slowtest" ${TEST_OPTIONS}
         python -m pip uninstall -yq PyQt6 PyQt6-sip PyQt6-Qt6
       displayName: 'PyQt6'
@@ -214,7 +214,7 @@ stages:
         set -e
         python -m pip install PySide2
         mne sys_info -pd
-        mne sys_info -pd | grep "qtpy: .* (PySide2=.*)$"
+        mne sys_info -pd | grep "qtpy .* (PySide2=.*)$"
         pytest -m "not slowtest" ${TEST_OPTIONS}
         python -m pip uninstall -yq PySide2
       displayName: 'PySide2'
@@ -223,7 +223,7 @@ stages:
         set -e
         python -m pip install PyQt5
         mne sys_info -pd
-        mne sys_info -pd | grep "qtpy: .* (PyQt5=.*)$"
+        mne sys_info -pd | grep "qtpy .* (PyQt5=.*)$"
         pytest -m "not slowtest" ${TEST_OPTIONS}
         python -m pip uninstall -yq PyQt5 PyQt5-sip PyQt5-Qt5
       displayName: 'PyQt5'
diff --git a/mne/utils/__init__.py b/mne/utils/__init__.py
index 93cac020c..9c0a443ae 100644
--- a/mne/utils/__init__.py
+++ b/mne/utils/__init__.py
@@ -52,7 +52,7 @@ from ._testing import (run_command_if_main, requires_sklearn,
                        assert_object_equal, assert_and_remove_boundary_annot,
                        _raw_annot, assert_dig_allclose, assert_meg_snr,
                        assert_snr, assert_stcs_equal, _click_ch_name,
-                       requires_openmeeg_mark)
+                       requires_openmeeg_mark, requires_mne_qt_browser)
 from .numerics import (hashfunc, _compute_row_norms,
                        _reg_pinv, random_permutation, _reject_data_segments,
                        compute_corr, _get_inst_data, array_split_idx,
diff --git a/mne/utils/config.py b/mne/utils/config.py
index 2be7fcce7..099f8333b 100644
--- a/mne/utils/config.py
+++ b/mne/utils/config.py
@@ -524,7 +524,7 @@ def sys_info(fid=None, show_paths=False, *, dependencies='user', unicode=True):
     """
     _validate_type(dependencies, str)
     _check_option('dependencies', dependencies, ('user', 'developer'))
-    ljust = 21 if dependencies == 'developer' else 18
+    ljust = 24 if dependencies == 'developer' else 21
     platform_str = platform.platform()
     if platform.system() == 'Darwin' and sys.version_info[:2] < (3, 8):
         # platform.platform() in Python < 3.8 doesn't call
@@ -538,12 +538,12 @@ def sys_info(fid=None, show_paths=False, *, dependencies='user', unicode=True):
         del macos_ver, macos_architecture
 
     out = partial(print, end='', file=fid)
-    out('Platform:'.ljust(ljust) + platform_str + '\n')
-    out('Python:'.ljust(ljust) + str(sys.version).replace('\n', ' ') + '\n')
-    out('Executable:'.ljust(ljust) + sys.executable + '\n')
-    out('CPU:'.ljust(ljust) + f'{platform.processor()} ')
+    out('Platform'.ljust(ljust) + platform_str + '\n')
+    out('Python'.ljust(ljust) + str(sys.version).replace('\n', ' ') + '\n')
+    out('Executable'.ljust(ljust) + sys.executable + '\n')
+    out('CPU'.ljust(ljust) + f'{platform.processor()} ')
     out(f'({multiprocessing.cpu_count()} cores)\n')
-    out('Memory:'.ljust(ljust))
+    out('Memory'.ljust(ljust))
     try:
         import psutil
     except ImportError:
@@ -551,6 +551,7 @@ def sys_info(fid=None, show_paths=False, *, dependencies='user', unicode=True):
     else:
         out(f'{psutil.virtual_memory().total / float(2 ** 30):0.1f} GB\n')
     out('\n')
+    ljust -= 3  # account for +/- symbols
     libs = _get_numpy_libs()
     unavailable = []
     use_mod_names = (
@@ -566,8 +567,8 @@ def sys_info(fid=None, show_paths=False, *, dependencies='user', unicode=True):
         'pyqtgraph', 'mne-qt-browser',
         '',
         '# Ecosystem (optional)',
-        'mne_bids', 'mne_nirs', 'mne_features', 'mne_connectivity',
-        'mne_icalabel',
+        'mne-bids', 'mne-nirs', 'mne-features', 'mne-connectivity',
+        'mne-icalabel',
         ''
     )
     if dependencies == 'developer':
@@ -576,7 +577,7 @@ def sys_info(fid=None, show_paths=False, *, dependencies='user', unicode=True):
             'pytest', 'nbclient', 'numpydoc', 'flake8', 'pydocstyle',
             '',
             '# Documentation',
-            'sphinx', 'sphinx_gallery', 'pydata_sphinx_theme',
+            'sphinx', 'sphinx-gallery', 'pydata-sphinx-theme',
             '',
         )
     try:
@@ -588,7 +589,7 @@ def sys_info(fid=None, show_paths=False, *, dependencies='user', unicode=True):
         if mod_name == '':  # break
             if unavailable:
                 out('└☐ ' if unicode else ' - ')
-                out('unavailable:'.ljust(ljust))
+                out('unavailable'.ljust(ljust))
                 out(f"{', '.join(unavailable)}\n")
                 unavailable = []
             if mi != len(use_mod_names) - 1:
@@ -603,12 +604,12 @@ def sys_info(fid=None, show_paths=False, *, dependencies='user', unicode=True):
         if last:
             pre = '└'
         try:
-            mod = import_module(mod_name)
+            mod = import_module(mod_name.replace("-", "_"))
         except Exception:
             unavailable.append(mod_name)
         else:
             out(f'{pre}☑ ' if unicode else ' + ')
-            out(f'{mod_name}:'.ljust(ljust))
+            out(f'{mod_name}'.ljust(ljust))
             if mod_name == 'vtk':
                 vtk_version = mod.vtkVersion()
                 # 9.0 dev has VersionFull but 9.0 doesn't


Test patch:
diff --git a/mne/utils/_testing.py b/mne/utils/_testing.py
index 233cbcff5..8b7bd10d4 100644
--- a/mne/utils/_testing.py
+++ b/mne/utils/_testing.py
@@ -130,6 +130,7 @@ requires_pandas = partial(requires_module, name='pandas')
 requires_pylsl = partial(requires_module, name='pylsl')
 requires_sklearn = partial(requires_module, name='sklearn')
 requires_mne = partial(requires_module, name='MNE-C', call=_mne_call)
+requires_mne_qt_browser = partial(requires_module, name='mne_qt_browser')
 
 
 def requires_mne_mark():
diff --git a/mne/utils/tests/test_config.py b/mne/utils/tests/test_config.py
index 5aff769e5..6b7df856f 100644
--- a/mne/utils/tests/test_config.py
+++ b/mne/utils/tests/test_config.py
@@ -5,7 +5,8 @@ from pathlib import Path
 
 from mne.utils import (set_config, get_config, get_config_path,
                        set_memmap_min_size, _get_stim_channel, sys_info,
-                       ClosingStringIO, get_subjects_dir)
+                       ClosingStringIO, get_subjects_dir,
+                       requires_mne_qt_browser)
 
 
 def test_config(tmp_path):
@@ -83,12 +84,21 @@ def test_sys_info():
     out = ClosingStringIO()
     sys_info(fid=out)
     out = out.getvalue()
-    assert ('numpy:' in out)
+    assert ('numpy' in out)
 
     if platform.system() == 'Darwin':
-        assert 'Platform:         macOS-' in out
+        assert 'Platform             macOS-' in out
     elif platform.system() == 'Linux':
-        assert 'Platform:         Linux' in out
+        assert 'Platform             Linux' in out
+
+
+@requires_mne_qt_browser
+def test_sys_info_qt_browser():
+    """Test if mne_qt_browser is correctly detected."""
+    out = ClosingStringIO()
+    sys_info(fid=out)
+    out = out.getvalue()
+    assert ('mne-qt-browser' in out)
 
 
 def test_get_subjects_dir(tmp_path, monkeypatch):