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.
- fishertools/__init__.py +16 -5
- fishertools/errors/__init__.py +11 -3
- fishertools/errors/exception_types.py +282 -0
- fishertools/errors/explainer.py +87 -1
- fishertools/errors/models.py +73 -1
- fishertools/errors/patterns.py +40 -0
- fishertools/examples/cli_example.py +156 -0
- fishertools/examples/learn_example.py +65 -0
- fishertools/examples/logger_example.py +176 -0
- fishertools/examples/menu_example.py +101 -0
- fishertools/examples/storage_example.py +175 -0
- fishertools/input_utils.py +185 -0
- fishertools/learn/__init__.py +19 -2
- fishertools/learn/examples.py +88 -1
- fishertools/learn/knowledge_engine.py +321 -0
- fishertools/learn/repl/__init__.py +19 -0
- fishertools/learn/repl/cli.py +31 -0
- fishertools/learn/repl/code_sandbox.py +229 -0
- fishertools/learn/repl/command_handler.py +544 -0
- fishertools/learn/repl/command_parser.py +165 -0
- fishertools/learn/repl/engine.py +479 -0
- fishertools/learn/repl/models.py +121 -0
- fishertools/learn/repl/session_manager.py +284 -0
- fishertools/learn/repl/test_code_sandbox.py +261 -0
- fishertools/learn/repl/test_code_sandbox_pbt.py +148 -0
- fishertools/learn/repl/test_command_handler.py +224 -0
- fishertools/learn/repl/test_command_handler_pbt.py +189 -0
- fishertools/learn/repl/test_command_parser.py +160 -0
- fishertools/learn/repl/test_command_parser_pbt.py +100 -0
- fishertools/learn/repl/test_engine.py +190 -0
- fishertools/learn/repl/test_session_manager.py +310 -0
- fishertools/learn/repl/test_session_manager_pbt.py +182 -0
- fishertools/learn/test_knowledge_engine.py +241 -0
- fishertools/learn/test_knowledge_engine_pbt.py +180 -0
- fishertools/patterns/__init__.py +46 -0
- fishertools/patterns/cli.py +175 -0
- fishertools/patterns/logger.py +140 -0
- fishertools/patterns/menu.py +99 -0
- fishertools/patterns/storage.py +127 -0
- fishertools/readme_transformer.py +631 -0
- fishertools/safe/__init__.py +6 -1
- fishertools/safe/files.py +329 -1
- fishertools/transform_readme.py +105 -0
- fishertools-0.4.0.dist-info/METADATA +104 -0
- fishertools-0.4.0.dist-info/RECORD +131 -0
- {fishertools-0.2.1.dist-info → fishertools-0.4.0.dist-info}/WHEEL +1 -1
- tests/test_documentation_properties.py +329 -0
- tests/test_documentation_structure.py +349 -0
- tests/test_errors/test_exception_types.py +446 -0
- tests/test_errors/test_exception_types_pbt.py +333 -0
- tests/test_errors/test_patterns.py +52 -0
- tests/test_input_utils/__init__.py +1 -0
- tests/test_input_utils/test_input_utils.py +65 -0
- tests/test_learn/test_examples.py +179 -1
- tests/test_learn/test_explain_properties.py +307 -0
- tests/test_patterns_cli.py +611 -0
- tests/test_patterns_docstrings.py +473 -0
- tests/test_patterns_logger.py +465 -0
- tests/test_patterns_menu.py +440 -0
- tests/test_patterns_storage.py +447 -0
- tests/test_readme_enhancements_v0_3_1.py +2036 -0
- tests/test_readme_transformer/__init__.py +1 -0
- tests/test_readme_transformer/test_readme_infrastructure.py +1023 -0
- tests/test_readme_transformer/test_transform_readme_integration.py +431 -0
- tests/test_safe/test_files.py +726 -1
- fishertools-0.2.1.dist-info/METADATA +0 -256
- fishertools-0.2.1.dist-info/RECORD +0 -81
- {fishertools-0.2.1.dist-info → fishertools-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {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()
|