From 7820bd6aba147bd85424676a59e9b14c50473bb4 Mon Sep 17 00:00:00 2001
From: Antti Kaihola <13725+akaihola@users.noreply.github.com>
Date: Mon, 28 Oct 2024 22:09:20 +0200
Subject: [PATCH] feat: verbose error if Black not found

---
 src/darker/formatters/black_formatter.py | 34 +++++++++++++++++++-----
 1 file changed, 27 insertions(+), 7 deletions(-)

diff --git a/src/darker/formatters/black_formatter.py b/src/darker/formatters/black_formatter.py
index a4af39eb1..6bcdd13e4 100644
--- a/src/darker/formatters/black_formatter.py
+++ b/src/darker/formatters/black_formatter.py
@@ -37,10 +37,13 @@
 from __future__ import annotations
 
 import logging
+import sys
 from typing import TYPE_CHECKING, TypedDict
 
+from darker.exceptions import DependencyError
 from darker.files import find_pyproject_toml
 from darker.formatters.base_formatter import BaseFormatter
+from darkgraylib.command_line import EXIT_CODE_DEPENDENCY
 from darkgraylib.config import ConfigurationError
 from darkgraylib.utils import TextDocument
 
@@ -90,11 +93,26 @@ def read_config(self, src: tuple[str, ...], args: Namespace) -> None:
         self._read_cli_args(args)
 
     def _read_config_file(self, config_path: str) -> None:  # noqa: C901
-        # Local import so Darker can be run without Black installed
-        from black import (  # pylint: disable=import-outside-toplevel
-            parse_pyproject_toml,
-            re_compile_maybe_verbose,
-        )
+        # Local import so Darker can be run without Black installed.
+        # Do error handling here. This is the first Black importing method being hit.
+        try:
+            from black import (  # pylint: disable=import-outside-toplevel
+                parse_pyproject_toml,
+                re_compile_maybe_verbose,
+            )
+        except ImportError as exc:
+            logger.warning(
+                "To re-format code using Black, install it using e.g."
+                " `pip install 'darker[black]'` or"
+                " `pip install black`"
+            )
+            logger.warning(
+                "To use a different formatter or no formatter, select it on the"
+                " command line (e.g. `--formatter=none`) or configuration"
+                " (e.g. `formatter=none`)"
+            )
+            message = "Can't find the Black package"
+            raise DependencyError(message) from exc
 
         raw_config = parse_pyproject_toml(config_path)
         if "line_length" in raw_config:
@@ -153,7 +171,8 @@ def run(self, content: TextDocument) -> TextDocument:
         :return: The reformatted content
 
         """
-        # Local import so Darker can be run without Black installed
+        # Local import so Darker can be run without Black installed.
+        # No need for error handling, already done in `BlackFormatter.read_config`.
         from black import format_str  # pylint: disable=import-outside-toplevel
 
         contents_for_black = content.string_with_newline("\n")
@@ -177,7 +196,8 @@ def _make_black_options(self) -> Mode:
         # pass them to Black's ``format_str()``. File exclusion options aren't needed
         # since at this point we already have a single file's content to work on.
 
-        # Local import so Darker can be run without Black installed
+        # Local import so Darker can be run without Black installed.
+        # No need for error handling, already done in `BlackFormatter.read_config`.
         from black import FileMode as Mode  # pylint: disable=import-outside-toplevel
         from black import TargetVersion  # pylint: disable=import-outside-toplevel