Repository: zulip/zulip-terminal
Pull request #1452 (2023-12-19 08:05:03)

config/themes: Convert pygments styles to urwid compatible styles.
<!-- See README for documentation, or ask in #zulip-terminal if unclear -->
### What does this PR do, and why?
Pygments 2.16.0 introduced a style to support a combination of bold and italic styling in pygments/pygments#2444. Both of our gruvbox themes and the light native theme gain a 'bold strong' style via pygments as a result, which urwid fails to parse and blocks the application from loading.

This work would represent a clean fix for #1431, which was temporarily fixed by pinning pygments at ~=2.15.1 in #1433. Pygments can be unpinned after this fix.

Add method `translate_styles` that manually converts the pygments `bold italic` style into the urwid-compatible `bold, italics`. 

In requirements, unpin pygments from `~=2.15.1` to `>=2.14.0,<2.18.0`.

### External discussion & connections
<!-- [x] all that apply, specifying topic and adding numbers after # for issues/PRs -->
- [x] Discussed in **#zulip-terminal** in `Translate pygments styles into urwid #T1434 #T1452`
- [ ] Fully fixes #
- [x] Partially fixes issue #1434
- [ ] Builds upon previous unmerged work in PR #
- [ ] Is a follow-up to work in PR #
- [ ] Requires merge of PR #
- [ ] Merge will enable work on #

### How did you test this?
<!-- [x] all that apply -->
- [x] Manually - Behavioral changes
- [ ] Manually - Visual changes
- [ ] Adapting existing automated tests
- [x] Adding automated tests for new behavior (or missing tests)
- [ ] Existing automated tests should already cover this (*only a refactor of tested code*)

### Self-review checklist for each commit
- [x] It is a [minimal coherent idea](https://github.com/zulip/zulip-terminal#structuring-commits---speeding-up-reviews-merging--development)
- [x] It has a commit summary following the [documented style](https://github.com/zulip/zulip-terminal#structuring-commits---speeding-up-reviews-merging--development) (title & body)
- [x] It has a commit summary describing the  motivation and reasoning for the change
- [x] It individually passes linting and tests
- [x] It contains test additions for any new behavior
- [x] It flows clearly from a previous branch commit, and/or prepares for the next commit



Patch:
diff --git a/setup.py b/setup.py
index d509d3e..39c9ff8 100644
--- a/setup.py
+++ b/setup.py
@@ -100,7 +100,7 @@ setup(
         "urwid_readline>=0.13",
         "beautifulsoup4>=4.11.1",
         "lxml~=4.9.2",
-        "pygments~=2.15.1",
+        "pygments>=2.14.0,<2.18.0",
         "typing_extensions~=4.5.0",
         "python-dateutil>=2.8.2",
         "pytz>=2022.7.1",
diff --git a/zulipterminal/config/themes.py b/zulipterminal/config/themes.py
index c9e5174..7c6dfe5 100644
--- a/zulipterminal/config/themes.py
+++ b/zulipterminal/config/themes.py
@@ -3,7 +3,7 @@ Styles and their colour mappings in each theme, with helper functions
 """
 from typing import Any, Dict, List, Optional, Tuple, Union
 
-from pygments.token import STANDARD_TYPES
+from pygments.token import STANDARD_TYPES, _TokenType
 
 from zulipterminal.config.color import term16
 from zulipterminal.themes import gruvbox_dark, gruvbox_light, zt_blue, zt_dark, zt_light
@@ -128,6 +128,13 @@ valid_16_color_codes = [
     "white",
 ]
 
+# These are style_translations for translating pygments styles into
+# urwid-compatible styles
+STYLE_TRANSLATIONS = {
+    " ": ",",
+    "italic": "italics",
+}
+
 
 class ThemeError(Exception):
     pass
@@ -255,6 +262,19 @@ def parse_themefile(
     return urwid_theme
 
 
+def generate_urwid_compatible_pygments_styles(
+    pygments_styles: Dict[_TokenType, str],
+    style_translations: Dict[str, str] = STYLE_TRANSLATIONS,
+) -> Dict[_TokenType, str]:
+    urwid_compatible_styles = {}
+    for token, style in pygments_styles.items():
+        updated_style = style
+        for old_value, new_value in style_translations.items():
+            updated_style = updated_style.replace(old_value, new_value)
+        urwid_compatible_styles[token] = updated_style
+    return urwid_compatible_styles
+
+
 def generate_pygments_styles(pygments: Dict[str, Any]) -> ThemeSpec:
     """
     This function adds pygments styles for use in syntax
@@ -278,6 +298,8 @@ def generate_pygments_styles(pygments: Dict[str, Any]) -> ThemeSpec:
     term16_bg = term16.background_color
 
     theme_styles_from_pygments: ThemeSpec = []
+    pygments_styles = generate_urwid_compatible_pygments_styles(pygments_styles)
+
     for token, css_class in STANDARD_TYPES.items():
         if css_class in pygments_overrides:
             pygments_styles[token] = pygments_overrides[css_class]


Test patch:
diff --git a/tests/config/test_themes.py b/tests/config/test_themes.py
index b06e35d..b95c4e3 100644
--- a/tests/config/test_themes.py
+++ b/tests/config/test_themes.py
@@ -5,13 +5,14 @@ from typing import Any, Dict, Optional, Tuple
 import pytest
 from pygments.styles.material import MaterialStyle
 from pygments.styles.perldoc import PerldocStyle
-from pygments.token import STANDARD_TYPES
+from pygments.token import STANDARD_TYPES, _TokenType
 from pytest import param as case
 from pytest_mock import MockerFixture
 
 from zulipterminal.config.regexes import REGEX_COLOR_VALID_FORMATS
 from zulipterminal.config.themes import (
     REQUIRED_STYLES,
+    STYLE_TRANSLATIONS,
     THEMES,
     InvalidThemeColorCode,
     MissingThemeAttributeError,
@@ -20,6 +21,7 @@ from zulipterminal.config.themes import (
     complete_and_incomplete_themes,
     generate_pygments_styles,
     generate_theme,
+    generate_urwid_compatible_pygments_styles,
     parse_themefile,
     valid_16_color_codes,
     validate_colors,
@@ -402,3 +404,65 @@ def test_validate_colors(color_depth: int = 16) -> None:
         + "- GRAY_244 = dark_gra\n"
         + "- LIGHT2 = whit"
     )
+
+
+@pytest.mark.parametrize(
+    "pygments_styles, expected_styles, style_translations",
+    [
+        case(
+            {},
+            {},
+            STYLE_TRANSLATIONS,
+            id="empty_input",
+        ),
+        case(
+            {
+                "token1": "style1",
+                "token2": "style2",
+            },
+            {
+                "token1": "style1",
+                "token2": "style2",
+            },
+            {},
+            id="empty_translations",
+        ),
+        case(
+            {
+                "token1": "bold italic",  # pygments/pygments#2444
+                "token2": "italic bold",  # + order shouldn't matter
+                "token3": "italic #abc",  # + italic should work with color
+            },
+            {
+                "token1": "bold,italics",
+                "token2": "italics,bold",
+                "token3": "italics,#abc",
+            },
+            STYLE_TRANSLATIONS,
+            id="default_translations",
+        ),
+        case(
+            {
+                "token1": "style italic",
+                "token2": "#abc",
+            },
+            {
+                "token1": "newstyle italic",
+                "token2": "#abc",
+            },
+            {"style": "newstyle"},
+            id="custom_translations",
+        ),
+    ],
+)
+def test_generate_urwid_compatible_pygments_styles(
+    pygments_styles: Dict[_TokenType, str],  # NOTE: placeholder string values used
+    expected_styles: Dict[_TokenType, str],  #       in parametrized dict keys
+    style_translations: Dict[str, str],
+) -> None:
+    generated_styles = generate_urwid_compatible_pygments_styles(
+        pygments_styles, style_translations
+    )
+
+    assert set(expected_styles) == set(generated_styles)  # keys unchanged
+    assert sorted(expected_styles.items()) == sorted(generated_styles.items())