From 0670931a60e1e93bf9506d2808754bcf09aaf308 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Fri, 5 Jul 2024 14:55:53 +0100 Subject: [PATCH 01/10] Extend formatting options to listen subcommand Listen streams a mix of WorkerEvent, ProgressEvent and DataEvent instances for anything that the server is doing. This extends the type handling in the format to each of these types to display the relevant level of information for each. Informational messages are changed to be printed to stderr so that the main stdout output can be piped to an external application (eg jq for json mode). --- src/blueapi/cli/cli.py | 12 ++++++++---- src/blueapi/cli/format.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/blueapi/cli/cli.py b/src/blueapi/cli/cli.py index 6455a9814..9a86b9a92 100644 --- a/src/blueapi/cli/cli.py +++ b/src/blueapi/cli/cli.py @@ -1,5 +1,6 @@ import json import logging +import sys from functools import wraps from pathlib import Path from pprint import pprint @@ -158,20 +159,23 @@ def listen_to_events(obj: dict) -> None: else: raise RuntimeError("Message bus needs to be configured") + fmt = obj["fmt"] + def on_event( event: WorkerEvent | ProgressEvent | DataEvent, context: MessageContext, ) -> None: - converted = json.dumps(event.model_dump(), indent=2) - print(converted) + fmt.display(event) print( "Subscribing to all bluesky events from " - f"{config.stomp.host}:{config.stomp.port}" + f"{config.stomp.host}:{config.stomp.port}", + file=sys.stderr, ) with event_bus_client: event_bus_client.subscribe_to_all_events(on_event) - input("Press enter to exit") + print("Press enter to exit", file=sys.stderr) + input() @controller.command(name="run") diff --git a/src/blueapi/cli/format.py b/src/blueapi/cli/format.py index 3baaafa99..a6d07fa13 100644 --- a/src/blueapi/cli/format.py +++ b/src/blueapi/cli/format.py @@ -10,13 +10,24 @@ from pydantic import BaseModel +from blueapi.core.bluesky_types import DataEvent from blueapi.service.model import DeviceResponse, PlanResponse +from blueapi.worker.event import ProgressEvent, WorkerEvent FALLBACK = pprint +NL = "\n" Stream = TextIO | None +def fmt_dict(t: dict[str, Any] | Any, ind: int = 1) -> str: + """Format a (possibly nested) dict into a human readable tree""" + if not isinstance(t, dict): + return t + pre = " " * (ind * 4) + return NL + NL.join(f"{pre}{k}: {fmt_dict(v, ind+1)}" for k, v in t.items() if v) + + class OutputFormat(str, enum.Enum): JSON = "json" FULL = "full" @@ -49,6 +60,15 @@ def display_full(obj: Any, stream: Stream): print(dev.name) for proto in dev.protocols: print(" " + proto) + case DataEvent(name=name, doc=doc): + print(f"{name.title()}: {fmt_dict(doc)}") + case WorkerEvent(state=state, task_status=task): + print(f"WorkerEvent: {state}{fmt_dict(task.model_dump() if task else {})}") + case ProgressEvent(): + print(f"Progress:{fmt_dict(obj.model_dump())}") + case BaseModel(): + print(obj.__class__.__name__, end='') + print(fmt_dict(obj.model_dump())) case other: FALLBACK(other, stream=stream) @@ -61,7 +81,7 @@ def display_json(obj: Any, stream: Stream): case DeviceResponse(devices=devices): print(json.dumps([d.model_dump() for d in devices], indent=2)) case BaseModel(): - print(json.dumps(obj.model_dump(), indent=2)) + print(json.dumps(obj.model_dump())) case _: print(json.dumps(obj)) @@ -83,6 +103,13 @@ def display_compact(obj: Any, stream: Stream): for dev in devices: print(dev.name) print(indent(textwrap.fill(", ".join(dev.protocols), 80), " ")) + case DataEvent(name=name): + print(f"Data Event: {name}") + case WorkerEvent(state=state): + print(f"Worker state: {state}") + case ProgressEvent(statuses=stats): + prog = max(100 * (s.percentage or 0) for s in stats.values()) or "???" + print(f"Progress: {prog}%") case other: FALLBACK(other, stream=stream) From d971244872eec47f0324bcbb6d7173208aa6c95e Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Thu, 18 Jul 2024 14:42:56 +0100 Subject: [PATCH 02/10] Fix ruff complaints --- src/blueapi/cli/format.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blueapi/cli/format.py b/src/blueapi/cli/format.py index a6d07fa13..596468aa8 100644 --- a/src/blueapi/cli/format.py +++ b/src/blueapi/cli/format.py @@ -67,7 +67,7 @@ def display_full(obj: Any, stream: Stream): case ProgressEvent(): print(f"Progress:{fmt_dict(obj.model_dump())}") case BaseModel(): - print(obj.__class__.__name__, end='') + print(obj.__class__.__name__, end="") print(fmt_dict(obj.model_dump())) case other: FALLBACK(other, stream=stream) @@ -108,7 +108,7 @@ def display_compact(obj: Any, stream: Stream): case WorkerEvent(state=state): print(f"Worker state: {state}") case ProgressEvent(statuses=stats): - prog = max(100 * (s.percentage or 0) for s in stats.values()) or "???" + prog = max(100 * (s.percentage or 0) for s in stats.values()) or "???" print(f"Progress: {prog}%") case other: FALLBACK(other, stream=stream) From 793e9fdefbb5554161da8fe95cd2ee194e8a67a9 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Thu, 5 Sep 2024 11:31:59 +0100 Subject: [PATCH 03/10] Fix base model formatting test --- tests/unit_tests/test_cli.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index 7f9e4cabc..c865b5c3e 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -167,7 +167,7 @@ def test_valid_stomp_config_for_listener( ) assert ( result.output - == "Subscribing to all bluesky events from localhost:61613\nPress enter to exit" + == "Subscribing to all bluesky events from localhost:61613\nPress enter to exit\n" ) assert result.exit_code == 0 @@ -528,19 +528,7 @@ def test_unknown_object_formatting(): def test_generic_base_model_formatting(): output = StringIO() obj = ExtendedModel(name="foo", keys=[1, 2, 3], metadata={"fizz": "buzz"}) - exp = dedent("""\ - { - "name": "foo", - "keys": [ - 1, - 2, - 3 - ], - "metadata": { - "fizz": "buzz" - } - } - """) + exp = '{"name": "foo", "keys": [1, 2, 3], "metadata": {"fizz": "buzz"}}\n' OutputFormat.JSON.display(obj, output) assert exp == output.getvalue() From 8c3d50f1e108cae5de92fe8215f4f495d4eab86f Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Thu, 5 Sep 2024 13:04:40 +0100 Subject: [PATCH 04/10] Fix line length linting --- tests/unit_tests/test_cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index c865b5c3e..0a5cc12e6 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -165,10 +165,10 @@ def test_valid_stomp_config_for_listener( ], input="\n", ) - assert ( - result.output - == "Subscribing to all bluesky events from localhost:61613\nPress enter to exit\n" - ) + assert result.output == dedent("""\ + Subscribing to all bluesky events from localhost:61613 + Press enter to exit + """) assert result.exit_code == 0 From 7199874db666e140b71e09a1ffd29c6aad9bb060 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Thu, 5 Sep 2024 14:36:08 +0100 Subject: [PATCH 05/10] Up format coverage --- src/blueapi/cli/format.py | 8 +++- tests/unit_tests/test_cli.py | 81 +++++++++++++++++++++++++++++++++++- 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/blueapi/cli/format.py b/src/blueapi/cli/format.py index 596468aa8..0b8474b13 100644 --- a/src/blueapi/cli/format.py +++ b/src/blueapi/cli/format.py @@ -106,9 +106,13 @@ def display_compact(obj: Any, stream: Stream): case DataEvent(name=name): print(f"Data Event: {name}") case WorkerEvent(state=state): - print(f"Worker state: {state}") + print(f"Worker Event: {state}") case ProgressEvent(statuses=stats): - prog = max(100 * (s.percentage or 0) for s in stats.values()) or "???" + prog = ( + max(100 * (s.percentage or 0) for s in stats.values()) + if stats + else "???" + ) print(f"Progress: {prog}%") case other: FALLBACK(other, stream=stream) diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index 0a5cc12e6..bebc020dc 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -4,6 +4,7 @@ from io import StringIO from pathlib import Path from textwrap import dedent +from typing import Any from unittest.mock import Mock, patch import pytest @@ -17,10 +18,10 @@ from blueapi import __version__ from blueapi.cli.cli import main -from blueapi.cli.format import OutputFormat +from blueapi.cli.format import OutputFormat, fmt_dict from blueapi.client.rest import BlueskyRemoteControlError from blueapi.config import ScratchConfig, ScratchRepository -from blueapi.core.bluesky_types import Plan +from blueapi.core.bluesky_types import DataEvent, Plan from blueapi.service.model import ( DeviceModel, DeviceResponse, @@ -28,6 +29,7 @@ PlanModel, PlanResponse, ) +from blueapi.worker.event import ProgressEvent, TaskStatus, WorkerEvent, WorkerState @pytest.fixture @@ -507,6 +509,66 @@ def test_plan_output_formatting(): assert output.getvalue() == full +def test_event_formatting(): + data = DataEvent( + name="start", doc={"foo": "bar", "fizz": {"buzz": (1, 2, 3), "hello": "world"}} + ) + worker = WorkerEvent( + state=WorkerState.RUNNING, + task_status=TaskStatus(task_id="count", task_complete=False, task_failed=False), + errors=[], + warnings=[], + ) + progress = ProgressEvent(task_id="start", statuses={}) + + _assert_matching_formatting( + OutputFormat.JSON, + data, + ( + """{"name": "start", "doc": """ + """{"foo": "bar", "fizz": {"buzz": [1, 2, 3], "hello": "world"}}}\n""" + ), + ) + _assert_matching_formatting(OutputFormat.COMPACT, data, "Data Event: start\n") + _assert_matching_formatting( + OutputFormat.FULL, + data, + ( + "Start: \n" + " foo: bar\n" + " fizz: \n" + " buzz: (1, 2, 3)\n" + " hello: world\n" + ), + ) + + _assert_matching_formatting( + OutputFormat.JSON, + worker, + ( + """{"state": "RUNNING", "task_status": """ + """{"task_id": "count", "task_complete": false, "task_failed": false}, """ + """"errors": [], "warnings": []}\n""" + ), + ) + _assert_matching_formatting( + OutputFormat.COMPACT, worker, "Worker Event: WorkerState.RUNNING\n" + ) + _assert_matching_formatting( + OutputFormat.FULL, + worker, + "WorkerEvent: WorkerState.RUNNING\n task_id: count\n", + ) + + _assert_matching_formatting( + OutputFormat.JSON, progress, """{"task_id": "start", "statuses": {}}\n""" + ) + _assert_matching_formatting(OutputFormat.COMPACT, progress, "Progress: ???%\n") + _assert_matching_formatting( + OutputFormat.FULL, progress, "Progress:\n task_id: start\n" + ) + + def test_unknown_object_formatting(): demo = {"foo": 42, "bar": ["hello", "World"]} @@ -525,6 +587,15 @@ def test_unknown_object_formatting(): assert exp == output.getvalue() +def test_dict_formatting(): + demo = {"name": "foo", "keys": [1, 2, 3], "metadata": {"fizz": "buzz"}} + exp = """\nname: foo\nkeys: [1, 2, 3]\nmetadata: \n fizz: buzz""" + assert fmt_dict(demo, 0) == exp + + demo = "not a dict" + assert fmt_dict(demo, 0) == "not a dict" + + def test_generic_base_model_formatting(): output = StringIO() obj = ExtendedModel(name="foo", keys=[1, 2, 3], metadata={"fizz": "buzz"}) @@ -552,3 +623,9 @@ def test_init_scratch_calls_setup_scratch(mock_setup_scratch: Mock, runner: CliR ) assert result.exit_code == 0 mock_setup_scratch.assert_called_once_with(expected_config) + + +def _assert_matching_formatting(fmt: OutputFormat, obj: Any, expected: str): + output = StringIO() + fmt.display(obj, output) + assert expected == output.getvalue() From bec5d6966f8d8f98a110b714f1ca6be1477283f4 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Mon, 9 Sep 2024 11:42:12 +0100 Subject: [PATCH 06/10] Try to handle enum format changes between 3.10 and 3.11+ f'{Foo.A}' uses str.__format__ in 3.10 but enum.__format__ in 3.11+. Use the name attribute to work with the string directly to ensure the same behaviour in both environments. --- src/blueapi/cli/format.py | 2 +- tests/unit_tests/test_cli.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/blueapi/cli/format.py b/src/blueapi/cli/format.py index 0b8474b13..871b1ebb8 100644 --- a/src/blueapi/cli/format.py +++ b/src/blueapi/cli/format.py @@ -106,7 +106,7 @@ def display_compact(obj: Any, stream: Stream): case DataEvent(name=name): print(f"Data Event: {name}") case WorkerEvent(state=state): - print(f"Worker Event: {state}") + print(f"Worker Event: {state.name}") case ProgressEvent(statuses=stats): prog = ( max(100 * (s.percentage or 0) for s in stats.values()) diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index bebc020dc..e0c76a753 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -551,13 +551,11 @@ def test_event_formatting(): """"errors": [], "warnings": []}\n""" ), ) - _assert_matching_formatting( - OutputFormat.COMPACT, worker, "Worker Event: WorkerState.RUNNING\n" - ) + _assert_matching_formatting(OutputFormat.COMPACT, worker, "Worker Event: RUNNING\n") _assert_matching_formatting( OutputFormat.FULL, worker, - "WorkerEvent: WorkerState.RUNNING\n task_id: count\n", + "WorkerEvent: RUNNING\n task_id: count\n", ) _assert_matching_formatting( From 8ce3ab26aecc907cf2abd90ab330a4d7d4241148 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Mon, 9 Sep 2024 12:42:17 +0100 Subject: [PATCH 07/10] Use name attribute for full format as well --- src/blueapi/cli/format.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/blueapi/cli/format.py b/src/blueapi/cli/format.py index 871b1ebb8..e912cb75b 100644 --- a/src/blueapi/cli/format.py +++ b/src/blueapi/cli/format.py @@ -63,7 +63,9 @@ def display_full(obj: Any, stream: Stream): case DataEvent(name=name, doc=doc): print(f"{name.title()}: {fmt_dict(doc)}") case WorkerEvent(state=state, task_status=task): - print(f"WorkerEvent: {state}{fmt_dict(task.model_dump() if task else {})}") + print( + f"WorkerEvent: {state.name}{fmt_dict(task.model_dump() if task else {})}" + ) case ProgressEvent(): print(f"Progress:{fmt_dict(obj.model_dump())}") case BaseModel(): From 0192ef050356c663c11d5fc84d18c69ba6ae41a3 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Mon, 9 Sep 2024 14:13:29 +0100 Subject: [PATCH 08/10] Make naming worse to keep ruff quiet --- src/blueapi/cli/format.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blueapi/cli/format.py b/src/blueapi/cli/format.py index e912cb75b..0811f7f32 100644 --- a/src/blueapi/cli/format.py +++ b/src/blueapi/cli/format.py @@ -62,9 +62,9 @@ def display_full(obj: Any, stream: Stream): print(" " + proto) case DataEvent(name=name, doc=doc): print(f"{name.title()}: {fmt_dict(doc)}") - case WorkerEvent(state=state, task_status=task): + case WorkerEvent(state=st, task_status=task): print( - f"WorkerEvent: {state.name}{fmt_dict(task.model_dump() if task else {})}" + f"WorkerEvent: {st.name}{fmt_dict(task.model_dump() if task else {})}" ) case ProgressEvent(): print(f"Progress:{fmt_dict(obj.model_dump())}") From b2d12b62110a782f073fc253252744bfb25c5900 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Mon, 9 Sep 2024 16:37:48 +0100 Subject: [PATCH 09/10] Remove trailing spaces from fmt_dict output And cover the missing two lines to get to coverage threshold --- src/blueapi/cli/format.py | 6 +++--- tests/unit_tests/test_cli.py | 34 +++++++++++++++++++++++++--------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/blueapi/cli/format.py b/src/blueapi/cli/format.py index 0811f7f32..8d8ac744a 100644 --- a/src/blueapi/cli/format.py +++ b/src/blueapi/cli/format.py @@ -23,9 +23,9 @@ def fmt_dict(t: dict[str, Any] | Any, ind: int = 1) -> str: """Format a (possibly nested) dict into a human readable tree""" if not isinstance(t, dict): - return t + return f" {t}" pre = " " * (ind * 4) - return NL + NL.join(f"{pre}{k}: {fmt_dict(v, ind+1)}" for k, v in t.items() if v) + return NL + NL.join(f"{pre}{k}:{fmt_dict(v, ind+1)}" for k, v in t.items() if v) class OutputFormat(str, enum.Enum): @@ -61,7 +61,7 @@ def display_full(obj: Any, stream: Stream): for proto in dev.protocols: print(" " + proto) case DataEvent(name=name, doc=doc): - print(f"{name.title()}: {fmt_dict(doc)}") + print(f"{name.title()}:{fmt_dict(doc)}") case WorkerEvent(state=st, task_status=task): print( f"WorkerEvent: {st.name}{fmt_dict(task.model_dump() if task else {})}" diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index e0c76a753..77588e35a 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -533,13 +533,13 @@ def test_event_formatting(): _assert_matching_formatting( OutputFormat.FULL, data, - ( - "Start: \n" - " foo: bar\n" - " fizz: \n" - " buzz: (1, 2, 3)\n" - " hello: world\n" - ), + dedent("""\ + Start: + foo: bar + fizz: + buzz: (1, 2, 3) + hello: world + """), ) _assert_matching_formatting( @@ -587,11 +587,11 @@ def test_unknown_object_formatting(): def test_dict_formatting(): demo = {"name": "foo", "keys": [1, 2, 3], "metadata": {"fizz": "buzz"}} - exp = """\nname: foo\nkeys: [1, 2, 3]\nmetadata: \n fizz: buzz""" + exp = """\nname: foo\nkeys: [1, 2, 3]\nmetadata:\n fizz: buzz""" assert fmt_dict(demo, 0) == exp demo = "not a dict" - assert fmt_dict(demo, 0) == "not a dict" + assert fmt_dict(demo, 0) == " not a dict" def test_generic_base_model_formatting(): @@ -601,6 +601,22 @@ def test_generic_base_model_formatting(): OutputFormat.JSON.display(obj, output) assert exp == output.getvalue() + model = ExtendedModel( + name="demo_model", keys=[1, 2, 3], metadata={"foo": "bar", "fizz": "buzz"} + ) + _assert_matching_formatting( + OutputFormat.FULL, + model, + dedent("""\ + ExtendedModel + name: demo_model + keys: [1, 2, 3] + metadata: + foo: bar + fizz: buzz + """), + ) + @patch("blueapi.cli.cli.setup_scratch") def test_init_scratch_calls_setup_scratch(mock_setup_scratch: Mock, runner: CliRunner): From d5f993d8624d3c3753219361cf16e2771dcd0a2a Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Mon, 9 Sep 2024 16:59:20 +0100 Subject: [PATCH 10/10] Use _assert_matching_formatting for previous format tests --- tests/unit_tests/test_cli.py | 56 ++++++++++-------------------------- 1 file changed, 15 insertions(+), 41 deletions(-) diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index 77588e35a..afc0cdece 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -349,13 +349,8 @@ def test_device_output_formatting(): HasName """) - output = StringIO() - OutputFormat.COMPACT.display(devices, out=output) - assert output.getvalue() == compact + _assert_matching_formatting(OutputFormat.COMPACT, devices, compact) - # json outputs valid json - output = StringIO() - OutputFormat.JSON.display(devices, out=output) json_out = dedent("""\ [ { @@ -366,16 +361,14 @@ def test_device_output_formatting(): } ] """) - assert output.getvalue() == json_out - _ = json.loads(output.getvalue()) + _assert_matching_formatting(OutputFormat.JSON, devices, json_out) + _ = json.loads(json_out) - output = StringIO() - OutputFormat.FULL.display(devices, out=output) full = dedent("""\ my-device HasName """) - assert output.getvalue() == full + _assert_matching_formatting(OutputFormat.FULL, devices, full) class ExtendedModel(BaseModel): @@ -407,13 +400,8 @@ def test_plan_output_formatting(): metadata=object """) - output = StringIO() - OutputFormat.COMPACT.display(plans, out=output) - assert output.getvalue() == compact + _assert_matching_formatting(OutputFormat.COMPACT, plans, compact) - # json outputs valid json - output = StringIO() - OutputFormat.JSON.display(plans, out=output) json_out = dedent("""\ [ { @@ -458,11 +446,9 @@ def test_plan_output_formatting(): } ] """) - assert output.getvalue() == json_out - _ = json.loads(output.getvalue()) + _assert_matching_formatting(OutputFormat.JSON, plans, json_out) + _ = json.loads(json_out) - output = StringIO() - OutputFormat.FULL.display(plans, out=output) full = dedent("""\ my-plan Summary of description @@ -506,7 +492,7 @@ def test_plan_output_formatting(): "type": "object" } """) - assert output.getvalue() == full + _assert_matching_formatting(OutputFormat.FULL, plans, full) def test_event_formatting(): @@ -570,19 +556,13 @@ def test_event_formatting(): def test_unknown_object_formatting(): demo = {"foo": 42, "bar": ["hello", "World"]} - output = StringIO() - OutputFormat.JSON.display(demo, output) exp = """{"foo": 42, "bar": ["hello", "World"]}\n""" - assert exp == output.getvalue() + _assert_matching_formatting(OutputFormat.JSON, demo, exp) - output = StringIO() - OutputFormat.COMPACT.display(demo, output) exp = """{'bar': ['hello', 'World'], 'foo': 42}\n""" - assert exp == output.getvalue() + _assert_matching_formatting(OutputFormat.COMPACT, demo, exp) - output = StringIO() - OutputFormat.FULL.display(demo, output) - assert exp == output.getvalue() + _assert_matching_formatting(OutputFormat.FULL, demo, exp) def test_dict_formatting(): @@ -595,24 +575,18 @@ def test_dict_formatting(): def test_generic_base_model_formatting(): - output = StringIO() - obj = ExtendedModel(name="foo", keys=[1, 2, 3], metadata={"fizz": "buzz"}) - exp = '{"name": "foo", "keys": [1, 2, 3], "metadata": {"fizz": "buzz"}}\n' - OutputFormat.JSON.display(obj, output) - assert exp == output.getvalue() + model = ExtendedModel(name="demo", keys=[1, 2, 3], metadata={"fizz": "buzz"}) + exp = '{"name": "demo", "keys": [1, 2, 3], "metadata": {"fizz": "buzz"}}\n' + _assert_matching_formatting(OutputFormat.JSON, model, exp) - model = ExtendedModel( - name="demo_model", keys=[1, 2, 3], metadata={"foo": "bar", "fizz": "buzz"} - ) _assert_matching_formatting( OutputFormat.FULL, model, dedent("""\ ExtendedModel - name: demo_model + name: demo keys: [1, 2, 3] metadata: - foo: bar fizz: buzz """), )