precon3d 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
precon3d/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Initializes the package."""
2
+
3
+ # Add import pdbp to an __init__.py of your project, which
4
+ # will automatically make Pdb+ the default debugger at breakpoints
5
+ # import pdbp
@@ -0,0 +1,327 @@
1
+ """
2
+ This module provides a custom command-line interface (CLI) toolkit,
3
+ utilizing the Typer and Click libraries for enhanced user interaction
4
+ and command management.
5
+ """
6
+
7
+ import sys
8
+ from enum import StrEnum
9
+ from typing import Final
10
+
11
+ import click
12
+ import typer
13
+ from rich.console import Console
14
+ from rich.table import Table
15
+ from rich import box
16
+
17
+
18
+ LOGO_TEXT: Final = r"""
19
+ +-------------+
20
+ / /|
21
+ / / |
22
+ +-------------+ | PRECON3D:
23
+ | slice 1 | | (PRE)pare and re(CON)struct
24
+ | 2 | | serial sectioning montage data
25
+ | 3 | | into 3D volume 🧊
26
+ | ... | +
27
+ | slice n | /
28
+ +-------------+/
29
+
30
+ """
31
+ console = Console()
32
+
33
+
34
+ class Color(StrEnum):
35
+ """Colors for custom typer formmatting
36
+
37
+ full list of colors for customization here:
38
+ https://rich.readthedocs.io/en/stable/appendix/colors.html
39
+ """
40
+
41
+ FRONT_MATTER: str = "dark_sea_green1"
42
+ MODULE: str = "magenta"
43
+ COMMAND: str = "orange1"
44
+ ERROR: str = "red"
45
+ STATEMENT: str = "white"
46
+ CURRENT_LEVEL: str = "yellow"
47
+ HEADER: str = "bold cyan"
48
+ PARAMETER: str = "green"
49
+ TYPE: str = "blue"
50
+ REQUIRED: str = "yellow"
51
+ DEAFULT: str = "magenta"
52
+ SHORT_HELP: str = "white"
53
+
54
+
55
+ class CustomCLIGroup(typer.core.TyperGroup):
56
+ """
57
+ CustomCLIGroup is an enhanced command group for module-level help
58
+ and error handling in the CLI.
59
+
60
+ This class extends Typer's base group functionality to provide a
61
+ rich, color-coded help and usage display using the Rich library. It
62
+ is designed to improve user interaction by offering:
63
+
64
+ 1. A dynamic "Usage:" line that adapts based on the current
65
+ command context.
66
+ 2. Display of a short help message (if available) for the current
67
+ command group.
68
+ 3. A formatted table listing subcommands (or modules) with
69
+ color-coded entries:
70
+ - Module names are shown in a designated module color.
71
+ - Commands are shown in a designated command color.
72
+ 4. Full help documentation (docstring) display when available.
73
+
74
+ Additional Features:
75
+ - When the main command is invoked without additional subcommands,
76
+ an ASCII art logo is printed to brand the CLI.
77
+ - The resolve_command method is overridden to catch cases where a
78
+ user supplies a non-existent command or module. In such cases,
79
+ it prints a descriptive error message, indicates available
80
+ options, and then displays the group help.
81
+ - Overrides several methods (such as format_help, get_help, main)
82
+ to integrate rich formatting and improved error management.
83
+
84
+ Methods:
85
+ list_commands(ctx):
86
+ Returns a sorted list of available command names.
87
+ format_help(ctx, formatter):
88
+ Displays the help message for the current group.
89
+ get_help(ctx):
90
+ Displays help and exits the program.
91
+ show_help(ctx):
92
+ Formats and prints the usage, short help, subcommand table,
93
+ and full help for the group.
94
+ format_exception(ctx, exc):
95
+ Overrides exception formatting (returns an empty string, as
96
+ errors are handled separately).
97
+ resolve_command(ctx, args):
98
+ Attempts to resolve a command; on failure, prints an error
99
+ message, shows available options, and exits.
100
+ main(*args, **kwargs):
101
+ Executes the main command loop with standalone mode disabled
102
+ to allow error bubbling.
103
+ """
104
+
105
+ def list_commands(self, _ctx):
106
+ """List all the commands (functions) added to the module"""
107
+ return sorted(self.commands.keys())
108
+
109
+ def format_help(self, ctx, _formatter):
110
+ """Displays the help message for the current group."""
111
+ self.show_help(ctx)
112
+
113
+ def get_help(self, ctx):
114
+ """Displays help and exits the program to handle errors."""
115
+ self.show_help(ctx)
116
+ ctx.exit()
117
+ return ""
118
+
119
+ def show_help(self, ctx: click.Context):
120
+ """Custom show help function displaying table for groups"""
121
+ # 1. Print usage header.
122
+ # Determine the usage line based on the current command path:
123
+ if ctx.command_path.strip() == "precon3d":
124
+ console.print(
125
+ f"[{Color.FRONT_MATTER}]"
126
+ f"{LOGO_TEXT}"
127
+ f"[/{Color.FRONT_MATTER}]"
128
+ )
129
+ # If only "precon3d" is provided, show [MODULE]:
130
+ usage_line = (
131
+ f"[{Color.CURRENT_LEVEL}]precon3d[/{Color.CURRENT_LEVEL}] "
132
+ f"[[{Color.MODULE}]MODULE[/{Color.MODULE}]] "
133
+ f"[[{Color.COMMAND}]COMMAND[/{Color.COMMAND}]] "
134
+ f"[[{Color.PARAMETER}]PARAMETERS[/{Color.PARAMETER}]]"
135
+ )
136
+ column_label = "Module"
137
+ else:
138
+ # Otherwise, just append [COMMAND] [OPTIONS]:
139
+ usage_line = (
140
+ f"[{Color.CURRENT_LEVEL}]{ctx.command_path}[/{Color.CURRENT_LEVEL}] "
141
+ f"[[{Color.COMMAND}]COMMAND[/{Color.COMMAND}]] "
142
+ f"[[{Color.PARAMETER}]PARAMETERS[/{Color.PARAMETER}]]"
143
+ )
144
+ column_label = "Command"
145
+ console.print(f"[bold]Usage:[/] {usage_line}\n")
146
+
147
+ # 2. Print short help for this group if available.
148
+ short_help = getattr(self, "short_help", "")
149
+ if short_help:
150
+ console.print(short_help + "\n", style=f"{Color.SHORT_HELP}")
151
+ # 3. Build a table listing subcommands.
152
+ table = Table(
153
+ show_header=True, header_style=f"{Color.HEADER}", box=None
154
+ )
155
+ table.add_column(column_label, justify="left")
156
+ table.add_column("Short help", justify="left")
157
+ for name, cmd in self.commands.items():
158
+ # Distinguish modules from commands:
159
+ if isinstance(cmd, typer.core.TyperGroup):
160
+ # For modules, display the name in {Color.MODULE}.
161
+ display_name = f"[{Color.MODULE}]{name}[/{Color.MODULE}]"
162
+ else:
163
+ # For function commands, display the name in {Color.COMMAND}.
164
+ display_name = f"[{Color.COMMAND}]{name}[/{Color.COMMAND}]"
165
+ short = getattr(cmd, "short_help", "") or ""
166
+ table.add_row(display_name, short)
167
+ console.print(table)
168
+
169
+ def format_exception(self, _ctx, _exc):
170
+ """Format exceptions generated by typer"""
171
+ # We override this method but do not print anything here.
172
+ return ""
173
+
174
+ def resolve_command(self, ctx, args):
175
+ """Attempts to resolve a command; on failure, prints an error
176
+ message, shows available options, and exits."""
177
+ try:
178
+ return super().resolve_command(ctx, args)
179
+ except click.exceptions.UsageError as e:
180
+ # Print error message.
181
+ console.print(f"Error: {e}\n", style=f"{Color.ERROR}")
182
+ # Print a message indicating viable options.
183
+ console.print(
184
+ f"\n[{Color.STATEMENT}]Availble options at this level ([/{Color.STATEMENT}]"
185
+ f"[{Color.CURRENT_LEVEL}]{ctx.command_path}[/{Color.CURRENT_LEVEL}]"
186
+ f"[{Color.STATEMENT}]) are:\n[/{Color.STATEMENT}]"
187
+ )
188
+ # Show this group's help then exit.
189
+ self.show_help(ctx)
190
+ ctx.exit(1)
191
+ return None # This return is never reached but silences pylint.
192
+
193
+ def main(self, *args, **kwargs):
194
+ """Executes the main command loop with standalone mode disabled
195
+ to allow error bubbling."""
196
+ # Force standalone_mode=False so errors bubble up.
197
+ kwargs.setdefault("standalone_mode", False)
198
+ try:
199
+ return super().main(*args, **kwargs)
200
+ except Exception as e: # pylint: disable=broad-exception-caught
201
+ # catch broad exceptions
202
+ console.print(f"Error: {e}\n", style=f"{Color.ERROR}")
203
+ sys.exit(1)
204
+
205
+
206
+ class CustomCLICommand(click.Command):
207
+ """
208
+ CustomCLICommand is an enhanced command class for individual CLI
209
+ commands that provides detailed, color-formatted help and error
210
+ handling using the Rich library.
211
+
212
+ This class extends Click's Command functionality to:
213
+ - Print a "Usage:" line with the current command path and its parameters.
214
+ - Display the command's short help message (if provided) in a highlighted style.
215
+ - Create a formatted table of parameters, detailing each parameter's:
216
+ - Name
217
+ - Type
218
+ - Required status
219
+ - Default value
220
+ - Associated short help
221
+ - Output the full detailed help (the command's docstring) when available.
222
+
223
+ Additionally, CustomCLICommand intercepts missing parameter exceptions
224
+ during argument parsing or command invocation. When such exceptions occur,
225
+ it prints an informative error message, displays the help for the command,
226
+ and exits the execution, ensuring that users are guided toward correct usage.
227
+
228
+ Methods:
229
+ - parse_args(ctx, args): Parses command-line arguments and handles missing parameters.
230
+ - invoke(ctx): Invokes the command and handles missing parameters.
231
+ - format_help(ctx, formatter): Displays the help message for the command.
232
+ - get_help(ctx): Displays the help and exits the process.
233
+ - show_help(ctx): Formats and prints:
234
+ - A usage header
235
+ - The command's short help (if available)
236
+ - A detailed table of parameters
237
+ - The full detailed help (if provided)
238
+ - format_exception(ctx, exc): Returns an empty string for higher-level error handling.
239
+ """
240
+
241
+ def __init__(self, *args, **kwargs):
242
+ kwargs.pop("rich_markup_mode", None)
243
+ kwargs.pop("rich_help_panel", None)
244
+ super().__init__(*args, **kwargs)
245
+
246
+ def parse_args(self, ctx, args):
247
+ try:
248
+ return super().parse_args(ctx, args)
249
+ except click.MissingParameter as e:
250
+ console.print(f"Error: {e}\n", style=f"{Color.ERROR}")
251
+ self.show_help(ctx)
252
+ ctx.exit(1)
253
+
254
+ def invoke(self, ctx):
255
+ try:
256
+ return super().invoke(ctx)
257
+ except click.MissingParameter as e:
258
+ console.print(f"Error: {e}\n", style=f"{Color.ERROR}")
259
+ self.show_help(ctx)
260
+ ctx.exit(1)
261
+
262
+ def format_help(self, ctx, _formatter):
263
+ self.show_help(ctx)
264
+
265
+ def get_help(self, ctx):
266
+ self.show_help(ctx)
267
+ ctx.exit()
268
+
269
+ def show_help(self, ctx: click.Context):
270
+ """Custom show help function displaying table for commands"""
271
+ # Print usage header for the command.
272
+ console.print(
273
+ f"[bold]Usage:[/] "
274
+ f"[{Color.CURRENT_LEVEL}]{ctx.command_path}[/{Color.CURRENT_LEVEL}] "
275
+ f"[[{Color.PARAMETER}]PARAMETERS[/{Color.PARAMETER}]]\n"
276
+ )
277
+
278
+ # Print short help if available.
279
+ short_help = getattr(self, "short_help", "")
280
+ if short_help:
281
+ console.print(short_help + "\n", style=f"{Color.SHORT_HELP}")
282
+
283
+ # Build table for parameters with additional detail.
284
+ console.print(f"[{Color.HEADER}][bold]Parameters:[/][/{Color.HEADER}]")
285
+ if self.params:
286
+ for param in self.params:
287
+ param_type = (
288
+ "Argument"
289
+ if isinstance(param, click.Argument)
290
+ else "Option"
291
+ )
292
+ required = "Yes" if param.required else "No"
293
+ default = (
294
+ str(param.default) if param.default is not None else ""
295
+ )
296
+ # help_text = getattr(param, "help", "") or ""
297
+ names = (
298
+ ", ".join(param.opts)
299
+ if isinstance(param, click.Option)
300
+ else param.name
301
+ )
302
+
303
+ # Determine the expected type
304
+ if isinstance(param.type, click.Path):
305
+ expected_type = "Path"
306
+ elif isinstance(
307
+ param.type, type
308
+ ): # Check if it's a built-in type
309
+ expected_type = param.type.__name__
310
+ else:
311
+ expected_type = str(
312
+ param.type
313
+ ) # Fallback to string representation
314
+
315
+ console.print(
316
+ f"[[{Color.PARAMETER}]{names}[/{Color.PARAMETER}]] ({param_type})"
317
+ )
318
+ console.print(f" Required: {required}")
319
+ console.print(f" Default: {default}")
320
+ console.print(f" Type: {expected_type}")
321
+ # console.print(f" Help: {help_text}\n")
322
+ else:
323
+ console.print("No parameters.\n")
324
+
325
+ def format_exception(self, _ctx, _exc):
326
+ """Format exceptions generated by typer"""
327
+ return ""