Repository: Textualize/rich (python)

Issue:
[BUG] Double rendering of notebook output with `rich>=12.6.0`
- [x] I've checked [docs](https://rich.readthedocs.io/en/latest/introduction.html) and [closed issues](https://github.com/Textualize/rich/issues?q=is%3Aissue+is%3Aclosed) for possible solutions.
- [x] I can't find my issue in the [FAQ](https://github.com/Textualize/rich/blob/master/FAQ.md).

**Describe the bug**
It appears the double rendering issue referenced in #1737 has returned starting in version 12.6.0.

With a poetry environment given by:

```toml
[tool.poetry]
name = "rich-bug-repro"
version = "0.0.1"
description = "..."
authors = ["John Doe"]

[tool.poetry.dependencies]
python = "^3.9"
jupyter = "^1.0.0"
rich = {version = "x.x.x", extras=["jupyter"]}  # fill out with desired version
```

a simple reproducing example is (from a jupyter notebook)

```python
from rich.progress import track
import time

for i in track(range(100)):
    time.sleep(0.01)
```

when `version` is set to any of `12.6.0,13.0.0,13.0.1` in the `pyproject.toml` above, the output of this example looks like

![image](https://user-images.githubusercontent.com/14932242/211386667-5eec044c-778a-4c1e-87d0-156fab83e7cb.png)

When using `<12.6.0`, you get the expected
![image](https://user-images.githubusercontent.com/14932242/211387105-cf666de3-716c-4040-8d2a-2faf1de70a65.png)

Note that this is strictly a jupyter problem: for any of these versions, running e.g.
```console
python -c 'from rich.progress import track;import time;[time.sleep(0.01) for _ in track(range(100))]'
```
results in just a single progress bar being printed, as expected.

**Platform**
<details>
<summary>Click to expand</summary>

What platform (Win/Linux/Mac) are you running on? What terminal software are you using?
Linux, both natively and in WSL2 on Windows.

I may ask you to copy and paste the output of the following commands. It may save some time if you do it now.

If you're using Rich in a terminal:

```
python -m rich.diagnose
pip freeze | grep rich
```

If you're using Rich in a Jupyter Notebook, run the following snippet in a cell
and paste the output in your bug report.

```python
from rich.diagnose import report
report()
```
```console
╭────────────────────── <class 'rich.console.Console'> ──────────────────────╮
│ A high level console interface.                                            │
│                                                                            │
│ ╭────────────────────────────────────────────────────────────────────────╮ │
│ │ <console width=115 ColorSystem.TRUECOLOR>                              │ │
│ ╰────────────────────────────────────────────────────────────────────────╯ │
│                                                                            │
│     color_system = 'truecolor'                                             │
│         encoding = 'utf-8'                                                 │
│             file = <ipykernel.iostream.OutStream object at 0x7f3f783203d0> │
│           height = 100                                                     │
│    is_alt_screen = False                                                   │
│ is_dumb_terminal = False                                                   │
│   is_interactive = True                                                    │
│       is_jupyter = True                                                    │
│      is_terminal = True                                                    │
│   legacy_windows = False                                                   │
│         no_color = False                                                   │
│          options = ConsoleOptions(                                         │
│                        size=ConsoleDimensions(width=115, height=100),      │
│                        legacy_windows=False,                               │
│                        min_width=1,                                        │
│                        max_width=115,                                      │
│                        is_terminal=True,                                   │
│                        encoding='utf-8',                                   │
│                        max_height=100,                                     │
│                        justify=None,                                       │
│                        overflow=None,                                      │
│                        no_wrap=False,                                      │
│                        highlight=None,                                     │
│                        markup=None,                                        │
│                        height=None                                         │
│                    )                                                       │
│            quiet = False                                                   │
│           record = False                                                   │
│         safe_box = True                                                    │
│             size = ConsoleDimensions(width=115, height=100)                │
│        soft_wrap = False                                                   │
│           stderr = False                                                   │
│            style = None                                                    │
│         tab_size = 8                                                       │
│            width = 115                                                     │
╰────────────────────────────────────────────────────────────────────────────╯
╭─── <class 'rich._windows.WindowsConsoleFeatures'> ────╮
│ Windows features available.                           │
│                                                       │
│ ╭───────────────────────────────────────────────────╮ │
│ │ WindowsConsoleFeatures(vt=False, truecolor=False) │ │
│ ╰───────────────────────────────────────────────────╯ │
│                                                       │
│ truecolor = False                                     │
│        vt = False                                     │
╰───────────────────────────────────────────────────────╯
╭────── Environment Variables ───────╮
│ {                                  │
│     'TERM': 'xterm-color',         │
│     'COLORTERM': None,             │
│     'CLICOLOR': '1',               │
│     'NO_COLOR': None,              │
│     'TERM_PROGRAM': None,          │
│     'COLUMNS': None,               │
│     'LINES': None,                 │
│     'JUPYTER_COLUMNS': None,       │
│     'JUPYTER_LINES': None,         │
│     'JPY_PARENT_PID': '2054',      │
│     'VSCODE_VERBOSE_LOGGING': None │
│ }                                  │
╰────────────────────────────────────╯
platform="Linux"
```
</details>


Pull request description:
Fix duplicate progress output in Jupyter
## Type of changes

- [x] Bug fix
- [ ] New feature
- [ ] Documentation / docstrings
- [ ] Tests
- [ ] Other

## Checklist

- [x] I've run the latest [black](https://github.com/psf/black) with default args on new code.
- [x] I've updated CHANGELOG.md and CONTRIBUTORS.md where appropriate.
- [x] I've added tests for new code.
- [x] I accept that @willmcgugan may be pedantic in the code review.

## Description

#2449 caused FORCE_COLOR to forcefully imply a terminal. This environment variable tends to be set in a Jupyter context. When `Console.is_terminal` is True, duplicate output is produced by progress bars (#2740). I haven't tracked down exactly what output when `is_terminal=True` causes the duplicated output (likely something involving carriage returns or control characters, which Jupyter doesn't _always_ get right), but the fact remains that if you're in a Jupyter kernel, you aren't in a terminal.

This PR shifts the handling of FORCE_COLOR away from `_force_terminal` and into `is_terminal`, where other interactive environment detection happens (idle).

Sample output from b89d0362e8ebcb18902f0f0a206879f1829b5c0b:

<img width="727" alt="Screenshot 2023-02-13 at 00 00 52" src="https://user-images.githubusercontent.com/151929/218342603-1de83268-fad0-4bfe-bccd-fdac3cab87e2.png">

closes #2740

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5d527500..db52baa2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+
 ## [13.3.2] - Unreleased
 
 ### Fixed
@@ -13,10 +14,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Fix syntax error when building with nuitka https://github.com/Textualize/rich/pull/2635
 - Fixed pretty printing of empty dataclass https://github.com/Textualize/rich/issues/2819
 - Fixes superfluous spaces in html output https://github.com/Textualize/rich/issues/2832
+- Fixed duplicate output in Jupyter https://github.com/Textualize/rich/pulls/2804
 
 ### Added
 
 - Added Polish README
+
 ## [13.3.1] - 2023-01-28
 
 ### Fixed
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index f8c87edf..310463ba 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -43,6 +43,7 @@ The following people have contributed to the development of Rich:
 - [Kyle Pollina](https://github.com/kylepollina)
 - [Sebastián Ramírez](https://github.com/tiangolo)
 - [Felipe Guedes](https://github.com/guedesfelipe)
+- [Min RK](https://github.com/minrk)
 - [Clément Robert](https://github.com/neutrinoceros)
 - [Brian Rutledge](https://github.com/bhrutledge)
 - [Tushar Sadhwani](https://github.com/tusharsadhwani)
diff --git a/rich/console.py b/rich/console.py
index a3aaa83b..3cad3632 100644
--- a/rich/console.py
+++ b/rich/console.py
@@ -711,11 +711,6 @@ class Console:
         self._force_terminal = None
         if force_terminal is not None:
             self._force_terminal = force_terminal
-        else:
-            # If FORCE_COLOR env var has any value at all, we force terminal.
-            force_color = self._environ.get("FORCE_COLOR")
-            if force_color is not None:
-                self._force_terminal = True
 
         self._file = file
         self.quiet = quiet
@@ -949,6 +944,15 @@ class Console:
             # Return False for Idle which claims to be a tty but can't handle ansi codes
             return False
 
+        if self.is_jupyter:
+            # return False for Jupyter, which may have FORCE_COLOR set
+            return False
+
+        # If FORCE_COLOR env var has any value at all, we assume a terminal.
+        force_color = self._environ.get("FORCE_COLOR")
+        if force_color is not None:
+            self._force_terminal = True
+
         isatty: Optional[Callable[[], bool]] = getattr(self.file, "isatty", None)
         try:
             return False if isatty is None else isatty()


Test patch:
diff --git a/tests/test_console.py b/tests/test_console.py
index 22775215..3bcddefd 100644
--- a/tests/test_console.py
+++ b/tests/test_console.py
@@ -971,3 +971,11 @@ def test_force_color(env_value):
     # means is_terminal returns True.
     console = Console(file=io.StringIO(), _environ={"FORCE_COLOR": env_value})
     assert console.is_terminal
+
+
+def test_force_color_jupyter():
+    # FORCE_COLOR above doesn't happen in a Jupyter kernel
+    console = Console(
+        file=io.StringIO(), _environ={"FORCE_COLOR": "1"}, force_jupyter=True
+    )
+    assert not console.is_terminal