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,333 @@
1
+ """
2
+ Property-based tests for exception type identification module.
3
+
4
+ These tests use Hypothesis to verify that exception type identification
5
+ works correctly across a wide range of inputs and scenarios.
6
+
7
+ **Validates: Requirements 1.1**
8
+ """
9
+
10
+ import pytest
11
+ from hypothesis import given, strategies as st, assume
12
+ from fishertools.errors.exception_types import (
13
+ identify_exception_type,
14
+ get_exception_type_info,
15
+ is_supported_exception_type,
16
+ get_exception_type_mapping,
17
+ get_supported_exception_types,
18
+ ExceptionTypeInfo,
19
+ EXCEPTION_TYPE_MAPPING
20
+ )
21
+
22
+
23
+ # Strategy for generating supported exception types
24
+ SUPPORTED_EXCEPTION_TYPES = [
25
+ TypeError, ValueError, IndexError, KeyError, AttributeError,
26
+ FileNotFoundError, PermissionError, ZeroDivisionError, NameError
27
+ ]
28
+
29
+
30
+ @st.composite
31
+ def supported_exceptions(draw):
32
+ """Strategy for generating supported exception instances."""
33
+ exc_type = draw(st.sampled_from(SUPPORTED_EXCEPTION_TYPES))
34
+ message = draw(st.text(min_size=0, max_size=100))
35
+ return exc_type(message)
36
+
37
+
38
+ @st.composite
39
+ def custom_exceptions(draw):
40
+ """Strategy for generating custom exception instances."""
41
+ class CustomException(Exception):
42
+ pass
43
+
44
+ message = draw(st.text(min_size=0, max_size=100))
45
+ return CustomException(message)
46
+
47
+
48
+ class TestExceptionTypeIdentificationProperty:
49
+ """Property-based tests for exception type identification.
50
+
51
+ **Property 1: Exception Type Identification**
52
+ *For any* exception object, the Error_Explainer should correctly identify
53
+ its type (TypeError, ValueError, etc.)
54
+ **Validates: Requirements 1.1**
55
+ """
56
+
57
+ @given(supported_exceptions())
58
+ def test_identify_returns_string(self, exc):
59
+ """Property: identify_exception_type always returns a string."""
60
+ result = identify_exception_type(exc)
61
+ assert isinstance(result, str)
62
+ assert len(result) > 0
63
+
64
+ @given(supported_exceptions())
65
+ def test_identify_returns_correct_type_name(self, exc):
66
+ """Property: identify_exception_type returns the correct exception type name."""
67
+ result = identify_exception_type(exc)
68
+ expected = type(exc).__name__
69
+ assert result == expected
70
+
71
+ @given(st.sampled_from(SUPPORTED_EXCEPTION_TYPES))
72
+ def test_identify_all_supported_types(self, exc_type):
73
+ """Property: identify_exception_type works for all supported exception types."""
74
+ exc = exc_type("test message")
75
+ result = identify_exception_type(exc)
76
+
77
+ # Result should be the exception type name
78
+ assert result == exc_type.__name__
79
+ # Result should be in the list of supported types
80
+ assert result in get_supported_exception_types()
81
+
82
+ @given(supported_exceptions())
83
+ def test_identify_consistent_with_type(self, exc):
84
+ """Property: identify_exception_type is consistent with type()."""
85
+ result = identify_exception_type(exc)
86
+ assert result == type(exc).__name__
87
+
88
+ @given(custom_exceptions())
89
+ def test_identify_custom_exception(self, exc):
90
+ """Property: identify_exception_type works for custom exceptions."""
91
+ result = identify_exception_type(exc)
92
+ assert result == "CustomException"
93
+
94
+ @given(supported_exceptions())
95
+ def test_identify_idempotent(self, exc):
96
+ """Property: identify_exception_type is idempotent."""
97
+ result1 = identify_exception_type(exc)
98
+ result2 = identify_exception_type(exc)
99
+ assert result1 == result2
100
+
101
+ @given(st.text(min_size=0, max_size=100))
102
+ def test_identify_with_different_messages(self, message):
103
+ """Property: identify_exception_type ignores exception message."""
104
+ exc1 = ValueError(message)
105
+ exc2 = ValueError("different message")
106
+
107
+ result1 = identify_exception_type(exc1)
108
+ result2 = identify_exception_type(exc2)
109
+
110
+ assert result1 == result2 == "ValueError"
111
+
112
+
113
+ class TestGetExceptionTypeInfoProperty:
114
+ """Property-based tests for get_exception_type_info function.
115
+
116
+ **Property 2: Explanation Completeness**
117
+ *For any* supported exception type, the explanation should contain a
118
+ non-empty simple explanation, at least one fix suggestion, and a code example
119
+ **Validates: Requirements 1.2, 1.3, 1.4**
120
+ """
121
+
122
+ @given(supported_exceptions())
123
+ def test_get_info_returns_exception_type_info(self, exc):
124
+ """Property: get_exception_type_info always returns ExceptionTypeInfo."""
125
+ result = get_exception_type_info(exc)
126
+ assert isinstance(result, ExceptionTypeInfo)
127
+
128
+ @given(supported_exceptions())
129
+ def test_get_info_has_all_fields(self, exc):
130
+ """Property: ExceptionTypeInfo has all required fields."""
131
+ info = get_exception_type_info(exc)
132
+
133
+ assert hasattr(info, 'exception_class')
134
+ assert hasattr(info, 'name')
135
+ assert hasattr(info, 'description')
136
+ assert hasattr(info, 'common_causes')
137
+
138
+ @given(supported_exceptions())
139
+ def test_get_info_fields_are_correct_types(self, exc):
140
+ """Property: ExceptionTypeInfo fields have correct types."""
141
+ info = get_exception_type_info(exc)
142
+
143
+ assert isinstance(info.exception_class, type)
144
+ assert isinstance(info.name, str)
145
+ assert isinstance(info.description, str)
146
+ assert isinstance(info.common_causes, list)
147
+
148
+ @given(supported_exceptions())
149
+ def test_get_info_fields_are_non_empty(self, exc):
150
+ """Property: ExceptionTypeInfo fields are non-empty."""
151
+ info = get_exception_type_info(exc)
152
+
153
+ assert len(info.name) > 0
154
+ assert len(info.description) > 0
155
+ assert len(info.common_causes) > 0
156
+
157
+ @given(supported_exceptions())
158
+ def test_get_info_common_causes_are_strings(self, exc):
159
+ """Property: All common causes are non-empty strings."""
160
+ info = get_exception_type_info(exc)
161
+
162
+ for cause in info.common_causes:
163
+ assert isinstance(cause, str)
164
+ assert len(cause) > 0
165
+
166
+ @given(supported_exceptions())
167
+ def test_get_info_exception_class_matches(self, exc):
168
+ """Property: ExceptionTypeInfo.exception_class matches the exception type."""
169
+ info = get_exception_type_info(exc)
170
+ assert info.exception_class == type(exc)
171
+
172
+ @given(supported_exceptions())
173
+ def test_get_info_name_matches_type(self, exc):
174
+ """Property: ExceptionTypeInfo.name matches the exception type name."""
175
+ info = get_exception_type_info(exc)
176
+ assert info.name == type(exc).__name__
177
+
178
+ @given(custom_exceptions())
179
+ def test_get_info_custom_exception(self, exc):
180
+ """Property: get_exception_type_info works for custom exceptions."""
181
+ info = get_exception_type_info(exc)
182
+
183
+ assert info.name == "CustomException"
184
+ assert info.exception_class == type(exc)
185
+ assert len(info.description) > 0
186
+
187
+ @given(supported_exceptions())
188
+ def test_get_info_idempotent(self, exc):
189
+ """Property: get_exception_type_info is idempotent."""
190
+ info1 = get_exception_type_info(exc)
191
+ info2 = get_exception_type_info(exc)
192
+
193
+ assert info1.name == info2.name
194
+ assert info1.description == info2.description
195
+ assert info1.common_causes == info2.common_causes
196
+
197
+
198
+ class TestIsSupportedExceptionTypeProperty:
199
+ """Property-based tests for is_supported_exception_type function."""
200
+
201
+ @given(supported_exceptions())
202
+ def test_supported_returns_boolean(self, exc):
203
+ """Property: is_supported_exception_type always returns a boolean."""
204
+ result = is_supported_exception_type(exc)
205
+ assert isinstance(result, bool)
206
+
207
+ @given(supported_exceptions())
208
+ def test_supported_returns_true_for_supported(self, exc):
209
+ """Property: is_supported_exception_type returns True for supported types."""
210
+ result = is_supported_exception_type(exc)
211
+ assert result is True
212
+
213
+ @given(custom_exceptions())
214
+ def test_supported_returns_false_for_custom(self, exc):
215
+ """Property: is_supported_exception_type returns False for custom types."""
216
+ result = is_supported_exception_type(exc)
217
+ assert result is False
218
+
219
+ @given(st.sampled_from(SUPPORTED_EXCEPTION_TYPES))
220
+ def test_supported_all_supported_types(self, exc_type):
221
+ """Property: is_supported_exception_type returns True for all supported types."""
222
+ exc = exc_type("test")
223
+ assert is_supported_exception_type(exc) is True
224
+
225
+ @given(supported_exceptions())
226
+ def test_supported_idempotent(self, exc):
227
+ """Property: is_supported_exception_type is idempotent."""
228
+ result1 = is_supported_exception_type(exc)
229
+ result2 = is_supported_exception_type(exc)
230
+ assert result1 == result2
231
+
232
+
233
+ class TestExceptionTypeMappingProperty:
234
+ """Property-based tests for exception type mapping functions."""
235
+
236
+ def test_mapping_contains_all_supported_types(self):
237
+ """Property: get_exception_type_mapping contains all supported types."""
238
+ mapping = get_exception_type_mapping()
239
+
240
+ for exc_type in SUPPORTED_EXCEPTION_TYPES:
241
+ assert exc_type in mapping
242
+
243
+ def test_mapping_values_are_valid_info(self):
244
+ """Property: All mapping values are valid ExceptionTypeInfo objects."""
245
+ mapping = get_exception_type_mapping()
246
+
247
+ for exc_type, info in mapping.items():
248
+ assert isinstance(info, ExceptionTypeInfo)
249
+ assert info.exception_class == exc_type
250
+ assert isinstance(info.name, str)
251
+ assert len(info.name) > 0
252
+ assert isinstance(info.description, str)
253
+ assert len(info.description) > 0
254
+ assert isinstance(info.common_causes, list)
255
+ assert len(info.common_causes) > 0
256
+
257
+ def test_supported_types_list_completeness(self):
258
+ """Property: get_supported_exception_types includes all mapped types."""
259
+ types_list = get_supported_exception_types()
260
+ mapping = get_exception_type_mapping()
261
+
262
+ for info in mapping.values():
263
+ assert info.name in types_list
264
+
265
+ def test_supported_types_list_no_duplicates(self):
266
+ """Property: get_supported_exception_types has no duplicates."""
267
+ types_list = get_supported_exception_types()
268
+ assert len(types_list) == len(set(types_list))
269
+
270
+ def test_supported_types_list_all_strings(self):
271
+ """Property: All items in supported types list are strings."""
272
+ types_list = get_supported_exception_types()
273
+ assert all(isinstance(t, str) for t in types_list)
274
+
275
+
276
+ class TestExceptionTypeIdentificationConsistency:
277
+ """Property-based tests for consistency between functions.
278
+
279
+ **Property 3: Structured Output**
280
+ *For any* exception, explain_error() should return an ExceptionExplanation
281
+ object with all required fields populated
282
+ **Validates: Requirements 1.6**
283
+ """
284
+
285
+ @given(supported_exceptions())
286
+ def test_identify_and_get_info_consistent(self, exc):
287
+ """Property: identify_exception_type and get_exception_type_info are consistent."""
288
+ exc_type_name = identify_exception_type(exc)
289
+ info = get_exception_type_info(exc)
290
+
291
+ assert exc_type_name == info.name
292
+
293
+ @given(supported_exceptions())
294
+ def test_identify_and_is_supported_consistent(self, exc):
295
+ """Property: identify_exception_type and is_supported_exception_type are consistent."""
296
+ exc_type_name = identify_exception_type(exc)
297
+ is_supported = is_supported_exception_type(exc)
298
+
299
+ # If it's supported, the name should be in the supported types list
300
+ if is_supported:
301
+ assert exc_type_name in get_supported_exception_types()
302
+
303
+ @given(supported_exceptions())
304
+ def test_get_info_and_is_supported_consistent(self, exc):
305
+ """Property: get_exception_type_info and is_supported_exception_type are consistent."""
306
+ info = get_exception_type_info(exc)
307
+ is_supported = is_supported_exception_type(exc)
308
+
309
+ # If it's supported, the info should be in the mapping
310
+ if is_supported:
311
+ mapping = get_exception_type_mapping()
312
+ assert type(exc) in mapping
313
+ assert mapping[type(exc)].name == info.name
314
+
315
+ @given(supported_exceptions())
316
+ def test_all_functions_consistent(self, exc):
317
+ """Property: All identification functions are mutually consistent."""
318
+ exc_type_name = identify_exception_type(exc)
319
+ info = get_exception_type_info(exc)
320
+ is_supported = is_supported_exception_type(exc)
321
+ mapping = get_exception_type_mapping()
322
+ types_list = get_supported_exception_types()
323
+
324
+ # All should agree on the exception type name
325
+ assert exc_type_name == info.name == type(exc).__name__
326
+
327
+ # All should agree it's supported
328
+ assert is_supported is True
329
+ assert type(exc) in mapping
330
+ assert exc_type_name in types_list
331
+
332
+ # Mapping should have correct info
333
+ assert mapping[type(exc)] == info
@@ -155,6 +155,58 @@ class TestErrorPatternMatching:
155
155
  assert "синтаксическая ошибка" in explanation.simple_explanation
156
156
  assert "скобки" in explanation.fix_tip or "отступы" in explanation.fix_tip
157
157
  assert ":" in explanation.code_example
158
+
159
+ def test_file_not_found_error_pattern_matching(self):
160
+ """Test FileNotFoundError pattern matching."""
161
+ explainer = ErrorExplainer()
162
+
163
+ # Test file not found error
164
+ exception = FileNotFoundError("[Errno 2] No such file or directory: 'data.txt'")
165
+ explanation = explainer.explain(exception)
166
+
167
+ assert explanation.error_type == "FileNotFoundError"
168
+ assert "не существует" in explanation.simple_explanation
169
+ assert "путь" in explanation.fix_tip or "файл" in explanation.fix_tip
170
+ assert "os.path.exists" in explanation.code_example or "exists" in explanation.code_example
171
+
172
+ def test_permission_error_pattern_matching(self):
173
+ """Test PermissionError pattern matching."""
174
+ explainer = ErrorExplainer()
175
+
176
+ # Test permission error
177
+ exception = PermissionError("[Errno 13] Permission denied: 'protected_file.txt'")
178
+ explanation = explainer.explain(exception)
179
+
180
+ assert explanation.error_type == "PermissionError"
181
+ assert "прав доступа" in explanation.simple_explanation or "Permission" in explanation.simple_explanation
182
+ assert "права" in explanation.fix_tip or "доступ" in explanation.fix_tip
183
+ assert "os.access" in explanation.code_example or "access" in explanation.code_example
184
+
185
+ def test_zero_division_error_pattern_matching(self):
186
+ """Test ZeroDivisionError pattern matching."""
187
+ explainer = ErrorExplainer()
188
+
189
+ # Test zero division error
190
+ exception = ZeroDivisionError("division by zero")
191
+ explanation = explainer.explain(exception)
192
+
193
+ assert explanation.error_type == "ZeroDivisionError"
194
+ assert "ноль" in explanation.simple_explanation
195
+ assert "делитель" in explanation.fix_tip or "ноль" in explanation.fix_tip
196
+ assert "!= 0" in explanation.code_example or "if" in explanation.code_example
197
+
198
+ def test_name_error_pattern_matching(self):
199
+ """Test NameError pattern matching."""
200
+ explainer = ErrorExplainer()
201
+
202
+ # Test name error
203
+ exception = NameError("name 'undefined_var' is not defined")
204
+ explanation = explainer.explain(exception)
205
+
206
+ assert explanation.error_type == "NameError"
207
+ assert "не была определена" in explanation.simple_explanation or "not defined" in explanation.simple_explanation
208
+ assert "определена" in explanation.fix_tip or "defined" in explanation.fix_tip
209
+ assert "=" in explanation.code_example
158
210
 
159
211
 
160
212
  class TestErrorPatternValidation:
@@ -0,0 +1 @@
1
+ """Tests for the input_utils module."""
@@ -0,0 +1,65 @@
1
+ """Unit tests for the input_utils module."""
2
+
3
+ import pytest
4
+ from fishertools.input_utils import ask_int, ask_float, ask_str, ask_choice
5
+
6
+
7
+ class TestAskInt:
8
+ """Tests for ask_int function."""
9
+
10
+ def test_ask_int_basic(self, monkeypatch):
11
+ """Test basic integer input."""
12
+ monkeypatch.setattr('builtins.input', lambda _: '42')
13
+ result = ask_int("Enter a number: ")
14
+ assert result == 42
15
+ assert isinstance(result, int)
16
+
17
+ def test_ask_int_with_min_max(self, monkeypatch):
18
+ """Test integer input with min/max constraints."""
19
+ monkeypatch.setattr('builtins.input', lambda _: '50')
20
+ result = ask_int("Enter a number: ", min=0, max=100)
21
+ assert result == 50
22
+
23
+
24
+ class TestAskFloat:
25
+ """Tests for ask_float function."""
26
+
27
+ def test_ask_float_basic(self, monkeypatch):
28
+ """Test basic float input."""
29
+ monkeypatch.setattr('builtins.input', lambda _: '3.14')
30
+ result = ask_float("Enter a number: ")
31
+ assert result == 3.14
32
+ assert isinstance(result, float)
33
+
34
+
35
+ class TestAskStr:
36
+ """Tests for ask_str function."""
37
+
38
+ def test_ask_str_basic(self, monkeypatch):
39
+ """Test basic string input."""
40
+ monkeypatch.setattr('builtins.input', lambda _: 'hello')
41
+ result = ask_str("Enter text: ")
42
+ assert result == 'hello'
43
+ assert isinstance(result, str)
44
+
45
+ def test_ask_str_strips_whitespace(self, monkeypatch):
46
+ """Test that whitespace is stripped from string input."""
47
+ monkeypatch.setattr('builtins.input', lambda _: ' hello ')
48
+ result = ask_str("Enter text: ")
49
+ assert result == 'hello'
50
+
51
+
52
+ class TestAskChoice:
53
+ """Tests for ask_choice function."""
54
+
55
+ def test_ask_choice_basic(self, monkeypatch):
56
+ """Test basic choice selection."""
57
+ monkeypatch.setattr('builtins.input', lambda _: 'red')
58
+ result = ask_choice("Choose a color: ", ['red', 'green', 'blue'])
59
+ assert result == 'red'
60
+
61
+ def test_ask_choice_numeric(self, monkeypatch):
62
+ """Test numeric choice selection."""
63
+ monkeypatch.setattr('builtins.input', lambda _: '1')
64
+ result = ask_choice("Choose a color: ", ['red', 'green', 'blue'])
65
+ assert result == 'red'
@@ -10,6 +10,7 @@ from fishertools.learn.examples import (
10
10
  generate_example,
11
11
  list_available_concepts,
12
12
  get_concept_info,
13
+ explain,
13
14
  CODE_EXAMPLES
14
15
  )
15
16
 
@@ -218,4 +219,181 @@ class TestIntegration:
218
219
 
219
220
  # list_available_concepts should not include invalid concept
220
221
  concepts = list_available_concepts()
221
- assert invalid_concept not in concepts
222
+ assert invalid_concept not in concepts
223
+
224
+
225
+
226
+ class TestExplain:
227
+ """Test the explain() function."""
228
+
229
+ def test_explain_valid_topic_returns_dict(self):
230
+ """Test that explain() returns a dict for valid topics."""
231
+ result = explain("list")
232
+
233
+ assert isinstance(result, dict)
234
+ assert len(result) > 0
235
+
236
+ def test_explain_returns_required_keys(self):
237
+ """Test that explain() returns all required keys."""
238
+ result = explain("int")
239
+
240
+ required_keys = {'description', 'when_to_use', 'example'}
241
+ assert set(result.keys()) == required_keys
242
+
243
+ def test_explain_all_values_are_strings(self):
244
+ """Test that all values in explain() result are strings."""
245
+ result = explain("dict")
246
+
247
+ assert isinstance(result['description'], str)
248
+ assert isinstance(result['when_to_use'], str)
249
+ assert isinstance(result['example'], str)
250
+
251
+ def test_explain_all_values_non_empty(self):
252
+ """Test that all values in explain() result are non-empty."""
253
+ result = explain("for")
254
+
255
+ assert len(result['description']) > 0
256
+ assert len(result['when_to_use']) > 0
257
+ assert len(result['example']) > 0
258
+
259
+ def test_explain_case_insensitive(self):
260
+ """Test that explain() is case insensitive."""
261
+ result_lower = explain("list")
262
+ result_upper = explain("LIST")
263
+ result_mixed = explain("List")
264
+
265
+ assert result_lower == result_upper
266
+ assert result_lower == result_mixed
267
+
268
+ def test_explain_whitespace_handling(self):
269
+ """Test that explain() handles whitespace correctly."""
270
+ result1 = explain("list")
271
+ result2 = explain(" list ")
272
+ result3 = explain("\tlist\n")
273
+
274
+ assert result1 == result2
275
+ assert result1 == result3
276
+
277
+ def test_explain_invalid_topic_raises_valueerror(self):
278
+ """Test that explain() raises ValueError for invalid topics."""
279
+ with pytest.raises(ValueError):
280
+ explain("invalid_topic_xyz")
281
+
282
+ def test_explain_error_message_helpful(self):
283
+ """Test that ValueError message is helpful."""
284
+ try:
285
+ explain("nonexistent")
286
+ pytest.fail("Should have raised ValueError")
287
+ except ValueError as e:
288
+ error_msg = str(e)
289
+
290
+ # Should mention the invalid topic
291
+ assert "nonexistent" in error_msg
292
+
293
+ # Should list available topics
294
+ assert "Available topics" in error_msg or "available" in error_msg.lower()
295
+
296
+ def test_explain_empty_string_raises_error(self):
297
+ """Test that explain() raises error for empty string."""
298
+ with pytest.raises(ValueError):
299
+ explain("")
300
+
301
+ def test_explain_whitespace_only_raises_error(self):
302
+ """Test that explain() raises error for whitespace-only string."""
303
+ with pytest.raises(ValueError):
304
+ explain(" ")
305
+
306
+ def test_explain_all_data_types(self):
307
+ """Test explain() for all data type topics."""
308
+ data_types = ['int', 'float', 'str', 'bool', 'list', 'tuple', 'set', 'dict']
309
+
310
+ for dtype in data_types:
311
+ result = explain(dtype)
312
+
313
+ assert isinstance(result, dict)
314
+ assert set(result.keys()) == {'description', 'when_to_use', 'example'}
315
+ assert all(len(v) > 0 for v in result.values())
316
+
317
+ def test_explain_all_control_structures(self):
318
+ """Test explain() for all control structure topics."""
319
+ control_structures = ['if', 'for', 'while', 'break', 'continue']
320
+
321
+ for cs in control_structures:
322
+ result = explain(cs)
323
+
324
+ assert isinstance(result, dict)
325
+ assert set(result.keys()) == {'description', 'when_to_use', 'example'}
326
+ assert all(len(v) > 0 for v in result.values())
327
+
328
+ def test_explain_all_function_features(self):
329
+ """Test explain() for all function feature topics."""
330
+ function_features = ['function', 'return', 'lambda', '*args', '**kwargs']
331
+
332
+ for ff in function_features:
333
+ result = explain(ff)
334
+
335
+ assert isinstance(result, dict)
336
+ assert set(result.keys()) == {'description', 'when_to_use', 'example'}
337
+ assert all(len(v) > 0 for v in result.values())
338
+
339
+ def test_explain_all_error_handling(self):
340
+ """Test explain() for all error handling topics."""
341
+ error_handling = ['try', 'except', 'finally', 'raise']
342
+
343
+ for eh in error_handling:
344
+ result = explain(eh)
345
+
346
+ assert isinstance(result, dict)
347
+ assert set(result.keys()) == {'description', 'when_to_use', 'example'}
348
+ assert all(len(v) > 0 for v in result.values())
349
+
350
+ def test_explain_all_file_operations(self):
351
+ """Test explain() for all file operation topics."""
352
+ file_operations = ['open', 'read', 'write', 'with']
353
+
354
+ for fo in file_operations:
355
+ result = explain(fo)
356
+
357
+ assert isinstance(result, dict)
358
+ assert set(result.keys()) == {'description', 'when_to_use', 'example'}
359
+ assert all(len(v) > 0 for v in result.values())
360
+
361
+ def test_explain_consistency(self):
362
+ """Test that explain() returns consistent results."""
363
+ result1 = explain("list")
364
+ result2 = explain("list")
365
+ result3 = explain("list")
366
+
367
+ assert result1 == result2
368
+ assert result2 == result3
369
+
370
+ def test_explain_description_meaningful(self):
371
+ """Test that descriptions are meaningful."""
372
+ result = explain("list")
373
+ description = result['description']
374
+
375
+ # Should be substantial
376
+ assert len(description) >= 20
377
+
378
+ # Should contain alphabetic characters
379
+ assert any(c.isalpha() for c in description)
380
+
381
+ def test_explain_when_to_use_meaningful(self):
382
+ """Test that when_to_use is meaningful."""
383
+ result = explain("dict")
384
+ when_to_use = result['when_to_use']
385
+
386
+ # Should be substantial
387
+ assert len(when_to_use) >= 20
388
+
389
+ # Should contain alphabetic characters
390
+ assert any(c.isalpha() for c in when_to_use)
391
+
392
+ def test_explain_example_is_code(self):
393
+ """Test that examples contain code."""
394
+ result = explain("for")
395
+ example = result['example']
396
+
397
+ # Should contain code-like patterns
398
+ code_indicators = ['=', '(', ')', '[', ']', '{', '}', 'print', 'def', 'if', 'for']
399
+ assert any(indicator in example for indicator in code_indicators)