Repository: Delgan/loguru

Issue:
Support for FORCE_COLOR
https://force-color.org/

Similar to #1178 / #1299 but forcing color to be enabled rather than disabled, which is useful for non TTY environments that do support color.

Prior art:
- [pytest](https://github.com/pytest-dev/pytest/blob/b0caf3d7adc45f773177424593431869fd2f82d8/src/_pytest/_io/terminalwriter.py#L43)
- [rich](https://github.com/Textualize/rich/blob/43d3b04725ab9731727fb1126e35980c62f32377/rich/console.py#L952)
- [uv](https://github.com/astral-sh/uv/blob/6d3614eece09b35d250bd0c1fe35bcc56fffebcc/crates/uv/src/settings.rs#L99)


Discussion:

Patch:
diff --git a/loguru/_colorama.py b/loguru/_colorama.py
index b380536c..220ddd89 100644
--- a/loguru/_colorama.py
+++ b/loguru/_colorama.py
@@ -7,9 +7,16 @@ def should_colorize(stream):
     if stream is None:
         return False
 
-    if "NO_COLOR" in os.environ:
+    # Per the spec (https://no-color.org/), this needs to check for a
+    # non-empty string, not just presence of the variable:
+    if os.getenv("NO_COLOR"):
         return False
 
+    # Per the spec (https://force-color.org/), this needs to check for a
+    # non-empty string, not just presence of the variable:
+    if os.getenv("FORCE_COLOR"):
+        return True
+
     if getattr(builtins, "__IPYTHON__", False) and (stream is sys.stdout or stream is sys.stderr):
         try:
             import ipykernel


Test patch:
diff --git a/tests/test_colorama.py b/tests/test_colorama.py
index ab659e09..1aea8e5a 100644
--- a/tests/test_colorama.py
+++ b/tests/test_colorama.py
@@ -154,19 +154,49 @@ def test_dumb_term_not_colored(monkeypatch, patched, expected):
 
 
 @pytest.mark.parametrize(
-    ("patched", "expected"),
+    ("patched", "no_color", "expected"),
     [
-        ("__stdout__", False),
-        ("__stderr__", False),
-        ("stdout", False),
-        ("stderr", False),
-        ("", False),
+        ("__stdout__", "1", False),
+        ("__stderr__", "1", False),
+        ("stdout", "1", False),
+        ("stderr", "1", False),
+        ("", "1", False),
+        # An empty value for NO_COLOR should not be applied:
+        ("__stdout__", "", True),
+        ("__stderr__", "", True),
+        ("stdout", "", True),
+        ("stderr", "", True),
+        ("", "", True),
     ],
 )
-def test_honor_no_color_standard(monkeypatch, patched, expected):
+def test_honor_no_color_standard(monkeypatch, patched, no_color, expected):
     stream = StreamIsattyTrue()
     with monkeypatch.context() as context:
-        context.setitem(os.environ, "NO_COLOR", "1")
+        context.setitem(os.environ, "NO_COLOR", no_color)
+        context.setattr(sys, patched, stream, raising=False)
+        assert should_colorize(stream) is expected
+
+
+@pytest.mark.parametrize(
+    ("patched", "force_color", "expected"),
+    [
+        ("__stdout__", "1", True),
+        ("__stderr__", "1", True),
+        ("stdout", "1", True),
+        ("stderr", "1", True),
+        ("", "1", True),
+        # An empty value for FORCE_COLOR should not be applied:
+        ("__stdout__", "", False),
+        ("__stderr__", "", False),
+        ("stdout", "", False),
+        ("stderr", "", False),
+        ("", "", False),
+    ],
+)
+def test_honor_force_color_standard(monkeypatch, patched, force_color, expected):
+    stream = StreamIsattyFalse()
+    with monkeypatch.context() as context:
+        context.setitem(os.environ, "FORCE_COLOR", force_color)
         context.setattr(sys, patched, stream, raising=False)
         assert should_colorize(stream) is expected