Repository: xonsh/xonsh

Issue:
$XONSH_STYLE_OVERRIDES cannot be assigned dict of {Token: str}
<!--- Provide a general summary of the issue in the Title above -->

## xonfig

<details>

```
+------------------+----------------------+
| xonsh            | 0.9.27               |
| Git SHA          | 71fe9014             |
| Commit Date      | Jan 29 08:58:58 2021 |
| Python           | 3.9.5                |
| PLY              | 3.11                 |
| have readline    | True                 |
| prompt toolkit   | 3.0.19               |
| shell type       | prompt_toolkit       |
| pygments         | 2.9.0                |
| on posix         | True                 |
| on linux         | True                 |
| distro           | ubuntu               |
| on darwin        | False                |
| on windows       | False                |
| on cygwin        | False                |
| on msys2         | False                |
| is superuser     | False                |
| default encoding | utf-8                |
| xonsh encoding   | utf-8                |
| encoding errors  | surrogateescape      |
| on jupyter       | False                |
| jupyter kernel   | None                 |
| xontrib 1        | apt_tabcomplete      |
| xontrib 2        | direnv               |
| xontrib 3        | kitty                |
+------------------+----------------------+
```

</details>

## Expected Behavior
[Per documentation](https://xon.sh/envvars.html#xonsh-style-overrides), `pygments.token.Token` values can be used as keys for `$XONSH_STYLE_OVERRIDES`. So, this ought to work:

```
from pygments.token import Token
$XONSH_STYLE_OVERRIDES = {
    Token.Comment: "red"
}
```

## Current Behavior
```
$ from pygments.token import Token
$ $XONSH_STYLE_OVERRIDES = {
.     Token.Comment: "red"
. }
.
/home/egnor/.local/lib/python3.9/site-packages/xonsh/tools.py:1694: RuntimeWarning: "{Token.Comment: 'red'}" can not be converted to str:str dictionary.
  warnings.warn(msg, RuntimeWarning)
```

Note that you can happily _add_ Token/str pairs, so this works just fine:

```
$ from pygments.token import Token
$ $XONSH_STYLE_OVERRIDES[Token.Comment] = "red"
```

## Steps to Reproduce
1. Run the code above
2. Sigh, and use something like `$XONSH_STYLE_OVERRIDES.update({ ... })` instead

(In general, a lot of these dict-like/list-like env-vars are a little inconsistent about type gating, it might be nice to have a general solution that ensures that both assignment and modification only use expected types, or can be converted to expected types. As it stands you can put weird types in many of them and the problem is only noticed downstream.)

## For community
⬇️  **Please click the 👍 reaction instead of leaving a `+1` or 👍  comment**



Discussion:
Hello, has this issue been fixed yet? If not, I would be interested in working on it. 
Hey @IAustin-YangI -- looks like it's still an issue on `main`, so you should definitely feel free to take a crack at it!

Resolving patch:
diff --git a/xonsh/environ.py b/xonsh/environ.py
--- a/xonsh/environ.py
+++ b/xonsh/environ.py
@@ -69,10 +69,10 @@
     is_nonstring_seq_of_strings,
     is_path,
     is_regex,
-    is_str_str_dict,
     is_string,
     is_string_or_callable,
     is_string_set,
+    is_tok_color_dict,
     is_valid_shlvl,
     logfile_opt_to_str,
     path_to_str,
@@ -97,7 +97,7 @@
     to_logfile_opt,
     to_repr_pretty_,
     to_shlvl,
-    to_str_str_dict,
+    to_tok_color_dict,
 )
 
 events.doc(
@@ -1151,8 +1151,8 @@ class GeneralSetting(Xettings):
         "``!()`` and ``![]`` operators.",
     )
     XONSH_STYLE_OVERRIDES = Var(
-        is_str_str_dict,
-        to_str_str_dict,
+        is_tok_color_dict,
+        to_tok_color_dict,
         dict_to_str,
         {},
         "A dictionary containing custom prompt_toolkit/pygments style definitions.\n"
@@ -1625,8 +1625,8 @@ class PTKSetting(PromptSetting):  # sub-classing -> sub-group
         "colors. Default is an empty string which means that prompt toolkit decide.",
     )
     PTK_STYLE_OVERRIDES = Var(
-        is_str_str_dict,
-        to_str_str_dict,
+        is_tok_color_dict,
+        to_tok_color_dict,
         dict_to_str,
         {},
         "A dictionary containing custom prompt_toolkit style definitions. (deprecated)",
diff --git a/xonsh/tools.py b/xonsh/tools.py
--- a/xonsh/tools.py
+++ b/xonsh/tools.py
@@ -1767,11 +1767,33 @@ def to_completion_mode(x):
     return y
 
 
-def is_str_str_dict(x):
-    """Tests if something is a str:str dictionary"""
-    return isinstance(x, dict) and all(
-        isinstance(k, str) and isinstance(v, str) for k, v in x.items()
-    )
+def is_tok_color_dict(x):
+    from pygments.token import _TokenType, string_to_tokentype
+
+    from xonsh.ptk_shell.shell import _style_from_pygments_dict
+
+    """Tests if something is a Token:Style dictionary"""
+    if not isinstance(x, dict):
+        return False
+    """Check if is a Token:str dict"""
+    for k, v in x.items():
+        if not isinstance(v, str):
+            return False
+        try:
+            k = _TokenType(k)
+            string_to_tokentype(k)
+        except (TypeError, AttributeError):
+            msg = f'"{k}" is not a valid Token.'
+            warnings.warn(msg, RuntimeWarning)
+            return False
+    """Check each str is a valid style"""
+    try:
+        _style_from_pygments_dict(x)
+    except (AssertionError, ValueError):
+        msg = f'"{x}" contains an invalid style.'
+        warnings.warn(msg, RuntimeWarning)
+        return False
+    return True
 
 
 def to_dict(x):
@@ -1787,13 +1809,13 @@ def to_dict(x):
     return x
 
 
-def to_str_str_dict(x):
-    """Converts a string to str:str dictionary"""
-    if is_str_str_dict(x):
+def to_tok_color_dict(x):
+    """Converts a string to Token:str dictionary"""
+    if is_tok_color_dict(x):
         return x
     x = to_dict(x)
-    if not is_str_str_dict(x):
-        msg = f'"{x}" can not be converted to str:str dictionary.'
+    if not is_tok_color_dict(x):
+        msg = f'"{x}" can not be converted to Token:str dictionary.'
         warnings.warn(msg, RuntimeWarning)
         x = dict()
     return x


Test patch:
diff --git a/tests/test_tools.py b/tests/test_tools.py
--- a/tests/test_tools.py
+++ b/tests/test_tools.py
@@ -61,6 +61,7 @@
     is_string,
     is_string_or_callable,
     is_string_seq,
+    is_tok_color_dict,
     is_writable_file,
     logfile_opt_to_str,
     path_to_str,
@@ -2000,3 +2001,38 @@ def test_is_regex_true():
 
 def test_is_regex_false():
     assert not is_regex("**")
+
+
+from xonsh.style_tools import Token
+
+
+@pytest.mark.parametrize(
+    "val, exp",
+    [
+        (
+            {
+                Token.Literal.String: "bold ansigreen",
+                "Token.Name.Tag": "underline ansiblue",
+            },
+            True,
+        ),
+        (
+            {
+                Token.Literal.String: "bold ansigreen",
+                1: "bold ansigreen",
+            },
+            False,
+        ),
+        ({1: "bold ansigreen"}, False),
+        (
+            {Token.Literal.String: "123"},
+            False,
+        ),
+        (
+            {"Token.Name.Tag": 123},
+            False,
+        ),
+    ],
+)
+def test_is_tok_color_dict(val, exp):
+    assert is_tok_color_dict(val) == exp