Skip to content

feat(emitter):add structured data output with JSON and table formatting#380

Open
dharapandya85 wants to merge 6 commits intocanonical:mainfrom
dharapandya85:feat/emitter-data-output
Open

feat(emitter):add structured data output with JSON and table formatting#380
dharapandya85 wants to merge 6 commits intocanonical:mainfrom
dharapandya85:feat/emitter-data-output

Conversation

@dharapandya85
Copy link

@dharapandya85 dharapandya85 commented Aug 25, 2025

Fixes : #348

  • Have you followed the guidelines for contributing?
  • Have you signed the CLA?
  • Have you successfully run make lint && make test?

This PR extends the Emitter class to support structured data output in multiple formats(JSON,table), with a registry for adding formats.
Implementation Details

  • A method on the emitter to write structured data to stdout
  • Output methods: TableFormatter for human-readable tables,JSONFormatterfor machine-readable JSON
  • Implemented usage in examples.py
  • Unit tests covering all formatters and edge cases
  • Added Emitter.data() method which accepts structured data(list[dict]) and uses registered formatter based on the --format flag.
  • Implemented JSONFormatter to fetch JSON outputs with json.dumps
  • Implemented TableFormatter for printing rows and columns
  • Emitter.register_formate(name,formatter) allows registering new formats
  • Default registry includes JSON and table
  • Updated CLI (dispatcher.py) to add -- format{json,table}
  • Added example_36 in examples.py to demo usage.
  • Added unit tests in test_messages_emitter.py
  • TableFomattter, handles list of dicts, empty list and dict input
  • JSONFormatter serializes list of data

Example Run

python examples.py 36 table
>>> Running example_33 with args: ('table',)
TABLE output:
name  | status   | version
--------------------------
App A | active   | 1.0.1
App B | inactive | 2.3.0
python examples.py 36 json
>>> Running example_33 with args: ('json',)
JSON output:
[
  {
    "name": "App A",
    "version": "1.0.1",
    "status": "active"
  },
  {
    "name": "App B",
    "version": "2.3.0",
    "status": "inactive"
  }
]

@dharapandya85 dharapandya85 requested a review from tigarmo as a code owner August 25, 2025 04:24
Copy link
Contributor

@lengau lengau left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, please fix the broken tests and lint errors here.

@dharapandya85 dharapandya85 requested review from a team and medubelko as code owners October 1, 2025 11:49
@dharapandya85 dharapandya85 force-pushed the feat/emitter-data-output branch from f33ec01 to 7a4f4b5 Compare October 23, 2025 13:45
@dharapandya85 dharapandya85 force-pushed the feat/emitter-data-output branch from ae4958a to 00bd742 Compare November 27, 2025 09:05
@medubelko medubelko dismissed their stale review November 27, 2025 23:20

Problem solved, and no doc changes that require my review.

@dharapandya85 dharapandya85 force-pushed the feat/emitter-data-output branch 3 times, most recently from e64a8dc to 2afdf38 Compare December 4, 2025 18:40
@lengau lengau requested a review from Copilot December 5, 2025 20:46
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds structured data output capabilities to the Emitter class, supporting JSON and table formatting for machine and human-readable output. The implementation includes a formatter registry pattern, CLI integration, and comprehensive test coverage.

Key changes:

  • Added BaseFormatter, JSONFormatter, and TableFormatter classes for flexible output formatting
  • Implemented Emitter.data(), Emitter.table(), and Emitter.json() methods for structured data output
  • Added --format CLI argument with json/table choices (filtered from help output)

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
craft_cli/messages.py Core formatter classes and Emitter methods for structured data output
craft_cli/dispatcher.py Added --format CLI argument and filtered it from command help
craft_cli/helptexts.py Fixed help text generation when options list is empty
craft_cli/printer.py Added raw_output parameter support to show() method
examples.py Added example_36 demonstrating structured output formats
tests/unit/test_messages_emitter.py Added unit tests for formatters and updated double init test
tests/unit/test_help.py Updated tests to reflect filtered global options from help
tests/integration/test_messages_integration.py Adjusted timing thresholds for test stability
craft_cli/init.py Exposed Emitter and complete in public API
craft_cli/pytest_plugin.py Added type ignore comments for pytest imports
craft_cli/completion/completion.py Added explicit type annotation for template render
docs/conf.py Added type ignore comment for sphinx import
pyproject.toml File deleted (full file removal)
Comments suppressed due to low confidence (1)

pyproject.toml:1

  • The entire pyproject.toml file has been deleted. This removes critical project metadata, dependencies, and build configuration. This change should be reverted unless there's a deliberate migration to a different build system.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Example:
-------
>>> formatter = JsonFormatter()
>>> formatter.format([{"name":"Alice","age":30})
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example code is missing a closing bracket ] and has unbalanced parentheses. Should be formatter.format([{\"name\":\"Alice\",\"age\":30}]).

Suggested change
>>> formatter.format([{"name":"Alice","age":30})
>>> formatter.format([{"name":"Alice","age":30}])

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot is correct here.

messages, "_get_log_filepath", lambda appname: tmp_path / FAKE_LOG_NAME
)

monkeypatch.setattr(messages, "_get_log_filepath", None)
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting _get_log_filepath to None will cause an error when it's called as a function. This should be a lambda or mock function that returns a path.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, this unit test is unrelated to the PR, so these changes should be reverted.

@mr-cal
Copy link
Contributor

mr-cal commented Jan 21, 2026

Hi @dharapandya85,

Are you still actively working on this? I haven't seen any activity or responses to the open comments since 2025-Dec-05.

@dharapandya85
Copy link
Author

Hi @dharapandya85,

Are you still actively working on this? I haven't seen any activity or responses to the open comments since 2025-Dec-05.

@mr-cal, working on this project.

@dharapandya85 dharapandya85 force-pushed the feat/emitter-data-output branch 4 times, most recently from 1bcd53d to 19bdba0 Compare January 23, 2026 17:59
@mr-cal mr-cal requested a review from lengau January 23, 2026 18:14
@dharapandya85 dharapandya85 force-pushed the feat/emitter-data-output branch 2 times, most recently from 7d7ccf1 to ec07206 Compare February 16, 2026 10:44
Comment on lines +250 to +255
result: str = template.render(
shell_cmd=shell_cmd,
commands=command_map,
global_opts=global_opts,
)
return result
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be reverted, the type checkers are able to deduce the type.

from typing import TYPE_CHECKING, Any, Literal, TextIO, TypeVar, cast

import platformdirs
import platformdirs # type: ignore [import-not-found]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert this, the import should be found.

Example:
-------
>>> formatter = JsonFormatter()
>>> formatter.format([{"name":"Alice","age":30})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot is correct here.

"""Output structured data to the terminal in a specific format.

:param data: The structured data to output( list of dicts or a single dict).
:param format: The format( defaults to 'table')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:param format: The format( defaults to 'table')
:param format: The format (defaults to 'table')

"""

def format(self, data: TabularData, headers: dict[str, str] | None = None) -> str:
_ = headers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see this getting used.

assert out.strip() == expected.strip()


def test_table_formatter_empty_list():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you test?

  • empty list
  • empty dict

Both as json and a table?



def test_table_formatter_simple_dict_list():
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you exercise printing the following data types as json and as a table?

  • str
  • int
  • float
  • bool
  • None

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you test a single column table?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert the changes here.

rows = [[str(row.get(h, "")) for h in table_headers] for row in data]
else:
table_headers = ["Key", "Value"]
rows = [[str(k), str(v)] for k, v in data.items()]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one reason I want to change the TabularData type in https://github.com/canonical/craft-cli/pull/380/changes#r2921516309.

@dharapandya85 dharapandya85 force-pushed the feat/emitter-data-output branch from ec07206 to 06fb779 Compare March 25, 2026 18:29
@dharapandya85
Copy link
Author

Thank you @mr-cal for detailed review. Please check now for further improvements. Some tests and linting checks are failing for now.

In messages.py, I used Mapping, to avoid strict dict types, if string, and other value types, are passed.

ValueType = str | int | float | bool | None
Row = Mapping[str, ValueType]
TabularData = Sequence[Row] | Row

and here, it is failing

if isinstance(data, Mapping):
            rows_data: Sequence[Row] = [data]
                     -------------   ^^^^^^ Incompatible value of type `list[(Sequence[Mapping[str, str | int | float | None]] & Top[Mapping[Unknown, object]]) | Mapping[str, str | int | float | None]]`

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Output formats for complex types

6 participants