Coverage for nexusLIMS/cli/main.py: 100%

83 statements  

« prev     ^ index     » next       coverage.py v7.11.3, created at 2026-03-24 05:23 +0000

1"""Unified CLI entrypoint for NexusLIMS. 

2 

3Provides the ``nexuslims`` command with subcommands for all NexusLIMS CLI 

4tools. Uses lazy loading so that ``nexuslims --help`` stays fast without 

5importing heavy modules. 

6 

7Usage 

8----- 

9 

10.. code-block:: bash 

11 

12 nexuslims --help 

13 nexuslims --version 

14 nexuslims build-records [OPTIONS] 

15 nexuslims config [dump|load|edit] 

16 nexuslims db [init|upgrade|view|...] 

17 nexuslims instruments manage 

18 nexuslims instruments list 

19""" 

20 

21from __future__ import annotations 

22 

23import os 

24 

25import click 

26 

27# Maps command name -> (module_path, attr_name) 

28_LAZY_COMMANDS: dict[str, tuple[str, str]] = { 

29 "build-records": ("nexusLIMS.cli.process_records", "main"), 

30 "config": ("nexusLIMS.cli.config", "main"), 

31} 

32 

33 

34class LazyGroup(click.Group): 

35 """Click group that lazily loads subcommands on first use.""" 

36 

37 def __init__(self, *args, lazy_commands: dict[str, tuple[str, str]], **kwargs): 

38 super().__init__(*args, **kwargs) 

39 self._lazy_commands = lazy_commands 

40 

41 def list_commands(self, ctx: click.Context) -> list[str]: 

42 """List all commands, including lazy ones.""" 

43 base = super().list_commands(ctx) 

44 # Merge lazy command names that aren't already registered 

45 lazy_names = sorted(name for name in self._lazy_commands if name not in base) 

46 return sorted(set(base + lazy_names)) 

47 

48 def get_command( 

49 self, ctx: click.Context, cmd_name: str 

50 ) -> click.BaseCommand | None: 

51 """Get a command, lazily importing it if necessary.""" 

52 # Check eagerly registered commands first 

53 cmd = super().get_command(ctx, cmd_name) 

54 if cmd is not None: 

55 return cmd 

56 

57 # Try lazy import 

58 if cmd_name in self._lazy_commands: 

59 module_path, attr_name = self._lazy_commands[cmd_name] 

60 return self._import_command(module_path, attr_name) 

61 

62 return None 

63 

64 @staticmethod 

65 def _import_command(module_path: str, attr_name: str) -> click.BaseCommand: 

66 """Import a command from a module path.""" 

67 import importlib # noqa: PLC0415 

68 

69 module = importlib.import_module(module_path) 

70 return getattr(module, attr_name) 

71 

72 

73def _get_db_group() -> click.BaseCommand: 

74 """Lazily build and return the db CLI group.""" 

75 from nexusLIMS.cli.migrate import _cli # noqa: PLC0415 

76 

77 return _cli() 

78 

79 

80def _build_instruments_group() -> click.Group: 

81 """Build the ``instruments`` group with its subcommands.""" 

82 

83 @click.group() 

84 def instruments() -> None: 

85 """Manage NexusLIMS instruments.""" 

86 

87 @instruments.command() 

88 @click.version_option( 

89 version=None, 

90 message=_format_version("nexuslims instruments manage"), 

91 ) 

92 def manage() -> None: 

93 """Launch the interactive instrument management TUI. 

94 

95 Opens a terminal UI for adding, editing, and deleting 

96 instruments in the NexusLIMS database. 

97 

98 \b 

99 Keybindings: 

100 ------------ 

101 - a Add new instrument 

102 - e Edit selected instrument 

103 - d Delete selected instrument 

104 - r Refresh list 

105 - Ctrl+T Toggle theme (dark/light) 

106 - ? Show help 

107 - Ctrl+Q Quit 

108 """ # noqa: D301 

109 from nexusLIMS.cli.manage_instruments import ( # noqa: PLC0415 

110 _ensure_database_initialized, 

111 _run_instrument_manager, 

112 ) 

113 

114 _ensure_database_initialized() 

115 _run_instrument_manager() 

116 

117 @instruments.command(name="list") 

118 @click.option( 

119 "--format", 

120 "-f", 

121 "output_format", 

122 type=click.Choice(["table", "json"]), 

123 default="table", 

124 show_default=True, 

125 help="Output format.", 

126 ) 

127 def list_instruments(output_format: str) -> None: 

128 """List all instruments in the database.""" 

129 from nexusLIMS.cli.manage_instruments import ( # noqa: PLC0415 

130 _ensure_database_initialized, 

131 _list_instruments, 

132 ) 

133 

134 _ensure_database_initialized() 

135 _list_instruments(output_format=output_format) 

136 

137 return instruments 

138 

139 

140def _format_version(prog_name: str) -> str: 

141 """Format version string with release date if available.""" 

142 from nexusLIMS.cli import _format_version as _fmt # noqa: PLC0415 

143 

144 return _fmt(prog_name) 

145 

146 

147@click.group( 

148 cls=LazyGroup, 

149 lazy_commands=_LAZY_COMMANDS, 

150 epilog="Tip: run 'nexuslims completion' to set up shell tab completion.", 

151) 

152@click.version_option( 

153 version=None, 

154 message=_format_version("nexuslims"), 

155) 

156def main() -> None: 

157 """NexusLIMS command-line interface. 

158 

159 Manage records, configuration, database migrations, and instruments. 

160 """ 

161 

162 

163@click.command() 

164@click.option( 

165 "--shell", 

166 type=click.Choice(["bash", "zsh", "fish"]), 

167 default=None, 

168 help="Shell type. Detected automatically from $SHELL if omitted.", 

169) 

170def _completion_command(shell: str | None) -> None: 

171 """Print shell completion setup instructions. 

172 

173 Add the printed line to your shell's rc file to enable tab completion 

174 for nexuslims commands, subcommands, and options. 

175 

176 \b 

177 Examples: 

178 nexuslims completion # auto-detect shell 

179 nexuslims completion --shell zsh 

180 nexuslims completion --shell bash 

181 nexuslims completion --shell fish 

182 """ # noqa: D301 

183 if shell is None: 

184 shell_path = os.environ.get("SHELL", "") 

185 if "zsh" in shell_path: 

186 shell = "zsh" 

187 elif "fish" in shell_path: 

188 shell = "fish" 

189 else: 

190 shell = "bash" 

191 

192 env_var = "_NEXUSLIMS_COMPLETE" 

193 

194 if shell == "fish": 

195 line = f"{env_var}=fish_source nexuslims | source" 

196 rc_file = "~/.config/fish/config.fish" 

197 elif shell == "zsh": 

198 line = f'eval "$({env_var}=zsh_source nexuslims)"' 

199 rc_file = "~/.zshrc" 

200 else: 

201 line = f'eval "$({env_var}=bash_source nexuslims)"' 

202 rc_file = "~/.bashrc" 

203 

204 click.echo(f"# Add this line to {rc_file}:") 

205 click.echo(line) 

206 click.echo() 

207 click.echo( 

208 "# Note: nexuslims must be on your PATH (e.g. via an activated venv or " 

209 "`uv tool install nexuslims`)." 

210 ) 

211 

212 

213# Register non-lazy subcommands 

214main.add_command(_build_instruments_group(), "instruments") 

215main.add_command(_completion_command, "completion") 

216 

217 

218# Register db as a lazy command via a callback 

219_original_get_command = main.get_command 

220 

221 

222def _patched_get_command(ctx: click.Context, cmd_name: str) -> click.BaseCommand | None: 

223 if cmd_name == "db": 

224 return _get_db_group() 

225 return _original_get_command(ctx, cmd_name) 

226 

227 

228main.get_command = _patched_get_command # type: ignore[method-assign] 

229 

230# Ensure "db" shows up in help 

231if "db" not in main._lazy_commands: # noqa: SLF001 

232 main._lazy_commands["db"] = ("nexusLIMS.cli.migrate", "_cli") # noqa: SLF001 

233 

234 

235if __name__ == "__main__": # pragma: no cover 

236 main()