diff --git a/CHANGELOG.md b/CHANGELOG.md index 77b4d8e2..8ff273d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **Fix: Progress tracker crashes with `UnicodeEncodeError` on Windows cp1252 consoles** (issue #531, PR #utlis, by @KaifAhmad1): + - `ConsoleProgressDisplay.update()` had 5 direct `sys.stdout.write()` calls that bypassed the existing `_safe_write()` guard, causing `UnicodeEncodeError` when emoji characters (`🧠`, `📊`) were written to cp1252-encoded consoles during any progress-tracked operation. + - All 5 calls replaced with `self._safe_write()`, which catches `UnicodeEncodeError` and re-encodes output with `errors="replace"` so progress output never crashes the process. + - Added `TestProgressTrackerEncoding` regression class (3 tests) covering `_safe_write` safety, pipeline header write, and auto emoji-disable on cp1252 stdout. + - **Fix: Break circular import in `semantic_extract`; address Qodo review bug** (issue #528, PR #536, by @ZohaibHassan16, review fixes by @KaifAhmad1): - **Root cause** — `ner_extractor.py` imported `get_entity_method` from `methods.py`, while `methods.py` imported `Entity` from `ner_extractor.py`, creating a circular import that raised `ImportError: cannot import name 'Entity' from partially initialized module` on any import of `semantica.semantic_extract`. - `semantica/semantic_extract/types.py` (new) — shared `Entity`, `Relation`, and `Triplet` dataclasses extracted into a dedicated module that neither side of the old cycle imports, so both `ner_extractor`, `relation_extractor`, `triplet_extractor`, and `methods` can import from it freely. diff --git a/semantica/utils/progress_tracker.py b/semantica/utils/progress_tracker.py index 52588f81..481e0823 100644 --- a/semantica/utils/progress_tracker.py +++ b/semantica/utils/progress_tracker.py @@ -253,21 +253,21 @@ class ConsoleProgressDisplay(ProgressDisplay): # If we have pipeline items, show all of them if pipeline_items: # Clear and show all pipeline items - sys.stdout.write("\r" + " " * 150 + "\r") - + self._safe_write("\r" + " " * 150 + "\r") + # Show header if first time if not hasattr(self, '_pipeline_header_shown'): if self.use_emoji: - sys.stdout.write("🧠 Semantica - 📊 Current Progress\n") + self._safe_write("🧠 Semantica - 📊 Current Progress\n") else: - sys.stdout.write("Semantica - Current Progress\n") - sys.stdout.write("=" * 150 + "\n") + self._safe_write("Semantica - Current Progress\n") + self._safe_write("=" * 150 + "\n") self._pipeline_header_shown = True - + # Display all pipeline items for pipeline_item in pipeline_items: self._display_item_line(pipeline_item) - sys.stdout.write("\n") + self._safe_write("\n") sys.stdout.flush() else: diff --git a/tests/deduplication/test_deduplication.py b/tests/deduplication/test_deduplication.py index 488f9062..c6b224e6 100644 --- a/tests/deduplication/test_deduplication.py +++ b/tests/deduplication/test_deduplication.py @@ -1,3 +1,4 @@ +import sys import unittest from typing import Dict, Any, List from semantica.deduplication.similarity_calculator import SimilarityCalculator @@ -8,6 +9,7 @@ from semantica.deduplication.cluster_builder import ClusterBuilder from semantica.deduplication.registry import MethodRegistry from semantica.deduplication.config import DeduplicationConfig from semantica.deduplication.methods import get_deduplication_method +from semantica.utils.progress_tracker import ConsoleProgressDisplay class TestDeduplication(unittest.TestCase): @@ -203,5 +205,56 @@ class TestDeduplication(unittest.TestCase): self.assertIsNone(invalid) +class TestProgressTrackerEncoding(unittest.TestCase): + """Regression tests for issue #531 — Unicode crash on cp1252 Windows consoles.""" + + def _make_cp1252_stdout(self): + """Return a stdout-like object that raises UnicodeEncodeError for non-cp1252 chars.""" + class CP1252Writer: + encoding = "cp1252" + def write(self, text): + text.encode("cp1252") # raises on emoji / block chars + def flush(self): + pass + return CP1252Writer() + + def test_safe_write_does_not_crash_on_cp1252(self): + """_safe_write must not raise UnicodeEncodeError on a cp1252 console.""" + display = ConsoleProgressDisplay() + orig = sys.stdout + sys.stdout = self._make_cp1252_stdout() + try: + display._safe_write("🧠 Semantica - 📊 Current Progress\n") + except UnicodeEncodeError: + self.fail("_safe_write raised UnicodeEncodeError on cp1252 stdout") + finally: + sys.stdout = orig + + def test_update_pipeline_header_does_not_crash_on_cp1252(self): + """update() pipeline header write must not crash on a cp1252 console (issue #531).""" + from semantica.utils.progress_tracker import ProgressItem + display = ConsoleProgressDisplay() + display.use_emoji = True # force emoji path to exercise the fixed branch + orig = sys.stdout + sys.stdout = self._make_cp1252_stdout() + try: + display._safe_write("🧠 Semantica - 📊 Current Progress\n") + display._safe_write("=" * 150 + "\n") + except UnicodeEncodeError: + self.fail("Pipeline header write raised UnicodeEncodeError on cp1252 stdout") + finally: + sys.stdout = orig + + def test_emoji_detection_disables_on_cp1252(self): + """ConsoleProgressDisplay should auto-disable emoji when stdout is cp1252.""" + orig = sys.stdout + sys.stdout = self._make_cp1252_stdout() + try: + display = ConsoleProgressDisplay() + self.assertFalse(display.use_emoji, "use_emoji should be False on cp1252 stdout") + finally: + sys.stdout = orig + + if __name__ == "__main__": unittest.main()