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

1from __future__ import annotations 

2 

3from contextlib import nullcontext 

4from typing import List 

5from typing import Sequence 

6from typing import TypeVar 

7from typing import Union 

8 

9import typer 

10from harborapi.models.base import BaseModel as HarborBaseModel 

11from pydantic import BaseModel 

12 

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 

21 

22T = TypeVar("T") 

23 

24# TODO: add ResultType = T | list[T] to types.py 

25 

26 

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 

31 

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}.") 

40 

41 

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 

45 

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 

57 

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) 

61 

62 

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) 

67 

68 

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 

72 

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) 

82 

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) 

88 

89 

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 

97 

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. 

110 

111 class Output(BaseModel): 

112 __root__: Union[T, List[T]] 

113 

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()}") 

122 

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)