From 25753d01810c4b69f92302a0663ae89e819d9cca Mon Sep 17 00:00:00 2001
From: Nils Pukropp <nils@narl.io>
Date: Fri, 2 Feb 2024 09:03:35 +0100
Subject: [PATCH] tut14

---
 Tutorium/tut14/README.md        | 153 ++++++++++++++++++++++++++++++++
 Tutorium/tut14/src/commands.py  |  36 ++++++++
 Tutorium/tut14/src/decorator.py |  67 ++++++++++++++
 3 files changed, 256 insertions(+)
 create mode 100644 Tutorium/tut14/src/commands.py
 create mode 100644 Tutorium/tut14/src/decorator.py

diff --git a/Tutorium/tut14/README.md b/Tutorium/tut14/README.md
index e69de29..145b112 100644
--- a/Tutorium/tut14/README.md
+++ b/Tutorium/tut14/README.md
@@ -0,0 +1,153 @@
+---
+marp: true
+paginate: true
+# class: invert
+theme: rose-pine
+footer: Tutorium 14 - 26.01.2024 - Nils Pukropp - https://s.narl.io/s/tutorium-14
+header:
+math: mathjax
+---
+
+# Tutorium 14 - 02.02.2024
+
+Decorator, Testing
+
+---
+
+# Decorator
+
+- **Design-Pattern**, oft auch **Wrapper** genannt
+- Verpackt ein Objekt um **zusätzliche Funktionalität** zu bieten
+  - Funktionen sind auch Objekte
+  - eine Klasse ist ein Objekt
+- Oft einfach **syntax sugar**
+
+---
+## Beispiel - execute_two_times
+
+```python
+def execute_two_times(fn: Callable[..., Any]) -> Callable[..., Any]:
+    def wrapper(*args, **kwargs)
+        fn(*args, **kwargs)
+        fn(*args, **kwargs)
+        return wrapper
+    return wrapper
+
+@execute_two_times()
+def print_two_times(msg: str):
+    print(msg)
+
+print_two_times("hello")    # hello
+                            # hello
+```
+
+---
+
+## Beispiel - execute_by
+
+```python
+def execute_by(n: int):
+    def wrapper(fn):
+        def wrapped_fn(*args, **kwargs):
+            for _ in range(0, n):
+                fn(*args, **kwargs)
+            return wrapped_fn
+        return wrapped_fn
+    return wrapper
+
+@execute_by(10)
+def print_ten_times(msg: str):
+    print(msg)
+
+print_ten_times("hello")    # hello
+                            # hello
+                            # ... (10 mal)
+```
+
+---
+
+## Beispiel - CommandExecutor
+
+```python
+class CommandExecutor[R]:
+
+    def __init__(self):
+        self.__commands: dict[str, Callable[..., R]] = {}
+```
+
+---
+
+## Beispiel - run
+
+```python
+    def run(self, name: str, *args, **kwargs) -> list[R]:
+        results : list[R] = []
+        for command_name, command in self.__commands.items():
+            if command_name == name:
+                results += [command(*args, **kwargs)]
+        return results
+```
+
+---
+
+## Beispiel - register
+
+```python
+    def register(self, cmd: Callable[..., R]) -> Callable[..., R]:
+        self.__commands[cmd.__name__] = cmd
+        return cmd
+```
+
+---
+
+## Beispiel - CommandExecutor
+
+```python
+class CommandExecutor[R]:
+    def __init__(self):
+        self.__commands: dict[str, Callable[..., R]] = {}
+    
+    def run(self, name: str, *args, **kwargs) -> list[R]:
+        results : list[R] = []
+        for command_name, command in self.__commands.items():
+            if command_name == name:
+                results += [command(*args, **kwargs)]
+        return results
+    
+    def register(self, cmd: Callable[..., R]) -> Callable[..., R]:
+        self.__commands[cmd.__name__] = cmd
+        return cmd
+```
+
+---
+
+## Beispiel - How to use
+
+```python
+app = CommandExecutor[str]()
+
+@app.register
+def hello_world() -> str:
+    return 'hello_world'
+
+@app.register 
+def divide(a: int, b: int) -> str:
+    if b == 0:
+        return "tried to divide by zero"
+    return str(a / b)
+
+print(app.run('hello_world'))
+print(app.run('divide', 5, 0))
+print(app.run('divide', 10, 2))
+```
+---
+
+## Decorator in der Klausur
+
+- Waren noch nie Bestandteil der Klausur
+- Mut zur Lücke
+- Kann euch natürlich nichts versprechen
+
+---
+
+# Testing mit `pytest`
\ No newline at end of file
diff --git a/Tutorium/tut14/src/commands.py b/Tutorium/tut14/src/commands.py
new file mode 100644
index 0000000..7c90ecd
--- /dev/null
+++ b/Tutorium/tut14/src/commands.py
@@ -0,0 +1,36 @@
+from typing import Callable, Iterator
+
+
+class CommandExecutor[R]:
+    def __init__(self):
+        self.__commands: dict[str, Callable[..., R]] = {}
+    
+    def run(self, name: str, *args, **kwargs) -> list[R]:
+        results : list[R] = []
+        for command_name, command in self.__commands.items():
+            if command_name == name:
+                results += [command(*args, **kwargs)]
+        return results
+    
+    def register(self, cmd: Callable[..., R]) -> Callable[..., R]:
+        self.__commands[cmd.__name__] = cmd
+        return cmd
+    
+    
+if __name__ == '__main__':
+    app = CommandExecutor[str]()
+    
+    @app.register
+    def hello_world() -> str:
+        return 'hello_world'
+   
+    @app.register 
+    def divide(a: int, b: int) -> str:
+        if b == 0:
+            return "tried to divide by zero"
+        return str(a / b)
+    
+    print(app.run('hello_world'))
+    print(app.run('divide', 5, 0))
+    print(app.run('divide', 10, 2))
+    
\ No newline at end of file
diff --git a/Tutorium/tut14/src/decorator.py b/Tutorium/tut14/src/decorator.py
new file mode 100644
index 0000000..c3a7265
--- /dev/null
+++ b/Tutorium/tut14/src/decorator.py
@@ -0,0 +1,67 @@
+from typing import Any, Callable
+
+
+def count_calls[T](func: Callable[..., T]) -> Callable[..., T]:
+    count_calls.calls = 0
+    def wrapper(*args, **kwargs) -> T:
+        init_calls = count_calls.calls
+        count_calls.calls += 1
+        result = func(*args, **kwargs)
+        wrapper.calls = count_calls.calls - init_calls
+        return result
+    return wrapper
+
+def f(x: int, y: int) -> int:
+    if x % 2 == 0:
+        return x // 2
+    else:
+        return x + 2 * y - 1
+
+def count_iterations(a: int, b: int) -> int:
+    f_a(a, a, b)
+    return f_a.calls - 1
+
+@count_calls
+def f_a(init: int, a: int, b: int) -> None:
+    if a < b:
+        return
+    return f_a(init, f(a, init), b)
+
+def execute_by(n: int):
+    def wrapper(fn):
+        def wrapped_fn(*args, **kwargs):
+            for _ in range(0, n):
+                fn(*args, **kwargs)
+            return wrapped_fn
+        return wrapped_fn
+    return wrapper
+
+@execute_by(10)
+def hello_world():
+    print('hello world!')
+   
+@execute_by(10) 
+def print_ten_times(msg: str):
+    print(msg)
+
+
+def execute_two_times(fn) -> Callable[..., Any]:
+    def wrapper(*args, **kwargs):
+        for _ in range(0, 2):
+            fn(*args, **kwargs)
+        return wrapper
+    return wrapper
+
+@execute_two_times
+def test(msg: str):
+    print(msg)
+
+if __name__ == '__main__':
+    assert (i := count_iterations(7, 6)) == 3, i
+    assert (i := count_iterations(3, 2)) == 4, i
+    assert (i := count_iterations(13, 9)) == 18, i
+    assert (i := count_iterations(13, 10)) == 8, i
+    assert (i := count_iterations(3, 4)) == 0, i
+    print_ten_times("hello world")
+    test("hello")
+    
\ No newline at end of file