freyja 0.0.0__py3-none-any.whl

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 (67) hide show
  1. LICENSE +21 -0
  2. README.md +309 -0
  3. freyja/__init__.py +8 -0
  4. freyja/cli/__init__.py +11 -0
  5. freyja/cli/class_handler.py +189 -0
  6. freyja/cli/enums.py +7 -0
  7. freyja/cli/execution_coordinator.py +125 -0
  8. freyja/cli/system/__init__.py +16 -0
  9. freyja/cli/system/completion.py +155 -0
  10. freyja/cli/system/tune_theme.py +644 -0
  11. freyja/cli/target_mode.py +3 -0
  12. freyja/command/__init__.py +13 -0
  13. freyja/command/command_discovery.py +351 -0
  14. freyja/command/command_executor.py +188 -0
  15. freyja/command/command_info.py +22 -0
  16. freyja/command/command_tree.py +233 -0
  17. freyja/command/validation.py +144 -0
  18. freyja/completion/__init__.py +22 -0
  19. freyja/completion/base.py +279 -0
  20. freyja/completion/bash.py +125 -0
  21. freyja/completion/fish.py +48 -0
  22. freyja/completion/installer.py +241 -0
  23. freyja/completion/powershell.py +53 -0
  24. freyja/completion/zsh.py +50 -0
  25. freyja/freyja_cli.py +149 -0
  26. freyja/help/__init__.py +9 -0
  27. freyja/help/help_formatter.py +722 -0
  28. freyja/help/help_formatting_engine.py +241 -0
  29. freyja/parser/__init__.py +9 -0
  30. freyja/parser/argument_parser.py +163 -0
  31. freyja/parser/command_parser.py +288 -0
  32. freyja/parser/docstring_parser.py +63 -0
  33. freyja/tests/__init__.py +1 -0
  34. freyja/tests/conftest.py +77 -0
  35. freyja/tests/test_adjust_strategy.py +57 -0
  36. freyja/tests/test_ansi_string.py +343 -0
  37. freyja/tests/test_cli_class.py +452 -0
  38. freyja/tests/test_cli_module.py +286 -0
  39. freyja/tests/test_color_adjustment.py +289 -0
  40. freyja/tests/test_color_formatter_rgb.py +156 -0
  41. freyja/tests/test_command_discovery.py +344 -0
  42. freyja/tests/test_completion.py +212 -0
  43. freyja/tests/test_comprehensive_module_cli.py +531 -0
  44. freyja/tests/test_data_struct_util.py +424 -0
  45. freyja/tests/test_examples.py +129 -0
  46. freyja/tests/test_hierarchical_command_groups.py +361 -0
  47. freyja/tests/test_hierarchical_help_formatter.py +504 -0
  48. freyja/tests/test_multi_class_cli.py +283 -0
  49. freyja/tests/test_rgb.py +333 -0
  50. freyja/tests/test_system.py +326 -0
  51. freyja/tests/test_text_util.py +383 -0
  52. freyja/tests/test_theme_color_adjustment.py +280 -0
  53. freyja/theme/__init__.py +28 -0
  54. freyja/theme/color_formatter.py +114 -0
  55. freyja/theme/enums.py +92 -0
  56. freyja/theme/rgb.py +355 -0
  57. freyja/theme/theme.py +385 -0
  58. freyja/theme/theme_style.py +34 -0
  59. freyja/utils/__init__.py +13 -0
  60. freyja/utils/ansi_string.py +157 -0
  61. freyja/utils/data_struct_util.py +66 -0
  62. freyja/utils/math_util.py +55 -0
  63. freyja/utils/text_util.py +73 -0
  64. freyja-0.0.0.dist-info/LICENSE +21 -0
  65. freyja-0.0.0.dist-info/METADATA +332 -0
  66. freyja-0.0.0.dist-info/RECORD +67 -0
  67. freyja-0.0.0.dist-info/WHEEL +4 -0
LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2019] [Tangled Path]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,309 @@
1
+ ![Freyja](freyja.png)
2
+
3
+ # Freyja ⚡
4
+ **No-dependency, zero-configuration CLI tool to build command-line interfaces purely from your code.**
5
+
6
+ Transform your Python functions and classes into powerful command-line applications in seconds! Freyja uses introspection and type annotations to automatically generate professional CLIs with zero configuration required.
7
+
8
+ ## Table of Contents
9
+ * [🚀 Why Freyja?](#-why-freyja)
10
+ * [⚡ Quick Start](#-quick-start)
11
+ * [🗂️ Module-based CLI](#️-module-based-cli)
12
+ * [🏗️ Class-based CLI](#️-class-based-cli)
13
+ * [Direct Methods Pattern](#direct-methods-pattern)
14
+ * [Inner Classes Pattern](#inner-classes-pattern)
15
+ * [✨ Key Features](#-key-features)
16
+ * [📚 Documentation](#-documentation)
17
+ * [🛠️ Development](#️-development)
18
+ * [⚙️ Requirements](#️-requirements)
19
+
20
+ ## 🚀 Why Freyja?
21
+
22
+ **Build CLIs in under 5 minutes!** No configuration files, no complex setup, no learning curve. Just add type annotations to your functions and Freyja does the rest.
23
+
24
+ ```bash
25
+ pip install freyja
26
+ # That's it! No dependencies, no configuration needed.
27
+ ```
28
+
29
+ **Before Freyja:**
30
+ ```bash
31
+ python script.py --config-file /path/to/config --database-host localhost --database-port 5432 --username admin --password secret --table-name users --action create --data '{"name": "Alice", "email": "alice@example.com"}'
32
+ ```
33
+
34
+ **After Freyja:**
35
+ ```bash
36
+ python script.py database--create-user --name Alice --email alice@example.com
37
+ # Global config handled automatically, clean syntax, built-in help
38
+ ```
39
+
40
+ ## ⚡ Quick Start
41
+
42
+ **Step 1:** Install Freyja
43
+ ```bash
44
+ pip install freyja
45
+ ```
46
+
47
+ **Step 2:** Add type annotations to your functions
48
+ ```python
49
+ def greet(name: str = "World", excited: bool = False) -> None:
50
+ """Greet someone by name."""
51
+ greeting = f"Hello, {name}!"
52
+ if excited:
53
+ greeting += " 🎉"
54
+ print(greeting)
55
+ ```
56
+
57
+ **Step 3:** Add 3 lines of Freyja code
58
+ ```python
59
+ from freyja import CLI
60
+ import sys
61
+
62
+ if __name__ == '__main__':
63
+ cli = CLI(sys.modules[__name__], title="My CLI")
64
+ cli.display()
65
+ ```
66
+
67
+ **Step 4:** Use your new CLI!
68
+ ```bash
69
+ python script.py greet --name Alice --excited
70
+ # Output: Hello, Alice! 🎉
71
+
72
+ python script.py --help
73
+ # Automatic help generation with beautiful formatting
74
+ ```
75
+
76
+ ## 🗂️ Module-based CLI
77
+
78
+ Perfect for functional programming styles and simple utilities. Every function becomes a command:
79
+
80
+ ```python
81
+ # data_processor.py
82
+ from freyja import CLI
83
+ import sys
84
+
85
+
86
+ def process_csv(input_file: str, output_format: str = "json", verbose: bool = False) -> None:
87
+ """Process CSV file and convert to specified format."""
88
+ print(f"Processing {input_file} -> {output_format}")
89
+ if verbose:
90
+ print("Verbose mode enabled")
91
+
92
+
93
+ def analyze_logs(log_file: str, pattern: str, max_lines: int = 1000) -> None:
94
+ """Analyze log files for specific patterns."""
95
+ print(f"Analyzing {log_file} for pattern: {pattern} (max {max_lines} lines)")
96
+
97
+
98
+ if __name__ == '__main__':
99
+ cli = CLI(sys.modules[__name__], title="Data Processing Tools")
100
+ cli.display()
101
+ ```
102
+
103
+ **Usage:**
104
+ ```bash
105
+ python data_processor.py process-csv --input-file data.csv --output-format xml --verbose
106
+ python data_processor.py analyze-logs --log-file app.log --pattern "ERROR" --max-lines 500
107
+ python data_processor.py --help # Beautiful auto-generated help
108
+ ```
109
+
110
+ ## 🏗️ Class-based CLI
111
+
112
+ Ideal for stateful applications and complex workflows. Supports two powerful patterns:
113
+
114
+ ### Direct Methods Pattern
115
+
116
+ Simple and clean - each method becomes a command:
117
+
118
+ ```python
119
+ # calculator.py
120
+ from freyja import CLI
121
+
122
+
123
+ class Calculator:
124
+ """Advanced calculator with memory and history."""
125
+
126
+ def __init__(self, precision: int = 2, memory_enabled: bool = True):
127
+ """Initialize calculator with global settings."""
128
+ self.precision = precision
129
+ self.memory = 0 if memory_enabled else None
130
+
131
+ def add(self, a: float, b: float, store_result: bool = False) -> None:
132
+ """Add two numbers together."""
133
+ result = round(a + b, self.precision)
134
+ print(f"{a} + {b} = {result}")
135
+
136
+ if store_result and self.memory is not None:
137
+ self.memory = result
138
+ print(f"Result stored in memory: {result}")
139
+
140
+ def multiply(self, a: float, b: float) -> None:
141
+ """Multiply two numbers."""
142
+ result = round(a * b, self.precision)
143
+ print(f"{a} × {b} = {result}")
144
+
145
+
146
+ if __name__ == '__main__':
147
+ cli = CLI(Calculator, title="Advanced Calculator")
148
+ cli.display()
149
+ ```
150
+
151
+ **Usage:**
152
+ ```bash
153
+ python calculator.py --precision 4 add --a 3.14159 --b 2.71828 --store-result
154
+ # Output: 3.14159 + 2.71828 = 5.8599
155
+ # Result stored in memory: 5.8599
156
+ ```
157
+
158
+ ### Inner Classes Pattern
159
+
160
+ Organize complex applications with flat double-dash commands:
161
+
162
+ ```python
163
+ # project_manager.py
164
+ from freyja import CLI
165
+ from pathlib import Path
166
+
167
+
168
+ class ProjectManager:
169
+ """Complete project management suite with organized command structure."""
170
+
171
+ def __init__(self, config_file: str = "config.json", debug: bool = False):
172
+ """Initialize with global settings."""
173
+ self.config_file = config_file
174
+ self.debug = debug
175
+
176
+ class Database:
177
+ """Database operations and management."""
178
+
179
+ def __init__(self, connection_string: str = "sqlite:///projects.db", timeout: int = 30):
180
+ """Initialize database connection."""
181
+ self.connection_string = connection_string
182
+ self.timeout = timeout
183
+
184
+ def migrate(self, version: str = "latest", dry_run: bool = False) -> None:
185
+ """Run database migrations."""
186
+ action = "Would run" if dry_run else "Running"
187
+ print(f"{action} migration to version: {version}")
188
+ print(f"Connection: {self.connection_string}")
189
+
190
+ def backup(self, output_path: Path, compress: bool = True) -> None:
191
+ """Create database backup."""
192
+ compression = "compressed" if compress else "uncompressed"
193
+ print(f"Creating {compression} backup at: {output_path}")
194
+
195
+ class Projects:
196
+ """Project creation and management operations."""
197
+
198
+ def __init__(self, workspace: str = "./projects", auto_save: bool = True):
199
+ """Initialize project operations."""
200
+ self.workspace = workspace
201
+ self.auto_save = auto_save
202
+
203
+ def create(self, name: str, template: str = "basic", description: str = "") -> None:
204
+ """Create a new project from template."""
205
+ print(f"Creating project '{name}' using '{template}' template")
206
+ print(f"Workspace: {self.workspace}")
207
+ print(f"Description: {description}")
208
+ print(f"Auto-save: {'enabled' if self.auto_save else 'disabled'}")
209
+
210
+ def deploy(self, project_name: str, environment: str = "staging", force: bool = False) -> None:
211
+ """Deploy project to specified environment."""
212
+ action = "Force deploying" if force else "Deploying"
213
+ print(f"{action} {project_name} to {environment}")
214
+
215
+
216
+ if __name__ == '__main__':
217
+ cli = CLI(ProjectManager, title="Project Management Suite")
218
+ cli.display()
219
+ ```
220
+
221
+ **Usage:**
222
+ ```bash
223
+ # Global + Sub-global + Command arguments (all flat)
224
+ python project_manager.py --config-file prod.json --debug \
225
+ database--migrate --connection-string postgres://prod --version 2.1.0 --dry-run
226
+
227
+ # Create new project with custom workspace
228
+ python project_manager.py projects--create --workspace /prod/projects --auto-save \
229
+ --name "web-app" --template "react" --description "Production web application"
230
+
231
+ # Deploy with force flag
232
+ python project_manager.py projects--deploy --project-name web-app --environment production --force
233
+
234
+ # Beautiful help shows all flat commands organized by group
235
+ python project_manager.py --help
236
+ ```
237
+
238
+ ## ✨ Key Features
239
+
240
+ 🚀 **Zero Configuration** - Works out of the box with just type annotations
241
+ ⚡ **Lightning Fast** - No runtime dependencies, minimal overhead
242
+ 🎯 **Type Safe** - Automatic validation from your type hints
243
+ 📚 **Auto Documentation** - Help text generated from your docstrings
244
+ 🎨 **Beautiful Output** - Professional themes and formatting
245
+ 🔧 **Flexible Architecture** - Module-based or class-based patterns
246
+ 📦 **No Dependencies** - Uses only Python standard library
247
+ 🌈 **Shell Completion** - Bash, Zsh, Fish, and PowerShell support
248
+ ✅ **Production Ready** - Battle-tested in enterprise applications
249
+
250
+ ## 📚 Documentation
251
+
252
+ **[📖 Complete Documentation Hub](docs/README.md)** - Everything you need to master Freyja
253
+
254
+ ### Quick Links
255
+ * **[🚀 Getting Started](docs/getting-started/README.md)** - Installation and first steps
256
+ * **[👤 User Guide](docs/user-guide/README.md)** - Comprehensive guides for both CLI modes
257
+ * **[⚙️ Features](docs/features/README.md)** - Type annotations, themes, completion, and more
258
+ * **[📋 Examples & Best Practices](docs/guides/README.md)** - Real-world examples and patterns
259
+ * **[❓ FAQ](docs/faq.md)** - Frequently asked questions
260
+ * **[🔧 API Reference](docs/reference/README.md)** - Complete API documentation
261
+
262
+ ## 🛠️ Development
263
+
264
+ **[📖 Development Guide](CLAUDE.md)** - Comprehensive guide for contributors
265
+
266
+ ### Quick Setup
267
+
268
+ ```bash
269
+ # Clone and setup
270
+ git clone https://github.com/tangledpath/freyja.git
271
+ cd freyja
272
+
273
+ # Install Poetry and setup environment
274
+ curl -sSL https://install.python-poetry.org | python3 -
275
+ ./bin/setup-dev.sh
276
+
277
+ # Run tests and examples
278
+ ./bin/test.sh
279
+ poetry run python examples/mod_example.py --help
280
+ poetry run python examples/cls_example.py --help
281
+ ```
282
+
283
+ ### Development Commands
284
+
285
+ ```bash
286
+ poetry install # Install dependencies
287
+ ./bin/test.sh # Run tests with coverage
288
+ ./bin/lint.sh # Run all linters and formatters
289
+ poetry build # Build package
290
+ ./bin/publish.sh # Publish to PyPI (maintainers)
291
+ ```
292
+
293
+ ## ⚙️ Requirements
294
+
295
+ * **Python 3.13.5+** (recommended) or Python 3.8+
296
+ * **Zero runtime dependencies** - uses only Python standard library
297
+ * **Type annotations required** - for automatic CLI generation
298
+ * **Docstrings recommended** - for automatic help text generation
299
+
300
+ ---
301
+
302
+ **Ready to transform your Python code into powerful CLIs?**
303
+
304
+ ```bash
305
+ pip install freyja
306
+ # Start building amazing command-line tools in minutes! ⚡
307
+ ```
308
+
309
+ **[📚 Get Started Now →](docs/getting-started/README.md)**
freyja/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """
2
+ Freyja: Zero-configuration FreyjaCLI generator from classes/module introspection.
3
+ Uses class/method/function introspection, typehints, and docstrings to build
4
+ a fully functional FreyjaCLI. Command groups can be made with inner-classes,
5
+ effectively making subclasses.
6
+ """
7
+ from .freyja_cli import FreyjaCLI
8
+ __all__ = [FreyjaCLI]
freyja/cli/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ from .class_handler import ClassHandler
2
+ from .execution_coordinator import ExecutionCoordinator
3
+ from .enums import TargetMode
4
+ from .system import SystemClassBuilder
5
+
6
+ __all__ = [
7
+ ClassHandler,
8
+ ExecutionCoordinator,
9
+ SystemClassBuilder,
10
+ TargetMode,
11
+ ]
@@ -0,0 +1,189 @@
1
+ import inspect
2
+ from typing import Dict, Type, List, Tuple, Optional, Any
3
+
4
+ """Multi-class FreyjaCLI command handling and collision detection.
5
+
6
+ Provides services for managing cmd_tree from multiple classes in a single FreyjaCLI,
7
+ including collision detection, command ordering, and source tracking.
8
+ """
9
+
10
+
11
+
12
+ class ClassHandler:
13
+ """Handles cmd_tree from multiple classes with collision detection and ordering."""
14
+
15
+ def __init__(self):
16
+ """Initialize multi-class handler."""
17
+ self.command_sources: Dict[str, Type] = {} # command_name -> source_class
18
+ self.class_commands: Dict[Type, List[str]] = {} # source_class -> [command_names]
19
+ self.collision_tracker: Dict[str, List[Type]] = {} # command_name -> [source_classes]
20
+
21
+ def track_command(self, command_name: str, source_class: Type) -> None:
22
+ """
23
+ Track a command and its source class for collision detection.
24
+
25
+ :param command_name: FreyjaCLI command name (e.g., 'file-operations--process-single')
26
+ :param source_class: Source class that defines this command
27
+ """
28
+ # Track which class this command comes from
29
+ if command_name in self.command_sources:
30
+ # Collision detected - track all sources
31
+ if command_name not in self.collision_tracker:
32
+ self.collision_tracker[command_name] = [self.command_sources[command_name]]
33
+ self.collision_tracker[command_name].append(source_class)
34
+ else:
35
+ self.command_sources[command_name] = source_class
36
+
37
+ # Track cmd_tree per class for ordering
38
+ if source_class not in self.class_commands:
39
+ self.class_commands[source_class] = []
40
+ self.class_commands[source_class].append(command_name)
41
+
42
+ def detect_collisions(self) -> List[Tuple[str, List[Type]]]:
43
+ """
44
+ Detect and return command name collisions.
45
+
46
+ :return: List of (command_name, [conflicting_classes]) tuples
47
+ """
48
+ return [(cmd, classes) for cmd, classes in self.collision_tracker.items()]
49
+
50
+ def has_collisions(self) -> bool:
51
+ """
52
+ Check if any command name collisions exist.
53
+
54
+ :return: True if collisions detected, False otherwise
55
+ """
56
+ return len(self.collision_tracker) > 0
57
+
58
+ def get_ordered_commands(self, class_order: List[Type]) -> List[str]:
59
+ """
60
+ Get cmd_tree ordered by class sequence, then alphabetically within each class.
61
+
62
+ :param class_order: Desired order of classes
63
+ :return: List of command names in proper order
64
+ """
65
+ ordered_commands = []
66
+
67
+ # Process classes in the specified order
68
+ for cls in class_order:
69
+ if cls in self.class_commands:
70
+ # Sort cmd_tree within this class alphabetically
71
+ class_commands = sorted(self.class_commands[cls])
72
+ ordered_commands.extend(class_commands)
73
+
74
+ return ordered_commands
75
+
76
+ def get_command_source(self, command_name: str) -> Optional[Type]:
77
+ """
78
+ Get the source class for a command.
79
+
80
+ :param command_name: FreyjaCLI command name
81
+ :return: Source class or None if not found
82
+ """
83
+ return self.command_sources.get(command_name)
84
+
85
+ def format_collision_error(self) -> str:
86
+ """
87
+ Format collision error message for user display.
88
+
89
+ :return: Formatted error message describing all collisions
90
+ """
91
+ if not self.has_collisions():
92
+ return ""
93
+
94
+ error_lines = ["Command name collisions detected:"]
95
+
96
+ for command_name, conflicting_classes in self.collision_tracker.items():
97
+ class_names = [cls.__name__ for cls in conflicting_classes]
98
+ error_lines.append(f" '{command_name}' conflicts between: {', '.join(class_names)}")
99
+
100
+ error_lines.append("")
101
+ error_lines.append("Solutions:")
102
+ error_lines.append("1. Rename methods in one of the conflicting classes")
103
+ error_lines.append("2. Use different inner class names to create unique command paths")
104
+ error_lines.append("3. Use separate FreyjaCLI instances for conflicting classes")
105
+
106
+ return "\n".join(error_lines)
107
+
108
+ def validate_classes(self, classes: List[Type]) -> None:
109
+ """Validate that classes can be used together without collisions.
110
+
111
+ :param classes: List of classes to validate
112
+ :raises ValueError: If command collisions are detected"""
113
+ # Simulate command discovery to detect collisions
114
+ temp_handler = ClassHandler()
115
+
116
+ for cls in classes:
117
+ # Simulate the command discovery process
118
+ self._simulate_class_commands(temp_handler, cls)
119
+
120
+ # Check for collisions
121
+ if temp_handler.has_collisions():
122
+ raise ValueError(temp_handler.format_collision_error())
123
+
124
+ def _simulate_class_commands(self, handler: 'ClassHandler', cls: Type) -> None:
125
+ """Simulate command discovery for collision detection.
126
+
127
+ :param handler: Handler to track cmd_tree in
128
+ :param cls: Class to simulate cmd_tree for"""
129
+ from freyja.utils.text_util import TextUtil
130
+
131
+ # Check for inner classes (hierarchical cmd_tree)
132
+ inner_classes = self._discover_inner_classes(cls)
133
+
134
+ if inner_classes:
135
+ # Inner class pattern - track both direct methods and inner class methods
136
+ # Direct methods
137
+ for name, obj in inspect.getmembers(cls):
138
+ if self._is_valid_method(name, obj, cls):
139
+ cli_name = TextUtil.kebab_case(name)
140
+ handler.track_command(cli_name, cls)
141
+
142
+ # Inner class methods
143
+ for class_name, inner_class in inner_classes.items():
144
+ command_name = TextUtil.kebab_case(class_name)
145
+
146
+ for method_name, method_obj in inspect.getmembers(inner_class):
147
+ if (not method_name.startswith('_') and
148
+ callable(method_obj) and
149
+ method_name != '__init__' and
150
+ inspect.isfunction(method_obj)):
151
+ # Create hierarchical command name
152
+ cli_name = f"{command_name}--{TextUtil.kebab_case(method_name)}"
153
+ handler.track_command(cli_name, cls)
154
+ else:
155
+ # Direct methods only
156
+ for name, obj in inspect.getmembers(cls):
157
+ if self._is_valid_method(name, obj, cls):
158
+ cli_name = TextUtil.kebab_case(name)
159
+ handler.track_command(cli_name, cls)
160
+
161
+ def _discover_inner_classes(self, cls: Type) -> Dict[str, Type]:
162
+ """Discover inner classes for a given class.
163
+
164
+ :param cls: Class to check for inner classes
165
+ :return: Dictionary of inner class name -> inner class"""
166
+ inner_classes = {}
167
+
168
+ for name, obj in inspect.getmembers(cls):
169
+ if (inspect.isclass(obj) and
170
+ not name.startswith('_') and
171
+ obj.__qualname__.endswith(f'{cls.__name__}.{name}')):
172
+ inner_classes[name] = obj
173
+
174
+ return inner_classes
175
+
176
+ def _is_valid_method(self, name: str, obj: Any, cls: Type) -> bool:
177
+ """Check if a method should be included as a FreyjaCLI command.
178
+
179
+ :param name: Method name
180
+ :param obj: Method object
181
+ :param cls: Containing class
182
+ :return: True if method should be included"""
183
+ return (
184
+ not name.startswith('_') and
185
+ callable(obj) and
186
+ (inspect.isfunction(obj) or inspect.ismethod(obj)) and
187
+ hasattr(obj, '__qualname__') and
188
+ cls.__name__ in obj.__qualname__
189
+ )
freyja/cli/enums.py ADDED
@@ -0,0 +1,7 @@
1
+ import enum
2
+
3
+
4
+ class TargetMode(enum.Enum):
5
+ """Target mode enum for command discovery."""
6
+ MODULE = 'module'
7
+ CLASS = 'class'
@@ -0,0 +1,125 @@
1
+ """FreyjaCLI execution coordination service.
2
+
3
+ Handles argument parsing and command execution coordination.
4
+ Extracted from FreyjaCLI class to reduce its size and improve separation of concerns.
5
+ """
6
+ from typing import *
7
+
8
+ from .enums import TargetMode
9
+
10
+
11
+ class ExecutionCoordinator:
12
+ """Coordinates FreyjaCLI argument parsing and command execution."""
13
+
14
+ def __init__(self, target_mode: TargetMode, executors: Dict[str, Any]):
15
+ """Initialize execution coordinator."""
16
+ self.target_mode = target_mode
17
+ self.executors = executors
18
+ self.command_tree = None
19
+
20
+ def parse_and_execute(self, parser, args: Optional[List[str]]) -> Any:
21
+ """Parse arguments and execute command."""
22
+ result = None
23
+
24
+ try:
25
+ parsed = parser.parse_args(args)
26
+
27
+ # Debug: Check what attributes are available
28
+ # parsed_attrs = [attr for attr in dir(parsed) if not attr.startswith('_')]
29
+ # print(f"DEBUG: parsed attributes: {parsed_attrs}")
30
+ # print(f"DEBUG: parsed.__dict__: {parsed.__dict__}")
31
+
32
+ if not hasattr(parsed, '_cli_function'):
33
+ # No command specified, show help
34
+ result = self._handle_no_command(parser, parsed)
35
+ else:
36
+ # Execute command
37
+ result = self._execute_command(parsed)
38
+
39
+ except SystemExit:
40
+ # Let argparse handle its own exits (help, errors, etc.)
41
+ raise
42
+
43
+ except Exception as e:
44
+ # Handle execution errors - for argparse-like errors, raise SystemExit
45
+ if isinstance(e, (ValueError, KeyError)) and 'parsed' not in locals():
46
+ # Parsing errors should raise SystemExit like argparse does
47
+ print(f"Error: {e}")
48
+ raise SystemExit(2)
49
+ else:
50
+ # Other execution errors
51
+ result = self._handle_execution_error(parsed if 'parsed' in locals() else None, e)
52
+
53
+ return result
54
+
55
+ def _handle_no_command(self, parser, parsed) -> int:
56
+ """Handle case where no command was specified."""
57
+ if hasattr(parsed, '_complete') and parsed._complete:
58
+ return 0 # Completion mode - don't show help
59
+
60
+ # Check for --help flag explicitly
61
+ if hasattr(parsed, 'help') and parsed.help:
62
+ parser.print_help()
63
+ return 0
64
+
65
+ # Default: show help and return success
66
+ parser.print_help()
67
+ return 0
68
+
69
+ def _execute_command(self, parsed) -> Any:
70
+ """Execute the command from parsed arguments."""
71
+ # Check if we have multiple classes (multiple executors)
72
+ if len(self.executors) > 1 and 'primary' not in self.executors:
73
+ return self._execute_multi_class_command(parsed)
74
+ else:
75
+ # Single class or module execution
76
+ executor = self.executors.get('primary')
77
+ if not executor:
78
+ raise RuntimeError("No executor available for command execution")
79
+
80
+ return executor.execute_command(
81
+ parsed=parsed,
82
+ target_mode=self.target_mode
83
+ )
84
+
85
+ def _execute_multi_class_command(self, parsed) -> Any:
86
+ """Execute command for multiple-class FreyjaCLI."""
87
+ function_name = parsed._function_name
88
+ source_class = self._find_source_class_for_function(function_name)
89
+
90
+ if not source_class:
91
+ raise ValueError(f"Could not find source class for function: {function_name}")
92
+
93
+ executor = self.executors.get(source_class)
94
+
95
+ if not executor:
96
+ raise ValueError(f"No executor found for class: {source_class.__name__}")
97
+
98
+ return executor.execute_command(
99
+ parsed=parsed,
100
+ target_mode=TargetMode.CLASS
101
+ )
102
+
103
+ def _find_source_class_for_function(self, function_name: str) -> Optional[Type]:
104
+ """Find the source class for a given function name."""
105
+ if self.command_tree:
106
+ return self.command_tree.find_source_class(function_name)
107
+ return None
108
+
109
+ def _handle_execution_error(self, parsed, error: Exception) -> int:
110
+ """Handle command execution errors."""
111
+ if isinstance(error, KeyboardInterrupt):
112
+ print("\nOperation cancelled by user")
113
+ return 130 # Standard exit code for SIGINT
114
+
115
+ print(f"Error executing command: {error}")
116
+
117
+ if parsed and hasattr(parsed, '_function_name'):
118
+ print(f"Function: {parsed._function_name}")
119
+
120
+ return 1
121
+
122
+ @staticmethod
123
+ def check_no_color_flag(args: List[str]) -> bool:
124
+ """Check if --no-color flag is present in arguments."""
125
+ return '--no-color' in args or '-n' in args
@@ -0,0 +1,16 @@
1
+ from .completion import Completion
2
+ from .tune_theme import TuneTheme
3
+
4
+ class SystemClassBuilder:
5
+ @staticmethod
6
+ def build(completion:bool=True, theme_tuner:bool=False) -> type:
7
+ system_class_dict:dict = {}
8
+
9
+ if completion: system_class_dict['Completion'] = Completion
10
+ if theme_tuner: system_class_dict['TuneTheme'] = TuneTheme
11
+ return type('System', (object,), system_class_dict)
12
+
13
+ # Create default System class for direct import
14
+ System = SystemClassBuilder.build(completion=True, theme_tuner=True)
15
+
16
+ __all__ = ['SystemClassBuilder', 'System', 'Completion', 'TuneTheme']