fix(calibre): make books list columns dynamic from returned fields

- Render the books list table dynamically based on actual fields returned
  by list_books() so the displayed columns match --fields without showing
  always-empty Formats column by default.
- Add E2E test verifying list_books honors a custom fields list.

Addresses reviewer feedback on PR #223 (Formats column always empty).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
ChengYe
2026-05-20 15:54:46 +00:00
committed by yuhao
co-authored by Copilot
parent dfc361325f
commit 6229641a37
2 changed files with 27 additions and 7 deletions
@@ -264,15 +264,20 @@ def books_list(ctx, search, sort, limit, fields):
if not book_list:
click.echo(" No books found.")
else:
click.echo(f" {'ID':<5} {'Title':<40} {'Authors':<30} {'Formats'}")
click.echo(f" {'─'*5} {'─'*40} {'─'*30} {'─'*20}")
# Determine columns from actual returned fields (id is always first)
actual_fields = list(book_list[0].keys())
non_id_fields = [f for f in actual_fields if f != "id"]
# Build header dynamically
header = f" {'ID':<5} " + " ".join(f"{f.capitalize():<30}" for f in non_id_fields)
sep = f" {'─'*5} " + " ".join(f"{'─'*30}" for _ in non_id_fields)
click.echo(header)
click.echo(sep)
for b in book_list:
click.echo(
f" {b.get('id', ''):<5} "
f"{str(b.get('title', ''))[:40]:<40} "
f"{str(b.get('authors', ''))[:30]:<30} "
f"{b.get('formats', '')}"
row = f" {b.get('id', ''):<5} " + " ".join(
f"{str(b.get(f, ''))[:30]:<30}" for f in non_id_fields
)
click.echo(row)
@books.command("search")
@@ -199,6 +199,21 @@ class TestLibraryOperations(CalibreTestMixin, unittest.TestCase):
self.assertIn("title", book)
print(f"\n list_books[0]: {book}")
def test_list_books_custom_fields(self):
"""list_books with explicit fields returns exactly those fields per book."""
from cli_anything.calibre.core.library import list_books
requested = ["id", "title", "tags"]
books = list_books(self.lib_dir, fields=requested)
self.assertGreater(len(books), 0)
book = books[0]
# Requested fields must be present
for field in requested:
self.assertIn(field, book, f"Missing field: {field}")
# Non-requested fields must be absent
for field in ("authors", "series", "rating"):
self.assertNotIn(field, book, f"Unexpected field: {field}")
print(f"\n list_books(fields={requested})[0]: {book}")
def test_search_books(self):
from cli_anything.calibre.core.library import search_books
ids = search_books(self.lib_dir, "title:Foundation")