Repository: cleanlab/cleanlab

Issue:
Ensure unit tests work with termcolor v2.1.0
tests/test_token_classification.py currently fails after termcolor just upgraded to v2.1.0, specifically `test_color_sentence` 

- [ ] update unit test code to make them pass with termcolor v2.1.0
- [ ] ensure new unit test code also works with older versions of termcolor pre v2.1.0 (suboptimal but ok if the unit test only works with versions post v2.0.0, as long as the package works with all termcolor versions currently supported).
- [ ] remove version upper bound on termcolor if it has been added to package in the meantime.

https://pypi.org/project/termcolor/
https://github.com/termcolor/termcolor/pull/25/files
https://github.com/cleanlab/cleanlab/actions/runs/3357515340/jobs/5563372689



Discussion:

Resolving patch:
diff --git a/cleanlab/internal/token_classification_utils.py b/cleanlab/internal/token_classification_utils.py
--- a/cleanlab/internal/token_classification_utils.py
+++ b/cleanlab/internal/token_classification_utils.py
@@ -249,10 +249,35 @@ def color_sentence(sentence: str, word: str) -> str:
     'This is a \x1b[31msentence\x1b[0m. This is another \x1b[31msentence\x1b[0m.'
     """
     colored_word = colored(word, "red")
-    colored_sentence, number_of_substitions = re.subn(
-        r"\b{}\b".format(re.escape(word)), colored_word, sentence
+    return _replace_sentence(sentence=sentence, word=word, new_word=colored_word)
+
+
+def _replace_sentence(sentence: str, word: str, new_word: str) -> str:
+    """
+    Searches for a given token in the sentence and returns the sentence where the given token has been replaced by
+    `new_word`.
+
+    Parameters
+    ----------
+    sentence:
+        a sentence where the word is searched
+
+    word:
+        keyword to find in `sentence`. Assumes the word exists in the sentence.
+
+    new_word:
+        the word to replace the keyword with
+
+    Returns
+    ---------
+    new_sentence:
+        `sentence` where the every occurrence of the word is replaced by `colored_word`
+    """
+
+    new_sentence, number_of_substitions = re.subn(
+        r"\b{}\b".format(re.escape(word)), new_word, sentence
     )
     if number_of_substitions == 0:
         # Use basic string manipulation if regex fails
-        colored_sentence = sentence.replace(word, colored_word)
-    return colored_sentence
+        new_sentence = sentence.replace(word, new_word)
+    return new_sentence


Test patch:
diff --git a/tests/test_token_classification.py b/tests/test_token_classification.py
--- a/tests/test_token_classification.py
+++ b/tests/test_token_classification.py
@@ -5,6 +5,7 @@
     mapping,
     merge_probs,
     color_sentence,
+    _replace_sentence,
 )
 from cleanlab.token_classification.filter import find_label_issues
 from cleanlab.token_classification.rank import (
@@ -134,6 +135,7 @@ def test_merge_probs_with_normalization():
     "sentence,word,expected",
     [
         ("Hello World", "World", f"Hello {C_L}World{C_R}"),
+        ("Hello World", "help", "Hello World"),
         ("If you and I were to meet", "I", f"If you and {C_L}I{C_R} were to meet"),
         ("If you and I were to meet", "If you and I", f"{C_L}If you and I{C_R} were to meet"),
         ("If you and I were to meet", "If you and I w", f"{C_L}If you and I w{C_R}ere to meet"),
@@ -149,6 +151,7 @@ def test_merge_probs_with_normalization():
     ],
     ids=[
         "single_word",
+        "no_match",
         "ignore_subwords",
         "multi-token_match",
         "substring_replacement",
@@ -159,11 +162,49 @@ def test_merge_probs_with_normalization():
         "issue_403-escape_special_regex_characters",
     ],
 )
-def test_color_sentence(sentence, word, expected):
+def test_color_sentence(monkeypatch: pytest.MonkeyPatch, sentence, word, expected):
+    monkeypatch.setattr("sys.stdout.isatty", lambda: True)
+
     colored = color_sentence(sentence, word)
     assert colored == expected
 
 
+@pytest.mark.parametrize(
+    "sentence,word,expected",
+    [
+        ("Hello World", "World", "Hello [EXPECTED]"),
+        ("Hello World", "help", "Hello World"),
+        ("If you and I were to meet", "I", "If you and [EXPECTED] were to meet"),
+        ("If you and I were to meet", "If you and I", "[EXPECTED] were to meet"),
+        ("If you and I were to meet", "If you and I w", "[EXPECTED]ere to meet"),
+        ("I think I know this", "I", "[EXPECTED] think [EXPECTED] know this"),
+        ("A good reason for a test", "a", "A good reason for [EXPECTED] test"),
+        ("ab ab a b ab", "ab a", "ab [EXPECTED] b ab"),
+        ("ab ab ab ab", "ab a", "[EXPECTED]b [EXPECTED]b"),
+        (
+            "Alan John Percivale (A.j.p.) Taylor died",
+            "(",
+            "Alan John Percivale [EXPECTED]A.j.p.) Taylor died",
+        ),
+    ],
+    ids=[
+        "single_word",
+        "no_match",
+        "ignore_subwords",
+        "multi-token_match",
+        "substring_replacement",
+        "multiple_matches",
+        "case_sensitive",
+        "only_word_boundary",
+        "non_overlapping_substrings",
+        "issue_403-escape_special_regex_characters",
+    ],
+)
+def test_replace_sentence(sentence, word, expected):
+    new_sentence = _replace_sentence(sentence, word, "[EXPECTED]")
+    assert new_sentence == expected
+
+
 issues = find_label_issues(labels, pred_probs)