Coverage for harbor_cli/state.py: 58%

61 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-09 12:09 +0100

1from __future__ import annotations 

2 

3import asyncio 

4from pathlib import Path 

5from typing import Awaitable 

6from typing import Optional 

7from typing import Tuple 

8from typing import Type 

9from typing import TYPE_CHECKING 

10from typing import TypeVar 

11 

12if TYPE_CHECKING: 

13 from rich.console import Console 

14 

15from harborapi import HarborAsyncClient 

16from harborapi.exceptions import StatusError 

17from pydantic import BaseModel 

18 

19from .config import HarborCLIConfig 

20from .exceptions import handle_status_error 

21 

22T = TypeVar("T") 

23 

24 

25class CommonOptions(BaseModel): 

26 """Options that can be used with any command. 

27 

28 These options are not specific to any particular command. 

29 """ 

30 

31 # Output 

32 verbose: bool = False 

33 with_stdout: bool = False 

34 # File 

35 output_file: Optional[Path] = None 

36 no_overwrite: bool = False 

37 

38 class Config: 

39 extra = "allow" 

40 

41 

42class State: 

43 """Class used to manage the state of the program. 

44 

45 It is used as a singleton shared between all commands. 

46 Unlike a context object, the state object is not passed to each 

47 command, but is instead accessed via the global state variable. 

48 """ 

49 

50 config: HarborCLIConfig 

51 client: HarborAsyncClient 

52 loop: asyncio.AbstractEventLoop 

53 options: CommonOptions 

54 repl: bool = False 

55 config_loaded: bool = False 

56 console: Optional[Console] 

57 

58 def __init__(self) -> None: 

59 """Initialize the state object.""" 

60 self.config = HarborCLIConfig() 

61 self.client = None # type: ignore # will be patched by init_state 

62 self.loop = asyncio.get_event_loop() 

63 self.options = CommonOptions() 

64 self.console = None 

65 

66 def add_config(self, config: "HarborCLIConfig") -> None: 

67 """Add a config object to the state.""" 

68 self.config = config 

69 self.config_loaded = True 

70 

71 def add_client(self, client: HarborAsyncClient) -> None: 

72 """Add a client object to the state.""" 

73 self.client = client 

74 

75 def _init_console(self) -> None: 

76 """Import the console object if it hasn't been imported yet. 

77 We do this here, so that we don't create a circular import 

78 between the state module and the output module.""" 

79 # fmt: off 

80 from .output.console import console 

81 self.console = console 

82 # fmt: on 

83 

84 def run( 

85 self, 

86 coro: Awaitable[T], 

87 status: Optional[str] = None, 

88 no_handle: Type[Exception] | Tuple[Type[Exception], ...] | None = None, 

89 ) -> T: 

90 """Run a coroutine in the event loop. 

91 

92 Parameters 

93 ---------- 

94 coro : Awaitable[T] 

95 The coroutine to run. 

96 no_handle : Type[Exception] | Tuple[Type[Exception], ...] 

97 A single exception type or a tuple of exception types that 

98 should not be passed to the default exception handler. 

99 Exceptions of this type will be raised as-is. 

100 """ 

101 if self.console is None: 

102 self._init_console() 

103 assert self.console is not None 

104 

105 if not status: 

106 status = "Working..." 

107 if not status.endswith("..."): # aesthetic :) 

108 status += "..." 

109 

110 try: 

111 # show spinner when running a coroutine 

112 with self.console.status(status): 

113 resp = self.loop.run_until_complete(coro) 

114 except StatusError as e: 

115 if no_handle and isinstance(e, no_handle): 

116 raise 

117 handle_status_error(e) 

118 return resp 

119 

120 

121state = State()