mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
Merge pull request #537 from Hawksight-AI/utlis
fix(utils): route all progress tracker stdout writes through _safe_wr…
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user