usecli 0.1.36__tar.gz → 0.1.37__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.36 → usecli-0.1.37}/PKG-INFO +1 -1
  2. {usecli-0.1.36 → usecli-0.1.37}/pyproject.toml +1 -1
  3. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/config/colors.py +78 -15
  4. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/shared/config/manager.py +105 -19
  5. {usecli-0.1.36 → usecli-0.1.37}/LICENSE +0 -0
  6. {usecli-0.1.36 → usecli-0.1.37}/README.md +0 -0
  7. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/__init__.py +0 -0
  8. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/__init__.py +0 -0
  9. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/commands/README.md +0 -0
  10. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/commands/__init__.py +0 -0
  11. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/commands/custom/README.md +0 -0
  12. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/commands/custom/__init__.py +0 -0
  13. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/commands/defaults/__init__.py +0 -0
  14. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
  15. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/commands/defaults/base/about_command.py +0 -0
  16. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/commands/defaults/base/help_command.py +0 -0
  17. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/commands/defaults/base/inspire_command.py +0 -0
  18. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
  19. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +0 -0
  20. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
  21. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/commands/defaults/core/utils.py +0 -0
  22. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
  23. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/commands/defaults/make/make_command.py +0 -0
  24. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/commands/defaults/make/make_theme_command.py +0 -0
  25. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/commands/init_command.py +0 -0
  26. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/config/__init__.py +0 -0
  27. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/core/__init__.py +0 -0
  28. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/core/base_command.py +0 -0
  29. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/core/error/__init__.py +0 -0
  30. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/core/error/handler.py +0 -0
  31. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/core/error/utils.py +0 -0
  32. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/core/exceptions/__init__.py +0 -0
  33. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/core/exceptions/base.py +0 -0
  34. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/core/exceptions/config.py +0 -0
  35. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/core/exceptions/usage.py +0 -0
  36. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/core/exceptions/validation.py +0 -0
  37. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/core/skill_generator.py +0 -0
  38. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/core/ui/__init__.py +0 -0
  39. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/core/ui/list.py +0 -0
  40. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/core/ui/title.py +0 -0
  41. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/core/validators/__init__.py +0 -0
  42. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/core/validators/network.py +0 -0
  43. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/core/validators/numeric.py +0 -0
  44. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/core/validators/path.py +0 -0
  45. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/core/validators/string.py +0 -0
  46. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/services/__init__.py +0 -0
  47. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/services/command_service.py +0 -0
  48. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/templates/command.py.j2 +0 -0
  49. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/templates/theme.toml.j2 +0 -0
  50. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/templates/usecli.config.toml.j2 +0 -0
  51. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/themes/ayu_dark.toml +0 -0
  52. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
  53. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
  54. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
  55. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
  56. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/themes/default.toml +0 -0
  57. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/themes/dracula.toml +0 -0
  58. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
  59. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/themes/nord.toml +0 -0
  60. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/themes/tokyo_night.toml +0 -0
  61. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/utils/__init__.py +0 -0
  62. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/utils/interactive/__init__.py +0 -0
  63. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
  64. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/menu.py +0 -0
  65. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/params.py +0 -0
  66. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/shared/__init__.py +0 -0
  67. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/shared/config/__init__.py +0 -0
  68. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/shared/config/globals.py +0 -0
  69. {usecli-0.1.36 → usecli-0.1.37}/src/usecli/ui.py +0 -0
  70. {usecli-0.1.36/src/usecli/cli → usecli-0.1.37/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.36
3
+ Version: 0.1.37
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.36"
3
+ version = "0.1.37"
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" }]
@@ -11,7 +11,9 @@ Usage:
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
+ import importlib.metadata
14
15
  import importlib.util
16
+ import os
15
17
  import sys
16
18
  from pathlib import Path
17
19
  from typing import Any, Callable, Final, final
@@ -26,7 +28,15 @@ PYPROJECT_TOML = "pyproject.toml"
26
28
  USECLI_CONFIG_TOML = "usecli.config.toml"
27
29
  DEFAULT_THEME_NAME = "default"
28
30
  THEMES_DIR = Path(__file__).resolve().parent.parent / "themes"
29
- _PACKAGE_PREFER_DIRS = {".venv", "venv", "site-packages"}
31
+ _SKIP_DIRS = {
32
+ ".venv",
33
+ "venv",
34
+ "site-packages",
35
+ "dist-packages",
36
+ "__pypackages__",
37
+ "pipx",
38
+ "venvs",
39
+ }
30
40
  DEFAULT_THEME_COLORS: dict[str, str] = {
31
41
  "primary": "#60D7FF",
32
42
  "secondary": "#5EFF87",
@@ -61,7 +71,7 @@ def _find_usecli_config_path(
61
71
  candidates = [
62
72
  path
63
73
  for path in candidates
64
- if not any(part in {".venv", "venv"} for part in path.parts)
74
+ if not any(part in _SKIP_DIRS for part in path.parts)
65
75
  ]
66
76
  if not candidates:
67
77
  return None
@@ -106,8 +116,64 @@ def _find_usecli_config_in_package() -> Path | None:
106
116
  return None
107
117
 
108
118
 
119
+ def _find_usecli_config_in_named_package(package_name: str) -> Path | None:
120
+ if not package_name:
121
+ return None
122
+ spec = importlib.util.find_spec(package_name)
123
+ if spec is None or not spec.submodule_search_locations:
124
+ return None
125
+ for location in spec.submodule_search_locations:
126
+ package_root = Path(location)
127
+ if not package_root.exists() or not package_root.is_dir():
128
+ continue
129
+ candidates = [
130
+ path for path in package_root.rglob(USECLI_CONFIG_TOML) if path.exists()
131
+ ]
132
+ if candidates:
133
+ candidates.sort(key=lambda path: (len(path.parts), str(path)))
134
+ return candidates[0]
135
+ return None
136
+
137
+
138
+ def _find_usecli_config_for_console_script() -> Path | None:
139
+ command_name = os.path.basename(sys.argv[0]) if sys.argv else ""
140
+ if not command_name:
141
+ return None
142
+ try:
143
+ distributions = importlib.metadata.distributions()
144
+ except Exception:
145
+ return None
146
+ for dist in distributions:
147
+ try:
148
+ entry_points = dist.entry_points
149
+ except Exception:
150
+ continue
151
+ for entry_point in entry_points:
152
+ if entry_point.group != "console_scripts":
153
+ continue
154
+ if entry_point.name != command_name:
155
+ continue
156
+ metadata = dist.metadata
157
+ dist_name = ""
158
+ if "Name" in metadata:
159
+ dist_name = metadata["Name"]
160
+ elif "name" in metadata:
161
+ dist_name = metadata["name"]
162
+ candidates: list[str] = []
163
+ if dist_name:
164
+ candidates.append(dist_name)
165
+ normalized = dist_name.replace("-", "_")
166
+ if normalized not in candidates:
167
+ candidates.append(normalized)
168
+ for package_name in candidates:
169
+ match = _find_usecli_config_in_named_package(package_name)
170
+ if match:
171
+ return match
172
+ return None
173
+
174
+
109
175
  def _is_preferred_package_path(path: Path) -> bool:
110
- return any(part in _PACKAGE_PREFER_DIRS for part in path.parts)
176
+ return any(part in _SKIP_DIRS for part in path.parts)
111
177
 
112
178
 
113
179
  def _is_within_usecli_package(start_dir: Path) -> bool:
@@ -132,10 +198,6 @@ def _find_project_root(start_dir: Path | None = None) -> Path | None:
132
198
  current = start_dir.resolve()
133
199
  git_root: Path | None = None
134
200
 
135
- package_match = _find_usecli_config_in_package()
136
- if package_match and _is_preferred_package_path(package_match):
137
- return package_match.parent
138
-
139
201
  while True:
140
202
  pyproject_path = current / PYPROJECT_TOML
141
203
  if pyproject_path.exists():
@@ -156,12 +218,14 @@ def _find_project_root(start_dir: Path | None = None) -> Path | None:
156
218
  current = parent
157
219
 
158
220
  search_root = git_root or start_dir.resolve()
159
- config_match = _find_usecli_config_path(
160
- search_root, start_dir, skip_venv=_is_within_usecli_package(start_dir)
161
- )
221
+ config_match = _find_usecli_config_path(search_root, start_dir, skip_venv=True)
162
222
  if config_match:
163
223
  return config_match.parent
164
224
 
225
+ console_match = _find_usecli_config_for_console_script()
226
+ if console_match:
227
+ return console_match.parent
228
+
165
229
  package_match = _find_usecli_config_in_package()
166
230
  if package_match:
167
231
  return package_match.parent
@@ -173,18 +237,17 @@ def _load_usecli_config(project_root: Path | None) -> dict[str, Any]:
173
237
  if project_root is None:
174
238
  return {}
175
239
 
176
- package_match = _find_usecli_config_in_package()
177
- if package_match and _is_preferred_package_path(package_match):
178
- return _load_usecli_config_file(package_match)
179
-
180
240
  config_path = project_root / USECLI_CONFIG_TOML
181
241
  if not config_path.exists():
182
242
  config_path = _find_usecli_config_path(
183
243
  project_root,
184
244
  project_root,
185
- skip_venv=_is_within_usecli_package(project_root),
245
+ skip_venv=True,
186
246
  )
187
247
  if not config_path or not config_path.exists():
248
+ console_match = _find_usecli_config_for_console_script()
249
+ if console_match:
250
+ return _load_usecli_config_file(console_match)
188
251
  package_match = _find_usecli_config_in_package()
189
252
  if package_match:
190
253
  config_path = package_match
@@ -5,7 +5,9 @@ Handles loading and accessing configuration from project-level files.
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ import importlib.metadata
8
9
  import importlib.util
10
+ import os
9
11
  import sys
10
12
  from pathlib import Path
11
13
  from typing import Any
@@ -59,8 +61,15 @@ def _dedupe_items(items: list[str]) -> list[str]:
59
61
  class ConfigManager:
60
62
  """Manages useCli configuration from project-level files."""
61
63
 
62
- _SKIP_DIRS = {".venv", "venv"}
63
- _PACKAGE_PREFER_DIRS = {".venv", "venv", "site-packages"}
64
+ _SKIP_DIRS = {
65
+ ".venv",
66
+ "venv",
67
+ "site-packages",
68
+ "dist-packages",
69
+ "__pypackages__",
70
+ "pipx",
71
+ "venvs",
72
+ }
64
73
 
65
74
  DEFAULT_CONFIG: dict[str, Any] = {
66
75
  "title": "usecli",
@@ -109,8 +118,25 @@ class ConfigManager:
109
118
  self.usecli_config_path: Path = usecli_config_path
110
119
  self.start_dir: Path = start_dir
111
120
  detected_root = find_project_root(start_dir)
112
- if detected_root is None and self.usecli_config_path.exists():
113
- detected_root = self.usecli_config_path.parent
121
+ if self.usecli_config_path.exists():
122
+ config_parent = self.usecli_config_path.parent
123
+ if detected_root is None:
124
+ detected_root = config_parent
125
+ else:
126
+ root_config = detected_root / USECLI_CONFIG_TOML
127
+ if self.usecli_config_path.resolve() != root_config.resolve():
128
+ detected_root = config_parent
129
+ else:
130
+ try:
131
+ self.usecli_config_path.relative_to(detected_root)
132
+ except ValueError:
133
+ detected_root = config_parent
134
+ else:
135
+ if any(
136
+ part in self._SKIP_DIRS
137
+ for part in self.usecli_config_path.parts
138
+ ):
139
+ detected_root = config_parent
114
140
  self.project_root: Path = (detected_root or start_dir).resolve()
115
141
  self._config: dict[str, Any] = {}
116
142
  self._overrides: dict[str, Any] = {}
@@ -170,10 +196,6 @@ class ConfigManager:
170
196
  def _find_usecli_config(cls, start_dir: Path) -> Path | None:
171
197
  current = start_dir.resolve()
172
198
 
173
- package_match = cls._find_usecli_config_in_package()
174
- if package_match and cls._is_preferred_package_path(package_match):
175
- return package_match
176
-
177
199
  while True:
178
200
  config_path = current / USECLI_CONFIG_TOML
179
201
  if config_path.exists():
@@ -184,17 +206,24 @@ class ConfigManager:
184
206
  break
185
207
  current = parent
186
208
 
187
- in_usecli_package = cls._is_within_usecli_package(start_dir)
188
- if in_usecli_package and package_match:
189
- return package_match
190
-
191
209
  search_root = find_project_root(start_dir) or start_dir.resolve()
192
210
  recursive_match = cls._find_usecli_config_in_tree(
193
- search_root, start_dir, skip_venv=in_usecli_package
211
+ search_root, start_dir, skip_venv=True
194
212
  )
195
213
  if recursive_match:
196
214
  return recursive_match
197
215
 
216
+ console_match = cls._find_usecli_config_for_console_script()
217
+ if console_match:
218
+ return console_match
219
+
220
+ if not cls._is_within_usecli_package(start_dir):
221
+ return None
222
+
223
+ package_match = cls._find_usecli_config_in_package()
224
+ if package_match:
225
+ return package_match
226
+
198
227
  return cls._find_usecli_config_on_sys_path()
199
228
 
200
229
  @staticmethod
@@ -253,9 +282,65 @@ class ConfigManager:
253
282
  return candidates[0]
254
283
  return None
255
284
 
285
+ @classmethod
286
+ def _find_usecli_config_in_named_package(cls, package_name: str) -> Path | None:
287
+ if not package_name:
288
+ return None
289
+ spec = importlib.util.find_spec(package_name)
290
+ if spec is None or not spec.submodule_search_locations:
291
+ return None
292
+ for location in spec.submodule_search_locations:
293
+ package_root = Path(location)
294
+ if not package_root.exists() or not package_root.is_dir():
295
+ continue
296
+ candidates = [
297
+ path for path in package_root.rglob(USECLI_CONFIG_TOML) if path.exists()
298
+ ]
299
+ if candidates:
300
+ candidates.sort(key=lambda path: (len(path.parts), str(path)))
301
+ return candidates[0]
302
+ return None
303
+
304
+ @classmethod
305
+ def _find_usecli_config_for_console_script(cls) -> Path | None:
306
+ command_name = os.path.basename(sys.argv[0]) if sys.argv else ""
307
+ if not command_name:
308
+ return None
309
+ try:
310
+ distributions = importlib.metadata.distributions()
311
+ except Exception:
312
+ return None
313
+ for dist in distributions:
314
+ try:
315
+ entry_points = dist.entry_points
316
+ except Exception:
317
+ continue
318
+ for entry_point in entry_points:
319
+ if entry_point.group != "console_scripts":
320
+ continue
321
+ if entry_point.name != command_name:
322
+ continue
323
+ metadata = dist.metadata
324
+ dist_name = ""
325
+ if "Name" in metadata:
326
+ dist_name = metadata["Name"]
327
+ elif "name" in metadata:
328
+ dist_name = metadata["name"]
329
+ candidates = []
330
+ if dist_name:
331
+ candidates.append(dist_name)
332
+ normalized = dist_name.replace("-", "_")
333
+ if normalized not in candidates:
334
+ candidates.append(normalized)
335
+ for package_name in candidates:
336
+ match = cls._find_usecli_config_in_named_package(package_name)
337
+ if match:
338
+ return match
339
+ return None
340
+
256
341
  @staticmethod
257
342
  def _is_preferred_package_path(path: Path) -> bool:
258
- return any(part in ConfigManager._PACKAGE_PREFER_DIRS for part in path.parts)
343
+ return any(part in ConfigManager._SKIP_DIRS for part in path.parts)
259
344
 
260
345
  @staticmethod
261
346
  def _is_within_usecli_package(start_dir: Path) -> bool:
@@ -433,10 +518,6 @@ def find_project_root(start_dir: Path | None = None) -> Path | None:
433
518
 
434
519
  current = start_dir.resolve()
435
520
 
436
- package_match = ConfigManager._find_usecli_config_in_package()
437
- if package_match and ConfigManager._is_preferred_package_path(package_match):
438
- return package_match.parent
439
-
440
521
  git_root: Path | None = None
441
522
  while True:
442
523
  pyproject_path = current / PYPROJECT_TOML
@@ -461,11 +542,16 @@ def find_project_root(start_dir: Path | None = None) -> Path | None:
461
542
  config_match = ConfigManager._find_usecli_config_in_tree(
462
543
  search_root,
463
544
  start_dir,
464
- skip_venv=ConfigManager._is_within_usecli_package(start_dir),
545
+ skip_venv=True,
465
546
  )
466
547
  if config_match:
467
548
  return config_match.parent
468
549
 
550
+ if ConfigManager._is_within_usecli_package(start_dir):
551
+ package_match = ConfigManager._find_usecli_config_in_package()
552
+ if package_match:
553
+ return package_match.parent
554
+
469
555
  return git_root
470
556
 
471
557
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes