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,447 @@
1
+ """
2
+ Property-based tests for the JSONStorage class in fishertools.patterns.
3
+
4
+ Tests the correctness properties of the JSONStorage class using hypothesis
5
+ for property-based testing.
6
+
7
+ **Validates: Requirements 9.2, 9.3, 9.4**
8
+ """
9
+
10
+ import pytest
11
+ import json
12
+ import os
13
+ import tempfile
14
+ import shutil
15
+ from pathlib import Path
16
+ from hypothesis import given, strategies as st, assume
17
+
18
+ from fishertools.patterns.storage import JSONStorage
19
+
20
+
21
+ class TestJSONStorageRoundTrip:
22
+ """
23
+ Property 4: JSONStorage Round Trip
24
+
25
+ For any valid Python dictionary, saving it with JSONStorage and then
26
+ loading it should return an equivalent dictionary.
27
+
28
+ **Validates: Requirements 9.2, 9.3**
29
+ """
30
+
31
+ def test_round_trip_simple_dict(self):
32
+ """Test round trip with a simple dictionary."""
33
+ with tempfile.TemporaryDirectory() as tmpdir:
34
+ storage = JSONStorage(os.path.join(tmpdir, "test.json"))
35
+ data = {"name": "Alice", "age": 30}
36
+
37
+ storage.save(data)
38
+ loaded = storage.load()
39
+
40
+ assert loaded == data
41
+
42
+ def test_round_trip_nested_dict(self):
43
+ """Test round trip with nested dictionaries."""
44
+ with tempfile.TemporaryDirectory() as tmpdir:
45
+ storage = JSONStorage(os.path.join(tmpdir, "test.json"))
46
+ data = {
47
+ "user": {
48
+ "name": "Bob",
49
+ "address": {
50
+ "city": "New York",
51
+ "zip": "10001"
52
+ }
53
+ },
54
+ "active": True
55
+ }
56
+
57
+ storage.save(data)
58
+ loaded = storage.load()
59
+
60
+ assert loaded == data
61
+
62
+ def test_round_trip_with_lists(self):
63
+ """Test round trip with lists in dictionary."""
64
+ with tempfile.TemporaryDirectory() as tmpdir:
65
+ storage = JSONStorage(os.path.join(tmpdir, "test.json"))
66
+ data = {
67
+ "items": [1, 2, 3, 4, 5],
68
+ "names": ["Alice", "Bob", "Charlie"]
69
+ }
70
+
71
+ storage.save(data)
72
+ loaded = storage.load()
73
+
74
+ assert loaded == data
75
+
76
+ def test_round_trip_with_various_types(self):
77
+ """Test round trip with various JSON-compatible types."""
78
+ with tempfile.TemporaryDirectory() as tmpdir:
79
+ storage = JSONStorage(os.path.join(tmpdir, "test.json"))
80
+ data = {
81
+ "string": "hello",
82
+ "integer": 42,
83
+ "float": 3.14,
84
+ "boolean": True,
85
+ "null": None,
86
+ "list": [1, "two", 3.0, False, None],
87
+ "nested": {"key": "value"}
88
+ }
89
+
90
+ storage.save(data)
91
+ loaded = storage.load()
92
+
93
+ assert loaded == data
94
+
95
+ def test_round_trip_empty_dict(self):
96
+ """Test round trip with empty dictionary."""
97
+ with tempfile.TemporaryDirectory() as tmpdir:
98
+ storage = JSONStorage(os.path.join(tmpdir, "test.json"))
99
+ data = {}
100
+
101
+ storage.save(data)
102
+ loaded = storage.load()
103
+
104
+ assert loaded == data
105
+
106
+ def test_round_trip_multiple_saves(self):
107
+ """Test that multiple saves overwrite previous data."""
108
+ with tempfile.TemporaryDirectory() as tmpdir:
109
+ storage = JSONStorage(os.path.join(tmpdir, "test.json"))
110
+
111
+ # First save
112
+ data1 = {"version": 1}
113
+ storage.save(data1)
114
+
115
+ # Second save
116
+ data2 = {"version": 2, "updated": True}
117
+ storage.save(data2)
118
+
119
+ loaded = storage.load()
120
+ assert loaded == data2
121
+
122
+ def test_round_trip_with_unicode(self):
123
+ """Test round trip with unicode characters."""
124
+ with tempfile.TemporaryDirectory() as tmpdir:
125
+ storage = JSONStorage(os.path.join(tmpdir, "test.json"))
126
+ data = {
127
+ "greeting": "Hello 世界",
128
+ "emoji": "🎉",
129
+ "accents": "café"
130
+ }
131
+
132
+ storage.save(data)
133
+ loaded = storage.load()
134
+
135
+ assert loaded == data
136
+
137
+ def test_round_trip_with_special_strings(self):
138
+ """Test round trip with special string characters."""
139
+ with tempfile.TemporaryDirectory() as tmpdir:
140
+ storage = JSONStorage(os.path.join(tmpdir, "test.json"))
141
+ data = {
142
+ "newline": "line1\nline2",
143
+ "tab": "col1\tcol2",
144
+ "quote": 'He said "hello"',
145
+ "backslash": "path\\to\\file"
146
+ }
147
+
148
+ storage.save(data)
149
+ loaded = storage.load()
150
+
151
+ assert loaded == data
152
+
153
+ @given(st.dictionaries(
154
+ st.text(min_size=1),
155
+ st.one_of(
156
+ st.integers(),
157
+ st.floats(allow_nan=False, allow_infinity=False),
158
+ st.text(),
159
+ st.booleans(),
160
+ st.none()
161
+ ),
162
+ min_size=0,
163
+ max_size=10
164
+ ))
165
+ def test_round_trip_property(self, data):
166
+ """
167
+ Property: For any valid Python dictionary with JSON-serializable values,
168
+ saving and loading should return an equivalent dictionary.
169
+ """
170
+ with tempfile.TemporaryDirectory() as tmpdir:
171
+ storage = JSONStorage(os.path.join(tmpdir, "test.json"))
172
+
173
+ storage.save(data)
174
+ loaded = storage.load()
175
+
176
+ assert loaded == data
177
+
178
+
179
+ class TestJSONStorageCreatesDirectories:
180
+ """
181
+ Property 5: JSONStorage Creates Directories
182
+
183
+ For any file path with non-existent parent directories, JSONStorage should
184
+ create all necessary directories when saving data.
185
+
186
+ **Validates: Requirements 9.4**
187
+ """
188
+
189
+ def test_creates_single_directory(self):
190
+ """Test that JSONStorage creates a single parent directory."""
191
+ with tempfile.TemporaryDirectory() as tmpdir:
192
+ file_path = os.path.join(tmpdir, "subdir", "test.json")
193
+ storage = JSONStorage(file_path)
194
+
195
+ # Directory should not exist yet
196
+ assert not os.path.exists(os.path.dirname(file_path))
197
+
198
+ # Save should create the directory
199
+ storage.save({"data": "test"})
200
+
201
+ # Directory should now exist
202
+ assert os.path.exists(os.path.dirname(file_path))
203
+ assert os.path.isdir(os.path.dirname(file_path))
204
+
205
+ def test_creates_nested_directories(self):
206
+ """Test that JSONStorage creates nested parent directories."""
207
+ with tempfile.TemporaryDirectory() as tmpdir:
208
+ file_path = os.path.join(tmpdir, "level1", "level2", "level3", "test.json")
209
+ storage = JSONStorage(file_path)
210
+
211
+ # Directories should not exist yet
212
+ assert not os.path.exists(os.path.dirname(file_path))
213
+
214
+ # Save should create all directories
215
+ storage.save({"data": "test"})
216
+
217
+ # All directories should now exist
218
+ assert os.path.exists(os.path.dirname(file_path))
219
+ assert os.path.isdir(os.path.dirname(file_path))
220
+
221
+ def test_creates_directories_with_special_names(self):
222
+ """Test that JSONStorage creates directories with special characters."""
223
+ with tempfile.TemporaryDirectory() as tmpdir:
224
+ file_path = os.path.join(tmpdir, "dir-with-dash", "dir_with_underscore", "test.json")
225
+ storage = JSONStorage(file_path)
226
+
227
+ storage.save({"data": "test"})
228
+
229
+ assert os.path.exists(os.path.dirname(file_path))
230
+
231
+ def test_does_not_fail_if_directory_exists(self):
232
+ """Test that JSONStorage doesn't fail if directory already exists."""
233
+ with tempfile.TemporaryDirectory() as tmpdir:
234
+ subdir = os.path.join(tmpdir, "existing")
235
+ os.makedirs(subdir)
236
+
237
+ file_path = os.path.join(subdir, "test.json")
238
+ storage = JSONStorage(file_path)
239
+
240
+ # Should not raise an exception
241
+ storage.save({"data": "test"})
242
+
243
+ assert os.path.exists(file_path)
244
+
245
+ def test_creates_directories_for_deeply_nested_paths(self):
246
+ """Test that JSONStorage creates directories for deeply nested paths."""
247
+ with tempfile.TemporaryDirectory() as tmpdir:
248
+ # Create a deeply nested path
249
+ nested_path = os.path.join(tmpdir, *[f"level{i}" for i in range(10)])
250
+ file_path = os.path.join(nested_path, "test.json")
251
+
252
+ storage = JSONStorage(file_path)
253
+ storage.save({"data": "test"})
254
+
255
+ assert os.path.exists(file_path)
256
+ assert os.path.isfile(file_path)
257
+
258
+ @given(st.lists(st.text(
259
+ alphabet="abcdefghijklmnopqrstuvwxyz0123456789_-",
260
+ min_size=1,
261
+ max_size=20
262
+ ), min_size=1, max_size=5))
263
+ def test_creates_directories_property(self, path_parts):
264
+ """
265
+ Property: For any valid path with non-existent parent directories,
266
+ JSONStorage should create all necessary directories when saving.
267
+ """
268
+ with tempfile.TemporaryDirectory() as tmpdir:
269
+ # Build a nested path
270
+ nested_path = os.path.join(tmpdir, *path_parts)
271
+ file_path = os.path.join(nested_path, "test.json")
272
+
273
+ storage = JSONStorage(file_path)
274
+ storage.save({"test": "data"})
275
+
276
+ # File should exist
277
+ assert os.path.exists(file_path)
278
+ # All parent directories should exist
279
+ assert os.path.isdir(os.path.dirname(file_path))
280
+
281
+
282
+ class TestJSONStorageFileOperations:
283
+ """Test basic file operations of JSONStorage."""
284
+
285
+ def test_exists_returns_false_for_nonexistent_file(self):
286
+ """Test that exists() returns False for non-existent file."""
287
+ with tempfile.TemporaryDirectory() as tmpdir:
288
+ storage = JSONStorage(os.path.join(tmpdir, "nonexistent.json"))
289
+ assert storage.exists() is False
290
+
291
+ def test_exists_returns_true_after_save(self):
292
+ """Test that exists() returns True after saving."""
293
+ with tempfile.TemporaryDirectory() as tmpdir:
294
+ file_path = os.path.join(tmpdir, "test.json")
295
+ storage = JSONStorage(file_path)
296
+
297
+ storage.save({"data": "test"})
298
+ assert storage.exists() is True
299
+
300
+ def test_load_returns_empty_dict_for_nonexistent_file(self):
301
+ """Test that load() returns empty dict for non-existent file."""
302
+ with tempfile.TemporaryDirectory() as tmpdir:
303
+ storage = JSONStorage(os.path.join(tmpdir, "nonexistent.json"))
304
+ loaded = storage.load()
305
+
306
+ assert loaded == {}
307
+
308
+ def test_save_creates_file(self):
309
+ """Test that save() creates the file."""
310
+ with tempfile.TemporaryDirectory() as tmpdir:
311
+ file_path = os.path.join(tmpdir, "test.json")
312
+ storage = JSONStorage(file_path)
313
+
314
+ assert not os.path.exists(file_path)
315
+ storage.save({"data": "test"})
316
+ assert os.path.exists(file_path)
317
+
318
+ def test_save_writes_valid_json(self):
319
+ """Test that save() writes valid JSON."""
320
+ with tempfile.TemporaryDirectory() as tmpdir:
321
+ file_path = os.path.join(tmpdir, "test.json")
322
+ storage = JSONStorage(file_path)
323
+
324
+ data = {"key": "value", "number": 42}
325
+ storage.save(data)
326
+
327
+ # Read file directly and parse JSON
328
+ with open(file_path, 'r') as f:
329
+ loaded = json.load(f)
330
+
331
+ assert loaded == data
332
+
333
+ def test_file_path_attribute(self):
334
+ """Test that file_path attribute is set correctly."""
335
+ file_path = "test/path/file.json"
336
+ storage = JSONStorage(file_path)
337
+
338
+ assert storage.file_path == file_path
339
+
340
+
341
+ class TestJSONStorageErrorHandling:
342
+ """Test error handling in JSONStorage."""
343
+
344
+ def test_save_raises_typeerror_for_non_serializable_data(self):
345
+ """Test that save() raises TypeError for non-JSON-serializable data."""
346
+ with tempfile.TemporaryDirectory() as tmpdir:
347
+ storage = JSONStorage(os.path.join(tmpdir, "test.json"))
348
+
349
+ # Try to save a non-serializable object
350
+ with pytest.raises(TypeError):
351
+ storage.save({"obj": object()})
352
+
353
+ def test_save_raises_typeerror_for_set(self):
354
+ """Test that save() raises TypeError for sets."""
355
+ with tempfile.TemporaryDirectory() as tmpdir:
356
+ storage = JSONStorage(os.path.join(tmpdir, "test.json"))
357
+
358
+ with pytest.raises(TypeError):
359
+ storage.save({"items": {1, 2, 3}})
360
+
361
+ def test_load_raises_error_for_corrupted_json(self):
362
+ """Test that load() raises error for corrupted JSON."""
363
+ with tempfile.TemporaryDirectory() as tmpdir:
364
+ file_path = os.path.join(tmpdir, "test.json")
365
+
366
+ # Write invalid JSON
367
+ with open(file_path, 'w') as f:
368
+ f.write("{invalid json}")
369
+
370
+ storage = JSONStorage(file_path)
371
+
372
+ with pytest.raises(json.JSONDecodeError):
373
+ storage.load()
374
+
375
+
376
+ class TestJSONStorageIntegration:
377
+ """Integration tests for JSONStorage."""
378
+
379
+ def test_multiple_storage_instances_same_file(self):
380
+ """Test that multiple storage instances can access the same file."""
381
+ with tempfile.TemporaryDirectory() as tmpdir:
382
+ file_path = os.path.join(tmpdir, "shared.json")
383
+
384
+ storage1 = JSONStorage(file_path)
385
+ storage1.save({"version": 1})
386
+
387
+ storage2 = JSONStorage(file_path)
388
+ loaded = storage2.load()
389
+
390
+ assert loaded == {"version": 1}
391
+
392
+ def test_storage_with_relative_path(self):
393
+ """Test that JSONStorage works with relative paths."""
394
+ with tempfile.TemporaryDirectory() as tmpdir:
395
+ # Change to temp directory
396
+ original_cwd = os.getcwd()
397
+ try:
398
+ os.chdir(tmpdir)
399
+
400
+ storage = JSONStorage("test.json")
401
+ storage.save({"data": "test"})
402
+
403
+ assert os.path.exists("test.json")
404
+ loaded = storage.load()
405
+ assert loaded == {"data": "test"}
406
+ finally:
407
+ os.chdir(original_cwd)
408
+
409
+ def test_storage_with_absolute_path(self):
410
+ """Test that JSONStorage works with absolute paths."""
411
+ with tempfile.TemporaryDirectory() as tmpdir:
412
+ file_path = os.path.abspath(os.path.join(tmpdir, "test.json"))
413
+ storage = JSONStorage(file_path)
414
+
415
+ storage.save({"data": "test"})
416
+ loaded = storage.load()
417
+
418
+ assert loaded == {"data": "test"}
419
+
420
+ def test_storage_preserves_data_types(self):
421
+ """Test that JSONStorage preserves data types correctly."""
422
+ with tempfile.TemporaryDirectory() as tmpdir:
423
+ storage = JSONStorage(os.path.join(tmpdir, "test.json"))
424
+
425
+ data = {
426
+ "int": 42,
427
+ "float": 3.14,
428
+ "string": "hello",
429
+ "bool_true": True,
430
+ "bool_false": False,
431
+ "null": None,
432
+ "list": [1, 2, 3],
433
+ "nested": {"key": "value"}
434
+ }
435
+
436
+ storage.save(data)
437
+ loaded = storage.load()
438
+
439
+ # Verify types are preserved
440
+ assert isinstance(loaded["int"], int)
441
+ assert isinstance(loaded["float"], float)
442
+ assert isinstance(loaded["string"], str)
443
+ assert isinstance(loaded["bool_true"], bool)
444
+ assert isinstance(loaded["bool_false"], bool)
445
+ assert loaded["null"] is None
446
+ assert isinstance(loaded["list"], list)
447
+ assert isinstance(loaded["nested"], dict)