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,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models for the Knowledge Engine REPL.
|
|
3
|
+
|
|
4
|
+
This module defines the core data structures used throughout the REPL system.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field, asdict
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import List, Dict, Optional, Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ExampleDisplay:
|
|
14
|
+
"""Represents a code example for display in the REPL."""
|
|
15
|
+
|
|
16
|
+
number: int
|
|
17
|
+
description: str
|
|
18
|
+
code: str
|
|
19
|
+
expected_output: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
22
|
+
"""Convert to dictionary for serialization."""
|
|
23
|
+
return asdict(self)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ExampleDisplay":
|
|
27
|
+
"""Create from dictionary."""
|
|
28
|
+
return cls(**data)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class TopicDisplay:
|
|
33
|
+
"""Represents a topic with all its information for display."""
|
|
34
|
+
|
|
35
|
+
name: str
|
|
36
|
+
category: str
|
|
37
|
+
difficulty: str
|
|
38
|
+
description: str
|
|
39
|
+
examples: List[ExampleDisplay] = field(default_factory=list)
|
|
40
|
+
common_mistakes: List[str] = field(default_factory=list)
|
|
41
|
+
related_topics: List[str] = field(default_factory=list)
|
|
42
|
+
tips: List[str] = field(default_factory=list)
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
45
|
+
"""Convert to dictionary for serialization."""
|
|
46
|
+
data = asdict(self)
|
|
47
|
+
data["examples"] = [ex.to_dict() for ex in self.examples]
|
|
48
|
+
return data
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_dict(cls, data: Dict[str, Any]) -> "TopicDisplay":
|
|
52
|
+
"""Create from dictionary."""
|
|
53
|
+
examples = [ExampleDisplay.from_dict(ex) for ex in data.get("examples", [])]
|
|
54
|
+
return cls(
|
|
55
|
+
name=data["name"],
|
|
56
|
+
category=data["category"],
|
|
57
|
+
difficulty=data["difficulty"],
|
|
58
|
+
description=data["description"],
|
|
59
|
+
examples=examples,
|
|
60
|
+
common_mistakes=data.get("common_mistakes", []),
|
|
61
|
+
related_topics=data.get("related_topics", []),
|
|
62
|
+
tips=data.get("tips", []),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class ProgressStats:
|
|
68
|
+
"""Statistics about learning progress."""
|
|
69
|
+
|
|
70
|
+
total_topics: int
|
|
71
|
+
viewed_topics: int
|
|
72
|
+
total_examples: int
|
|
73
|
+
executed_examples: int
|
|
74
|
+
categories_explored: Dict[str, int] = field(default_factory=dict)
|
|
75
|
+
difficulty_distribution: Dict[str, int] = field(default_factory=dict)
|
|
76
|
+
session_duration: float = 0.0
|
|
77
|
+
last_viewed_topic: Optional[str] = None
|
|
78
|
+
|
|
79
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
80
|
+
"""Convert to dictionary for serialization."""
|
|
81
|
+
return asdict(self)
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ProgressStats":
|
|
85
|
+
"""Create from dictionary."""
|
|
86
|
+
return cls(**data)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class SessionState:
|
|
91
|
+
"""Represents the state of a REPL session."""
|
|
92
|
+
|
|
93
|
+
current_topic: Optional[str] = None
|
|
94
|
+
viewed_topics: List[str] = field(default_factory=list)
|
|
95
|
+
executed_examples: Dict[str, List[int]] = field(default_factory=dict)
|
|
96
|
+
session_history: List[str] = field(default_factory=list)
|
|
97
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
98
|
+
last_updated: datetime = field(default_factory=datetime.now)
|
|
99
|
+
|
|
100
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
101
|
+
"""Convert to dictionary for serialization."""
|
|
102
|
+
return {
|
|
103
|
+
"current_topic": self.current_topic,
|
|
104
|
+
"viewed_topics": self.viewed_topics,
|
|
105
|
+
"executed_examples": self.executed_examples,
|
|
106
|
+
"session_history": self.session_history,
|
|
107
|
+
"created_at": self.created_at.isoformat(),
|
|
108
|
+
"last_updated": self.last_updated.isoformat(),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def from_dict(cls, data: Dict[str, Any]) -> "SessionState":
|
|
113
|
+
"""Create from dictionary."""
|
|
114
|
+
return cls(
|
|
115
|
+
current_topic=data.get("current_topic"),
|
|
116
|
+
viewed_topics=data.get("viewed_topics", []),
|
|
117
|
+
executed_examples=data.get("executed_examples", {}),
|
|
118
|
+
session_history=data.get("session_history", []),
|
|
119
|
+
created_at=datetime.fromisoformat(data["created_at"]) if "created_at" in data else datetime.now(),
|
|
120
|
+
last_updated=datetime.fromisoformat(data["last_updated"]) if "last_updated" in data else datetime.now(),
|
|
121
|
+
)
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session manager for tracking learning progress and session state.
|
|
3
|
+
|
|
4
|
+
This module handles tracking viewed topics, executed examples, and persisting
|
|
5
|
+
session state to disk for resuming learning sessions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Dict, List, Optional, Any
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from fishertools.learn.repl.models import SessionState, ProgressStats
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SessionManager:
|
|
18
|
+
"""
|
|
19
|
+
Manages learning progress and session state for the REPL.
|
|
20
|
+
|
|
21
|
+
Tracks:
|
|
22
|
+
- Viewed topics
|
|
23
|
+
- Executed examples
|
|
24
|
+
- Session history
|
|
25
|
+
- Learning statistics
|
|
26
|
+
|
|
27
|
+
Persists state to JSON files for resuming sessions.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
>>> manager = SessionManager()
|
|
31
|
+
>>> manager.mark_topic_viewed("Lists")
|
|
32
|
+
>>> manager.mark_example_executed("Lists", 1)
|
|
33
|
+
>>> stats = manager.get_progress()
|
|
34
|
+
>>> manager.save_session()
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
DEFAULT_SESSION_FILE = ".repl_session.json"
|
|
38
|
+
DEFAULT_PROGRESS_FILE = ".repl_progress.json"
|
|
39
|
+
|
|
40
|
+
def __init__(self, storage_path: Optional[str] = None):
|
|
41
|
+
"""
|
|
42
|
+
Initialize the session manager.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
storage_path: Optional path to store session files.
|
|
46
|
+
Defaults to user's home directory.
|
|
47
|
+
"""
|
|
48
|
+
if storage_path is None:
|
|
49
|
+
storage_path = str(Path.home())
|
|
50
|
+
|
|
51
|
+
self.storage_path = Path(storage_path)
|
|
52
|
+
self.session_file = self.storage_path / self.DEFAULT_SESSION_FILE
|
|
53
|
+
self.progress_file = self.storage_path / self.DEFAULT_PROGRESS_FILE
|
|
54
|
+
|
|
55
|
+
# Initialize session state
|
|
56
|
+
self.state = SessionState()
|
|
57
|
+
self.session_start_time = datetime.now()
|
|
58
|
+
|
|
59
|
+
# Load existing session if available
|
|
60
|
+
self.load_session()
|
|
61
|
+
|
|
62
|
+
def mark_topic_viewed(self, topic_name: str) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Mark a topic as viewed.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
topic_name: Name of the topic to mark as viewed
|
|
68
|
+
"""
|
|
69
|
+
if topic_name not in self.state.viewed_topics:
|
|
70
|
+
self.state.viewed_topics.append(topic_name)
|
|
71
|
+
|
|
72
|
+
# Add to session history
|
|
73
|
+
if not self.state.session_history or self.state.session_history[-1] != topic_name:
|
|
74
|
+
self.state.session_history.append(topic_name)
|
|
75
|
+
|
|
76
|
+
self.state.last_updated = datetime.now()
|
|
77
|
+
|
|
78
|
+
def mark_example_executed(self, topic_name: str, example_num: int) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Mark an example as executed.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
topic_name: Name of the topic
|
|
84
|
+
example_num: Number of the example
|
|
85
|
+
"""
|
|
86
|
+
if topic_name not in self.state.executed_examples:
|
|
87
|
+
self.state.executed_examples[topic_name] = []
|
|
88
|
+
|
|
89
|
+
if example_num not in self.state.executed_examples[topic_name]:
|
|
90
|
+
self.state.executed_examples[topic_name].append(example_num)
|
|
91
|
+
|
|
92
|
+
self.state.last_updated = datetime.now()
|
|
93
|
+
|
|
94
|
+
def get_progress(self) -> ProgressStats:
|
|
95
|
+
"""
|
|
96
|
+
Get current learning progress statistics.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
ProgressStats object with current statistics
|
|
100
|
+
"""
|
|
101
|
+
# Calculate statistics
|
|
102
|
+
total_topics = len(self.state.viewed_topics)
|
|
103
|
+
viewed_topics = len(self.state.viewed_topics)
|
|
104
|
+
|
|
105
|
+
total_examples = sum(len(examples) for examples in self.state.executed_examples.values())
|
|
106
|
+
executed_examples = total_examples
|
|
107
|
+
|
|
108
|
+
# Calculate session duration
|
|
109
|
+
session_duration = (datetime.now() - self.session_start_time).total_seconds()
|
|
110
|
+
|
|
111
|
+
return ProgressStats(
|
|
112
|
+
total_topics=total_topics,
|
|
113
|
+
viewed_topics=viewed_topics,
|
|
114
|
+
total_examples=total_examples,
|
|
115
|
+
executed_examples=executed_examples,
|
|
116
|
+
categories_explored={},
|
|
117
|
+
difficulty_distribution={},
|
|
118
|
+
session_duration=session_duration,
|
|
119
|
+
last_viewed_topic=self.state.current_topic,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def get_viewed_topics(self) -> List[str]:
|
|
123
|
+
"""
|
|
124
|
+
Get list of viewed topics.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
List of topic names that have been viewed
|
|
128
|
+
"""
|
|
129
|
+
return self.state.viewed_topics.copy()
|
|
130
|
+
|
|
131
|
+
def get_executed_examples(self) -> Dict[str, List[int]]:
|
|
132
|
+
"""
|
|
133
|
+
Get dictionary of executed examples by topic.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Dictionary mapping topic names to lists of executed example numbers
|
|
137
|
+
"""
|
|
138
|
+
return {k: v.copy() for k, v in self.state.executed_examples.items()}
|
|
139
|
+
|
|
140
|
+
def set_current_topic(self, topic_name: str) -> None:
|
|
141
|
+
"""
|
|
142
|
+
Set the current topic.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
topic_name: Name of the current topic
|
|
146
|
+
"""
|
|
147
|
+
self.state.current_topic = topic_name
|
|
148
|
+
self.state.last_updated = datetime.now()
|
|
149
|
+
|
|
150
|
+
def get_current_topic(self) -> Optional[str]:
|
|
151
|
+
"""
|
|
152
|
+
Get the current topic.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Name of the current topic or None if not set
|
|
156
|
+
"""
|
|
157
|
+
return self.state.current_topic
|
|
158
|
+
|
|
159
|
+
def get_session_history(self) -> List[str]:
|
|
160
|
+
"""
|
|
161
|
+
Get history of viewed topics in current session.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
List of topic names in order they were viewed
|
|
165
|
+
"""
|
|
166
|
+
return self.state.session_history.copy()
|
|
167
|
+
|
|
168
|
+
def save_session(self) -> bool:
|
|
169
|
+
"""
|
|
170
|
+
Save session state to persistent storage.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
True if save was successful, False otherwise
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
# Ensure storage directory exists
|
|
177
|
+
self.storage_path.mkdir(parents=True, exist_ok=True)
|
|
178
|
+
|
|
179
|
+
# Save session state
|
|
180
|
+
session_data = self.state.to_dict()
|
|
181
|
+
with open(self.session_file, 'w', encoding='utf-8') as f:
|
|
182
|
+
json.dump(session_data, f, indent=2, default=str)
|
|
183
|
+
|
|
184
|
+
return True
|
|
185
|
+
except Exception as e:
|
|
186
|
+
print(f"Warning: Could not save session: {e}")
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
def load_session(self) -> bool:
|
|
190
|
+
"""
|
|
191
|
+
Load session state from persistent storage.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
True if load was successful, False otherwise
|
|
195
|
+
"""
|
|
196
|
+
try:
|
|
197
|
+
if not self.session_file.exists():
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
with open(self.session_file, 'r', encoding='utf-8') as f:
|
|
201
|
+
session_data = json.load(f)
|
|
202
|
+
|
|
203
|
+
self.state = SessionState.from_dict(session_data)
|
|
204
|
+
self.session_start_time = datetime.now()
|
|
205
|
+
return True
|
|
206
|
+
except Exception as e:
|
|
207
|
+
print(f"Warning: Could not load session: {e}")
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
def reset_progress(self) -> None:
|
|
211
|
+
"""
|
|
212
|
+
Reset all learning progress.
|
|
213
|
+
|
|
214
|
+
Clears viewed topics, executed examples, and session history.
|
|
215
|
+
"""
|
|
216
|
+
self.state = SessionState()
|
|
217
|
+
self.session_start_time = datetime.now()
|
|
218
|
+
|
|
219
|
+
def clear_session_history(self) -> None:
|
|
220
|
+
"""
|
|
221
|
+
Clear the session history.
|
|
222
|
+
|
|
223
|
+
Keeps viewed topics and executed examples but clears the history.
|
|
224
|
+
"""
|
|
225
|
+
self.state.session_history = []
|
|
226
|
+
self.state.last_updated = datetime.now()
|
|
227
|
+
|
|
228
|
+
def get_session_info(self) -> Dict[str, Any]:
|
|
229
|
+
"""
|
|
230
|
+
Get information about the current session.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Dictionary with session information
|
|
234
|
+
"""
|
|
235
|
+
session_duration = (datetime.now() - self.session_start_time).total_seconds()
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
"current_topic": self.state.current_topic,
|
|
239
|
+
"topics_viewed": len(self.state.viewed_topics),
|
|
240
|
+
"examples_executed": sum(len(ex) for ex in self.state.executed_examples.values()),
|
|
241
|
+
"session_duration_seconds": session_duration,
|
|
242
|
+
"session_history_length": len(self.state.session_history),
|
|
243
|
+
"created_at": self.state.created_at.isoformat(),
|
|
244
|
+
"last_updated": self.state.last_updated.isoformat(),
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
def is_topic_viewed(self, topic_name: str) -> bool:
|
|
248
|
+
"""
|
|
249
|
+
Check if a topic has been viewed.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
topic_name: Name of the topic
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
True if topic has been viewed, False otherwise
|
|
256
|
+
"""
|
|
257
|
+
return topic_name in self.state.viewed_topics
|
|
258
|
+
|
|
259
|
+
def is_example_executed(self, topic_name: str, example_num: int) -> bool:
|
|
260
|
+
"""
|
|
261
|
+
Check if an example has been executed.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
topic_name: Name of the topic
|
|
265
|
+
example_num: Number of the example
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
True if example has been executed, False otherwise
|
|
269
|
+
"""
|
|
270
|
+
if topic_name not in self.state.executed_examples:
|
|
271
|
+
return False
|
|
272
|
+
return example_num in self.state.executed_examples[topic_name]
|
|
273
|
+
|
|
274
|
+
def get_examples_executed_for_topic(self, topic_name: str) -> List[int]:
|
|
275
|
+
"""
|
|
276
|
+
Get list of executed examples for a topic.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
topic_name: Name of the topic
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
List of executed example numbers
|
|
283
|
+
"""
|
|
284
|
+
return self.state.executed_examples.get(topic_name, []).copy()
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for the CodeSandbox class.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from fishertools.learn.repl.code_sandbox import CodeSandbox
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestCodeSandboxBasic:
|
|
10
|
+
"""Test basic code execution in the sandbox."""
|
|
11
|
+
|
|
12
|
+
def test_execute_simple_print(self):
|
|
13
|
+
"""Test executing simple print statement."""
|
|
14
|
+
sandbox = CodeSandbox()
|
|
15
|
+
success, output = sandbox.execute("print('Hello')")
|
|
16
|
+
assert success is True
|
|
17
|
+
assert "Hello" in output
|
|
18
|
+
|
|
19
|
+
def test_execute_arithmetic(self):
|
|
20
|
+
"""Test executing arithmetic operations."""
|
|
21
|
+
sandbox = CodeSandbox()
|
|
22
|
+
success, output = sandbox.execute("print(2 + 2)")
|
|
23
|
+
assert success is True
|
|
24
|
+
assert "4" in output
|
|
25
|
+
|
|
26
|
+
def test_execute_variable_assignment(self):
|
|
27
|
+
"""Test executing variable assignment."""
|
|
28
|
+
sandbox = CodeSandbox()
|
|
29
|
+
success, output = sandbox.execute("x = 5\nprint(x * 2)")
|
|
30
|
+
assert success is True
|
|
31
|
+
assert "10" in output
|
|
32
|
+
|
|
33
|
+
def test_execute_list_operations(self):
|
|
34
|
+
"""Test executing list operations."""
|
|
35
|
+
sandbox = CodeSandbox()
|
|
36
|
+
success, output = sandbox.execute("lst = [1, 2, 3]\nprint(sum(lst))")
|
|
37
|
+
assert success is True
|
|
38
|
+
assert "6" in output
|
|
39
|
+
|
|
40
|
+
def test_execute_loop(self):
|
|
41
|
+
"""Test executing for loop."""
|
|
42
|
+
sandbox = CodeSandbox()
|
|
43
|
+
code = """
|
|
44
|
+
for i in range(3):
|
|
45
|
+
print(i)
|
|
46
|
+
"""
|
|
47
|
+
success, output = sandbox.execute(code)
|
|
48
|
+
assert success is True
|
|
49
|
+
assert "0" in output
|
|
50
|
+
assert "1" in output
|
|
51
|
+
assert "2" in output
|
|
52
|
+
|
|
53
|
+
def test_execute_function_definition(self):
|
|
54
|
+
"""Test executing function definition."""
|
|
55
|
+
sandbox = CodeSandbox()
|
|
56
|
+
code = """
|
|
57
|
+
def add(a, b):
|
|
58
|
+
return a + b
|
|
59
|
+
|
|
60
|
+
print(add(3, 4))
|
|
61
|
+
"""
|
|
62
|
+
success, output = sandbox.execute(code)
|
|
63
|
+
assert success is True
|
|
64
|
+
assert "7" in output
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestCodeSandboxErrors:
|
|
68
|
+
"""Test error handling in the sandbox."""
|
|
69
|
+
|
|
70
|
+
def test_syntax_error(self):
|
|
71
|
+
"""Test handling of syntax errors."""
|
|
72
|
+
sandbox = CodeSandbox()
|
|
73
|
+
success, output = sandbox.execute("print('unclosed")
|
|
74
|
+
assert success is False
|
|
75
|
+
assert "Syntax Error" in output
|
|
76
|
+
|
|
77
|
+
def test_runtime_error(self):
|
|
78
|
+
"""Test handling of runtime errors."""
|
|
79
|
+
sandbox = CodeSandbox()
|
|
80
|
+
success, output = sandbox.execute("print(1 / 0)")
|
|
81
|
+
assert success is False
|
|
82
|
+
assert "ZeroDivisionError" in output
|
|
83
|
+
|
|
84
|
+
def test_name_error(self):
|
|
85
|
+
"""Test handling of undefined variables."""
|
|
86
|
+
sandbox = CodeSandbox()
|
|
87
|
+
success, output = sandbox.execute("print(undefined_var)")
|
|
88
|
+
assert success is False
|
|
89
|
+
assert "NameError" in output
|
|
90
|
+
|
|
91
|
+
def test_empty_code(self):
|
|
92
|
+
"""Test handling of empty code."""
|
|
93
|
+
sandbox = CodeSandbox()
|
|
94
|
+
success, output = sandbox.execute("")
|
|
95
|
+
assert success is False
|
|
96
|
+
assert "empty" in output.lower()
|
|
97
|
+
|
|
98
|
+
def test_whitespace_only_code(self):
|
|
99
|
+
"""Test handling of whitespace-only code."""
|
|
100
|
+
sandbox = CodeSandbox()
|
|
101
|
+
success, output = sandbox.execute(" \n \n ")
|
|
102
|
+
assert success is False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TestCodeSandboxRestrictions:
|
|
106
|
+
"""Test that dangerous operations are blocked."""
|
|
107
|
+
|
|
108
|
+
def test_block_import(self):
|
|
109
|
+
"""Test that imports are blocked."""
|
|
110
|
+
sandbox = CodeSandbox()
|
|
111
|
+
success, output = sandbox.execute("import os")
|
|
112
|
+
assert success is False
|
|
113
|
+
assert "import" in output.lower() or "not allowed" in output.lower()
|
|
114
|
+
|
|
115
|
+
def test_block_from_import(self):
|
|
116
|
+
"""Test that from imports are blocked."""
|
|
117
|
+
sandbox = CodeSandbox()
|
|
118
|
+
success, output = sandbox.execute("from os import path")
|
|
119
|
+
assert success is False
|
|
120
|
+
assert "import" in output.lower() or "not allowed" in output.lower()
|
|
121
|
+
|
|
122
|
+
def test_block_open_function(self):
|
|
123
|
+
"""Test that open() is blocked."""
|
|
124
|
+
sandbox = CodeSandbox()
|
|
125
|
+
success, output = sandbox.execute("open('file.txt')")
|
|
126
|
+
assert success is False
|
|
127
|
+
assert "not allowed" in output.lower() or "file" in output.lower()
|
|
128
|
+
|
|
129
|
+
def test_block_exec(self):
|
|
130
|
+
"""Test that exec() is blocked."""
|
|
131
|
+
sandbox = CodeSandbox()
|
|
132
|
+
success, output = sandbox.execute("exec('print(1)')")
|
|
133
|
+
assert success is False
|
|
134
|
+
assert "not allowed" in output.lower()
|
|
135
|
+
|
|
136
|
+
def test_block_eval(self):
|
|
137
|
+
"""Test that eval() is blocked."""
|
|
138
|
+
sandbox = CodeSandbox()
|
|
139
|
+
success, output = sandbox.execute("eval('1+1')")
|
|
140
|
+
assert success is False
|
|
141
|
+
assert "not allowed" in output.lower()
|
|
142
|
+
|
|
143
|
+
def test_block_globals(self):
|
|
144
|
+
"""Test that globals() is blocked."""
|
|
145
|
+
sandbox = CodeSandbox()
|
|
146
|
+
success, output = sandbox.execute("globals()")
|
|
147
|
+
assert success is False
|
|
148
|
+
assert "not allowed" in output.lower()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TestCodeSandboxMath:
|
|
152
|
+
"""Test math operations in the sandbox."""
|
|
153
|
+
|
|
154
|
+
def test_math_module_available(self):
|
|
155
|
+
"""Test that math module is available."""
|
|
156
|
+
sandbox = CodeSandbox()
|
|
157
|
+
success, output = sandbox.execute("import math\nprint(math.pi)")
|
|
158
|
+
# Math module should be available through restricted globals
|
|
159
|
+
# But import should be blocked
|
|
160
|
+
assert success is False
|
|
161
|
+
|
|
162
|
+
def test_builtin_math_functions(self):
|
|
163
|
+
"""Test built-in math functions."""
|
|
164
|
+
sandbox = CodeSandbox()
|
|
165
|
+
success, output = sandbox.execute("print(abs(-5))")
|
|
166
|
+
assert success is True
|
|
167
|
+
assert "5" in output
|
|
168
|
+
|
|
169
|
+
def test_pow_function(self):
|
|
170
|
+
"""Test pow function."""
|
|
171
|
+
sandbox = CodeSandbox()
|
|
172
|
+
success, output = sandbox.execute("print(pow(2, 3))")
|
|
173
|
+
assert success is True
|
|
174
|
+
assert "8" in output
|
|
175
|
+
|
|
176
|
+
def test_round_function(self):
|
|
177
|
+
"""Test round function."""
|
|
178
|
+
sandbox = CodeSandbox()
|
|
179
|
+
success, output = sandbox.execute("print(round(3.7))")
|
|
180
|
+
assert success is True
|
|
181
|
+
assert "4" in output
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class TestCodeSandboxUtilityMethods:
|
|
185
|
+
"""Test utility methods of CodeSandbox."""
|
|
186
|
+
|
|
187
|
+
def test_get_available_builtins(self):
|
|
188
|
+
"""Test getting list of available built-ins."""
|
|
189
|
+
sandbox = CodeSandbox()
|
|
190
|
+
builtins = sandbox.get_available_builtins()
|
|
191
|
+
assert isinstance(builtins, list)
|
|
192
|
+
assert "print" in builtins
|
|
193
|
+
assert "len" in builtins
|
|
194
|
+
assert "sum" in builtins
|
|
195
|
+
|
|
196
|
+
def test_get_blocked_builtins(self):
|
|
197
|
+
"""Test getting list of blocked built-ins."""
|
|
198
|
+
sandbox = CodeSandbox()
|
|
199
|
+
blocked = sandbox.get_blocked_builtins()
|
|
200
|
+
assert isinstance(blocked, list)
|
|
201
|
+
assert "open" in blocked
|
|
202
|
+
assert "exec" in blocked
|
|
203
|
+
assert "eval" in blocked
|
|
204
|
+
|
|
205
|
+
def test_get_blocked_modules(self):
|
|
206
|
+
"""Test getting list of blocked modules."""
|
|
207
|
+
sandbox = CodeSandbox()
|
|
208
|
+
blocked = sandbox.get_blocked_modules()
|
|
209
|
+
assert isinstance(blocked, list)
|
|
210
|
+
assert "os" in blocked
|
|
211
|
+
assert "sys" in blocked
|
|
212
|
+
assert "subprocess" in blocked
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class TestCodeSandboxEdgeCases:
|
|
216
|
+
"""Test edge cases in code execution."""
|
|
217
|
+
|
|
218
|
+
def test_multiline_code(self):
|
|
219
|
+
"""Test executing multiline code."""
|
|
220
|
+
sandbox = CodeSandbox()
|
|
221
|
+
code = """
|
|
222
|
+
x = 10
|
|
223
|
+
y = 20
|
|
224
|
+
z = x + y
|
|
225
|
+
print(z)
|
|
226
|
+
"""
|
|
227
|
+
success, output = sandbox.execute(code)
|
|
228
|
+
assert success is True
|
|
229
|
+
assert "30" in output
|
|
230
|
+
|
|
231
|
+
def test_nested_loops(self):
|
|
232
|
+
"""Test executing nested loops."""
|
|
233
|
+
sandbox = CodeSandbox()
|
|
234
|
+
code = """
|
|
235
|
+
for i in range(2):
|
|
236
|
+
for j in range(2):
|
|
237
|
+
print(f"{i},{j}")
|
|
238
|
+
"""
|
|
239
|
+
success, output = sandbox.execute(code)
|
|
240
|
+
assert success is True
|
|
241
|
+
assert "0,0" in output
|
|
242
|
+
|
|
243
|
+
def test_list_comprehension(self):
|
|
244
|
+
"""Test list comprehension."""
|
|
245
|
+
sandbox = CodeSandbox()
|
|
246
|
+
success, output = sandbox.execute("print([x*2 for x in range(3)])")
|
|
247
|
+
assert success is True
|
|
248
|
+
assert "0" in output
|
|
249
|
+
assert "2" in output
|
|
250
|
+
assert "4" in output
|
|
251
|
+
|
|
252
|
+
def test_dictionary_operations(self):
|
|
253
|
+
"""Test dictionary operations."""
|
|
254
|
+
sandbox = CodeSandbox()
|
|
255
|
+
code = """
|
|
256
|
+
d = {'a': 1, 'b': 2}
|
|
257
|
+
print(d['a'])
|
|
258
|
+
"""
|
|
259
|
+
success, output = sandbox.execute(code)
|
|
260
|
+
assert success is True
|
|
261
|
+
assert "1" in output
|