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,465 @@
1
+ """
2
+ Property-based tests for the SimpleLogger class in fishertools.patterns.
3
+
4
+ Tests the correctness properties of the SimpleLogger class using hypothesis
5
+ for property-based testing.
6
+
7
+ **Validates: Requirements 10.2, 10.3, 10.5**
8
+ """
9
+
10
+ import pytest
11
+ import os
12
+ import tempfile
13
+ from datetime import datetime
14
+ from hypothesis import given, strategies as st, assume
15
+
16
+ from fishertools.patterns.logger import SimpleLogger
17
+
18
+
19
+ class TestSimpleLoggerWritesMessages:
20
+ """
21
+ Property 6: SimpleLogger Writes Messages
22
+
23
+ For any message and log level, calling the appropriate logging method should
24
+ result in the message being written to the log file with the correct level
25
+ and a timestamp.
26
+
27
+ **Validates: Requirements 10.2, 10.3**
28
+ """
29
+
30
+ def test_info_writes_message(self):
31
+ """Test that info() writes a message to the log file."""
32
+ with tempfile.TemporaryDirectory() as tmpdir:
33
+ log_path = os.path.join(tmpdir, "test.log")
34
+ logger = SimpleLogger(log_path)
35
+
36
+ logger.info("Test info message")
37
+
38
+ # Read the log file
39
+ with open(log_path, 'r') as f:
40
+ content = f.read()
41
+
42
+ assert "Test info message" in content
43
+ assert "[INFO]" in content
44
+
45
+ def test_warning_writes_message(self):
46
+ """Test that warning() writes a message to the log file."""
47
+ with tempfile.TemporaryDirectory() as tmpdir:
48
+ log_path = os.path.join(tmpdir, "test.log")
49
+ logger = SimpleLogger(log_path)
50
+
51
+ logger.warning("Test warning message")
52
+
53
+ # Read the log file
54
+ with open(log_path, 'r') as f:
55
+ content = f.read()
56
+
57
+ assert "Test warning message" in content
58
+ assert "[WARNING]" in content
59
+
60
+ def test_error_writes_message(self):
61
+ """Test that error() writes a message to the log file."""
62
+ with tempfile.TemporaryDirectory() as tmpdir:
63
+ log_path = os.path.join(tmpdir, "test.log")
64
+ logger = SimpleLogger(log_path)
65
+
66
+ logger.error("Test error message")
67
+
68
+ # Read the log file
69
+ with open(log_path, 'r') as f:
70
+ content = f.read()
71
+
72
+ assert "Test error message" in content
73
+ assert "[ERROR]" in content
74
+
75
+ def test_message_includes_timestamp(self):
76
+ """Test that logged messages include a timestamp."""
77
+ with tempfile.TemporaryDirectory() as tmpdir:
78
+ log_path = os.path.join(tmpdir, "test.log")
79
+ logger = SimpleLogger(log_path)
80
+
81
+ logger.info("Test message")
82
+
83
+ # Read the log file
84
+ with open(log_path, 'r') as f:
85
+ content = f.read()
86
+
87
+ # Check for timestamp format YYYY-MM-DD HH:MM:SS
88
+ import re
89
+ timestamp_pattern = r'\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]'
90
+ assert re.search(timestamp_pattern, content)
91
+
92
+ def test_message_format_is_correct(self):
93
+ """Test that the log message format is [TIMESTAMP] [LEVEL] message."""
94
+ with tempfile.TemporaryDirectory() as tmpdir:
95
+ log_path = os.path.join(tmpdir, "test.log")
96
+ logger = SimpleLogger(log_path)
97
+
98
+ logger.info("Test message")
99
+
100
+ # Read the log file
101
+ with open(log_path, 'r') as f:
102
+ content = f.read().strip()
103
+
104
+ # Check format: [TIMESTAMP] [LEVEL] message
105
+ import re
106
+ pattern = r'\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\] \[INFO\] Test message'
107
+ assert re.match(pattern, content)
108
+
109
+ def test_multiple_messages_are_appended(self):
110
+ """Test that multiple messages are appended to the log file."""
111
+ with tempfile.TemporaryDirectory() as tmpdir:
112
+ log_path = os.path.join(tmpdir, "test.log")
113
+ logger = SimpleLogger(log_path)
114
+
115
+ logger.info("First message")
116
+ logger.warning("Second message")
117
+ logger.error("Third message")
118
+
119
+ # Read the log file
120
+ with open(log_path, 'r') as f:
121
+ content = f.read()
122
+
123
+ assert "First message" in content
124
+ assert "Second message" in content
125
+ assert "Third message" in content
126
+
127
+ # Count lines
128
+ lines = content.strip().split('\n')
129
+ assert len(lines) == 3
130
+
131
+ def test_messages_with_special_characters(self):
132
+ """Test that messages with special characters are logged correctly."""
133
+ with tempfile.TemporaryDirectory() as tmpdir:
134
+ log_path = os.path.join(tmpdir, "test.log")
135
+ logger = SimpleLogger(log_path)
136
+
137
+ special_message = "Message with special chars: !@#$%^&*()"
138
+ logger.info(special_message)
139
+
140
+ # Read the log file
141
+ with open(log_path, 'r') as f:
142
+ content = f.read()
143
+
144
+ assert special_message in content
145
+
146
+ def test_messages_with_unicode(self):
147
+ """Test that messages with unicode characters are logged correctly."""
148
+ with tempfile.TemporaryDirectory() as tmpdir:
149
+ log_path = os.path.join(tmpdir, "test.log")
150
+ logger = SimpleLogger(log_path)
151
+
152
+ unicode_message = "Message with unicode: 你好世界 🎉"
153
+ logger.info(unicode_message)
154
+
155
+ # Read the log file with UTF-8 encoding
156
+ with open(log_path, 'r', encoding='utf-8') as f:
157
+ content = f.read()
158
+
159
+ assert unicode_message in content
160
+
161
+ def test_messages_with_newlines(self):
162
+ """Test that messages with newlines are logged correctly."""
163
+ with tempfile.TemporaryDirectory() as tmpdir:
164
+ log_path = os.path.join(tmpdir, "test.log")
165
+ logger = SimpleLogger(log_path)
166
+
167
+ message_with_newline = "Line 1\nLine 2"
168
+ logger.info(message_with_newline)
169
+
170
+ # Read the log file
171
+ with open(log_path, 'r') as f:
172
+ content = f.read()
173
+
174
+ assert message_with_newline in content
175
+
176
+ def test_empty_message(self):
177
+ """Test that empty messages are logged correctly."""
178
+ with tempfile.TemporaryDirectory() as tmpdir:
179
+ log_path = os.path.join(tmpdir, "test.log")
180
+ logger = SimpleLogger(log_path)
181
+
182
+ logger.info("")
183
+
184
+ # Read the log file
185
+ with open(log_path, 'r') as f:
186
+ content = f.read()
187
+
188
+ # Should have a log entry with empty message
189
+ assert "[INFO]" in content
190
+
191
+ @given(st.text(min_size=0, max_size=500, alphabet=st.characters(blacklist_categories=('Cc', 'Cs'))))
192
+ def test_any_message_is_logged(self, message):
193
+ """
194
+ Property: For any message string, calling info() should write it to
195
+ the log file with the correct format.
196
+ """
197
+ with tempfile.TemporaryDirectory() as tmpdir:
198
+ log_path = os.path.join(tmpdir, "test.log")
199
+ logger = SimpleLogger(log_path)
200
+
201
+ logger.info(message)
202
+
203
+ # Read the log file with UTF-8 encoding
204
+ with open(log_path, 'r', encoding='utf-8') as f:
205
+ content = f.read()
206
+
207
+ # Message should be in the log
208
+ assert message in content
209
+ # Should have the correct level
210
+ assert "[INFO]" in content
211
+
212
+
213
+ class TestSimpleLoggerCreatesFile:
214
+ """
215
+ Property 7: SimpleLogger Creates File
216
+
217
+ For any log file path that doesn't exist, SimpleLogger should create the
218
+ file when the first message is logged.
219
+
220
+ **Validates: Requirements 10.5**
221
+ """
222
+
223
+ def test_creates_file_on_first_write(self):
224
+ """Test that SimpleLogger creates the log file on first write."""
225
+ with tempfile.TemporaryDirectory() as tmpdir:
226
+ log_path = os.path.join(tmpdir, "test.log")
227
+
228
+ # File should not exist yet
229
+ assert not os.path.exists(log_path)
230
+
231
+ logger = SimpleLogger(log_path)
232
+ logger.info("First message")
233
+
234
+ # File should now exist
235
+ assert os.path.exists(log_path)
236
+ assert os.path.isfile(log_path)
237
+
238
+ def test_creates_parent_directories(self):
239
+ """Test that SimpleLogger creates parent directories if needed."""
240
+ with tempfile.TemporaryDirectory() as tmpdir:
241
+ log_path = os.path.join(tmpdir, "logs", "app", "test.log")
242
+
243
+ # Parent directories should not exist
244
+ assert not os.path.exists(os.path.dirname(log_path))
245
+
246
+ logger = SimpleLogger(log_path)
247
+ logger.info("Test message")
248
+
249
+ # Parent directories should now exist
250
+ assert os.path.exists(os.path.dirname(log_path))
251
+ assert os.path.isfile(log_path)
252
+
253
+ def test_creates_single_parent_directory(self):
254
+ """Test that SimpleLogger creates a single parent directory."""
255
+ with tempfile.TemporaryDirectory() as tmpdir:
256
+ log_path = os.path.join(tmpdir, "logs", "test.log")
257
+
258
+ logger = SimpleLogger(log_path)
259
+ logger.info("Test message")
260
+
261
+ assert os.path.exists(log_path)
262
+
263
+ def test_creates_deeply_nested_directories(self):
264
+ """Test that SimpleLogger creates deeply nested directories."""
265
+ with tempfile.TemporaryDirectory() as tmpdir:
266
+ log_path = os.path.join(tmpdir, "a", "b", "c", "d", "e", "test.log")
267
+
268
+ logger = SimpleLogger(log_path)
269
+ logger.info("Test message")
270
+
271
+ assert os.path.exists(log_path)
272
+
273
+ def test_does_not_fail_if_directory_exists(self):
274
+ """Test that SimpleLogger doesn't fail if directory already exists."""
275
+ with tempfile.TemporaryDirectory() as tmpdir:
276
+ log_dir = os.path.join(tmpdir, "logs")
277
+ os.makedirs(log_dir)
278
+
279
+ log_path = os.path.join(log_dir, "test.log")
280
+
281
+ logger = SimpleLogger(log_path)
282
+ logger.info("Test message")
283
+
284
+ assert os.path.exists(log_path)
285
+
286
+ def test_appends_to_existing_file(self):
287
+ """Test that SimpleLogger appends to an existing log file."""
288
+ with tempfile.TemporaryDirectory() as tmpdir:
289
+ log_path = os.path.join(tmpdir, "test.log")
290
+
291
+ # Create initial log file with content
292
+ with open(log_path, 'w') as f:
293
+ f.write("Initial content\n")
294
+
295
+ logger = SimpleLogger(log_path)
296
+ logger.info("New message")
297
+
298
+ # Read the log file
299
+ with open(log_path, 'r') as f:
300
+ content = f.read()
301
+
302
+ # Both messages should be present
303
+ assert "Initial content" in content
304
+ assert "New message" in content
305
+
306
+ def test_file_path_attribute_is_set(self):
307
+ """Test that file_path attribute is set correctly."""
308
+ log_path = "test/path/app.log"
309
+ logger = SimpleLogger(log_path)
310
+
311
+ assert logger.file_path == log_path
312
+
313
+ @given(st.lists(st.text(
314
+ alphabet="abcdefghijklmnopqrstuvwxyz0123456789_-",
315
+ min_size=1,
316
+ max_size=20
317
+ ), min_size=1, max_size=5))
318
+ def test_creates_file_for_any_path(self, path_parts):
319
+ """
320
+ Property: For any valid path with non-existent parent directories,
321
+ SimpleLogger should create the file when logging.
322
+ """
323
+ with tempfile.TemporaryDirectory() as tmpdir:
324
+ # Build a nested path
325
+ nested_path = os.path.join(tmpdir, *path_parts)
326
+ log_path = os.path.join(nested_path, "test.log")
327
+
328
+ logger = SimpleLogger(log_path)
329
+ logger.info("Test message")
330
+
331
+ # File should exist
332
+ assert os.path.exists(log_path)
333
+ assert os.path.isfile(log_path)
334
+
335
+
336
+ class TestSimpleLoggerIntegration:
337
+ """Integration tests for SimpleLogger."""
338
+
339
+ def test_multiple_loggers_same_file(self):
340
+ """Test that multiple logger instances can write to the same file."""
341
+ with tempfile.TemporaryDirectory() as tmpdir:
342
+ log_path = os.path.join(tmpdir, "shared.log")
343
+
344
+ logger1 = SimpleLogger(log_path)
345
+ logger1.info("Message from logger 1")
346
+
347
+ logger2 = SimpleLogger(log_path)
348
+ logger2.warning("Message from logger 2")
349
+
350
+ # Read the log file
351
+ with open(log_path, 'r') as f:
352
+ content = f.read()
353
+
354
+ assert "Message from logger 1" in content
355
+ assert "Message from logger 2" in content
356
+
357
+ def test_all_log_levels(self):
358
+ """Test that all log levels work correctly."""
359
+ with tempfile.TemporaryDirectory() as tmpdir:
360
+ log_path = os.path.join(tmpdir, "test.log")
361
+ logger = SimpleLogger(log_path)
362
+
363
+ logger.info("Info message")
364
+ logger.warning("Warning message")
365
+ logger.error("Error message")
366
+
367
+ # Read the log file
368
+ with open(log_path, 'r') as f:
369
+ content = f.read()
370
+
371
+ assert "[INFO]" in content
372
+ assert "[WARNING]" in content
373
+ assert "[ERROR]" in content
374
+ assert "Info message" in content
375
+ assert "Warning message" in content
376
+ assert "Error message" in content
377
+
378
+ def test_logger_with_relative_path(self):
379
+ """Test that SimpleLogger works with relative paths."""
380
+ with tempfile.TemporaryDirectory() as tmpdir:
381
+ original_cwd = os.getcwd()
382
+ try:
383
+ os.chdir(tmpdir)
384
+
385
+ logger = SimpleLogger("test.log")
386
+ logger.info("Test message")
387
+
388
+ assert os.path.exists("test.log")
389
+
390
+ with open("test.log", 'r') as f:
391
+ content = f.read()
392
+
393
+ assert "Test message" in content
394
+ finally:
395
+ os.chdir(original_cwd)
396
+
397
+ def test_logger_with_absolute_path(self):
398
+ """Test that SimpleLogger works with absolute paths."""
399
+ with tempfile.TemporaryDirectory() as tmpdir:
400
+ log_path = os.path.abspath(os.path.join(tmpdir, "test.log"))
401
+ logger = SimpleLogger(log_path)
402
+
403
+ logger.info("Test message")
404
+
405
+ assert os.path.exists(log_path)
406
+
407
+ def test_logger_preserves_message_content(self):
408
+ """Test that SimpleLogger preserves exact message content."""
409
+ with tempfile.TemporaryDirectory() as tmpdir:
410
+ log_path = os.path.join(tmpdir, "test.log")
411
+ logger = SimpleLogger(log_path)
412
+
413
+ messages = [
414
+ "Simple message",
415
+ "Message with numbers 12345",
416
+ "Message with symbols !@#$%",
417
+ "Message with quotes 'single' and \"double\"",
418
+ "Message with tabs\tand\tspaces"
419
+ ]
420
+
421
+ for msg in messages:
422
+ logger.info(msg)
423
+
424
+ # Read the log file
425
+ with open(log_path, 'r') as f:
426
+ content = f.read()
427
+
428
+ # All messages should be preserved
429
+ for msg in messages:
430
+ assert msg in content
431
+
432
+
433
+ class TestSimpleLoggerErrorHandling:
434
+ """Test error handling in SimpleLogger."""
435
+
436
+ def test_raises_ioerror_for_invalid_path(self):
437
+ """Test that SimpleLogger raises an error for invalid paths."""
438
+ # Use a path that will fail during file operations
439
+ # On Windows, we can use a reserved device name
440
+ invalid_path = "CON" # Reserved device name on Windows
441
+ logger = SimpleLogger(invalid_path)
442
+
443
+ with pytest.raises((IOError, OSError, ValueError)):
444
+ logger.info("Test message")
445
+
446
+ def test_raises_ioerror_for_permission_denied(self):
447
+ """Test that SimpleLogger raises IOError for permission denied."""
448
+ with tempfile.TemporaryDirectory() as tmpdir:
449
+ log_path = os.path.join(tmpdir, "test.log")
450
+ logger = SimpleLogger(log_path)
451
+
452
+ # Create the file
453
+ logger.info("Initial message")
454
+
455
+ # Make the file read-only
456
+ os.chmod(log_path, 0o444)
457
+
458
+ try:
459
+ # Try to write to read-only file
460
+ with pytest.raises(IOError):
461
+ logger.info("Another message")
462
+ finally:
463
+ # Restore permissions for cleanup
464
+ os.chmod(log_path, 0o644)
465
+