Repository: jeertmans/manim-slides (language: python)

Issue:
[FEATURE] `manim-slides checkhealth` command
### Terms

- [X] Checked the [existing issues](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue+label%3Adocumentation+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;

### Description

Currently, many issues lack information about the Manim Slides installation (correct venv, presence of one or multiple Manim bindings, Qt bindings) and I think a command that outputs this information would be of great help.

### Screenshots

_No response_

### Additional information

It would make sense to export the Manim and ManimGL version (at least inside the package) and change the issue template to ask for `manim-slides checkhealth`'s output instead of `pip freeze`.


Discussion / hints:

Fix patch:
diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
index d88d1b35..5b558032 100644
--- a/.github/ISSUE_TEMPLATE/bug.yml
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -73,22 +73,22 @@ body:
     description: |
       Please copy and paste the output of `python --version`.
       Make sure to activate your virtual environment first (if any).
-      This will be automatically formatted into code, so no need for backticks.
     placeholder: Python 3.11.8
   validations:
-    required: false
+    required: true
 
 - type: textarea
   id: venv
   attributes:
     label: Python environment
     description: |
-      Please copy and paste the output of `pip freeze`.
+      Please copy and paste the output of `manim-slides checkhealth`.
       Make sure to activate your virtual environment first (if any).
       This will be automatically formatted into code, so no need for backticks.
+      If Manim Slides installation failed, enter 'N/A' instead.
     render: shell
   validations:
-    required: false
+    required: true
 
 - type: dropdown
   id: platform
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 56755fff..f1213c70 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 (unreleased)=
 ## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.1.7...HEAD)
 
+(unreleased-added)=
+### Added
+
+- Added `manim-slides checkhealth` command to easily obtain important information
+  for debug purposes.
+  [#458](https://github.com/jeertmans/manim-slides/pull/458)
+
 (unreleased-chore)=
 ### Chore
 
diff --git a/manim_slides/__main__.py b/manim_slides/__main__.py
index 90932379..f90cd4d9 100644
--- a/manim_slides/__main__.py
+++ b/manim_slides/__main__.py
@@ -5,6 +5,7 @@
 from click_default_group import DefaultGroup
 
 from .__version__ import __version__
+from .checkhealth import checkhealth
 from .convert import convert
 from .logger import logger
 from .present import list_scenes, present
@@ -63,6 +64,7 @@ def cli(notify_outdated_version: bool) -> None:
 
 
 cli.add_command(convert)
+cli.add_command(checkhealth)
 cli.add_command(init)
 cli.add_command(list_scenes)
 cli.add_command(present)
diff --git a/manim_slides/checkhealth.py b/manim_slides/checkhealth.py
new file mode 100644
index 00000000..f79fa864
--- /dev/null
+++ b/manim_slides/checkhealth.py
@@ -0,0 +1,39 @@
+import sys
+
+import click
+
+from .__version__ import __version__
+
+
+@click.command()
+def checkhealth() -> None:
+    """Check Manim Slides' installation."""
+    click.echo(f"Manim Slides version: {__version__}")
+    click.echo(f"Python executable: {sys.executable}")
+    click.echo("Manim bindings:")
+
+    click.echo(f"Modules: {sys.modules.keys()}")
+
+    try:
+        from manim import __version__ as manimce_version
+
+        click.echo(f"\tmanim (version: {manimce_version})")
+    except ImportError:
+        click.secho("\tmanim not found", bold=True)
+
+    try:
+        from manimlib import __version__ as manimlib_version
+
+        click.echo(f"\tmanimgl (version: {manimlib_version})")
+    except ImportError:
+        click.secho("\tmanimgl not found", bold=True)
+
+    try:
+        from qtpy import API, QT_VERSION
+
+        click.echo(f"Qt API: {API} (version: {QT_VERSION})")
+    except ImportError:
+        click.secho(
+            "No Qt API found, some Manim Slides commands will not be available",
+            bold=True,
+        )
diff --git a/pyproject.toml b/pyproject.toml
index 9c9013c3..49b2b845 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,6 +33,7 @@ dependencies = [
   "rtoml==0.9.0;sys_platform=='win32' and python_version<'3.13'",
   "rtoml>=0.9.0;sys_platform!='win32' or python_version>='3.13'",
   "tqdm>=4.64.1",
+  "pytest-missing-modules>=0.1.0",
 ]
 description = "Tool for live presentations using manim"
 dynamic = ["readme", "version"]
@@ -217,6 +218,7 @@ isort = {known-first-party = ["manim_slides", "tests"]}
 dev-dependencies = [
   "bump-my-version>=0.20.3",
   "pre-commit>=3.5.0",
+  "setuptools>=73.0.1",
 ]
 managed = true
 
diff --git a/requirements-dev.lock b/requirements-dev.lock
index ed8cb3ae..51e3de58 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -7,7 +7,6 @@
 #   all-features: true
 #   with-sources: false
 #   generate-hashes: false
-#   universal: false
 
 -e file:.
 alabaster==1.0.0
@@ -49,12 +48,6 @@ click-default-group==1.2.4
     # via manim-slides
 cloup==3.0.5
     # via manim
-colorama==0.4.6
-    # via click
-    # via ipython
-    # via pytest
-    # via sphinx
-    # via tqdm
 colour==0.1.5
     # via manimgl
 comm==0.2.2
@@ -198,6 +191,7 @@ nodeenv==1.9.1
 numpy==1.24.0
     # via --override (workspace)
     # via contourpy
+    # via ipython
     # via isosurfaces
     # via manim
     # via manim-slides
@@ -205,6 +199,7 @@ numpy==1.24.0
     # via mapbox-earcut
     # via matplotlib
     # via moderngl-window
+    # via networkx
     # via pyrr
     # via scipy
 packaging==24.1
@@ -220,6 +215,8 @@ pandocfilters==1.5.1
     # via nbconvert
 parso==0.8.4
     # via jedi
+pexpect==4.9.0
+    # via ipython
 pillow==10.4.0
     # via manim
     # via manim-slides
@@ -243,6 +240,8 @@ prompt-toolkit==3.0.47
     # via questionary
 psutil==6.0.0
     # via ipykernel
+ptyprocess==0.7.0
+    # via pexpect
 pure-eval==0.2.3
     # via stack-data
 pycairo==1.26.1
@@ -294,11 +293,14 @@ pytest==8.3.2
     # via manim-slides
     # via pytest-cov
     # via pytest-env
+    # via pytest-missing-modules
     # via pytest-qt
 pytest-cov==5.0.0
     # via manim-slides
 pytest-env==1.1.3
     # via manim-slides
+pytest-missing-modules==0.1.0
+    # via manim-slides
 pytest-qt==4.4.0
     # via manim-slides
 python-dateutil==2.9.0.post0
@@ -308,9 +310,6 @@ python-dotenv==1.0.1
     # via pydantic-settings
 python-pptx==1.0.2
     # via manim-slides
-pywin32==306
-    # via jupyter-core
-    # via plumbum
 pyyaml==6.0.2
     # via manimgl
     # via myst-parser
@@ -347,6 +346,7 @@ scipy==1.14.1
 screeninfo==0.8.1
     # via manim
     # via manimgl
+setuptools==73.0.1
 shiboken6==6.7.2
     # via pyside6
     # via pyside6-addons
diff --git a/requirements.lock b/requirements.lock
index cef486b2..324dcc7b 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -7,7 +7,6 @@
 #   all-features: true
 #   with-sources: false
 #   generate-hashes: false
-#   universal: false
 
 -e file:.
 alabaster==1.0.0
@@ -42,12 +41,6 @@ click-default-group==1.2.4
     # via manim-slides
 cloup==3.0.5
     # via manim
-colorama==0.4.6
-    # via click
-    # via ipython
-    # via pytest
-    # via sphinx
-    # via tqdm
 colour==0.1.5
     # via manimgl
 comm==0.2.2
@@ -183,6 +176,7 @@ networkx==3.3
 numpy==1.24.0
     # via --override (workspace)
     # via contourpy
+    # via ipython
     # via isosurfaces
     # via manim
     # via manim-slides
@@ -190,6 +184,7 @@ numpy==1.24.0
     # via mapbox-earcut
     # via matplotlib
     # via moderngl-window
+    # via networkx
     # via pyrr
     # via scipy
 packaging==24.1
@@ -205,6 +200,8 @@ pandocfilters==1.5.1
     # via nbconvert
 parso==0.8.4
     # via jedi
+pexpect==4.9.0
+    # via ipython
 pillow==10.4.0
     # via manim
     # via manim-slides
@@ -225,6 +222,8 @@ prompt-toolkit==3.0.47
     # via ipython
 psutil==6.0.0
     # via ipykernel
+ptyprocess==0.7.0
+    # via pexpect
 pure-eval==0.2.3
     # via stack-data
 pycairo==1.26.1
@@ -272,11 +271,14 @@ pytest==8.3.2
     # via manim-slides
     # via pytest-cov
     # via pytest-env
+    # via pytest-missing-modules
     # via pytest-qt
 pytest-cov==5.0.0
     # via manim-slides
 pytest-env==1.1.3
     # via manim-slides
+pytest-missing-modules==0.1.0
+    # via manim-slides
 pytest-qt==4.4.0
     # via manim-slides
 python-dateutil==2.9.0.post0
@@ -284,9 +286,6 @@ python-dateutil==2.9.0.post0
     # via matplotlib
 python-pptx==1.0.2
     # via manim-slides
-pywin32==306
-    # via jupyter-core
-    # via plumbum
 pyyaml==6.0.2
     # via manimgl
     # via myst-parser


Test patch:
diff --git a/tests/test_checkhealth.py b/tests/test_checkhealth.py
new file mode 100644
index 00000000..39c8f59e
--- /dev/null
+++ b/tests/test_checkhealth.py
@@ -0,0 +1,66 @@
+import importlib.util
+import sys
+from itertools import chain, combinations
+
+import pytest
+from click.testing import CliRunner
+from pytest_missing_modules.plugin import MissingModulesContextGenerator
+
+from manim_slides.__version__ import __version__
+from manim_slides.checkhealth import checkhealth
+
+MANIM_NOT_INSTALLED = importlib.util.find_spec("manim") is None
+MANIMGL_NOT_INSTALLED = importlib.util.find_spec("manimlib") is None
+PYQT6_NOT_INSTALLED = importlib.util.find_spec("PyQt6") is None
+PYSIDE6_NOT_INSTALLED = importlib.util.find_spec("PySide6") is None
+
+
+@pytest.mark.filterwarnings("ignore:Selected binding 'pyqt6' could not be found")
+@pytest.mark.parametrize(
+    "names",
+    list(
+        chain.from_iterable(
+            combinations(("manim", "manimlib", "PyQt6", "PySide6"), r=r)
+            for r in range(0, 5)
+        )
+    ),
+)
+def test_checkhealth(
+    names: tuple[str, ...], missing_modules: MissingModulesContextGenerator
+) -> None:
+    runner = CliRunner()
+
+    manim_missing = "manim" in names or MANIM_NOT_INSTALLED
+    manimlib_missing = "manimlib" in names or MANIMGL_NOT_INSTALLED
+    pyqt6_missing = "PyQt6" in names or PYQT6_NOT_INSTALLED
+    pyside6_missing = "PySide6" in names or PYSIDE6_NOT_INSTALLED
+
+    if "qtpy" in sys.modules:
+        del sys.modules["qtpy"]  # Avoid using cached module
+
+    with missing_modules(*names):
+        result = runner.invoke(
+            checkhealth,
+            env={"QT_API": "pyqt6", "FORCE_QT_API": "1"},
+        )
+
+        assert result.exit_code == 0
+        assert f"Manim Slides version: {__version__}" in result.output
+        assert sys.executable in result.output
+
+        if manim_missing:
+            assert "manim not found" in result.output
+        else:
+            assert "manim (version:" in result.output
+
+        if manimlib_missing:
+            assert "manimgl not found" in result.output
+        else:
+            assert "manimgl (version:" in result.output
+
+        if pyqt6_missing and pyside6_missing:
+            assert "No Qt API found" in result.output
+        elif pyqt6_missing:
+            assert "Qt API: pyside6 (version:" in result.output
+        else:
+            assert "Qt API: pyqt6 (version:" in result.output