Skip to content

Commit 4bdc674

Browse files
committed
polish plugin catalog table display
1 parent 9dbe7ab commit 4bdc674

2 files changed

Lines changed: 139 additions & 9 deletions

File tree

packages/data-designer/src/data_designer/cli/controllers/plugin_catalog_controller.py

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
from data_designer.config.utils.constants import NordColor
4040

4141
NARROW_CATALOG_LAYOUT_WIDTH = 100
42+
CATALOG_TABLE_ROW_LEADING = 1
43+
CHECKMARK = "✓"
44+
X_MARK = "x"
4245

4346

4447
class PluginCatalogController:
@@ -66,14 +69,15 @@ def run_list(
6669
entries = self._list_entries_or_exit(catalog.alias, refresh=refresh, include_incompatible=include_incompatible)
6770

6871
print_header("Data Designer Plugin Packages")
69-
print_info(f"Catalog: {catalog.alias} ({catalog.url})")
7072
console.print()
7173

7274
if not entries:
7375
self._display_empty_list_state(catalog.alias, include_incompatible=include_incompatible)
76+
_print_catalog_reference(catalog)
7477
return
7578

7679
self._display_catalog_entries(entries)
80+
_print_catalog_reference(catalog)
7781

7882
def run_search(
7983
self,
@@ -93,7 +97,6 @@ def run_search(
9397
)
9498

9599
print_header("Data Designer Plugin Package Search")
96-
print_info(f"Catalog: {catalog.alias} ({catalog.url})")
97100
print_info(f"Query: {query}")
98101
console.print()
99102

@@ -103,9 +106,11 @@ def run_search(
103106
catalog.alias,
104107
include_incompatible=include_incompatible,
105108
)
109+
_print_catalog_reference(catalog)
106110
return
107111

108112
self._display_catalog_entries(entries)
113+
_print_catalog_reference(catalog)
109114

110115
def run_info(
111116
self,
@@ -530,15 +535,21 @@ def _suggest_entries(
530535
return []
531536

532537
def _display_catalog_entries(self, entries: list[PluginCatalogEntry]) -> None:
538+
installed_plugins = self.catalog_service.list_installed_plugins()
533539
if _console_width() < NARROW_CATALOG_LAYOUT_WIDTH:
534-
self._display_catalog_entries_vertical(entries)
540+
self._display_catalog_entries_vertical(entries, installed_plugins)
535541
return
536542

537-
table = Table(title="Catalog Plugin Packages", border_style=NordColor.NORD8.value)
543+
table = Table(
544+
title="Catalog Plugin Packages",
545+
border_style=NordColor.NORD8.value,
546+
leading=CATALOG_TABLE_ROW_LEADING,
547+
)
538548
table.add_column("Package", style=NordColor.NORD14.value, no_wrap=True)
539549
table.add_column("Description", style=NordColor.NORD4.value)
540550
table.add_column("Runtime Plugins", style=NordColor.NORD9.value)
541-
table.add_column("Compatible", style=NordColor.NORD13.value, no_wrap=True)
551+
table.add_column("Compatible", style=NordColor.NORD13.value, justify="center", no_wrap=True)
552+
table.add_column("Installed", style=NordColor.NORD14.value, justify="center", no_wrap=True)
542553
table.add_column("Docs", style=NordColor.NORD7.value)
543554

544555
for package_entries in self.catalog_service.group_entries_by_package(entries).values():
@@ -549,12 +560,17 @@ def _display_catalog_entries(self, entries: list[PluginCatalogEntry]) -> None:
549560
entry.package.name,
550561
entry.description,
551562
_format_runtime_plugins(package_entries),
552-
"yes" if compatibility.is_compatible else "no",
563+
_format_compatibility_marker(compatibility),
564+
_format_installed_marker(package_entries, installed_plugins),
553565
_format_docs_link(docs_url),
554566
)
555567
console.print(table)
556568

557-
def _display_catalog_entries_vertical(self, entries: list[PluginCatalogEntry]) -> None:
569+
def _display_catalog_entries_vertical(
570+
self,
571+
entries: list[PluginCatalogEntry],
572+
installed_plugins: list[InstalledPluginInfo],
573+
) -> None:
558574
for index, package_entries in enumerate(self.catalog_service.group_entries_by_package(entries).values()):
559575
entry = package_entries[0]
560576
compatibility = self.catalog_service.evaluate_compatibility(entry)
@@ -564,7 +580,8 @@ def _display_catalog_entries_vertical(self, entries: list[PluginCatalogEntry]) -
564580
console.print(Text(entry.package.name, style=f"bold {NordColor.NORD14.value}"))
565581
console.print(f" Description: {entry.description}")
566582
console.print(f" Runtime plugins: {_format_runtime_plugins(package_entries)}")
567-
console.print(f" Compatible: {'yes' if compatibility.is_compatible else 'no'}")
583+
console.print(f" Compatible: {_format_compatibility_marker(compatibility)}")
584+
console.print(f" Installed: {_format_installed_marker(package_entries, installed_plugins)}")
568585
if docs_url:
569586
console.print(f" Docs: {docs_url}")
570587

@@ -606,6 +623,16 @@ def _display_commands(commands: list[list[str]]) -> None:
606623
console.print(f" [bold]{shlex.join(command)}[/bold]")
607624

608625

626+
def _print_catalog_reference(catalog: PluginCatalogConfig) -> None:
627+
console.print()
628+
catalog_link = Text.assemble(
629+
" 🗂️ Catalog: ",
630+
(catalog.alias, Style(color=NordColor.NORD14.value, bold=True, link=catalog.url)),
631+
)
632+
console.print(catalog_link)
633+
console.print()
634+
635+
609636
def _target_description(mode: str, project_root: str | None) -> str:
610637
if mode == "uv-project" and project_root is not None:
611638
return f"current uv project ({project_root})"
@@ -616,6 +643,31 @@ def _format_runtime_plugins(entries: list[PluginCatalogEntry]) -> str:
616643
return ", ".join(f"{entry.name} ({entry.plugin_type.value})" for entry in entries)
617644

618645

646+
def _format_checkmark(value: bool) -> str:
647+
return CHECKMARK if value else ""
648+
649+
650+
def _format_compatibility_marker(compatibility: CompatibilityResult) -> str:
651+
return CHECKMARK if compatibility.is_compatible else X_MARK
652+
653+
654+
def _format_installed_marker(
655+
package_entries: list[PluginCatalogEntry],
656+
installed_plugins: list[InstalledPluginInfo],
657+
) -> str:
658+
return _format_checkmark(_package_entries_are_installed(package_entries, installed_plugins))
659+
660+
661+
def _package_entries_are_installed(
662+
package_entries: list[PluginCatalogEntry],
663+
installed_plugins: list[InstalledPluginInfo],
664+
) -> bool:
665+
installed_entry_points = {(plugin.name, plugin.entry_point_value) for plugin in installed_plugins}
666+
return bool(package_entries) and all(
667+
(entry.entry_point.name, entry.entry_point.value) in installed_entry_points for entry in package_entries
668+
)
669+
670+
619671
def _package_alias(package_name: str) -> str | None:
620672
canonical_package_name = canonicalize_name(package_name)
621673
if not canonical_package_name.startswith(DATA_DESIGNER_PLUGIN_PACKAGE_PREFIX):

packages/data-designer/tests/cli/controllers/test_plugin_catalog_controller.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def controller(tmp_path: Path) -> PluginCatalogController:
3131
plugin_controller.catalog_service = MagicMock()
3232
plugin_controller.catalog_service.get_runtime_plugin_entries.return_value = []
3333
plugin_controller.catalog_service.suggest_entries.return_value = []
34+
plugin_controller.catalog_service.list_installed_plugins.return_value = []
3435
plugin_controller.install_service = MagicMock()
3536
return plugin_controller
3637

@@ -129,6 +130,10 @@ def test_run_list_renders_package_first_catalog_table(
129130
"data-designer-text-transform": package_entries,
130131
}
131132
controller.catalog_service.evaluate_compatibility.return_value = CompatibilityResult(True, [])
133+
controller.catalog_service.list_installed_plugins.return_value = [
134+
InstalledPluginInfo(name="text-column", entry_point_value="data_designer_text_transform.plugin:plugin"),
135+
InstalledPluginInfo(name="text-processor", entry_point_value="data_designer_text_transform.plugin:plugin"),
136+
]
132137

133138
controller.run_list(catalog_alias="local", include_incompatible=True)
134139

@@ -137,19 +142,33 @@ def test_run_list_renders_package_first_catalog_table(
137142
]
138143
assert printed_tables
139144
assert printed_tables[0].title == "Catalog Plugin Packages"
145+
assert printed_tables[0].leading == 1
140146
assert [column.header for column in printed_tables[0].columns] == [
141147
"Package",
142148
"Description",
143149
"Runtime Plugins",
144150
"Compatible",
151+
"Installed",
145152
"Docs",
146153
]
147154
assert list(printed_tables[0].columns[1].cells) == ["Transform text records"]
148-
docs_cell = list(printed_tables[0].columns[4].cells)[0]
155+
assert list(printed_tables[0].columns[3].cells) == ["✓"]
156+
assert list(printed_tables[0].columns[4].cells) == ["✓"]
157+
docs_cell = list(printed_tables[0].columns[5].cells)[0]
149158
assert isinstance(docs_cell, Text)
150159
assert docs_cell.plain == "docs"
151160
assert docs_cell.style is not None
152161
assert docs_cell.style.link == "https://docs.example.test/plugins/data-designer-text-transform/"
162+
catalog_footer = mock_console.print.call_args_list[-2].args[0]
163+
assert isinstance(catalog_footer, Text)
164+
assert catalog_footer.plain == " 🗂️ Catalog: local"
165+
catalog_footer_spans = catalog_footer.spans
166+
assert catalog_footer_spans
167+
assert catalog_footer_spans[0].style is not None
168+
assert (
169+
catalog_footer_spans[0].style.link
170+
== "https://raw.githubusercontent.com/acme/dd-plugins/main/catalog/plugins.json"
171+
)
153172

154173
rendered_output = StringIO()
155174
narrow_console = Console(
@@ -164,6 +183,59 @@ def test_run_list_renders_package_first_catalog_table(
164183
controller.catalog_service.group_entries_by_package.assert_called_once_with(package_entries)
165184

166185

186+
@patch("data_designer.cli.controllers.plugin_catalog_controller.console")
187+
def test_run_list_leaves_installed_column_empty_when_runtime_entry_points_are_missing(
188+
mock_console: MagicMock,
189+
controller: PluginCatalogController,
190+
) -> None:
191+
package_entries = [
192+
_entry(name="text-column", plugin_type="column-generator"),
193+
_entry(name="text-processor", plugin_type="processor"),
194+
]
195+
catalog = _catalog()
196+
controller.catalog_service.get_catalog.return_value = catalog
197+
controller.catalog_service.list_entries.return_value = package_entries
198+
controller.catalog_service.group_entries_by_package.return_value = {
199+
"data-designer-text-transform": package_entries,
200+
}
201+
controller.catalog_service.evaluate_compatibility.return_value = CompatibilityResult(True, [])
202+
controller.catalog_service.list_installed_plugins.return_value = [
203+
InstalledPluginInfo(name="text-column", entry_point_value="data_designer_text_transform.plugin:plugin"),
204+
]
205+
206+
controller.run_list(catalog_alias="local", include_incompatible=True)
207+
208+
printed_tables = [
209+
call.args[0] for call in mock_console.print.call_args_list if call.args and isinstance(call.args[0], Table)
210+
]
211+
assert list(printed_tables[0].columns[4].cells) == [""]
212+
213+
214+
@patch("data_designer.cli.controllers.plugin_catalog_controller.console")
215+
def test_run_list_marks_incompatible_packages_with_x(
216+
mock_console: MagicMock,
217+
controller: PluginCatalogController,
218+
) -> None:
219+
entry = _entry()
220+
catalog = _catalog()
221+
controller.catalog_service.get_catalog.return_value = catalog
222+
controller.catalog_service.list_entries.return_value = [entry]
223+
controller.catalog_service.group_entries_by_package.return_value = {
224+
"data-designer-text-transform": [entry],
225+
}
226+
controller.catalog_service.evaluate_compatibility.return_value = CompatibilityResult(
227+
False,
228+
["Data Designer 0.5.7 does not satisfy >=99.0"],
229+
)
230+
231+
controller.run_list(catalog_alias="local", include_incompatible=True)
232+
233+
printed_tables = [
234+
call.args[0] for call in mock_console.print.call_args_list if call.args and isinstance(call.args[0], Table)
235+
]
236+
assert list(printed_tables[0].columns[3].cells) == ["x"]
237+
238+
167239
@patch("data_designer.cli.controllers.plugin_catalog_controller.console")
168240
def test_run_list_uses_vertical_layout_in_narrow_terminals(
169241
mock_console: MagicMock,
@@ -181,13 +253,19 @@ def test_run_list_uses_vertical_layout_in_narrow_terminals(
181253
"data-designer-retrieval-sdg": package_entries,
182254
}
183255
controller.catalog_service.evaluate_compatibility.return_value = CompatibilityResult(True, [])
256+
controller.catalog_service.list_installed_plugins.return_value = [
257+
InstalledPluginInfo(name="document-chunker", entry_point_value="data_designer_text_transform.plugin:plugin"),
258+
InstalledPluginInfo(name="embedding-dedup", entry_point_value="data_designer_text_transform.plugin:plugin"),
259+
]
184260

185261
controller.run_list(catalog_alias="local")
186262

187263
printed_tables = [
188264
call.args[0] for call in mock_console.print.call_args_list if call.args and isinstance(call.args[0], Table)
189265
]
190266
assert printed_tables == []
267+
mock_console.print.assert_any_call(" Compatible: ✓")
268+
mock_console.print.assert_any_call(" Installed: ✓")
191269
mock_console.print.assert_any_call(
192270
" Runtime plugins: document-chunker (seed-reader), embedding-dedup (column-generator)"
193271
)

0 commit comments

Comments
 (0)