Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/wcwidth/_constants.py: 50%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""Shared data tables and constants for wcwidth.py, _wcwidth.py, and _wcswidth.py."""
2from __future__ import annotations
4# std imports
5import os
6from operator import index as _index
7from functools import lru_cache
9from typing import Tuple, NamedTuple
11# local
12from .table_mc import CATEGORY_MC
13from .table_wide import WIDE_EASTASIAN
14from .table_zero import ZERO_WIDTH
15from .table_grapheme import (ISC_VIRAMA,
16 EXTENDED_PICTOGRAPHIC,
17 ISC_INVISIBLE_STACKER,
18 GRAPHEME_REGIONAL_INDICATOR)
19from .table_ambiguous import AMBIGUOUS_EASTASIAN
20from .table_overrides import (SFZ_OVERRIDES,
21 SRI_OVERRIDES,
22 VS15_OVERRIDES,
23 VS16_OVERRIDES,
24 WIDE_OVERRIDES,
25 NARROW_OVERRIDES)
26from .unicode_versions import list_versions
27from .table_term_programs import ALIASES, KNOWN_TERMINALS
29_RangeTuple = Tuple[Tuple[int, int], ...]
32__all__ = (
33 "_REGIONAL_INDICATOR_SET",
34 "_ISC_VIRAMA_SET",
35 "_LATEST_VERSION",
36 "_CATEGORY_MC_TABLE",
37 "_EMOJI_ZWJ_SET",
38 "_FITZPATRICK_RANGE",
39 "_ZERO_WIDTH_TABLE",
40 "_WIDE_EASTASIAN_TABLE",
41 "_AMBIGUOUS_TABLE",
42 "resolve_terminal",
43 "get_term_overrides",
44 "list_term_programs",
45)
47_REGIONAL_INDICATOR_SET = frozenset(
48 range(GRAPHEME_REGIONAL_INDICATOR[0][0], GRAPHEME_REGIONAL_INDICATOR[0][1] + 1)
49)
50_ISC_VIRAMA_SET = frozenset(
51 cp for lo, hi in (*ISC_VIRAMA, *ISC_INVISIBLE_STACKER)
52 for cp in range(lo, hi + 1)
53)
54# pylint: disable=invalid-name
55_LATEST_VERSION = list_versions()[-1]
56_CATEGORY_MC_TABLE = CATEGORY_MC[_LATEST_VERSION]
57_EMOJI_ZWJ_SET = frozenset(
58 cp for lo, hi in EXTENDED_PICTOGRAPHIC for cp in range(lo, hi + 1)
59) | _REGIONAL_INDICATOR_SET
60_FITZPATRICK_RANGE = (0x1F3FB, 0x1F3FF)
62_ZERO_WIDTH_TABLE = ZERO_WIDTH[_LATEST_VERSION]
63_WIDE_EASTASIAN_TABLE = WIDE_EASTASIAN[_LATEST_VERSION]
64_AMBIGUOUS_TABLE = AMBIGUOUS_EASTASIAN[_LATEST_VERSION]
67def list_term_programs() -> tuple[str, ...]:
68 """
69 Return all recognized values for the ``term_program`` argument.
71 Includes canonical terminal names and their TERM/TERM_PROGRAM aliases.
73 .. versionadded:: 0.8.0
74 """
75 return tuple(sorted(KNOWN_TERMINALS | ALIASES.keys()))
78def _merge_ranges(*tuples: _RangeTuple) -> _RangeTuple:
79 """Merge multiple sorted range tuples into one sorted, non-overlapping tuple."""
80 all_ranges: list[tuple[int, int]] = []
81 for t in tuples:
82 all_ranges.extend(t)
83 if not all_ranges:
84 return ()
85 all_ranges.sort(key=lambda r: r[0])
86 merged = [all_ranges[0]]
87 for lo, hi in all_ranges[1:]:
88 _, prev_hi = merged[-1]
89 if lo <= prev_hi:
90 merged[-1] = (merged[-1][0], max(prev_hi, hi))
91 else:
92 merged.append((lo, hi))
93 return tuple(merged)
96class TerminalOverrides(NamedTuple):
97 """Pre-merged override range tuples for a single terminal."""
99 narrower: _RangeTuple
100 vs16_narrower: _RangeTuple
101 vs15_wider: _RangeTuple
102 zeroer: _RangeTuple
103 narrow_wider: _RangeTuple
104 narrow_zeroer: _RangeTuple
107_EMPTY_OVERRIDES = TerminalOverrides((), (), (), (), (), ())
110@lru_cache(maxsize=32)
111def get_term_overrides(term_canonical: str) -> TerminalOverrides:
112 """Return a TerminalOverrides, with all empty tuples when there are no overrides."""
113 # wide, sri, sfz: all narrow characters Unicode expects wide (no 'wider' data exists)
114 narrower = _merge_ranges(
115 WIDE_OVERRIDES.get(term_canonical, {}).get('narrower', ()),
116 SRI_OVERRIDES.get(term_canonical, {}).get('narrower', ()),
117 SFZ_OVERRIDES.get(term_canonical, {}).get('narrower', ()),
118 )
119 vs16_narrower = VS16_OVERRIDES.get(term_canonical, {}).get('narrower', ())
120 vs15_wider = VS15_OVERRIDES.get(term_canonical, {}).get('wider', ())
121 zeroer = _merge_ranges(
122 WIDE_OVERRIDES.get(term_canonical, {}).get('zeroer', ()),
123 SRI_OVERRIDES.get(term_canonical, {}).get('zeroer', ()),
124 SFZ_OVERRIDES.get(term_canonical, {}).get('zeroer', ()),
125 )
126 narrow_wider = NARROW_OVERRIDES.get(term_canonical, {}).get('wider', ())
127 narrow_zeroer = NARROW_OVERRIDES.get(term_canonical, {}).get('narrow_zeroer', ())
128 # vs15_narrower intentionally excluded: no known terminal narrows VS15
129 # vs16_wider intentionally excluded: any 'wider' entries in emoji_vs16_results
130 # ucs-detect YAML are from the vs16n baseline test (base char without VS16),
131 # not actual VS16 correction data.
133 if not (narrower or vs16_narrower or vs15_wider or zeroer
134 or narrow_wider or narrow_zeroer):
135 return _EMPTY_OVERRIDES
136 return TerminalOverrides(narrower, vs16_narrower, vs15_wider, zeroer,
137 narrow_wider, narrow_zeroer)
140@lru_cache(maxsize=32)
141def resolve_terminal(term_program: bool | str = False) -> str | None:
142 """
143 Resolve a terminal identifier to its canonical name.
145 :param term_program: Terminal identifier. ``False`` (default) disables override lookup.
146 ``True`` reads the ``TERM_PROGRAM`` environment variable, falling back to ``TERM``.
147 A string value is used directly (canonical name, alias, XTVERSION/ENQ result, etc.).
148 :returns: Canonical terminal name if recognized, ``None`` otherwise.
150 The auto-detection path (``term_program=True``) reads environment variables at call time
151 and caches the result. The environment is assumed immutable for the process lifetime;
152 callers that change ``TERM`` or ``TERM_PROGRAM`` mid-process must call
153 :func:`resolve_terminal.cache_clear` afterward.
154 """
155 if term_program is False:
156 return None
157 if term_program is True:
158 term_program = os.environ.get('TERM_PROGRAM', '') or os.environ.get('TERM', '')
159 if not term_program:
160 return None
161 key = term_program.strip().lower()
162 canonical = ALIASES.get(key, key)
163 if canonical not in KNOWN_TERMINALS:
164 return None
165 return canonical
168def _clamp_ambiguous_width(ambiguous_width: int) -> int:
169 """Clamp *ambiguous_width* to 1 or 2."""
170 # this operator.index() rejects bool/float and overall this is faster than
171 # combining min() and max().
172 value = _index(ambiguous_width)
173 if value < 1:
174 return 1
175 if value > 2:
176 return 2
177 return value