Skip to content

Commit

Permalink
Add a max_rows argument to encode_text() (#115)
Browse files Browse the repository at this point in the history
This controls the maximum number of rows that will be returned,
defaulting to ROWS (6). It can be set to a lower value to produce a
partial board or 0 to support unlimited rows.

ValueError is no longer raised when the result exceeds max_rows.
Instead, the result is truncated to max_rows.

Also, canonically print the board's dimensions as (COLS, ROWS), which
is a more standard representation.
  • Loading branch information
jparise committed Jun 6, 2023
1 parent f67e27a commit f2d24a3
Show file tree
Hide file tree
Showing 4 changed files with 33 additions and 21 deletions.
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ as integer `character codes <https://docs.vestaboard.com/characters>`_. Vesta
includes some helpful routines for working with these character codes.

.. automodule:: vesta.chars
:members: Row, Rows
:members: COLS, ROWS, Row, Rows

.. autoclass:: vesta.Color
:show-inheritance:
Expand Down
7 changes: 4 additions & 3 deletions tests/test_chars.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,10 @@ def test_unknown_alignments(self):
with pytest.raises(ValueError, match="unknown vertical alignment"):
encode_text("a", valign="unknown") # type: ignore

def test_maximum_rows(self):
with pytest.raises(ValueError, match=f"results in {ROWS + 1} lines"):
encode_text("a\n" * (ROWS + 1))
def test_max_rows(self):
assert len(encode_text("a\n" * (ROWS + 1))) == ROWS
assert len(encode_text("a\n" * 5, max_rows=4)) == 4
assert len(encode_text("a\n" * 10, max_rows=0)) == 10

def test_margin(self):
chars = [
Expand Down
6 changes: 3 additions & 3 deletions tests/test_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def test_post_message_list(self, client: Client, respx_mock: MockRouter):
client.post_message("sub_id", chars)

def test_post_message_list_dimensions(self, client: Client):
with pytest.raises(ValueError, match=rf"expected a \({ROWS}, {COLS}\) array"):
with pytest.raises(ValueError, match=rf"expected a \({COLS}, {ROWS}\) array"):
client.post_message("sub_id", [])

def test_post_message_type(self, client: Client):
Expand Down Expand Up @@ -159,7 +159,7 @@ def test_write_message(self, local_client: LocalClient, respx_mock: MockRouter):
assert respx_mock.calls.last.request.content == json.dumps(chars).encode()

def test_write_message_dimensions(self, local_client: LocalClient):
with pytest.raises(ValueError, match=rf"expected a \({ROWS}, {COLS}\) array"):
with pytest.raises(ValueError, match=rf"expected a \({COLS}, {ROWS}\) array"):
local_client.write_message([])


Expand Down Expand Up @@ -198,5 +198,5 @@ def test_write_message(self, rw_client: ReadWriteClient, respx_mock: MockRouter)
assert respx_mock.calls.last.request.content == json.dumps(chars).encode()

def test_write_message_dimensions(self, rw_client: ReadWriteClient):
with pytest.raises(ValueError, match=rf"expected a \({ROWS}, {COLS}\) array"):
with pytest.raises(ValueError, match=rf"expected a \({COLS}, {ROWS}\) array"):
rw_client.write_message([])
39 changes: 25 additions & 14 deletions vesta/chars.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import math
import sys
from typing import Container
from typing import Final
from typing import List
from typing import Literal
from typing import Optional
Expand All @@ -32,11 +33,15 @@
from typing import Union
from typing import cast

ROWS = 6
COLS = 22
PRINTABLE = " ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$() - +&=;: '\"%,. /? °"
CHARMAP = {c: i for i, c in enumerate(PRINTABLE) if i == 0 or c != " "}

#: The number of columns on a board.
COLS: Final[int] = 22

#: The number of rows on a board.
ROWS: Final[int] = 6

#: A row of character codes.
Row = List[int]

Expand All @@ -50,7 +55,7 @@ def validate_rows(rows: Rows) -> None:
:raises ValueError: if ``rows`` does not have the correct dimensions
"""
if len(rows) != ROWS or not all(len(row) == COLS for row in rows):
raise ValueError(f"expected a ({ROWS}, {COLS}) array of encoded characters")
raise ValueError(f"expected a ({COLS}, {ROWS}) array of encoded characters")


class Color(enum.IntEnum):
Expand Down Expand Up @@ -153,6 +158,7 @@ def encode_text(
s: str,
align: Literal["left", "center", "right"] = "left",
valign: Optional[Literal["top", "middle", "bottom"]] = "top",
max_rows: int = ROWS,
margin: int = 0,
fill: int = Color.BLANK,
breaks: Container[int] = frozenset({0}),
Expand All @@ -164,8 +170,12 @@ def encode_text(
``align`` controls the text's alignment within the row: `left`, `right`, or
`center`. ``valign`` controls the text's vertical alignment within the full
board: `top`, `middle`, `bottom`, or None (to never add rows, potentially
resulting in a partial board).
board (up to ``max_rows``): `top`, `middle`, `bottom`, or None (to never add
rows, potentially resulting in a partial board).
``max_rows`` determines the maximum number of rows that will be returned,
potentially truncating the result. When ``max_rows`` is zero, the row count
is unlimited.
``margin`` specifies the width (in columns) of the left and right margins.
The ``fill`` character code (generally a :py:class:`Color`) is used to fill
Expand All @@ -178,9 +188,7 @@ def encode_text(
cannot be found, the line will be broken at the column limit (potentially
mid-"word").
:raises ValueError: if the string contains unsupported characters or codes,
or if the resulting encoding sequence would exceed the
maximum number of supported rows
:raises ValueError: if the string contains unsupported characters or codes
>>> encode_text('multiple\\nlines\\nof\\ntext', align="center", valign="middle")
... # doctest: +NORMALIZE_WHITESPACE
Expand Down Expand Up @@ -210,20 +218,23 @@ def find_break(line: Row) -> Tuple[int, int]:

rows.append(_format_row(line, align, margin, fill))

if max_rows < 1: # unlimited rows
return rows

nrows = len(rows)
if nrows < ROWS and valign is not None:
if nrows < max_rows and valign is not None:
empty = [fill] * COLS
if valign == "top":
rows += [empty] * (ROWS - nrows)
rows += [empty] * (max_rows - nrows)
elif valign == "bottom":
rows = [empty] * (ROWS - nrows) + rows
rows = [empty] * (max_rows - nrows) + rows
elif valign == "middle":
pad = (ROWS - nrows) / 2
pad = (max_rows - nrows) / 2
rows = [empty] * math.floor(pad) + rows + [empty] * math.ceil(pad)
else:
raise ValueError(f"unknown vertical alignment: {valign}")
elif nrows > ROWS:
raise ValueError(f"{s!r} results in {nrows} lines (max {ROWS})")
elif nrows > max_rows:
rows = rows[:max_rows]

return rows

Expand Down

0 comments on commit f2d24a3

Please sign in to comment.