diff --git a/.gitignore b/.gitignore index bf4997a..76f1197 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ config.py +*.cpp *.in *.out *.exe diff --git a/.pylintrc b/.pylintrc index de2c0b1..61e0596 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,3 +1,3 @@ [MASTER] -py-version=3.5 +py-version=3.6 disable=R0902,R0903,R0913,R0917,R0912 \ No newline at end of file diff --git a/cyaron/compare.py b/cyaron/compare.py index 7363a9b..3b4fec0 100644 --- a/cyaron/compare.py +++ b/cyaron/compare.py @@ -12,6 +12,7 @@ class CompareMismatch(ValueError): + def __init__(self, name, mismatch): super(CompareMismatch, self).__init__(name, mismatch) self.name = name @@ -22,6 +23,7 @@ def __str__(self): class Compare: + @staticmethod def __compare_two(name, content, std, grader): (result, info) = CYaRonGraders.invoke(grader, content, std) @@ -67,7 +69,8 @@ def output(cls, *files, **kwargs): max_workers = kwargs["max_workers"] job_pool = kwargs["job_pool"] if kwargs["stop_on_incorrect"] is not None: - log.warn("parameter stop_on_incorrect is deprecated and has no effect.") + log.warn( + "parameter stop_on_incorrect is deprecated and has no effect.") if (max_workers is None or max_workers >= 0) and job_pool is None: max_workers = cls.__normal_max_workers(max_workers) @@ -75,13 +78,11 @@ def output(cls, *files, **kwargs): from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers=max_workers) as job_pool: - return cls.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 @@ -124,7 +125,8 @@ def program(cls, *programs, **kwargs): max_workers = kwargs["max_workers"] job_pool = kwargs["job_pool"] if kwargs["stop_on_incorrect"] is not None: - log.warn("parameter stop_on_incorrect is deprecated and has no effect.") + log.warn( + "parameter stop_on_incorrect is deprecated and has no effect.") if (max_workers is None or max_workers >= 0) and job_pool is None: max_workers = cls.__normal_max_workers(max_workers) @@ -132,39 +134,35 @@ def program(cls, *programs, **kwargs): from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers=max_workers) as job_pool: - return cls.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 if not isinstance(input, IO): - raise TypeError( - "expect {}, got {}".format(type(IO).__name__, type(input).__name__) - ) + raise TypeError("expect {}, got {}".format( + type(IO).__name__, + type(input).__name__)) input.flush_buffer() input.input_file.seek(0) if std_program is not None: def get_std(): - with open( - 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( std_program, shell=(not list_like(std_program)), stdin=input.input_file, universal_newlines=True, - ) - ) + )) input_file.seek(0) return content @@ -188,15 +186,11 @@ def get_std(): def do(program_name): timeout = None - if ( - list_like(program_name) - and len(program_name) == 2 - and int_like(program_name[-1]) - ): + 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: + with open(os.dup(input.input_file.fileno()), "r", + newline="\n") as input_file: if timeout is None: content = make_unicode( subprocess.check_output( @@ -204,8 +198,7 @@ def do(program_name): shell=(not list_like(program_name)), stdin=input_file, universal_newlines=True, - ) - ) + )) else: content = make_unicode( subprocess.check_output( @@ -214,8 +207,7 @@ def do(program_name): stdin=input_file, universal_newlines=True, timeout=timeout, - ) - ) + )) input_file.seek(0) cls.__compare_two(program_name, content, std, grader) diff --git a/cyaron/consts.py b/cyaron/consts.py index c838ef3..6114ede 100644 --- a/cyaron/consts.py +++ b/cyaron/consts.py @@ -1,7 +1,6 @@ from __future__ import absolute_import import math import string - """Constants Package. Constants: ALPHABET_SMALL -> All the lower ascii letters @@ -18,7 +17,7 @@ ALPHABET_CAPITAL = string.ascii_uppercase ALPHABET = ALPHABET_SMALL + ALPHABET_CAPITAL NUMBERS = string.digits -SENTENCE_SEPARATORS = ',,,,,,,;;:' # 70% ',' 20% ';' 10% ':' -SENTENCE_TERMINATORS = '....!' # 80% '.' 20% '!' +SENTENCE_SEPARATORS = ',,,,,,,;;:' # 70% ',' 20% ';' 10% ':' +SENTENCE_TERMINATORS = '....!' # 80% '.' 20% '!' DEFAULT_GRADER = "NOIPStyle" diff --git a/cyaron/graders/fulltext.py b/cyaron/graders/fulltext.py index 8460b6f..1f313da 100644 --- a/cyaron/graders/fulltext.py +++ b/cyaron/graders/fulltext.py @@ -2,9 +2,10 @@ 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, HashMismatch(content, std, 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/graderregistry.py b/cyaron/graders/graderregistry.py index 2fd1419..659f239 100644 --- a/cyaron/graders/graderregistry.py +++ b/cyaron/graders/graderregistry.py @@ -2,6 +2,7 @@ class GraderRegistry: _registry = dict() def grader(self, name): + def wrapper(func): self._registry[name] = func return func @@ -15,4 +16,4 @@ def check(self, name): return name in self._registry -CYaRonGraders = GraderRegistry() \ No newline at end of file +CYaRonGraders = GraderRegistry() diff --git a/cyaron/graders/mismatch.py b/cyaron/graders/mismatch.py index 70c2dfc..5cf9799 100644 --- a/cyaron/graders/mismatch.py +++ b/cyaron/graders/mismatch.py @@ -1,5 +1,6 @@ class Mismatch(ValueError): """exception for content mismatch""" + def __init__(self, content, std, *args): """ content -> content got @@ -9,8 +10,10 @@ def __init__(self, content, std, *args): self.content = content self.std = std + class HashMismatch(Mismatch): """exception for hash mismatch""" + def __init__(self, content, std, content_hash, std_hash): """ content -> content got @@ -18,16 +21,27 @@ def __init__(self, content, std, content_hash, std_hash): content_hash -> hash of content std_hash -> hash of std """ - super(HashMismatch, self).__init__(content, std, content_hash, std_hash) + 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_hash, self.std_hash) + 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): + + def __init__(self, + content, + std, + err_msg, + lineno=None, + colno=None, + content_token=None, + std_token=None): """ content -> content got std -> content expected @@ -37,7 +51,8 @@ def __init__(self, content, std, err_msg, lineno=None, colno=None, content_token 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) + 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 diff --git a/cyaron/graders/noipstyle.py b/cyaron/graders/noipstyle.py index bf6fa21..8ed53b7 100644 --- a/cyaron/graders/noipstyle.py +++ b/cyaron/graders/noipstyle.py @@ -21,12 +21,14 @@ def noipstyle(content, std): 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, TextMismatch( - content, std, 'Too short on line {}.', i + 1, j + 1, - content_lines[i][j:j + 5], std_lines[i][j:j + 5]) + return False, TextMismatch(content, std, + 'Too short on line {}.', 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, TextMismatch( - content, std, 'Too long on line {}.', i + 1, j + 1, - content_lines[i][j:j + 5], std_lines[i][j:j + 5]) + return False, TextMismatch(content, std, + 'Too long on line {}.', i + 1, + j + 1, content_lines[i][j:j + 5], + std_lines[i][j:j + 5]) return True, None diff --git a/cyaron/io.py b/cyaron/io.py index bbfa7f9..3bee2ee 100644 --- a/cyaron/io.py +++ b/cyaron/io.py @@ -3,12 +3,14 @@ Classes: IO: IO tool class. It will process the input and output files. """ + from __future__ import absolute_import import os import re +import signal import subprocess import tempfile -from typing import Union, overload, Optional +from typing import Union, overload, Optional, List, cast from io import IOBase from . import log from .utils import list_like, make_unicode @@ -18,34 +20,39 @@ class IO: """IO tool class. It will process the input and output files.""" @overload - def __init__(self, - input_file: Optional[Union[IOBase, str, int]] = None, - output_file: Optional[Union[IOBase, str, int]] = None, - data_id: Optional[int] = None, - disable_output: bool = False, - make_dirs: bool = False): + def __init__( + self, + input_file: Optional[Union[IOBase, str, int]] = None, + output_file: Optional[Union[IOBase, str, int]] = None, + data_id: Optional[int] = None, + disable_output: bool = False, + make_dirs: bool = False, + ): ... @overload - def __init__(self, - data_id: Optional[int] = None, - file_prefix: Optional[str] = None, - input_suffix: str = '.in', - output_suffix: str = '.out', - disable_output: bool = False, - make_dirs: bool = False): + def __init__( + self, + data_id: Optional[int] = None, + file_prefix: Optional[str] = None, + input_suffix: str = ".in", + output_suffix: str = ".out", + disable_output: bool = False, + make_dirs: bool = False, + ): ... def __init__( # type: ignore - self, - input_file: Optional[Union[IOBase, str, int]] = None, - output_file: Optional[Union[IOBase, str, int]] = None, - data_id: Optional[int] = None, - file_prefix: Optional[str] = None, - input_suffix: str = '.in', - output_suffix: str = '.out', - disable_output: bool = False, - make_dirs: bool = False): + self, + input_file: Optional[Union[IOBase, str, int]] = None, + output_file: Optional[Union[IOBase, str, int]] = None, + data_id: Optional[int] = None, + file_prefix: Optional[str] = None, + input_suffix: str = ".in", + output_suffix: str = ".out", + disable_output: bool = False, + make_dirs: bool = False, + ): """ Args: input_file (optional): input file object or filename or file descriptor. @@ -84,12 +91,13 @@ def __init__( # type: ignore # if the dir "./io" not found it will be created """ self.__closed = False - self.input_file, self.output_file = None, None + self.input_file = cast(IOBase, None) + self.output_file = None if file_prefix is not None: # legacy mode - input_file = '{}{{}}{}'.format(self.__escape_format(file_prefix), + input_file = "{}{{}}{}".format(self.__escape_format(file_prefix), self.__escape_format(input_suffix)) - output_file = '{}{{}}{}'.format( + output_file = "{}{{}}{}".format( self.__escape_format(file_prefix), self.__escape_format(output_suffix)) self.input_filename, self.output_filename = None, None @@ -101,9 +109,13 @@ def __init__( # type: ignore self.output_file = None self.is_first_char = {} - def __init_file(self, f: Union[IOBase, str, int, - None], data_id: Union[int, None], - file_type: str, make_dirs: bool): + def __init_file( + self, + f: Union[IOBase, str, int, None], + data_id: Union[int, None], + file_type: str, + make_dirs: bool, + ): if isinstance(f, IOBase): # consider ``f`` as a file object if file_type == "i": @@ -112,8 +124,12 @@ def __init_file(self, f: Union[IOBase, str, int, self.output_file = f elif isinstance(f, int): # consider ``f`` as a file descor - self.__init_file(open(f, 'w+', encoding="utf-8", newline='\n'), - data_id, file_type, make_dirs) + self.__init_file( + open(f, "w+", encoding="utf-8", newline="\n"), + data_id, + file_type, + make_dirs, + ) elif f is None: # consider wanna temp file fd, self.input_filename = tempfile.mkstemp() @@ -133,8 +149,11 @@ def __init_file(self, f: Union[IOBase, str, int, else: self.output_filename = filename self.__init_file( - open(filename, 'w+', newline='\n', encoding='utf-8'), data_id, - file_type, make_dirs) + open(filename, "w+", newline="\n", encoding="utf-8"), + data_id, + file_type, + make_dirs, + ) def __escape_format(self, st: str): """replace "{}" to "{{}}" """ @@ -211,6 +230,15 @@ def __clear(self, file: IOBase, pos: int = 0): self.is_first_char[file] = True file.seek(pos) + @staticmethod + def _kill_process_and_children(proc: subprocess.Popen): + if os.name == "posix": + os.killpg(os.getpgid(proc.pid), signal.SIGKILL) + elif os.name == "nt": + os.system(f"TASKKILL /F /T /PID {proc.pid} > nul") + else: + proc.kill() # Not currently supported + def input_write(self, *args, **kwargs): """ Write every element in *args into the input file. Splits with `separator`. @@ -243,7 +271,11 @@ def input_clear_content(self, pos: int = 0): self.__clear(self.input_file, pos) - def output_gen(self, shell_cmd, time_limit=None): + def output_gen(self, + shell_cmd: Union[str, List[str]], + time_limit: Optional[float] = None, + *, + replace_EOL: bool = True): """ Run the command `shell_cmd` (usually the std program) and send it the input file as stdin. Write its output to the output file. @@ -251,30 +283,37 @@ def output_gen(self, shell_cmd, time_limit=None): shell_cmd: the command to run, usually the std program. time_limit: the time limit (seconds) of the command to run. None means infinity. Defaults to None. + replace_EOL: Set whether to replace the end-of-line sequence with `'\\n'`. + Defaults to True. """ if self.output_file is None: raise ValueError("Output file is disabled") self.flush_buffer() origin_pos = self.input_file.tell() self.input_file.seek(0) - if time_limit is not None: - subprocess.check_call( - shell_cmd, - shell=True, - timeout=time_limit, - stdin=self.input_file.fileno(), - stdout=self.output_file.fileno(), - universal_newlines=True, - ) + + proc = subprocess.Popen( + shell_cmd, + shell=True, + stdin=self.input_file.fileno(), + stdout=subprocess.PIPE, + universal_newlines=replace_EOL, + preexec_fn=os.setsid if os.name == "posix" else None, + ) + + try: + output, _ = proc.communicate(timeout=time_limit) + except subprocess.TimeoutExpired: + # proc.kill() # didn't work because `shell=True`. + self._kill_process_and_children(proc) + raise else: - subprocess.check_call( - shell_cmd, - shell=True, - stdin=self.input_file.fileno(), - stdout=self.output_file.fileno(), - universal_newlines=True, - ) - self.input_file.seek(origin_pos) + if replace_EOL: + self.output_file.write(output) + else: + os.write(self.output_file.fileno(), output) + finally: + self.input_file.seek(origin_pos) log.debug(self.output_filename, " done") @@ -309,6 +348,8 @@ def output_clear_content(self, pos: int = 0): Args: pos: Where file will truncate """ + if self.output_file is None: + raise ValueError("Output file is disabled") self.__clear(self.output_file, pos) def flush_buffer(self): diff --git a/cyaron/log.py b/cyaron/log.py index 33b3b0c..2d0a57b 100644 --- a/cyaron/log.py +++ b/cyaron/log.py @@ -5,13 +5,18 @@ try: import colorful except ImportError: + class colorful: + def __getattr__(self, attr): return lambda st: st + colorful = colorful() from .utils import make_unicode __print = print + + def _print(*args, **kwargs): flush = False if 'flush' in kwargs: @@ -21,6 +26,7 @@ def _print(*args, **kwargs): if flush: kwargs.get('file', sys.stdout).flush() + def _join_dict(a, b): """join two dict""" c = a.copy() @@ -28,15 +34,20 @@ def _join_dict(a, b): c[k] = v return c + _log_funcs = {} _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) + rv = _log_funcs.get(funcname, lambda *args, **kwargs: None)(*args, + **kwargs) _log_lock.release() return rv + """5 log levels 1. debug: debug info 2. info: common info @@ -51,6 +62,7 @@ def log(funcname, *args, **kwargs): warn = partial(log, 'warn') error = partial(log, 'error') + def register_logfunc(funcname, func): """register logfunc str funcname -> name of logfunc @@ -64,10 +76,20 @@ def register_logfunc(funcname, func): 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})) -_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) + +_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 +) 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) @@ -75,6 +97,7 @@ def register_logfunc(funcname, func): _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) @@ -83,6 +106,7 @@ def set_quiet(): register_logfunc('warn', None) register_logfunc('error', _default_error) + def set_normal(): """set log mode to "normal" """ register_logfunc('debug', None) @@ -91,6 +115,7 @@ def set_normal(): register_logfunc('warn', _default_warn) register_logfunc('error', _default_error) + def set_verbose(): """set log mode to "verbose" """ register_logfunc('debug', _default_debug) @@ -99,4 +124,5 @@ def set_verbose(): register_logfunc('warn', _default_warn) register_logfunc('error', _default_error) + set_normal() diff --git a/cyaron/merger.py b/cyaron/merger.py index e7069b6..eefd1df 100644 --- a/cyaron/merger.py +++ b/cyaron/merger.py @@ -1,6 +1,8 @@ from .graph import * + class Merger: + def __init__(self, *graphs, **kwargs): """__init__(self, *graphs, **kwargs) -> None put several graphs into one @@ -9,14 +11,15 @@ def __init__(self, *graphs, **kwargs): None """ self.graphs = graphs - self.G = Graph(sum([len(i.edges) - 1 for i in graphs]), graphs[0].directed) - - counter = 0 + self.G = Graph(sum([len(i.edges) - 1 for i in graphs]), + graphs[0].directed) + + counter = 0 for graph in self.graphs: graph.offset = counter for edge in graph.iterate_edges(): - self.G.add_edge(edge.start + counter, - edge.end + counter, + self.G.add_edge(edge.start + counter, + edge.end + counter, weight=edge.weight) counter += len(graph.edges) - 1 @@ -27,10 +30,10 @@ def __add_edge(self, u, v, **kwargs): **kwargs: int weight -> edge weight """ - self.G.add_edge(self.graphs[ u[0] ].offset + u[1], - self.graphs[ v[0] ].offset + v[1], - weight=kwargs.get("weight", 1)) - + self.G.add_edge(self.graphs[u[0]].offset + u[1], + self.graphs[v[0]].offset + v[1], + weight=kwargs.get("weight", 1)) + def add_edge(self, u, v, **kwargs): """add_edge(self, u, v, **kwargs) -> None """ @@ -38,7 +41,7 @@ def add_edge(self, u, v, **kwargs): def to_str(self, **kwargs): return self.G.to_str(**kwargs) - + def __str__(self): return self.to_str() diff --git a/cyaron/output_capture.py b/cyaron/output_capture.py index 4bcac58..878cbcf 100644 --- a/cyaron/output_capture.py +++ b/cyaron/output_capture.py @@ -5,6 +5,7 @@ except ImportError: from io import StringIO + @contextmanager def captured_output(): new_out, new_err = StringIO(), StringIO() @@ -13,4 +14,4 @@ def captured_output(): sys.stdout, sys.stderr = new_out, new_err yield sys.stdout, sys.stderr finally: - sys.stdout, sys.stderr = old_out, old_err \ No newline at end of file + sys.stdout, sys.stderr = old_out, old_err diff --git a/cyaron/polygon.py b/cyaron/polygon.py index d0dfca8..3a007fd 100644 --- a/cyaron/polygon.py +++ b/cyaron/polygon.py @@ -6,7 +6,8 @@ class Polygon: - def __init__(self,points=[]): + + def __init__(self, points=[]): if not list_like(points): raise Exception("polygon must be constructed by a list of points") self.points = points @@ -110,21 +111,27 @@ def __conquer(points): if len(points) <= 2: return points if len(points) == 3: - (points[1],points[2])=(points[2],points[1]) + (points[1], points[2]) = (points[2], points[1]) return points divide_id = random.randint(2, len(points) - 1) divide_point1 = points[divide_id] divide_k = random.uniform(0.01, 0.99) - divide_point2 = [divide_k * (points[1][0] - points[0][0]) + points[0][0], - divide_k * (points[1][1] - points[0][1]) + points[0][1]] + divide_point2 = [ + divide_k * (points[1][0] - points[0][0]) + points[0][0], + divide_k * (points[1][1] - points[0][1]) + points[0][1] + ] # path: points[0]->points[divide]->points[1] # dividing line in the form Ax+By+C=0 - divide_line = [divide_point2[1] - divide_point1[1], - divide_point1[0] - divide_point2[0], - -divide_point1[0] * divide_point2[1] - + divide_point1[1] * divide_point2[0]] - p0 = (divide_line[0] * points[0][0] + divide_line[1] * points[0][1] + divide_line[2] >= 0) - p1 = (divide_line[0] * points[1][0] + divide_line[1] * points[1][1] + divide_line[2] >= 0) + divide_line = [ + divide_point2[1] - divide_point1[1], + divide_point1[0] - divide_point2[0], + -divide_point1[0] * divide_point2[1] + + divide_point1[1] * divide_point2[0] + ] + p0 = (divide_line[0] * points[0][0] + divide_line[1] * points[0][1] + + divide_line[2] >= 0) + p1 = (divide_line[0] * points[1][0] + divide_line[1] * points[1][1] + + divide_line[2] >= 0) if p0 == p1: # the divide point isn't good enough... return Polygon.__conquer(points) s = [[], []] @@ -135,7 +142,8 @@ def __conquer(points): for i in range(2, len(points)): if i == divide_id: continue - pt = (divide_line[0] * points[i][0] + divide_line[1] * points[i][1] + divide_line[2] >= 0) + pt = (divide_line[0] * points[i][0] + + divide_line[1] * points[i][1] + divide_line[2] >= 0) s[pt].append(points[i]) pa = Polygon.__conquer(s[p0]) pb = Polygon.__conquer(s[not p0]) @@ -152,17 +160,18 @@ def simple_polygon(points): if len(points) <= 3: return Polygon(points) # divide by points[0], points[1] - divide_line = [points[1][1] - points[0][1], - points[0][0] - points[1][0], - -points[0][0] * points[1][1] - + points[0][1] * points[1][0]] + divide_line = [ + points[1][1] - points[0][1], points[0][0] - points[1][0], + -points[0][0] * points[1][1] + points[0][1] * points[1][0] + ] s = [[], []] s[0].append(points[0]) s[0].append(points[1]) s[1].append(points[1]) s[1].append(points[0]) for i in range(2, len(points)): - pt = (divide_line[0] * points[i][0] + divide_line[1] * points[i][1] + divide_line[2] >= 0) + pt = (divide_line[0] * points[i][0] + + divide_line[1] * points[i][1] + divide_line[2] >= 0) s[pt].append(points[i]) pa = Polygon.__conquer(s[0]) pb = Polygon.__conquer(s[1]) diff --git a/cyaron/string.py b/cyaron/string.py index 6af1e5c..d6484f4 100644 --- a/cyaron/string.py +++ b/cyaron/string.py @@ -6,6 +6,7 @@ class String: + @staticmethod def random(length_range, **kwargs): length = length_range @@ -22,7 +23,8 @@ def random(length_range, **kwargs): def random_sentence(word_count_range, **kwargs): word_count = word_count_range if list_like(word_count_range): - word_count = random.randint(word_count_range[0], word_count_range[1]) + word_count = random.randint(word_count_range[0], + word_count_range[1]) word_length_range = kwargs.get("word_length_range", (3, 8)) first_letter_uppercase = kwargs.get("first_letter_uppercase", True) @@ -32,7 +34,8 @@ def random_sentence(word_count_range, **kwargs): if word_separators is None or len(word_separators) == 0: word_separators = [""] - sentence_terminators = kwargs.get("sentence_terminators", SENTENCE_TERMINATORS) + sentence_terminators = kwargs.get("sentence_terminators", + SENTENCE_TERMINATORS) if sentence_terminators is None or len(sentence_terminators) == 0: sentence_terminators = [""] @@ -44,7 +47,8 @@ def random_sentence(word_count_range, **kwargs): # We cannot just `sentence_separators.join()` here # since we want to randomly select one on each join - sentence = reduce(lambda x, y: x + random.choice(word_separators) + y, words) + sentence = reduce(lambda x, y: x + random.choice(word_separators) + y, + words) sentence += random.choice(sentence_terminators) return sentence @@ -53,7 +57,8 @@ def random_sentence(word_count_range, **kwargs): def random_paragraph(sentence_count_range, **kwargs): sentence_count = sentence_count_range if list_like(sentence_count_range): - sentence_count = random.randint(sentence_count_range[0], sentence_count_range[1]) + sentence_count = random.randint(sentence_count_range[0], + sentence_count_range[1]) word_count_range = kwargs.get("word_count_range", (6, 10)) @@ -68,11 +73,13 @@ def random_paragraph(sentence_count_range, **kwargs): if sentence_joiners is None or len(sentence_joiners) == 0: sentence_joiners = [""] - sentence_separators = kwargs.get("sentence_separators", SENTENCE_SEPARATORS) + sentence_separators = kwargs.get("sentence_separators", + SENTENCE_SEPARATORS) if sentence_separators is None or len(sentence_separators) == 0: sentence_separators = [""] - sentence_terminators = kwargs.get("sentence_terminators", SENTENCE_TERMINATORS) + sentence_terminators = kwargs.get("sentence_terminators", + SENTENCE_TERMINATORS) if sentence_terminators is None or len(sentence_terminators) == 0: sentence_terminators = [""] kwargs["sentence_terminators"] = None @@ -95,7 +102,8 @@ def random_paragraph(sentence_count_range, **kwargs): sentences.append(string) - paragraph = reduce(lambda x, y: x + random.choice(sentence_joiners) + y, sentences) + paragraph = reduce( + lambda x, y: x + random.choice(sentence_joiners) + y, sentences) return paragraph @staticmethod diff --git a/cyaron/tests/compare_test.py b/cyaron/tests/compare_test.py index 883845b..7b6c487 100644 --- a/cyaron/tests/compare_test.py +++ b/cyaron/tests/compare_test.py @@ -11,6 +11,7 @@ log.set_verbose() + class TestCompare(unittest.TestCase): def setUp(self): diff --git a/cyaron/tests/io_test.py b/cyaron/tests/io_test.py index f722e87..02b5a98 100644 --- a/cyaron/tests/io_test.py +++ b/cyaron/tests/io_test.py @@ -1,5 +1,7 @@ import unittest +import sys import os +import time import shutil import tempfile import subprocess @@ -70,38 +72,47 @@ def test_output_gen(self): with IO("test_gen.in", "test_gen.out") as test: test.output_gen("echo 233") - with open("test_gen.out") as f: + with open("test_gen.out", "rb") as f: output = f.read() - self.assertEqual(output.strip("\n"), "233") + self.assertEqual(output, b"233\n") def test_output_gen_time_limit_exceeded(self): - time_limit_exceeded = False - with captured_output() as (out, err): - with open("long_time.py", "w") as f: - f.write("import time\ntime.sleep(10)\nprint(1)") + with captured_output(): + TIMEOUT = 0.02 + WAIT_TIME = 0.4 # If the wait time is too short, an error may occur + with open("long_time.py", "w", encoding="utf-8") as f: + f.write("import time, os\n" + "fn = input()\n" + f"time.sleep({WAIT_TIME})\n" + "os.remove(fn)\n") - try: - with IO("test_gen.in", "test_gen.out") as test: - test.output_gen("python long_time.py", time_limit=1) - except subprocess.TimeoutExpired: - time_limit_exceeded = True - self.assertEqual(time_limit_exceeded, True) + with IO("test_gen.in", "test_gen.out") as test: + fd, input_filename = tempfile.mkstemp() + os.close(fd) + abs_input_filename: str = os.path.abspath(input_filename) + with self.assertRaises(subprocess.TimeoutExpired): + test.input_writeln(abs_input_filename) + test.output_gen(f'"{sys.executable}" long_time.py', + time_limit=TIMEOUT) + time.sleep(WAIT_TIME) + try: + os.remove(input_filename) + except FileNotFoundError: + self.fail("Child processes have not been terminated.") def test_output_gen_time_limit_not_exceeded(self): - time_limit_exceeded = False - with captured_output() as (out, err): - with open("short_time.py", "w") as f: - f.write("import time\ntime.sleep(0.2)\nprint(1)") - - try: - with IO("test_gen.in", "test_gen.out") as test: - test.output_gen("python short_time.py", time_limit=1) - except subprocess.TimeoutExpired: - time_limit_exceeded = True - with open("test_gen.out") as f: + with captured_output(): + with open("short_time.py", "w", encoding="utf-8") as f: + f.write("import time\n" + "time.sleep(0.1)\n" + "print(1)") + + with IO("test_gen.in", "test_gen.out") as test: + test.output_gen(f'"{sys.executable}" short_time.py', + time_limit=0.5) + with open("test_gen.out", encoding="utf-8") as f: output = f.read() - self.assertEqual(output.strip("\n"), "1") - self.assertEqual(time_limit_exceeded, False) + self.assertEqual(output, "1\n") def test_init_overload(self): with IO(file_prefix="data{", data_id=5) as test: @@ -126,10 +137,7 @@ def test_make_dirs(self): mkdir_false = False try: - with IO( - "./automkdir_false/data.in", - "./automkdir_false/data.out", - ): + with IO("./automkdir_false/data.in", "./automkdir_false/data.out"): pass except FileNotFoundError: mkdir_false = True diff --git a/cyaron/tests/polygon_test.py b/cyaron/tests/polygon_test.py index 7548f3d..e30633e 100644 --- a/cyaron/tests/polygon_test.py +++ b/cyaron/tests/polygon_test.py @@ -3,8 +3,11 @@ class TestPolygon(unittest.TestCase): + def test_convex_hull(self): - hull = Polygon.convex_hull(300, fx=lambda x: int(x * 100000), fy=lambda x: int(x * 100000)) + hull = Polygon.convex_hull(300, + fx=lambda x: int(x * 100000), + fy=lambda x: int(x * 100000)) points = hull.points points = sorted(points) # unique diff --git a/cyaron/tests/sequence_test.py b/cyaron/tests/sequence_test.py index 272a1d5..035fcb3 100644 --- a/cyaron/tests/sequence_test.py +++ b/cyaron/tests/sequence_test.py @@ -20,4 +20,3 @@ def test_func_get_one(self): def test_func_get_many(self): seq = Sequence(lambda i, f: 3 * i + 2 * f(i - 1), [0]) self.assertEqual(seq.get(3, 5), [33, 78, 171]) - diff --git a/cyaron/tests/vector_test.py b/cyaron/tests/vector_test.py index 1380de4..20519df 100644 --- a/cyaron/tests/vector_test.py +++ b/cyaron/tests/vector_test.py @@ -7,6 +7,7 @@ def has_duplicates(lst: list): class TestVector(unittest.TestCase): + def test_unique_vector(self): v = Vector.random(10**5, [10**6]) self.assertFalse(has_duplicates(list(map(lambda tp: tuple(tp), v)))) @@ -14,8 +15,9 @@ def test_unique_vector(self): v = Vector.random(1000, [(10**5, 10**6)]) self.assertTrue(all(map(lambda v: 10**5 <= v[0] <= 10**6, v))) with self.assertRaises( - Exception, - msg="1st param is so large that CYaRon can not generate unique vectors", + Exception, + msg= + "1st param is so large that CYaRon can not generate unique vectors", ): v = Vector.random(10**5, [10**4]) diff --git a/cyaron/visual.py b/cyaron/visual.py index 7b5f122..b301899 100644 --- a/cyaron/visual.py +++ b/cyaron/visual.py @@ -1,7 +1,8 @@ -from .graph import * +from .graph import * from .merger import Merger import pygraphviz as pgv + def visualize(graph, output_path="a.png"): """visualize(graph, **kwargs) -> None Graph/Merger graph -> the graph/Merger that will be visualized @@ -14,7 +15,7 @@ def visualize(graph, output_path="a.png"): G.add_nodes_from([i for i in xrange(1, len(graph.edges))]) for edge in graph.iterate_edges(): G.add_edge(edge.start, edge.end, label=edge.weight) - + G.node_attr['shape'] = 'egg' G.node_attr['width'] = '0.25' G.node_attr['height'] = '0.25' @@ -22,5 +23,3 @@ def visualize(graph, output_path="a.png"): G.layout(prog='dot') G.draw(output_path) - - diff --git a/poetry.lock b/poetry.lock index b1294ef..e9bf565 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "colorama" diff --git a/yapf_style.cfg b/yapf_style.cfg new file mode 100644 index 0000000..81ac2dc --- /dev/null +++ b/yapf_style.cfg @@ -0,0 +1,413 @@ +[style] +# Align closing bracket with visual indentation. +align_closing_bracket_with_visual_indent=True + +# Allow dictionary keys to exist on multiple lines. For example: +# +# x = { +# ('this is the first element of a tuple', +# 'this is the second element of a tuple'): +# value, +# } +allow_multiline_dictionary_keys=False + +# Allow lambdas to be formatted on more than one line. +allow_multiline_lambdas=False + +# Allow splitting before a default / named assignment in an argument list. +allow_split_before_default_or_named_assigns=True + +# Allow splits before the dictionary value. +allow_split_before_dict_value=True + +# Let spacing indicate operator precedence. For example: +# +# a = 1 * 2 + 3 / 4 +# b = 1 / 2 - 3 * 4 +# c = (1 + 2) * (3 - 4) +# d = (1 - 2) / (3 + 4) +# e = 1 * 2 - 3 +# f = 1 + 2 + 3 + 4 +# +# will be formatted as follows to indicate precedence: +# +# a = 1*2 + 3/4 +# b = 1/2 - 3*4 +# c = (1+2) * (3-4) +# d = (1-2) / (3+4) +# e = 1*2 - 3 +# f = 1 + 2 + 3 + 4 +# +arithmetic_precedence_indication=False + +# Number of blank lines surrounding top-level function and class +# definitions. +blank_lines_around_top_level_definition=2 + +# Number of blank lines between top-level imports and variable +# definitions. +blank_lines_between_top_level_imports_and_variables=1 + +# Insert a blank line before a class-level docstring. +blank_line_before_class_docstring=False + +# Insert a blank line before a module docstring. +blank_line_before_module_docstring=False + +# Insert a blank line before a 'def' or 'class' immediately nested +# within another 'def' or 'class'. For example: +# +# class Foo: +# # <------ this blank line +# def method(): +# pass +blank_line_before_nested_class_or_def=True + +# Do not split consecutive brackets. Only relevant when +# dedent_closing_brackets is set. For example: +# +# call_func_that_takes_a_dict( +# { +# 'key1': 'value1', +# 'key2': 'value2', +# } +# ) +# +# would reformat to: +# +# call_func_that_takes_a_dict({ +# 'key1': 'value1', +# 'key2': 'value2', +# }) +coalesce_brackets=False + +# The column limit. +column_limit=79 + +# The style for continuation alignment. Possible values are: +# +# - SPACE: Use spaces for continuation alignment. This is default behavior. +# - FIXED: Use fixed number (CONTINUATION_INDENT_WIDTH) of columns +# (ie: CONTINUATION_INDENT_WIDTH/INDENT_WIDTH tabs or +# CONTINUATION_INDENT_WIDTH spaces) for continuation alignment. +# - VALIGN-RIGHT: Vertically align continuation lines to multiple of +# INDENT_WIDTH columns. Slightly right (one tab or a few spaces) if +# cannot vertically align continuation lines with indent characters. +continuation_align_style=SPACE + +# Indent width used for line continuations. +continuation_indent_width=4 + +# Put closing brackets on a separate line, dedented, if the bracketed +# expression can't fit in a single line. Applies to all kinds of brackets, +# including function definitions and calls. For example: +# +# config = { +# 'key1': 'value1', +# 'key2': 'value2', +# } # <--- this bracket is dedented and on a separate line +# +# time_series = self.remote_client.query_entity_counters( +# entity='dev3246.region1', +# key='dns.query_latency_tcp', +# transform=Transformation.AVERAGE(window=timedelta(seconds=60)), +# start_ts=now()-timedelta(days=3), +# end_ts=now(), +# ) # <--- this bracket is dedented and on a separate line +dedent_closing_brackets=False + +# Disable the heuristic which places each list element on a separate line +# if the list is comma-terminated. +# +# Note: The behavior of this flag changed in v0.40.3. Before, if this flag +# was true, we would split lists that contained a trailing comma or a +# comment. Now, we have a separate flag, `DISABLE_SPLIT_LIT_WITH_COMMENT`, +# that controls splitting when a list contains a comment. To get the old +# behavior, set both flags to true. More information in CHANGELOG.md. +disable_ending_comma_heuristic=False + +# +# Don't put every element on a new line within a list that contains +# interstitial comments. +disable_split_list_with_comment=False + +# Place each dictionary entry onto its own line. +each_dict_entry_on_separate_line=True + +# Require multiline dictionary even if it would normally fit on one line. +# For example: +# +# config = { +# 'key1': 'value1' +# } +force_multiline_dict=False + +# The regex for an i18n comment. The presence of this comment stops +# reformatting of that line, because the comments are required to be +# next to the string they translate. +i18n_comment= + +# The i18n function call names. The presence of this function stops +# reformattting on that line, because the string it has cannot be moved +# away from the i18n comment. +i18n_function_call= + +# Indent blank lines. +indent_blank_lines=False + +# Put closing brackets on a separate line, indented, if the bracketed +# expression can't fit in a single line. Applies to all kinds of brackets, +# including function definitions and calls. For example: +# +# config = { +# 'key1': 'value1', +# 'key2': 'value2', +# } # <--- this bracket is indented and on a separate line +# +# time_series = self.remote_client.query_entity_counters( +# entity='dev3246.region1', +# key='dns.query_latency_tcp', +# transform=Transformation.AVERAGE(window=timedelta(seconds=60)), +# start_ts=now()-timedelta(days=3), +# end_ts=now(), +# ) # <--- this bracket is indented and on a separate line +indent_closing_brackets=False + +# Indent the dictionary value if it cannot fit on the same line as the +# dictionary key. For example: +# +# config = { +# 'key1': +# 'value1', +# 'key2': value1 + +# value2, +# } +indent_dictionary_value=False + +# The number of columns to use for indentation. +indent_width=4 + +# Join short lines into one line. E.g., single line 'if' statements. +join_multiple_lines=True + +# Do not include spaces around selected binary operators. For example: +# +# 1 + 2 * 3 - 4 / 5 +# +# will be formatted as follows when configured with "*,/": +# +# 1 + 2*3 - 4/5 +no_spaces_around_selected_binary_operators= + +# Use spaces around default or named assigns. +spaces_around_default_or_named_assign=False + +# Adds a space after the opening '{' and before the ending '}' dict +# delimiters. +# +# {1: 2} +# +# will be formatted as: +# +# { 1: 2 } +spaces_around_dict_delimiters=False + +# Adds a space after the opening '[' and before the ending ']' list +# delimiters. +# +# [1, 2] +# +# will be formatted as: +# +# [ 1, 2 ] +spaces_around_list_delimiters=False + +# Use spaces around the power operator. +spaces_around_power_operator=False + +# Use spaces around the subscript / slice operator. For example: +# +# my_list[1 : 10 : 2] +spaces_around_subscript_colon=False + +# Adds a space after the opening '(' and before the ending ')' tuple +# delimiters. +# +# (1, 2, 3) +# +# will be formatted as: +# +# ( 1, 2, 3 ) +spaces_around_tuple_delimiters=False + +# The number of spaces required before a trailing comment. +# This can be a single value (representing the number of spaces +# before each trailing comment) or list of values (representing +# alignment column values; trailing comments within a block will +# be aligned to the first column value that is greater than the maximum +# line length within the block). For example: +# +# With spaces_before_comment=5: +# +# 1 + 1 # Adding values +# +# will be formatted as: +# +# 1 + 1 # Adding values <-- 5 spaces between the end of the +# # statement and comment +# +# With spaces_before_comment=15, 20: +# +# 1 + 1 # Adding values +# two + two # More adding +# +# longer_statement # This is a longer statement +# short # This is a shorter statement +# +# a_very_long_statement_that_extends_beyond_the_final_column # Comment +# short # This is a shorter statement +# +# will be formatted as: +# +# 1 + 1 # Adding values <-- end of line comments in block +# # aligned to col 15 +# two + two # More adding +# +# longer_statement # This is a longer statement <-- end of line +# # comments in block aligned to col 20 +# short # This is a shorter statement +# +# a_very_long_statement_that_extends_beyond_the_final_column # Comment <-- the end of line comments are aligned based on the line length +# short # This is a shorter statement +# +spaces_before_comment=2 + +# Insert a space between the ending comma and closing bracket of a list, +# etc. +space_between_ending_comma_and_closing_bracket=True + +# Use spaces inside brackets, braces, and parentheses. For example: +# +# method_call( 1 ) +# my_dict[ 3 ][ 1 ][ get_index( *args, **kwargs ) ] +# my_set = { 1, 2, 3 } +space_inside_brackets=False + +# Split before arguments. +split_all_comma_separated_values=False + +# Split before arguments, but do not split all subexpressions recursively +# (unless needed). +split_all_top_level_comma_separated_values=False + +# Split before arguments if the argument list is terminated by a +# comma. +split_arguments_when_comma_terminated=False + +# Set to True to prefer splitting before '+', '-', '*', '/', '//', or '@' +# rather than after. +split_before_arithmetic_operator=False + +# Set to True to prefer splitting before '&', '|' or '^' rather than +# after. +split_before_bitwise_operator=True + +# Split before the closing bracket if a list or dict literal doesn't fit on +# a single line. +split_before_closing_bracket=True + +# Split before a dictionary or set generator (comp_for). For example, note +# the split before the 'for': +# +# foo = { +# variable: 'Hello world, have a nice day!' +# for variable in bar if variable != 42 +# } +split_before_dict_set_generator=True + +# Split before the '.' if we need to split a longer expression: +# +# foo = ('This is a really long string: {}, {}, {}, {}'.format(a, b, c, d)) +# +# would reformat to something like: +# +# foo = ('This is a really long string: {}, {}, {}, {}' +# .format(a, b, c, d)) +split_before_dot=False + +# Split after the opening paren which surrounds an expression if it doesn't +# fit on a single line. +split_before_expression_after_opening_paren=False + +# If an argument / parameter list is going to be split, then split before +# the first argument. +split_before_first_argument=False + +# Set to True to prefer splitting before 'and' or 'or' rather than +# after. +split_before_logical_operator=True + +# Split named assignments onto individual lines. +split_before_named_assigns=True + +# Set to True to split list comprehensions and generators that have +# non-trivial expressions and multiple clauses before each of these +# clauses. For example: +# +# result = [ +# a_long_var + 100 for a_long_var in xrange(1000) +# if a_long_var % 10] +# +# would reformat to something like: +# +# result = [ +# a_long_var + 100 +# for a_long_var in xrange(1000) +# if a_long_var % 10] +split_complex_comprehension=False + +# The penalty for splitting right after the opening bracket. +split_penalty_after_opening_bracket=300 + +# The penalty for splitting the line after a unary operator. +split_penalty_after_unary_operator=10000 + +# The penalty of splitting the line around the '+', '-', '*', '/', '//', +# `%`, and '@' operators. +split_penalty_arithmetic_operator=300 + +# The penalty for splitting right before an if expression. +split_penalty_before_if_expr=0 + +# The penalty of splitting the line around the '&', '|', and '^' operators. +split_penalty_bitwise_operator=300 + +# The penalty for splitting a list comprehension or generator +# expression. +split_penalty_comprehension=80 + +# The penalty for characters over the column limit. +split_penalty_excess_character=7000 + +# The penalty incurred by adding a line split to the logical line. The +# more line splits added the higher the penalty. +split_penalty_for_added_line_split=30 + +# The penalty of splitting a list of "import as" names. For example: +# +# from a_very_long_or_indented_module_name_yada_yad import (long_argument_1, +# long_argument_2, +# long_argument_3) +# +# would reformat to something like: +# +# from a_very_long_or_indented_module_name_yada_yad import ( +# long_argument_1, long_argument_2, long_argument_3) +split_penalty_import_names=0 + +# The penalty of splitting the line around the 'and' and 'or' operators. +split_penalty_logical_operator=300 + +# Use the Tab character for indentation. +use_tabs=False +