Repository: pallets/werkzeug

Issue:
Fails to color Windows console
On windows the new Flask prints weird characters on console, instead of coloring the line as on linux. eg: "←[37m

I would rather go black/white (like in previous version) if coloring doesnt work.

2020-05-03 14:05:47,398 - INFO - 127.0.0.1 - - [03/May/2020 14:05:47] "←[37mGET /result HTTP/1.1←[0m" 200 -
2020-05-03 14:06:42,922 - INFO - 127.0.0.1 - - [03/May/2020 14:06:42] "←[37mGET /result HTTP/1.1←[0m" 200 -
2020-05-03 14:07:24,007 - INFO - 127.0.0.1 - - [03/May/2020 14:07:24] "←[37mGET /result HTTP/1.1←[0m" 200 -
2020-05-03 14:07:24,210 - INFO - 127.0.0.1 - - [03/May/2020 14:07:24] "←[37mGET /static/images/favicon.ico HTT
2020-05-03 14:08:19,539 - INFO - 127.0.0.1 - - [03/May/2020 14:08:19] "←[37mGET /result HTTP/1.1←[0m" 200 -
2020-05-03 14:09:15,065 - INFO - 127.0.0.1 - - [03/May/2020 14:09:15] "←[37mGET /result HTTP/1.1←[0m" 200 -
2020-05-03 14:10:10,579 - INFO - 127.0.0.1 - - [03/May/2020 14:10:10] "←[37mGET /result HTTP/1.1←[0m" 200 -

Env:
Flask==1.1.2
Win7 x64
Python 3.6.7 x64
Werkzeug==1.0.1




Discussion:
I was unable to duplicate. 

Command Prompt view: 
![image](https://user-images.githubusercontent.com/35072659/81009627-9cd94500-8e1a-11ea-956d-f2eb22bf1575.png)

Pycharm Terminal: 
![image](https://user-images.githubusercontent.com/35072659/81009734-c98d5c80-8e1a-11ea-9620-80b022e1f290.png)

PyCharm Run: 
![image](https://user-images.githubusercontent.com/35072659/81009826-f477b080-8e1a-11ea-8a10-2a68b7073849.png)

I'm running Windows 10 x64, Python 3.7.4, flask 1.1.2, Werkzeug 1.0.1





I'm not really sure how to explain this. If anything, is an issue with Click, but Click isn't supposed to emit color info on Windows if colorama isn't installed. I have seen more reports of this since switching to Click, but it's not consistent, as shown in the previous screenshots.
Tested on Win7 and Win10 same issue, although I see coloring working, just not for REST calls:
![2020-05-04_215343](https://user-images.githubusercontent.com/4163396/81012651-d5484580-8e51-11ea-806d-854a5e1f77b1.jpg)

I have these installed:
click==7.1.1
colorama==0.4.3
coloredlogs==14.0

These all work with coloring:
from colorama import Fore, Back, Style
print(Fore.RED + 'some red text')
print('\033[31m' + 'some red text')

import click
click.echo('\033[31m' + 'some red text')

from Flask:
 "←[37mGET "  <- this might be escaped invalidly in the code (needs double back slashes?), it worked for me with single
I was able to correct the issue by adding the following code to my flask app

```python
import os
import sys

if sys.platform.lower() == "win32": 
    os.system('color')
```
See https://stackoverflow.com/questions/287871/how-to-print-colored-text-in-python for details.

It seems that the double brackets are being filtered in the logs as @mrx23dot suggested.
I spent some more time investigating this issue. The color addition comes from `click.style`. Here's the relevant code from `serving.WSGIRequestHandler`:

```python
def log_request(self, code="-", size="-") -> None:
        try:
            path = uri_to_iri(self.path)
            msg = f"{self.command} {path} {self.request_version}"
        except AttributeError:
            # path isn't set if the requestline was bad
            msg = self.requestline

        code = str(code)

        if click:
            color = click.style

            if code[0] == "1":  # 1xx - Informational
                msg = color(msg, bold=True)
            elif code[0] == "2":  # 2xx - Success
                msg = color(msg, fg="white")
            elif code == "304":  # 304 - Resource Not Modified
                msg = color(msg, fg="cyan")
            elif code[0] == "3":  # 3xx - Redirection
                msg = color(msg, fg="green")
            elif code == "404":  # 404 - Resource Not Found
                msg = color(msg, fg="yellow")
            elif code[0] == "4":  # 4xx - Client Error
                msg = color(msg, fg="red", bold=True)
            else:  # 5xx, or any other response
                msg = color(msg, fg="magenta", bold=True)

        self.log("info", '"%s" %s %s', msg, code, size)
``` 

You can see that the `click.style` function is simply wrapping the initial message in ANSI style codes. For Linux and OS X, ANSI codes are supported by default, so those users won't notice this issue. However, on Windows, the terminal does not support this by default. If using Windows 10, a simple workaround is to use the following command to enable ANSI styling:

```python
import sys
import os

if sys.platform.lower() == 'win32':
    os.system('color')
```

This will address most users with the issue, but might not fix some edge cases (e.g. small-footprint or embedded unix distros). The `os.system('color')`  (or really any `os.system` call) can be put almost anywhere in the code base. As long as it is called before the output is printed, the output will show up properly colored. This is probably why users of PyCharm don't notice the bug. The PyCharm python terminal likely makes an `os.system` call to handle colored text output.

So the way I see it there are 2 questions:
1.  Should werkzeug fix this minor issue or leave the burden on Windows 10 users to call `os.system`?
2. If we fix this in werkzeug, where should we add the `os.system` call? It only needs to be called once, so perhaps it could be added to `WSGIRequestHandler.__init__`? There are probably better options, but that's all I can think of right now.

 
> I was able to correct the issue by adding the following code to my flask app
> 
> ```python
> import os
> import sys
> 
> if sys.platform.lower() == "win32": 
>     os.system('color')
> ```
> 
> See https://stackoverflow.com/questions/287871/how-to-print-colored-text-in-python for details.
> 
> It seems that the double brackets are being filtered in the logs as @mrx23dot suggested.

Thanks. add the code in config.py , it's works for me. 😀
I looked into the `os.system` hack, and it appears to be an unintended side effect of how cpython interacts with the Windows API, leaving some global state changed after the process ends. I don't want to rely on that.

When I switched to Click for styling, I thought it would handle not setting styles on Windows, but it turns out `click.style` isn't concerned with that (and can't really be), it's `click.echo` that handles stripping the ANSI codes if needed based on the output stream. Also, Click doesn't call `colorama.init()` because it wants to avoid changing global state, instead `click.echo` wraps the stream with colorama locally.

Since the `logging` module uses its own output system (`StreamHandler` in this case), colorama would have to be enabled globally, or the handler could be replaced with a custom one that wraps the stream or strips the codes. The problem with that is that if users configure their own logging (such as adding a root handler), they suddenly have to deal with the same issue.

Resolving patch:
diff --git a/src/werkzeug/_internal.py b/src/werkzeug/_internal.py
--- a/src/werkzeug/_internal.py
+++ b/src/werkzeug/_internal.py
@@ -183,6 +183,20 @@ def _has_level_handler(logger: logging.Logger) -> bool:
     return False
 
 
+class _ColorStreamHandler(logging.StreamHandler):
+    """On Windows, wrap stream with Colorama for ANSI style support."""
+
+    def __init__(self):
+        try:
+            import colorama
+        except ImportError:
+            stream = None
+        else:
+            stream = colorama.AnsiToWin32(sys.stderr)
+
+        super().__init__(stream)
+
+
 def _log(type: str, message: str, *args, **kwargs) -> None:
     """Log a message to the 'werkzeug' logger.
 
@@ -200,7 +214,7 @@ def _log(type: str, message: str, *args, **kwargs) -> None:
             _logger.setLevel(logging.INFO)
 
         if not _has_level_handler(_logger):
-            _logger.addHandler(logging.StreamHandler())
+            _logger.addHandler(_ColorStreamHandler())
 
     getattr(_logger, type)(message.rstrip(), *args, **kwargs)
 
diff --git a/src/werkzeug/serving.py b/src/werkzeug/serving.py
--- a/src/werkzeug/serving.py
+++ b/src/werkzeug/serving.py
@@ -42,10 +42,13 @@ def __getattr__(self, name):
 
     ssl = _SslDummy()  # type: ignore
 
-try:
-    import click
-except ImportError:
-    click = None  # type: ignore
+_log_add_style = True
+
+if os.name == "nt":
+    try:
+        __import__("colorama")
+    except ImportError:
+        _log_add_style = False
 
 can_fork = hasattr(os, "fork")
 
@@ -392,23 +395,21 @@ def log_request(self, code: t.Union[int, str] = "-", size: t.Union[int, str] = "
 
         code = str(code)
 
-        if click:
-            color = click.style
-
+        if _log_add_style:
             if code[0] == "1":  # 1xx - Informational
-                msg = color(msg, bold=True)
-            elif code[0] == "2":  # 2xx - Success
-                msg = color(msg)
+                msg = _ansi_style(msg, "bold")
+            elif code == "200":  # 2xx - Success
+                pass
             elif code == "304":  # 304 - Resource Not Modified
-                msg = color(msg, fg="cyan")
+                msg = _ansi_style(msg, "cyan")
             elif code[0] == "3":  # 3xx - Redirection
-                msg = color(msg, fg="green")
+                msg = _ansi_style(msg, "green")
             elif code == "404":  # 404 - Resource Not Found
-                msg = color(msg, fg="yellow")
+                msg = _ansi_style(msg, "yellow")
             elif code[0] == "4":  # 4xx - Client Error
-                msg = color(msg, fg="red", bold=True)
+                msg = _ansi_style(msg, "bold", "red")
             else:  # 5xx, or any other response
-                msg = color(msg, fg="magenta", bold=True)
+                msg = _ansi_style(msg, "bold", "magenta")
 
         self.log("info", '"%s" %s %s', msg, code, size)
 
@@ -426,6 +427,22 @@ def log(self, type: str, message: str, *args) -> None:
         )
 
 
+def _ansi_style(value, *styles):
+    codes = {
+        "bold": 1,
+        "red": 31,
+        "green": 32,
+        "yellow": 33,
+        "magenta": 35,
+        "cyan": 36,
+    }
+
+    for style in styles:
+        value = f"\x1b[{codes[style]}m{value}"
+
+    return f"{value}\x1b[0m"
+
+
 def generate_adhoc_ssl_pair(
     cn: t.Optional[str] = None,
 ) -> t.Tuple["Certificate", "RSAPrivateKeyWithSerialization"]:


Test patch: