From 41d8ad6ca4147bfecab94d97f95933c7e2040856 Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Thu, 27 Jul 2017 10:16:41 +0800 Subject: [PATCH 01/28] improve class ``IO`` ``__init__`` input_file/output_file overload to support file object file descriptor filename template ``__init__`` make tempfile avoid race ``__del__``, ``__exit__`` avoid double closing and pre-delete to avoid race ``__output_gen`` uses seek to avoid reopen --- cyaron/io.py | 150 ++++++++++++++++++++++++++++----------------------- 1 file changed, 84 insertions(+), 66 deletions(-) diff --git a/cyaron/io.py b/cyaron/io.py index fe51af6..857782a 100644 --- a/cyaron/io.py +++ b/cyaron/io.py @@ -1,21 +1,27 @@ from __future__ import absolute_import from .utils import * -from io import open +from io import open, IOBase import subprocess import tempfile import os +import re class IO(object): """Class IO: IO tool class. It will process the input and output files.""" - def __init__(self, *args, **kwargs): - """__init__(self, *args, **kwargs) -> None - (str,str) args -> The file names of input file and output file. Index 0 is the name of input file, and index 1 is for output file - **kwargs: + def __init__(self, input_file=None, output_file=None, data_id=None, file_prefix=None, input_suffix='.in', output_suffix='.out', disable_output=False): + """__init__(self, input_file=None, output_file=None, data_id=None, file_prefix=None, input_suffix='.in', output_suffix='.out', disable_output=False) -> None + input_file, output_file overload: + None -> make a temp file (if file_prefix is None) + file object -> treat the file-like object as in/output file + int -> open file by file descriptor + str -> a filename or filename template like 'awd{}.in'. ``{}`` will be replaced by ``data_id`` + int data_id -> the id of the data. if it's None, the file names will not contain the id. + legacy argumants: str file_prefix -> the prefix for the input and output files - int data_id -> the id of the data. if it's None, the file names will not contain the id. str input_suffix = ".in" -> the suffix of the input file str output_suffix = ".out" -> the suffix of the output file + disable_output -> bool, set to True to disable output Examples: IO("a","b") -> create input file "a" and output file "b" IO("a.in","b.out") -> create input file "a.in" and output file "b.out" @@ -24,74 +30,85 @@ def __init__(self, *args, **kwargs): IO(file_prefix="data",input_suffix=".input") -> create input file "data.input" and output file "data.out" IO(file_prefix="data",output_suffix=".output") -> create input file "data.in" and output file "data.output" IO(file_prefix="data",data_id=2,input_suffix=".input") -> create input file "data2.input" and output file "data2.out" + IO("data{}.in","data{}.out",data_id=2) -> create input file "data2.in" and output file "data2.out" + IO(open('data.in', 'w+'), open('data.out', 'w+')) -> input file "data.in" and output file "data.out" """ - if len(args) == 0: - if not "file_prefix" in kwargs: - self.file_flag = 0 - (fd, self.input_filename) = tempfile.mkstemp() - os.close(fd) - (fd, self.output_filename) = tempfile.mkstemp() - os.close(fd) + if file_prefix is not None: + # legacy mode + input_file = '{}{{}}{}'.format(self.__escape_format(file_prefix), self.__escape_format(input_suffix)) + output_file = '{}{{}}{}'.format(self.__escape_format(file_prefix), self.__escape_format(output_suffix)) + self.input_filename, self.output_filename = None, None + self.__input_temp, self.__output_temp = False, False + self.__init_file(input_file, data_id, 'i') + if not disable_output: + self.__init_file(output_file, data_id, 'o') + else: + self.output_file = None + self.__closed = False + self.is_first_char = {} + + def __init_file(self, f, data_id, file_type): + if isinstance(f, IOBase): + # consider ``f`` as a file object + if file_type == 'i': + self.input_file = f + else: + self.output_file = f + elif isinstance(f, int): + # consider ``f`` as a file descor + self.__init_file(os.fdopen(f, 'w+', newline='\n'), data_id, file_type) + elif f is None: + # consider wanna temp file + fd, self.input_filename = tempfile.mkstemp() + self.__init_file(fd, data_id, file_type) + if file_type == 'i': + self.__input_temp = True else: - self.file_flag = 2 - if "data_id" in kwargs: - filename_prefix = "%s%d" % (kwargs["file_prefix"], kwargs["data_id"]) - else: - filename_prefix = kwargs["file_prefix"] - - input_suffix = kwargs.get("input_suffix", ".in") - output_suffix = kwargs.get("output_suffix", ".out") - disable_output = kwargs.get("disable_output", False) - self.input_filename = filename_prefix + input_suffix - self.output_filename = filename_prefix + output_suffix if not disable_output else None - elif len(args) == 1: - self.file_flag = 1 - self.input_filename = args[0] - (fd, self.output_filename) = tempfile.mkstemp() - os.close(fd) - elif len(args) == 2: - self.file_flag = 2 - self.input_filename = args[0] - self.output_filename = args[1] + self.__output_temp = True else: - raise Exception("Invalid argument count") + # consider ``f`` as filename template + self.__init_file(open(f.format(data_id), 'w+', newline='\n'), data_id, file_type) - self.input_file = open(self.input_filename, 'w+', newline='\n') - self.output_file = open(self.output_filename, 'w+', newline='\n') if self.output_filename else None - self.is_first_char = dict() - if self.file_flag != 0: - print("Processing %s" % self.input_filename) + def __escape_format(self, st): + """replace "{}" to "{{}}" """ + return re.sub(r'\{', '{{', re.sub(r'\}', '}}', st)) - def __del__(self): - """__del__(self) -> None - Delete the IO object and close the input file and the output file - """ + def __del_files(self): + """delete files""" + if self.__input_temp: + os.remove(self.input_filename) + if self.__output_temp: + os.remove(self.output_filename) + + def close(self): + """Delete the IO object and close the input file and the output file""" + if self.__closed: + # avoid double close + return + deleted = False try: - self.input_file.close() - self.output_file.close() - if self.file_flag <= 1: - os.remove(self.output_filename) - if self.file_flag == 0: - os.remove(self.input_filename) - except Exception: + # on posix, one can remove a file while it's opend by a process + # the file then will be not visable to others, but process still have the file descriptor + # it is recommand to remove temp file before close it on posix to avoid race + # on nt, it will just fail and raise OSError so that after closing remove it again + self.__del_files() + deleted = True + except OSError: pass + self.input_file.close() + self.output_file.close() + if not deleted: + self.__del_files() + self.__closed = True + + def __del__(self): + self.close() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): - """__del__(self) -> None - Exit the context of the IO object and close the input file and the output file - """ - try: - self.input_file.close() - self.output_file.close() - if self.file_flag <= 1: - os.remove(self.output_filename) - if self.file_flag == 0: - os.remove(self.input_filename) - except Exception: - pass + self.close() def __write(self, file, *args, **kwargs): """__write(self, file, *args, **kwargs) -> None @@ -135,11 +152,12 @@ def output_gen(self, shell_cmd): Run the command shell_cmd(usually the std programme) and send it the input file as stdin. Write its output to the output file. str shell_cmd -> the command to run, usually the std programme """ - self.input_file.close() - with open(self.input_filename, 'r') as f: - self.output_file.write(make_unicode(subprocess.check_output(shell_cmd, shell=True, stdin=f, universal_newlines=True))) + self.flush_buffer() + origin_pos = self.input_file.tell() + self.input_file.seek(0) + subprocess.check_call(shell_cmd, shell=True, stdin=self.input_file, stdout=self.output_file, universal_newlines=True) + self.input_file.seek(origin_pos) - self.input_file = open(self.input_filename, 'a+') print(self.output_filename, " done") def output_write(self, *args, **kwargs): From 607be025950dcca9fb82e3de26a2b3bd27e850b0 Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Thu, 27 Jul 2017 12:11:07 +0800 Subject: [PATCH 02/28] add test for IO __init__ overload and fix filename setting --- cyaron/io.py | 7 ++++++- cyaron/tests/io_test.py | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/cyaron/io.py b/cyaron/io.py index 857782a..ed4159b 100644 --- a/cyaron/io.py +++ b/cyaron/io.py @@ -67,7 +67,12 @@ def __init_file(self, f, data_id, file_type): self.__output_temp = True else: # consider ``f`` as filename template - self.__init_file(open(f.format(data_id), 'w+', newline='\n'), data_id, file_type) + filename = f.format(data_id) + if file_type == 'i': + self.input_filename = filename + else: + self.output_filename = filename + self.__init_file(open(filename, 'w+', newline='\n'), data_id, file_type) def __escape_format(self, st): """replace "{}" to "{{}}" """ diff --git a/cyaron/tests/io_test.py b/cyaron/tests/io_test.py index 0ef2075..02b10c4 100644 --- a/cyaron/tests/io_test.py +++ b/cyaron/tests/io_test.py @@ -56,4 +56,17 @@ def test_output_gen(self): with open("test_gen.out") as f: output = f.read() - self.assertEqual(output.strip("\n"), "233") \ No newline at end of file + self.assertEqual(output.strip("\n"), "233") + + def test_init_overload(self): + with IO(file_prefix='data{', data_id=5) as test: + self.assertEqual(test.input_filename, 'data{5.in') + self.assertEqual(test.output_filename, 'data{5.out') + with IO('data{}.in', 'data{}.out', 5) as test: + self.assertEqual(test.input_filename, 'data5.in') + self.assertEqual(test.output_filename, 'data5.out') + with open('data5.in', 'w+') as fin: + with open('data5.out', 'w+') as fout: + with IO(fin, fout) as test: + self.assertEqual(test.input_file, fin) + self.assertEqual(test.output_file, fout) From 5d73147425eea1cfdafb2b171ec96a30d36e8fcd Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Thu, 27 Jul 2017 12:52:15 +0800 Subject: [PATCH 03/28] add ``cyaron.log`` add module ``log`` to control logging --- cyaron/log.py | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 cyaron/log.py diff --git a/cyaron/log.py b/cyaron/log.py new file mode 100644 index 0000000..bfeef90 --- /dev/null +++ b/cyaron/log.py @@ -0,0 +1,71 @@ +from __future__ import print_function +from functools import partial +import sys + +_print = print + +def _join_dict(a, b): + """join two dict""" + c = a.copy() + for k, v in b.items(): + c[k] = v + return c + +_log_funcs = {} +_log = lambda funcname, *args, **kwargs: _log_funcs.get(funcname, lambda *args, **kwargs: None)(*args, **kwargs) + +"""5 log levels +1. debug: debug info +2. info: common info +3. print: print output +4. warn: warnings +5. error: errors +""" + +debug = partial(_log, 'debug') +info = partial(_log, 'info') +print = partial(_log, 'print') +warn = partial(_log, 'warn') +error = partial(_log, 'error') + +def register_logfunc(funcname, func): + """register logfunc + str funcname -> name of logfunc + callable func -> logfunc + """ + if func is not None: + _log_funcs[funcname] = func + else: + try: + del _log_funcs[funcname] + except KeyError: + pass + +_nb_print = lambda *args, **kwargs: _print(*args, **_join_dict(kwargs, {'flush': True})) +_nb_print_e = lambda *args, **kwargs: _print(*args, **_join_dict(kwargs, {'file': sys.stderr, 'flush': True})) + +def set_quiet(): + """set log mode to "quiet" """ + register_logfunc('debug', None) + register_logfunc('info', None) + register_logfunc('print', _nb_print) + register_logfunc('warn', None) + register_logfunc('error', _nb_print_e) + +def set_normal(): + """set log mode to "normal" """ + register_logfunc('debug', None) + register_logfunc('info', _nb_print) + register_logfunc('print', _nb_print) + register_logfunc('warn', _nb_print_e) + register_logfunc('error', _nb_print_e) + +def set_verbose(): + """set log mode to "verbose" """ + register_logfunc('debug', _nb_print) + register_logfunc('info', _nb_print) + register_logfunc('print', _nb_print) + register_logfunc('warn', _nb_print_e) + register_logfunc('error', _nb_print_e) + +set_normal() From c358483d8bd08a33775dc256cab1b966cb4fd9ff Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Thu, 27 Jul 2017 12:56:35 +0800 Subject: [PATCH 04/28] use log in IO --- cyaron/io.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cyaron/io.py b/cyaron/io.py index ed4159b..f1d64e0 100644 --- a/cyaron/io.py +++ b/cyaron/io.py @@ -1,5 +1,6 @@ from __future__ import absolute_import from .utils import * +from . import log from io import open, IOBase import subprocess import tempfile @@ -70,6 +71,7 @@ def __init_file(self, f, data_id, file_type): filename = f.format(data_id) if file_type == 'i': self.input_filename = filename + log.info("Processing %s" % self.input_filename) else: self.output_filename = filename self.__init_file(open(filename, 'w+', newline='\n'), data_id, file_type) @@ -163,7 +165,7 @@ def output_gen(self, shell_cmd): subprocess.check_call(shell_cmd, shell=True, stdin=self.input_file, stdout=self.output_file, universal_newlines=True) self.input_file.seek(origin_pos) - print(self.output_filename, " done") + log.info(self.output_filename, " done") def output_write(self, *args, **kwargs): """output_write(self, *args, **kwargs) -> None From 5bd8bc0728a46ab49a05f73e13a756d113e36e8d Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Thu, 27 Jul 2017 14:01:43 +0800 Subject: [PATCH 05/28] structed compare mismatch message add ``cyaron.graders.mismatch`` Mismatch HashMismatch TextMismatch modify ``cyaron.graders.fulltext`` & ``cyaron.graders.noipstyle`` to return Mismatch class instead of str error message --- cyaron/graders/fulltext.py | 3 ++- cyaron/graders/mismatch.py | 28 ++++++++++++++++++++++++++++ cyaron/graders/noipstyle.py | 13 +++++++------ 3 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 cyaron/graders/mismatch.py diff --git a/cyaron/graders/fulltext.py b/cyaron/graders/fulltext.py index 5b5260a..5e9a8ec 100644 --- a/cyaron/graders/fulltext.py +++ b/cyaron/graders/fulltext.py @@ -1,9 +1,10 @@ import hashlib from .graderregistry import CYaRonGraders +from .mismatch import HashMismatch @CYaRonGraders.grader("FullText") def fulltext(content, std): content_hash = hashlib.sha256(content.encode('utf-8')).hexdigest() std_hash = hashlib.sha256(std.encode('utf-8')).hexdigest() - return (True, None) if content_hash == std_hash else (False, "Hash mismatch: read %s, expected %s" % (content_hash, std_hash)) + return (True, None) if content_hash == std_hash else (False, HashMismatch(content_hash, std_hash)) diff --git a/cyaron/graders/mismatch.py b/cyaron/graders/mismatch.py new file mode 100644 index 0000000..9203bfe --- /dev/null +++ b/cyaron/graders/mismatch.py @@ -0,0 +1,28 @@ +class Mismatch(ValueError): + """exception for content mismatch""" + def __init__(self, content, std, *args): + """ + content -> content got + std -> content expected + """ + super().__init__(content, std, *args) + self.content = content + self.std = std + +class HashMismatch(Mismatch): + """exception for hash mismatch""" + def __str__(self): + return "Hash mismatch: read %s, expected %s" % (self.content, self.std) + +class TextMismatch(Mismatch): + """exception for text mismatch""" + def __init__(self, content, std, err_msg, lineno=None, colno=None, content_token=None, std_token=None): + super().__init__(content, std, err_msg, lineno, colno, content_token, std_token) + self.err_msg = err_msg.format(lineno, colno, content_token, std_token) + self.lineno = lineno + self.colno = colno + self.content_token = content_token + self.std_token = std_token + + def __str__(self): + return self.err_msg diff --git a/cyaron/graders/noipstyle.py b/cyaron/graders/noipstyle.py index 3895d53..9af2d9b 100644 --- a/cyaron/graders/noipstyle.py +++ b/cyaron/graders/noipstyle.py @@ -1,5 +1,6 @@ from ..utils import * from .graderregistry import CYaRonGraders +from .mismatch import TextMismatch @CYaRonGraders.grader("NOIPStyle") @@ -7,17 +8,17 @@ def noipstyle(content, std): content_lines = strtolines(content.replace('\r\n', '\n')) std_lines = strtolines(std.replace('\r\n', '\n')) if len(content_lines) != len(std_lines): - return False, 'Too many or too few lines.' + return False, TextMismatch(content, std, 'Too many or too few lines.') for i in range(len(content_lines)): if std_lines[i] != content_lines[i]: for j in range(min(len(std_lines[i]), len(content_lines[i]))): if std_lines[i][j] != content_lines[i][j]: - return (False, 'On line %d column %d, read %s, expected %s.' - % (i + 1, j + 1, content_lines[i][j:j + 5], std_lines[i][j:j + 5])) + return (False, TextMismatch(content, std, 'On line {} column {}, read {}, expected {}.', + i + 1, j + 1, content_lines[i][j:j + 5], std_lines[i][j:j + 5])) if len(std_lines[i]) > len(content_lines[i]): - return False, 'Too short on line %d.' % i + return False, TextMismatch(content, std, 'Too short on line {}.', i) if len(std_lines[i]) < len(content_lines[i]): - return False, 'Too long on line %d.' % i + return False, TextMismatch(content, std, 'Too long on line {}.', i) - return True, None \ No newline at end of file + return True, None From 190dcdaef31403ec9bbfada318932024623373ce Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Thu, 27 Jul 2017 15:28:52 +0800 Subject: [PATCH 06/28] use log in ``cyaron.compare`` use log in ``cyaron.compare``, prints error to stderr add arguments to ``__compare_two`` raise_on_incorrect raise an error when incorrect dump_on_incorrect dump data on incorrect (default: True) ``stop_on_incorrect`` exit with code 1 instead of 0 to mark error state use TypeError instead of Exception when arguments missing to fit the behavior of python's --- cyaron/compare.py | 53 ++++++++++++++++++++++-------------- cyaron/tests/compare_test.py | 12 +++++--- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/cyaron/compare.py b/cyaron/compare.py index cf3991c..54a6eeb 100644 --- a/cyaron/compare.py +++ b/cyaron/compare.py @@ -1,5 +1,5 @@ from __future__ import absolute_import -from cyaron import IO +from cyaron import IO, log from cyaron.utils import * from cyaron.consts import * from cyaron.graders import CYaRonGraders @@ -15,24 +15,30 @@ def __compare_two(name, content, std, grader, **kwargs): info = info if info is not None else "" status = "Correct" if result else "!!!INCORRECT!!!" - print("%s: %s %s" % (name, status, info)) + (log.print if result else log.error)("%s: %s %s" % (name, status, info)) stop_on_incorrect = kwargs.get("stop_on_incorrect", False) + raise_on_incorrect = kwargs.get("raise_on_incorrect", False) + dump_on_incorrect = kwargs.get("dump_on_incorrect", True) custom_dump_data = kwargs.get("dump_data", None) - if stop_on_incorrect and not result: + if (stop_on_incorrect or raise_on_incorrect) and not result: if custom_dump_data: (dump_name, dump_lambda) = custom_dump_data with open(dump_name, "w", newline='\n') as f: f.write(dump_lambda()) - with open("std.out", "w", newline='\n') as f: - f.write(std) - with open("%s.out" % name, "w", newline='\n') as f: - f.write(content) + if dump_on_incorrect: + with open("std.out", "w", newline='\n') as f: + f.write(std) + with open("%s.out" % name, "w", newline='\n') as f: + f.write(content) - print("Relevant files dumped.") + log.info("Relevant files dumped.") - sys.exit(0) + if stop_on_incorrect: + sys.exit(1) + else: + raise info @staticmethod @@ -48,51 +54,58 @@ def __process_file(file): @staticmethod def output(*args, **kwargs): if len(args) == 0: - raise Exception("You must specify some files to compare.") + raise TypeError("You must specify some files to compare.") if "std" not in kwargs: - raise Exception("You must specify a std.") + raise TypeError("You must specify a std.") (_, std) = Compare.__process_file(kwargs["std"]) grader = kwargs.get("grader", DEFAULT_GRADER) - stop_on_incorrect = kwargs.get("stop_on_incorrect", False) + del kwargs["std"] + try: del kwargs["grader"] + except KeyError: pass for file in args: (file_name, content) = Compare.__process_file(file) - Compare.__compare_two(file_name, content, std, grader, stop_on_incorrect=stop_on_incorrect) + Compare.__compare_two(file_name, content, std, grader, **kwargs) @staticmethod def program(*args, **kwargs): if len(args) == 0: - raise Exception("You must specify some programs to compare.") + raise TypeError("You must specify some programs to compare.") if "input" not in kwargs: - raise Exception("You must specify an input.") + raise TypeError("You must specify an input.") input = kwargs['input'] + del kwargs['input'] if not isinstance(input, IO): - raise Exception("Input must be an IO instance.") + raise TypeError("Input must be an IO instance.") input.flush_buffer() input.input_file.seek(0) std = None if "std" not in kwargs and "std_program" not in kwargs: - raise Exception("You must specify a std or a std_program.") + raise TypeError("You must specify a std or a std_program.") else: if "std_program" in kwargs: std = make_unicode(subprocess.check_output(kwargs['std_program'], shell=True, stdin=input.input_file, universal_newlines=True)) + del kwargs['std_program'] else: (_, std) = Compare.__process_file(kwargs["std"]) + del kwargs['std'] grader = kwargs.get("grader", DEFAULT_GRADER) - stop_on_incorrect = kwargs.get("stop_on_incorrect", False) + try: del kwargs["grader"] + except KeyError: pass for program_name in args: + kws = kwargs.copy() input.input_file.seek(0) content = make_unicode(subprocess.check_output(program_name, shell=True, stdin=input.input_file, universal_newlines=True)) input.input_file.seek(0) + kws['dump_data'] = kws.get('dump_data', ("error_input.in", lambda: input.input_file.read())) # Lazy dump Compare.__compare_two(program_name, content, std, grader, - stop_on_incorrect=stop_on_incorrect, - dump_data=("error_input.in", lambda: input.input_file.read())) # Lazy dump + **kws) input.input_file.seek(0, 2) diff --git a/cyaron/tests/compare_test.py b/cyaron/tests/compare_test.py index 1cc7c1e..f7716be 100644 --- a/cyaron/tests/compare_test.py +++ b/cyaron/tests/compare_test.py @@ -45,7 +45,9 @@ def test_noipstyle_incorrect(self): Compare.output("test_another_incorrect.out", std=io) result = out.getvalue().strip() - self.assertEqual(result, "test_another_incorrect.out: !!!INCORRECT!!! On line 2 column 7, read 4, expected 3.") + stderr = err.getvalue().strip() + self.assertEqual(result, "") + self.assertEqual(stderr, "test_another_incorrect.out: !!!INCORRECT!!! On line 2 column 7, read 4, expected 3.") def test_fulltext_program(self): with open("correct.py", "w") as f: @@ -64,6 +66,8 @@ def test_fulltext_program(self): Compare.program("python correct.py", "python incorrect.py", std=io, input=io, grader="FullText") result = out.getvalue().strip() - correct_text = 'python correct.py: Correct \npython incorrect.py: !!!INCORRECT!!! Hash mismatch: read 53c234e5e8472b6ac51c1ae1cab3fe06fad053beb8ebfd8977b010655bfdd3c3, expected 4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865' - self.assertEqual(result, correct_text) - + stderr = err.getvalue().strip() + correct_out = 'python correct.py: Correct' + correct_err = 'python incorrect.py: !!!INCORRECT!!! Hash mismatch: read 53c234e5e8472b6ac51c1ae1cab3fe06fad053beb8ebfd8977b010655bfdd3c3, expected 4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865' + self.assertEqual(result, correct_out) + self.assertEqual(stderr, correct_err) From 7e817b4711a52856b5cc0036adc150acd0a785bb Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Thu, 27 Jul 2017 15:43:20 +0800 Subject: [PATCH 07/28] fix issue #23 --- cyaron/math.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/cyaron/math.py b/cyaron/math.py index 9a21471..04de755 100644 --- a/cyaron/math.py +++ b/cyaron/math.py @@ -3,7 +3,7 @@ forked from https://blog.dreamshire.com/common-functions-routines-project-euler/ ''' from __future__ import absolute_import -from math import sqrt, ceil +from math import sqrt, ceil, gcd from functools import reduce import random import itertools @@ -176,25 +176,6 @@ def factor(n): factors.append((f, e)) -#--- greatest common divisor---------------------------------------------------------------------- -def gcd(a, b): - """ - Compute the greatest common divisor of a and b. Examples: - - >>> gcd(14, 15) #co-prime - 1 - >>> gcd(5*5, 3*5) - 5 - """ - if a < 0: a = -a - if b < 0: b = -b - if a == 0: return b - while (b): a, b = b, a%b - return a - - - - #--- generate permutations----------------------------------------------------------------------- def perm(n, s): """ From e0e3eec80d051f024073a3df30701358fdc047a3 Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Thu, 27 Jul 2017 16:58:25 +0800 Subject: [PATCH 08/28] add colorful log --- cyaron/log.py | 32 +++++++++++++++++++++----------- setup.py | 2 +- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/cyaron/log.py b/cyaron/log.py index bfeef90..cfdca80 100644 --- a/cyaron/log.py +++ b/cyaron/log.py @@ -1,6 +1,8 @@ from __future__ import print_function from functools import partial import sys +import colorful +from .utils import make_unicode _print = print @@ -43,29 +45,37 @@ def register_logfunc(funcname, func): _nb_print = lambda *args, **kwargs: _print(*args, **_join_dict(kwargs, {'flush': True})) _nb_print_e = lambda *args, **kwargs: _print(*args, **_join_dict(kwargs, {'file': sys.stderr, 'flush': True})) +_cl_print = lambda color, *args, **kwargs: _nb_print(*[color(make_unicode(item)) for item in args], **kwargs) +_cl_print_e = lambda color, *args, **kwargs: _nb_print_e(*[color(make_unicode(item)) for item in args], **kwargs) + +_default_debug = partial(_cl_print, colorful.cyan) +_default_info = partial(_cl_print, colorful.blue) +_default_print = _nb_print +_default_warn = partial(_cl_print_e, colorful.yellow) +_default_error = partial(_cl_print_e, colorful.red) def set_quiet(): """set log mode to "quiet" """ register_logfunc('debug', None) register_logfunc('info', None) - register_logfunc('print', _nb_print) + register_logfunc('print', _default_print) register_logfunc('warn', None) - register_logfunc('error', _nb_print_e) + register_logfunc('error', _default_error) def set_normal(): """set log mode to "normal" """ register_logfunc('debug', None) - register_logfunc('info', _nb_print) - register_logfunc('print', _nb_print) - register_logfunc('warn', _nb_print_e) - register_logfunc('error', _nb_print_e) + register_logfunc('info', _default_info) + register_logfunc('print', _default_print) + register_logfunc('warn', _default_warn) + register_logfunc('error', _default_error) def set_verbose(): """set log mode to "verbose" """ - register_logfunc('debug', _nb_print) - register_logfunc('info', _nb_print) - register_logfunc('print', _nb_print) - register_logfunc('warn', _nb_print_e) - register_logfunc('error', _nb_print_e) + register_logfunc('debug', _default_debug) + register_logfunc('info', _default_info) + register_logfunc('print', _default_print) + register_logfunc('warn', _default_warn) + register_logfunc('error', _default_error) set_normal() diff --git a/setup.py b/setup.py index 0cac8c4..dabeecc 100644 --- a/setup.py +++ b/setup.py @@ -12,5 +12,5 @@ packages=find_packages(), include_package_data=True, platforms='any', - install_requires=[], + install_requires=['colorful>=0.3.5'], ) From 69a45762112a23bfa6887fbf4f4204244e49a99a Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Thu, 27 Jul 2017 17:04:02 +0800 Subject: [PATCH 09/28] plain output when outfile is not a tty --- cyaron/log.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cyaron/log.py b/cyaron/log.py index cfdca80..523736b 100644 --- a/cyaron/log.py +++ b/cyaron/log.py @@ -45,8 +45,8 @@ def register_logfunc(funcname, func): _nb_print = lambda *args, **kwargs: _print(*args, **_join_dict(kwargs, {'flush': True})) _nb_print_e = lambda *args, **kwargs: _print(*args, **_join_dict(kwargs, {'file': sys.stderr, 'flush': True})) -_cl_print = lambda color, *args, **kwargs: _nb_print(*[color(make_unicode(item)) for item in args], **kwargs) -_cl_print_e = lambda color, *args, **kwargs: _nb_print_e(*[color(make_unicode(item)) for item in args], **kwargs) +_cl_print = lambda color, *args, **kwargs: _nb_print(*[color(make_unicode(item)) for item in args], **kwargs) if sys.stdout.isatty() else _nb_print(*args, **kwargs) +_cl_print_e = lambda color, *args, **kwargs: _nb_print_e(*[color(make_unicode(item)) for item in args], **kwargs) if sys.stderr.isatty() else _nb_print_e(*args, **kwargs) _default_debug = partial(_cl_print, colorful.cyan) _default_info = partial(_cl_print, colorful.blue) From c0485917aa5ce5b087d20d5f709b587aef63b090 Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Thu, 27 Jul 2017 17:55:21 +0800 Subject: [PATCH 10/28] fix CI --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index bb6314e..a8d77a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,5 +5,6 @@ python: - "3.6" - "pypy" - "pypy3" -script: python unit_test.py - +script: + - python setup.py install + - python unit_test.py From 3a3cbc0130df5785b9cbeb3f4999741258497c5b Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Thu, 27 Jul 2017 18:17:52 +0800 Subject: [PATCH 11/28] fix log.print in ``cyaron.compare`` --- cyaron/compare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cyaron/compare.py b/cyaron/compare.py index 54a6eeb..0a52992 100644 --- a/cyaron/compare.py +++ b/cyaron/compare.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import absolute_import, print_function from cyaron import IO, log from cyaron.utils import * from cyaron.consts import * From 9d4cf064d9488c542bb7f397a6134e5e3fe6cb0f Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Thu, 27 Jul 2017 18:19:13 +0800 Subject: [PATCH 12/28] fix #23 python2 does not has ``math.gcd`` use ``fractools.gcd`` --- cyaron/math.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cyaron/math.py b/cyaron/math.py index 04de755..210ff62 100644 --- a/cyaron/math.py +++ b/cyaron/math.py @@ -3,7 +3,8 @@ forked from https://blog.dreamshire.com/common-functions-routines-project-euler/ ''' from __future__ import absolute_import -from math import sqrt, ceil, gcd +from math import sqrt, ceil +from fractions import gcd from functools import reduce import random import itertools From 178453507c7d5faed919a8b3e974eb191151e51d Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Thu, 27 Jul 2017 18:25:20 +0800 Subject: [PATCH 13/28] python2 print 'flush' keyword workaround --- cyaron/log.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cyaron/log.py b/cyaron/log.py index 523736b..d3e3aa5 100644 --- a/cyaron/log.py +++ b/cyaron/log.py @@ -4,7 +4,15 @@ import colorful from .utils import make_unicode -_print = print +__print = print +def _print(*args, **kwargs): + flush = False + if 'flush' in kwargs: + flush = kwargs['flush'] + del kwargs['flush'] + __print(*args, **kwargs) + if flush: + kwargs.get('file', sys.stdout).flush() def _join_dict(a, b): """join two dict""" From 1e637e80d5b57229e87474641453deff3715cc3f Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Thu, 27 Jul 2017 18:36:23 +0800 Subject: [PATCH 14/28] python compat file object check --- cyaron/io.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cyaron/io.py b/cyaron/io.py index f1d64e0..5df1ec7 100644 --- a/cyaron/io.py +++ b/cyaron/io.py @@ -49,7 +49,11 @@ def __init__(self, input_file=None, output_file=None, data_id=None, file_prefix= self.is_first_char = {} def __init_file(self, f, data_id, file_type): - if isinstance(f, IOBase): + try: + is_file = isinstance(f, file) + except NameError: + is_file = False + if isinstance(f, IOBase) or is_file: # consider ``f`` as a file object if file_type == 'i': self.input_file = f From 15aa903329ae2f257a522b3cd83a87c74299fdac Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Thu, 27 Jul 2017 18:39:58 +0800 Subject: [PATCH 15/28] fix super() in python2 --- cyaron/graders/mismatch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cyaron/graders/mismatch.py b/cyaron/graders/mismatch.py index 9203bfe..d12bc62 100644 --- a/cyaron/graders/mismatch.py +++ b/cyaron/graders/mismatch.py @@ -5,7 +5,7 @@ def __init__(self, content, std, *args): content -> content got std -> content expected """ - super().__init__(content, std, *args) + super(Mismatch, self).__init__(content, std, *args) self.content = content self.std = std @@ -17,7 +17,7 @@ def __str__(self): class TextMismatch(Mismatch): """exception for text mismatch""" def __init__(self, content, std, err_msg, lineno=None, colno=None, content_token=None, std_token=None): - super().__init__(content, std, err_msg, lineno, colno, content_token, std_token) + super(TextMismatch, self).__init__(content, std, err_msg, lineno, colno, content_token, std_token) self.err_msg = err_msg.format(lineno, colno, content_token, std_token) self.lineno = lineno self.colno = colno From 3dba93d8eab492f833c90e5ff8e241989f148513 Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Thu, 27 Jul 2017 19:39:48 +0800 Subject: [PATCH 16/28] fix CI --- .travis.yml | 2 +- cyaron/log.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a8d77a4..92a7078 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,5 +6,5 @@ python: - "pypy" - "pypy3" script: - - python setup.py install + - if [ $TRAVIS_BUILD_NUMBER != 5 ]; pip install colorful>=0.3.5; fi - python unit_test.py diff --git a/cyaron/log.py b/cyaron/log.py index d3e3aa5..69e6da3 100644 --- a/cyaron/log.py +++ b/cyaron/log.py @@ -1,7 +1,13 @@ from __future__ import print_function from functools import partial import sys -import colorful +try: + import colorful +except ImportError: + class colorful: + def __getattr__(self, attr): + return lambda st: st + colorful = colorful() from .utils import make_unicode __print = print From 4345e8f912234df772a1e6552112d5834fa5c0ae Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Thu, 27 Jul 2017 20:02:17 +0800 Subject: [PATCH 17/28] use tox --- .travis.yml | 5 ++--- tox.ini | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml index 92a7078..7ae0c62 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,5 @@ python: - "3.6" - "pypy" - "pypy3" -script: - - if [ $TRAVIS_BUILD_NUMBER != 5 ]; pip install colorful>=0.3.5; fi - - python unit_test.py +install: pip install tox-travis +script: tox diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..ed1e45d --- /dev/null +++ b/tox.ini @@ -0,0 +1,2 @@ +[testenv] +commands=python unit_test.py From 47149c1ac04c4da5d54f97a2f70e56361ccbc319 Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Thu, 27 Jul 2017 20:24:04 +0800 Subject: [PATCH 18/28] import log in __init__ --- cyaron/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cyaron/__init__.py b/cyaron/__init__.py index 6fdb07d..c9b81a1 100644 --- a/cyaron/__init__.py +++ b/cyaron/__init__.py @@ -17,4 +17,5 @@ from .math import * from .merger import Merger #from .visual import visualize +from . import log from random import randint, randrange, uniform, choice, random From e4476c1f63b986b81e605a8ca4015bab054e52ef Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Fri, 28 Jul 2017 08:29:06 +0800 Subject: [PATCH 19/28] expose log.log --- cyaron/log.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cyaron/log.py b/cyaron/log.py index 69e6da3..4d60be6 100644 --- a/cyaron/log.py +++ b/cyaron/log.py @@ -28,7 +28,7 @@ def _join_dict(a, b): return c _log_funcs = {} -_log = lambda funcname, *args, **kwargs: _log_funcs.get(funcname, lambda *args, **kwargs: None)(*args, **kwargs) +log = lambda funcname, *args, **kwargs: _log_funcs.get(funcname, lambda *args, **kwargs: None)(*args, **kwargs) """5 log levels 1. debug: debug info @@ -38,11 +38,11 @@ def _join_dict(a, b): 5. error: errors """ -debug = partial(_log, 'debug') -info = partial(_log, 'info') -print = partial(_log, 'print') -warn = partial(_log, 'warn') -error = partial(_log, 'error') +debug = partial(log, 'debug') +info = partial(log, 'info') +print = partial(log, 'print') +warn = partial(log, 'warn') +error = partial(log, 'error') def register_logfunc(funcname, func): """register logfunc From c8e2d4cff135dc602db664198a0d9c4ac004a2f3 Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Fri, 28 Jul 2017 17:16:46 +0800 Subject: [PATCH 20/28] modify HashMismatch.__init__ to accept original content --- cyaron/graders/fulltext.py | 2 +- cyaron/graders/mismatch.py | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/cyaron/graders/fulltext.py b/cyaron/graders/fulltext.py index 5e9a8ec..8460b6f 100644 --- a/cyaron/graders/fulltext.py +++ b/cyaron/graders/fulltext.py @@ -6,5 +6,5 @@ def fulltext(content, std): content_hash = hashlib.sha256(content.encode('utf-8')).hexdigest() std_hash = hashlib.sha256(std.encode('utf-8')).hexdigest() - return (True, None) if content_hash == std_hash else (False, HashMismatch(content_hash, std_hash)) + return (True, None) if content_hash == std_hash else (False, HashMismatch(content, std, content_hash, std_hash)) diff --git a/cyaron/graders/mismatch.py b/cyaron/graders/mismatch.py index d12bc62..70c2dfc 100644 --- a/cyaron/graders/mismatch.py +++ b/cyaron/graders/mismatch.py @@ -11,12 +11,32 @@ def __init__(self, content, std, *args): class HashMismatch(Mismatch): """exception for hash mismatch""" + def __init__(self, content, std, content_hash, std_hash): + """ + content -> content got + std -> content expected + content_hash -> hash of content + std_hash -> hash of std + """ + super(HashMismatch, self).__init__(content, std, content_hash, std_hash) + self.content_hash = content_hash + self.std_hash = std_hash + def __str__(self): - return "Hash mismatch: read %s, expected %s" % (self.content, self.std) + return "Hash mismatch: read %s, expected %s" % (self.content_hash, self.std_hash) class TextMismatch(Mismatch): """exception for text mismatch""" def __init__(self, content, std, err_msg, lineno=None, colno=None, content_token=None, std_token=None): + """ + content -> content got + std -> content expected + err_msg -> error message template like "wrong on line {} col {} read {} expected {}" + lineno -> line number + colno -> column number + content_token -> the token of content mismatch + std_token -> the token of std + """ super(TextMismatch, self).__init__(content, std, err_msg, lineno, colno, content_token, std_token) self.err_msg = err_msg.format(lineno, colno, content_token, std_token) self.lineno = lineno From 0d07fcb41337b081bd29ece4a8e62d96a420598c Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Fri, 28 Jul 2017 20:10:42 +0800 Subject: [PATCH 21/28] concurrent ``cyaron.compare`` --- cyaron/compare.py | 136 ++++++++++++++++------------------- cyaron/io.py | 8 +-- cyaron/tests/compare_test.py | 48 +++++++++---- 3 files changed, 103 insertions(+), 89 deletions(-) diff --git a/cyaron/compare.py b/cyaron/compare.py index 0a52992..fe4c5f3 100644 --- a/cyaron/compare.py +++ b/cyaron/compare.py @@ -6,40 +6,18 @@ import subprocess import sys from io import open +import os class Compare: @staticmethod - def __compare_two(name, content, std, grader, **kwargs): + def __compare_two(name, content, std, grader): (result, info) = CYaRonGraders.invoke(grader, content, std) - - info = info if info is not None else "" status = "Correct" if result else "!!!INCORRECT!!!" - (log.print if result else log.error)("%s: %s %s" % (name, status, info)) - - stop_on_incorrect = kwargs.get("stop_on_incorrect", False) - raise_on_incorrect = kwargs.get("raise_on_incorrect", False) - dump_on_incorrect = kwargs.get("dump_on_incorrect", True) - custom_dump_data = kwargs.get("dump_data", None) - if (stop_on_incorrect or raise_on_incorrect) and not result: - if custom_dump_data: - (dump_name, dump_lambda) = custom_dump_data - with open(dump_name, "w", newline='\n') as f: - f.write(dump_lambda()) - - if dump_on_incorrect: - with open("std.out", "w", newline='\n') as f: - f.write(std) - with open("%s.out" % name, "w", newline='\n') as f: - f.write(content) - - log.info("Relevant files dumped.") - - if stop_on_incorrect: - sys.exit(1) - else: - raise info - + info = info if info is not None else "" + log.debug("{}: {} {}".format(name, status, info)) + if not result: + raise info @staticmethod def __process_file(file): @@ -52,60 +30,72 @@ def __process_file(file): return file, f.read() @staticmethod - def output(*args, **kwargs): - if len(args) == 0: - raise TypeError("You must specify some files to compare.") - - if "std" not in kwargs: - raise TypeError("You must specify a std.") - (_, std) = Compare.__process_file(kwargs["std"]) - - grader = kwargs.get("grader", DEFAULT_GRADER) - del kwargs["std"] - try: del kwargs["grader"] - except KeyError: pass + def output(*files, std, grader=DEFAULT_GRADER, max_workers=-1, job_pool=None): + if (max_workers is None or max_workers >= 0) and job_pool is None: + try: + from concurrent.futures import ThreadPoolExecutor + with ThreadPoolExecutor(max_workers=max_workers) as job_pool: + return Compare.output(*files, std=std, grader=grader, max_workers=max_workers, job_pool=job_pool) + except ImportError: + pass + + def get_std(): + nonlocal std + (_, std) = Compare.__process_file(std) + if job_pool is not None: + job_pool.submit(get_std).result() + else: + get_std() - for file in args: + def do(file): (file_name, content) = Compare.__process_file(file) - Compare.__compare_two(file_name, content, std, grader, **kwargs) + Compare.__compare_two(file_name, content, std, grader) + + if job_pool is not None: + job_pool.map(do, files) + else: + [x for x in map(do, files)] @staticmethod - def program(*args, **kwargs): - if len(args) == 0: - raise TypeError("You must specify some programs to compare.") + def program(*programs, input, std=None, std_program=None, grader=DEFAULT_GRADER, max_workers=-1, job_pool=None): + if (max_workers is None or max_workers >= 0) and job_pool is None: + try: + from concurrent.futures import ThreadPoolExecutor + with ThreadPoolExecutor(max_workers=max_workers) as job_pool: + return Compare.program(*programs, input=input, std=std, std_program=std_program, grader=grader, max_workers=max_workers, job_pool=job_pool) + except ImportError: + pass - if "input" not in kwargs: - raise TypeError("You must specify an input.") - input = kwargs['input'] - del kwargs['input'] if not isinstance(input, IO): - raise TypeError("Input must be an IO instance.") + raise TypeError("expect {}, got {}".format(type(IO).__name__, type(input).__name__)) input.flush_buffer() input.input_file.seek(0) - std = None - if "std" not in kwargs and "std_program" not in kwargs: - raise TypeError("You must specify a std or a std_program.") - else: - if "std_program" in kwargs: - std = make_unicode(subprocess.check_output(kwargs['std_program'], shell=True, stdin=input.input_file, universal_newlines=True)) - del kwargs['std_program'] + if std_program is not None: + def get_std(): + nonlocal std + std = make_unicode(subprocess.check_output(std_program, shell=(not list_like(std_program)), stdin=input.input_file, universal_newlines=True)) + if job_pool is not None: + job_pool.submit(get_std).result() else: - (_, std) = Compare.__process_file(kwargs["std"]) - del kwargs['std'] - - grader = kwargs.get("grader", DEFAULT_GRADER) - try: del kwargs["grader"] - except KeyError: pass - - for program_name in args: - kws = kwargs.copy() - input.input_file.seek(0) - content = make_unicode(subprocess.check_output(program_name, shell=True, stdin=input.input_file, universal_newlines=True)) + get_std() + elif std is not None: + def get_std(): + nonlocal std + (_, std) = Compare.__process_file(std) + if job_pool is not None: + job_pool.submit(get_std).result() + else: + get_std() + else: + raise TypeError('program() missing 1 required non-None positional argument: \'std\' or \'std_program\'') - input.input_file.seek(0) - kws['dump_data'] = kws.get('dump_data', ("error_input.in", lambda: input.input_file.read())) # Lazy dump - Compare.__compare_two(program_name, content, std, grader, - **kws) + def do(program_name): + with os.fdopen(os.dup(input.input_file.fileno()), 'r', newline='\n') as input_file: + content = make_unicode(subprocess.check_output(program_name, shell=(not list_like(program_name)), stdin=input_file, universal_newlines=True)) + Compare.__compare_two(program_name, content, std, grader) - input.input_file.seek(0, 2) + if job_pool is not None: + job_pool.map(do, programs) + else: + [x for x in map(do, programs)] diff --git a/cyaron/io.py b/cyaron/io.py index 5df1ec7..8de72f7 100644 --- a/cyaron/io.py +++ b/cyaron/io.py @@ -75,7 +75,7 @@ def __init_file(self, f, data_id, file_type): filename = f.format(data_id) if file_type == 'i': self.input_filename = filename - log.info("Processing %s" % self.input_filename) + log.debug("Processing %s" % self.input_filename) else: self.output_filename = filename self.__init_file(open(filename, 'w+', newline='\n'), data_id, file_type) @@ -86,9 +86,9 @@ def __escape_format(self, st): def __del_files(self): """delete files""" - if self.__input_temp: + if self.__input_temp and self.input_filename is not None: os.remove(self.input_filename) - if self.__output_temp: + if self.__output_temp and self.output_filename is not None: os.remove(self.output_filename) def close(self): @@ -169,7 +169,7 @@ def output_gen(self, shell_cmd): subprocess.check_call(shell_cmd, shell=True, stdin=self.input_file, stdout=self.output_file, universal_newlines=True) self.input_file.seek(origin_pos) - log.info(self.output_filename, " done") + log.debug(self.output_filename, " done") def output_write(self, *args, **kwargs): """output_write(self, *args, **kwargs) -> None diff --git a/cyaron/tests/compare_test.py b/cyaron/tests/compare_test.py index f7716be..12a92fb 100644 --- a/cyaron/tests/compare_test.py +++ b/cyaron/tests/compare_test.py @@ -1,9 +1,13 @@ import unittest import os +import sys import shutil import tempfile -from cyaron import IO, Compare +from cyaron import IO, Compare, log from cyaron.output_capture import captured_output +from cyaron.graders.mismatch import * + +log.set_verbose() class TestCompare(unittest.TestCase): @@ -41,13 +45,18 @@ def test_noipstyle_incorrect(self): with open("test_another_incorrect.out", "w") as f: f.write("test123\r\ntest124 ") - with captured_output() as (out, err): - Compare.output("test_another_incorrect.out", std=io) + try: + with captured_output() as (out, err): + Compare.output("test_another_incorrect.out", std=io) + except TextMismatch as e: + self.assertEqual(e.content, 'test123\r\r\ntest124 ') + self.assertEqual(e.std, 'test123 \ntest123\n\n') + self.assertEqual(str(e), 'On line 2 column 7, read 4, expected 3.') + else: + self.assertTrue(False) result = out.getvalue().strip() - stderr = err.getvalue().strip() - self.assertEqual(result, "") - self.assertEqual(stderr, "test_another_incorrect.out: !!!INCORRECT!!! On line 2 column 7, read 4, expected 3.") + self.assertEqual(result, "test_another_incorrect.out: !!!INCORRECT!!! On line 2 column 7, read 4, expected 3.") def test_fulltext_program(self): with open("correct.py", "w") as f: @@ -62,12 +71,27 @@ def test_fulltext_program(self): io.output_writeln("1") - with captured_output() as (out, err): - Compare.program("python correct.py", "python incorrect.py", std=io, input=io, grader="FullText") + try: + with captured_output() as (out, err): + Compare.program("python correct.py", "python incorrect.py", std=io, input=io, grader="FullText") + except HashMismatch as e: + self.assertEqual(e.content, '2\n') + self.assertEqual(e.std, '1\n') + self.assertEqual(e.content_hash, '53c234e5e8472b6ac51c1ae1cab3fe06fad053beb8ebfd8977b010655bfdd3c3') + self.assertEqual(e.std_hash, '4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865') + else: + self.assertTrue(False) result = out.getvalue().strip() - stderr = err.getvalue().strip() - correct_out = 'python correct.py: Correct' - correct_err = 'python incorrect.py: !!!INCORRECT!!! Hash mismatch: read 53c234e5e8472b6ac51c1ae1cab3fe06fad053beb8ebfd8977b010655bfdd3c3, expected 4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865' + correct_out = 'python correct.py: Correct \npython incorrect.py: !!!INCORRECT!!! Hash mismatch: read 53c234e5e8472b6ac51c1ae1cab3fe06fad053beb8ebfd8977b010655bfdd3c3, expected 4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865' self.assertEqual(result, correct_out) - self.assertEqual(stderr, correct_err) + + def test_concurrent(self): + programs = ['test{}.py'.format(i) for i in range(16)] + for fn in programs: + with open(fn, 'w') as f: + f.write('print({})'.format(16)) + with open('std.py', 'w') as f: + f.write('print({})'.format(16)) + with IO() as test: + Compare.program(*[(sys.executable, program) for program in programs], std_program=(sys.executable, 'std.py'), max_workers=None, input=test) From 833094a2f54c1a88016536fa32fe9215fe8b926e Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Fri, 28 Jul 2017 20:26:20 +0800 Subject: [PATCH 22/28] add test for ``Compare.output`` --- cyaron/tests/compare_test.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cyaron/tests/compare_test.py b/cyaron/tests/compare_test.py index 12a92fb..dd3bcd0 100644 --- a/cyaron/tests/compare_test.py +++ b/cyaron/tests/compare_test.py @@ -95,3 +95,14 @@ def test_concurrent(self): f.write('print({})'.format(16)) with IO() as test: Compare.program(*[(sys.executable, program) for program in programs], std_program=(sys.executable, 'std.py'), max_workers=None, input=test) + + ios = [IO() for i in range(16)] + try: + for f in ios: + f.output_write('16') + with IO() as std: + std.output_write('16') + Compare.output(*ios, std=std, max_workers=None) + finally: + for io in ios: + io.close() From 03f23eeed19000adf704865dd4b2e87a49a27618 Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Fri, 28 Jul 2017 20:34:43 +0800 Subject: [PATCH 23/28] thread-safe log --- cyaron/log.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cyaron/log.py b/cyaron/log.py index 4d60be6..33b3b0c 100644 --- a/cyaron/log.py +++ b/cyaron/log.py @@ -1,6 +1,7 @@ from __future__ import print_function from functools import partial import sys +from threading import Lock try: import colorful except ImportError: @@ -28,7 +29,13 @@ def _join_dict(a, b): return c _log_funcs = {} -log = lambda funcname, *args, **kwargs: _log_funcs.get(funcname, lambda *args, **kwargs: None)(*args, **kwargs) +_log_lock = Lock() +def log(funcname, *args, **kwargs): + """log with log function specified by ``funcname``""" + _log_lock.acquire() + rv = _log_funcs.get(funcname, lambda *args, **kwargs: None)(*args, **kwargs) + _log_lock.release() + return rv """5 log levels 1. debug: debug info From bb454773ca0958a898a698be141f309b5adc303c Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Fri, 28 Jul 2017 20:37:02 +0800 Subject: [PATCH 24/28] fix unittest --- cyaron/tests/compare_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cyaron/tests/compare_test.py b/cyaron/tests/compare_test.py index dd3bcd0..99dade4 100644 --- a/cyaron/tests/compare_test.py +++ b/cyaron/tests/compare_test.py @@ -49,7 +49,7 @@ def test_noipstyle_incorrect(self): with captured_output() as (out, err): Compare.output("test_another_incorrect.out", std=io) except TextMismatch as e: - self.assertEqual(e.content, 'test123\r\r\ntest124 ') + self.assertEqual(e.content, 'test123\r\ntest124 ') self.assertEqual(e.std, 'test123 \ntest123\n\n') self.assertEqual(str(e), 'On line 2 column 7, read 4, expected 3.') else: From 59bf57a70288c798cc2f8e0a034093d9f42ec09c Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Fri, 28 Jul 2017 20:51:56 +0800 Subject: [PATCH 25/28] use classmethod instead of staticmethod --- cyaron/compare.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cyaron/compare.py b/cyaron/compare.py index fe4c5f3..89c53ee 100644 --- a/cyaron/compare.py +++ b/cyaron/compare.py @@ -29,40 +29,40 @@ def __process_file(file): with open(file, "r", newline='\n') as f: return file, f.read() - @staticmethod - def output(*files, std, grader=DEFAULT_GRADER, max_workers=-1, job_pool=None): + @classmethod + def output(cls, *files, std, grader=DEFAULT_GRADER, max_workers=-1, job_pool=None): if (max_workers is None or max_workers >= 0) and job_pool is None: try: from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers=max_workers) as job_pool: - return Compare.output(*files, std=std, grader=grader, max_workers=max_workers, job_pool=job_pool) + return cls.output(*files, std=std, grader=grader, max_workers=max_workers, job_pool=job_pool) except ImportError: pass def get_std(): nonlocal std - (_, std) = Compare.__process_file(std) + (_, std) = cls.__process_file(std) if job_pool is not None: job_pool.submit(get_std).result() else: get_std() def do(file): - (file_name, content) = Compare.__process_file(file) - Compare.__compare_two(file_name, content, std, grader) + (file_name, content) = cls.__process_file(file) + cls.__compare_two(file_name, content, std, grader) if job_pool is not None: job_pool.map(do, files) else: [x for x in map(do, files)] - @staticmethod - def program(*programs, input, std=None, std_program=None, grader=DEFAULT_GRADER, max_workers=-1, job_pool=None): + @classmethod + def program(cls, *programs, input, std=None, std_program=None, grader=DEFAULT_GRADER, max_workers=-1, job_pool=None): if (max_workers is None or max_workers >= 0) and job_pool is None: try: from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers=max_workers) as job_pool: - return Compare.program(*programs, input=input, std=std, std_program=std_program, grader=grader, max_workers=max_workers, job_pool=job_pool) + return cls.program(*programs, input=input, std=std, std_program=std_program, grader=grader, max_workers=max_workers, job_pool=job_pool) except ImportError: pass @@ -82,7 +82,7 @@ def get_std(): elif std is not None: def get_std(): nonlocal std - (_, std) = Compare.__process_file(std) + (_, std) = cls.__process_file(std) if job_pool is not None: job_pool.submit(get_std).result() else: @@ -93,7 +93,7 @@ def get_std(): def do(program_name): with os.fdopen(os.dup(input.input_file.fileno()), 'r', newline='\n') as input_file: content = make_unicode(subprocess.check_output(program_name, shell=(not list_like(program_name)), stdin=input_file, universal_newlines=True)) - Compare.__compare_two(program_name, content, std, grader) + cls.__compare_two(program_name, content, std, grader) if job_pool is not None: job_pool.map(do, programs) From fb7ba26a74ecf578461709037fc5f4c6eb4353a4 Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Sat, 29 Jul 2017 07:46:13 +0800 Subject: [PATCH 26/28] fix on python2 --- cyaron/compare.py | 41 +++++++++++++++++++++++++---------------- cyaron/io.py | 2 +- cyaron/utils.py | 26 +++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/cyaron/compare.py b/cyaron/compare.py index 89c53ee..4c75e61 100644 --- a/cyaron/compare.py +++ b/cyaron/compare.py @@ -30,7 +30,12 @@ def __process_file(file): return file, f.read() @classmethod - def output(cls, *files, std, grader=DEFAULT_GRADER, max_workers=-1, job_pool=None): + def output(cls, *files, **kwargs): + kwargs = unpack_kwargs('output', kwargs, ('std', ('grader', DEFAULT_GRADER), ('max_workers', -1), ('job_pool', None))) + std = kwargs['std'] + grader = kwargs['grader'] + max_workers = kwargs['max_workers'] + job_pool = kwargs['job_pool'] if (max_workers is None or max_workers >= 0) and job_pool is None: try: from concurrent.futures import ThreadPoolExecutor @@ -40,12 +45,11 @@ def output(cls, *files, std, grader=DEFAULT_GRADER, max_workers=-1, job_pool=Non pass def get_std(): - nonlocal std - (_, std) = cls.__process_file(std) + return cls.__process_file(std)[1] if job_pool is not None: - job_pool.submit(get_std).result() + std = job_pool.submit(get_std).result() else: - get_std() + std = get_std() def do(file): (file_name, content) = cls.__process_file(file) @@ -57,7 +61,14 @@ def do(file): [x for x in map(do, files)] @classmethod - def program(cls, *programs, input, std=None, std_program=None, grader=DEFAULT_GRADER, max_workers=-1, job_pool=None): + def program(cls, *programs, **kwargs): + kwargs = unpack_kwargs('program', kwargs, ('input', ('std', None), ('std_program', None), ('grader', DEFAULT_GRADER), ('max_workers', -1), ('job_pool', None))) + input = kwargs['input'] + std = kwargs['std'] + std_program = kwargs['std_program'] + grader = kwargs['grader'] + max_workers = kwargs['max_workers'] + job_pool = kwargs['job_pool'] if (max_workers is None or max_workers >= 0) and job_pool is None: try: from concurrent.futures import ThreadPoolExecutor @@ -73,25 +84,23 @@ def program(cls, *programs, input, std=None, std_program=None, grader=DEFAULT_GR if std_program is not None: def get_std(): - nonlocal std - std = make_unicode(subprocess.check_output(std_program, shell=(not list_like(std_program)), stdin=input.input_file, universal_newlines=True)) + return make_unicode(subprocess.check_output(std_program, shell=(not list_like(std_program)), stdin=input.input_file, universal_newlines=True)) if job_pool is not None: - job_pool.submit(get_std).result() + std = job_pool.submit(get_std).result() else: - get_std() + std = get_std() elif std is not None: def get_std(): - nonlocal std - (_, std) = cls.__process_file(std) + return cls.__process_file(std)[1] if job_pool is not None: - job_pool.submit(get_std).result() + std = job_pool.submit(get_std).result() else: - get_std() + std = get_std() else: - raise TypeError('program() missing 1 required non-None positional argument: \'std\' or \'std_program\'') + raise TypeError('program() missing 1 required non-None keyword-only argument: \'std\' or \'std_program\'') def do(program_name): - with os.fdopen(os.dup(input.input_file.fileno()), 'r', newline='\n') as input_file: + with open(os.dup(input.input_file.fileno()), 'r', newline='\n') as input_file: content = make_unicode(subprocess.check_output(program_name, shell=(not list_like(program_name)), stdin=input_file, universal_newlines=True)) cls.__compare_two(program_name, content, std, grader) diff --git a/cyaron/io.py b/cyaron/io.py index 8de72f7..83497fc 100644 --- a/cyaron/io.py +++ b/cyaron/io.py @@ -61,7 +61,7 @@ def __init_file(self, f, data_id, file_type): self.output_file = f elif isinstance(f, int): # consider ``f`` as a file descor - self.__init_file(os.fdopen(f, 'w+', newline='\n'), data_id, file_type) + self.__init_file(open(f, 'w+', newline='\n'), data_id, file_type) elif f is None: # consider wanna temp file fd, self.input_filename = tempfile.mkstemp() diff --git a/cyaron/utils.py b/cyaron/utils.py index eaf61df..19a6136 100644 --- a/cyaron/utils.py +++ b/cyaron/utils.py @@ -27,4 +27,28 @@ def make_unicode(data): try: return unicode(data) except NameError: - return str(data) \ No newline at end of file + return str(data) + +def unpack_kwargs(funcname, kwargs, arg_pattern): + rv = {} + kwargs = kwargs.copy() + for tp in arg_pattern: + if list_like(tp): + k, v = tp + rv[k] = kwargs.get(k, v) + try: + del kwargs[k] + except KeyError: + pass + else: + error = False + try: + rv[tp] = kwargs[tp] + del kwargs[tp] + except KeyError as e: + error = True + if error: + raise TypeError('{}() missing 1 required keyword-only argument: \'{}\''.format(funcname, tp)) + if kwargs: + raise TypeError('{}() got an unexpected keyword argument \'{}\''.format(funcname, next(iter(kwargs.items()))[0])) + return rv From 59b94707654375362bc0d15e83522dc13d8a5464 Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Sat, 29 Jul 2017 08:13:24 +0800 Subject: [PATCH 27/28] fix ``max_workers`` in python3.4- --- cyaron/compare.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cyaron/compare.py b/cyaron/compare.py index 4c75e61..b84b14f 100644 --- a/cyaron/compare.py +++ b/cyaron/compare.py @@ -4,6 +4,7 @@ from cyaron.consts import * from cyaron.graders import CYaRonGraders import subprocess +import multiprocessing import sys from io import open import os @@ -29,6 +30,14 @@ def __process_file(file): with open(file, "r", newline='\n') as f: return file, f.read() + @staticmethod + def __normal_max_workers(workers): + if workers is None: + if sys.version_info < (3, 5): + cpu = multiprocessing.cpu_count() + return cpu * 5 if cpu is not None else 1 + return workers + @classmethod def output(cls, *files, **kwargs): kwargs = unpack_kwargs('output', kwargs, ('std', ('grader', DEFAULT_GRADER), ('max_workers', -1), ('job_pool', None))) @@ -37,6 +46,7 @@ def output(cls, *files, **kwargs): max_workers = kwargs['max_workers'] job_pool = kwargs['job_pool'] if (max_workers is None or max_workers >= 0) and job_pool is None: + max_workers = cls.__normal_max_workers(max_workers) try: from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers=max_workers) as job_pool: @@ -70,6 +80,7 @@ def program(cls, *programs, **kwargs): max_workers = kwargs['max_workers'] job_pool = kwargs['job_pool'] if (max_workers is None or max_workers >= 0) and job_pool is None: + max_workers = cls.__normal_max_workers(max_workers) try: from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers=max_workers) as job_pool: From f9f7f897f47979b38a3b80478963de955f5b747c Mon Sep 17 00:00:00 2001 From: TitanSnow Date: Sun, 30 Jul 2017 17:57:00 +0800 Subject: [PATCH 28/28] add CompareMismatch & timeout --- cyaron/compare.py | 17 +++++++++++++++-- cyaron/tests/compare_test.py | 20 ++++++++++++++++++-- cyaron/utils.py | 10 ++++++++++ 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/cyaron/compare.py b/cyaron/compare.py index b84b14f..fbe0ef2 100644 --- a/cyaron/compare.py +++ b/cyaron/compare.py @@ -10,6 +10,13 @@ import os +class CompareMismatch(ValueError): + def __init__(self, name, mismatch): + super(CompareMismatch, self).__init__(name, mismatch) + self.name = name + self.mismatch = mismatch + + class Compare: @staticmethod def __compare_two(name, content, std, grader): @@ -18,7 +25,7 @@ def __compare_two(name, content, std, grader): info = info if info is not None else "" log.debug("{}: {} {}".format(name, status, info)) if not result: - raise info + raise CompareMismatch(name, info) @staticmethod def __process_file(file): @@ -111,8 +118,14 @@ def get_std(): raise TypeError('program() missing 1 required non-None keyword-only argument: \'std\' or \'std_program\'') def do(program_name): + timeout = None + if list_like(program_name) and len(program_name) == 2 and int_like(program_name[-1]): + program_name, timeout = program_name with open(os.dup(input.input_file.fileno()), 'r', newline='\n') as input_file: - content = make_unicode(subprocess.check_output(program_name, shell=(not list_like(program_name)), stdin=input_file, universal_newlines=True)) + if timeout is None: + content = make_unicode(subprocess.check_output(program_name, shell=(not list_like(program_name)), stdin=input_file, universal_newlines=True)) + else: + content = make_unicode(subprocess.check_output(program_name, shell=(not list_like(program_name)), stdin=input_file, universal_newlines=True, timeout=timeout)) cls.__compare_two(program_name, content, std, grader) if job_pool is not None: diff --git a/cyaron/tests/compare_test.py b/cyaron/tests/compare_test.py index 99dade4..1046b7c 100644 --- a/cyaron/tests/compare_test.py +++ b/cyaron/tests/compare_test.py @@ -3,9 +3,11 @@ import sys import shutil import tempfile +import subprocess from cyaron import IO, Compare, log from cyaron.output_capture import captured_output from cyaron.graders.mismatch import * +from cyaron.compare import CompareMismatch log.set_verbose() @@ -48,7 +50,9 @@ def test_noipstyle_incorrect(self): try: with captured_output() as (out, err): Compare.output("test_another_incorrect.out", std=io) - except TextMismatch as e: + except CompareMismatch as e: + self.assertEqual(e.name, 'test_another_incorrect.out') + e = e.mismatch self.assertEqual(e.content, 'test123\r\ntest124 ') self.assertEqual(e.std, 'test123 \ntest123\n\n') self.assertEqual(str(e), 'On line 2 column 7, read 4, expected 3.') @@ -74,7 +78,9 @@ def test_fulltext_program(self): try: with captured_output() as (out, err): Compare.program("python correct.py", "python incorrect.py", std=io, input=io, grader="FullText") - except HashMismatch as e: + except CompareMismatch as e: + self.assertEqual(e.name, 'python incorrect.py') + e = e.mismatch self.assertEqual(e.content, '2\n') self.assertEqual(e.std, '1\n') self.assertEqual(e.content_hash, '53c234e5e8472b6ac51c1ae1cab3fe06fad053beb8ebfd8977b010655bfdd3c3') @@ -106,3 +112,13 @@ def test_concurrent(self): finally: for io in ios: io.close() + + def test_timeout(self): + if sys.version_info >= (3, 3): + with IO() as test: + try: + Compare.program(((sys.executable, '-c', '__import__(\'time\').sleep(10)'), 1), std=test, input=test) + except subprocess.TimeoutExpired: + pass + else: + self.assertTrue(False) diff --git a/cyaron/utils.py b/cyaron/utils.py index 19a6136..fc4e49b 100644 --- a/cyaron/utils.py +++ b/cyaron/utils.py @@ -13,6 +13,16 @@ def list_like(data): return isinstance(data, tuple) or isinstance(data, list) +def int_like(data): + isint = False + try: + isint = isint or isinstance(date, long) + except NameError: + pass + isint = isint or isinstance(data, int) + return isint + + def strtolines(str): lines = str.split('\n') for i in range(len(lines)):