Skip to content

Commit d361971

Browse files
committed
text_wrap adds justify and use in format_table
Fixes #601
1 parent e009a41 commit d361971

4 files changed

Lines changed: 107 additions & 20 deletions

File tree

apsw/ext.py

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2055,13 +2055,25 @@ def repl(s):
20552055
)
20562056

20572057
# break headers and cells into lines
2058-
def wrap(text: str, width: int) -> list[str]:
2059-
return list(apsw.unicode.text_wrap(text, width)) or [""]
2058+
def wrap(text: str, width: int, justify: apsw.unicode.Justify, hyphen: str) -> list[str]:
2059+
return list(apsw.unicode.text_wrap(text, width, justify=justify, hyphen=hyphen)) or [""]
2060+
2061+
# special formatting
2062+
formats = {
2063+
int: {"justify": apsw.unicode.Justify.RIGHT, "hyphen": ""},
2064+
float: {"justify": apsw.unicode.Justify.LEFT, "hyphen": ""},
2065+
"header": {"justify": apsw.unicode.Justify.CENTER, "hyphen": "-"},
2066+
"_": {"justify": apsw.unicode.Justify.LEFT, "hyphen": "-"},
2067+
}
2068+
2069+
colnames = [wrap(colnames[i], colwidths[i], **formats["header"]) for i in range(len(colwidths))] # type: ignore
20602070

2061-
colnames = [wrap(colnames[i], colwidths[i]) for i in range(len(colwidths))] # type: ignore
20622071
for row in rows:
20632072
for i, (text, t) in enumerate(row): # type: ignore[misc]
2064-
row[i] = (wrap(text, colwidths[i]), t) # type: ignore
2073+
row[i] = (
2074+
wrap(text, colwidths[i], **formats.get(t, formats["_"])),
2075+
t,
2076+
) # type: ignore
20652077

20662078
## output
20672079
# are any cells more than one line?
@@ -2085,29 +2097,22 @@ def do_bar(chars: str) -> None:
20852097
line += chars[2]
20862098
out_lines.append(line)
20872099

2088-
def do_row(row, sep: str, *, centre: bool = False, header: bool = False) -> None:
2100+
def do_row(row, sep: str, *, header: bool = False) -> None:
20892101
for n in range(max(len(cell[0]) for cell in row)):
20902102
line = sep
20912103
for i, (cell, t) in enumerate(row):
2092-
text = cell[n] if n < len(cell) else ""
2093-
text = " " + text.rstrip() + " "
2094-
lt = apsw.unicode.text_width(text)
2095-
extra = " " * max(colwidths[i] + 2 - lt, 0)
2096-
if centre:
2097-
lpad = extra[: len(extra) // 2]
2098-
rpad = extra[len(extra) // 2 :]
2099-
else:
2100-
lpad = ""
2101-
rpad = extra
2104+
text = cell[n] if n < len(cell) else " " * colwidths[i]
2105+
# we do a space on each side
2106+
text = " " + text + " "
21022107
if header:
2103-
text = colour_wrap(lpad + text + rpad, None, header=True)
2108+
text = colour_wrap(text, None, header=True)
21042109
else:
2105-
text = lpad + colour_wrap(text, t) + rpad
2110+
text = colour_wrap(text, t)
21062111
line += text + sep
21072112
out_lines.append(line)
21082113

21092114
do_bar("╭─┬╮" if use_unicode else "+-++")
2110-
do_row([(c, None) for c in colnames], "│" if use_unicode else "|", centre=True, header=True)
2115+
do_row([(c, None) for c in colnames], "│" if use_unicode else "|", header=True)
21112116

21122117
# rows
21132118
which = 0

apsw/tests/ftstests.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2167,6 +2167,65 @@ def testIssue600(self):
21672167
res = list(apsw.unicode.text_wrap(text, width, **args))
21682168
self.assertEqual(res, expected)
21692169

2170+
def testWrapJustify(self):
2171+
"text_wrap justification"
2172+
2173+
longer = "a to the four verylongsingleword fivey ytrkes seven77 eightsse"
2174+
2175+
J = apsw.unicode.Justify
2176+
for text, justify, width, expected in (
2177+
("hello", J.LEFT, 10, ["hello "]),
2178+
("hello", J.RIGHT, 10, [" hello"]),
2179+
("hello", J.CENTER, 10, [" hello "]),
2180+
(
2181+
longer,
2182+
J.LEFT,
2183+
10,
2184+
[
2185+
"a to the ",
2186+
"four ",
2187+
"verylongs-",
2188+
"ingleword ",
2189+
"fivey ",
2190+
"ytrkes ",
2191+
"seven77 ",
2192+
"eightsse ",
2193+
],
2194+
),
2195+
(
2196+
longer,
2197+
J.RIGHT,
2198+
10,
2199+
[
2200+
" a to the",
2201+
" four",
2202+
"verylongs-",
2203+
" ingleword",
2204+
" fivey",
2205+
" ytrkes",
2206+
" seven77",
2207+
" eightsse",
2208+
],
2209+
),
2210+
(
2211+
longer,
2212+
J.CENTER,
2213+
10,
2214+
[
2215+
" a to the ",
2216+
" four ",
2217+
"verylongs-",
2218+
"ingleword ",
2219+
" fivey ",
2220+
" ytrkes ",
2221+
" seven77 ",
2222+
" eightsse ",
2223+
],
2224+
),
2225+
):
2226+
res = list(apsw.unicode.text_wrap(text, width, justify=justify))
2227+
self.assertEqual(res, expected)
2228+
21702229
def testVersion(self):
21712230
# check we have it in the date list
21722231
self.assertIn(apsw.unicode.unicode_version, apsw.unicode.version_dates)

apsw/unicode.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114

115115
from typing import Iterator, Iterable, Any
116116

117+
import enum
117118
import re
118119

119120
### BEGIN UNICODE UPDATE SECTION ###
@@ -559,6 +560,10 @@ def append_paragraph(p: list[str]) -> None:
559560
# turn back into newline as the expected delimiter
560561
return "\n".join(paragraphs) + "\n"
561562

563+
class Justify(enum.Enum):
564+
LEFT = 0
565+
CENTER = 1
566+
RIGHT = 2
562567

563568
def text_wrap(
564569
text: str,
@@ -568,6 +573,7 @@ def text_wrap(
568573
hyphen: str = "-",
569574
combine_space: bool = True,
570575
invalid: str = "?",
576+
justify: Justify = Justify.LEFT
571577
) -> Iterator[str]:
572578
"""Similar to :func:`textwrap.wrap` but Unicode grapheme cluster and line break aware
573579
@@ -585,6 +591,7 @@ def text_wrap(
585591
off.
586592
:param invalid: If invalid codepoints are encountered such as control characters and surrogates
587593
then they are replaced with this.
594+
:param justify: Where to align text within each line
588595
589596
This yields one line of :class:`str` at a time, which will be
590597
exactly ``width`` when output to a terminal. It will be right
@@ -597,6 +604,17 @@ def text_wrap(
597604

598605
hyphen_width = text_width(hyphen)
599606

607+
def do_justify(line: str, n_spaces: int):
608+
match justify:
609+
case Justify.LEFT:
610+
return line + " " * n_spaces
611+
case Justify.RIGHT:
612+
return " " * n_spaces + line
613+
case Justify.CENTER:
614+
l = n_spaces // 2
615+
r = n_spaces - l
616+
return " " * l + line + " " *r
617+
600618
if hyphen_width:
601619
# we don't need a hyphen if the text already fits in one line
602620
# which avoids truncating indent unnecessarily
@@ -664,7 +682,7 @@ def text_wrap(
664682
line_width = len(indent)
665683
seg_width = text_width(segment)
666684
continue
667-
yield "".join(accumulated) + " " * (width - line_width)
685+
yield do_justify("".join(accumulated), width - line_width)
668686
if combine_space and segment[0] == " ":
669687
# we added a space, but don't need it on new line
670688
segment = segment[1:]
@@ -678,7 +696,7 @@ def text_wrap(
678696
if len(accumulated) != 1:
679697
# length 1 means it is only indent which we don't output
680698
# as last line
681-
yield "".join(accumulated) + " " * (width - line_width)
699+
yield do_justify("".join(accumulated), width - line_width)
682700

683701

684702
def codepoint_name(codepoint: int | str) -> str | None:

doc/changes.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ APSW changes by version
1616
Fix wrapping, indents. and space for hyphens in
1717
:func:`apsw.unicode.text_wrap` under various conditions (:issue:`600`)
1818

19+
Added option to :func:`apsw.unicode.text_wrap` for justifying text. This
20+
is used in :func:`apsw.ext.format_query_table` to centre column names,
21+
and right align integers. That updates the :doc:`shell <shell>` output.
22+
(:issue:`601`)
23+
1924
3.51.2.0
2025
========
2126

0 commit comments

Comments
 (0)