Skip to content

Commit d97f092

Browse files
authored
Merge pull request #196 from wimglenn/better_line_width_estimate
i18n: more accurate line width estimate...possible?
2 parents 8a4e741 + eb27ce4 commit d97f092

File tree

4 files changed

+102
-6
lines changed

4 files changed

+102
-6
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ Jan Balster
2222
Grig Gheorghiu
2323
Bob Ippolito
2424
Christian Tismer
25+
Wim Glenn

CHANGELOG

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
1.6.0 (unreleased)
2+
==================
3+
4+
- add ``TerminalWriter.width_of_current_line`` (i18n version of
5+
``TerminalWriter.chars_on_current_line``), a read-only property
6+
that tracks how wide the current line is, attempting to take
7+
into account international characters in the calculation.
8+
19
1.5.4 (2018-06-27)
210
==================
311

py/_io/terminalwriter.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"""
66

77

8-
import sys, os
8+
import sys, os, unicodedata
99
import py
1010
py3k = sys.version_info[0] >= 3
1111
from py.builtin import text, bytes
@@ -53,6 +53,21 @@ def get_terminal_width():
5353

5454
terminal_width = get_terminal_width()
5555

56+
char_width = {
57+
'A': 1, # "Ambiguous"
58+
'F': 2, # Fullwidth
59+
'H': 1, # Halfwidth
60+
'N': 1, # Neutral
61+
'Na': 1, # Narrow
62+
'W': 2, # Wide
63+
}
64+
65+
66+
def get_line_width(text):
67+
text = unicodedata.normalize('NFC', text)
68+
return sum(char_width.get(unicodedata.east_asian_width(c), 1) for c in text)
69+
70+
5671
# XXX unify with _escaped func below
5772
def ansi_print(text, esc, file=None, newline=True, flush=False):
5873
if file is None:
@@ -140,6 +155,7 @@ def __init__(self, file=None, stringio=False, encoding=None):
140155
self.hasmarkup = should_do_markup(file)
141156
self._lastlen = 0
142157
self._chars_on_current_line = 0
158+
self._width_of_current_line = 0
143159

144160
@property
145161
def fullwidth(self):
@@ -164,6 +180,16 @@ def chars_on_current_line(self):
164180
"""
165181
return self._chars_on_current_line
166182

183+
@property
184+
def width_of_current_line(self):
185+
"""Return an estimate of the width so far in the current line.
186+
187+
.. versionadded:: 1.6.0
188+
189+
:rtype: int
190+
"""
191+
return self._width_of_current_line
192+
167193
def _escaped(self, text, esc):
168194
if esc and self.hasmarkup:
169195
text = (''.join(['\x1b[%sm' % cod for cod in esc]) +
@@ -223,12 +249,17 @@ def write(self, msg, **kw):
223249
markupmsg = msg
224250
write_out(self._file, markupmsg)
225251

226-
def _update_chars_on_current_line(self, text):
227-
fields = text.rsplit('\n', 1)
228-
if '\n' in text:
229-
self._chars_on_current_line = len(fields[-1])
252+
def _update_chars_on_current_line(self, text_or_bytes):
253+
newline = b'\n' if isinstance(text_or_bytes, bytes) else '\n'
254+
current_line = text_or_bytes.rsplit(newline, 1)[-1]
255+
if isinstance(current_line, bytes):
256+
current_line = current_line.decode('utf-8', errors='replace')
257+
if newline in text_or_bytes:
258+
self._chars_on_current_line = len(current_line)
259+
self._width_of_current_line = get_line_width(current_line)
230260
else:
231-
self._chars_on_current_line += len(fields[-1])
261+
self._chars_on_current_line += len(current_line)
262+
self._width_of_current_line += get_line_width(current_line)
232263

233264
def line(self, s='', **kw):
234265
self.write(s, **kw)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# coding: utf-8
2+
from __future__ import unicode_literals
3+
4+
from py._io.terminalwriter import TerminalWriter
5+
6+
7+
def test_terminal_writer_line_width_init():
8+
tw = TerminalWriter()
9+
assert tw.chars_on_current_line == 0
10+
assert tw.width_of_current_line == 0
11+
12+
13+
def test_terminal_writer_line_width_update():
14+
tw = TerminalWriter()
15+
tw.write('hello world')
16+
assert tw.chars_on_current_line == 11
17+
assert tw.width_of_current_line == 11
18+
19+
20+
def test_terminal_writer_line_width_update_with_newline():
21+
tw = TerminalWriter()
22+
tw.write('hello\nworld')
23+
assert tw.chars_on_current_line == 5
24+
assert tw.width_of_current_line == 5
25+
26+
27+
def test_terminal_writer_line_width_update_with_wide_text():
28+
tw = TerminalWriter()
29+
tw.write('乇乂ㄒ尺卂 ㄒ卄丨匚匚')
30+
assert tw.chars_on_current_line == 11
31+
assert tw.width_of_current_line == 21 # 5*2 + 1 + 5*2
32+
33+
34+
def test_terminal_writer_line_width_update_with_wide_bytes():
35+
tw = TerminalWriter()
36+
tw.write('乇乂ㄒ尺卂 ㄒ卄丨匚匚'.encode('utf-8'))
37+
assert tw.chars_on_current_line == 11
38+
assert tw.width_of_current_line == 21
39+
40+
41+
def test_terminal_writer_line_width_composed():
42+
tw = TerminalWriter()
43+
text = 'café food'
44+
assert len(text) == 9
45+
tw.write(text)
46+
assert tw.chars_on_current_line == 9
47+
assert tw.width_of_current_line == 9
48+
49+
50+
def test_terminal_writer_line_width_combining():
51+
tw = TerminalWriter()
52+
text = 'café food'
53+
assert len(text) == 10
54+
tw.write(text)
55+
assert tw.chars_on_current_line == 10
56+
assert tw.width_of_current_line == 9

0 commit comments

Comments
 (0)