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