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,307 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Property-based tests for the explain() function in fishertools.learn.
|
|
3
|
+
|
|
4
|
+
Tests the correctness properties of the explain() function using hypothesis
|
|
5
|
+
for property-based testing.
|
|
6
|
+
|
|
7
|
+
**Validates: Requirements 1.1, 1.2, 1.6**
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
from hypothesis import given, strategies as st, assume
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
from fishertools.learn.examples import explain
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Get the list of valid topics from the explanations.json file
|
|
19
|
+
def get_valid_topics():
|
|
20
|
+
"""Get all valid topics from explanations.json."""
|
|
21
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
22
|
+
explanations_path = os.path.join(
|
|
23
|
+
current_dir, '..', '..', 'fishertools', 'learn', 'explanations.json'
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
with open(explanations_path, 'r', encoding='utf-8') as f:
|
|
27
|
+
explanations = json.load(f)
|
|
28
|
+
|
|
29
|
+
return sorted(explanations.keys())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
VALID_TOPICS = get_valid_topics()
|
|
33
|
+
|
|
34
|
+
# Define the required topics from the specification
|
|
35
|
+
REQUIRED_TOPICS = {
|
|
36
|
+
'int', 'float', 'str', 'bool', 'list', 'tuple', 'set', 'dict',
|
|
37
|
+
'if', 'for', 'while', 'break', 'continue',
|
|
38
|
+
'function', 'return', 'lambda', '*args', '**kwargs',
|
|
39
|
+
'try', 'except', 'finally', 'raise',
|
|
40
|
+
'open', 'read', 'write', 'with'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestExplainReturnsValidStructure:
|
|
45
|
+
"""
|
|
46
|
+
Property 1: Explain Returns Valid Structure
|
|
47
|
+
|
|
48
|
+
For any valid topic name, calling explain(topic) should return a dictionary
|
|
49
|
+
containing exactly the keys: description, when_to_use, and example, with all
|
|
50
|
+
values being non-empty strings.
|
|
51
|
+
|
|
52
|
+
**Validates: Requirements 1.1, 1.3, 1.4, 1.5**
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
@given(st.sampled_from(VALID_TOPICS))
|
|
56
|
+
def test_explain_returns_dict_with_correct_keys(self, topic):
|
|
57
|
+
"""Test that explain() returns a dict with the correct keys."""
|
|
58
|
+
result = explain(topic)
|
|
59
|
+
|
|
60
|
+
# Should return a dictionary
|
|
61
|
+
assert isinstance(result, dict)
|
|
62
|
+
|
|
63
|
+
# Should have exactly the required keys
|
|
64
|
+
expected_keys = {'description', 'when_to_use', 'example'}
|
|
65
|
+
assert set(result.keys()) == expected_keys
|
|
66
|
+
|
|
67
|
+
@given(st.sampled_from(VALID_TOPICS))
|
|
68
|
+
def test_explain_returns_non_empty_strings(self, topic):
|
|
69
|
+
"""Test that all values in the returned dict are non-empty strings."""
|
|
70
|
+
result = explain(topic)
|
|
71
|
+
|
|
72
|
+
# All values should be strings
|
|
73
|
+
assert isinstance(result['description'], str)
|
|
74
|
+
assert isinstance(result['when_to_use'], str)
|
|
75
|
+
assert isinstance(result['example'], str)
|
|
76
|
+
|
|
77
|
+
# All values should be non-empty
|
|
78
|
+
assert len(result['description']) > 0
|
|
79
|
+
assert len(result['when_to_use']) > 0
|
|
80
|
+
assert len(result['example']) > 0
|
|
81
|
+
|
|
82
|
+
@given(st.sampled_from(VALID_TOPICS))
|
|
83
|
+
def test_explain_description_is_meaningful(self, topic):
|
|
84
|
+
"""Test that description contains meaningful content."""
|
|
85
|
+
result = explain(topic)
|
|
86
|
+
description = result['description']
|
|
87
|
+
|
|
88
|
+
# Description should be at least 20 characters
|
|
89
|
+
assert len(description) >= 20
|
|
90
|
+
|
|
91
|
+
# Description should contain alphabetic characters
|
|
92
|
+
assert any(c.isalpha() for c in description)
|
|
93
|
+
|
|
94
|
+
@given(st.sampled_from(VALID_TOPICS))
|
|
95
|
+
def test_explain_when_to_use_is_meaningful(self, topic):
|
|
96
|
+
"""Test that when_to_use contains meaningful content."""
|
|
97
|
+
result = explain(topic)
|
|
98
|
+
when_to_use = result['when_to_use']
|
|
99
|
+
|
|
100
|
+
# when_to_use should be at least 20 characters
|
|
101
|
+
assert len(when_to_use) >= 20
|
|
102
|
+
|
|
103
|
+
# when_to_use should contain alphabetic characters
|
|
104
|
+
assert any(c.isalpha() for c in when_to_use)
|
|
105
|
+
|
|
106
|
+
@given(st.sampled_from(VALID_TOPICS))
|
|
107
|
+
def test_explain_example_is_code(self, topic):
|
|
108
|
+
"""Test that example contains code-like content."""
|
|
109
|
+
result = explain(topic)
|
|
110
|
+
example = result['example']
|
|
111
|
+
|
|
112
|
+
# Example should be at least 10 characters
|
|
113
|
+
assert len(example) >= 10
|
|
114
|
+
|
|
115
|
+
# Example should contain code-like patterns (=, (), etc.)
|
|
116
|
+
code_indicators = ['=', '(', ')', '[', ']', '{', '}', 'print', 'def', 'if', 'for']
|
|
117
|
+
assert any(indicator in example for indicator in code_indicators)
|
|
118
|
+
|
|
119
|
+
@given(st.sampled_from(VALID_TOPICS))
|
|
120
|
+
def test_explain_case_insensitive(self, topic):
|
|
121
|
+
"""Test that explain() is case insensitive."""
|
|
122
|
+
result_lower = explain(topic.lower())
|
|
123
|
+
result_upper = explain(topic.upper())
|
|
124
|
+
result_mixed = explain(topic.capitalize())
|
|
125
|
+
|
|
126
|
+
# All should return the same result
|
|
127
|
+
assert result_lower == result_upper
|
|
128
|
+
assert result_lower == result_mixed
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TestExplainRejectsInvalidTopics:
|
|
132
|
+
"""
|
|
133
|
+
Property 2: Explain Rejects Invalid Topics
|
|
134
|
+
|
|
135
|
+
For any invalid topic name, calling explain(topic) should raise a ValueError
|
|
136
|
+
exception.
|
|
137
|
+
|
|
138
|
+
**Validates: Requirements 1.2**
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
@given(st.text(min_size=1))
|
|
142
|
+
def test_explain_raises_valueerror_for_invalid_topics(self, topic):
|
|
143
|
+
"""Test that explain() raises ValueError for invalid topics."""
|
|
144
|
+
# Skip if the topic happens to be valid (after normalization)
|
|
145
|
+
assume(topic.lower().strip() not in VALID_TOPICS)
|
|
146
|
+
|
|
147
|
+
# Should raise ValueError
|
|
148
|
+
with pytest.raises(ValueError):
|
|
149
|
+
explain(topic)
|
|
150
|
+
|
|
151
|
+
@given(st.text(min_size=1))
|
|
152
|
+
def test_explain_error_message_lists_available_topics(self, topic):
|
|
153
|
+
"""Test that ValueError message lists available topics."""
|
|
154
|
+
# Skip if the topic happens to be valid (after normalization)
|
|
155
|
+
assume(topic.lower().strip() not in VALID_TOPICS)
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
explain(topic)
|
|
159
|
+
pytest.fail("Should have raised ValueError")
|
|
160
|
+
except ValueError as e:
|
|
161
|
+
error_msg = str(e)
|
|
162
|
+
|
|
163
|
+
# Error message should mention available topics
|
|
164
|
+
assert "Available topics" in error_msg or "available topics" in error_msg.lower()
|
|
165
|
+
|
|
166
|
+
# Error message should contain at least one valid topic
|
|
167
|
+
assert any(valid_topic in error_msg for valid_topic in VALID_TOPICS)
|
|
168
|
+
|
|
169
|
+
def test_explain_rejects_empty_string(self):
|
|
170
|
+
"""Test that explain() rejects empty string."""
|
|
171
|
+
with pytest.raises(ValueError):
|
|
172
|
+
explain("")
|
|
173
|
+
|
|
174
|
+
def test_explain_rejects_whitespace_only(self):
|
|
175
|
+
"""Test that explain() rejects whitespace-only strings."""
|
|
176
|
+
with pytest.raises(ValueError):
|
|
177
|
+
explain(" ")
|
|
178
|
+
|
|
179
|
+
def test_explain_rejects_special_characters(self):
|
|
180
|
+
"""Test that explain() rejects topics with only special characters."""
|
|
181
|
+
with pytest.raises(ValueError):
|
|
182
|
+
explain("!@#$%^&*()")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class TestExplainAllRequiredTopicsAvailable:
|
|
186
|
+
"""
|
|
187
|
+
Property 3: All Required Topics Available
|
|
188
|
+
|
|
189
|
+
For all required topics (int, float, str, bool, list, tuple, set, dict, if,
|
|
190
|
+
for, while, break, continue, function, return, lambda, *args, **kwargs, try,
|
|
191
|
+
except, finally, raise, open, read, write, with), calling explain(topic)
|
|
192
|
+
should succeed and return a valid explanation.
|
|
193
|
+
|
|
194
|
+
**Validates: Requirements 1.6, 2.1, 3.1, 4.1, 5.1, 6.1**
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
@given(st.sampled_from(sorted(REQUIRED_TOPICS)))
|
|
198
|
+
def test_all_required_topics_available(self, topic):
|
|
199
|
+
"""Test that all required topics are available."""
|
|
200
|
+
# Should not raise an exception
|
|
201
|
+
result = explain(topic)
|
|
202
|
+
|
|
203
|
+
# Should return valid structure
|
|
204
|
+
assert isinstance(result, dict)
|
|
205
|
+
assert set(result.keys()) == {'description', 'when_to_use', 'example'}
|
|
206
|
+
assert all(isinstance(v, str) and len(v) > 0 for v in result.values())
|
|
207
|
+
|
|
208
|
+
def test_all_required_topics_explicitly(self):
|
|
209
|
+
"""Test all required topics explicitly."""
|
|
210
|
+
for topic in REQUIRED_TOPICS:
|
|
211
|
+
result = explain(topic)
|
|
212
|
+
|
|
213
|
+
# Verify structure
|
|
214
|
+
assert isinstance(result, dict)
|
|
215
|
+
assert 'description' in result
|
|
216
|
+
assert 'when_to_use' in result
|
|
217
|
+
assert 'example' in result
|
|
218
|
+
|
|
219
|
+
# Verify non-empty values
|
|
220
|
+
assert len(result['description']) > 0
|
|
221
|
+
assert len(result['when_to_use']) > 0
|
|
222
|
+
assert len(result['example']) > 0
|
|
223
|
+
|
|
224
|
+
def test_required_topics_count(self):
|
|
225
|
+
"""Test that all required topics are present."""
|
|
226
|
+
available_topics = set(VALID_TOPICS)
|
|
227
|
+
|
|
228
|
+
# All required topics should be available
|
|
229
|
+
missing_topics = REQUIRED_TOPICS - available_topics
|
|
230
|
+
assert len(missing_topics) == 0, f"Missing topics: {missing_topics}"
|
|
231
|
+
|
|
232
|
+
def test_required_topics_have_meaningful_content(self):
|
|
233
|
+
"""Test that required topics have meaningful explanations."""
|
|
234
|
+
for topic in REQUIRED_TOPICS:
|
|
235
|
+
result = explain(topic)
|
|
236
|
+
|
|
237
|
+
# Each field should have substantial content
|
|
238
|
+
assert len(result['description']) >= 20
|
|
239
|
+
assert len(result['when_to_use']) >= 20
|
|
240
|
+
assert len(result['example']) >= 10
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class TestExplainEdgeCases:
|
|
244
|
+
"""Test edge cases and boundary conditions."""
|
|
245
|
+
|
|
246
|
+
def test_explain_with_leading_trailing_whitespace(self):
|
|
247
|
+
"""Test that explain() handles leading/trailing whitespace."""
|
|
248
|
+
result1 = explain("list")
|
|
249
|
+
result2 = explain(" list ")
|
|
250
|
+
result3 = explain("\tlist\n")
|
|
251
|
+
|
|
252
|
+
# All should return the same result
|
|
253
|
+
assert result1 == result2
|
|
254
|
+
assert result1 == result3
|
|
255
|
+
|
|
256
|
+
def test_explain_consistency_across_calls(self):
|
|
257
|
+
"""Test that explain() returns consistent results across multiple calls."""
|
|
258
|
+
result1 = explain("dict")
|
|
259
|
+
result2 = explain("dict")
|
|
260
|
+
result3 = explain("dict")
|
|
261
|
+
|
|
262
|
+
# All calls should return identical results
|
|
263
|
+
assert result1 == result2
|
|
264
|
+
assert result2 == result3
|
|
265
|
+
|
|
266
|
+
@given(st.sampled_from(VALID_TOPICS))
|
|
267
|
+
def test_explain_result_is_independent(self, topic):
|
|
268
|
+
"""Test that modifying returned dict doesn't affect future calls."""
|
|
269
|
+
result1 = explain(topic)
|
|
270
|
+
|
|
271
|
+
# Modify the returned dict
|
|
272
|
+
result1['description'] = "MODIFIED"
|
|
273
|
+
result1['new_key'] = "NEW VALUE"
|
|
274
|
+
|
|
275
|
+
# Next call should return unmodified result
|
|
276
|
+
result2 = explain(topic)
|
|
277
|
+
assert result2['description'] != "MODIFIED"
|
|
278
|
+
assert 'new_key' not in result2
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class TestExplainIntegration:
|
|
282
|
+
"""Integration tests for explain() function."""
|
|
283
|
+
|
|
284
|
+
def test_explain_with_all_valid_topics(self):
|
|
285
|
+
"""Test explain() with all valid topics."""
|
|
286
|
+
for topic in VALID_TOPICS:
|
|
287
|
+
result = explain(topic)
|
|
288
|
+
|
|
289
|
+
# Should return valid structure
|
|
290
|
+
assert isinstance(result, dict)
|
|
291
|
+
assert set(result.keys()) == {'description', 'when_to_use', 'example'}
|
|
292
|
+
assert all(isinstance(v, str) and len(v) > 0 for v in result.values())
|
|
293
|
+
|
|
294
|
+
def test_explain_error_message_quality(self):
|
|
295
|
+
"""Test that error messages are helpful."""
|
|
296
|
+
try:
|
|
297
|
+
explain("nonexistent_topic_xyz")
|
|
298
|
+
pytest.fail("Should have raised ValueError")
|
|
299
|
+
except ValueError as e:
|
|
300
|
+
error_msg = str(e)
|
|
301
|
+
|
|
302
|
+
# Error message should be informative
|
|
303
|
+
assert "nonexistent_topic_xyz" in error_msg
|
|
304
|
+
assert "Available topics" in error_msg or "available" in error_msg.lower()
|
|
305
|
+
|
|
306
|
+
# Should list some topics
|
|
307
|
+
assert len(error_msg) > 50 # Substantial message
|