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
« prev ^ index » next coverage.py v7.11.3, created at 2026-03-24 05:23 +0000
1"""Unified CLI entrypoint for NexusLIMS.
3Provides the ``nexuslims`` command with subcommands for all NexusLIMS CLI
4tools. Uses lazy loading so that ``nexuslims --help`` stays fast without
5importing heavy modules.
7Usage
8-----
10.. code-block:: bash
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"""
21from __future__ import annotations
23import os
25import click
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}
34class LazyGroup(click.Group):
35 """Click group that lazily loads subcommands on first use."""
37 def __init__(self, *args, lazy_commands: dict[str, tuple[str, str]], **kwargs):
38 super().__init__(*args, **kwargs)
39 self._lazy_commands = lazy_commands
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))
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
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)
62 return None
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
69 module = importlib.import_module(module_path)
70 return getattr(module, attr_name)
73def _get_db_group() -> click.BaseCommand:
74 """Lazily build and return the db CLI group."""
75 from nexusLIMS.cli.migrate import _cli # noqa: PLC0415
77 return _cli()
80def _build_instruments_group() -> click.Group:
81 """Build the ``instruments`` group with its subcommands."""
83 @click.group()
84 def instruments() -> None:
85 """Manage NexusLIMS instruments."""
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.
95 Opens a terminal UI for adding, editing, and deleting
96 instruments in the NexusLIMS database.
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 )
114 _ensure_database_initialized()
115 _run_instrument_manager()
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 )
134 _ensure_database_initialized()
135 _list_instruments(output_format=output_format)
137 return instruments
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
144 return _fmt(prog_name)
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.
159 Manage records, configuration, database migrations, and instruments.
160 """
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.
173 Add the printed line to your shell's rc file to enable tab completion
174 for nexuslims commands, subcommands, and options.
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"
192 env_var = "_NEXUSLIMS_COMPLETE"
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"
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 )
213# Register non-lazy subcommands
214main.add_command(_build_instruments_group(), "instruments")
215main.add_command(_completion_command, "completion")
218# Register db as a lazy command via a callback
219_original_get_command = main.get_command
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)
228main.get_command = _patched_get_command # type: ignore[method-assign]
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
235if __name__ == "__main__": # pragma: no cover
236 main()