Skip to content

Commit aa1b6e7

Browse files
Merge pull request #114 from AlfredChester:master
New feature: 为 IO.output_gen 方法增加 time_limit 参数
2 parents 8027e5a + 40e7247 commit aa1b6e7

File tree

4 files changed

+179
-54
lines changed

4 files changed

+179
-54
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,5 +134,7 @@ target/
134134
# Pycharm
135135
venv
136136

137+
*.DS_Store
138+
137139
# VS Code
138-
.vscode
140+
.vscode

cyaron/compare.py

Lines changed: 103 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def __init__(self, name, mismatch):
1818
self.mismatch = mismatch
1919

2020
def __str__(self):
21-
return 'In program: \'{}\'. {}'.format(self.name,self.mismatch)
21+
return "In program: '{}'. {}".format(self.name, self.mismatch)
2222

2323

2424
class Compare:
@@ -38,7 +38,7 @@ def __process_file(file):
3838
file.output_file.seek(0)
3939
return file.output_filename, file.output_file.read()
4040
else:
41-
with open(file, "r", newline='\n') as f:
41+
with open(file, "r", newline="\n") as f:
4242
return file, f.read()
4343

4444
@staticmethod
@@ -51,26 +51,43 @@ def __normal_max_workers(workers):
5151

5252
@classmethod
5353
def output(cls, *files, **kwargs):
54-
kwargs = unpack_kwargs('output', kwargs, ('std', ('grader', DEFAULT_GRADER), ('max_workers', -1),
55-
('job_pool', None), ('stop_on_incorrect', None)))
56-
std = kwargs['std']
57-
grader = kwargs['grader']
58-
max_workers = kwargs['max_workers']
59-
job_pool = kwargs['job_pool']
60-
if kwargs['stop_on_incorrect'] is not None:
54+
kwargs = unpack_kwargs(
55+
"output",
56+
kwargs,
57+
(
58+
"std",
59+
("grader", DEFAULT_GRADER),
60+
("max_workers", -1),
61+
("job_pool", None),
62+
("stop_on_incorrect", None),
63+
),
64+
)
65+
std = kwargs["std"]
66+
grader = kwargs["grader"]
67+
max_workers = kwargs["max_workers"]
68+
job_pool = kwargs["job_pool"]
69+
if kwargs["stop_on_incorrect"] is not None:
6170
log.warn("parameter stop_on_incorrect is deprecated and has no effect.")
6271

6372
if (max_workers is None or max_workers >= 0) and job_pool is None:
6473
max_workers = cls.__normal_max_workers(max_workers)
6574
try:
6675
from concurrent.futures import ThreadPoolExecutor
76+
6777
with ThreadPoolExecutor(max_workers=max_workers) as job_pool:
68-
return cls.output(*files, std=std, grader=grader, max_workers=max_workers, job_pool=job_pool)
78+
return cls.output(
79+
*files,
80+
std=std,
81+
grader=grader,
82+
max_workers=max_workers,
83+
job_pool=job_pool
84+
)
6985
except ImportError:
7086
pass
7187

7288
def get_std():
7389
return cls.__process_file(std)[1]
90+
7491
if job_pool is not None:
7592
std = job_pool.submit(get_std).result()
7693
else:
@@ -87,61 +104,118 @@ def do(file):
87104

88105
@classmethod
89106
def program(cls, *programs, **kwargs):
90-
kwargs = unpack_kwargs('program', kwargs, ('input', ('std', None), ('std_program', None),
91-
('grader', DEFAULT_GRADER), ('max_workers', -1),
92-
('job_pool', None), ('stop_on_incorrect', None)))
93-
input = kwargs['input']
94-
std = kwargs['std']
95-
std_program = kwargs['std_program']
96-
grader = kwargs['grader']
97-
max_workers = kwargs['max_workers']
98-
job_pool = kwargs['job_pool']
99-
if kwargs['stop_on_incorrect'] is not None:
107+
kwargs = unpack_kwargs(
108+
"program",
109+
kwargs,
110+
(
111+
"input",
112+
("std", None),
113+
("std_program", None),
114+
("grader", DEFAULT_GRADER),
115+
("max_workers", -1),
116+
("job_pool", None),
117+
("stop_on_incorrect", None),
118+
),
119+
)
120+
input = kwargs["input"]
121+
std = kwargs["std"]
122+
std_program = kwargs["std_program"]
123+
grader = kwargs["grader"]
124+
max_workers = kwargs["max_workers"]
125+
job_pool = kwargs["job_pool"]
126+
if kwargs["stop_on_incorrect"] is not None:
100127
log.warn("parameter stop_on_incorrect is deprecated and has no effect.")
101128

102129
if (max_workers is None or max_workers >= 0) and job_pool is None:
103130
max_workers = cls.__normal_max_workers(max_workers)
104131
try:
105132
from concurrent.futures import ThreadPoolExecutor
133+
106134
with ThreadPoolExecutor(max_workers=max_workers) as job_pool:
107-
return cls.program(*programs, input=input, std=std, std_program=std_program, grader=grader, max_workers=max_workers, job_pool=job_pool)
135+
return cls.program(
136+
*programs,
137+
input=input,
138+
std=std,
139+
std_program=std_program,
140+
grader=grader,
141+
max_workers=max_workers,
142+
job_pool=job_pool
143+
)
108144
except ImportError:
109145
pass
110146

111147
if not isinstance(input, IO):
112-
raise TypeError("expect {}, got {}".format(type(IO).__name__, type(input).__name__))
148+
raise TypeError(
149+
"expect {}, got {}".format(type(IO).__name__, type(input).__name__)
150+
)
113151
input.flush_buffer()
114152
input.input_file.seek(0)
115153

116154
if std_program is not None:
155+
117156
def get_std():
118-
with open(os.dup(input.input_file.fileno()), 'r', newline='\n') as input_file:
119-
content = make_unicode(subprocess.check_output(std_program, shell=(not list_like(std_program)), stdin=input.input_file, universal_newlines=True))
157+
with open(
158+
os.dup(input.input_file.fileno()), "r", newline="\n"
159+
) as input_file:
160+
content = make_unicode(
161+
subprocess.check_output(
162+
std_program,
163+
shell=(not list_like(std_program)),
164+
stdin=input.input_file,
165+
universal_newlines=True,
166+
)
167+
)
120168
input_file.seek(0)
121169
return content
170+
122171
if job_pool is not None:
123172
std = job_pool.submit(get_std).result()
124173
else:
125174
std = get_std()
126175
elif std is not None:
176+
127177
def get_std():
128178
return cls.__process_file(std)[1]
179+
129180
if job_pool is not None:
130181
std = job_pool.submit(get_std).result()
131182
else:
132183
std = get_std()
133184
else:
134-
raise TypeError('program() missing 1 required non-None keyword-only argument: \'std\' or \'std_program\'')
185+
raise TypeError(
186+
"program() missing 1 required non-None keyword-only argument: 'std' or 'std_program'"
187+
)
135188

136189
def do(program_name):
137190
timeout = None
138-
if list_like(program_name) and len(program_name) == 2 and int_like(program_name[-1]):
191+
if (
192+
list_like(program_name)
193+
and len(program_name) == 2
194+
and int_like(program_name[-1])
195+
):
139196
program_name, timeout = program_name
140-
with open(os.dup(input.input_file.fileno()), 'r', newline='\n') as input_file:
197+
with open(
198+
os.dup(input.input_file.fileno()), "r", newline="\n"
199+
) as input_file:
141200
if timeout is None:
142-
content = make_unicode(subprocess.check_output(program_name, shell=(not list_like(program_name)), stdin=input_file, universal_newlines=True))
201+
content = make_unicode(
202+
subprocess.check_output(
203+
program_name,
204+
shell=(not list_like(program_name)),
205+
stdin=input_file,
206+
universal_newlines=True,
207+
)
208+
)
143209
else:
144-
content = make_unicode(subprocess.check_output(program_name, shell=(not list_like(program_name)), stdin=input_file, universal_newlines=True, timeout=timeout))
210+
content = make_unicode(
211+
subprocess.check_output(
212+
program_name,
213+
shell=(not list_like(program_name)),
214+
stdin=input_file,
215+
universal_newlines=True,
216+
timeout=timeout,
217+
)
218+
)
145219
input_file.seek(0)
146220
cls.__compare_two(program_name, content, std, grader)
147221

cyaron/io.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ def __init__(self,
8484
self.__escape_format(output_suffix))
8585
self.input_filename, self.output_filename = None, None
8686
self.__input_temp, self.__output_temp = False, False
87-
self.__init_file(input_file, data_id, 'i')
87+
self.__init_file(input_file, data_id, "i")
8888
if not disable_output:
89-
self.__init_file(output_file, data_id, 'o')
89+
self.__init_file(output_file, data_id, "o")
9090
else:
9191
self.output_file = None
9292
self.__closed = False
@@ -96,7 +96,7 @@ def __init_file(self, f: Union[IOBase, str, int, None],
9696
data_id: Union[int, None], file_type: str):
9797
if isinstance(f, IOBase):
9898
# consider ``f`` as a file object
99-
if file_type == 'i':
99+
if file_type == "i":
100100
self.input_file = f
101101
else:
102102
self.output_file = f
@@ -108,14 +108,14 @@ def __init_file(self, f: Union[IOBase, str, int, None],
108108
# consider wanna temp file
109109
fd, self.input_filename = tempfile.mkstemp()
110110
self.__init_file(fd, data_id, file_type)
111-
if file_type == 'i':
111+
if file_type == "i":
112112
self.__input_temp = True
113113
else:
114114
self.__output_temp = True
115115
else:
116116
# consider ``f`` as filename template
117-
filename = f.format(data_id or '')
118-
if file_type == 'i':
117+
filename = f.format(data_id or "")
118+
if file_type == "i":
119119
self.input_filename = filename
120120
else:
121121
self.output_filename = filename
@@ -125,7 +125,7 @@ def __init_file(self, f: Union[IOBase, str, int, None],
125125

126126
def __escape_format(self, st: str):
127127
"""replace "{}" to "{{}}" """
128-
return re.sub(r'\{', '{{', re.sub(r'\}', '}}', st))
128+
return re.sub(r"\{", "{{", re.sub(r"\}", "}}", st))
129129

130130
def __del_files(self):
131131
"""delete files"""
@@ -207,21 +207,35 @@ def input_writeln(self, *args, **kwargs):
207207
args.append("\n")
208208
self.input_write(*args, **kwargs)
209209

210-
def output_gen(self, shell_cmd):
210+
def output_gen(self, shell_cmd, time_limit=None):
211211
"""
212212
Run the command `shell_cmd` (usually the std program) and send it the input file as stdin.
213213
Write its output to the output file.
214214
Args:
215215
shell_cmd: the command to run, usually the std program.
216+
time_limit: the time limit (seconds) of the command to run.
217+
None means infinity. Defaults to None.
216218
"""
217219
self.flush_buffer()
218220
origin_pos = self.input_file.tell()
219221
self.input_file.seek(0)
220-
subprocess.check_call(shell_cmd,
221-
shell=True,
222-
stdin=self.input_file,
223-
stdout=self.output_file,
224-
universal_newlines=True)
222+
if time_limit is not None:
223+
subprocess.check_call(
224+
shell_cmd,
225+
shell=True,
226+
timeout=time_limit,
227+
stdin=self.input_file,
228+
stdout=self.output_file,
229+
universal_newlines=True,
230+
)
231+
else:
232+
subprocess.check_call(
233+
shell_cmd,
234+
shell=True,
235+
stdin=self.input_file,
236+
stdout=self.output_file,
237+
universal_newlines=True,
238+
)
225239
self.input_file.seek(origin_pos)
226240

227241
log.debug(self.output_filename, " done")

cyaron/tests/io_test.py

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33
import shutil
44
import tempfile
5+
import subprocess
56
from cyaron import IO
67
from cyaron.output_capture import captured_output
78

@@ -26,7 +27,12 @@ def test_create_files_simple(self):
2627

2728
def test_create_files_prefix_id(self):
2829
with captured_output() as (out, err):
29-
IO(file_prefix="test_prefix", data_id=233, input_suffix=".inp", output_suffix=".ans")
30+
IO(
31+
file_prefix="test_prefix",
32+
data_id=233,
33+
input_suffix=".inp",
34+
output_suffix=".ans",
35+
)
3036
self.assertTrue(os.path.exists("test_prefix233.inp"))
3137
self.assertTrue(os.path.exists("test_prefix233.ans"))
3238

@@ -50,8 +56,8 @@ def test_write_stuff(self):
5056
input = f.read()
5157
with open("test_write.out") as f:
5258
output = f.read()
53-
self.assertEqual(input.split(), ['1', '2', '3', '4', '5', '6', '7', '8', '9'])
54-
self.assertEqual(output.split(), ['9', '8', '7', '6', '5', '4', '3', '2', '1'])
59+
self.assertEqual(input.split(), ["1", "2", "3", "4", "5", "6", "7", "8", "9"])
60+
self.assertEqual(output.split(), ["9", "8", "7", "6", "5", "4", "3", "2", "1"])
5561
self.assertEqual(input.count("\n"), 2)
5662
self.assertEqual(output.count("\n"), 2)
5763

@@ -64,15 +70,44 @@ def test_output_gen(self):
6470
output = f.read()
6571
self.assertEqual(output.strip("\n"), "233")
6672

73+
def test_output_gen_time_limit_exceeded(self):
74+
time_limit_exceeded = False
75+
with captured_output() as (out, err):
76+
with open("long_time.py", "w") as f:
77+
f.write("import time\ntime.sleep(10)\nprint(1)")
78+
79+
try:
80+
with IO("test_gen.in", "test_gen.out") as test:
81+
test.output_gen("python long_time.py", time_limit=1)
82+
except subprocess.TimeoutExpired:
83+
time_limit_exceeded = True
84+
self.assertEqual(time_limit_exceeded, True)
85+
86+
def test_output_gen_time_limit_not_exceeded(self):
87+
time_limit_exceeded = False
88+
with captured_output() as (out, err):
89+
with open("short_time.py", "w") as f:
90+
f.write("import time\ntime.sleep(0.2)\nprint(1)")
91+
92+
try:
93+
with IO("test_gen.in", "test_gen.out") as test:
94+
test.output_gen("python short_time.py", time_limit=1)
95+
except subprocess.TimeoutExpired:
96+
time_limit_exceeded = True
97+
with open("test_gen.out") as f:
98+
output = f.read()
99+
self.assertEqual(output.strip("\n"), "1")
100+
self.assertEqual(time_limit_exceeded, False)
101+
67102
def test_init_overload(self):
68-
with IO(file_prefix='data{', data_id=5) as test:
69-
self.assertEqual(test.input_filename, 'data{5.in')
70-
self.assertEqual(test.output_filename, 'data{5.out')
71-
with IO('data{}.in', 'data{}.out', 5) as test:
72-
self.assertEqual(test.input_filename, 'data5.in')
73-
self.assertEqual(test.output_filename, 'data5.out')
74-
with open('data5.in', 'w+') as fin:
75-
with open('data5.out', 'w+') as fout:
103+
with IO(file_prefix="data{", data_id=5) as test:
104+
self.assertEqual(test.input_filename, "data{5.in")
105+
self.assertEqual(test.output_filename, "data{5.out")
106+
with IO("data{}.in", "data{}.out", 5) as test:
107+
self.assertEqual(test.input_filename, "data5.in")
108+
self.assertEqual(test.output_filename, "data5.out")
109+
with open("data5.in", "w+") as fin:
110+
with open("data5.out", "w+") as fout:
76111
with IO(fin, fout) as test:
77112
self.assertEqual(test.input_file, fin)
78113
self.assertEqual(test.output_file, fout)

0 commit comments

Comments
 (0)