usecli 0.1.35__tar.gz → 0.1.36__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.35 → usecli-0.1.36}/PKG-INFO +1 -1
  2. {usecli-0.1.35 → usecli-0.1.36}/pyproject.toml +1 -1
  3. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/config/colors.py +83 -8
  4. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/shared/config/manager.py +78 -8
  5. {usecli-0.1.35 → usecli-0.1.36}/LICENSE +0 -0
  6. {usecli-0.1.35 → usecli-0.1.36}/README.md +0 -0
  7. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/__init__.py +0 -0
  8. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/__init__.py +0 -0
  9. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/commands/README.md +0 -0
  10. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/commands/__init__.py +0 -0
  11. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/commands/custom/README.md +0 -0
  12. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/commands/custom/__init__.py +0 -0
  13. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/commands/defaults/__init__.py +0 -0
  14. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
  15. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/commands/defaults/base/about_command.py +0 -0
  16. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/commands/defaults/base/help_command.py +0 -0
  17. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/commands/defaults/base/inspire_command.py +0 -0
  18. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
  19. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +0 -0
  20. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
  21. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/commands/defaults/core/utils.py +0 -0
  22. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
  23. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/commands/defaults/make/make_command.py +0 -0
  24. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/commands/defaults/make/make_theme_command.py +0 -0
  25. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/commands/init_command.py +0 -0
  26. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/config/__init__.py +0 -0
  27. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/core/__init__.py +0 -0
  28. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/core/base_command.py +0 -0
  29. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/core/error/__init__.py +0 -0
  30. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/core/error/handler.py +0 -0
  31. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/core/error/utils.py +0 -0
  32. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/core/exceptions/__init__.py +0 -0
  33. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/core/exceptions/base.py +0 -0
  34. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/core/exceptions/config.py +0 -0
  35. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/core/exceptions/usage.py +0 -0
  36. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/core/exceptions/validation.py +0 -0
  37. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/core/skill_generator.py +0 -0
  38. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/core/ui/__init__.py +0 -0
  39. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/core/ui/list.py +0 -0
  40. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/core/ui/title.py +0 -0
  41. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/core/validators/__init__.py +0 -0
  42. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/core/validators/network.py +0 -0
  43. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/core/validators/numeric.py +0 -0
  44. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/core/validators/path.py +0 -0
  45. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/core/validators/string.py +0 -0
  46. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/services/__init__.py +0 -0
  47. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/services/command_service.py +0 -0
  48. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/templates/command.py.j2 +0 -0
  49. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/templates/theme.toml.j2 +0 -0
  50. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/templates/usecli.config.toml.j2 +0 -0
  51. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/themes/ayu_dark.toml +0 -0
  52. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
  53. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
  54. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
  55. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
  56. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/themes/default.toml +0 -0
  57. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/themes/dracula.toml +0 -0
  58. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
  59. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/themes/nord.toml +0 -0
  60. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/themes/tokyo_night.toml +0 -0
  61. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/usecli.config.toml +0 -0
  62. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/utils/__init__.py +0 -0
  63. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/utils/interactive/__init__.py +0 -0
  64. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
  65. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/menu.py +0 -0
  66. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/params.py +0 -0
  67. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/shared/__init__.py +0 -0
  68. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/shared/config/__init__.py +0 -0
  69. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/shared/config/globals.py +0 -0
  70. {usecli-0.1.35 → usecli-0.1.36}/src/usecli/ui.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usecli
3
- Version: 0.1.35
3
+ Version: 0.1.36
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.35"
3
+ version = "0.1.36"
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,6 +11,7 @@ Usage:
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
+ import importlib.util
14
15
  import sys
15
16
  from pathlib import Path
16
17
  from typing import Any, Callable, Final, final
@@ -25,6 +26,7 @@ PYPROJECT_TOML = "pyproject.toml"
25
26
  USECLI_CONFIG_TOML = "usecli.config.toml"
26
27
  DEFAULT_THEME_NAME = "default"
27
28
  THEMES_DIR = Path(__file__).resolve().parent.parent / "themes"
29
+ _PACKAGE_PREFER_DIRS = {".venv", "venv", "site-packages"}
28
30
  DEFAULT_THEME_COLORS: dict[str, str] = {
29
31
  "primary": "#60D7FF",
30
32
  "secondary": "#5EFF87",
@@ -48,15 +50,19 @@ DEFAULT_THEME_COLORS: dict[str, str] = {
48
50
  }
49
51
 
50
52
 
51
- def _find_usecli_config_path(root_dir: Path, start_dir: Path) -> Path | None:
53
+ def _find_usecli_config_path(
54
+ root_dir: Path, start_dir: Path, *, skip_venv: bool
55
+ ) -> Path | None:
52
56
  if not root_dir.exists() or not root_dir.is_dir():
53
57
  return None
54
58
 
55
- candidates = [
56
- path
57
- for path in root_dir.rglob(USECLI_CONFIG_TOML)
58
- if not any(part in {".venv", "venv"} for part in path.parts)
59
- ]
59
+ candidates = [path for path in root_dir.rglob(USECLI_CONFIG_TOML)]
60
+ if skip_venv:
61
+ candidates = [
62
+ path
63
+ for path in candidates
64
+ if not any(part in {".venv", "venv"} for part in path.parts)
65
+ ]
60
66
  if not candidates:
61
67
  return None
62
68
 
@@ -83,6 +89,42 @@ def _find_usecli_config_path(root_dir: Path, start_dir: Path) -> Path | None:
83
89
  return selection[0]
84
90
 
85
91
 
92
+ def _find_usecli_config_in_package() -> Path | None:
93
+ spec = importlib.util.find_spec(_get_package_name())
94
+ if spec is None or not spec.submodule_search_locations:
95
+ return None
96
+ for location in spec.submodule_search_locations:
97
+ package_root = Path(location)
98
+ if not package_root.exists() or not package_root.is_dir():
99
+ continue
100
+ candidates = [
101
+ path for path in package_root.rglob(USECLI_CONFIG_TOML) if path.exists()
102
+ ]
103
+ if candidates:
104
+ candidates.sort(key=lambda path: (len(path.parts), str(path)))
105
+ return candidates[0]
106
+ return None
107
+
108
+
109
+ def _is_preferred_package_path(path: Path) -> bool:
110
+ return any(part in _PACKAGE_PREFER_DIRS for part in path.parts)
111
+
112
+
113
+ def _is_within_usecli_package(start_dir: Path) -> bool:
114
+ spec = importlib.util.find_spec(_get_package_name())
115
+ if spec is None or not spec.submodule_search_locations:
116
+ return False
117
+ start_dir = start_dir.resolve()
118
+ for location in spec.submodule_search_locations:
119
+ package_root = Path(location)
120
+ try:
121
+ start_dir.relative_to(package_root)
122
+ return True
123
+ except ValueError:
124
+ continue
125
+ return False
126
+
127
+
86
128
  def _find_project_root(start_dir: Path | None = None) -> Path | None:
87
129
  if start_dir is None:
88
130
  start_dir = Path.cwd()
@@ -90,6 +132,10 @@ def _find_project_root(start_dir: Path | None = None) -> Path | None:
90
132
  current = start_dir.resolve()
91
133
  git_root: Path | None = None
92
134
 
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
+
93
139
  while True:
94
140
  pyproject_path = current / PYPROJECT_TOML
95
141
  if pyproject_path.exists():
@@ -110,10 +156,16 @@ def _find_project_root(start_dir: Path | None = None) -> Path | None:
110
156
  current = parent
111
157
 
112
158
  search_root = git_root or start_dir.resolve()
113
- config_match = _find_usecli_config_path(search_root, start_dir)
159
+ config_match = _find_usecli_config_path(
160
+ search_root, start_dir, skip_venv=_is_within_usecli_package(start_dir)
161
+ )
114
162
  if config_match:
115
163
  return config_match.parent
116
164
 
165
+ package_match = _find_usecli_config_in_package()
166
+ if package_match:
167
+ return package_match.parent
168
+
117
169
  return git_root
118
170
 
119
171
 
@@ -121,12 +173,28 @@ def _load_usecli_config(project_root: Path | None) -> dict[str, Any]:
121
173
  if project_root is None:
122
174
  return {}
123
175
 
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
+
124
180
  config_path = project_root / USECLI_CONFIG_TOML
125
181
  if not config_path.exists():
126
- config_path = _find_usecli_config_path(project_root, project_root)
182
+ config_path = _find_usecli_config_path(
183
+ project_root,
184
+ project_root,
185
+ skip_venv=_is_within_usecli_package(project_root),
186
+ )
187
+ if not config_path or not config_path.exists():
188
+ package_match = _find_usecli_config_in_package()
189
+ if package_match:
190
+ config_path = package_match
127
191
  if not config_path or not config_path.exists():
128
192
  return {}
129
193
 
194
+ return _load_usecli_config_file(config_path)
195
+
196
+
197
+ def _load_usecli_config_file(config_path: Path) -> dict[str, Any]:
130
198
  try:
131
199
  data = tomllib.loads(config_path.read_text())
132
200
  except (tomllib.TOMLDecodeError, OSError):
@@ -145,6 +213,13 @@ def _load_usecli_config(project_root: Path | None) -> dict[str, Any]:
145
213
  return {}
146
214
 
147
215
 
216
+ def _get_package_name() -> str:
217
+ package = __package__ or __name__
218
+ if not package:
219
+ return "usecli"
220
+ return package.split(".")[0]
221
+
222
+
148
223
  def _normalize_color(value: Any) -> str | None:
149
224
  if not isinstance(value, str):
150
225
  return None
@@ -5,6 +5,7 @@ Handles loading and accessing configuration from project-level files.
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ import importlib.util
8
9
  import sys
9
10
  from pathlib import Path
10
11
  from typing import Any
@@ -59,6 +60,7 @@ class ConfigManager:
59
60
  """Manages useCli configuration from project-level files."""
60
61
 
61
62
  _SKIP_DIRS = {".venv", "venv"}
63
+ _PACKAGE_PREFER_DIRS = {".venv", "venv", "site-packages"}
62
64
 
63
65
  DEFAULT_CONFIG: dict[str, Any] = {
64
66
  "title": "usecli",
@@ -168,6 +170,10 @@ class ConfigManager:
168
170
  def _find_usecli_config(cls, start_dir: Path) -> Path | None:
169
171
  current = start_dir.resolve()
170
172
 
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
+
171
177
  while True:
172
178
  config_path = current / USECLI_CONFIG_TOML
173
179
  if config_path.exists():
@@ -178,23 +184,33 @@ class ConfigManager:
178
184
  break
179
185
  current = parent
180
186
 
187
+ in_usecli_package = cls._is_within_usecli_package(start_dir)
188
+ if in_usecli_package and package_match:
189
+ return package_match
190
+
181
191
  search_root = find_project_root(start_dir) or start_dir.resolve()
182
- recursive_match = cls._find_usecli_config_in_tree(search_root, start_dir)
192
+ recursive_match = cls._find_usecli_config_in_tree(
193
+ search_root, start_dir, skip_venv=in_usecli_package
194
+ )
183
195
  if recursive_match:
184
196
  return recursive_match
185
197
 
186
198
  return cls._find_usecli_config_on_sys_path()
187
199
 
188
200
  @staticmethod
189
- def _find_usecli_config_in_tree(root_dir: Path, start_dir: Path) -> Path | None:
201
+ def _find_usecli_config_in_tree(
202
+ root_dir: Path, start_dir: Path, *, skip_venv: bool
203
+ ) -> Path | None:
190
204
  if not root_dir.exists() or not root_dir.is_dir():
191
205
  return None
192
206
 
193
- candidates = [
194
- path
195
- for path in root_dir.rglob(USECLI_CONFIG_TOML)
196
- if not any(part in ConfigManager._SKIP_DIRS for part in path.parts)
197
- ]
207
+ candidates = [path for path in root_dir.rglob(USECLI_CONFIG_TOML)]
208
+ if skip_venv:
209
+ candidates = [
210
+ path
211
+ for path in candidates
212
+ if not any(part in ConfigManager._SKIP_DIRS for part in path.parts)
213
+ ]
198
214
  if not candidates:
199
215
  return None
200
216
 
@@ -220,6 +236,42 @@ class ConfigManager:
220
236
  selection.sort(key=_depth_key)
221
237
  return selection[0]
222
238
 
239
+ @staticmethod
240
+ def _find_usecli_config_in_package() -> Path | None:
241
+ spec = importlib.util.find_spec(_get_package_name())
242
+ if spec is None or not spec.submodule_search_locations:
243
+ return None
244
+ for location in spec.submodule_search_locations:
245
+ package_root = Path(location)
246
+ if not package_root.exists() or not package_root.is_dir():
247
+ continue
248
+ candidates = [
249
+ path for path in package_root.rglob(USECLI_CONFIG_TOML) if path.exists()
250
+ ]
251
+ if candidates:
252
+ candidates.sort(key=lambda path: (len(path.parts), str(path)))
253
+ return candidates[0]
254
+ return None
255
+
256
+ @staticmethod
257
+ def _is_preferred_package_path(path: Path) -> bool:
258
+ return any(part in ConfigManager._PACKAGE_PREFER_DIRS for part in path.parts)
259
+
260
+ @staticmethod
261
+ def _is_within_usecli_package(start_dir: Path) -> bool:
262
+ spec = importlib.util.find_spec(_get_package_name())
263
+ if spec is None or not spec.submodule_search_locations:
264
+ return False
265
+ start_dir = start_dir.resolve()
266
+ for location in spec.submodule_search_locations:
267
+ package_root = Path(location)
268
+ try:
269
+ start_dir.relative_to(package_root)
270
+ return True
271
+ except ValueError:
272
+ continue
273
+ return False
274
+
223
275
  @staticmethod
224
276
  def _find_usecli_config_on_sys_path() -> Path | None:
225
277
  for entry in sys.path:
@@ -328,6 +380,9 @@ class ConfigManager:
328
380
 
329
381
  def reload(self) -> None:
330
382
  """Reload configuration from disk."""
383
+ self.usecli_config_path = self._find_usecli_config(self.start_dir) or (
384
+ self.start_dir / USECLI_CONFIG_TOML
385
+ )
331
386
  self._load_config()
332
387
 
333
388
  @property
@@ -378,6 +433,10 @@ def find_project_root(start_dir: Path | None = None) -> Path | None:
378
433
 
379
434
  current = start_dir.resolve()
380
435
 
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
+
381
440
  git_root: Path | None = None
382
441
  while True:
383
442
  pyproject_path = current / PYPROJECT_TOML
@@ -399,8 +458,19 @@ def find_project_root(start_dir: Path | None = None) -> Path | None:
399
458
  current = parent
400
459
 
401
460
  search_root = git_root or start_dir.resolve()
402
- config_match = ConfigManager._find_usecli_config_in_tree(search_root, start_dir)
461
+ config_match = ConfigManager._find_usecli_config_in_tree(
462
+ search_root,
463
+ start_dir,
464
+ skip_venv=ConfigManager._is_within_usecli_package(start_dir),
465
+ )
403
466
  if config_match:
404
467
  return config_match.parent
405
468
 
406
469
  return git_root
470
+
471
+
472
+ def _get_package_name() -> str:
473
+ package = __package__ or __name__
474
+ if not package:
475
+ return "usecli"
476
+ return package.split(".")[0]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes