Coverage for harbor_cli/output/render.py: 84%
69 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-09 12:09 +0100
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-09 12:09 +0100
1from __future__ import annotations
3from contextlib import nullcontext
4from typing import List
5from typing import Sequence
6from typing import TypeVar
7from typing import Union
9import typer
10from harborapi.models.base import BaseModel as HarborBaseModel
11from pydantic import BaseModel
13from ..exceptions import OverwriteError
14from ..format import OutputFormat
15from ..logs import logger
16from ..state import state
17from .console import console
18from .table import BuiltinTypeException
19from .table import EmptySequenceError
20from .table import get_renderable
22T = TypeVar("T")
24# TODO: add ResultType = T | list[T] to types.py
27def render_result(result: T, ctx: typer.Context | None = None) -> None:
28 """Render the result of a command."""
29 fmt = state.config.output.format
30 paging = state.config.output.paging
32 ctx_manager = console.pager() if paging else nullcontext()
33 with ctx_manager: # type: ignore # not quite sure why mypy is complaining here
34 if fmt == OutputFormat.TABLE:
35 render_table(result, ctx)
36 elif fmt == OutputFormat.JSON: 36 ↛ 39line 36 didn't jump to line 39, because the condition on line 36 was never false
37 render_json(result, ctx)
38 else:
39 raise ValueError(f"Unknown output format {fmt!r}.")
42def render_table(result: T | Sequence[T], ctx: typer.Context | None = None) -> None:
43 """Render the result of a command as a table."""
44 # TODO: handle "primitives" like strings and numbers
46 # Try to render compact table if enabled
47 compact = state.config.output.table.compact
48 if compact:
49 try:
50 render_table_compact(result)
51 except NotImplementedError as e:
52 logger.debug(f"Unable to render compact table: {e}")
53 except (EmptySequenceError, BuiltinTypeException):
54 pass # can't render these types
55 else:
56 return
58 # If we got to this point, we have not printed a compact table.
59 # Use built-in table rendering from harborapi.
60 render_table_full(result)
63def render_table_compact(result: T | Sequence[T]) -> None:
64 """Render the result of a command as a compact table."""
65 renderable = get_renderable(result)
66 console.print(renderable)
69def render_table_full(result: T | Sequence[T]) -> None:
70 show_description = state.config.output.table.description
71 max_depth = state.config.output.table.max_depth
73 def print_item(item: T | str) -> None:
74 """Prints a harbor base model as a table (optionally with description),
75 if it is a harborapi BaseModel, otherwise just prints the item."""
76 if isinstance(item, HarborBaseModel):
77 console.print(
78 item.as_panel(with_description=show_description, max_depth=max_depth)
79 )
80 else:
81 console.print(item)
83 if isinstance(result, Sequence) and not isinstance(result, str):
84 for item in result: 84 ↛ 85line 84 didn't jump to line 85, because the loop on line 84 never started
85 print_item(item)
86 else:
87 print_item(result)
90def render_json(result: T | Sequence[T], ctx: typer.Context | None = None) -> None:
91 """Render the result of a command as JSON."""
92 p = state.options.output_file
93 with_stdout = state.options.with_stdout
94 no_overwrite = state.options.no_overwrite
95 indent = state.config.output.JSON.indent
96 # sort_keys = state.config.output.JSON.sort_keys
98 # We need to convert the types to JSON serializable types (base types)
99 # Pydantic can handle this for us to some extent.
100 # https://twitter.com/samuel_colvin/status/1617531798367645696
101 #
102 # To make the JSON serialization compatible with a wider range of
103 # data types, we wrap the data in a Pydantic model with a single field
104 # named __root__, which renders the data as the root value of the JSON object:
105 # Output(__root__={"foo": "bar"}).json() -> '{"foo": "bar"}'
106 # This is especially useful for serializing types like Paths and Timestamps.
107 #
108 # In pydantic v2, the __root__ field is going away, and we will
109 # be able to serialize/marshal any data type directly.
111 class Output(BaseModel):
112 __root__: Union[T, List[T]]
114 o = Output(__root__=result)
115 o_json = o.json(indent=indent)
116 if p: 116 ↛ 117line 116 didn't jump to line 117, because the condition on line 116 was never true
117 if p.exists() and no_overwrite:
118 raise OverwriteError(f"File {p.resolve()} exists.")
119 with open(p, "w") as f:
120 f.write(o_json)
121 logger.info(f"Output written to {p.resolve()}")
123 # Print to stdout if no output file is specified or if the
124 # --with-stdout flag is set.
125 if not p or with_stdout: 125 ↛ exitline 125 didn't return from function 'render_json', because the condition on line 125 was never false
126 # We have to specify indent again here, because print_json()
127 # ignores the indent of the JSON string passed to it.
128 console.print_json(o_json, indent=indent)