Skip to content

Commit 9901b24

Browse files
committed
wip: Completely rework the formatting API and mark the old one as deprecated (to be removed in a future commits)
1 parent 75b3802 commit 9901b24

14 files changed

Lines changed: 1185 additions & 110 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
* Improved the performance of `String.normalize_spaces()` by using `str.translate()` instead of multiple `str.replace()` calls.
3434
* Improved the performance of `Data.remove_duplicates()` for lists and tuples: hashable items now deduplicate in O(n) using `dict.fromkeys()`, with an O(n²) equality-check fallback only for unhashable items (*lists, dicts, sets*).
3535
* The `Console.log()` method no longer forces the title to be all uppercase, giving the user a bit more freedom in how they want to format their title.
36+
* Added a new, typed, operator-based formatting API in `format_codes`.<br>
37+
The `Format` class (*alias* `F`) exposes every ANSI style/color attribute and uses `|` to combine codes and `()` to apply them to text – e.g. `(F.BOLD | F.RED)("hi")` and `F.hex("#F67")("hi")`.<br>
38+
The new `FormatCodes(*segments, sep="\n")` class builds the ANSI string on construction and exposes `.ansi`, `.raw`, `.code_positions`, `.print()` and `.input()`.<br>
39+
A companion `Term` class provides commonly used cursor- and screen-control sequences (`Term.HIDE_CURSOR`, `Term.up(n)`, `Term.move(row, col)`, `Term.title(text)`, …).
3640

3741
**BREAKING CHANGES:**
3842

@@ -43,6 +47,9 @@
4347
* Removed the `format_linebreaks` param from `Console.log()`, as the whole point of the `log()` method is to get a nicely formatted log message.
4448
* The `Console.get_args()` method no longer treats unknown flags as values but therefore saves them to the new `unknown_flags` property of the returned `ParsedArgs` object.
4549
* Changed the type of `ParsedArgData.values` and `ArgData.values` from <code>list[*str*]</code> to <code>tuple[*str*, ...]</code>, since the values of an argument should be immutable after parsing.
50+
* The original bracket-syntax `FormatCodes` class has been renamed to `deprFormatCodes` and moved into a new `depr_format_codes` module.<br>
51+
All internal call sites in the library still use the deprecated implementation; both APIs are exported in parallel.<br>
52+
`deprFormatCodes` will be removed in a future release – migrate to the new operator-based API at your convenience.
4653

4754

4855
<span id="v1-9-7" />

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,12 @@ from xulbux.color import rgba, hsla, hexa
108108
</tr>
109109
<tr>
110110
<td><a href="https://github.com/xulbux/python-lib-xulbux/wiki/color"><img src="https://img.shields.io/badge/color-B272FC?style=for-the-badge" alt="color"></a></td>
111-
<td><code>rgba</code><code>hsla</code><code>hexa</code><code>Color</code> classes, which include methods to work with<br>
111+
<td><code>rgba</code> <code>hsla</code> <code>hexa</code> <code>Color</code> classes, which include methods to work with<br>
112112
colors in various formats.</td>
113113
</tr>
114114
<tr>
115115
<td><a href="https://github.com/xulbux/python-lib-xulbux/wiki/console"><img src="https://img.shields.io/badge/console-B272FC?style=for-the-badge" alt="console"></a></td>
116-
<td><code>Console</code><code>ProgressBar</code> classes, which include methods for logging<br>
116+
<td><code>Console</code> <code>ProgressBar</code> classes, which include methods for logging<br>
117117
and other actions within the console.</td>
118118
</tr>
119119
<tr>
@@ -134,8 +134,8 @@ from xulbux.color import rgba, hsla, hexa
134134
</tr>
135135
<tr>
136136
<td><a href="https://github.com/xulbux/python-lib-xulbux/wiki/format_codes"><img src="https://img.shields.io/badge/format__codes-B272FC?style=for-the-badge" alt="format_codes"></a></td>
137-
<td><code>FormatCodes</code> class, which includes methods to print and work with strings that contain<br>
138-
special formatting codes, which are then converted to ANSI codes for pretty terminal output.</td>
137+
<td><code>Format</code> (<i>alias</i> <code>F</code>) <code>FormatCodes</code> <code>Term</code> classes for building richly formatted terminal output<br>
138+
via a typed, operator-based syntax and for emitting cursor- and screen-control sequences.</td>
139139
</tr>
140140
<tr>
141141
<td><a href="https://github.com/xulbux/python-lib-xulbux/wiki/json"><img src="https://img.shields.io/badge/json-B272FC?style=for-the-badge" alt="json"></a></td>

src/xulbux/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"File",
2626
"FileSys",
2727
"FormatCodes",
28+
"deprFormatCodes",
2829
"Json",
2930
"Regex",
3031
"String",
@@ -39,6 +40,7 @@
3940
from .file import File
4041
from .file_sys import FileSys
4142
from .format_codes import FormatCodes
43+
from .depr_format_codes import deprFormatCodes
4244
from .json import Json
4345
from .regex import Regex
4446
from .string import String

src/xulbux/base/consts.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,26 +77,33 @@ class ANSI:
7777
"""Constants and utilities for ANSI escape code sequences."""
7878

7979
CHAR_ESCAPED: Final = r"\x1b"
80-
"""Printable ANSI escape character."""
80+
"""**DEPRECATED** – only used by `depr_format_codes`. Will be removed together with that module.\n
81+
Printable ANSI escape character."""
8182
CHAR: Final = "\x1b"
8283
"""ANSI escape character."""
8384
START: Final = "["
84-
"""Start of an ANSI escape sequence."""
85+
"""**DEPRECATED** – only used by `depr_format_codes`. Will be removed together with that module.\n
86+
Start of an ANSI escape sequence."""
8587
SEP: Final = ";"
86-
"""Separator between ANSI escape sequence parts."""
88+
"""**DEPRECATED** – only used by `depr_format_codes`. Will be removed together with that module.\n
89+
Separator between ANSI escape sequence parts."""
8790
END: Final = "m"
88-
"""End of an ANSI escape sequence."""
91+
"""**DEPRECATED** – only used by `depr_format_codes`. Will be removed together with that module.\n
92+
End of an ANSI escape sequence."""
8993

9094
@classmethod
9195
def seq(cls, placeholders: int = 1, /) -> FormattableString:
92-
"""Generates an ANSI escape sequence with the specified number of placeholders."""
96+
"""**DEPRECATED** – only used by `depr_format_codes`. Will be removed together with that module.\n
97+
Generates an ANSI escape sequence with the specified number of placeholders."""
9398

9499
return cls.CHAR + cls.START + cls.SEP.join(["{}" for _ in range(placeholders)]) + cls.END
95100

96101
SEQ_COLOR: Final[FormattableString] = CHAR + START + "38" + SEP + "2" + SEP + "{}" + SEP + "{}" + SEP + "{}" + END
97-
"""ANSI escape sequence with three placeholders for setting the RGB text color."""
102+
"""**DEPRECATED** – only used by `depr_format_codes`. Will be removed together with that module.\n
103+
ANSI escape sequence with three placeholders for setting the RGB text color."""
98104
SEQ_BG_COLOR: Final[FormattableString] = CHAR + START + "48" + SEP + "2" + SEP + "{}" + SEP + "{}" + SEP + "{}" + END
99-
"""ANSI escape sequence with three placeholders for setting the RGB background color."""
105+
"""**DEPRECATED** – only used by `depr_format_codes`. Will be removed together with that module.\n
106+
ANSI escape sequence with three placeholders for setting the RGB background color."""
100107

101108
SEQ_LINK_OPEN: Final[FormattableString] = CHAR + "]8;;{}" + CHAR + "\\"
102109
"""OSC 8 hyperlink opening sequence with a placeholder for the URL."""
@@ -113,7 +120,8 @@ def seq(cls, placeholders: int = 1, /) -> FormattableString:
113120
"cyan",
114121
"white",
115122
}
116-
"""The standard terminal color names."""
123+
"""**DEPRECATED** – only used by `depr_format_codes` and as the seed for `COLOR_VARIANTS_MAP`.\n
124+
The standard terminal color names."""
117125

118126
COLOR_VARIANTS_MAP: Final[set[str]] = COLOR_MAP | {
119127
"br:black",
@@ -125,7 +133,8 @@ def seq(cls, placeholders: int = 1, /) -> FormattableString:
125133
"br:cyan",
126134
"br:white",
127135
}
128-
"""All color variants that can be used in formatting."""
136+
"""**DEPRECATED** – only used by `depr_format_codes` and as the seed for `COLOR_VARIANTS_MAP`.\n
137+
All color variants that can be used in formatting."""
129138

130139
CODES_MAP: Final[dict[str | tuple[str, ...], int]] = {
131140
################# SPECIFIC RESETS ##################
@@ -186,4 +195,5 @@ def seq(cls, placeholders: int = 1, /) -> FormattableString:
186195
"bg:br:cyan": 106,
187196
"bg:br:white": 107,
188197
}
189-
"""Dictionary mapping format keys to their corresponding ANSI code numbers."""
198+
"""**DEPRECATED** – only used by `depr_format_codes`. Will be removed together with that module.\n
199+
Dictionary mapping format keys to their corresponding ANSI code numbers."""

src/xulbux/cli/help.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .. import __version__
2-
from ..format_codes import FormatCodes
2+
from ..depr_format_codes import deprFormatCodes
33
from ..console import Console
44

55
from urllib.error import HTTPError
@@ -52,7 +52,7 @@ def is_latest_version() -> Optional[bool]:
5252
"punctuator": "br:black",
5353
"text": "white",
5454
}
55-
CLI_HELP = FormatCodes.to_ansi(
55+
CLI_HELP = deprFormatCodes.to_ansi(
5656
rf"""[_]
5757
[b|#7075FF] __ __
5858
[b|#7075FF] _ __ __ __/ / / /_ __ ___ __
@@ -89,6 +89,6 @@ def show_help() -> None:
8989
"""CLI command function for `xulbux-lib` command,<br>
9090
which shows some information about the library."""
9191

92-
FormatCodes._config_terminal()
92+
deprFormatCodes._config_terminal()
9393
print(CLI_HELP)
9494
Console.pause_exit(" [dim](Press any key to exit...)\n\n", pause=True)

src/xulbux/cli/tools.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from ..format_codes import FormatCodes
1+
from ..depr_format_codes import deprFormatCodes
22
from ..console import Console
33

44

@@ -10,19 +10,19 @@ def render_format_codes():
1010
vals = args.input.values
1111

1212
if not vals:
13-
FormatCodes.print(
13+
deprFormatCodes.print(
1414
"\n[_|i|dim]Provide a string to parse and render\n"
1515
"its format codes as ANSI terminal output.[_]\n"
1616
)
1717

1818
else:
19-
ansi = FormatCodes.to_ansi("".join(vals))
20-
ansi_escaped = FormatCodes.escape_ansi(ansi)
21-
ansi_stripped = FormatCodes.remove_ansi(ansi)
19+
ansi = deprFormatCodes.to_ansi("".join(vals))
20+
ansi_escaped = deprFormatCodes.escape_ansi(ansi)
21+
ansi_stripped = deprFormatCodes.remove_ansi(ansi)
2222

2323
print(f"\n{ansi}\n")
2424

2525
if len(ansi) != len(ansi_stripped):
26-
FormatCodes.print(f"[_|i|dim]{ansi_escaped}[_]\n")
26+
deprFormatCodes.print(f"[_|i|dim]{ansi_escaped}[_]\n")
2727
else:
28-
FormatCodes.print("[_|i|dim](The provided string doesn't contain any valid format codes.)\n")
28+
deprFormatCodes.print("[_|i|dim](The provided string doesn't contain any valid format codes.)\n")

src/xulbux/console.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from .base.decorators import mypyc_attr
88
from .base.consts import CHARS, ANSI
99

10-
from .format_codes import _PATTERNS as _FC_PATTERNS, FormatCodes
10+
from .depr_format_codes import _PATTERNS as _FC_PATTERNS, deprFormatCodes
1111
from .string import String
1212
from .color import Color
1313
from .regex import LazyRegex
@@ -454,9 +454,9 @@ def pause_exit(
454454
* `exit_code` – The exit code to use when exiting the program.
455455
* `reset_ansi` – Whether to reset the ANSI formatting after printing the prompt."""
456456

457-
FormatCodes.print(prompt, end="", flush=True)
457+
deprFormatCodes.print(prompt, end="", flush=True)
458458
if reset_ansi:
459-
FormatCodes.print("[_]", end="")
459+
deprFormatCodes.print("[_]", end="")
460460
if pause:
461461
cls._read_single_key()
462462
if exit:
@@ -532,7 +532,7 @@ def log(
532532
px, mx = " " * title_px, " " * title_mx
533533

534534
# TITLE LENGTH INCLUDING PADDING AND MARGIN
535-
title_len: int = len(FormatCodes.remove(title)) + (title_px * 2) + (title_mx * 2)
535+
title_len: int = len(deprFormatCodes.remove(title)) + (title_px * 2) + (title_mx * 2)
536536

537537
# CALCULATE DISTANCE TO NEXT TAB STOP
538538
tab: str = " " * (-title_len % tab_size)
@@ -541,7 +541,7 @@ def log(
541541
wrap_len: int = cls.width - (title_len + len(tab))
542542

543543
# REMOVE ALL FORMAT CODES AS THEY WON'T AFFECT THE VISIBLE LENGTH OF THE PROMPT
544-
clean_prompt, removals = (*FormatCodes.remove(str(prompt), get_removals=True, _ignore_linebreaks=True), )
544+
clean_prompt, removals = (*deprFormatCodes.remove(str(prompt), get_removals=True, _ignore_linebreaks=True), )
545545

546546
# SPLIT PROMPT INTO LINES AND THEN SPLIT EACH LINE INTO CHUNKS THAT FIT WITHIN THE WRAP LENGTH
547547
prompt_lst: list[str] = list(chain.from_iterable(cls._process_lines(clean_prompt, wrap_len)))
@@ -557,7 +557,7 @@ def log(
557557
f"{tab}{f'[{default_color}]' if default_color else ''}{prompt}[_]"
558558
)
559559

560-
FormatCodes.print(out, default_color=default_color, end=end)
560+
deprFormatCodes.print(out, default_color=default_color, end=end)
561561

562562
@classmethod
563563
def debug(
@@ -782,7 +782,7 @@ def log_box_filled(
782782
+ "[*]"
783783
) for line, unfmt in zip(lines, unfmt_lines)]
784784

785-
FormatCodes.print(
785+
deprFormatCodes.print(
786786
( \
787787
f"{start}{spaces_l}[{bg_fc}]{pady}[*]\n"
788788
+ "\n".join(lines)
@@ -893,7 +893,7 @@ def log_box_bordered(
893893
+ border_r
894894
) for line, unfmt in zip(lines, unfmt_lines)]
895895

896-
FormatCodes.print(
896+
deprFormatCodes.print(
897897
( \
898898
f"{start}{border_t}[_]\n"
899899
+ "\n".join(lines)
@@ -928,14 +928,14 @@ def confirm(
928928
information about formatting codes, see the `format_codes` module documentation."""
929929

930930
confirmed = cls.input(
931-
FormatCodes.to_ansi(
931+
deprFormatCodes.to_ansi(
932932
f"{start}{str(prompt)} [_|dim](({'Y' if default_is_yes else 'y'}/{'n' if default_is_yes else 'N'}): )",
933933
default_color=default_color,
934934
)
935935
).strip().lower() in ({"", "y", "yes"} if default_is_yes else {"y", "yes"})
936936

937937
if end:
938-
FormatCodes.print(end, end="")
938+
deprFormatCodes.print(end, end="")
939939
return confirmed
940940

941941
@classmethod
@@ -967,11 +967,11 @@ def multiline_input(
967967
kb = KeyBindings()
968968
kb.add("c-d", eager=True)(cls._multiline_input_submit)
969969

970-
FormatCodes.print(start + str(prompt), default_color=default_color)
970+
deprFormatCodes.print(start + str(prompt), default_color=default_color)
971971
if show_keybindings:
972-
FormatCodes.print("[dim][[b](CTRL+D)[dim] : end of input][_dim]")
972+
deprFormatCodes.print("[dim][[b](CTRL+D)[dim] : end of input][_dim]")
973973
input_string = _pt.prompt(input_prefix, multiline=True, wrap_lines=True, key_bindings=kb)
974-
FormatCodes.print("[_]" if reset_ansi else "", end=end[1:] if end.startswith("\n") else end)
974+
deprFormatCodes.print("[_]" if reset_ansi else "", end=end[1:] if end.startswith("\n") else end)
975975

976976
return input_string
977977

@@ -1084,7 +1084,7 @@ def input(
10841084

10851085
custom_style = Style.from_dict({"bottom-toolbar": "noreverse"})
10861086
session: _pt.PromptSession[str] = _pt.PromptSession(
1087-
message=_pt.formatted_text.ANSI(FormatCodes.to_ansi(str(prompt), default_color=default_color)),
1087+
message=_pt.formatted_text.ANSI(deprFormatCodes.to_ansi(str(prompt), default_color=default_color)),
10881088
validator=_ConsoleInputValidator(
10891089
helper.get_text,
10901090
mask_char=mask_char,
@@ -1094,13 +1094,13 @@ def input(
10941094
validate_while_typing=True,
10951095
key_bindings=kb,
10961096
bottom_toolbar=helper.bottom_toolbar,
1097-
placeholder=_pt.formatted_text.ANSI(FormatCodes.to_ansi(f"[i|br:black]{placeholder}[_i|_c]"))
1097+
placeholder=_pt.formatted_text.ANSI(deprFormatCodes.to_ansi(f"[i|br:black]{placeholder}[_i|_c]"))
10981098
if placeholder else "",
10991099
style=custom_style,
11001100
)
1101-
FormatCodes.print(start, end="")
1101+
deprFormatCodes.print(start, end="")
11021102
session.prompt()
1103-
FormatCodes.print(end, end="")
1103+
deprFormatCodes.print(end, end="")
11041104

11051105
if (result_text := helper.get_text()) in {"", None}:
11061106
if default_val is not None:
@@ -1242,7 +1242,7 @@ def _prepare_log_box(
12421242
else:
12431243
lines = [line for val in values for line in str(val).splitlines()]
12441244

1245-
unfmt_lines = [FormatCodes.remove(line, default_color) for line in lines]
1245+
unfmt_lines = [deprFormatCodes.remove(line, default_color) for line in lines]
12461246
max_line_len = max(len(line) for line in unfmt_lines) if unfmt_lines else 0
12471247

12481248
return lines, unfmt_lines, max_line_len
@@ -1632,7 +1632,7 @@ def bottom_toolbar(self) -> _pt.formatted_text.ANSI:
16321632
if self.max_len and len(text_to_check) == self.max_len:
16331633
toolbar_msgs.append("[b|#000|bg:br:yellow]( Maximum length reached )")
16341634

1635-
return _pt.formatted_text.ANSI(FormatCodes.to_ansi(" ".join(toolbar_msgs)))
1635+
return _pt.formatted_text.ANSI(deprFormatCodes.to_ansi(" ".join(toolbar_msgs)))
16361636

16371637
except Exception:
16381638
return _pt.formatted_text.ANSI("")
@@ -1987,7 +1987,7 @@ def _draw_progress_bar(self, current: int, total: int, /, label: Optional[str] =
19871987
)
19881988

19891989
bar = f"{self._create_bar(current, total, max(1, bar_width))}[*]"
1990-
progress_text = _PATTERNS.bar.sub(FormatCodes.to_ansi(bar), formatted)
1990+
progress_text = _PATTERNS.bar.sub(deprFormatCodes.to_ansi(bar), formatted)
19911991

19921992
self._current_progress_str = progress_text
19931993
self._last_line_len = len(progress_text)
@@ -2014,9 +2014,9 @@ def _get_formatted_info_and_bar_width(
20142014
fmt_parts.append(fmt_part)
20152015

20162016
fmt_str = self.sep.join(fmt_parts)
2017-
fmt_str = FormatCodes.to_ansi(fmt_str)
2017+
fmt_str = deprFormatCodes.to_ansi(fmt_str)
20182018

2019-
bar_space = Console.width - len(FormatCodes.remove_ansi(_PATTERNS.bar.sub("", fmt_str)))
2019+
bar_space = Console.width - len(deprFormatCodes.remove_ansi(_PATTERNS.bar.sub("", fmt_str)))
20202020
bar_width = min(bar_space, self.max_width) if bar_space > 0 else 0
20212021

20222022
return fmt_str, bar_width
@@ -2315,8 +2315,8 @@ def _animation_loop(self) -> None:
23152315

23162316
self._flush_buffer()
23172317

2318-
frame = FormatCodes.to_ansi(f"{self.frames[self._frame_index % len(self.frames)]}[*]")
2319-
formatted = FormatCodes.to_ansi(self.sep.join(
2318+
frame = deprFormatCodes.to_ansi(f"{self.frames[self._frame_index % len(self.frames)]}[*]")
2319+
formatted = deprFormatCodes.to_ansi(self.sep.join(
23202320
fmt_part for part in self.throbber_format if \
23212321
(fmt_part := _PATTERNS.animation.sub(frame, _PATTERNS.label.sub(self.label or "", part)))
23222322
))

src/xulbux/data.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from .base.types import IndexIterableTT, IndexIterable, DataObjTT, DataObj as DataObjType
77

8-
from .format_codes import FormatCodes
8+
from .depr_format_codes import deprFormatCodes
99
from .string import String
1010
from .regex import Regex
1111

@@ -541,7 +541,7 @@ def print(
541541
----------------------------------------------------------------------------------------------------------------
542542
For more detailed information about formatting codes, see the `format_codes` module documentation."""
543543

544-
FormatCodes.print(
544+
deprFormatCodes.print(
545545
cls.render(
546546
data,
547547
indent=indent,

0 commit comments

Comments
 (0)