updated decorator and util
This commit is contained in:
109
src/testing.py
109
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
|
||||
|
@ -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
|
@ -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)))
|
225
src/utils.py
225
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
|
||||
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
|
||||
|
Reference in New Issue
Block a user