Repository navigation
test(tabulate): hide wcwidth from the reference import #229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,16 @@ | |
|
|
||
| _cached_tabulate = sys.modules.pop("tabulate", None) | ||
|
|
||
| # The reference switches to ``wcwidth`` for column widths when it is | ||
| # installed, while this module's default is ``len(s)`` (``WIDE_CHARS_MODE`` | ||
| # is opt-in). Hide ``wcwidth`` from the reference import — ``None`` in | ||
| # ``sys.modules`` makes the import fail cleanly — so both sides compute | ||
| # widths the same way regardless of the environment. Without this the | ||
| # unicode/CJK cases differ as soon as ``wcwidth`` is a transitive dependency. | ||
| _cached_wcwidth = sys.modules.get("wcwidth") | ||
| _cached_wcwidth_present = "wcwidth" in sys.modules | ||
| sys.modules["wcwidth"] = None # type: ignore[assignment] | ||
|
|
||
| try: | ||
| import tabulate as _ref_tabulate_mod | ||
|
|
||
|
|
@@ -35,10 +45,135 @@ | |
| sys.modules.pop("tabulate", None) | ||
| if _cached_tabulate is not None: | ||
| sys.modules["tabulate"] = _cached_tabulate | ||
| if _cached_wcwidth_present: | ||
| sys.modules["wcwidth"] = _cached_wcwidth # type: ignore[assignment] | ||
| else: | ||
| sys.modules.pop("wcwidth", None) | ||
|
|
||
| sys.path.insert(0, _this_dir) | ||
| import tabulate as _our_module # noqa: E402 | ||
| from tabulate import tabulate as our_tabulate # noqa: E402 | ||
|
|
||
|
|
||
| def test_reference_width_mode_is_len() -> None: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the one thing the PR trades away rather than fixes: with So an extra test that imports the reference with |
||
| """The comparisons below assume the reference counts ``len(s)``. | ||
|
|
||
| Our default is ``len(s)``; the reference switches to ``wcwidth`` when | ||
| that package is importable. If this fails, the guard above stopped | ||
| working and the unicode/CJK cases compare two different width rules | ||
| rather than two implementations. | ||
| """ | ||
| assert getattr(_ref_tabulate_mod, "wcwidth", None) is None | ||
|
|
||
|
|
||
| def _reference_using_wcwidth(): | ||
| """Import the reference again with ``wcwidth`` visible. | ||
|
|
||
| The module-level import hides ``wcwidth`` so that both sides use | ||
| ``len(s)``; this reproduces the configuration a user of the reference | ||
| has, where it detects wide characters automatically. Skips when | ||
| ``wcwidth`` is not installed. | ||
| """ | ||
| pytest.importorskip("wcwidth") | ||
| saved_path = sys.path[:] | ||
| saved_tabulate = sys.modules.pop("tabulate", None) | ||
| try: | ||
| sys.path = [ | ||
| p | ||
| for p in sys.path | ||
| if os.path.abspath(p or os.curdir) | ||
| not in ( | ||
| os.path.abspath(_this_dir), | ||
| os.path.abspath(os.path.join(_this_dir, "..")), | ||
| ) | ||
| ] | ||
| import tabulate as ref | ||
|
|
||
| return ref | ||
| finally: | ||
| sys.path = saved_path | ||
| sys.modules.pop("tabulate", None) | ||
| if saved_tabulate is not None: | ||
| sys.modules["tabulate"] = saved_tabulate | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| ("data", "kwargs"), | ||
| [ | ||
| ([["café", 1], ["élève", 2]], {}), | ||
| ([["你好", 1], ["世界", 2]], {"headers": ["Text", "Num"]}), | ||
| ([["东京Tokyo", 1], ["NYC", 2]], {"headers": ["City", "ID"]}), | ||
| ([["\U0001f600", "smile"]], {}), | ||
| ], | ||
| ) | ||
| def test_wide_chars_mode_matches_the_wcwidth_reference(data, kwargs) -> None: | ||
| """``WIDE_CHARS_MODE`` reproduces what the reference does with wcwidth. | ||
|
|
||
| Hiding ``wcwidth`` above makes the default-mode comparison | ||
| environment-independent, but it also means nothing else exercises the | ||
| wide-character width path. This pins it against the configuration the | ||
| reference has when the package is installed. | ||
| """ | ||
| ref = _reference_using_wcwidth() | ||
| if getattr(ref, "wcwidth", None) is None: | ||
| pytest.skip("the reference did not import wcwidth") | ||
|
|
||
| previous = _our_module.WIDE_CHARS_MODE | ||
| _our_module.WIDE_CHARS_MODE = True | ||
| try: | ||
| assert _our_module.tabulate(data, **kwargs) == ref.tabulate(data, **kwargs) | ||
| finally: | ||
| _our_module.WIDE_CHARS_MODE = previous | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "text", | ||
| [ | ||
| "café", # combining acute | ||
| "élève", # precomposed | ||
| "你好", # CJK, wide | ||
| "\U0001f600", # emoji, wide | ||
| "ab", # zero-width space | ||
| "a͏b", # combining grapheme joiner, ccc=0 | ||
| "क्ष", # Devanagari virama cluster | ||
| "각", # Hangul leading + medial + final jamo | ||
| ], | ||
| ) | ||
| def test_zero_width_shapes_match_wcwidth(text: str) -> None: | ||
| """Per-character widths agree with ``wcwidth.wcswidth``.""" | ||
| wcwidth = pytest.importorskip("wcwidth") | ||
| previous = _our_module.WIDE_CHARS_MODE | ||
| _our_module.WIDE_CHARS_MODE = True | ||
| try: | ||
| assert _our_module._visible_width(text) == wcwidth.wcswidth(text) | ||
| finally: | ||
| _our_module.WIDE_CHARS_MODE = previous | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| ("text", "expected"), | ||
| [ | ||
| ("\U0001f468\U0001f469\U0001f467", 6), # family, ZWJ | ||
| ("1️⃣", 1), # keycap sequence | ||
| ], | ||
| ) | ||
| def test_emoji_presentation_sequences_count_per_component( | ||
| text: str, expected: int | ||
| ) -> None: | ||
| """Documented divergence: ``wcwidth`` measures grapheme clusters. | ||
|
|
||
| A ZWJ family emoji is 2 columns under ``wcwidth`` and 6 here; a keycap | ||
| is 2 there and 1 here. Pinning the per-component count keeps the | ||
| docstring honest. | ||
| """ | ||
| previous = _our_module.WIDE_CHARS_MODE | ||
| _our_module.WIDE_CHARS_MODE = True | ||
| try: | ||
| assert _our_module._visible_width(text) == expected | ||
| finally: | ||
| _our_module.WIDE_CHARS_MODE = previous | ||
|
|
||
|
|
||
| # ── Helpers ── | ||
|
|
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding
"Mc"here is exact rather than approximate, and it closes the one residue that hits ordinary text: everyMccode point reports 0 underwcwidth0.9.3 (I checked all 445 - there is noMcwith width 1), while this rule leaves them at 1. Effect on real strings with the flag on:so the ID column shifts by one on any row containing an Indic spacing vowel sign.
Cfdeliberately stays as it is: the 14 exceptions (U+00AD, U+0600-U+0605, U+06DD, U+070F, U+110BD, U+110CD) are better documented than special-cased. Filed as #232 so it is tracked either way.