usecli 0.1.54__tar.gz → 0.1.56__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.54 → usecli-0.1.56}/PKG-INFO +1 -1
  2. {usecli-0.1.54 → usecli-0.1.56}/pyproject.toml +1 -1
  3. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/__init__.py +45 -7
  4. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/core/base_command.py +1 -37
  5. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/shared/config/manager.py +110 -5
  6. {usecli-0.1.54 → usecli-0.1.56}/LICENSE +0 -0
  7. {usecli-0.1.54 → usecli-0.1.56}/README.md +0 -0
  8. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/__init__.py +0 -0
  9. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/commands/README.md +0 -0
  10. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/commands/__init__.py +0 -0
  11. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/commands/custom/README.md +0 -0
  12. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/commands/custom/__init__.py +0 -0
  13. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/commands/defaults/__init__.py +0 -0
  14. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
  15. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/commands/defaults/base/about_command.py +0 -0
  16. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/commands/defaults/base/help_command.py +0 -0
  17. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/commands/defaults/base/inspire_command.py +0 -0
  18. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
  19. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +0 -0
  20. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
  21. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/commands/defaults/core/utils.py +0 -0
  22. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
  23. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/commands/defaults/make/make_command.py +0 -0
  24. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/commands/defaults/make/make_theme_command.py +0 -0
  25. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/commands/init_command.py +0 -0
  26. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/config/__init__.py +0 -0
  27. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/config/colors.py +0 -0
  28. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/core/__init__.py +0 -0
  29. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/core/error/__init__.py +0 -0
  30. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/core/error/handler.py +0 -0
  31. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/core/error/utils.py +0 -0
  32. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/core/exceptions/__init__.py +0 -0
  33. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/core/exceptions/base.py +0 -0
  34. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/core/exceptions/config.py +0 -0
  35. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/core/exceptions/usage.py +0 -0
  36. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/core/exceptions/validation.py +0 -0
  37. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/core/ui/__init__.py +0 -0
  38. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/core/ui/list.py +0 -0
  39. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/core/ui/title.py +0 -0
  40. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/core/ui/title.txt +0 -0
  41. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/core/validators/__init__.py +0 -0
  42. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/core/validators/network.py +0 -0
  43. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/core/validators/numeric.py +0 -0
  44. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/core/validators/path.py +0 -0
  45. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/core/validators/string.py +0 -0
  46. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/services/__init__.py +0 -0
  47. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/services/command_service.py +0 -0
  48. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/templates/command.py.j2 +0 -0
  49. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/templates/theme.toml.j2 +0 -0
  50. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/templates/usecli.config.toml.j2 +0 -0
  51. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/themes/ayu_dark.toml +0 -0
  52. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
  53. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
  54. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
  55. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
  56. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/themes/default.toml +0 -0
  57. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/themes/dracula.toml +0 -0
  58. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
  59. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/themes/nord.toml +0 -0
  60. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/themes/tokyo_night.toml +0 -0
  61. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/utils/__init__.py +0 -0
  62. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/utils/interactive/__init__.py +0 -0
  63. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
  64. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/menu.py +0 -0
  65. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/params.py +0 -0
  66. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/shared/__init__.py +0 -0
  67. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/shared/config/__init__.py +0 -0
  68. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/shared/config/globals.py +0 -0
  69. {usecli-0.1.54 → usecli-0.1.56}/src/usecli/ui.py +0 -0
  70. {usecli-0.1.54 → usecli-0.1.56}/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.54
3
+ Version: 0.1.56
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.54"
3
+ version = "0.1.56"
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,14 +2,25 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import shutil
5
6
  import sys
6
7
  from importlib import import_module
8
+ from typing import Any, Optional, Sequence
7
9
 
8
10
  import click
9
11
  import typer
10
12
  from click.exceptions import BadParameter, ClickException, Exit, UsageError
11
13
  from typer.core import TyperGroup
12
14
 
15
+ try:
16
+ from typer._click.exceptions import BadParameter as TyperBadParameter # type: ignore[import-untyped]
17
+ from typer._click.exceptions import ClickException as TyperClickException # type: ignore[import-untyped]
18
+ from typer._click.exceptions import UsageError as TyperUsageError # type: ignore[import-untyped]
19
+ except ImportError:
20
+ TyperBadParameter = BadParameter
21
+ TyperClickException = ClickException
22
+ TyperUsageError = UsageError
23
+
13
24
  from usecli.cli.config.colors import COLOR
14
25
  from usecli.cli.core.base_command import BaseCommand
15
26
  from usecli.cli.core.exceptions import UsecliBadParameter, UsecliUsageError
@@ -156,6 +167,31 @@ class PrefixMatchingGroup(TyperGroup):
156
167
 
157
168
  return FilteredListCommand(cmd_name)
158
169
 
170
+ def main(
171
+ self,
172
+ args: Optional[Sequence[str]] = None,
173
+ prog_name: Optional[str] = None,
174
+ complete_var: Optional[str] = None,
175
+ standalone_mode: bool = True,
176
+ windows_expand_args: bool = True,
177
+ **extra: Any,
178
+ ) -> Any:
179
+ """Override main to disable standalone mode.
180
+
181
+ Click's default standalone_mode=True catches ClickException
182
+ internally and calls sys.exit(), preventing our custom error
183
+ handlers from running. Setting standalone_mode=False lets
184
+ exceptions propagate to our styled error handlers in invoke().
185
+ """
186
+ return super().main(
187
+ args=args,
188
+ prog_name=prog_name,
189
+ complete_var=complete_var,
190
+ standalone_mode=False,
191
+ windows_expand_args=windows_expand_args,
192
+ **extra,
193
+ )
194
+
159
195
  def invoke(self, ctx: click.Context) -> None:
160
196
  """Invoke the group with custom error handling.
161
197
 
@@ -169,15 +205,15 @@ class PrefixMatchingGroup(TyperGroup):
169
205
  return super().invoke(ctx)
170
206
  except Exit:
171
207
  sys.exit(0)
172
- except BadParameter as e:
208
+ except (BadParameter, TyperBadParameter) as e:
173
209
  styled_error = UsecliBadParameter(e.message, ctx=e.ctx, param=e.param)
174
210
  styled_error.show()
175
211
  sys.exit(styled_error.exit_code)
176
- except UsageError as e:
212
+ except (UsageError, TyperUsageError) as e:
177
213
  styled_error = UsecliUsageError(e.message, ctx=e.ctx)
178
214
  styled_error.show()
179
215
  sys.exit(styled_error.exit_code)
180
- except ClickException as e:
216
+ except (ClickException, TyperClickException) as e:
181
217
  if hasattr(e, "show"):
182
218
  e.show()
183
219
  sys.exit(e.exit_code if hasattr(e, "exit_code") else 1)
@@ -247,8 +283,10 @@ def run_app(
247
283
  raise typer.Exit()
248
284
 
249
285
  if version:
286
+ config = get_config()
287
+ command_path = shutil.which(sys.argv[0]) or sys.argv[0]
250
288
  console.print(
251
- f"[bold blue]CLI Version:[/bold blue] [green]{service.version}[/green]"
289
+ f"[bold {theme.SECONDARY}]{config.get('title')} {service.version}[/bold {theme.SECONDARY}] [{theme.INFO}]({command_path})[/{theme.INFO}]"
252
290
  )
253
291
  raise typer.Exit()
254
292
 
@@ -276,15 +314,15 @@ def main() -> None:
276
314
  app()
277
315
  except Exit:
278
316
  sys.exit(0)
279
- except BadParameter as e:
317
+ except (BadParameter, TyperBadParameter) as e:
280
318
  styled_error = UsecliBadParameter(e.message, ctx=e.ctx, param=e.param)
281
319
  styled_error.show()
282
320
  sys.exit(styled_error.exit_code)
283
- except UsageError as e:
321
+ except (UsageError, TyperUsageError) as e:
284
322
  styled_error = UsecliUsageError(e.message, ctx=e.ctx)
285
323
  styled_error.show()
286
324
  sys.exit(styled_error.exit_code)
287
- except ClickException as e:
325
+ except (ClickException, TyperClickException) as e:
288
326
  if hasattr(e, "show"):
289
327
  e.show()
290
328
  sys.exit(e.exit_code if hasattr(e, "exit_code") else 1)
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from abc import ABC, abstractmethod
6
- from typing import TYPE_CHECKING, Any, ClassVar, Optional, Sequence
6
+ from typing import TYPE_CHECKING, Any, ClassVar
7
7
 
8
8
  import typer
9
9
  from click.exceptions import Exit
@@ -192,42 +192,6 @@ class CustomHelpCommand(TyperCommand):
192
192
  for param in getattr(self, "params", [])
193
193
  )
194
194
 
195
- def main(
196
- self,
197
- args: Optional[Sequence[str]] = None,
198
- prog_name: Optional[str] = None,
199
- complete_var: Optional[str] = None,
200
- standalone_mode: bool = True,
201
- windows_expand_args: bool = True,
202
- **extra: Any,
203
- ) -> Any:
204
- """Override main to disable standalone mode.
205
-
206
- Click's default standalone_mode=True catches ClickException
207
- internally and calls sys.exit(), preventing our custom error
208
- handlers from running. Setting standalone_mode=False lets
209
- exceptions propagate to our styled error handlers.
210
-
211
- Args:
212
- args: Command-line arguments.
213
- prog_name: Program name for help display.
214
- complete_var: Shell completion variable name.
215
- standalone_mode: Must be False for custom error handling.
216
- windows_expand_args: Windows argument expansion flag.
217
- **extra: Additional context arguments.
218
-
219
- Returns:
220
- The command return value.
221
- """
222
- return super().main(
223
- args=args,
224
- prog_name=prog_name,
225
- complete_var=complete_var,
226
- standalone_mode=False,
227
- windows_expand_args=windows_expand_args,
228
- **extra,
229
- )
230
-
231
195
  def invoke(self, ctx: ClickContext) -> Any:
232
196
  interactive = ctx.params.pop("interactive", False)
233
197
  if interactive:
@@ -7,6 +7,7 @@ from __future__ import annotations
7
7
 
8
8
  import importlib.metadata
9
9
  import importlib.util
10
+ import json
10
11
  import os
11
12
  import sys
12
13
  from pathlib import Path
@@ -138,6 +139,8 @@ class ConfigManager:
138
139
  ):
139
140
  detected_root = config_parent
140
141
  self.project_root: Path = (detected_root or start_dir).resolve()
142
+ if self._is_in_venv(self.project_root):
143
+ self.project_root = start_dir.resolve()
141
144
  self._config: dict[str, Any] = {}
142
145
  self._overrides: dict[str, Any] = {}
143
146
  self._load_config()
@@ -279,9 +282,26 @@ class ConfigManager:
279
282
 
280
283
  @staticmethod
281
284
  def _find_usecli_config_in_package() -> Path | None:
282
- spec = importlib.util.find_spec(_get_package_name())
285
+ package_name = _get_package_name()
286
+ spec = importlib.util.find_spec(package_name)
283
287
  if spec is None or not spec.submodule_search_locations:
284
288
  return None
289
+
290
+ command_name = ConfigManager._get_command_name()
291
+ aliases = ConfigManager._get_console_script_aliases(command_name)
292
+
293
+ try:
294
+ dist = importlib.metadata.distribution(package_name)
295
+ source_root = ConfigManager._resolve_editable_source_root(dist)
296
+ if source_root:
297
+ source_config = ConfigManager._search_source_for_config(
298
+ source_root, command_name, aliases
299
+ )
300
+ if source_config:
301
+ return source_config
302
+ except Exception:
303
+ pass
304
+
285
305
  for location in spec.submodule_search_locations:
286
306
  package_root = Path(location)
287
307
  if not package_root.exists() or not package_root.is_dir():
@@ -289,8 +309,6 @@ class ConfigManager:
289
309
  candidates = [
290
310
  path for path in package_root.rglob(USECLI_CONFIG_TOML) if path.exists()
291
311
  ]
292
- command_name = ConfigManager._get_command_name()
293
- aliases = ConfigManager._get_console_script_aliases(command_name)
294
312
  if command_name:
295
313
  candidates = [
296
314
  path
@@ -311,6 +329,22 @@ class ConfigManager:
311
329
  spec = importlib.util.find_spec(package_name)
312
330
  if spec is None or not spec.submodule_search_locations:
313
331
  return None
332
+
333
+ command_name = cls._get_command_name()
334
+ aliases = cls._get_console_script_aliases(command_name)
335
+
336
+ try:
337
+ dist = importlib.metadata.distribution(package_name)
338
+ source_root = cls._resolve_editable_source_root(dist)
339
+ if source_root:
340
+ source_config = cls._search_source_for_config(
341
+ source_root, command_name, aliases
342
+ )
343
+ if source_config:
344
+ return source_config
345
+ except Exception:
346
+ pass
347
+
314
348
  for location in spec.submodule_search_locations:
315
349
  package_root = Path(location)
316
350
  if not package_root.exists() or not package_root.is_dir():
@@ -318,8 +352,6 @@ class ConfigManager:
318
352
  candidates = [
319
353
  path for path in package_root.rglob(USECLI_CONFIG_TOML) if path.exists()
320
354
  ]
321
- command_name = cls._get_command_name()
322
- aliases = cls._get_console_script_aliases(command_name)
323
355
  if command_name:
324
356
  candidates = [
325
357
  path
@@ -362,7 +394,15 @@ class ConfigManager:
362
394
  normalized = dist_name.replace("-", "_")
363
395
  if normalized not in candidates:
364
396
  candidates.append(normalized)
397
+ aliases = cls._get_console_script_aliases(command_name)
365
398
  for package_name in candidates:
399
+ source_root = cls._resolve_editable_source_root(dist)
400
+ if source_root:
401
+ source_config = cls._search_source_for_config(
402
+ source_root, command_name, aliases
403
+ )
404
+ if source_config:
405
+ return source_config
366
406
  match = cls._find_usecli_config_in_named_package(package_name)
367
407
  if match:
368
408
  return match
@@ -471,6 +511,71 @@ class ConfigManager:
471
511
  aliases = {command_name}
472
512
  return normalized in aliases
473
513
 
514
+ @staticmethod
515
+ def _is_in_venv(path: Path) -> bool:
516
+ resolved = path.resolve()
517
+ return any(part in ConfigManager._SKIP_DIRS for part in resolved.parts)
518
+
519
+ @staticmethod
520
+ def _resolve_editable_source_root(
521
+ dist: importlib.metadata.Distribution,
522
+ ) -> Path | None:
523
+ """Resolve the source directory for an editable-installed package.
524
+
525
+ Reads ``direct_url.json`` from the distribution's metadata to find the
526
+ local source tree. Returns the source root or ``None`` when the
527
+ distribution is not an editable install or the source no longer exists.
528
+ """
529
+ try:
530
+ text = dist.read_text("direct_url.json")
531
+ except Exception:
532
+ return None
533
+ if not text:
534
+ return None
535
+ try:
536
+ data = json.loads(text)
537
+ except (json.JSONDecodeError, TypeError):
538
+ return None
539
+ if not isinstance(data, dict):
540
+ return None
541
+ if data.get("dir_info", {}).get("editable") is not True:
542
+ return None
543
+ url = data.get("url", "")
544
+ if not url:
545
+ return None
546
+ # ``url`` is a ``file://`` URI.
547
+ if url.startswith("file://"):
548
+ url = url[len("file://") :]
549
+ source = Path(url)
550
+ if source.exists() and source.is_dir():
551
+ return source.resolve()
552
+ return None
553
+
554
+ @staticmethod
555
+ def _search_source_for_config(
556
+ source_root: Path,
557
+ command_name: str | None,
558
+ aliases: set[str] | None,
559
+ ) -> Path | None:
560
+ """Search a source tree for a ``usecli.config.toml`` that matches."""
561
+ if not source_root.exists() or not source_root.is_dir():
562
+ return None
563
+ candidates = [
564
+ p
565
+ for p in source_root.rglob(USECLI_CONFIG_TOML)
566
+ if not any(part in ConfigManager._SKIP_DIRS for part in p.parts)
567
+ ]
568
+ if command_name:
569
+ candidates = [
570
+ p
571
+ for p in candidates
572
+ if ConfigManager._config_matches_command(p, command_name, aliases)
573
+ ]
574
+ if not candidates:
575
+ return None
576
+ candidates.sort(key=lambda p: (len(p.parts), str(p)))
577
+ return candidates[0]
578
+
474
579
  def get(self, key: str, default: Any = None) -> Any:
475
580
  """Get a configuration value using dot notation.
476
581
 
File without changes
File without changes
File without changes
File without changes
File without changes