fishertools 0.2.1__py3-none-any.whl → 0.4.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 (69) hide show
  1. fishertools/__init__.py +16 -5
  2. fishertools/errors/__init__.py +11 -3
  3. fishertools/errors/exception_types.py +282 -0
  4. fishertools/errors/explainer.py +87 -1
  5. fishertools/errors/models.py +73 -1
  6. fishertools/errors/patterns.py +40 -0
  7. fishertools/examples/cli_example.py +156 -0
  8. fishertools/examples/learn_example.py +65 -0
  9. fishertools/examples/logger_example.py +176 -0
  10. fishertools/examples/menu_example.py +101 -0
  11. fishertools/examples/storage_example.py +175 -0
  12. fishertools/input_utils.py +185 -0
  13. fishertools/learn/__init__.py +19 -2
  14. fishertools/learn/examples.py +88 -1
  15. fishertools/learn/knowledge_engine.py +321 -0
  16. fishertools/learn/repl/__init__.py +19 -0
  17. fishertools/learn/repl/cli.py +31 -0
  18. fishertools/learn/repl/code_sandbox.py +229 -0
  19. fishertools/learn/repl/command_handler.py +544 -0
  20. fishertools/learn/repl/command_parser.py +165 -0
  21. fishertools/learn/repl/engine.py +479 -0
  22. fishertools/learn/repl/models.py +121 -0
  23. fishertools/learn/repl/session_manager.py +284 -0
  24. fishertools/learn/repl/test_code_sandbox.py +261 -0
  25. fishertools/learn/repl/test_code_sandbox_pbt.py +148 -0
  26. fishertools/learn/repl/test_command_handler.py +224 -0
  27. fishertools/learn/repl/test_command_handler_pbt.py +189 -0
  28. fishertools/learn/repl/test_command_parser.py +160 -0
  29. fishertools/learn/repl/test_command_parser_pbt.py +100 -0
  30. fishertools/learn/repl/test_engine.py +190 -0
  31. fishertools/learn/repl/test_session_manager.py +310 -0
  32. fishertools/learn/repl/test_session_manager_pbt.py +182 -0
  33. fishertools/learn/test_knowledge_engine.py +241 -0
  34. fishertools/learn/test_knowledge_engine_pbt.py +180 -0
  35. fishertools/patterns/__init__.py +46 -0
  36. fishertools/patterns/cli.py +175 -0
  37. fishertools/patterns/logger.py +140 -0
  38. fishertools/patterns/menu.py +99 -0
  39. fishertools/patterns/storage.py +127 -0
  40. fishertools/readme_transformer.py +631 -0
  41. fishertools/safe/__init__.py +6 -1
  42. fishertools/safe/files.py +329 -1
  43. fishertools/transform_readme.py +105 -0
  44. fishertools-0.4.0.dist-info/METADATA +104 -0
  45. fishertools-0.4.0.dist-info/RECORD +131 -0
  46. {fishertools-0.2.1.dist-info → fishertools-0.4.0.dist-info}/WHEEL +1 -1
  47. tests/test_documentation_properties.py +329 -0
  48. tests/test_documentation_structure.py +349 -0
  49. tests/test_errors/test_exception_types.py +446 -0
  50. tests/test_errors/test_exception_types_pbt.py +333 -0
  51. tests/test_errors/test_patterns.py +52 -0
  52. tests/test_input_utils/__init__.py +1 -0
  53. tests/test_input_utils/test_input_utils.py +65 -0
  54. tests/test_learn/test_examples.py +179 -1
  55. tests/test_learn/test_explain_properties.py +307 -0
  56. tests/test_patterns_cli.py +611 -0
  57. tests/test_patterns_docstrings.py +473 -0
  58. tests/test_patterns_logger.py +465 -0
  59. tests/test_patterns_menu.py +440 -0
  60. tests/test_patterns_storage.py +447 -0
  61. tests/test_readme_enhancements_v0_3_1.py +2036 -0
  62. tests/test_readme_transformer/__init__.py +1 -0
  63. tests/test_readme_transformer/test_readme_infrastructure.py +1023 -0
  64. tests/test_readme_transformer/test_transform_readme_integration.py +431 -0
  65. tests/test_safe/test_files.py +726 -1
  66. fishertools-0.2.1.dist-info/METADATA +0 -256
  67. fishertools-0.2.1.dist-info/RECORD +0 -81
  68. {fishertools-0.2.1.dist-info → fishertools-0.4.0.dist-info}/licenses/LICENSE +0 -0
  69. {fishertools-0.2.1.dist-info → fishertools-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,241 @@
1
+ """
2
+ Unit tests for the Knowledge Engine module.
3
+ """
4
+
5
+ import pytest
6
+ import os
7
+ import json
8
+ from fishertools.learn.knowledge_engine import (
9
+ KnowledgeEngine, get_topic, list_topics, search_topics,
10
+ get_random_topic, get_learning_path, get_engine
11
+ )
12
+
13
+
14
+ class TestKnowledgeEngine:
15
+ """Test suite for KnowledgeEngine class."""
16
+
17
+ @pytest.fixture
18
+ def engine(self):
19
+ """Create a KnowledgeEngine instance for testing."""
20
+ return KnowledgeEngine()
21
+
22
+ def test_engine_initialization(self, engine):
23
+ """Test that engine initializes correctly."""
24
+ assert engine is not None
25
+ assert len(engine.topics) > 0
26
+ assert len(engine.categories) > 0
27
+
28
+ def test_get_topic_existing(self, engine):
29
+ """Test getting an existing topic."""
30
+ topic = engine.get_topic("Lists")
31
+ assert topic is not None
32
+ assert topic["topic"] == "Lists"
33
+ assert "description" in topic
34
+ assert "example" in topic
35
+ assert "common_mistakes" in topic
36
+ assert "related_topics" in topic
37
+
38
+ def test_get_topic_nonexistent(self, engine):
39
+ """Test getting a non-existent topic returns None."""
40
+ topic = engine.get_topic("NonExistentTopic")
41
+ assert topic is None
42
+
43
+ def test_list_topics(self, engine):
44
+ """Test listing all topics."""
45
+ topics = engine.list_topics()
46
+ assert isinstance(topics, list)
47
+ assert len(topics) == 35
48
+ assert "Lists" in topics
49
+ assert "Variables and Assignment" in topics
50
+ # Check that list is sorted
51
+ assert topics == sorted(topics)
52
+
53
+ def test_search_topics_by_name(self, engine):
54
+ """Test searching topics by name."""
55
+ results = engine.search_topics("list")
56
+ assert len(results) > 0
57
+ assert "Lists" in results
58
+ assert "List Indexing" in results
59
+ assert "List Slicing" in results
60
+
61
+ def test_search_topics_case_insensitive(self, engine):
62
+ """Test that search is case-insensitive."""
63
+ results1 = engine.search_topics("loop")
64
+ results2 = engine.search_topics("LOOP")
65
+ assert results1 == results2
66
+
67
+ def test_search_topics_by_description(self, engine):
68
+ """Test searching topics by description."""
69
+ results = engine.search_topics("immutable")
70
+ assert len(results) > 0
71
+
72
+ def test_get_random_topic(self, engine):
73
+ """Test getting a random topic."""
74
+ topic = engine.get_random_topic()
75
+ assert topic is not None
76
+ assert "topic" in topic
77
+ assert "description" in topic
78
+
79
+ def test_get_related_topics(self, engine):
80
+ """Test getting related topics."""
81
+ related = engine.get_related_topics("Lists")
82
+ assert isinstance(related, list)
83
+ # All related topics should exist
84
+ for topic_name in related:
85
+ assert engine.get_topic(topic_name) is not None
86
+
87
+ def test_get_related_topics_nonexistent(self, engine):
88
+ """Test getting related topics for non-existent topic."""
89
+ related = engine.get_related_topics("NonExistent")
90
+ assert related == []
91
+
92
+ def test_get_topics_by_category(self, engine):
93
+ """Test getting topics by category."""
94
+ basic_types = engine.get_topics_by_category("Basic Types")
95
+ assert len(basic_types) == 5
96
+ assert "Variables and Assignment" in basic_types
97
+ assert "Integers and Floats" in basic_types
98
+
99
+ def test_get_topics_by_category_collections(self, engine):
100
+ """Test getting Collections category topics."""
101
+ collections = engine.get_topics_by_category("Collections")
102
+ assert len(collections) == 6
103
+
104
+ def test_get_topics_by_category_nonexistent(self, engine):
105
+ """Test getting non-existent category."""
106
+ topics = engine.get_topics_by_category("NonExistent")
107
+ assert topics == []
108
+
109
+ def test_get_learning_path(self, engine):
110
+ """Test getting learning path."""
111
+ path = engine.get_learning_path()
112
+ assert isinstance(path, list)
113
+ assert len(path) == 35
114
+ # Check that it's ordered by order field
115
+ assert path[0] == "Variables and Assignment" # order=1
116
+ assert path[-1] == "Enumerate" # order=35
117
+
118
+ def test_topic_structure(self, engine):
119
+ """Test that all topics have required fields."""
120
+ for topic_name in engine.list_topics():
121
+ topic = engine.get_topic(topic_name)
122
+ assert "topic" in topic
123
+ assert "category" in topic
124
+ assert "description" in topic
125
+ assert "when_to_use" in topic
126
+ assert "example" in topic
127
+ assert "common_mistakes" in topic
128
+ assert "related_topics" in topic
129
+ assert "difficulty" in topic
130
+ assert "order" in topic
131
+
132
+ def test_categories_consistency(self, engine):
133
+ """Test that all categories are valid."""
134
+ valid_categories = {
135
+ "Basic Types", "Collections", "Control Flow", "Functions",
136
+ "String Operations", "File Operations", "Error Handling", "Advanced Basics"
137
+ }
138
+ for category in engine.categories.keys():
139
+ assert category in valid_categories
140
+
141
+ def test_related_topics_exist(self, engine):
142
+ """Test that all related topics exist in the knowledge base."""
143
+ for topic_name in engine.list_topics():
144
+ topic = engine.get_topic(topic_name)
145
+ for related_name in topic.get("related_topics", []):
146
+ assert engine.get_topic(related_name) is not None, \
147
+ f"Related topic '{related_name}' not found for '{topic_name}'"
148
+
149
+
150
+ class TestModuleFunctions:
151
+ """Test suite for module-level functions."""
152
+
153
+ def test_get_topic_function(self):
154
+ """Test module-level get_topic function."""
155
+ topic = get_topic("Lists")
156
+ assert topic is not None
157
+ assert topic["topic"] == "Lists"
158
+
159
+ def test_list_topics_function(self):
160
+ """Test module-level list_topics function."""
161
+ topics = list_topics()
162
+ assert len(topics) == 35
163
+
164
+ def test_search_topics_function(self):
165
+ """Test module-level search_topics function."""
166
+ results = search_topics("loop")
167
+ assert len(results) > 0
168
+
169
+ def test_get_random_topic_function(self):
170
+ """Test module-level get_random_topic function."""
171
+ topic = get_random_topic()
172
+ assert topic is not None
173
+ assert "topic" in topic
174
+
175
+ def test_get_learning_path_function(self):
176
+ """Test module-level get_learning_path function."""
177
+ path = get_learning_path()
178
+ assert len(path) == 35
179
+
180
+ def test_get_engine_singleton(self):
181
+ """Test that get_engine returns singleton."""
182
+ engine1 = get_engine()
183
+ engine2 = get_engine()
184
+ assert engine1 is engine2
185
+
186
+
187
+ class TestExamples:
188
+ """Test that all examples are valid Python code."""
189
+
190
+ @pytest.fixture
191
+ def engine(self):
192
+ """Create a KnowledgeEngine instance for testing."""
193
+ return KnowledgeEngine()
194
+
195
+ def test_all_examples_executable(self, engine):
196
+ """Test that all examples can be executed."""
197
+ for topic_name in engine.list_topics():
198
+ topic = engine.get_topic(topic_name)
199
+ example = topic.get("example", "")
200
+
201
+ # Try to compile the example
202
+ try:
203
+ compile(example, f"<example:{topic_name}>", "exec")
204
+ except SyntaxError as e:
205
+ pytest.fail(f"Example in '{topic_name}' has syntax error: {e}")
206
+
207
+ def test_example_contains_output_comments(self, engine):
208
+ """Test that examples have output comments where appropriate."""
209
+ for topic_name in engine.list_topics():
210
+ topic = engine.get_topic(topic_name)
211
+ example = topic.get("example", "")
212
+ # Most examples should have comments with output
213
+ # But not all - some examples don't have print statements
214
+ if "print" in example and len(example) > 50:
215
+ # Only check for comments in longer examples with print
216
+ pass # Skip this check as not all examples need comments
217
+
218
+
219
+ class TestPerformance:
220
+ """Test performance requirements."""
221
+
222
+ def test_engine_initialization_performance(self):
223
+ """Test that engine initializes quickly."""
224
+ import time
225
+ start = time.time()
226
+ engine = KnowledgeEngine()
227
+ elapsed = (time.time() - start) * 1000 # Convert to ms
228
+ assert elapsed < 100, f"Engine initialization took {elapsed}ms, should be < 100ms"
229
+
230
+ def test_search_performance(self):
231
+ """Test that search is fast."""
232
+ import time
233
+ engine = KnowledgeEngine()
234
+ start = time.time()
235
+ results = engine.search_topics("loop")
236
+ elapsed = (time.time() - start) * 1000 # Convert to ms
237
+ assert elapsed < 100, f"Search took {elapsed}ms, should be < 100ms"
238
+
239
+
240
+ if __name__ == "__main__":
241
+ pytest.main([__file__, "-v"])
@@ -0,0 +1,180 @@
1
+ """
2
+ Property-based tests for the Knowledge Engine module using Hypothesis.
3
+
4
+ **Validates: Requirements 1.2-1.5, 4.1, 4.3, 5.1, 5.2**
5
+ """
6
+
7
+ import pytest
8
+ from hypothesis import given, strategies as st
9
+ from fishertools.learn.knowledge_engine import KnowledgeEngine
10
+
11
+
12
+ @pytest.fixture
13
+ def engine():
14
+ """Create a KnowledgeEngine instance for testing."""
15
+ return KnowledgeEngine()
16
+
17
+
18
+ class TestProperty1GetTopicStructure:
19
+ """Property 1: get_topic returns correct structure
20
+
21
+ **Validates: Requirements 1.2, 2.1**
22
+ """
23
+
24
+ @given(st.sampled_from(["Lists", "Variables and Assignment", "For Loops"]))
25
+ def test_get_topic_returns_dict_with_required_fields(self, topic_name):
26
+ """For any existing topic, get_topic returns dict with all required fields."""
27
+ engine = KnowledgeEngine()
28
+ topic = engine.get_topic(topic_name)
29
+
30
+ assert topic is not None
31
+ assert isinstance(topic, dict)
32
+ assert "topic" in topic
33
+ assert "description" in topic
34
+ assert "when_to_use" in topic
35
+ assert "example" in topic
36
+ assert "common_mistakes" in topic
37
+ assert "related_topics" in topic
38
+
39
+
40
+ class TestProperty2ListTopicsComplete:
41
+ """Property 2: list_topics returns all topics
42
+
43
+ **Validates: Requirements 1.4**
44
+ """
45
+
46
+ def test_list_topics_returns_all_loaded_topics(self):
47
+ """For any Knowledge Engine, list_topics returns all loaded topics."""
48
+ engine = KnowledgeEngine()
49
+ topics_list = engine.list_topics()
50
+
51
+ # Should have exactly 35 topics
52
+ assert len(topics_list) == 35
53
+
54
+ # All topics should be strings
55
+ assert all(isinstance(t, str) for t in topics_list)
56
+
57
+ # All topics should be retrievable
58
+ for topic_name in topics_list:
59
+ assert engine.get_topic(topic_name) is not None
60
+
61
+
62
+ class TestProperty3SearchTopicsRelevant:
63
+ """Property 3: search_topics finds relevant topics
64
+
65
+ **Validates: Requirements 1.5**
66
+ """
67
+
68
+ @given(st.sampled_from(["list", "loop", "function", "string", "error"]))
69
+ def test_search_topics_returns_only_relevant(self, keyword):
70
+ """For any keyword, search_topics returns only topics containing that keyword."""
71
+ engine = KnowledgeEngine()
72
+ results = engine.search_topics(keyword)
73
+
74
+ # All results should contain the keyword (case-insensitive)
75
+ for topic_name in results:
76
+ topic = engine.get_topic(topic_name)
77
+ full_text = (
78
+ topic["topic"].lower() +
79
+ topic["description"].lower() +
80
+ topic["when_to_use"].lower()
81
+ )
82
+ assert keyword.lower() in full_text
83
+
84
+
85
+ class TestProperty4RelatedTopicsExist:
86
+ """Property 4: get_related_topics returns existing topics
87
+
88
+ **Validates: Requirements 1.5**
89
+ """
90
+
91
+ @given(st.sampled_from(["Lists", "Variables and Assignment", "For Loops", "Functions"]))
92
+ def test_related_topics_all_exist(self, topic_name):
93
+ """For any topic, all related topics exist in the knowledge base."""
94
+ engine = KnowledgeEngine()
95
+ related = engine.get_related_topics(topic_name)
96
+
97
+ # All related topics should exist
98
+ for related_name in related:
99
+ assert engine.get_topic(related_name) is not None
100
+
101
+
102
+ class TestProperty5ExamplesValid:
103
+ """Property 5: examples are valid Python code
104
+
105
+ **Validates: Requirements 5.1, 5.2**
106
+ """
107
+
108
+ @given(st.sampled_from(["Lists", "Variables and Assignment", "For Loops", "Dictionaries"]))
109
+ def test_examples_are_valid_python(self, topic_name):
110
+ """For any topic, the example is valid Python code."""
111
+ engine = KnowledgeEngine()
112
+ topic = engine.get_topic(topic_name)
113
+ example = topic.get("example", "")
114
+
115
+ # Should be compilable Python
116
+ try:
117
+ compile(example, f"<example:{topic_name}>", "exec")
118
+ except SyntaxError:
119
+ pytest.fail(f"Example in '{topic_name}' is not valid Python")
120
+
121
+
122
+ class TestProperty6CategoriesConsistent:
123
+ """Property 6: categories are consistent
124
+
125
+ **Validates: Requirements 4.1**
126
+ """
127
+
128
+ def test_all_topics_have_valid_category(self):
129
+ """For any topic, the category is one of the valid categories."""
130
+ engine = KnowledgeEngine()
131
+ valid_categories = {
132
+ "Basic Types", "Collections", "Control Flow", "Functions",
133
+ "String Operations", "File Operations", "Error Handling", "Advanced Basics"
134
+ }
135
+
136
+ for topic_name in engine.list_topics():
137
+ topic = engine.get_topic(topic_name)
138
+ assert topic["category"] in valid_categories
139
+
140
+
141
+ class TestProperty7RelatedTopicsConsistent:
142
+ """Property 7: related_topics contain existing topics
143
+
144
+ **Validates: Requirements 4.1**
145
+ """
146
+
147
+ def test_all_related_topics_exist(self):
148
+ """For any topic, all related topics exist in the knowledge base."""
149
+ engine = KnowledgeEngine()
150
+
151
+ for topic_name in engine.list_topics():
152
+ topic = engine.get_topic(topic_name)
153
+ for related_name in topic.get("related_topics", []):
154
+ assert engine.get_topic(related_name) is not None, \
155
+ f"Related topic '{related_name}' not found for '{topic_name}'"
156
+
157
+
158
+ class TestProperty8LearningPathOrdered:
159
+ """Property 8: get_learning_path returns topics in correct order
160
+
161
+ **Validates: Requirements 4.3**
162
+ """
163
+
164
+ def test_learning_path_ordered_by_difficulty(self):
165
+ """For any Knowledge Engine, learning path is ordered from simple to complex."""
166
+ engine = KnowledgeEngine()
167
+ path = engine.get_learning_path()
168
+
169
+ # Should have all 35 topics
170
+ assert len(path) == 35
171
+
172
+ # Should be ordered by order field
173
+ for i in range(len(path) - 1):
174
+ current_topic = engine.get_topic(path[i])
175
+ next_topic = engine.get_topic(path[i + 1])
176
+ assert current_topic["order"] <= next_topic["order"]
177
+
178
+
179
+ if __name__ == "__main__":
180
+ pytest.main([__file__, "-v"])
@@ -0,0 +1,46 @@
1
+ """
2
+ Patterns module for fishertools.
3
+
4
+ This module provides reusable pattern templates for common programming tasks,
5
+ including menu systems, data storage, logging, and command-line interfaces.
6
+
7
+ Available patterns:
8
+ - simple_menu: Interactive console menu
9
+ - JSONStorage: JSON-based data persistence
10
+ - SimpleLogger: Simple file-based logging
11
+ - SimpleCLI: Command-line interface builder
12
+
13
+ Example:
14
+ from fishertools.patterns import simple_menu, JSONStorage, SimpleLogger, SimpleCLI
15
+
16
+ # Use simple_menu
17
+ def greet():
18
+ print("Hello!")
19
+
20
+ simple_menu({"Greet": greet})
21
+
22
+ # Use JSONStorage
23
+ storage = JSONStorage("data.json")
24
+ storage.save({"name": "Alice"})
25
+ data = storage.load()
26
+
27
+ # Use SimpleLogger
28
+ logger = SimpleLogger("app.log")
29
+ logger.info("Application started")
30
+
31
+ # Use SimpleCLI
32
+ cli = SimpleCLI("myapp", "My application")
33
+
34
+ @cli.command("greet", "Greet someone")
35
+ def greet_cmd(name):
36
+ print(f"Hello, {name}!")
37
+
38
+ cli.run()
39
+ """
40
+
41
+ from fishertools.patterns.menu import simple_menu
42
+ from fishertools.patterns.storage import JSONStorage
43
+ from fishertools.patterns.logger import SimpleLogger
44
+ from fishertools.patterns.cli import SimpleCLI
45
+
46
+ __all__ = ["simple_menu", "JSONStorage", "SimpleLogger", "SimpleCLI"]
@@ -0,0 +1,175 @@
1
+ """
2
+ SimpleCLI pattern for command-line interface creation.
3
+
4
+ This module provides the SimpleCLI class for creating command-line interfaces
5
+ with minimal boilerplate code. Commands are registered via decorators and
6
+ executed based on command-line arguments.
7
+
8
+ Example:
9
+ cli = SimpleCLI("myapp", "My application")
10
+
11
+ @cli.command("greet", "Greet someone")
12
+ def greet(name):
13
+ print(f"Hello, {name}!")
14
+
15
+ @cli.command("goodbye", "Say goodbye")
16
+ def goodbye(name):
17
+ print(f"Goodbye, {name}!")
18
+
19
+ cli.run()
20
+ """
21
+
22
+
23
+ class SimpleCLI:
24
+ """
25
+ Create command-line interfaces with minimal boilerplate.
26
+
27
+ This class provides a decorator-based system for registering commands and
28
+ a run() method for parsing and executing them. Supports --help flag and
29
+ graceful error handling.
30
+
31
+ Parameters:
32
+ name (str): The name of the CLI application.
33
+ description (str): A description of what the application does.
34
+
35
+ Attributes:
36
+ name (str): The application name.
37
+ description (str): The application description.
38
+
39
+ Methods:
40
+ command(name, description): Decorator to register a command.
41
+ run(args=None): Parse and execute commands.
42
+
43
+ Raises:
44
+ ValueError: If command registration fails.
45
+
46
+ Example:
47
+ cli = SimpleCLI("calculator", "Simple calculator")
48
+
49
+ @cli.command("add", "Add two numbers")
50
+ def add(a, b):
51
+ print(f"{a} + {b} = {int(a) + int(b)}")
52
+
53
+ @cli.command("multiply", "Multiply two numbers")
54
+ def multiply(a, b):
55
+ print(f"{a} * {b} = {int(a) * int(b)}")
56
+
57
+ cli.run()
58
+
59
+ Note:
60
+ - Commands are registered via @cli.command() decorator
61
+ - Use --help or -h to show available commands
62
+ - Invalid commands display an error and show help
63
+ - Command handlers receive arguments as strings
64
+ - Each command can have its own parameters
65
+ """
66
+
67
+ def __init__(self, name, description):
68
+ """
69
+ Initialize SimpleCLI with name and description.
70
+
71
+ Parameters:
72
+ name (str): The name of the CLI application.
73
+ description (str): A description of the application.
74
+ """
75
+ self.name = name
76
+ self.description = description
77
+ self.commands = {}
78
+
79
+ def command(self, name, description):
80
+ """
81
+ Decorator to register a command.
82
+
83
+ Parameters:
84
+ name (str): The command name.
85
+ description (str): A description of what the command does.
86
+
87
+ Returns:
88
+ callable: A decorator function that registers the command.
89
+
90
+ Example:
91
+ @cli.command("status", "Show application status")
92
+ def show_status():
93
+ print("Application is running")
94
+ """
95
+ def decorator(func):
96
+ self.commands[name] = {
97
+ "handler": func,
98
+ "description": description
99
+ }
100
+ return func
101
+ return decorator
102
+
103
+ def run(self, args=None):
104
+ """
105
+ Parse and execute commands.
106
+
107
+ Parses command-line arguments and executes the appropriate command
108
+ handler. Shows help on --help or invalid commands.
109
+
110
+ Parameters:
111
+ args (list, optional): Command-line arguments. If None, uses sys.argv[1:].
112
+
113
+ Returns:
114
+ None
115
+
116
+ Raises:
117
+ SystemExit: On --help or invalid command (after displaying message).
118
+
119
+ Example:
120
+ cli.run() # Uses sys.argv
121
+ cli.run(["add", "5", "3"]) # Uses provided args
122
+ """
123
+ import sys
124
+
125
+ # Use provided args or sys.argv[1:]
126
+ if args is None:
127
+ args = sys.argv[1:]
128
+
129
+ # Handle no arguments or help flags
130
+ if not args or args[0] in ("--help", "-h"):
131
+ self._show_help()
132
+ return
133
+
134
+ # Get the command name
135
+ command_name = args[0]
136
+ command_args = args[1:]
137
+
138
+ # Check if command exists
139
+ if command_name not in self.commands:
140
+ print(f"Error: Unknown command '{command_name}'")
141
+ self._show_help()
142
+ return
143
+
144
+ # Execute the command
145
+ try:
146
+ handler = self.commands[command_name]["handler"]
147
+ handler(*command_args)
148
+ except TypeError as e:
149
+ print(f"Error: Invalid arguments for command '{command_name}'")
150
+ print(f"Details: {e}")
151
+ except Exception as e:
152
+ print(f"Error: Command '{command_name}' failed")
153
+ print(f"Details: {e}")
154
+
155
+ def _show_help(self):
156
+ """
157
+ Display help information.
158
+
159
+ Shows the application name, description, and all available commands
160
+ with their descriptions.
161
+
162
+ Returns:
163
+ None
164
+ """
165
+ print(f"\n{self.name}")
166
+ print(f"{self.description}")
167
+ print("\nAvailable commands:")
168
+
169
+ if not self.commands:
170
+ print(" (no commands registered)")
171
+ else:
172
+ for cmd_name, cmd_info in self.commands.items():
173
+ print(f" {cmd_name:<20} {cmd_info['description']}")
174
+
175
+ print()