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,160 @@
1
+ """
2
+ Unit tests for the CommandParser class.
3
+ """
4
+
5
+ import pytest
6
+ from fishertools.learn.repl.command_parser import CommandParser
7
+
8
+
9
+ class TestCommandParserBasic:
10
+ """Test basic command parsing functionality."""
11
+
12
+ def test_parse_simple_command(self):
13
+ """Test parsing a simple command without arguments."""
14
+ cmd_type, args = CommandParser.parse("/help")
15
+ assert cmd_type == "command"
16
+ assert args == ["help"]
17
+
18
+ def test_parse_command_with_arguments(self):
19
+ """Test parsing a command with arguments."""
20
+ cmd_type, args = CommandParser.parse("/search python")
21
+ assert cmd_type == "command"
22
+ assert args == ["search", "python"]
23
+
24
+ def test_parse_command_with_multiple_arguments(self):
25
+ """Test parsing a command with multiple arguments."""
26
+ cmd_type, args = CommandParser.parse("/search python lists")
27
+ assert cmd_type == "command"
28
+ assert args == ["search", "python", "lists"]
29
+
30
+ def test_parse_topic_name(self):
31
+ """Test parsing a topic name (no leading /)."""
32
+ cmd_type, args = CommandParser.parse("Lists")
33
+ assert cmd_type == "topic"
34
+ assert args == ["Lists"]
35
+
36
+ def test_parse_topic_with_spaces(self):
37
+ """Test parsing a topic name with spaces."""
38
+ cmd_type, args = CommandParser.parse("For Loops")
39
+ assert cmd_type == "topic"
40
+ assert args == ["For Loops"]
41
+
42
+ def test_parse_quoted_arguments(self):
43
+ """Test parsing command with quoted arguments."""
44
+ cmd_type, args = CommandParser.parse('/search "list comprehension"')
45
+ assert cmd_type == "command"
46
+ assert args == ["search", "list comprehension"]
47
+
48
+ def test_parse_empty_input_raises_error(self):
49
+ """Test that empty input raises ValueError."""
50
+ with pytest.raises(ValueError):
51
+ CommandParser.parse("")
52
+
53
+ def test_parse_whitespace_only_raises_error(self):
54
+ """Test that whitespace-only input raises ValueError."""
55
+ with pytest.raises(ValueError):
56
+ CommandParser.parse(" ")
57
+
58
+ def test_parse_invalid_command_raises_error(self):
59
+ """Test that invalid command raises ValueError."""
60
+ with pytest.raises(ValueError):
61
+ CommandParser.parse("/invalid_command")
62
+
63
+ def test_parse_command_only_slash_raises_error(self):
64
+ """Test that just "/" raises ValueError."""
65
+ with pytest.raises(ValueError):
66
+ CommandParser.parse("/")
67
+
68
+ def test_parse_command_case_insensitive(self):
69
+ """Test that commands are case-insensitive."""
70
+ cmd_type1, args1 = CommandParser.parse("/HELP")
71
+ cmd_type2, args2 = CommandParser.parse("/Help")
72
+ cmd_type3, args3 = CommandParser.parse("/help")
73
+
74
+ assert args1 == args2 == args3 == ["help"]
75
+
76
+ def test_parse_topic_case_preserved(self):
77
+ """Test that topic names preserve case."""
78
+ cmd_type, args = CommandParser.parse("Lists")
79
+ assert args == ["Lists"]
80
+
81
+ cmd_type, args = CommandParser.parse("lists")
82
+ assert args == ["lists"]
83
+
84
+
85
+ class TestCommandParserEdgeCases:
86
+ """Test edge cases in command parsing."""
87
+
88
+ def test_parse_command_with_leading_trailing_spaces(self):
89
+ """Test parsing command with leading/trailing spaces."""
90
+ cmd_type, args = CommandParser.parse(" /help ")
91
+ assert cmd_type == "command"
92
+ assert args == ["help"]
93
+
94
+ def test_parse_topic_with_leading_trailing_spaces(self):
95
+ """Test parsing topic with leading/trailing spaces."""
96
+ cmd_type, args = CommandParser.parse(" Lists ")
97
+ assert cmd_type == "topic"
98
+ assert args == ["Lists"]
99
+
100
+ def test_parse_command_with_extra_spaces_between_args(self):
101
+ """Test parsing command with extra spaces between arguments."""
102
+ cmd_type, args = CommandParser.parse("/search python lists")
103
+ assert cmd_type == "command"
104
+ assert args == ["search", "python", "lists"]
105
+
106
+ def test_parse_command_with_special_characters_in_args(self):
107
+ """Test parsing command with special characters in arguments."""
108
+ cmd_type, args = CommandParser.parse("/search list[0]")
109
+ assert cmd_type == "command"
110
+ assert args == ["search", "list[0]"]
111
+
112
+
113
+ class TestCommandParserUtilityMethods:
114
+ """Test utility methods of CommandParser."""
115
+
116
+ def test_is_command_true(self):
117
+ """Test is_command returns True for commands."""
118
+ assert CommandParser.is_command("/help") is True
119
+ assert CommandParser.is_command("/search python") is True
120
+
121
+ def test_is_command_false(self):
122
+ """Test is_command returns False for non-commands."""
123
+ assert CommandParser.is_command("Lists") is False
124
+ assert CommandParser.is_command("python") is False
125
+
126
+ def test_extract_command_name(self):
127
+ """Test extracting command name from command string."""
128
+ assert CommandParser.extract_command_name("/help") == "help"
129
+ assert CommandParser.extract_command_name("/SEARCH") == "search"
130
+ assert CommandParser.extract_command_name("/search python") == "search"
131
+
132
+ def test_extract_command_name_invalid_input(self):
133
+ """Test extract_command_name with invalid input."""
134
+ with pytest.raises(ValueError):
135
+ CommandParser.extract_command_name("Lists")
136
+
137
+ def test_normalize_topic_name(self):
138
+ """Test normalizing topic names."""
139
+ assert CommandParser.normalize_topic_name("Lists") == "Lists"
140
+ assert CommandParser.normalize_topic_name(" Lists ") == "Lists"
141
+ assert CommandParser.normalize_topic_name("For Loops") == "For Loops"
142
+
143
+
144
+ class TestCommandParserAllValidCommands:
145
+ """Test that all valid commands can be parsed."""
146
+
147
+ def test_all_valid_commands(self):
148
+ """Test parsing all valid commands."""
149
+ valid_commands = [
150
+ "help", "list", "search", "random", "categories", "category",
151
+ "path", "related", "progress", "stats", "hint", "tip", "tips",
152
+ "run", "modify", "exit_edit", "history", "clear_history", "session",
153
+ "reset_progress", "commands", "about", "tutorial", "next", "prev",
154
+ "goto", "exit", "quit"
155
+ ]
156
+
157
+ for cmd in valid_commands:
158
+ cmd_type, args = CommandParser.parse(f"/{cmd}")
159
+ assert cmd_type == "command"
160
+ assert args[0] == cmd
@@ -0,0 +1,100 @@
1
+ """
2
+ Property-based tests for CommandParser using Hypothesis.
3
+
4
+ **Validates: Requirements 1.2, 1.3**
5
+ """
6
+
7
+ import pytest
8
+ from hypothesis import given, strategies as st, assume
9
+ from fishertools.learn.repl.command_parser import CommandParser
10
+
11
+
12
+ class TestCommandParserProperties:
13
+ """Property-based tests for command parsing consistency."""
14
+
15
+ @given(st.text(min_size=1))
16
+ def test_parse_consistency(self, input_str):
17
+ """
18
+ Property 1: Command Parsing Consistency
19
+
20
+ For any user input string, parsing it should consistently identify
21
+ the same command type and arguments across multiple invocations.
22
+
23
+ **Validates: Requirements 1.2, 1.3**
24
+ """
25
+ # Skip empty or whitespace-only strings
26
+ if not input_str or not input_str.strip():
27
+ return
28
+
29
+ try:
30
+ # Parse the same input twice
31
+ result1 = CommandParser.parse(input_str)
32
+ result2 = CommandParser.parse(input_str)
33
+
34
+ # Results should be identical
35
+ assert result1 == result2, f"Parsing inconsistent for input: {input_str}"
36
+ except ValueError:
37
+ # If parsing fails, it should fail consistently
38
+ with pytest.raises(ValueError):
39
+ CommandParser.parse(input_str)
40
+
41
+ @given(st.text(min_size=1, max_size=100))
42
+ def test_command_type_is_valid(self, input_str):
43
+ """
44
+ For any valid input, the command type should be one of the valid types.
45
+
46
+ **Validates: Requirements 1.2, 1.3**
47
+ """
48
+ if not input_str or not input_str.strip():
49
+ return
50
+
51
+ try:
52
+ cmd_type, args = CommandParser.parse(input_str)
53
+ assert cmd_type in ["command", "topic"], f"Invalid command type: {cmd_type}"
54
+ assert isinstance(args, list), "Arguments should be a list"
55
+ assert len(args) > 0, "Arguments list should not be empty"
56
+ except ValueError:
57
+ # Invalid input is acceptable
58
+ pass
59
+
60
+ @given(st.text(min_size=1))
61
+ def test_parse_returns_tuple(self, input_str):
62
+ """
63
+ For any valid input, parse should return a tuple of (str, list).
64
+
65
+ **Validates: Requirements 1.2, 1.3**
66
+ """
67
+ if not input_str or not input_str.strip():
68
+ return
69
+
70
+ try:
71
+ result = CommandParser.parse(input_str)
72
+ assert isinstance(result, tuple), "Result should be a tuple"
73
+ assert len(result) == 2, "Result should have 2 elements"
74
+ assert isinstance(result[0], str), "First element should be string"
75
+ assert isinstance(result[1], list), "Second element should be list"
76
+ except ValueError:
77
+ # Invalid input is acceptable
78
+ pass
79
+
80
+ @given(st.text(min_size=1))
81
+ def test_command_parsing_idempotent(self, input_str):
82
+ """
83
+ For any input that parses successfully, parsing it multiple times
84
+ should always produce the same result.
85
+
86
+ **Validates: Requirements 1.2, 1.3**
87
+ """
88
+ if not input_str or not input_str.strip():
89
+ return
90
+
91
+ try:
92
+ results = [CommandParser.parse(input_str) for _ in range(3)]
93
+ # All results should be identical
94
+ assert all(r == results[0] for r in results), \
95
+ f"Parsing not idempotent for: {input_str}"
96
+ except ValueError:
97
+ # If it fails once, it should always fail
98
+ for _ in range(3):
99
+ with pytest.raises(ValueError):
100
+ CommandParser.parse(input_str)
@@ -0,0 +1,190 @@
1
+ """
2
+ Unit tests for the REPLEngine class.
3
+ """
4
+
5
+ import pytest
6
+ import tempfile
7
+ from unittest.mock import patch, MagicMock
8
+ from fishertools.learn.knowledge_engine import KnowledgeEngine
9
+ from fishertools.learn.repl.engine import REPLEngine
10
+ from fishertools.learn.repl.session_manager import SessionManager
11
+
12
+
13
+ @pytest.fixture
14
+ def engine():
15
+ """Fixture to provide a Knowledge Engine instance."""
16
+ return KnowledgeEngine()
17
+
18
+
19
+ @pytest.fixture
20
+ def session_manager():
21
+ """Fixture to provide a SessionManager instance."""
22
+ with tempfile.TemporaryDirectory() as tmpdir:
23
+ yield SessionManager(tmpdir)
24
+
25
+
26
+ @pytest.fixture
27
+ def repl_engine(engine, session_manager):
28
+ """Fixture to provide a REPLEngine instance."""
29
+ return REPLEngine(engine, session_manager)
30
+
31
+
32
+ class TestREPLEngineInitialization:
33
+ """Test REPL engine initialization."""
34
+
35
+ def test_create_repl_engine(self, repl_engine):
36
+ """Test creating a REPL engine."""
37
+ assert repl_engine is not None
38
+ assert repl_engine.engine is not None
39
+ assert repl_engine.session_manager is not None
40
+ assert repl_engine.command_handler is not None
41
+ assert repl_engine.code_sandbox is not None
42
+
43
+ def test_repl_engine_default_initialization(self):
44
+ """Test REPL engine with default initialization."""
45
+ engine = REPLEngine()
46
+ assert engine is not None
47
+ assert engine.engine is not None
48
+ assert engine.session_manager is not None
49
+
50
+
51
+ class TestREPLEngineTopicDisplay:
52
+ """Test topic display functionality."""
53
+
54
+ def test_display_topic_sets_current_topic(self, repl_engine, engine):
55
+ """Test that displaying a topic sets current topic."""
56
+ topics = engine.list_topics()
57
+ if topics:
58
+ repl_engine._display_topic(topics[0])
59
+ assert repl_engine.current_topic == topics[0]
60
+
61
+ def test_display_topic_marks_viewed(self, repl_engine, engine, session_manager):
62
+ """Test that displaying a topic marks it as viewed."""
63
+ topics = engine.list_topics()
64
+ if topics:
65
+ repl_engine._display_topic(topics[0])
66
+ assert session_manager.is_topic_viewed(topics[0])
67
+
68
+
69
+ class TestREPLEngineNavigation:
70
+ """Test navigation functionality."""
71
+
72
+ def test_navigate_next_from_first_topic(self, repl_engine, engine):
73
+ """Test navigating to next topic from first topic."""
74
+ path = engine.get_learning_path()
75
+ if len(path) > 1:
76
+ repl_engine._display_topic(path[0])
77
+ repl_engine._navigate_next()
78
+ assert repl_engine.current_topic == path[1]
79
+
80
+ def test_navigate_prev_from_second_topic(self, repl_engine, engine):
81
+ """Test navigating to previous topic from second topic."""
82
+ path = engine.get_learning_path()
83
+ if len(path) > 1:
84
+ repl_engine._display_topic(path[1])
85
+ repl_engine._navigate_prev()
86
+ assert repl_engine.current_topic == path[0]
87
+
88
+ def test_navigate_to_topic(self, repl_engine, engine):
89
+ """Test navigating to a specific topic."""
90
+ topics = engine.list_topics()
91
+ if topics:
92
+ repl_engine._navigate_to_topic(topics[0])
93
+ assert repl_engine.current_topic == topics[0]
94
+
95
+
96
+ class TestREPLEngineCodeExecution:
97
+ """Test code execution functionality."""
98
+
99
+ def test_run_example_marks_executed(self, repl_engine, engine, session_manager):
100
+ """Test that running an example marks it as executed."""
101
+ topics = engine.list_topics()
102
+ if topics:
103
+ topic = engine.get_topic(topics[0])
104
+ examples = topic.get("examples", [])
105
+ if examples:
106
+ repl_engine._display_topic(topics[0])
107
+ repl_engine._run_example(1)
108
+ assert session_manager.is_example_executed(topics[0], 1)
109
+
110
+ def test_run_invalid_example_number(self, repl_engine, engine):
111
+ """Test running an invalid example number."""
112
+ topics = engine.list_topics()
113
+ if topics:
114
+ repl_engine._display_topic(topics[0])
115
+ # Should not crash with invalid example number
116
+ repl_engine._run_example(999)
117
+
118
+
119
+ class TestREPLEngineEditMode:
120
+ """Test edit mode functionality."""
121
+
122
+ def test_enter_edit_mode(self, repl_engine, engine):
123
+ """Test entering edit mode."""
124
+ topics = engine.list_topics()
125
+ if topics:
126
+ topic = engine.get_topic(topics[0])
127
+ examples = topic.get("examples", [])
128
+ if examples:
129
+ repl_engine._display_topic(topics[0])
130
+ repl_engine._enter_edit_mode(1)
131
+ assert repl_engine.in_edit_mode is True
132
+ assert repl_engine.edit_topic == topics[0]
133
+ assert repl_engine.edit_example_num == 1
134
+
135
+ def test_exit_edit_mode(self, repl_engine, engine):
136
+ """Test exiting edit mode."""
137
+ topics = engine.list_topics()
138
+ if topics:
139
+ topic = engine.get_topic(topics[0])
140
+ examples = topic.get("examples", [])
141
+ if examples:
142
+ repl_engine._display_topic(topics[0])
143
+ repl_engine._enter_edit_mode(1)
144
+ repl_engine._exit_edit_mode()
145
+ assert repl_engine.in_edit_mode is False
146
+
147
+
148
+ class TestREPLEngineCommandHandling:
149
+ """Test command handling."""
150
+
151
+ def test_handle_list_command(self, repl_engine):
152
+ """Test handling /list command."""
153
+ # Should not crash
154
+ repl_engine._handle_command(["list"])
155
+
156
+ def test_handle_help_command(self, repl_engine):
157
+ """Test handling /help command."""
158
+ # Should not crash
159
+ repl_engine._handle_command(["help"])
160
+
161
+ def test_handle_progress_command(self, repl_engine):
162
+ """Test handling /progress command."""
163
+ # Should not crash
164
+ repl_engine._handle_command(["progress"])
165
+
166
+ def test_handle_exit_command(self, repl_engine):
167
+ """Test handling /exit command."""
168
+ repl_engine._handle_command(["exit"])
169
+ assert repl_engine.running is False
170
+
171
+
172
+ class TestREPLEngineInputProcessing:
173
+ """Test input processing."""
174
+
175
+ def test_process_command_input(self, repl_engine):
176
+ """Test processing command input."""
177
+ # Should not crash
178
+ repl_engine._process_input("/help")
179
+
180
+ def test_process_topic_input(self, repl_engine, engine):
181
+ """Test processing topic input."""
182
+ topics = engine.list_topics()
183
+ if topics:
184
+ # Should not crash
185
+ repl_engine._process_input(topics[0])
186
+
187
+ def test_process_invalid_input(self, repl_engine):
188
+ """Test processing invalid input."""
189
+ # Should not crash
190
+ repl_engine._process_input("/invalid_command_xyz")