|
93 | 93 | },
|
94 | 94 | {
|
95 | 95 | "cell_type": "code",
|
96 |
| - "execution_count": 2, |
97 |
| - "id": "81d0784f", |
| 96 | + "execution_count": 3, |
| 97 | + "id": "f924a813", |
98 | 98 | "metadata": {},
|
99 | 99 | "outputs": [],
|
100 | 100 | "source": [
|
101 |
| - "# (background, foreground)\n", |
102 | 101 | "_COLORS = {\n",
|
103 |
| - " '+': (\"#d2f5d6\", \"#22863a\"), # green additions\n", |
104 |
| - " '-': (\"#f8d7da\", \"#b31d28\"), # red deletions\n", |
105 |
| - " '@': (None, \"#6f42c1\"), # purple hunk headers\n", |
| 102 | + " '+': (\"#d2f5d6\", \"#22863a\"), # additions (green)\n", |
| 103 | + " '-': (\"#f8d7da\", \"#b31d28\"), # deletions (red)\n", |
| 104 | + " '@': (None, \"#6f42c1\"), # hunk header (purple)\n", |
106 | 105 | "}\n",
|
107 | 106 | "\n",
|
| 107 | + "def _css(**rules: str) -> str:\n", |
| 108 | + " \"\"\"Convert kwargs to a CSS string (snake_case → kebab-case).\"\"\"\n", |
| 109 | + " return \";\".join(f\"{k.replace('_', '-')}: {v}\" for k, v in rules.items())\n", |
| 110 | + "\n", |
| 111 | + "def _render(html_str: str) -> None:\n", |
| 112 | + " \"\"\"Render inside Jupyter if available, else print to stdout.\"\"\"\n", |
| 113 | + " try:\n", |
| 114 | + " display # type: ignore[name-defined]\n", |
| 115 | + " from IPython.display import HTML # noqa: WPS433\n", |
| 116 | + " display(HTML(html_str))\n", |
| 117 | + " except NameError:\n", |
| 118 | + " print(html_str, flush=True)\n", |
| 119 | + "\n", |
| 120 | + "# ---------- diff helpers ------------------------------------------------------\n", |
| 121 | + "\n", |
108 | 122 | "def _style(line: str) -> str:\n",
|
109 |
| - " \"\"\"Return HTML-escaped diff line wrapped with span + colors.\"\"\"\n", |
110 |
| - " bg, fg = _COLORS.get(line[0], (None, None))\n", |
111 |
| - " css = \";\".join(filter(None, (\n", |
112 |
| - " f\"background:{bg}\" if bg else \"\",\n", |
113 |
| - " f\"color:{fg}\" if fg else \"\"\n", |
114 |
| - " )))\n", |
| 123 | + " \"\"\"Wrap a diff line in a <span> with optional colors.\"\"\"\n", |
| 124 | + " bg, fg = _COLORS.get(line[:1], (None, None))\n", |
| 125 | + " css = \";\".join(s for s in (f\"background:{bg}\" if bg else \"\",\n", |
| 126 | + " f\"color:{fg}\" if fg else \"\") if s)\n", |
115 | 127 | " return f'<span style=\"{css}\">{html.escape(line)}</span>'\n",
|
116 | 128 | "\n",
|
117 |
| - "def _wrap(details: Iterable[str]) -> str:\n", |
118 |
| - " \"\"\"Wrap styled diff lines inside a collapsible <details> block.\"\"\"\n", |
119 |
| - " body = \"<br>\".join(details)\n", |
| 129 | + "def _wrap(lines: Iterable[str]) -> str:\n", |
| 130 | + " body = \"<br>\".join(lines)\n", |
120 | 131 | " return (\n",
|
121 | 132 | " \"<details>\"\n",
|
122 | 133 | " \"<summary>🕵️♂️ Critique & Diff (click to expand)</summary>\"\n",
|
|
125 | 136 | " )\n",
|
126 | 137 | "\n",
|
127 | 138 | "def show_critique_and_diff(old: str, new: str) -> str:\n",
|
128 |
| - " \"\"\"Display / return a GitHub-friendly HTML diff between two texts.\"\"\"\n", |
129 |
| - " raw_diff = difflib.unified_diff(old.splitlines(), new.splitlines(),\n", |
130 |
| - " fromfile=\"old\", tofile=\"new\", lineterm=\"\")\n", |
131 |
| - " styled = map(_style, raw_diff)\n", |
132 |
| - " html_block = _wrap(styled)\n", |
133 |
| - "\n", |
134 |
| - " if _IN_IPYTHON:\n", |
135 |
| - " display(HTML(html_block))\n", |
136 |
| - " else:\n", |
137 |
| - " sys.stdout.write(html_block)" |
138 |
| - ] |
139 |
| - }, |
140 |
| - { |
141 |
| - "cell_type": "code", |
142 |
| - "execution_count": 3, |
143 |
| - "id": "f924a813", |
144 |
| - "metadata": {}, |
145 |
| - "outputs": [], |
146 |
| - "source": [ |
147 |
| - "def _css(**rules: str) -> str:\n", |
148 |
| - " return \"; \".join(f\"{k.replace('_', '-')}: {v}\" for k, v in rules.items())\n", |
149 |
| - "\n", |
150 |
| - "def _render(html_str: str) -> None:\n", |
151 |
| - " \"\"\"Render inside Jupyter (if present) or fall back to stdout.\"\"\"\n", |
152 |
| - " try:\n", |
153 |
| - " display # type: ignore\n", |
154 |
| - " display(HTML(html_str))\n", |
155 |
| - " except NameError:\n", |
156 |
| - " print(html_str, flush=True)\n", |
157 |
| - "\n", |
158 |
| - "\n", |
159 |
| - "CARD = _css(background=\"#f8f9fa\", border_radius=\"8px\", padding=\"18px 22px\",\n", |
160 |
| - " margin_bottom=\"18px\", border=\"1px solid #e0e0e0\",\n", |
161 |
| - " box_shadow=\"0 1px 4px #0001\")\n", |
162 |
| - "TITLE = _css(font_weight=\"600\", font_size=\"1.1em\", color=\"#2d3748\",\n", |
163 |
| - " margin_bottom=\"6px\")\n", |
164 |
| - "LABEL = _css(color=\"#718096\", font_size=\"0.95em\", font_weight=\"500\",\n", |
165 |
| - " margin_right=\"6px\")\n", |
166 |
| - "EXTRACT = _css(font_family=\"monospace\", background=\"#f1f5f9\", padding=\"7px 10px\",\n", |
167 |
| - " border_radius=\"5px\", display=\"block\", margin_top=\"3px\",\n", |
168 |
| - " white_space=\"pre-wrap\", color=\"#1a202c\")\n", |
169 |
| - "\n", |
| 139 | + " \"\"\"Display & return a GitHub-style HTML diff between *old* and *new*.\"\"\"\n", |
| 140 | + " diff = difflib.unified_diff(old.splitlines(), new.splitlines(),\n", |
| 141 | + " fromfile=\"old\", tofile=\"new\", lineterm=\"\")\n", |
| 142 | + " html_block = _wrap(map(_style, diff))\n", |
| 143 | + " _render(html_block)\n", |
| 144 | + " return html_block\n", |
| 145 | + "\n", |
| 146 | + "# ---------- “card” helpers ----------------------------------------------------\n", |
| 147 | + "\n", |
| 148 | + "CARD = _css(background=\"#f8f9fa\", border_radius=\"8px\", padding=\"18px 22px\",\n", |
| 149 | + " margin_bottom=\"18px\", border=\"1px solid #e0e0e0\",\n", |
| 150 | + " box_shadow=\"0 1px 4px #0001\")\n", |
| 151 | + "TITLE = _css(font_weight=\"600\", font_size=\"1.1em\", color=\"#2d3748\",\n", |
| 152 | + " margin_bottom=\"6px\")\n", |
| 153 | + "LABEL = _css(color=\"#718096\", font_size=\"0.95em\", font_weight=\"500\",\n", |
| 154 | + " margin_right=\"6px\")\n", |
| 155 | + "EXTRACT = _css(font_family=\"monospace\", background=\"#f1f5f9\", padding=\"7px 10px\",\n", |
| 156 | + " border_radius=\"5px\", display=\"block\", margin_top=\"3px\",\n", |
| 157 | + " white_space=\"pre-wrap\", color=\"#1a202c\")\n", |
170 | 158 | "\n",
|
171 | 159 | "def display_cards(\n",
|
172 | 160 | " items: Iterable[Any],\n",
|
|
175 | 163 | " field_labels: Optional[Dict[str, str]] = None,\n",
|
176 | 164 | " card_title_prefix: str = \"Item\",\n",
|
177 | 165 | ") -> None:\n",
|
178 |
| - " \"\"\"\n", |
179 |
| - " Render objects as HTML “cards” in notebooks or plaintext in console.\n", |
180 |
| - " All public attrs (minus title_attr) are shown unless `field_labels` overrides.\n", |
181 |
| - " \"\"\"\n", |
| 166 | + " \"\"\"Render objects as HTML “cards” (or plaintext when not in IPython).\"\"\"\n", |
182 | 167 | " items = list(items)\n",
|
183 | 168 | " if not items:\n",
|
184 | 169 | " _render(\"<em>No data to display.</em>\")\n",
|
185 | 170 | " return\n",
|
186 | 171 | "\n",
|
187 |
| - " # default field labels from first object\n", |
| 172 | + " # auto-derive field labels if none supplied\n", |
188 | 173 | " if field_labels is None:\n",
|
189 |
| - " sample = items[0]\n", |
190 |
| - " public_attrs = [a for a in dir(sample)\n", |
191 |
| - " if not a.startswith(\"_\") and not callable(getattr(sample, a))]\n", |
192 |
| - " public_attrs.remove(title_attr)\n", |
193 |
| - " field_labels = {a: a.replace(\"_\", \" \").title() for a in public_attrs}\n", |
| 174 | + " sample = items[0]\n", |
| 175 | + " field_labels = {\n", |
| 176 | + " a: a.replace(\"_\", \" \").title()\n", |
| 177 | + " for a in dir(sample)\n", |
| 178 | + " if not a.startswith(\"_\")\n", |
| 179 | + " and not callable(getattr(sample, a))\n", |
| 180 | + " and a != title_attr\n", |
| 181 | + " }\n", |
194 | 182 | "\n",
|
195 | 183 | " cards = []\n",
|
196 | 184 | " for idx, obj in enumerate(items, 1):\n",
|
197 |
| - " title = getattr(obj, title_attr, \"<missing title>\")\n", |
198 |
| - " rows = [ # header\n", |
199 |
| - " f'<div style=\"{TITLE}\">{card_title_prefix} {idx}: {escape(str(title))}</div>'\n", |
200 |
| - " ]\n", |
| 185 | + " title_html = html.escape(str(getattr(obj, title_attr, \"<missing title>\")))\n", |
| 186 | + " rows = [f'<div style=\"{TITLE}\">{card_title_prefix} {idx}: {title_html}</div>']\n", |
201 | 187 | "\n",
|
202 | 188 | " for attr, label in field_labels.items():\n",
|
203 |
| - " if attr == title_attr:\n", |
204 |
| - " continue\n", |
205 | 189 | " value = getattr(obj, attr, None)\n",
|
206 | 190 | " if value is None:\n",
|
207 | 191 | " continue\n",
|
208 | 192 | " rows.append(\n",
|
209 |
| - " f'<div><span style=\"{LABEL}\">{escape(label)}:</span>'\n", |
210 |
| - " f'<span style=\"{EXTRACT}\">{escape(str(value))}</span></div>'\n", |
| 193 | + " f'<div><span style=\"{LABEL}\">{html.escape(label)}:</span>'\n", |
| 194 | + " f'<span style=\"{EXTRACT}\">{html.escape(str(value))}</span></div>'\n", |
211 | 195 | " )\n",
|
212 | 196 | "\n",
|
213 |
| - " card_html = f'<div style=\"{CARD}\">' + \"\".join(rows) + \"</div>\"\n", |
214 |
| - " cards.append(card_html)\n", |
| 197 | + " cards.append(f'<div style=\"{CARD}\">{\"\".join(rows)}</div>')\n", |
215 | 198 | "\n",
|
216 | 199 | " _render(\"\\n\".join(cards))"
|
217 | 200 | ]
|
|
0 commit comments