Repository: mne-tools/mne-python

Issue:
Arguments of the layout reader
While working on #11473, it was not that straightforward to add support for `Path` in the I/O roundtrip for layouts because of the strange arguments from [`read_layout`](https://mne.tools/dev/generated/mne.channels.read_layout.html#mne.channels.read_layout).

`kind` is supposed to be the name of the layout. According to the docstring, without the suffix.
`path` is supposed to be the folder in which the layout file is.

In reality, `kind` can be provided with or without suffix, and it can be the path to a layout file in which case the value provided in `path` is ignored. https://github.com/mne-tools/mne-python/blob/d740e47c8bbce81e6c0beda304748e606f20fc6c/mne/channels/tests/test_layout.py#L60-L61

------------------

I propose to add a new argument `fname`:

```
fname : path-like | str
    Either the path to a .lout or .lay file or the name of a built-in layout.
    c.f. Notes for a list of the available built-in layouts.
```

And make the list of built-in layouts include the suffix to reduce ambiguity. 
And to deprecate both `kind` and `path`. It's definitely backward incompatible, but it would simplify this reader.


Discussion:
I think it's useful to be able to just say `'standard-1020'` or whatever so that users don't have to know where the MNE built-in layouts are stored. Is there a way to keep that but otherwise do as you suggest?
> And make the list of built-in layouts include the suffix to reduce ambiguity.

This point can be omitted, in which case `fname` is either the path to a layout or the name of a layout.
SGTM.

Resolving patch:
diff --git a/mne/channels/layout.py b/mne/channels/layout.py
--- a/mne/channels/layout.py
+++ b/mne/channels/layout.py
@@ -163,21 +163,33 @@ def _read_lay(fname):
     return box, pos, names, ids
 
 
-def read_layout(kind, path=None, scale=True):
+def read_layout(fname=None, path="", scale=True, *, kind=None):
     """Read layout from a file.
 
     Parameters
     ----------
-    kind : str
-        The name of the ``.lout`` file (e.g. ``kind='Vectorview-all'`` for
-        ``'Vectorview-all.lout'``).
+    fname : path-like | str
+        Either the path to a ``.lout`` or ``.lay`` file or the name of a
+        built-in layout. c.f. Notes for a list of the available built-in
+        layouts.
     path : path-like | None
         The path of the folder containing the Layout file. Defaults to the
         ``mne/channels/data/layouts`` folder inside your mne-python
         installation.
+
+        .. deprecated:: v1.4
+           The ``kind`` and ``path`` parameters will be removed in version
+           1.5. Please use the ``fname`` parameter instead.
     scale : bool
         Apply useful scaling for out the box plotting using ``layout.pos``.
         Defaults to True.
+    kind : str | None
+        The name of the ``.lout`` file (e.g. ``kind='Vectorview-all'`` for
+        ``'Vectorview-all.lout'``).
+
+        .. deprecated:: v1.4
+           The ``kind`` and ``path`` parameters will be removed in version
+           1.5. Please use the ``fname`` parameter instead.
 
     Returns
     -------
@@ -190,7 +202,7 @@ def read_layout(kind, path=None, scale=True):
 
     Notes
     -----
-    Valid ``kind`` arguments are:
+    Valid ``fname`` arguments are:
 
     .. table::
        :widths: auto
@@ -241,22 +253,55 @@ def read_layout(kind, path=None, scale=True):
     """
     readers = {".lout": _read_lout, ".lay": _read_lay}
 
-    if path is None:
-        path = Path(__file__).parent / "data" / "layouts"
-    # kind should be the name as a string, but let's consider the case where
-    # the path to the file is provided instead.
-    kind = Path(kind)
-    if len(kind.suffix) == 0 and (path / kind.with_suffix(".lout")).exists():
-        kind = kind.with_suffix(".lout")
-    elif len(kind.suffix) == 0 and (path / kind.with_suffix(".lay")).exists():
-        kind = kind.with_suffix(".lay")
-
-    fname = kind if kind.exists() else path / kind.name
-    if fname.suffix not in (".lout", ".lay"):
-        raise ValueError(
-            "Unknown layout type. Should be of type .lout or .lay."
+    if fname is None:  # deprecated in 1.4
+        warn(
+            "Argument 'kind' and 'path' are deprecated in favor of 'fname'.",
+            DeprecationWarning,
         )
-    kind = fname.stem
+        if path == "" or path is None:
+            path = Path(__file__).parent / "data" / "layouts"
+        # kind should be the name as a string, but let's consider the case
+        # where the path to the file is provided instead.
+        kind = Path(kind)
+        if (
+            len(kind.suffix) == 0
+            and (path / kind.with_suffix(".lout")).exists()
+        ):
+            kind = kind.with_suffix(".lout")
+        elif (
+            len(kind.suffix) == 0
+            and (path / kind.with_suffix(".lay")).exists()
+        ):
+            kind = kind.with_suffix(".lay")
+
+        fname = kind if kind.exists() else path / kind.name
+        if fname.suffix not in (".lout", ".lay"):
+            raise ValueError(
+                "Unknown layout type. Should be of type .lout or .lay."
+            )
+        kind = fname.stem
+    else:
+        # to be removed along the deprecated argument
+        if kind is not None or path != "":
+            warn(
+                "Argument 'kind' and 'path' are deprecated in favor of "
+                "'fname' and should not be provided alongside 'fname'.",
+                DeprecationWarning,
+            )
+        if isinstance(fname, str):
+            # is it a built-in layout?
+            directory = Path(__file__).parent / "data" / "layouts"
+            if (directory / fname).exists():
+                fname = directory / fname
+            elif (directory / fname).with_suffix(".lout").exists():
+                fname = (directory / fname).with_suffix(".lout")
+            elif (directory / fname).with_suffix(".lay").exists():
+                fname = (directory / fname).with_suffix(".lay")
+        # if not, it must be a valid path provided as str or Path
+        fname = _check_fname(fname, "read", must_exist=True, name="layout")
+        # and it must have a valid extension
+        _check_option("fname extension", fname.suffix, readers)
+        kind = fname.stem
     box, pos, names, ids = readers[fname.suffix](fname)
 
     if scale:
@@ -495,7 +540,7 @@ def find_layout(info, ch_type=None, exclude='bads'):
         return generate_2d_layout(xy, ch_names=ch_names, name='custom',
                                   normalize=True)
 
-    layout = read_layout(layout_name)
+    layout = read_layout(fname=layout_name)
     if not is_old_vv:
         layout.names = _clean_names(layout.names, remove_whitespace=True)
     if has_CTF_grad:


Test patch:
diff --git a/mne/channels/tests/test_layout.py b/mne/channels/tests/test_layout.py
--- a/mne/channels/tests/test_layout.py
+++ b/mne/channels/tests/test_layout.py
@@ -54,22 +54,36 @@ def _get_test_info():
 
 def test_io_layout_lout(tmp_path):
     """Test IO with .lout files."""
-    layout = read_layout('Vectorview-all', scale=False)
-    layout.save(tmp_path / "foobar.lout")
+    layout = read_layout(fname="Vectorview-all", scale=False)
+    layout.save(tmp_path / "foobar.lout", overwrite=True)
     layout_read = read_layout(
-        tmp_path / "foobar.lout", path="./", scale=False
+        fname=tmp_path / "foobar.lout", scale=False,
     )
     assert_array_almost_equal(layout.pos, layout_read.pos, decimal=2)
     assert layout.names == layout_read.names
     assert "<Layout |" in layout.__repr__()
 
+    # deprecation
+    with pytest.warns(DeprecationWarning, match="should not be provided"):
+        layout_read = read_layout(
+            fname=tmp_path / "foobar.lout", kind="Vectorview-all", scale=False,
+        )
+    with pytest.warns(DeprecationWarning, match="should not be provided"):
+        layout_read = read_layout(
+            fname=tmp_path / "foobar.lout", path=None, scale=False,
+        )
+    with pytest.warns(
+        DeprecationWarning, match="'kind' and 'path' are deprecated"
+    ):
+        layout_read = read_layout(kind="Vectorview-all", scale=False)
+
 
 def test_io_layout_lay(tmp_path):
     """Test IO with .lay files."""
-    layout = read_layout('CTF151', scale=False)
+    layout = read_layout(fname="CTF151", scale=False)
     layout.save(str(tmp_path / "foobar.lay"))
     layout_read = read_layout(
-        str(tmp_path / "foobar.lay"), path="./", scale=False
+        fname=tmp_path / "foobar.lay", scale=False
     )
     assert_array_almost_equal(layout.pos, layout_read.pos, decimal=2)
     assert layout.names == layout_read.names
@@ -141,17 +155,17 @@ def test_find_topomap_coords():
 
 def test_make_eeg_layout(tmp_path):
     """Test creation of EEG layout."""
-    tmp_name = 'foo'
-    lout_name = 'test_raw'
-    lout_orig = read_layout(kind=lout_name, path=lout_path)
+    lout_orig = read_layout(fname=lout_path / "test_raw.lout")
     info = read_info(fif_fname)
-    info['bads'].append(info['ch_names'][360])
+    info["bads"].append(info["ch_names"][360])
     layout = make_eeg_layout(info, exclude=[])
-    assert_array_equal(len(layout.names), len([ch for ch in info['ch_names']
-                                               if ch.startswith('EE')]))
-    layout.save(str(tmp_path / (tmp_name + ".lout")))
-    lout_new = read_layout(kind=tmp_name, path=tmp_path, scale=False)
-    assert_array_equal(lout_new.kind, tmp_name)
+    assert_array_equal(
+        len(layout.names),
+        len([ch for ch in info["ch_names"] if ch.startswith("EE")]),
+    )
+    layout.save(str(tmp_path / "foo.lout"))
+    lout_new = read_layout(fname=tmp_path / "foo.lout", scale=False)
+    assert_array_equal(lout_new.kind, "foo")
     assert_allclose(layout.pos, lout_new.pos, atol=0.1)
     assert_array_equal(lout_orig.names, lout_new.names)
 
@@ -166,13 +180,11 @@ def test_make_eeg_layout(tmp_path):
 
 def test_make_grid_layout(tmp_path):
     """Test creation of grid layout."""
-    tmp_name = 'bar'
-    lout_name = 'test_ica'
-    lout_orig = read_layout(kind=lout_name, path=lout_path)
+    lout_orig = read_layout(fname=lout_path / "test_ica.lout")
     layout = make_grid_layout(_get_test_info())
-    layout.save(str(tmp_path / (tmp_name + ".lout")))
-    lout_new = read_layout(kind=tmp_name, path=tmp_path)
-    assert_array_equal(lout_new.kind, tmp_name)
+    layout.save(str(tmp_path / "bar.lout"))
+    lout_new = read_layout(fname=tmp_path / "bar.lout")
+    assert_array_equal(lout_new.kind, "bar")
     assert_array_equal(lout_orig.pos, lout_new.pos)
     assert_array_equal(lout_orig.names, lout_new.names)