Repository: BlueBrain/Atlas-Download-Tools (language: python)

Issue:
Move the CLI subcommands to submodules
As our CLI grows it's becoming too messy to define all commands in the `cli` module.

I'm suggesting to add submodules in the following fashion:
```
atldld
  cli
    dataset
    info
    new_command
    ...
    root
```
The `root` module would contain the CLI entry point and look something like this:
```python
# atldld.cli.root
import click

from atldld.cli.dataset import dataset_group
from atldld.cli.info import info_group
from atldld.cli.new_command import new_command_group
...

@click.group()
def entry_point():
    """Run the command line interface for Atlas-Download-Tools."""

entry_point.add_command(dataset_group)
entry_point.add_command(info_group)
entry_point.add_command(new_command_group)
...
```


Discussion / hints:

Fix patch:
diff --git a/setup.py b/setup.py
index db1ebe4..47a81ee 100644
--- a/setup.py
+++ b/setup.py
@@ -71,7 +71,7 @@
     python_requires=python_requires,
     install_requires=install_requires,
     extras_require=extras_require,
-    entry_points={"console_scripts": ["atldld = atldld.cli:root"]},
+    entry_points={"console_scripts": ["atldld = atldld.cli.root:root_cmd"]},
     description=description,
     long_description=long_description,
     long_description_content_type="text/x-rst",
diff --git a/src/atldld/__main__.py b/src/atldld/__main__.py
index dca6be9..60d7783 100644
--- a/src/atldld/__main__.py
+++ b/src/atldld/__main__.py
@@ -24,6 +24,6 @@
 
     $ python -m pdb -m atldld
 """
-from atldld.cli import root
+from atldld.cli.root import root_cmd
 
-root()
+root_cmd()
diff --git a/src/atldld/cli/__init__.py b/src/atldld/cli/__init__.py
new file mode 100644
index 0000000..cc70eaf
--- /dev/null
+++ b/src/atldld/cli/__init__.py
@@ -0,0 +1,17 @@
+# The package atldld is a tool to download atlas data.
+#
+# Copyright (C) 2021 EPFL/Blue Brain Project
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+"""The command line interface (CLI) for Atlas-Download-Tools."""
diff --git a/src/atldld/cli.py b/src/atldld/cli/dataset.py
similarity index 81%
rename from src/atldld/cli.py
rename to src/atldld/cli/dataset.py
index 6a23603..a32e3d0 100644
--- a/src/atldld/cli.py
+++ b/src/atldld/cli/dataset.py
@@ -14,70 +14,13 @@
 #
 # You should have received a copy of the GNU Lesser General Public License
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
-"""The command line interface (CLI) for Atlas-Download-Tools."""
+"""Implementation of the "atldld dataset" subcommand."""
 import pathlib
 from typing import Any, Dict, Optional, Sequence
 
 import click
 
-import atldld
 
-
-@click.group(
-    help="""\
-    Atlas-Download-Tools command line interface.
-
-    For detailed instructions see the documentation of the corresponding sub-commands.
-    """
-)
-def root():
-    """Run the command line interface for Atlas-Download-Tools."""
-
-
-# ============================= Info subcommand ================================
-
-
-@click.group(help="Informational subcommands.")
-def info():
-    """Run informational subcommands."""
-
-
-@click.command(help="Version of Atlas-Download-Tools.")
-def version():
-    """Print the version of Atlas-Download-Tools."""
-    click.echo(f"Atlas-Download-Tools version {atldld.__version__}")
-
-
-@click.command(
-    name="cache",
-    help="""
-    Location of the atldld cache directory.
-
-    By default it is configured as a subdirectory of the OS-specific cache
-    directory. If the XDG_CACHE_HOME environment variable is set its value will
-    override the OS-specific cache directory.
-    """,
-)
-def cache_dir():
-    """Print the location of the global cache directory."""
-    import os
-
-    from atldld.config import user_cache_dir
-
-    if "XDG_CACHE_HOME" in os.environ:
-        suffix = " (configured via XDG_CACHE_HOME)"
-    else:
-        suffix = ""
-    click.secho(f"Location of the atldld cache{suffix}:", fg="green")
-    click.echo(str(user_cache_dir(create=False).resolve().as_uri()))
-
-
-root.add_command(info)
-info.add_command(version)
-info.add_command(cache_dir)
-
-
-# ============================= Dataset subcommand =============================
 def get_dataset_meta_or_abort(
     dataset_id: int, include: Optional[Sequence[str]] = None
 ) -> Dict[str, Any]:
@@ -128,12 +71,12 @@ def get_dataset_meta_or_abort(
     return meta
 
 
-@click.group(help="Commands related to atlas datasets")
-def dataset():
+@click.group("dataset", help="Commands related to atlas datasets")
+def dataset_cmd():
     """Run dataset subcommands."""
 
 
-@click.command("info", help="Get information for a given dataset ID")
+@dataset_cmd.command("info", help="Get information for a given dataset ID")
 @click.argument("dataset_id", type=int)
 def dataset_info(dataset_id):
     """Get information for a given dataset ID."""
@@ -168,7 +111,9 @@ def dataset_info(dataset_id):
     click.secho(textwrap.dedent(output).strip(), fg="green")
 
 
-@click.command("download", help="Download and synchronize an entire section dataset")
+@dataset_cmd.command(
+    "download", help="Download and synchronize an entire section dataset"
+)
 @click.argument("dataset_id", type=str)
 @click.argument(
     "output_folder",
@@ -274,7 +219,7 @@ def dataset_download(
         json.dump(metadata, f, indent=4)
 
 
-@click.command("preview", help="Plot a preview of dataset slices")
+@dataset_cmd.command("preview", help="Plot a preview of dataset slices")
 @click.argument("dataset_id", type=int)
 @click.option(
     "-o",
@@ -323,9 +268,3 @@ def dataset_preview(dataset_id, output_dir):
     fig.savefig(img_path)
     click.secho("Figure was saved in ", fg="green", nl=False)
     click.secho(f"{img_path.resolve().as_uri()}", fg="yellow", bold=True)
-
-
-root.add_command(dataset)
-dataset.add_command(dataset_info)
-dataset.add_command(dataset_download)
-dataset.add_command(dataset_preview)
diff --git a/src/atldld/cli/info.py b/src/atldld/cli/info.py
new file mode 100644
index 0000000..bc017d3
--- /dev/null
+++ b/src/atldld/cli/info.py
@@ -0,0 +1,55 @@
+# The package atldld is a tool to download atlas data.
+#
+# Copyright (C) 2021 EPFL/Blue Brain Project
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+"""Implementation of the "atldld info" subcommand."""
+import click
+
+import atldld
+
+
+@click.group("info", help="Informational subcommands.")
+def info_cmd():
+    """Run informational subcommands."""
+
+
+@info_cmd.command("version", help="Version of Atlas-Download-Tools.")
+def info_version():
+    """Print the version of Atlas-Download-Tools."""
+    click.echo(f"Atlas-Download-Tools version {atldld.__version__}")
+
+
+@info_cmd.command(
+    name="cache",
+    help="""
+    Location of the atldld cache directory.
+
+    By default it is configured as a subdirectory of the OS-specific cache
+    directory. If the XDG_CACHE_HOME environment variable is set its value will
+    override the OS-specific cache directory.
+    """,
+)
+def info_cache():
+    """Print the location of the global cache directory."""
+    import os
+
+    from atldld.config import user_cache_dir
+
+    if "XDG_CACHE_HOME" in os.environ:
+        suffix = " (configured via XDG_CACHE_HOME)"
+    else:
+        suffix = ""
+    click.secho(f"Location of the atldld cache{suffix}:", fg="green")
+    click.echo(str(user_cache_dir(create=False).resolve().as_uri()))
diff --git a/src/atldld/cli/root.py b/src/atldld/cli/root.py
new file mode 100644
index 0000000..8efc5c6
--- /dev/null
+++ b/src/atldld/cli/root.py
@@ -0,0 +1,48 @@
+# The package atldld is a tool to download atlas data.
+#
+# Copyright (C) 2021 EPFL/Blue Brain Project
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+"""Implementation of the "atldld" command line entrypoint.
+
+We follow a particular naming for the functions that implement the CLI commands.
+Given the command `atldld group command` the function representing the `group`
+subcommand shall be named `group_cmd` and the function that implements `command`
+shall be named `group_command`. Both `group_cmd` and `group_command` shall be
+placed in the submodule `atldld.cli.group`.
+
+For example, the `atldld dataset download` command leads to the following
+functions:
+* `atldld.cli.dataset::dataset_cmd` (subcommand group)
+* `atldld.cli.dataset::dataset_download` (`download` command implementation)
+"""
+import click
+
+from atldld.cli.dataset import dataset_cmd
+from atldld.cli.info import info_cmd
+
+
+@click.group(
+    help="""\
+    Atlas-Download-Tools command line interface.
+
+    For detailed instructions see the documentation of the corresponding sub-commands.
+    """
+)
+def root_cmd():
+    """Run the command line interface for Atlas-Download-Tools."""
+
+
+root_cmd.add_command(info_cmd)
+root_cmd.add_command(dataset_cmd)


Test patch:
diff --git a/tests/test_cli.py b/tests/cli/test_dataset_cmd.py
similarity index 85%
rename from tests/test_cli.py
rename to tests/cli/test_dataset_cmd.py
index 86c534d..79d578c 100644
--- a/tests/test_cli.py
+++ b/tests/cli/test_dataset_cmd.py
@@ -1,7 +1,22 @@
+# The package atldld is a tool to download atlas data.
+#
+# Copyright (C) 2021 EPFL/Blue Brain Project
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
 import json
 import pathlib
 import re
-import subprocess
 import textwrap
 from collections import defaultdict
 from typing import Any, Dict
@@ -12,75 +27,21 @@
 import responses
 from click.testing import CliRunner
 
-import atldld
 from atldld.base import DisplacementField
-from atldld.cli import (
-    dataset,
+from atldld.cli.dataset import (
+    dataset_cmd,
     dataset_download,
     dataset_info,
     dataset_preview,
     get_dataset_meta_or_abort,
-    info,
-    root,
 )
 from atldld.sync import DatasetNotFoundError
 
 
-def test_cli_entrypoint_is_installed():
-    subprocess.check_call("atldld")
-
-
-class TestCliRoot:
-    def test_running_without_arguments_prints_help(self):
-        runner = CliRunner()
-        result = runner.invoke(root)
-        assert result.exit_code == 0
-        assert result.output.startswith("Usage:")
-
-    @pytest.mark.parametrize("subgroup", ("info", "dataset"))
-    def test_subgroup_installed(self, subgroup):
-        runner = CliRunner()
-        result = runner.invoke(root, [subgroup])
-        assert result.exit_code == 0
-
-
-class TestInfoSubgroup:
-    def test_running_without_arguments_prints_help(self):
-        runner = CliRunner()
-        result = runner.invoke(info)
-        assert result.exit_code == 0
-        assert result.output.startswith("Usage:")
-
-    def test_version_command_works(self):
-        runner = CliRunner()
-        result = runner.invoke(info, ["version"])
-        assert result.exit_code == 0
-        assert atldld.__version__ in result.output
-
-    def test_cache_command_works(self):
-        runner = CliRunner()
-        result = runner.invoke(info, ["cache"])
-        assert result.exit_code == 0
-        assert "atldld cache" in result.output.lower()
-
-    def test_cache_command_shows_xdg(self, monkeypatch, tmpdir):
-        runner = CliRunner()
-
-        monkeypatch.delenv("XDG_CACHE_HOME")
-        result = runner.invoke(info, ["cache"])
-        assert result.exit_code == 0
-        assert "XDG_CACHE_HOME" not in result.output
-
-        monkeypatch.setenv("XDG_CACHE_HOME", str(tmpdir))
-        result = runner.invoke(info, ["cache"])
-        assert result.exit_code == 0
-        assert "XDG_CACHE_HOME" in result.output
-
-
 class TestDatasetSubgroup:
     def test_running_without_arguments_prints_help(self):
         runner = CliRunner()
-        result = runner.invoke(dataset)
+        result = runner.invoke(dataset_cmd)
         assert result.exit_code == 0
         assert result.output.startswith("Usage:")
 
diff --git a/tests/cli/test_info_cmd.py b/tests/cli/test_info_cmd.py
new file mode 100644
index 0000000..56f34d3
--- /dev/null
+++ b/tests/cli/test_info_cmd.py
@@ -0,0 +1,54 @@
+# The package atldld is a tool to download atlas data.
+#
+# Copyright (C) 2021 EPFL/Blue Brain Project
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+from click.testing import CliRunner
+
+import atldld
+from atldld.cli.info import info_cmd
+from atldld.cli.root import root_cmd
+
+
+class TestInfoSubgroup:
+    def test_running_without_arguments_prints_help(self):
+        runner = CliRunner()
+        result = runner.invoke(root_cmd)
+        assert result.exit_code == 0
+        assert result.output.startswith("Usage:")
+
+    def test_version_command_works(self):
+        runner = CliRunner()
+        result = runner.invoke(info_cmd, ["version"])
+        assert result.exit_code == 0
+        assert atldld.__version__ in result.output
+
+    def test_cache_command_works(self):
+        runner = CliRunner()
+        result = runner.invoke(info_cmd, ["cache"])
+        assert result.exit_code == 0
+        assert "atldld cache" in result.output.lower()
+
+    def test_cache_command_shows_xdg(self, monkeypatch, tmpdir):
+        runner = CliRunner()
+
+        monkeypatch.delenv("XDG_CACHE_HOME")
+        result = runner.invoke(info_cmd, ["cache"])
+        assert result.exit_code == 0
+        assert "XDG_CACHE_HOME" not in result.output
+
+        monkeypatch.setenv("XDG_CACHE_HOME", str(tmpdir))
+        result = runner.invoke(info_cmd, ["cache"])
+        assert result.exit_code == 0
+        assert "XDG_CACHE_HOME" in result.output
diff --git a/tests/cli/test_root.py b/tests/cli/test_root.py
new file mode 100644
index 0000000..8068bb1
--- /dev/null
+++ b/tests/cli/test_root.py
@@ -0,0 +1,40 @@
+# The package atldld is a tool to download atlas data.
+#
+# Copyright (C) 2021 EPFL/Blue Brain Project
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+import subprocess
+
+import pytest
+from click.testing import CliRunner
+
+from atldld.cli.root import root_cmd
+
+
+def test_cli_entrypoint_is_installed():
+    subprocess.check_call("atldld")
+
+
+class TestCliRoot:
+    def test_running_without_arguments_prints_help(self):
+        runner = CliRunner()
+        result = runner.invoke(root_cmd)
+        assert result.exit_code == 0
+        assert result.output.startswith("Usage:")
+
+    @pytest.mark.parametrize("subgroup", ("info", "dataset"))
+    def test_subgroup_installed(self, subgroup):
+        runner = CliRunner()
+        result = runner.invoke(root_cmd, [subgroup])
+        assert result.exit_code == 0
diff --git a/tests/test_plot.py b/tests/test_plot.py
index 3e44040..282eee9 100644
--- a/tests/test_plot.py
+++ b/tests/test_plot.py
@@ -1,3 +1,19 @@
+# The package atldld is a tool to download atlas data.
+#
+# Copyright (C) 2021 EPFL/Blue Brain Project
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
 import numpy as np
 from matplotlib.figure import Figure