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,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