usecli 0.1.59__tar.gz → 0.1.60__tar.gz

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.
Files changed (73) hide show
  1. {usecli-0.1.59 → usecli-0.1.60}/PKG-INFO +1 -1
  2. {usecli-0.1.59 → usecli-0.1.60}/pyproject.toml +1 -1
  3. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/__init__.py +72 -11
  4. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/commands/defaults/base/about_command.py +67 -32
  5. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/commands/defaults/base/help_command.py +2 -1
  6. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/commands/defaults/base/inspire_command.py +4 -6
  7. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/commands/defaults/core/utils.py +16 -3
  8. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/config/colors.py +69 -18
  9. usecli-0.1.60/src/usecli/cli/core/__init__.py +62 -0
  10. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/core/base_command.py +20 -7
  11. usecli-0.1.60/src/usecli/cli/core/exceptions/__init__.py +30 -0
  12. usecli-0.1.60/src/usecli/cli/core/ui/__init__.py +46 -0
  13. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/core/ui/title.py +1 -2
  14. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/services/command_service.py +49 -9
  15. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/shared/config/manager.py +37 -32
  16. usecli-0.1.59/src/usecli/cli/core/__init__.py +0 -66
  17. usecli-0.1.59/src/usecli/cli/core/exceptions/__init__.py +0 -16
  18. usecli-0.1.59/src/usecli/cli/core/ui/__init__.py +0 -31
  19. {usecli-0.1.59 → usecli-0.1.60}/LICENSE +0 -0
  20. {usecli-0.1.59 → usecli-0.1.60}/README.md +0 -0
  21. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/__init__.py +0 -0
  22. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/commands/README.md +0 -0
  23. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/commands/__init__.py +0 -0
  24. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/commands/custom/README.md +0 -0
  25. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/commands/custom/__init__.py +0 -0
  26. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/commands/defaults/__init__.py +0 -0
  27. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
  28. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
  29. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +0 -0
  30. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
  31. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
  32. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/commands/defaults/make/make_command.py +0 -0
  33. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/commands/defaults/make/make_theme_command.py +0 -0
  34. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/commands/init_command.py +0 -0
  35. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/config/__init__.py +0 -0
  36. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/core/error/__init__.py +0 -0
  37. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/core/error/handler.py +0 -0
  38. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/core/error/utils.py +0 -0
  39. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/core/exceptions/base.py +0 -0
  40. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/core/exceptions/config.py +0 -0
  41. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/core/exceptions/usage.py +0 -0
  42. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/core/exceptions/validation.py +0 -0
  43. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/core/ui/list.py +0 -0
  44. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/core/ui/title.txt +0 -0
  45. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/core/validators/__init__.py +0 -0
  46. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/core/validators/network.py +0 -0
  47. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/core/validators/numeric.py +0 -0
  48. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/core/validators/path.py +0 -0
  49. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/core/validators/string.py +0 -0
  50. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/services/__init__.py +0 -0
  51. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/templates/command.py.j2 +0 -0
  52. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/templates/theme.toml.j2 +0 -0
  53. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/templates/usecli.config.toml.j2 +0 -0
  54. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/themes/ayu_dark.toml +0 -0
  55. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
  56. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
  57. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
  58. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
  59. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/themes/default.toml +0 -0
  60. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/themes/dracula.toml +0 -0
  61. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
  62. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/themes/nord.toml +0 -0
  63. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/themes/tokyo_night.toml +0 -0
  64. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/utils/__init__.py +0 -0
  65. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/utils/interactive/__init__.py +0 -0
  66. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
  67. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/menu.py +0 -0
  68. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/params.py +0 -0
  69. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/shared/__init__.py +0 -0
  70. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/shared/config/__init__.py +0 -0
  71. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/shared/config/globals.py +0 -0
  72. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/ui.py +0 -0
  73. {usecli-0.1.59 → usecli-0.1.60}/src/usecli/usecli.config.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usecli
3
- Version: 0.1.59
3
+ Version: 0.1.60
4
4
  Summary: A powerful Python CLI framework for building beautiful, developer-friendly command-line tools.
5
5
  Author: Edward Boswell
6
6
  Author-email: Edward Boswell <thememium@gmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usecli"
3
- version = "0.1.59"
3
+ version = "0.1.60"
4
4
  description = "A powerful Python CLI framework for building beautiful, developer-friendly command-line tools."
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Edward Boswell", email = "thememium@gmail.com" }]
@@ -2,16 +2,24 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import shutil
6
5
  import sys
7
6
  from importlib import import_module
8
- from typing import Any, Optional, Sequence
7
+ from typing import TYPE_CHECKING, Any, Optional, Sequence
9
8
 
10
9
  import click
11
10
  import typer
12
11
  from click.exceptions import BadParameter, ClickException, Exit, UsageError
13
12
  from typer.core import TyperGroup
14
13
 
14
+ if TYPE_CHECKING:
15
+ from rich.console import Console
16
+
17
+ from usecli.menu import Menu
18
+ from usecli.params import Argument, Option
19
+ from usecli.ui import Confirm, Prompt
20
+
21
+ console: Console
22
+
15
23
  try:
16
24
  from typer._click.exceptions import BadParameter as TyperBadParameter # type: ignore[import-untyped]
17
25
  from typer._click.exceptions import ClickException as TyperClickException # type: ignore[import-untyped]
@@ -23,13 +31,8 @@ except ImportError:
23
31
 
24
32
  from usecli.cli.config.colors import COLOR
25
33
  from usecli.cli.core.base_command import BaseCommand
26
- from usecli.cli.core.exceptions import UsecliBadParameter, UsecliUsageError
27
- from usecli.cli.core.ui.list import list_commands
28
34
  from usecli.cli.services.command_service import CommandService
29
- from usecli.menu import Menu
30
- from usecli.params import Argument, Option
31
35
  from usecli.shared.config.manager import get_config
32
- from usecli.ui import Confirm, Console, Prompt, console
33
36
 
34
37
  colors = import_module("usecli.cli.config.colors")
35
38
  theme = COLOR
@@ -37,6 +40,27 @@ theme = COLOR
37
40
  sys.modules.setdefault(__name__ + ".colors", colors)
38
41
  sys.modules.setdefault("colors", colors)
39
42
 
43
+ _LAZY_EXPORTS = {
44
+ "Menu": ("usecli.menu", "Menu"),
45
+ "Argument": ("usecli.params", "Argument"),
46
+ "Option": ("usecli.params", "Option"),
47
+ "Prompt": ("usecli.ui", "Prompt"),
48
+ "Confirm": ("usecli.ui", "Confirm"),
49
+ "Console": ("usecli.ui", "Console"),
50
+ "console": ("usecli.ui", "console"),
51
+ }
52
+
53
+
54
+ def __getattr__(name: str) -> Any:
55
+ export = _LAZY_EXPORTS.get(name)
56
+ if export is None:
57
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
58
+ module_name, attr_name = export
59
+ value = getattr(import_module(module_name), attr_name)
60
+ globals()[name] = value
61
+ return value
62
+
63
+
40
64
  __all__ = [
41
65
  "BaseCommand",
42
66
  "console",
@@ -52,6 +76,10 @@ __all__ = [
52
76
  ]
53
77
 
54
78
 
79
+ def _console():
80
+ return __getattr__("console")
81
+
82
+
55
83
  def _is_interactive_flag_present() -> bool:
56
84
  """Check if -i/--interactive flag is present in sys.argv.
57
85
 
@@ -206,10 +234,14 @@ class PrefixMatchingGroup(TyperGroup):
206
234
  except Exit:
207
235
  sys.exit(0)
208
236
  except (BadParameter, TyperBadParameter) as e:
237
+ from usecli.cli.core.exceptions import UsecliBadParameter
238
+
209
239
  styled_error = UsecliBadParameter(e.message, ctx=e.ctx, param=e.param)
210
240
  styled_error.show()
211
241
  sys.exit(styled_error.exit_code)
212
242
  except (UsageError, TyperUsageError) as e:
243
+ from usecli.cli.core.exceptions import UsecliUsageError
244
+
213
245
  styled_error = UsecliUsageError(e.message, ctx=e.ctx)
214
246
  styled_error.show()
215
247
  sys.exit(styled_error.exit_code)
@@ -241,18 +273,34 @@ class FilteredListCommand(click.Command):
241
273
  Args:
242
274
  ctx: The Click context.
243
275
  """
276
+ from usecli.cli.core.ui.list import list_commands
277
+
244
278
  list_commands(app, prefix_filter=self.prefix_filter)
245
279
  return None
246
280
 
247
281
 
282
+ def _get_default_help() -> str:
283
+ return "Usecli CLI - An elegant CLI framework for Python"
284
+
285
+
248
286
  app = typer.Typer(
249
- help=_get_cli_help_text(),
287
+ help=_get_default_help(),
250
288
  invoke_without_command=True,
251
289
  no_args_is_help=False,
252
290
  cls=PrefixMatchingGroup,
253
291
  pretty_exceptions_enable=False, # Use custom error styling
254
292
  )
255
293
 
294
+ _help_resolved = False
295
+
296
+
297
+ def _resolve_help():
298
+ global _help_resolved
299
+ if not _help_resolved:
300
+ app.info.help = _get_cli_help_text()
301
+ _help_resolved = True
302
+
303
+
256
304
  service = CommandService(app)
257
305
  service.load_commands()
258
306
 
@@ -278,14 +326,20 @@ def run_app(
278
326
  version: Flag to show version and exit.
279
327
  help: Flag to show help and exit.
280
328
  """
329
+ _resolve_help()
330
+
281
331
  if help:
332
+ from usecli.cli.core.ui.list import list_commands
333
+
282
334
  list_commands(app)
283
335
  raise typer.Exit()
284
336
 
285
337
  if version:
338
+ import shutil
339
+
286
340
  config = get_config()
287
341
  command_path = shutil.which(sys.argv[0]) or sys.argv[0]
288
- console.print(
342
+ _console().print(
289
343
  f"[bold {theme.SECONDARY}]{config.get('title')} {service.version}[/bold {theme.SECONDARY}] [{theme.INFO}]({command_path})[/{theme.INFO}]"
290
344
  )
291
345
  raise typer.Exit()
@@ -305,18 +359,21 @@ def run_app(
305
359
  prefix_filter: str | None = None
306
360
  if ctx.obj and isinstance(ctx.obj, dict):
307
361
  prefix_filter = ctx.obj.get("prefix_filter")
362
+ from usecli.cli.core.ui.list import list_commands
363
+
308
364
  list_commands(app, prefix_filter=prefix_filter)
309
365
 
310
366
 
311
367
  def main() -> None:
312
368
  """Run the CLI application with custom error handling."""
369
+ _resolve_help()
313
370
  config = get_config()
314
371
  command_name = config._get_command_name()
315
372
  if command_name == "usecli" and not config.is_usecli_direct_dependency():
316
- console.print(
373
+ _console().print(
317
374
  "[bold red]Error:[/bold red] usecli is not a direct dependency of this project."
318
375
  )
319
- console.print(
376
+ _console().print(
320
377
  "Add it to your [cyan]pyproject.toml[/cyan] dependencies or dependency-groups."
321
378
  )
322
379
  sys.exit(1)
@@ -326,10 +383,14 @@ def main() -> None:
326
383
  except Exit:
327
384
  sys.exit(0)
328
385
  except (BadParameter, TyperBadParameter) as e:
386
+ from usecli.cli.core.exceptions import UsecliBadParameter
387
+
329
388
  styled_error = UsecliBadParameter(e.message, ctx=e.ctx, param=e.param)
330
389
  styled_error.show()
331
390
  sys.exit(styled_error.exit_code)
332
391
  except (UsageError, TyperUsageError) as e:
392
+ from usecli.cli.core.exceptions import UsecliUsageError
393
+
333
394
  styled_error = UsecliUsageError(e.message, ctx=e.ctx)
334
395
  styled_error.show()
335
396
  sys.exit(styled_error.exit_code)
@@ -1,30 +1,53 @@
1
1
  from __future__ import annotations
2
2
 
3
- import importlib.metadata
4
3
  import os
5
- import platform
6
- import re
7
4
  import sys
8
- from importlib.metadata import PackageNotFoundError
9
- from importlib.metadata import version as get_version
10
5
  from pathlib import Path
11
6
 
12
- if sys.version_info >= (3, 11):
13
- import tomllib
14
- else:
15
- import tomli as tomllib
16
-
17
- from rich.console import Console
18
-
19
7
  from usecli.cli.config.colors import COLOR
20
8
  from usecli.cli.core.base_command import BaseCommand
21
- from usecli.cli.core.ui.title import get_project_name, get_script_command_name
22
- from usecli.shared.config.manager import ConfigManager, get_config
9
+ from usecli.shared.config.manager import get_config
10
+
11
+
12
+ class _LazyConsole:
13
+ _console = None
14
+
15
+ def _get_console(self):
16
+ if self._console is None:
17
+ from rich.console import Console
18
+
19
+ self._console = Console()
20
+ return self._console
21
+
22
+ def __getattr__(self, name):
23
+ return getattr(self._get_console(), name)
24
+
25
+
26
+ console = _LazyConsole()
27
+
28
+
29
+ def _load_toml(text: str):
30
+ if sys.version_info >= (3, 11):
31
+ import tomllib
32
+ else:
33
+ import tomli as tomllib
34
+
35
+ return tomllib.loads(text)
23
36
 
24
- console = Console()
37
+
38
+ def _toml_decode_error():
39
+ if sys.version_info >= (3, 11):
40
+ pass
41
+ else:
42
+ pass
43
+
44
+ return _toml_decode_error()
25
45
 
26
46
 
27
47
  def _get_version() -> str:
48
+ from importlib.metadata import PackageNotFoundError
49
+ from importlib.metadata import version as get_version
50
+
28
51
  try:
29
52
  return get_version("usecli")
30
53
  except PackageNotFoundError:
@@ -36,6 +59,8 @@ def _parse_dependency_requirement(req: str) -> tuple[str, str | None]:
36
59
  if not req_core:
37
60
  return "", None
38
61
 
62
+ import re
63
+
39
64
  match = re.match(r"^([A-Za-z0-9_.-]+)", req_core)
40
65
  if not match:
41
66
  return "", None
@@ -57,11 +82,11 @@ def _parse_dependency_requirement(req: str) -> tuple[str, str | None]:
57
82
  return name, remainder
58
83
 
59
84
 
60
- def _get_console_script_distribution(
61
- command_name: str | None,
62
- ) -> importlib.metadata.Distribution | None:
85
+ def _get_console_script_distribution(command_name: str | None):
63
86
  if not command_name:
64
87
  return None
88
+ import importlib.metadata
89
+
65
90
  try:
66
91
  distributions = importlib.metadata.distributions()
67
92
  except Exception:
@@ -79,9 +104,7 @@ def _get_console_script_distribution(
79
104
  return None
80
105
 
81
106
 
82
- def _get_package_dependencies_from_distribution(
83
- dist: importlib.metadata.Distribution,
84
- ) -> list[tuple[str, str | None]]:
107
+ def _get_package_dependencies_from_distribution(dist) -> list[tuple[str, str | None]]:
85
108
  requires = dist.requires or []
86
109
  result: list[tuple[str, str | None]] = []
87
110
  for req in requires:
@@ -93,10 +116,12 @@ def _get_package_dependencies_from_distribution(
93
116
  return result
94
117
 
95
118
 
96
- def _get_dependencies(config: ConfigManager) -> list[tuple[str, str | None]]:
119
+ def _get_dependencies(config) -> list[tuple[str, str | None]]:
97
120
  command_name = os.path.basename(sys.argv[0]) if sys.argv else None
98
121
  dist = _get_console_script_distribution(command_name)
99
122
  if dist is None:
123
+ from usecli.cli.core.ui.title import get_script_command_name
124
+
100
125
  primary_command = get_script_command_name(default=None)
101
126
  dist = _get_console_script_distribution(primary_command)
102
127
  if dist is not None:
@@ -107,8 +132,8 @@ def _get_dependencies(config: ConfigManager) -> list[tuple[str, str | None]]:
107
132
  return []
108
133
 
109
134
  try:
110
- data = tomllib.loads(pyproject_path.read_text())
111
- except (tomllib.TOMLDecodeError, OSError):
135
+ data = _load_toml(pyproject_path.read_text())
136
+ except (_toml_decode_error(), OSError):
112
137
  return []
113
138
 
114
139
  deps = data.get("project", {}).get("dependencies", [])
@@ -125,16 +150,18 @@ def _get_dependencies(config: ConfigManager) -> list[tuple[str, str | None]]:
125
150
  return result
126
151
 
127
152
 
128
- def _get_application_distribution() -> importlib.metadata.Distribution | None:
153
+ def _get_application_distribution():
129
154
  command_name = os.path.basename(sys.argv[0]) if sys.argv else None
130
155
  dist = _get_console_script_distribution(command_name)
131
156
  if dist is None:
157
+ from usecli.cli.core.ui.title import get_script_command_name
158
+
132
159
  primary_command = get_script_command_name(default=None)
133
160
  dist = _get_console_script_distribution(primary_command)
134
161
  return dist
135
162
 
136
163
 
137
- def _get_application_version(config: ConfigManager) -> str:
164
+ def _get_application_version(config) -> str:
138
165
  dist = _get_application_distribution()
139
166
  if dist is not None:
140
167
  return dist.version
@@ -146,7 +173,7 @@ def _get_application_version(config: ConfigManager) -> str:
146
173
  return _get_version()
147
174
 
148
175
 
149
- def _get_application_description(config: ConfigManager) -> str:
176
+ def _get_application_description(config) -> str:
150
177
  description = config.get("description")
151
178
  if (
152
179
  config.has_key("description")
@@ -165,14 +192,14 @@ def _get_application_description(config: ConfigManager) -> str:
165
192
  )
166
193
 
167
194
 
168
- def _get_project_description(config: ConfigManager) -> str | None:
195
+ def _get_project_description(config) -> str | None:
169
196
  pyproject_path = config.pyproject_path
170
197
  if not pyproject_path.exists():
171
198
  return None
172
199
 
173
200
  try:
174
- data = tomllib.loads(pyproject_path.read_text())
175
- except (tomllib.TOMLDecodeError, OSError):
201
+ data = _load_toml(pyproject_path.read_text())
202
+ except (_toml_decode_error(), OSError):
176
203
  return None
177
204
 
178
205
  description = data.get("project", {}).get("description")
@@ -202,6 +229,8 @@ def _get_installed_script_commands(command_name: str | None) -> list[str]:
202
229
 
203
230
 
204
231
  def _get_script_commands() -> list[str]:
232
+ from usecli.cli.core.ui.title import get_script_command_name
233
+
205
234
  primary_command = get_script_command_name(default=None)
206
235
  command_name = os.path.basename(sys.argv[0]) if sys.argv else primary_command
207
236
  installed_commands = _get_installed_script_commands(command_name)
@@ -217,8 +246,8 @@ def _get_script_commands() -> list[str]:
217
246
  return []
218
247
 
219
248
  try:
220
- data = tomllib.loads(pyproject_path.read_text())
221
- except (tomllib.TOMLDecodeError, OSError):
249
+ data = _load_toml(pyproject_path.read_text())
250
+ except (_toml_decode_error(), OSError):
222
251
  return []
223
252
 
224
253
  scripts = data.get("project", {}).get("scripts", {})
@@ -245,6 +274,8 @@ class AboutCommand(BaseCommand):
245
274
  def handle(self) -> None:
246
275
  config = get_config()
247
276
  version = _get_application_version(config)
277
+ from usecli.cli.core.ui.title import get_project_name
278
+
248
279
  app_name = get_project_name()
249
280
  description = _get_application_description(config)
250
281
 
@@ -262,6 +293,8 @@ class AboutCommand(BaseCommand):
262
293
  version_label = "Cli Version" if dist is not None else "Application Version"
263
294
  self._print_row(name_label, app_name)
264
295
  self._print_row(version_label, version)
296
+ import platform
297
+
265
298
  self._print_row("Python Version", platform.python_version())
266
299
  self._print_row("Platform", f"[{COLOR.FOREGROUND_MUTED}]{platform.platform()}")
267
300
 
@@ -282,6 +315,8 @@ class AboutCommand(BaseCommand):
282
315
  if deps:
283
316
  for dep_name, spec in deps:
284
317
  try:
318
+ from importlib.metadata import version as get_version
319
+
285
320
  installed_version = get_version(dep_name)
286
321
  self._print_row(dep_name, installed_version)
287
322
  except Exception:
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from usecli.cli.core.base_command import BaseCommand
6
- from usecli.cli.core.ui.list import list_commands
7
6
 
8
7
 
9
8
  class HelpCommand(BaseCommand):
@@ -19,4 +18,6 @@ class HelpCommand(BaseCommand):
19
18
 
20
19
  def handle(self) -> None:
21
20
  """Handle the command execution."""
21
+ from usecli.cli.core.ui.list import list_commands
22
+
22
23
  list_commands(self.app)
@@ -4,15 +4,10 @@ from __future__ import annotations
4
4
 
5
5
  import random
6
6
 
7
- from rich.console import Console
8
- from rich.panel import Panel
9
-
10
7
  from usecli.cli.config.colors import COLOR
11
8
  from usecli.cli.core.base_command import BaseCommand
12
9
  from usecli.shared.config.manager import get_config
13
10
 
14
- console = Console()
15
-
16
11
 
17
12
  class InspireCommand(BaseCommand):
18
13
  """Command for displaying random inspirational quotes."""
@@ -78,7 +73,10 @@ class InspireCommand(BaseCommand):
78
73
  selected = random.choice(quotes)
79
74
  quote, author = selected.rsplit(" - ", 1)
80
75
 
81
- console.print(
76
+ from rich.console import Console
77
+ from rich.panel import Panel
78
+
79
+ Console().print(
82
80
  Panel(
83
81
  f"{quote}\n\n[{COLOR.FOREGROUND_MUTED}]— {author}[/{COLOR.FOREGROUND_MUTED}]",
84
82
  border_style=COLOR.PANEL_PRIMARY,
@@ -8,12 +8,25 @@ from datetime import datetime, timedelta
8
8
  from pathlib import Path
9
9
  from typing import TYPE_CHECKING
10
10
 
11
- from rich.console import Console
12
-
13
11
  if TYPE_CHECKING:
14
12
  pass
15
13
 
16
- console = Console()
14
+
15
+ class _LazyConsole:
16
+ _console = None
17
+
18
+ def _get_console(self):
19
+ if self._console is None:
20
+ from rich.console import Console
21
+
22
+ self._console = Console()
23
+ return self._console
24
+
25
+ def __getattr__(self, name):
26
+ return getattr(self._get_console(), name)
27
+
28
+
29
+ console = _LazyConsole()
17
30
 
18
31
 
19
32
  def is_interactive() -> bool:
@@ -11,18 +11,19 @@ Usage:
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
- import importlib.metadata
15
- import importlib.util
16
14
  import os
17
15
  import sys
18
16
  import time
19
17
  from pathlib import Path
20
18
  from typing import Any, Callable, Final, Protocol, cast, final
21
19
 
22
- if sys.version_info >= (3, 11):
23
- import tomllib
24
- else:
25
- import tomli as tomllib
20
+
21
+ def _import_tomllib():
22
+ if sys.version_info >= (3, 11):
23
+ import tomllib
24
+ else:
25
+ import tomli as tomllib
26
+ return tomllib
26
27
 
27
28
 
28
29
  PYPROJECT_TOML = "pyproject.toml"
@@ -114,6 +115,8 @@ def _get_console_script_aliases(command_name: str | None) -> set[str]:
114
115
  return set()
115
116
  aliases: set[str] = {command_name}
116
117
  try:
118
+ import importlib.metadata
119
+
117
120
  distributions = importlib.metadata.distributions()
118
121
  except Exception:
119
122
  return aliases
@@ -139,7 +142,7 @@ def _config_matches_command(path: Path, command_name: str | None) -> bool:
139
142
  return True
140
143
  try:
141
144
  data = _load_usecli_config_file(path)
142
- except (tomllib.TOMLDecodeError, OSError):
145
+ except OSError:
143
146
  return True
144
147
  config_command = data.get("command_name")
145
148
  if not isinstance(config_command, str):
@@ -205,6 +208,8 @@ def _find_usecli_config_path_for_command(
205
208
 
206
209
 
207
210
  def _find_usecli_config_in_package() -> Path | None:
211
+ import importlib.util
212
+
208
213
  spec = importlib.util.find_spec(_get_package_name())
209
214
  if spec is None or not spec.submodule_search_locations:
210
215
  return None
@@ -222,6 +227,8 @@ def _find_usecli_config_in_package() -> Path | None:
222
227
 
223
228
 
224
229
  def _find_usecli_config_in_named_package(package_name: str) -> Path | None:
230
+ import importlib.util
231
+
225
232
  if not package_name:
226
233
  return None
227
234
  spec = importlib.util.find_spec(package_name)
@@ -241,6 +248,8 @@ def _find_usecli_config_in_named_package(package_name: str) -> Path | None:
241
248
 
242
249
 
243
250
  def _find_usecli_config_for_console_script() -> Path | None:
251
+ import importlib.metadata
252
+
244
253
  command_name = os.path.basename(sys.argv[0]) if sys.argv else ""
245
254
  if not command_name:
246
255
  return None
@@ -282,6 +291,8 @@ def _is_preferred_package_path(path: Path) -> bool:
282
291
 
283
292
 
284
293
  def _is_within_usecli_package(start_dir: Path) -> bool:
294
+ import importlib.util
295
+
285
296
  spec = importlib.util.find_spec(_get_package_name())
286
297
  if spec is None or not spec.submodule_search_locations:
287
298
  return False
@@ -365,6 +376,7 @@ def _load_usecli_config(
365
376
 
366
377
 
367
378
  def _load_usecli_config_file(config_path: Path) -> dict[str, Any]:
379
+ tomllib = _import_tomllib()
368
380
  try:
369
381
  data = tomllib.loads(config_path.read_text())
370
382
  except (tomllib.TOMLDecodeError, OSError):
@@ -556,6 +568,7 @@ def _resolve_theme_path(
556
568
 
557
569
 
558
570
  def _load_theme_file(theme_path: Path) -> dict[str, Any]:
571
+ tomllib = _import_tomllib()
559
572
  try:
560
573
  with open(theme_path, "rb") as theme_file:
561
574
  data = tomllib.load(theme_file)
@@ -578,7 +591,15 @@ def _load_theme() -> tuple[dict[str, str], dict[str, str], str, Path | None]:
578
591
  theme_name = config_theme.strip()
579
592
 
580
593
  theme_path = _resolve_theme_path(theme_name, project_root, config_values)
581
- theme_data = _load_theme_file(theme_path) if theme_path else {}
594
+ package_default_theme = (THEMES_DIR / f"{DEFAULT_THEME_NAME}.toml").resolve()
595
+ if (
596
+ theme_name == DEFAULT_THEME_NAME
597
+ and theme_path
598
+ and theme_path.resolve() == package_default_theme
599
+ ):
600
+ theme_data = {}
601
+ else:
602
+ theme_data = _load_theme_file(theme_path) if theme_path else {}
582
603
 
583
604
  colors = _merge_theme_values(
584
605
  DEFAULT_THEME_COLORS,
@@ -711,19 +732,49 @@ def _apply_theme(
711
732
 
712
733
 
713
734
  def _ensure_theme_loaded(color_class: type[Any]) -> None:
714
- global _THEME_CONTEXT
715
- context = _theme_context()
716
- if context == _THEME_CONTEXT:
717
- return
718
- colors, ansi, _, _ = _load_theme()
719
- _THEME_CONTEXT = context
720
- _THEME_COLORS.update(colors)
721
- _THEME_ANSI.update(ansi)
735
+ global _THEME_CONTEXT, _THEME_LOADED
736
+ if _THEME_LOADED:
737
+ context = _theme_context()
738
+ if context == _THEME_CONTEXT:
739
+ return
740
+ colors, ansi, _, _ = _load_theme()
741
+ _THEME_CONTEXT = context
742
+ _THEME_COLORS.update(colors)
743
+ _THEME_ANSI.update(ansi)
744
+ else:
745
+ colors, ansi, _, _ = _load_theme()
746
+ _THEME_CONTEXT = _theme_context()
747
+ _THEME_COLORS.update(colors)
748
+ _THEME_ANSI.update(ansi)
749
+ _THEME_LOADED = True
722
750
  _apply_theme(cast(_ColorNamespace, color_class), _THEME_COLORS, _THEME_ANSI)
723
751
 
724
752
 
725
- _THEME_COLORS, _THEME_ANSI, _THEME_NAME, _THEME_PATH = _load_theme()
726
- _THEME_CONTEXT: tuple[Path | None, Path | None, str, Path | None] = _theme_context()
753
+ # Deferred: _load_theme() and _theme_context() are called on first COLOR attribute access
754
+ # via _ColorMeta.__getattribute__ -> _ensure_theme_loaded, instead of at import time.
755
+ # This avoids ~50ms of filesystem walking during startup when colors aren't needed yet.
756
+ _THEME_LOADED: bool = False
757
+ _THEME_COLORS: dict[str, str] = DEFAULT_THEME_COLORS.copy()
758
+ _THEME_ANSI: dict[str, str] = {
759
+ "reset": "\033[0m",
760
+ "primary": "",
761
+ "secondary": "",
762
+ "accent": "",
763
+ "foreground": "",
764
+ "foreground_muted": "",
765
+ "red": "",
766
+ "green": "",
767
+ "yellow": "",
768
+ "blue": "",
769
+ }
770
+ _THEME_NAME: str = DEFAULT_THEME_NAME
771
+ _THEME_PATH: Path | None = None
772
+ _THEME_CONTEXT: tuple[Path | None, Path | None, str, Path | None] = (
773
+ None,
774
+ None,
775
+ DEFAULT_THEME_NAME,
776
+ None,
777
+ )
727
778
 
728
779
 
729
780
  class _ColorMeta(type):