codeterrain 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
codeterrain/budget.py ADDED
@@ -0,0 +1,128 @@
1
+ """Output budget: text tables cut at about N tokens, estimated as one token per four characters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Callable, Sequence
7
+
8
+ DEFAULT_BUDGET = 2000
9
+ CHARS_PER_TOKEN = 4
10
+ MAX_COLUMN_WIDTH = 60
11
+
12
+
13
+ #: C0 controls other than tab and newline, DEL, C1 controls, the bidi marks, overrides and
14
+ #: isolates, the Unicode line and paragraph separators, and lone surrogates (an undecodable file
15
+ #: name).
16
+ _UNSAFE = re.compile(
17
+ "[\x00-\x08\x0b-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069\u200e\u200f\u061c\u2028\u2029\ud800-\udfff]"
18
+ )
19
+
20
+
21
+ def terminal_safe(text: str) -> str:
22
+ """Escape characters that a terminal would act on, so repository text cannot drive it.
23
+
24
+ Fixture names, paths and error messages come from the scanned repository; an escape
25
+ sequence in one could clear the screen or forge rows. Each unsafe character is shown as
26
+ ``\\xNN`` or ``\\uNNNN``. Tab and newline stay, since the text output is line-based.
27
+ """
28
+ return _UNSAFE.sub(
29
+ lambda m: f"\\x{ord(m[0]):02x}" if ord(m[0]) < 0x100 else f"\\u{ord(m[0]):04x}", text
30
+ )
31
+
32
+
33
+ #: Everything ``_UNSAFE`` covers, plus tab, newline and carriage return.
34
+ _UNSAFE_IN_FIELD = re.compile(
35
+ "[\x00-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069\u200e\u200f\u061c\u2028\u2029\ud800-\udfff]"
36
+ )
37
+
38
+
39
+ def field_safe(text: str) -> str:
40
+ """Escape one value taken from the repository before it is placed in a line of output.
41
+
42
+ ``terminal_safe`` runs on the finished output and has to keep the renderer's own newlines and
43
+ tabs; a newline inside a fixture name or a route path would still start a forged row. A field
44
+ therefore has every control escaped, layout characters included.
45
+ """
46
+ return _UNSAFE_IN_FIELD.sub(
47
+ lambda m: f"\\x{ord(m[0]):02x}" if ord(m[0]) < 0x100 else f"\\u{ord(m[0]):04x}", text
48
+ )
49
+
50
+
51
+ def estimate_tokens(text: str) -> int:
52
+ return -(-len(text) // CHARS_PER_TOKEN)
53
+
54
+
55
+ def posix_path(path: str) -> str:
56
+ """Output paths are repo-relative POSIX; Windows backslashes are normalised."""
57
+ return path.replace("\\", "/")
58
+
59
+
60
+ def cut_lines(
61
+ lines: Sequence[str],
62
+ budget: int,
63
+ hint: str,
64
+ keep: int = 1,
65
+ omitted: Callable[[int], str] | None = None,
66
+ more: int = 0,
67
+ ) -> str:
68
+ """Join ``lines`` within ``budget`` tokens; the first ``keep`` lines (headers) always stay.
69
+
70
+ When rows are dropped the last line reads ``+N more; narrow with <hint>``, or ``+N more:
71
+ <omitted(shown)>; narrow with <hint>`` to say what was left out (``shown`` rows were kept).
72
+ ``more`` rows follow ``lines`` that were never formatted, and are always dropped. A budget of
73
+ 0 or less means no limit.
74
+ """
75
+ text = "\n".join(lines)
76
+ limit = budget * CHARS_PER_TOKEN
77
+ if budget <= 0 or (not more and len(text) + 1 <= limit):
78
+ return text
79
+ # The detail of every row is the longest the detail of the rows left out can be.
80
+ detail = f": {omitted(0)}" if omitted else ""
81
+ total_rows = len(lines) - keep + more
82
+ used = sum(len(line) + 1 for line in lines[:keep])
83
+ shown = 0
84
+ for line in lines[keep:]:
85
+ remaining = total_rows - shown - 1
86
+ footer = f"+{remaining} more{detail}; narrow with {hint}"
87
+ if used + len(line) + 1 + (len(footer) + 1 if remaining else 0) > limit:
88
+ break
89
+ used += len(line) + 1
90
+ shown += 1
91
+ dropped = total_rows - shown
92
+ kept = list(lines[: keep + shown])
93
+ if dropped:
94
+ detail = f": {omitted(shown)}" if omitted else ""
95
+ kept.append(f"+{dropped} more{detail}; narrow with {hint}")
96
+ return "\n".join(kept)
97
+
98
+
99
+ def render_table(
100
+ headers: Sequence[str],
101
+ rows: Sequence[Sequence[str]],
102
+ budget: int,
103
+ hint: str,
104
+ omitted: Callable[[int], str] | None = None,
105
+ ) -> str:
106
+ """Column-aligned text table (last column unpadded), cut to the budget."""
107
+ if budget > 0:
108
+ # Every row costs at least its newline (trailing empty cells are stripped), so no row
109
+ # past this many can fit: those are counted, never formatted. A table cut this way is
110
+ # aligned on the rows that could be shown.
111
+ fits = budget * CHARS_PER_TOKEN
112
+ if len(rows) > fits:
113
+ cut = render_table(headers, rows[:fits], 0, hint)
114
+ return cut_lines(
115
+ cut.split("\n"), budget, hint, omitted=omitted, more=len(rows) - fits
116
+ )
117
+ cells = [[field_safe(posix_path(str(c))) for c in row] for row in rows]
118
+ widths = [len(h) for h in headers]
119
+ for row in cells:
120
+ for i, cell in enumerate(row[:-1]):
121
+ widths[i] = max(widths[i], min(len(cell), MAX_COLUMN_WIDTH))
122
+
123
+ def fmt(row: Sequence[str]) -> str:
124
+ parts = [cell.ljust(widths[i]) for i, cell in enumerate(row[:-1])]
125
+ parts.append(row[-1])
126
+ return " ".join(parts).rstrip()
127
+
128
+ return cut_lines([fmt(headers), *(fmt(r) for r in cells)], budget, hint, omitted=omitted)