-
Notifications
You must be signed in to change notification settings - Fork 3
/
ProblemFolder.py
213 lines (167 loc) · 7.79 KB
/
ProblemFolder.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
from Problem import *
from TopCoderParser import *
import os
import json
import shutil
## naming formats ##
FOLDER_NAME_FORMAT = "%d_%s"
JSON_FILE_FORMAT = "%s.json"
HTML_FILE_FORMAT = "%s.html"
PYTHON_FILE_FORMAT = "%s.py"
INIT_FILE_FORMAT = "__init__.py"
class ProblemFolder(object):
"""The class for TopCoder problem directories, which contain TopCoder
problems.
Has methods for adding and removing Problem objects from the directory.
self.problems is a list of tuples, (path_to_dir, number, name), for each problem.
"""
## init ##
def __init__(self, directory):
"""Creates a new Problem Folder in the given directory.
If problems already exist in that directory, adds them to the object."""
self.loc = directory
# get existing problems
self.problems, broken_problems = load_existing_problems(self.loc)
## public methods ##
def get_problem_numbers(self):
"""Returns a list of all problem numbers in this directory."""
return [x[1] for x in self.problems]
def scrape_and_add_problem(self, n, force = False, opener = None):
"""Given a problem number, scrapes the problem and adds it to the
directory.
If provided with an opener, uses that instead of re-connecting.
If force is True, overwrites any existing Python files for this problem."""
# get problem
problem = scrape_problem(n, opener)
if problem:
# save problem
print "Saving files...",
error_msg = self.add_problem(problem, force)
if error_msg == None:
print "OK"
else:
raise Exception(error_msg)
def find_problem(self, name = None, number = None, path = None):
"""Returns a list of problem tuples (path, number, problem) that match the search query."""
# how many search terms were specified?
rule_num = len([x for x in [name, number, path] if x != None])
matches = []
for problem in self.problems:
score = 0
if path != None:
if problem[0] == path:
score += 1
if number != None:
if problem[1] == number:
score += 1
if name != None:
if problem[2] == name:
score += 1
if score == rule_num:
matches.append(problem)
return matches
def load_problem(self, problem):
"""Takes a tuple (rel_path, number, name) and returns the full problem object."""
return Problem(problem[0] + os.sep + (JSON_FILE_FORMAT % problem[2]))
def add_problem(self, problem, force = False):
"""Adds a new problem to this folder.
If this problem already exists, overwrites the description files (but
not any Python scripts, unless force is True).
"""
# decide where to save this problem
problem_foldername = FOLDER_NAME_FORMAT % (problem[P_PROBLEM_NUMBER], problem[P_PROBLEM_NAME])
target_dir = self.loc + os.sep + problem_foldername
# decide on filenames
json_path = target_dir + os.sep + (JSON_FILE_FORMAT % problem[P_PROBLEM_NAME])
html_path = target_dir + os.sep + (HTML_FILE_FORMAT % problem[P_PROBLEM_NAME])
python_path = target_dir + os.sep + (PYTHON_FILE_FORMAT % problem[P_PROBLEM_DEFINITION]['class'])
init_path = target_dir + os.sep + (INIT_FILE_FORMAT)
# does this problem directory already exist?
if not os.path.isdir(target_dir):
# create folder
os.mkdir(target_dir)
# does this problem already exist?
if not self.find_problem(path=target_dir):
# save problem tuple
self.problems.append((target_dir, problem[P_PROBLEM_NUMBER], problem[P_PROBLEM_NAME]))
# save JSON and HTML files, regardless
problem.to_json_file(json_path)
problem.to_html_file(html_path)
# check if python file exists: if it doesn't, create it
if force or not os.access(python_path, os.F_OK):
problem.to_python_file(python_path)
# create init file, if it doesn't exist
if force or not os.access(init_path, os.F_OK):
open(init_path, 'w').close()
def del_problem(self, problem):
"""Deletes all problems with the same name and number as the given problem.
Removes the problem directory from the file system, including any files
in it.
Returns the number of problems deleted."""
problems_to_delete = self.find_problem(name=problem[P_PROBLEM_NAME], number=problem[P_PROBLEM_NUMBER])
for p in problems_to_delete:
# delete entire folder
shutil.rmtree(p[0])
# delete from list
self.problems.remove(p)
return len(problems_to_delete)
def test_problems(self, problems):
"""Given a list of problem tuples (rel_path, number, name), runs the tests
for each problem.
Uses the method provided in the Python file in the problem directory."""
for p in problems:
# load problem
problem = self.load_problem(p)
print " * Running tests for problem %d: %s *" % (problem[P_PROBLEM_NUMBER], problem[P_PROBLEM_NAME])
# execute python file text in namespace ns
ns = {}
python_filename = p[0] + os.sep + (PYTHON_FILE_FORMAT % problem[P_PROBLEM_DEFINITION]['class'])
python_file = open(python_filename, 'rU')
exec python_file in ns
python_file.close()
# get method
method = ns[problem[P_PROBLEM_DEFINITION]['method']]
# run tests
problem.test_method(method)
def clean(self):
"""Refreshes and re-scans the directory for problems, as well as re-creating missing files.
The difference between this and __init__ is that this removes all directories that contain
invalid JSON files, as well as recreating HTML files."""
working_problems, broken_problems = load_existing_problems(self.loc)
self.problems = []
# remove broken problems
for path in broken_problems:
shutil.rmtree(path)
# recreate missing files
for path, number, name in working_problems:
self.add_problem(Problem(path + os.sep + (JSON_FILE_FORMAT % name)))
## helper functions ##
def load_existing_problems(directory):
"""Given a directory, finds all problem folders.
Returns two lists:
- the first is a list of all valid problem files in the directory, as a tuple (dir_path, num, problem_name)
- the second is a list of all problem folders that contain an invalid JSON file
e.g. [('./47_WordFilter/problem', 47, 'WordFilter'], ['./48_Name', './49_BadJSON']
A problem directory is valid if there is a single JSON file inside the
directory, in the correct format.
"""
problems = []
broken_problems = []
for root, dirs, files in os.walk(directory):
# is there only 1 JSON file?
json_files = [x for x in files if os.path.splitext(x)[1].lower() == '.json']
if len(json_files) == 1:
# load json file
json_path = root + os.sep + json_files[0]
try:
json_file = open(json_path, 'rU')
data = json.load(json_file)
# add to list
problems.append((root, data[P_PROBLEM_NUMBER], data[P_PROBLEM_NAME]))
# close json file
json_file.close()
del data
except ValueError, e:
# json file could not be decoded
broken_problems.append(root)
return (problems, broken_problems)