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,440 @@
1
+ """
2
+ Property-based tests for the simple_menu() function in fishertools.patterns.
3
+
4
+ Tests the correctness properties of the simple_menu() function using hypothesis
5
+ for property-based testing.
6
+
7
+ **Validates: Requirements 8.1, 8.3, 8.4**
8
+ """
9
+
10
+ import pytest
11
+ from hypothesis import given, strategies as st, assume
12
+ from unittest.mock import patch, MagicMock
13
+ from io import StringIO
14
+
15
+ from fishertools.patterns.menu import simple_menu
16
+
17
+
18
+ class TestSimpleMenuAcceptsDictionary:
19
+ """
20
+ Property 11: Simple Menu Accepts Dictionary
21
+
22
+ For any dictionary of options, simple_menu should accept it and process
23
+ valid selections.
24
+
25
+ **Validates: Requirements 8.1, 8.3**
26
+ """
27
+
28
+ def test_simple_menu_accepts_valid_dict(self):
29
+ """Test that simple_menu accepts a valid dictionary."""
30
+ def func1():
31
+ return "func1"
32
+
33
+ def func2():
34
+ return "func2"
35
+
36
+ options = {
37
+ "Option 1": func1,
38
+ "Option 2": func2
39
+ }
40
+
41
+ # Should not raise an exception when given valid input
42
+ with patch('builtins.input', return_value='quit'):
43
+ simple_menu(options)
44
+
45
+ def test_simple_menu_displays_menu_options(self):
46
+ """Test that simple_menu displays all menu options."""
47
+ def func1():
48
+ pass
49
+
50
+ def func2():
51
+ pass
52
+
53
+ options = {
54
+ "First Option": func1,
55
+ "Second Option": func2
56
+ }
57
+
58
+ with patch('builtins.input', return_value='quit'):
59
+ with patch('builtins.print') as mock_print:
60
+ simple_menu(options)
61
+
62
+ # Check that menu options were printed
63
+ printed_text = ' '.join(str(call) for call in mock_print.call_args_list)
64
+ assert "First Option" in printed_text
65
+ assert "Second Option" in printed_text
66
+
67
+ def test_simple_menu_executes_selected_function(self):
68
+ """Test that simple_menu executes the selected function."""
69
+ mock_func1 = MagicMock()
70
+ mock_func2 = MagicMock()
71
+
72
+ options = {
73
+ "Option 1": mock_func1,
74
+ "Option 2": mock_func2
75
+ }
76
+
77
+ # Select option 1, then quit
78
+ with patch('builtins.input', side_effect=['1', 'quit']):
79
+ simple_menu(options)
80
+
81
+ # Option 1 should have been called
82
+ mock_func1.assert_called_once()
83
+ mock_func2.assert_not_called()
84
+
85
+ def test_simple_menu_executes_correct_function_for_each_option(self):
86
+ """Test that simple_menu executes the correct function for each option."""
87
+ mock_func1 = MagicMock()
88
+ mock_func2 = MagicMock()
89
+ mock_func3 = MagicMock()
90
+
91
+ options = {
92
+ "Option 1": mock_func1,
93
+ "Option 2": mock_func2,
94
+ "Option 3": mock_func3
95
+ }
96
+
97
+ # Select option 2, then quit
98
+ with patch('builtins.input', side_effect=['2', 'quit']):
99
+ simple_menu(options)
100
+
101
+ # Only option 2 should have been called
102
+ mock_func1.assert_not_called()
103
+ mock_func2.assert_called_once()
104
+ mock_func3.assert_not_called()
105
+
106
+ def test_simple_menu_with_single_option(self):
107
+ """Test that simple_menu works with a single option."""
108
+ mock_func = MagicMock()
109
+
110
+ options = {"Only Option": mock_func}
111
+
112
+ with patch('builtins.input', side_effect=['1', 'quit']):
113
+ simple_menu(options)
114
+
115
+ mock_func.assert_called_once()
116
+
117
+ def test_simple_menu_with_many_options(self):
118
+ """Test that simple_menu works with many options."""
119
+ funcs = [MagicMock() for _ in range(10)]
120
+ options = {f"Option {i+1}": func for i, func in enumerate(funcs)}
121
+
122
+ # Select option 5, then quit
123
+ with patch('builtins.input', side_effect=['5', 'quit']):
124
+ simple_menu(options)
125
+
126
+ # Only option 5 should have been called
127
+ for i, func in enumerate(funcs):
128
+ if i == 4: # 0-indexed, so option 5 is index 4
129
+ func.assert_called_once()
130
+ else:
131
+ func.assert_not_called()
132
+
133
+ def test_simple_menu_preserves_option_order(self):
134
+ """Test that simple_menu preserves the order of options."""
135
+ funcs = [MagicMock() for _ in range(3)]
136
+ options = {
137
+ "First": funcs[0],
138
+ "Second": funcs[1],
139
+ "Third": funcs[2]
140
+ }
141
+
142
+ with patch('builtins.input', return_value='quit'):
143
+ with patch('builtins.print') as mock_print:
144
+ simple_menu(options)
145
+
146
+ # Get all printed output
147
+ printed_text = ' '.join(str(call) for call in mock_print.call_args_list)
148
+
149
+ # Check that options appear in order
150
+ first_pos = printed_text.find("First")
151
+ second_pos = printed_text.find("Second")
152
+ third_pos = printed_text.find("Third")
153
+
154
+ assert first_pos < second_pos < third_pos
155
+
156
+
157
+ class TestSimpleMenuRejectsInvalidOptions:
158
+ """
159
+ Property 12: Simple Menu Rejects Invalid Options
160
+
161
+ For any invalid menu selection, simple_menu should display an error and
162
+ re-prompt.
163
+
164
+ **Validates: Requirements 8.4**
165
+ """
166
+
167
+ def test_simple_menu_rejects_out_of_range_number(self):
168
+ """Test that simple_menu rejects out-of-range numbers."""
169
+ mock_func = MagicMock()
170
+ options = {"Option 1": mock_func}
171
+
172
+ # Try option 5 (out of range), then quit
173
+ with patch('builtins.input', side_effect=['5', 'quit']):
174
+ with patch('builtins.print') as mock_print:
175
+ simple_menu(options)
176
+
177
+ # Should print an error message
178
+ printed_text = ' '.join(str(call) for call in mock_print.call_args_list)
179
+ assert "Error" in printed_text or "error" in printed_text.lower()
180
+
181
+ # Function should not have been called
182
+ mock_func.assert_not_called()
183
+
184
+ def test_simple_menu_rejects_negative_number(self):
185
+ """Test that simple_menu rejects negative numbers."""
186
+ mock_func = MagicMock()
187
+ options = {"Option 1": mock_func}
188
+
189
+ # Try negative number, then quit
190
+ with patch('builtins.input', side_effect=['-1', 'quit']):
191
+ with patch('builtins.print') as mock_print:
192
+ simple_menu(options)
193
+
194
+ # Should print an error message
195
+ printed_text = ' '.join(str(call) for call in mock_print.call_args_list)
196
+ assert "Error" in printed_text or "error" in printed_text.lower()
197
+
198
+ # Function should not have been called
199
+ mock_func.assert_not_called()
200
+
201
+ def test_simple_menu_rejects_non_numeric_input(self):
202
+ """Test that simple_menu rejects non-numeric input."""
203
+ mock_func = MagicMock()
204
+ options = {"Option 1": mock_func}
205
+
206
+ # Try non-numeric input, then quit
207
+ with patch('builtins.input', side_effect=['abc', 'quit']):
208
+ with patch('builtins.print') as mock_print:
209
+ simple_menu(options)
210
+
211
+ # Should print an error message
212
+ printed_text = ' '.join(str(call) for call in mock_print.call_args_list)
213
+ assert "Error" in printed_text or "error" in printed_text.lower()
214
+
215
+ # Function should not have been called
216
+ mock_func.assert_not_called()
217
+
218
+ def test_simple_menu_rejects_zero(self):
219
+ """Test that simple_menu rejects zero as a selection."""
220
+ mock_func = MagicMock()
221
+ options = {"Option 1": mock_func}
222
+
223
+ # Try zero, then quit
224
+ with patch('builtins.input', side_effect=['0', 'quit']):
225
+ with patch('builtins.print') as mock_print:
226
+ simple_menu(options)
227
+
228
+ # Should print an error message
229
+ printed_text = ' '.join(str(call) for call in mock_print.call_args_list)
230
+ assert "Error" in printed_text or "error" in printed_text.lower()
231
+
232
+ # Function should not have been called
233
+ mock_func.assert_not_called()
234
+
235
+ def test_simple_menu_reprompts_after_invalid_input(self):
236
+ """Test that simple_menu re-prompts after invalid input."""
237
+ mock_func = MagicMock()
238
+ options = {"Option 1": mock_func}
239
+
240
+ # Try invalid input, then valid input, then quit
241
+ with patch('builtins.input', side_effect=['invalid', '1', 'quit']):
242
+ simple_menu(options)
243
+
244
+ # Function should have been called after valid input
245
+ mock_func.assert_called_once()
246
+
247
+ def test_simple_menu_handles_multiple_invalid_inputs(self):
248
+ """Test that simple_menu handles multiple invalid inputs."""
249
+ mock_func = MagicMock()
250
+ options = {"Option 1": mock_func}
251
+
252
+ # Try multiple invalid inputs, then valid input, then quit
253
+ with patch('builtins.input', side_effect=['abc', '5', '-1', '1', 'quit']):
254
+ simple_menu(options)
255
+
256
+ # Function should have been called once after valid input
257
+ mock_func.assert_called_once()
258
+
259
+ def test_simple_menu_rejects_float_input(self):
260
+ """Test that simple_menu rejects float input."""
261
+ mock_func = MagicMock()
262
+ options = {"Option 1": mock_func}
263
+
264
+ # Try float input, then quit
265
+ with patch('builtins.input', side_effect=['1.5', 'quit']):
266
+ with patch('builtins.print') as mock_print:
267
+ simple_menu(options)
268
+
269
+ # Should print an error message
270
+ printed_text = ' '.join(str(call) for call in mock_print.call_args_list)
271
+ assert "Error" in printed_text or "error" in printed_text.lower()
272
+
273
+ # Function should not have been called
274
+ mock_func.assert_not_called()
275
+
276
+
277
+ class TestSimpleMenuExitHandling:
278
+ """Test exit/quit command handling."""
279
+
280
+ def test_simple_menu_exits_on_quit(self):
281
+ """Test that simple_menu exits on 'quit' command."""
282
+ mock_func = MagicMock()
283
+ options = {"Option 1": mock_func}
284
+
285
+ with patch('builtins.input', return_value='quit'):
286
+ simple_menu(options)
287
+
288
+ # Function should not have been called
289
+ mock_func.assert_not_called()
290
+
291
+ def test_simple_menu_exits_on_exit(self):
292
+ """Test that simple_menu exits on 'exit' command."""
293
+ mock_func = MagicMock()
294
+ options = {"Option 1": mock_func}
295
+
296
+ with patch('builtins.input', return_value='exit'):
297
+ simple_menu(options)
298
+
299
+ # Function should not have been called
300
+ mock_func.assert_not_called()
301
+
302
+ def test_simple_menu_exits_on_quit_uppercase(self):
303
+ """Test that simple_menu exits on 'QUIT' command."""
304
+ mock_func = MagicMock()
305
+ options = {"Option 1": mock_func}
306
+
307
+ with patch('builtins.input', return_value='QUIT'):
308
+ simple_menu(options)
309
+
310
+ # Function should not have been called
311
+ mock_func.assert_not_called()
312
+
313
+ def test_simple_menu_exits_on_exit_uppercase(self):
314
+ """Test that simple_menu exits on 'EXIT' command."""
315
+ mock_func = MagicMock()
316
+ options = {"Option 1": mock_func}
317
+
318
+ with patch('builtins.input', return_value='EXIT'):
319
+ simple_menu(options)
320
+
321
+ # Function should not have been called
322
+ mock_func.assert_not_called()
323
+
324
+ def test_simple_menu_exits_on_quit_with_whitespace(self):
325
+ """Test that simple_menu exits on 'quit' with whitespace."""
326
+ mock_func = MagicMock()
327
+ options = {"Option 1": mock_func}
328
+
329
+ with patch('builtins.input', return_value=' quit '):
330
+ simple_menu(options)
331
+
332
+ # Function should not have been called
333
+ mock_func.assert_not_called()
334
+
335
+
336
+ class TestSimpleMenuErrorHandling:
337
+ """Test error handling in simple_menu."""
338
+
339
+ def test_simple_menu_raises_typeerror_for_non_dict(self):
340
+ """Test that simple_menu raises TypeError for non-dict input."""
341
+ with pytest.raises(TypeError):
342
+ simple_menu([("Option 1", lambda: None)])
343
+
344
+ def test_simple_menu_raises_typeerror_for_non_callable_values(self):
345
+ """Test that simple_menu raises TypeError for non-callable values."""
346
+ with pytest.raises(TypeError):
347
+ simple_menu({"Option 1": "not a function"})
348
+
349
+ def test_simple_menu_raises_valueerror_for_empty_dict(self):
350
+ """Test that simple_menu raises ValueError for empty dictionary."""
351
+ with pytest.raises(ValueError):
352
+ simple_menu({})
353
+
354
+ def test_simple_menu_raises_typeerror_for_none(self):
355
+ """Test that simple_menu raises TypeError for None input."""
356
+ with pytest.raises(TypeError):
357
+ simple_menu(None)
358
+
359
+ def test_simple_menu_raises_typeerror_for_string(self):
360
+ """Test that simple_menu raises TypeError for string input."""
361
+ with pytest.raises(TypeError):
362
+ simple_menu("not a dict")
363
+
364
+
365
+ class TestSimpleMenuIntegration:
366
+ """Integration tests for simple_menu."""
367
+
368
+ def test_simple_menu_full_workflow(self):
369
+ """Test a complete workflow with simple_menu."""
370
+ results = []
371
+
372
+ def option1():
373
+ results.append("option1")
374
+
375
+ def option2():
376
+ results.append("option2")
377
+
378
+ options = {
379
+ "First": option1,
380
+ "Second": option2
381
+ }
382
+
383
+ # Select option 1, then option 2, then quit
384
+ with patch('builtins.input', side_effect=['1', '2', 'quit']):
385
+ simple_menu(options)
386
+
387
+ # Both functions should have been called in order
388
+ assert results == ["option1", "option2"]
389
+
390
+ def test_simple_menu_with_exception_in_function(self):
391
+ """Test that simple_menu handles exceptions in selected functions."""
392
+ def failing_func():
393
+ raise ValueError("Test error")
394
+
395
+ def working_func():
396
+ pass
397
+
398
+ options = {
399
+ "Failing": failing_func,
400
+ "Working": working_func
401
+ }
402
+
403
+ # Select failing function, then working function, then quit
404
+ # The ValueError from failing_func will be caught and treated as invalid input
405
+ with patch('builtins.input', side_effect=['1', '2', 'quit']):
406
+ simple_menu(options)
407
+
408
+ def test_simple_menu_displays_numbered_options(self):
409
+ """Test that simple_menu displays options with numbers."""
410
+ options = {
411
+ "Option A": lambda: None,
412
+ "Option B": lambda: None,
413
+ "Option C": lambda: None
414
+ }
415
+
416
+ with patch('builtins.input', return_value='quit'):
417
+ with patch('builtins.print') as mock_print:
418
+ simple_menu(options)
419
+
420
+ # Get all printed output
421
+ printed_text = ' '.join(str(call) for call in mock_print.call_args_list)
422
+
423
+ # Should contain numbered options
424
+ assert "1" in printed_text
425
+ assert "2" in printed_text
426
+ assert "3" in printed_text
427
+
428
+ def test_simple_menu_with_special_characters_in_option_names(self):
429
+ """Test that simple_menu handles special characters in option names."""
430
+ mock_func = MagicMock()
431
+ options = {
432
+ "Option with spaces": mock_func,
433
+ "Option-with-dashes": mock_func,
434
+ "Option_with_underscores": mock_func
435
+ }
436
+
437
+ with patch('builtins.input', side_effect=['1', 'quit']):
438
+ simple_menu(options)
439
+
440
+ mock_func.assert_called_once()