usecli 0.1.63__tar.gz → 0.1.65__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 (70) hide show
  1. {usecli-0.1.63 → usecli-0.1.65}/PKG-INFO +1 -1
  2. {usecli-0.1.63 → usecli-0.1.65}/pyproject.toml +1 -1
  3. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/__init__.py +5 -1
  4. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/commands/defaults/base/about_command.py +2 -16
  5. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/config/colors.py +30 -56
  6. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/shared/config/manager.py +97 -47
  7. {usecli-0.1.63 → usecli-0.1.65}/LICENSE +0 -0
  8. {usecli-0.1.63 → usecli-0.1.65}/README.md +0 -0
  9. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/__init__.py +0 -0
  10. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/commands/README.md +0 -0
  11. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/commands/__init__.py +0 -0
  12. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/commands/custom/README.md +0 -0
  13. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/commands/custom/__init__.py +0 -0
  14. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/commands/defaults/__init__.py +0 -0
  15. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
  16. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/commands/defaults/base/help_command.py +0 -0
  17. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/commands/defaults/base/inspire_command.py +0 -0
  18. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
  19. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +0 -0
  20. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
  21. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/commands/defaults/core/utils.py +0 -0
  22. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
  23. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/commands/defaults/make/make_command.py +0 -0
  24. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/commands/defaults/make/make_theme_command.py +0 -0
  25. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/commands/init_command.py +0 -0
  26. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/config/__init__.py +0 -0
  27. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/core/__init__.py +0 -0
  28. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/core/base_command.py +0 -0
  29. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/core/error/__init__.py +0 -0
  30. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/core/error/handler.py +0 -0
  31. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/core/error/utils.py +0 -0
  32. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/core/exceptions/__init__.py +0 -0
  33. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/core/exceptions/base.py +0 -0
  34. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/core/exceptions/config.py +0 -0
  35. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/core/exceptions/usage.py +0 -0
  36. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/core/exceptions/validation.py +0 -0
  37. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/core/ui/__init__.py +0 -0
  38. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/core/ui/list.py +0 -0
  39. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/core/ui/title.py +0 -0
  40. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/core/ui/title.txt +0 -0
  41. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/core/validators/__init__.py +0 -0
  42. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/core/validators/network.py +0 -0
  43. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/core/validators/numeric.py +0 -0
  44. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/core/validators/path.py +0 -0
  45. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/core/validators/string.py +0 -0
  46. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/services/__init__.py +0 -0
  47. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/services/command_service.py +0 -0
  48. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/templates/command.py.j2 +0 -0
  49. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/templates/theme.toml.j2 +0 -0
  50. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/templates/usecli.config.toml.j2 +0 -0
  51. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/themes/ayu_dark.toml +0 -0
  52. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
  53. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
  54. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
  55. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
  56. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/themes/default.toml +0 -0
  57. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/themes/dracula.toml +0 -0
  58. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
  59. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/themes/nord.toml +0 -0
  60. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/themes/tokyo_night.toml +0 -0
  61. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/utils/__init__.py +0 -0
  62. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/utils/interactive/__init__.py +0 -0
  63. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
  64. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/menu.py +0 -0
  65. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/params.py +0 -0
  66. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/shared/__init__.py +0 -0
  67. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/shared/config/__init__.py +0 -0
  68. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/shared/config/globals.py +0 -0
  69. {usecli-0.1.63 → usecli-0.1.65}/src/usecli/ui.py +0 -0
  70. {usecli-0.1.63 → usecli-0.1.65}/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.63
3
+ Version: 0.1.65
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.63"
3
+ version = "0.1.65"
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" }]
@@ -15,6 +15,8 @@ if TYPE_CHECKING:
15
15
  import typer
16
16
  from rich.console import Console
17
17
 
18
+ from usecli.cli.config.colors import COLOR
19
+ from usecli.cli.config.colors import COLOR as theme
18
20
  from usecli.cli.core.base_command import BaseCommand
19
21
  from usecli.menu import Menu
20
22
  from usecli.params import Argument, Option
@@ -48,7 +50,7 @@ def __getattr__(name: str) -> Any:
48
50
  return value
49
51
 
50
52
  # Handle CLI framework components - lazy initialization
51
- if name in ("app", "service", "BaseCommand", "colors", "theme"):
53
+ if name in ("app", "service", "BaseCommand", "colors", "COLOR", "theme"):
52
54
  _ensure_cli_initialized()
53
55
  return globals()[name]
54
56
 
@@ -191,6 +193,7 @@ def _ensure_cli_initialized() -> None:
191
193
  # Setup module aliasing
192
194
  colors = import_module("usecli.cli.config.colors")
193
195
  globals()["colors"] = colors
196
+ globals()["COLOR"] = COLOR
194
197
  globals()["theme"] = COLOR
195
198
  sys.modules.setdefault(__name__ + ".colors", colors)
196
199
  sys.modules.setdefault("colors", colors)
@@ -229,6 +232,7 @@ def _get_service():
229
232
 
230
233
  __all__ = [
231
234
  "BaseCommand",
235
+ "COLOR",
232
236
  "console",
233
237
  "Console",
234
238
  "main",
@@ -85,23 +85,9 @@ def _parse_dependency_requirement(req: str) -> tuple[str, str | None]:
85
85
  def _get_console_script_distribution(command_name: str | None):
86
86
  if not command_name:
87
87
  return None
88
- import importlib.metadata
88
+ from usecli.shared.config.manager import _find_distribution_for_console_script
89
89
 
90
- try:
91
- distributions = importlib.metadata.distributions()
92
- except Exception:
93
- return None
94
- for dist in distributions:
95
- try:
96
- entry_points = dist.entry_points
97
- except Exception:
98
- continue
99
- for entry_point in entry_points:
100
- if entry_point.group != "console_scripts":
101
- continue
102
- if entry_point.name == command_name:
103
- return dist
104
- return None
90
+ return _find_distribution_for_console_script(command_name)
105
91
 
106
92
 
107
93
  def _get_package_dependencies_from_distribution(dist) -> list[tuple[str, str | None]]:
@@ -175,41 +175,21 @@ def _get_command_name() -> str | None:
175
175
  return command if command else None
176
176
 
177
177
 
178
- _distributions_cache: list[Any] | None = None
179
-
180
-
181
- def _get_distributions() -> list[Any]:
182
- global _distributions_cache
183
- if _distributions_cache is not None:
184
- return _distributions_cache
185
- try:
186
- import importlib.metadata
187
-
188
- _distributions_cache = list(importlib.metadata.distributions())
189
- except Exception:
190
- _distributions_cache = []
191
- return _distributions_cache
192
-
193
-
194
178
  def _get_console_script_aliases(command_name: str | None) -> set[str]:
195
- """Get all aliases for a console script from package metadata."""
179
+ from usecli.shared.config.manager import _find_distribution_for_console_script
180
+
196
181
  if not command_name:
197
182
  return set()
198
183
  aliases: set[str] = {command_name}
199
- distributions = _get_distributions()
200
- for dist in distributions:
184
+ dist = _find_distribution_for_console_script(command_name)
185
+ if dist is not None:
201
186
  try:
202
- entry_points = dist.entry_points
203
- except Exception:
204
- continue
205
- names = [
206
- entry_point.name
207
- for entry_point in entry_points
208
- if entry_point.group == "console_scripts"
209
- ]
210
- if command_name in names:
187
+ names = [
188
+ ep.name for ep in dist.entry_points if ep.group == "console_scripts"
189
+ ]
211
190
  aliases.update(names)
212
- break
191
+ except Exception:
192
+ pass
213
193
  return aliases
214
194
 
215
195
 
@@ -315,36 +295,30 @@ def _find_usecli_config_in_named_package(package_name: str) -> Path | None:
315
295
 
316
296
 
317
297
  def _find_usecli_config_for_console_script() -> Path | None:
298
+ from usecli.shared.config.manager import _find_distribution_for_console_script
299
+
318
300
  command_name = os.path.basename(sys.argv[0]) if sys.argv else ""
319
301
  if not command_name:
320
302
  return None
321
- distributions = _get_distributions()
322
- for dist in distributions:
323
- try:
324
- entry_points = dist.entry_points
325
- except Exception:
326
- continue
327
- for entry_point in entry_points:
328
- if entry_point.group != "console_scripts":
329
- continue
330
- if entry_point.name != command_name:
331
- continue
332
- metadata = dist.metadata
333
- dist_name = ""
334
- if "Name" in metadata:
335
- dist_name = metadata["Name"]
336
- elif "name" in metadata:
337
- dist_name = metadata["name"]
338
- candidates: list[str] = []
339
- if dist_name:
340
- candidates.append(dist_name)
341
- normalized = dist_name.replace("-", "_")
342
- if normalized not in candidates:
343
- candidates.append(normalized)
344
- for package_name in candidates:
345
- match = _find_usecli_config_in_named_package(package_name)
346
- if match:
347
- return match
303
+ dist = _find_distribution_for_console_script(command_name)
304
+ if dist is None:
305
+ return None
306
+ metadata = dist.metadata
307
+ dist_name = ""
308
+ if "Name" in metadata:
309
+ dist_name = metadata["Name"]
310
+ elif "name" in metadata:
311
+ dist_name = metadata["name"]
312
+ candidates: list[str] = []
313
+ if dist_name:
314
+ candidates.append(dist_name)
315
+ normalized = dist_name.replace("-", "_")
316
+ if normalized not in candidates:
317
+ candidates.append(normalized)
318
+ for package_name in candidates:
319
+ match = _find_usecli_config_in_named_package(package_name)
320
+ if match:
321
+ return match
348
322
  return None
349
323
 
350
324
 
@@ -164,6 +164,49 @@ def _reset_distributions_cache() -> None:
164
164
  _distributions_cache = None
165
165
 
166
166
 
167
+ def _find_distribution_for_console_script(
168
+ command_name: str,
169
+ ) -> Any | None:
170
+ """Find the distribution that owns a console_script by name.
171
+
172
+ Uses targeted O(1) lookups before falling back to a full O(N) scan.
173
+ """
174
+ if not command_name:
175
+ return None
176
+
177
+ metadata = _get_importlib_metadata()
178
+
179
+ # Fast path 1: command name directly (e.g. 'gitgepa' → 'git-gepa')
180
+ try:
181
+ dist = metadata.distribution(command_name)
182
+ for ep in dist.entry_points:
183
+ if ep.group == "console_scripts" and ep.name == command_name:
184
+ return dist
185
+ except Exception:
186
+ pass
187
+
188
+ # Fast path 2: current package name (e.g. 'usecli')
189
+ package_name = _get_package_name()
190
+ if package_name and package_name != command_name:
191
+ try:
192
+ dist = metadata.distribution(package_name)
193
+ for ep in dist.entry_points:
194
+ if ep.group == "console_scripts" and ep.name == command_name:
195
+ return dist
196
+ except Exception:
197
+ pass
198
+
199
+ # Slow path: scan all distributions
200
+ for dist in _get_distributions():
201
+ try:
202
+ for ep in dist.entry_points:
203
+ if ep.group == "console_scripts" and ep.name == command_name:
204
+ return dist
205
+ except Exception:
206
+ continue
207
+ return None
208
+
209
+
167
210
  def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
168
211
  result = base.copy()
169
212
  for key, value in override.items():
@@ -543,41 +586,33 @@ class ConfigManager:
543
586
  command_name = os.path.basename(sys.argv[0]) if sys.argv else ""
544
587
  if not command_name:
545
588
  return None
546
- distributions = _get_distributions()
547
- for dist in distributions:
548
- try:
549
- entry_points = dist.entry_points
550
- except Exception:
551
- continue
552
- for entry_point in entry_points:
553
- if entry_point.group != "console_scripts":
554
- continue
555
- if entry_point.name != command_name:
556
- continue
557
- metadata = dist.metadata
558
- dist_name = ""
559
- if "Name" in metadata:
560
- dist_name = metadata["Name"]
561
- elif "name" in metadata:
562
- dist_name = metadata["name"]
563
- candidates = []
564
- if dist_name:
565
- candidates.append(dist_name)
566
- normalized = dist_name.replace("-", "_")
567
- if normalized not in candidates:
568
- candidates.append(normalized)
569
- aliases = cls._get_console_script_aliases(command_name)
570
- for package_name in candidates:
571
- source_root = cls._resolve_editable_source_root(dist)
572
- if source_root:
573
- source_config = cls._search_source_for_config(
574
- source_root, command_name, aliases
575
- )
576
- if source_config:
577
- return source_config
578
- match = cls._find_usecli_config_in_named_package(package_name)
579
- if match:
580
- return match
589
+ dist = _find_distribution_for_console_script(command_name)
590
+ if dist is None:
591
+ return None
592
+ metadata = dist.metadata
593
+ dist_name = ""
594
+ if "Name" in metadata:
595
+ dist_name = metadata["Name"]
596
+ elif "name" in metadata:
597
+ dist_name = metadata["name"]
598
+ candidates = []
599
+ if dist_name:
600
+ candidates.append(dist_name)
601
+ normalized = dist_name.replace("-", "_")
602
+ if normalized not in candidates:
603
+ candidates.append(normalized)
604
+ aliases = cls._get_console_script_aliases(command_name)
605
+ for package_name in candidates:
606
+ source_root = cls._resolve_editable_source_root(dist)
607
+ if source_root:
608
+ source_config = cls._search_source_for_config(
609
+ source_root, command_name, aliases
610
+ )
611
+ if source_config:
612
+ return source_config
613
+ match = cls._find_usecli_config_in_named_package(package_name)
614
+ if match:
615
+ return match
581
616
  return None
582
617
 
583
618
  @staticmethod
@@ -651,20 +686,15 @@ class ConfigManager:
651
686
  if not command_name:
652
687
  return set()
653
688
  aliases: set[str] = {command_name}
654
- distributions = _get_distributions()
655
- for dist in distributions:
689
+ dist = _find_distribution_for_console_script(command_name)
690
+ if dist is not None:
656
691
  try:
657
- entry_points = dist.entry_points
658
- except Exception:
659
- continue
660
- names = [
661
- entry_point.name
662
- for entry_point in entry_points
663
- if entry_point.group == "console_scripts"
664
- ]
665
- if command_name in names:
692
+ names = [
693
+ ep.name for ep in dist.entry_points if ep.group == "console_scripts"
694
+ ]
666
695
  aliases.update(names)
667
- break
696
+ except Exception:
697
+ pass
668
698
  return aliases
669
699
 
670
700
  @staticmethod
@@ -953,22 +983,37 @@ def reset_config() -> None:
953
983
  global _config_manager, _config_cwd
954
984
  _config_manager = None
955
985
  _config_cwd = None
986
+ _config_search_cache.clear()
987
+ _project_root_cache.clear()
988
+
989
+
990
+ def _reset_project_root_cache() -> None:
991
+ _project_root_cache.clear()
992
+
993
+
994
+ _project_root_cache: dict[str, Path | None] = {}
956
995
 
957
996
 
958
997
  def find_project_root(start_dir: Path | None = None) -> Path | None:
959
998
  if start_dir is None:
960
999
  start_dir = _get_path().cwd()
961
1000
 
1001
+ cache_key = str(start_dir.resolve())
1002
+ if cache_key in _project_root_cache:
1003
+ return _project_root_cache[cache_key]
1004
+
962
1005
  current = start_dir.resolve()
963
1006
 
964
1007
  git_root: Path | None = None
965
1008
  while True:
966
1009
  pyproject_path = current / PYPROJECT_TOML
967
1010
  if pyproject_path.exists():
1011
+ _project_root_cache[cache_key] = current
968
1012
  return current
969
1013
 
970
1014
  usecli_path = current / USECLI_CONFIG_TOML
971
1015
  if usecli_path.exists():
1016
+ _project_root_cache[cache_key] = current
972
1017
  return current
973
1018
 
974
1019
  git_dir = current / ".git"
@@ -986,16 +1031,19 @@ def find_project_root(start_dir: Path | None = None) -> Path | None:
986
1031
  # Skip expensive recursive search when search root is a high-level directory.
987
1032
  # Global tools running from HOME or / will never find a project config this way.
988
1033
  if str(search_root) in _get_high_level_dirs():
1034
+ _project_root_cache[cache_key] = git_root
989
1035
  return git_root
990
1036
 
991
1037
  # Try fast lookups before expensive rglob (perf: global tools).
992
1038
  console_match = ConfigManager._find_usecli_config_for_console_script()
993
1039
  if console_match:
1040
+ _project_root_cache[cache_key] = console_match.parent
994
1041
  return console_match.parent
995
1042
 
996
1043
  if ConfigManager._is_within_usecli_package(start_dir):
997
1044
  package_match = ConfigManager._find_usecli_config_in_package()
998
1045
  if package_match:
1046
+ _project_root_cache[cache_key] = package_match.parent
999
1047
  return package_match.parent
1000
1048
 
1001
1049
  config_match = ConfigManager._find_usecli_config_in_tree(
@@ -1004,8 +1052,10 @@ def find_project_root(start_dir: Path | None = None) -> Path | None:
1004
1052
  skip_venv=True,
1005
1053
  )
1006
1054
  if config_match:
1055
+ _project_root_cache[cache_key] = config_match.parent
1007
1056
  return config_match.parent
1008
1057
 
1058
+ _project_root_cache[cache_key] = git_root
1009
1059
  return git_root
1010
1060
 
1011
1061
 
File without changes
File without changes
File without changes
File without changes
File without changes