From 5ae74983bb6ee88f21726fd70eacd933c064a788 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Tue, 6 Feb 2024 06:07:45 +0100 Subject: [PATCH] updated decorator and util --- src/testing.py | 109 +++--------------- src/tests/primes.py | 29 ----- src/tests/test_testing.py | 103 ----------------- src/utils.py | 225 ++++++++++++++++++++++++++++++-------- 4 files changed, 195 insertions(+), 271 deletions(-) delete mode 100644 src/tests/primes.py delete mode 100644 src/tests/test_testing.py diff --git a/src/testing.py b/src/testing.py index 6a69b84..3bcb19c 100644 --- a/src/testing.py +++ b/src/testing.py @@ -1,16 +1,10 @@ -import functools import multiprocessing -from typing import Callable, Iterable, Iterator +from typing import Any, Callable, Iterator class TimeoutException(Exception): pass -def get_subtasks_for(cls: type) -> Iterator[type]: - """get all tasks in a tasks aka subtasks""" - return [e for e in cls.__dict__.values() if hasattr(e, 'is_task')] - - def get_tests_for(cls: type) -> Iterator[Callable[[], None]]: """get all functions that are declared as tests""" return [e for e in cls.__dict__.values() if hasattr(e, 'is_test')] @@ -23,7 +17,7 @@ def test_wrapper(test: Callable[[], None]): test.cause = e -def run_test(test: Callable[[], None]): +def run_test(test: Callable[[], None]) -> None: """run a test and catch any unexpected behavior and mark it as failed with the exception as cause""" if not hasattr(test, 'is_test'): return @@ -36,101 +30,28 @@ def run_test(test: Callable[[], None]): test.cause = TimeoutException(f"test failed after {timeout} seconds") -def run_tests_for_task(task: type) -> None: +def run_tests_for_task(task: object) -> None: for test in task.tests: run_test(test) - for task in task.tasks: - run_tests_for_task(task) -def points_to_deduct(e: type | Callable) -> int: - match e: - case type() if hasattr(e, 'is_task'): - to_detuct = 0 - for test in e.tests: - if hasattr(test, 'is_test'): - to_detuct += test.to_deduct() - for task in e.tasks: - to_detuct += points_to_deduct(task) - return e.max_points if to_detuct > e.max_points else to_detuct - case Callable(test): - return test.to_detuct() - case _: - return 0 - - -class Exercise(object): - def __init__(self, id: str) -> None: - self.__tasks: list[type] = [] - self.__id = id - self.__max_points = 0 - self.__points = 0 - - def register(self, cls: type) -> None: - if hasattr(cls, 'is_task'): - self.__tasks.append(cls) - self.__max_points += cls.max_points - return cls - - def run(self) -> str: - self.run_tests() - self.deduct_points() - - @property - def id(self) -> str: - return self.__id - - @property - def max_points(self) -> float: - return self.__max_points - - @property - def points(self) -> float: - return self.__points - - @property - def tasks(self) -> Iterable[object]: - return self.__tasks - - - - def run_tests(self): - for task in self.__tasks: - run_tests_for_task(task) - - def deduct_points(self): - for task in self.__tasks: - to_detuct = points_to_deduct(task) - task.points = 0 if to_detuct > task.max_points else task.max_points - to_detuct - self.__points = functools.reduce( - lambda a, b: a + b, map(lambda t: t.points, self.__tasks), 0.0) - - def get_points(self) -> float: - return self.__points - - def get_max_points(self) -> float: - return self.__max_points - - -def eip_task(header: str, max_points: float, ex: Exercise) -> Callable[[type], type]: +def eip_task(name: str, max_points: float) -> Callable[[type], type]: def wrapper(cls: type) -> type: - cls.is_task = True - cls.header = header - cls.max_points = max_points - cls.tasks = get_subtasks_for(cls) - cls.tests = get_tests_for(cls) - ex.register(cls) + cls.is_task: bool = True + cls.name: str = name + cls.max_points: float = max_points + cls.tests: Callable[..., None] = get_tests_for(cls) return cls return wrapper -def eip_test(msg: str, to_deduct: float, timeout = 10) -> Callable[[], None]: +def eip_test(msg: str, points: float, timeout = 180) -> Callable[[], None]: def wrapper(test: Callable[[], None]) -> Callable[[], None]: - test.is_test = True - test.msg = msg - test.has_failed = False - test.to_deduct = lambda: to_deduct if test.has_failed else 0 - test.timeout = timeout - test.cause = None + test.is_test: bool = True + test.msg: str = msg + test.has_failed: bool = False + test.points: float = points + test.timeout: int = timeout + test.cause: Any | None = None return test return wrapper diff --git a/src/tests/primes.py b/src/tests/primes.py deleted file mode 100644 index 2cf672d..0000000 --- a/src/tests/primes.py +++ /dev/null @@ -1,29 +0,0 @@ -def is_prime(n: int) -> bool: - if n <= 2: - return False - - for i in range(2, n // 2 + 1): - if n % i == 0: - return False - - return True - - -def next_prime(n: int) -> int: - num = n + 1 - while not is_prime(num): - num += 1 - return num - -def prime_factorize(n: int) -> list[int]: - prime_factors: list[int] = [] - num = n - prime = 2 - while num > 1: - if num % prime == 0: - prime_factors.append(prime) - num //= prime - prime = 2 - else: - prime = next_prime(prime) - return prime_factors diff --git a/src/tests/test_testing.py b/src/tests/test_testing.py deleted file mode 100644 index 5d0bc16..0000000 --- a/src/tests/test_testing.py +++ /dev/null @@ -1,103 +0,0 @@ -from utils import has_annotation_callable, format -from testing import eip_task, eip_test, Exercise - -PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, - 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97] -PRIME_FACTORS = {2: [2], 3: [3], 4: [2, 2], 5: [5], 6: [2, 3], 7: [7], 8: [2, 2, 2], 9: [3, 3], 10: [2, 5], 11: [11], 12: [2, 2, 3], 13: [13], 14: [2, 7], 15: [3, 5], 16: [2, 2, 2, 2], 17: [17], 18: [2, 3, 3], 19: [19], 20: [2, 2, 5], 21: [3, 7], 22: [2, 11], 23: [23], 24: [2, 2, 2, 3], 25: [5, 5], 26: [2, 13], 27: [3, 3, 3], 28: [2, 2, 7], 29: [29], 30: [2, 3, 5], 31: [31], 32: [2, 2, 2, 2, 2], 33: [3, 11], 34: [2, 17], 35: [5, 7], 36: [2, 2, 3, 3], 37: [37], 38: [2, 19], 39: [3, 13], 40: [2, 2, 2, 5], 41: [41], 42: [2, 3, 7], 43: [43], 44: [2, 2, 11], 45: [3, 3, 5], 46: [2, 23], 47: [47], 48: [2, 2, 2, 2, 3], 49: [7, 7], 50: [2, 5, 5], 51: [3, 17], 52: [2, 2, 13], 53: [53], 54: [ - 2, 3, 3, 3], 55: [5, 11], 56: [2, 2, 2, 7], 57: [3, 19], 58: [2, 29], 59: [59], 60: [2, 2, 3, 5], 61: [61], 62: [2, 31], 63: [3, 3, 7], 64: [2, 2, 2, 2, 2, 2], 65: [5, 13], 66: [2, 3, 11], 67: [67], 68: [2, 2, 17], 69: [3, 23], 70: [2, 5, 7], 71: [71], 72: [2, 2, 2, 3, 3], 73: [73], 74: [2, 37], 75: [3, 5, 5], 76: [2, 2, 19], 77: [7, 11], 78: [2, 3, 13], 79: [79], 80: [2, 2, 2, 2, 5], 81: [3, 3, 3, 3], 82: [2, 41], 83: [83], 84: [2, 2, 3, 7], 85: [5, 17], 86: [2, 43], 87: [3, 29], 88: [2, 2, 2, 11], 89: [89], 90: [2, 3, 3, 5], 91: [7, 13], 92: [2, 2, 23], 93: [3, 31], 94: [2, 47], 95: [5, 19], 96: [2, 2, 2, 2, 2, 3], 97: [97], 98: [2, 7, 7], 99: [3, 3, 11], 100: [2, 2, 5, 5]} -MAX = 100 - - -primes_exercise = Exercise("primes") - - -@eip_task("a) `is_prime`", 4, primes_exercise) -class IsPrime: - - @eip_test("`is_prime` nicht implementiert", 4) - def is_implemented(): - from primes import is_prime as _ - - @eip_test("Manche Primzahlen evaluieren zu `False`", 2) - def test_primes(): - from primes import is_prime - for prime in PRIMES: - assert is_prime(prime), f"`is_prime({prime}) = False`" - - @eip_test("Manche Zahlen evaluieren zu `True` obwohl keine Primzahl", 2) - def test_non_primes(): - from primes import is_prime - for not_prime in [num for num in range(4, MAX + 1) if num not in PRIMES]: - assert not is_prime(not_prime), f"`is_prime({not_prime}) = True`" - - @eip_test("Typeannotation ist nicht korrekt/unvollständig", 0.5) - def test_annotation(): - from primes import is_prime - has_annotation_callable(is_prime, [int], bool) - - -@eip_task("b) `next_prime`", 6, primes_exercise) -class NextPrime: - - @eip_test("`next_prime` nicht implementiert", 6) - def is_implemented(): - from primes import next_prime as _ - - @eip_test("Stimmt nicht für alle überprüften Werte", 4) - def test_next_prime(): - from primes import next_prime - for num in range(2, PRIMES[len(PRIMES) - 1]): - prime = next_prime(num) - i = 0 - while i < len(PRIMES) - 1 and num >= PRIMES[i + 1]: - i += 1 - p1 = PRIMES[i] - p2 = PRIMES[i + 1] - assert p1 <= num < p2 and p2 == prime, f"`next_prime({num}) = {prime} != {p2}`" - - @eip_test("`next_prime` evaluiert nicht `2` für Werte kleiner als `2`", 1) - def test_lower_values_next_prime(): - import primes - for num in range(-2, 2): - assert primes.next_prime(num) == 2, f"`next_prime({num}) != 2`" - - @eip_test("`Typannotation inkorrekt/unvollständig", 0.5) - def test_next_prime_annotation(): - import primes - has_annotation_callable(primes.next_prime, [int], int) - - -@eip_task("c) `prime_factorize`", 10, primes_exercise) -class PrimeFactorize: - - @eip_test("`prime_factorize` nicht implementiert", 10) - def prime_factorize_implemented(): - from primes import prime_factorize as _ - - @eip_test("`prime_factorize` gibt nicht immer die korrekten Faktoren", 8) - def test_prime_factorize(): - import primes - for prime in PRIMES: - assert primes.prime_factorize(prime) == [prime], f"prime_factorize({prime}) = {primes.prime_factorize(prime)} != [{prime}]" - prime_factors = [(n, primes.prime_factorize(n)) - for n in range(2, MAX + 1)] - for n, factors in prime_factors: - for factor in factors: - assert factor in PRIMES, f"{factor} in {factors} is not prime" - assert PRIME_FACTORS[n] == factors, f"prime_factorize({n}) = {factors} != {PRIME_FACTORS[n]}" - - @eip_test("`prime_factorize` gibt keine leere Liste für Werte kleiner 2", 2) - def test_lower_values_next_prime(): - import primes - for num in range(-2, 2): - assert primes.next_prime(num) == 2 - - @eip_test("`Typannotation inkorrekt/unvollständig", 0.5) - def test_type_annotation(): - import primes - has_annotation_callable(primes.prime_factorize, [int], list[int]) - - -if __name__ == "__main__": - primes_exercise.run() - print("\n".join(format(primes_exercise))) diff --git a/src/utils.py b/src/utils.py index 399a500..82d337e 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,14 +1,34 @@ -from typing import Any, Callable, TypeAliasType +import ast +from copy import deepcopy +from dataclasses import fields, is_dataclass +from enum import Enum, auto +import inspect +from typing import Any, Callable, Iterable, Iterator, Optional, TypeAliasType -from more_itertools import flatten -from testing import Exercise, points_to_deduct +class AnnotationLevel(Enum): + NONE = auto() + WRONG = auto() + CORRECT = auto() -def has_annotation_callable(f: Callable, param_types: list[type], return_type: object = None) -> bool: + def __bool__(self) -> bool: + match self: + case AnnotationLevel.NONE | AnnotationLevel.WRONG: + return False + case AnnotationLevel.CORRECT: + return True + + __nonzero__ = __bool__ + + +def has_annotation_function(f: Callable, param_types: list[type], return_type: object = None) -> AnnotationLevel: a = f.__annotations__ + if len(a) == 0: + return AnnotationLevel.NONE + ret = a['return'] if 'return' in a else None if ret != return_type: - return False + return AnnotationLevel.WRONG params = list(a.values())[:-1] if 'return' in a else list(a.values()) for param, param_type in zip(params, param_types): @@ -17,48 +37,163 @@ def has_annotation_callable(f: Callable, param_types: list[type], return_type: o while isinstance(param_type, TypeAliasType): param_type = param_type.__value__ if param != param_type: + return AnnotationLevel.WRONG + return AnnotationLevel.CORRECT + + +def has_annotation_dataclass(cls: object, types: list[object]) -> AnnotationLevel: + if not is_dataclass(cls): + AnnotationLevel.WRONG + types_given = [f.type for f in fields(cls)] + if types == types_given: + return AnnotationLevel.CORRECT + return AnnotationLevel.WRONG + + +def has_annotation_method(f: Callable, param_types: list[object], return_type: object = None) -> AnnotationLevel: + a = f.__annotations__ + if len(a) == 0: + return AnnotationLevel.NONE + + ret = a['return'] if 'return' in a else None + if ret != return_type: + return AnnotationLevel.WRONG + + self_a = a['self'] if 'self' in a else f.__qualname__.split('.')[0] + if self_a != f.__qualname__.split('.')[0]: + return AnnotationLevel.WRONG + + params = [a[p] for p in a if p != 'return' and p != 'self'] + if params != param_types: + return AnnotationLevel.WRONG + return AnnotationLevel.CORRECT + + +def has_annotation_lambda(module, f_name: str, param_types: list[object], return_type: object = None) -> AnnotationLevel: + annotation = module.__annotations__[f_name] + + if annotation.__name__ != 'Callable' and len(annotation.__args__) == 0: + return AnnotationLevel.NONE + if annotation.__name__ != 'Callable': + return AnnotationLevel.WRONG + + all_types = annotation.__args__ + types, ret_type = all_types[:-1], all_types[-1] + if ret_type != return_type: + return AnnotationLevel.WRONG + + for param, param_type in zip(types, param_types): + while isinstance(param, TypeAliasType): + param = param.__value__ + while isinstance(param_type, TypeAliasType): + param_type = param_type.__value__ + if param != param_type: + return AnnotationLevel.WRONG + return AnnotationLevel.CORRECT + + +def has_attributes(o: object, inherited_attr: list[str], new_attr: list[str]): + attr = [f.name for f in fields(o)] + if attr != inherited_attr + new_attr: + return False + + for attr in inherited_attr: + if attr in o.__annotations__: return False return True -def format_type(t) -> str: - while isinstance(t, TypeAliasType): - t = t.__value__ - match t: - case type(): - return t.__name__.split('.')[-1] - case _: - return str(t).split('.')[-1] - -def format_callable(f: Callable[..., Any]) -> str: - a = list(f.__annotations__.values() if 'return' in f.__annotations__ else f.__annotations__.values() + [None]) - a = [format_type(t) for t in a] - return f"{f.__name__}({", ".join(a[:-1])}) -> {a[-1]}" + +def function_used(f: Callable, name: str) -> bool: + source = inspect.getsource(f) + if source.startswith(4 * " "): + source = "\n".join(l[4:] for l in source.split("\n")) + return name in [c.func.id for c in ast.walk(ast.parse(source)) if isinstance(c, ast.Call) and isinstance(c.func, ast.Name)] -def assert_annotation_callable(f: Callable[..., Any], param_types: list[type], return_type: object = None): - assert has_annotation_callable(f, param_types, return_type), f"{f.__name__}({", ".join(format_type(p) for p in param_types)}) -> {format_type(return_type)} != {format_callable(f)}" - -def format(ob: Exercise | object | Callable[..., Any]) -> list[str]: - match ob: - case Exercise(): - out = [f"# {ob.id} ({ob.points:.2g} / {ob.max_points:.2g})"] - return out + list(flatten(map(format, ob.tasks))) - case object() if hasattr(ob, 'is_task'): - return format_task("##", ob) - case Callable() if hasattr(ob, 'is_test'): - return format_test(ob) - case _: - return [] - -def format_test(test: Callable[..., Any]) -> list[str]: - if not test.has_failed: - return [] - return [f"- {test.msg} [`-{test.to_deduct():.2g}p`]"] - -def format_task(prefix: str, task: object) -> list[str]: - out = [f"{prefix} {task.header} - ({(task.max_points - points_to_deduct(task)):.2g} / {task.max_points:.2g})"] - for t in task.tasks: - out += format_task(prefix + "#", t) - for t in task.tests: - out += format_test(t) - return out \ No newline at end of file +def list_comprehension_used(f: Callable) -> bool: + source = inspect.getsource(f) + tree = ast.parse(source) + + for node in ast.walk(tree): + if not isinstance(node, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)): + return False + return True + + +def function_not_used(f: Callable, name: str) -> bool: + return not function_used(f, name) + + +def list_comprehension_not_used(f: Callable) -> bool: + return not list_comprehension_used(f) + + +def has_side_effects(f: Callable, *args) -> bool: + args_copy = deepcopy(args) + f(*args) + return args == args_copy + + +def has_no_return(f: Callable, *args) -> bool: + args_copy = deepcopy(args) + return f(*args_copy) is None + +def is_enum(cls: object) -> bool: + return issubclass(cls, Enum) and not is_dataclass(cls) + + +def uses_pattern_matching(f: Callable) -> bool: + source = inspect.getsource(f) + tree = ast.parse(source) + return any(isinstance(node, ast.Match) for node in ast.walk(tree)) + + +def is_gen(g: Iterator, res: Iterable, check_end: bool = True, max: int = 100) -> bool: + if not isinstance(g, Iterator): + return False + for i, x in enumerate(res): + try: + el = next(g) + if el != x: + return False + except StopIteration: + return False + if i >= max: + break + if not check_end: + return True + try: + next(g) + except StopIteration: + return True + return False + + +def __remove_docstrings(lines: list[str]) -> list[str]: + new_lines = [] + i = 0 + while i < len(lines): + line = lines[i].strip() + if not line.startswith('"'): + new_lines.append(line) + i += 1 + continue + j = 0 + while j + 1 < len(line) and line[j + 1] == '"': + j += 1 + prefix = line[:j + 1] + i += 1 + if line.endswith(prefix): + continue + while i < len(lines) and prefix not in lines[i]: + i += 1 + return new_lines + + +def is_oneliner(f: Callable) -> bool: + if f.__name__ == '': + return + inspect.getsource(f) + lines = [line for line in inspect.getsource(f).split( + '\n') if not line.strip().startswith('#') and line.strip() != ''] + return len(__remove_docstrings(lines)) == 2