From fe161d22f9bca9033200bd3fe5bf3bd15221aba6 Mon Sep 17 00:00:00 2001 From: devteamaegis Date: Sun, 2 Aug 2026 16:30:12 -0400 Subject: [PATCH] fix(table): don't crash on --query lists mixing scalars and objects _build_table decided how to treat a list by looking at current[0] only. When element 0 wasn't a dict, every element fell through to the row branch, where a dict satisfies all(self._scalar_type(el) for el in item) because that iterates the dict's keys. It was then added as a row of keys - dropping the values outright when the column counts happened to match, and raising ValueError when they didn't. A --query multi-select list such as '[InstanceId, State]' returns a scalar next to an object, so --output table exited 255. Route on whether every element is a dict, and give a dict in a mixed list a section of its own. --- .../bugfix-table-output-mixed-list.json | 5 +++ awscli/formatter.py | 21 ++++++++- tests/unit/output/test_table_formatter.py | 43 +++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 .changes/next-release/bugfix-table-output-mixed-list.json diff --git a/.changes/next-release/bugfix-table-output-mixed-list.json b/.changes/next-release/bugfix-table-output-mixed-list.json new file mode 100644 index 000000000000..bda656576dbc --- /dev/null +++ b/.changes/next-release/bugfix-table-output-mixed-list.json @@ -0,0 +1,5 @@ +{ + "category": "``table`` output", + "description": "Fix ``ValueError`` when ``--output table`` renders a list that contains both a scalar and an object, which a ``--query`` multi-select list such as ``[InstanceId, State]`` produces", + "type": "bugfix" +} diff --git a/awscli/formatter.py b/awscli/formatter.py index e13b6fd72c62..c7ef18653a30 100644 --- a/awscli/formatter.py +++ b/awscli/formatter.py @@ -137,10 +137,27 @@ def _build_table(self, title, current, indent_level=0): if title is not None: self.table.new_section(title, indent_level=indent_level) if isinstance(current, list): - if isinstance(current[0], dict): + if all(isinstance(el, dict) for el in current): self._build_sub_table_from_list(current, indent_level, title) else: - for item in current: + # The elements don't all share a shape, so a dict can't be + # rendered as a row alongside them. Give it a section of + # its own, and start a new one for whatever follows it. + needs_new_section = False + for i, item in enumerate(current): + if isinstance(item, dict): + if i > 0: + self.table.new_section( + title, indent_level=indent_level + ) + self._build_sub_table_from_dict(item, indent_level) + needs_new_section = True + continue + if needs_new_section: + self.table.new_section( + title, indent_level=indent_level + ) + needs_new_section = False if self._scalar_type(item): self.table.add_row([item]) elif all(self._scalar_type(el) for el in item): diff --git a/tests/unit/output/test_table_formatter.py b/tests/unit/output/test_table_formatter.py index 37c1d193c45e..883dbe7cd2b8 100644 --- a/tests/unit/output/test_table_formatter.py +++ b/tests/unit/output/test_table_formatter.py @@ -362,6 +362,41 @@ """ +# A --query multi-select list such as '[InstanceId, State]' returns a list +# holding both a scalar and an object. +MIXED_SCALAR_FIRST = ["i-123", {"Code": 16, "Name": "running"}] + +MIXED_SCALAR_FIRST_TABLE = """\ +--------------------- +| OperationName | ++-------------------+ +| i-123 | ++-------------------+ +| OperationName | ++-------+-----------+ +| Code | Name | ++-------+-----------+ +| 16 | running | ++-------+-----------+ +""" + +MIXED_DICT_FIRST = [{"Code": 16, "Name": "running"}, "i-123"] + +MIXED_DICT_FIRST_TABLE = """\ +--------------------- +| OperationName | ++-------+-----------+ +| Code | Name | ++-------+-----------+ +| 16 | running | ++-------+-----------+ +| OperationName | ++-------------------+ +| i-123 | ++-------------------+ +""" + + class Object(object): def __init__(self, **kwargs): self.__dict__.update(kwargs) @@ -425,3 +460,11 @@ def test_jmespath_filtered_response(self): def test_jmespath_filtered_dict_response(self): self.assert_data_renders_to(data=JMESPATH_FILTERED_RESPONSE_DICT, table=JMESPATH_FILTERED_RESPONSE_DICT_TABLE) + + def test_mixed_list_with_leading_scalar(self): + self.assert_data_renders_to(data=MIXED_SCALAR_FIRST, + table=MIXED_SCALAR_FIRST_TABLE) + + def test_mixed_list_with_leading_dict(self): + self.assert_data_renders_to(data=MIXED_DICT_FIRST, + table=MIXED_DICT_FIRST_TABLE)