Repository: tonybaloney/wily

Issue:
Wily diff output
Hi,

I wanted to use wily as an external tool in Pycharm, in order to see the wily diff of a file.
`wily.exe diff foobar.py`
However this fails and I get this message:

`
...
File "c:\programdata\anaconda3\envs\foobar\lib\encodings\cp1252.py", line 19, in encode
    return codecs.charmap_encode(input,self.errors,encoding_table)[0]
UnicodeEncodeError: 'charmap' codec can't encode characters in position 0-123: character maps to <undefined>`

Same happens in the cmd when I wants to pipe the output to a file.
`wily.exe diff foobar.py > wilydiff.txt`

The output options are not valid for the diffs.
Any idea? Thanks

Comments:

Patch:
diff --git a/src/wily/__main__.py b/src/wily/__main__.py
index 1b4c7ef..617aefe 100644
--- a/src/wily/__main__.py
+++ b/src/wily/__main__.py
@@ -11,6 +11,7 @@ from wily.archivers import resolve_archiver
 from wily.cache import exists, get_default_metrics
 from wily.config import DEFAULT_CONFIG_PATH, DEFAULT_GRID_STYLE
 from wily.config import load as load_config
+from wily.helper import get_style
 from wily.helper.custom_enums import ReportFormat
 from wily.lang import _
 from wily.operators import resolve_operators
@@ -279,6 +280,8 @@ def report(
     else:
         new_output = new_output / "wily_report" / "index.html"
 
+    style = get_style(console_format)
+
     from wily.commands.report import report
 
     logger.debug(f"Running report on {file} for metric {metrics}")
@@ -292,7 +295,7 @@ def report(
         output=new_output,
         include_message=message,
         format=ReportFormat[format],
-        console_format=console_format,
+        console_format=style,
         changes_only=changes,
     )
 
diff --git a/src/wily/commands/diff.py b/src/wily/commands/diff.py
index fd05c91..fc19c6f 100644
--- a/src/wily/commands/diff.py
+++ b/src/wily/commands/diff.py
@@ -14,7 +14,8 @@ import tabulate
 from wily import format_date, format_revision, logger
 from wily.archivers import resolve_archiver
 from wily.commands.build import run_operator
-from wily.config import DEFAULT_GRID_STYLE, DEFAULT_PATH
+from wily.config import DEFAULT_PATH
+from wily.helper import get_style
 from wily.operators import (
     BAD_COLORS,
     GOOD_COLORS,
@@ -160,9 +161,8 @@ def diff(config, files, metrics, changes_only=True, detail=True, revision=None):
     descriptions = [metric.description for operator, metric in metrics]
     headers = ("File", *descriptions)
     if len(results) > 0:
+        style = get_style()
         print(
             # But it still makes more sense to show the newest at the top, so reverse again
-            tabulate.tabulate(
-                headers=headers, tabular_data=results, tablefmt=DEFAULT_GRID_STYLE
-            )
+            tabulate.tabulate(headers=headers, tabular_data=results, tablefmt=style)
         )
diff --git a/src/wily/commands/index.py b/src/wily/commands/index.py
index 8150bdf..e7f1a55 100644
--- a/src/wily/commands/index.py
+++ b/src/wily/commands/index.py
@@ -6,7 +6,7 @@ Print information about the wily cache and what is in the index.
 import tabulate
 
 from wily import MAX_MESSAGE_WIDTH, format_date, format_revision, logger
-from wily.config import DEFAULT_GRID_STYLE
+from wily.helper import get_style
 from wily.state import State
 
 
@@ -54,8 +54,6 @@ def index(config, include_message=False):
         headers = ("Revision", "Author", "Message", "Date")
     else:
         headers = ("Revision", "Author", "Date")
-    print(
-        tabulate.tabulate(
-            headers=headers, tabular_data=data, tablefmt=DEFAULT_GRID_STYLE
-        )
-    )
+
+    style = get_style()
+    print(tabulate.tabulate(headers=headers, tabular_data=data, tablefmt=style))
diff --git a/src/wily/commands/list_metrics.py b/src/wily/commands/list_metrics.py
index 6a8ef61..d9b81d1 100644
--- a/src/wily/commands/list_metrics.py
+++ b/src/wily/commands/list_metrics.py
@@ -5,12 +5,13 @@ TODO : Only show metrics for the operators that the cache has?
 """
 import tabulate
 
-from wily.config import DEFAULT_GRID_STYLE
+from wily.helper import get_style
 from wily.operators import ALL_OPERATORS
 
 
 def list_metrics():
     """List metrics available."""
+    style = get_style()
     for name, operator in ALL_OPERATORS.items():
         print(f"{name} operator:")
         if len(operator.cls.metrics) > 0:
@@ -18,6 +19,6 @@ def list_metrics():
                 tabulate.tabulate(
                     headers=("Name", "Description", "Type"),
                     tabular_data=operator.cls.metrics,
-                    tablefmt=DEFAULT_GRID_STYLE,
+                    tablefmt=style,
                 )
             )
diff --git a/src/wily/commands/rank.py b/src/wily/commands/rank.py
index 5600b81..32a3e86 100644
--- a/src/wily/commands/rank.py
+++ b/src/wily/commands/rank.py
@@ -17,7 +17,8 @@ import tabulate
 
 from wily import format_date, format_revision, logger
 from wily.archivers import resolve_archiver
-from wily.config import DEFAULT_GRID_STYLE, DEFAULT_PATH
+from wily.config import DEFAULT_PATH
+from wily.helper import get_style
 from wily.operators import resolve_metric_as_tuple
 from wily.state import State
 
@@ -117,11 +118,8 @@ def rank(config, path, metric, revision_index, limit, threshold, descending):
     data.append(["Total", total])
 
     headers = ("File", metric.description)
-    print(
-        tabulate.tabulate(
-            headers=headers, tabular_data=data, tablefmt=DEFAULT_GRID_STYLE
-        )
-    )
+    style = get_style()
+    print(tabulate.tabulate(headers=headers, tabular_data=data, tablefmt=style))
 
     if threshold and total < threshold:
         logger.error(
diff --git a/src/wily/helper/__init__.py b/src/wily/helper/__init__.py
index a1f80be..7c52235 100644
--- a/src/wily/helper/__init__.py
+++ b/src/wily/helper/__init__.py
@@ -1,1 +1,12 @@
 """Helper package for wily."""
+import sys
+
+from wily.config import DEFAULT_GRID_STYLE
+
+
+def get_style(style=DEFAULT_GRID_STYLE):
+    """Select the tablefmt style for tabulate according to what sys.stdout can handle."""
+    if style == DEFAULT_GRID_STYLE:
+        if sys.stdout.encoding.lower() not in ("utf-8", "utf8"):
+            style = "grid"
+    return style


Test patch:
diff --git a/test/unit/test_helper.py b/test/unit/test_helper.py
new file mode 100644
index 0000000..6b29d3a
--- /dev/null
+++ b/test/unit/test_helper.py
@@ -0,0 +1,26 @@
+from io import BytesIO, TextIOWrapper
+from unittest import mock
+
+from wily.config import DEFAULT_GRID_STYLE
+from wily.helper import get_style
+
+
+def test_get_style():
+    output = TextIOWrapper(BytesIO(), encoding="utf-8")
+    with mock.patch("sys.stdout", output):
+        style = get_style()
+    assert style == DEFAULT_GRID_STYLE
+
+
+def test_get_style_charmap():
+    output = TextIOWrapper(BytesIO(), encoding="charmap")
+    with mock.patch("sys.stdout", output):
+        style = get_style()
+    assert style == "grid"
+
+
+def test_get_style_charmap_not_default_grid_style():
+    output = TextIOWrapper(BytesIO(), encoding="charmap")
+    with mock.patch("sys.stdout", output):
+        style = get_style("something_else")
+    assert style == "something_else"