updated decorator and util

This commit is contained in:
2024-02-06 06:07:45 +01:00
parent c54518ed65
commit 5ae74983bb
4 changed files with 195 additions and 271 deletions

View File

@ -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

View File

@ -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

View File

@ -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)))

View File

@ -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
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__ == '<lambda>':
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