From 1ba3be92b3da3ba72bdb556474273d655e23442a Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 3 Sep 2026 13:04:47 -0400 Subject: [PATCH 1/4] Add multi-column mode to html_table() renderer --- great_docs/_apiref/_render/html_table.py | 70 +++++++++++++++++++----- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/great_docs/_apiref/_render/html_table.py b/great_docs/_apiref/_render/html_table.py index c3c952d0..7fb2ddc4 100644 --- a/great_docs/_apiref/_render/html_table.py +++ b/great_docs/_apiref/_render/html_table.py @@ -37,31 +37,74 @@ def replace_link(match: re.Match[str]) -> str: def html_table( - rows: Sequence[tuple[Stringable, Stringable | None]], + rows: Sequence[tuple[Stringable, Stringable | None]] | Sequence[Sequence[str]], *, + headers: Sequence[str] | None = None, + col_widths: Sequence[int] | None = None, table_class: str = "gd-summary-table", ) -> str: """ - Render rows as an HTML table + Render rows as an HTML table. - Styling is handled by the .gd-summary-table class in great-docs.scss, - which overrides Bootstrap defaults for a cleaner appearance. + When `headers=` is provided the table switches to **multi-column mode**: the thead row is + visible, optional per-column widths are applied via inline `style="width: X%"` on each `` + (the CSS class must set `table-layout: fixed`), and Bootstrap's base `caption-top table` classes + are added alongside `table_class=` so the output matches what Quarto emits for Markdown tables. + + When `headers=` is `None` the function operates in legacy two-column mode: rows are + `(name, description)` tuples, the `name` cell undergoes Markdown-link-to-HTML conversion, and + styling is handled entirely by `table_class=` (default `gd-summary-table`). Parameters ---------- rows - Sequence of (name, description) tuples. Name can contain markdown links. + Multi-column mode: sequence of rows, each a sequence of HTML-ready cell strings (caller is + responsible for `html.escape` on text values). Legacy mode: sequence of + `(name, description)` tuples where `name` may contain Pandoc-style Markdown links. + headers + Column header labels (plain text). Providing this activates multi-column mode. + col_widths + Integer percentage widths for each column (multi-column mode only). When given, each `` + gets an inline `style="width: X%"`. The CSS class must set `table-layout: fixed` for the + widths to take effect. table_class - CSS class to apply to the table for styling. + Extra CSS class applied to the `` element. Returns ------- str HTML string with table markup. """ - # Build table rows - body_rows: list[str] = [] - for name, desc in rows: + if headers is not None: + # ── Multi-column mode ────────────────────────────────────────── + # Mirror the classes Quarto emits for markdown tables so Bootstrap + # base styles apply, then tack on the caller's custom class. + full_class = f"caption-top table {table_class}" + + if col_widths is not None: + header_cells = "".join( + f'' + for h, w in zip(headers, col_widths) + ) + else: + header_cells = "".join(f"" for h in headers) + + body_rows = [ + " " + "".join(f"" for cell in row) + "" + for row in rows # type: ignore[union-attr] + ] + + return ( + f'
{html.escape(h)}{html.escape(h)}
{cell}
\n' + f'\n {header_cells}\n\n' + f"\n" + "\n".join(body_rows) + "\n\n
" + ) + + # ── Legacy two-column mode ───────────────────────────────────────── + # Styling is handled by the .gd-summary-table class in great-docs.scss, + # which overrides Bootstrap defaults for a cleaner appearance. + body_rows_2col: list[str] = [] + for name, desc in rows: # type: ignore[misc] name, desc = str(name), str(desc) if desc is not None else "" # Convert markdown links to HTML name = _md_link_to_html(name) @@ -71,12 +114,11 @@ def html_table( desc = " ".join(line.strip() for line in desc.split("\n") if line.strip()) else: desc = "" - body_rows.append(f" \n {name}\n {desc}\n ") + body_rows_2col.append(f" \n {name}\n {desc}\n ") - table_body = "\n".join(body_rows) + table_body = "\n".join(body_rows_2col) - # Assemble full HTML (no inline styles needed - SCSS handles styling) - html_out = f""" + return f"""
@@ -87,5 +129,3 @@ def html_table( {table_body}
Name
""" - - return html_out From 8ba85759426506c0b903aa00265b0b987b68548c Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 3 Sep 2026 13:04:56 -0400 Subject: [PATCH 2/4] Fix dependency table column sizing --- great_docs/assets/great-docs.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/great_docs/assets/great-docs.scss b/great_docs/assets/great-docs.scss index 3a47f525..ff0480e7 100644 --- a/great_docs/assets/great-docs.scss +++ b/great_docs/assets/great-docs.scss @@ -206,6 +206,12 @@ body.quarto-dark table.gd-summary-table td:last-child { color: var(--bs-body-color); } +// Dependency tables on the Package Info page (fixed column widths via ) +table.gd-dep-table { + table-layout: fixed; + width: 100%; +} + .table > tbody { // quarto adds a bottom color that is different from the border color, // that looks good the default From 1f8225d2e2a1daf010a3726abf73b47b481e4b7f Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 3 Sep 2026 13:05:15 -0400 Subject: [PATCH 3/4] Render dependency tables with HTML helper --- great_docs/core.py | 106 ++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/great_docs/core.py b/great_docs/core.py index 72c4616b..a088efc2 100644 --- a/great_docs/core.py +++ b/great_docs/core.py @@ -11160,6 +11160,9 @@ def _generate_package_info_page(self) -> str | None: if not self._config.is_python_project: return None + import html as _html + + from ._apiref._render.html_table import html_table from ._icons import get_icon_svg from ._translations import get_translation @@ -11192,36 +11195,49 @@ def _generate_package_info_page(self) -> str | None: all_dep_names |= {d["name"] for d in deps} pypi_dates = self._fetch_pypi_dates(all_dep_names) - sections: list[str] = [] + # Column widths (%) + _COL_WIDTHS_4 = (35, 30, 25, 10) # Package / Version / Last Published / PyPI + _COL_WIDTHS_5 = (28, 24, 20, 20, 8) # … + Environment Marker - # ── Runtime Dependencies ────────────────────────────────────── - if runtime_deps: - has_markers = any(d.get("marker") for d in runtime_deps) - header_cols = f"| {_pkg_col} | {_ver_col} | Last Published | PyPI |" - separator = "|----|----|----|----|" - if has_markers: - header_cols = f"| {_pkg_col} | {_ver_col} | {_marker_col} | Last Published | PyPI |" - separator = "|----|----|----|----|----|" - - rows: list[str] = [] - for dep in runtime_deps: + def _build_dep_rows(deps: list[dict], has_markers: bool) -> list[list[str]]: + dep_rows: list[list[str]] = [] + for dep in deps: name = dep["name"] - spec = f"`{dep['specifier']}`" if dep["specifier"] else "—" - date_str = pypi_dates.get(name, "—") - pypi_cell = f"[{_link_svg}](https://pypi.org/project/{name}/){{.gd-pypi-link}}" + spec = f"{_html.escape(dep['specifier'])}" if dep["specifier"] else "—" + date_str = _html.escape(pypi_dates.get(name, "—")) + pypi_cell = ( + f'{_link_svg}' + ) + pkg_cell = f'{_html.escape(name)}' if has_markers: - marker = f"`{dep['marker']}`" if dep.get("marker") else "" - rows.append( - f"| `{name}`{{.gd-no-link}} | {spec} | {marker}" - f" | {date_str} | {pypi_cell} |" + marker = ( + f"{_html.escape(dep['marker'])}" if dep.get("marker") else "" ) + dep_rows.append([pkg_cell, spec, marker, date_str, pypi_cell]) else: - rows.append(f"| `{name}`{{.gd-no-link}} | {spec} | {date_str} | {pypi_cell} |") + dep_rows.append([pkg_cell, spec, date_str, pypi_cell]) + return dep_rows + sections: list[str] = [] + + # ── Runtime Dependencies ────────────────────────────────────── + if runtime_deps: + has_markers = any(d.get("marker") for d in runtime_deps) + headers = ( + [_pkg_col, _ver_col, _marker_col, "Last Published", "PyPI"] + if has_markers + else [_pkg_col, _ver_col, "Last Published", "PyPI"] + ) + col_widths = _COL_WIDTHS_5 if has_markers else _COL_WIDTHS_4 + table_html = html_table( + _build_dep_rows(runtime_deps, has_markers), + headers=headers, + col_widths=col_widths, + table_class="gd-dep-table", + ) sections.append(f"## {_runtime}\n") - sections.append(header_cols) - sections.append(separator) - sections.extend(rows) + sections.append(table_html) sections.append("") # ── Optional Dependencies ───────────────────────────────────── @@ -11233,34 +11249,19 @@ def _generate_package_info_page(self) -> str | None: if group_deps: has_markers = any(d.get("marker") for d in group_deps) - header_cols = f"| {_pkg_col} | {_ver_col} | Last Published | PyPI |" - separator = "|----|----|----|----|" - if has_markers: - header_cols = ( - f"| {_pkg_col} | {_ver_col} | {_marker_col} | Last Published | PyPI |" - ) - separator = "|----|----|----|----|----|" - - sections.append(header_cols) - sections.append(separator) - - for dep in group_deps: - name = dep["name"] - spec = f"`{dep['specifier']}`" if dep["specifier"] else "—" - date_str = pypi_dates.get(name, "—") - pypi_cell = ( - f"[{_link_svg}](https://pypi.org/project/{name}/){{.gd-pypi-link}}" - ) - if has_markers: - marker = f"`{dep['marker']}`" if dep.get("marker") else "" - sections.append( - f"| `{name}`{{.gd-no-link}} | {spec} | {marker}" - f" | {date_str} | {pypi_cell} |" - ) - else: - sections.append( - f"| `{name}`{{.gd-no-link}} | {spec} | {date_str} | {pypi_cell} |" - ) + headers = ( + [_pkg_col, _ver_col, _marker_col, "Last Published", "PyPI"] + if has_markers + else [_pkg_col, _ver_col, "Last Published", "PyPI"] + ) + col_widths = _COL_WIDTHS_5 if has_markers else _COL_WIDTHS_4 + table_html = html_table( + _build_dep_rows(group_deps, has_markers), + headers=headers, + col_widths=col_widths, + table_class="gd-dep-table", + ) + sections.append(table_html) sections.append("") else: sections.append("*No dependencies declared.*\n") @@ -16440,8 +16441,7 @@ class Result: d2_modified = _render_d2(self.project_path, cache_dir=d2_cache) if d2_modified: log.detail( - f"Rendered d2 diagrams in {len(d2_modified)} page(s): " - + ", ".join(d2_modified) + f"Rendered d2 diagrams in {len(d2_modified)} page(s): " + ", ".join(d2_modified) ) # Get environment with QUARTO_PYTHON set From cdb0b612cc639af03da8454e534341c983192842 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Thu, 3 Sep 2026 14:10:59 -0400 Subject: [PATCH 4/4] Update package info page test assertions --- tests/test_great_docs.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_great_docs.py b/tests/test_great_docs.py index eaba06a3..8ca2a3d6 100644 --- a/tests/test_great_docs.py +++ b/tests/test_great_docs.py @@ -42320,9 +42320,9 @@ def test_generate_package_info_page_basic(): content = pkg_info.read_text(encoding="utf-8") assert "Package Info" in content - assert "`numpy`{.gd-no-link}" in content - assert "`>=1.24`" in content - assert "`pandas`{.gd-no-link}" in content + assert 'class="gd-no-link">numpy' in content + assert ">=1.24" in content + assert 'class="gd-no-link">pandas' in content assert "pypi.org/project/numpy" in content assert "pypi.org/project/pandas" in content assert "Runtime Dependencies" in content @@ -42366,9 +42366,9 @@ def test_generate_package_info_page_with_optional_deps(): assert "Optional Dependencies" in content assert "### `dev`" in content assert "### `docs`" in content - assert "`pytest`{.gd-no-link}" in content or "`pytest`" in content - assert "`coverage`{.gd-no-link}" in content or "`coverage`" in content - assert "`quartodoc`{.gd-no-link}" in content or "`quartodoc`" in content + assert "pytest" in content + assert "coverage" in content + assert "quartodoc" in content # Summary counts assert "3 groups" in content or "2 groups" in content @@ -42397,7 +42397,7 @@ def test_generate_package_info_page_with_markers(): content = (gd_dir / "package-info.qmd").read_text(encoding="utf-8") assert "Environment Marker" in content assert "python_version" in content - assert "`typing-extensions`" in content or "`typing_extensions`" in content + assert "typing-extensions" in content or "typing_extensions" in content def test_generate_package_info_page_disabled():