Changeset fe7d338 in flexograder


Ignore:
Timestamp:
11/09/25 22:08:21 (2 months ago)
Author:
Enrico Schwass <ennoausberlin@…>
Branches:
master
Children:
bb42e13
Parents:
e77bfb3
Message:

extract gui views and menu - first thoughts on mediator, event bus or announcer design pattern

Files:
4 added
5 edited

Legend:

Unmodified
Added
Removed
  • builder/question_factory.py

    re77bfb3 rfe7d338  
    3434
    3535    domain = q.get("domain") or default_domain
    36     print(domain)
    3736    text = q.get("text", "")
    3837    estate = EntityState.DRAFT
  • examples/python/intermediate.json

    re77bfb3 rfe7d338  
    44  "generated_at": "2025-10-20",
    55  "questions": [
    6     { "domain": "PY-ARITHM", "type": "radio", "text": "Welcher Operator liefert in Python 3 die Ganzzahl-Division (Abrunden Richtung −∞)?", "options": [
    7       {"text": "//", "points": 1},
    8       {"text": "/" , "points": 0},
    9       {"text": "%", "points": 0}
    10     ]},
    11     { "domain": "PY-ARITHM", "type": "checkbox", "text": "Welche Aussagen über Ganzzahlen (int) in Python sind korrekt?", "options": [
    12       {"text": "int kann beliebig groß werden (nur durch RAM begrenzt).", "points": 1},
    13       {"text": "int hat fest 64-Bit.", "points": 0},
    14       {"text": "Negatives Ergebnis bei -7 // 3 ist -3.", "points": 1},
    15       {"text": "Bei 7 % 3 ist der Rest 1.", "points": 1}
    16     ]},
    17     { "domain": "PY-ARITHM", "type": "radio", "text": "Welches Ergebnis hat 2 ** 3 ** 2?", "options": [
    18       {"text": "512 (weil Exponentiation rechtsassoziativ ist).", "points": 1},
    19       {"text": "64", "points": 0},
    20       {"text": "Fehler, da unklar.", "points": 0}
    21     ]},
    22     { "domain": "PY-ARITHM", "type": "checkbox", "text": "Welche Funktionen helfen beim Runden/Umwandeln?", "options": [
    23       {"text": "round(x)", "points": 1},
    24       {"text": "int(x)", "points": 1},
    25       {"text": "float.to_int(x)", "points": 0},
    26       {"text": "abs(x)", "points": 1}
    27     ]},
    28     { "domain": "PY-ARITHM", "type": "radio", "text": "Was liefert 0.1 + 0.2 in Python typischerweise?", "options": [
    29       {"text": "Einen Wert nahe 0.3 wegen Binär-Float, daher vergleicht man besser mit math.isclose.", "points": 1},
    30       {"text": "Genau 0.3, immer.", "points": 0},
    31       {"text": "Eine Exception.", "points": 0}
    32     ]},
    33 
    34     { "domain": "PY-CNTRLSTRCT", "type": "radio", "text": "Wie wiederholt man Code, bis eine Bedingung erfüllt ist?", "options": [
    35       {"text": "while-Schleife", "points": 1},
    36       {"text": "if-Anweisung", "points": 0},
    37       {"text": "import-Anweisung", "points": 0}
    38     ]},
    39     { "domain": "PY-CNTRLSTRCT", "type": "checkbox", "text": "Welche Schlüsselwörter passen zu Schleifen?", "options": [
    40       {"text": "break (Schleife vorzeitig beenden)", "points": 1},
    41       {"text": "continue (nächste Iteration)", "points": 1},
    42       {"text": "nextloop (existiert nicht)", "points": 0},
    43       {"text": "else: läuft, wenn kein break passierte", "points": 1}
    44     ]},
    45     { "domain": "PY-CNTRLSTRCT", "type": "radio", "text": "Wie iteriert man idiomatisch über Index und Element einer Liste?", "options": [
    46       {"text": "for i, x in enumerate(items):", "points": 1},
    47       {"text": "for i in items: x = items[i]", "points": 0},
    48       {"text": "for (i, x) in range(items):", "points": 0}
    49     ]},
    50     { "domain": "PY-CNTRLSTRCT", "type": "checkbox", "text": "Welche Teile gehören zur if-Syntax?", "options": [
    51       {"text": "if …:", "points": 1},
    52       {"text": "elif …:", "points": 1},
    53       {"text": "elseif …:", "points": 0},
    54       {"text": "else:", "points": 1}
    55     ]},
    56     { "domain": "PY-CNTRLSTRCT", "type": "radio", "text": "Wofür steht der Walrus-Operator (:=)?", "options": [
    57       {"text": "Zuweisung in einem Ausdruck (z. B. while (n := input()) != '')", "points": 1},
    58       {"text": "Vergleich auf Gleichheit", "points": 0},
    59       {"text": "Exponentiation", "points": 0}
    60     ]},
    61 
    62     { "domain": "PY-FILES", "type": "radio", "text": "Wie öffnet man eine Textdatei sicher zum Lesen in UTF-8?", "options": [
    63       {"text": "open(path, 'r', encoding='utf-8')", "points": 1},
    64       {"text": "open(path)", "points": 0},
    65       {"text": "open('utf-8', path)", "points": 0}
    66     ]},
    67     { "domain": "PY-FILES", "type": "checkbox", "text": "Welche Vorteile hat der with-Block beim Datei-I/O?", "options": [
    68       {"text": "Automatisches Schließen der Datei.", "points": 1},
    69       {"text": "Weniger Risiko für Ressourcen-Leaks.", "points": 1},
    70       {"text": "Er macht das Lesen schneller per se.", "points": 0},
    71       {"text": "Kompakterer Code.", "points": 1}
    72     ]},
    73     { "domain": "PY-FILES", "type": "radio", "text": "Welcher Modus legt eine neue Datei an und wirft Fehler, falls sie existiert?", "options": [
    74       {"text": "'x'", "points": 1},
    75       {"text": "'w'", "points": 0},
    76       {"text": "'a'", "points": 0}
    77     ]},
    78     { "domain": "PY-FILES", "type": "checkbox", "text": "Welche Bibliotheken sind für Pfade hilfreich?", "options": [
    79       {"text": "pathlib.Path", "points": 1},
    80       {"text": "os.path", "points": 1},
    81       {"text": "sys.path zum Dateischreiben", "points": 0},
    82       {"text": "glob für Dateimuster", "points": 1}
    83     ]},
    84     { "domain": "PY-FILES", "type": "radio", "text": "Wie liest man große Dateien speicherschonend Zeile für Zeile?", "options": [
    85       {"text": "for line in f:", "points": 1},
    86       {"text": "lines = f.readlines()", "points": 0},
    87       {"text": "text = f.read()", "points": 0}
    88     ]},
    89 
    90     { "domain": "PY-TYPES", "type": "radio", "text": "Wozu dienen Typannotationen in Python?", "options": [
    91       {"text": "Sie unterstützen Tools/IDE/Typchecker; zur Laufzeit optional.", "points": 1},
    92       {"text": "Sie erzwingen strikt die Typen zur Laufzeit.", "points": 0},
    93       {"text": "Sie ersetzen Docstrings.", "points": 0}
    94     ]},
    95     { "domain": "PY-TYPES", "type": "checkbox", "text": "Welche sind gültige Typannotationen (Beispiele ab Python 3.9)?", "options": [
    96       {"text": "list[int]", "points": 1},
    97       {"text": "dict[str, int]", "points": 1},
    98       {"text": "Optional[int] bzw. int | None", "points": 1},
    99       {"text": "tuple<int>", "points": 0}
    100     ]},
    101     { "domain": "PY-TYPES", "type": "radio", "text": "Welche Annotation beschreibt 'Funktion erhält int und gibt bool zurück'?", "options": [
    102       {"text": "Callable[[int], bool]", "points": 1},
    103       {"text": "function(int)->bool", "points": 0},
    104       {"text": "call[int->bool]", "points": 0}
    105     ]},
    106     { "domain": "PY-TYPES", "type": "checkbox", "text": "Welche Stdlib-Hilfen definieren einfache Datenobjekte?", "options": [
    107       {"text": "dataclasses.dataclass", "points": 1},
    108       {"text": "typing.TypedDict", "points": 1},
    109       {"text": "collections.namedtuple", "points": 1},
    110       {"text": "functools.singledispatch", "points": 0}
    111     ]},
    112     { "domain": "PY-TYPES", "type": "radio", "text": "Was bedeutet `Any`?", "options": [
    113       {"text": "Beliebiger Typ (Typchecker schränkt kaum ein).", "points": 1},
    114       {"text": "Nur Ganzzahlen erlaubt.", "points": 0},
    115       {"text": "Ist identisch zu `object` in jeder Hinsicht.", "points": 0}
    116     ]},
    117 
    118     { "domain": "PY-STREAMS", "type": "radio", "text": "Was gibt eine Listen-Comprehension typischerweise zurück?", "options": [
    119       {"text": "Eine neue Liste", "points": 1},
    120       {"text": "Einen Generator", "points": 0},
    121       {"text": "Ein Tupel", "points": 0}
    122     ]},
    123     { "domain": "PY-STREAMS", "type": "checkbox", "text": "Welche sind Lazy-Konstrukte?", "options": [
    124       {"text": "(x for x in seq)  (Generator-Expression)", "points": 1},
    125       {"text": "map(func, seq)", "points": 1},
    126       {"text": "filter(func, seq)", "points": 1},
    127       {"text": "[x for x in seq]", "points": 0}
    128     ]},
    129     { "domain": "PY-STREAMS", "type": "radio", "text": "Wofür steht `yield` in einer Funktion?", "options": [
    130       {"text": "Es macht die Funktion zu einem Generator (liefert Werte schrittweise).", "points": 1},
    131       {"text": "Es beendet das Programm.", "points": 0},
    132       {"text": "Es importiert ein Modul.", "points": 0}
    133     ]},
    134     { "domain": "PY-STREAMS", "type": "checkbox", "text": "Welche Tools sind nützlich für Iterables?", "options": [
    135       {"text": "itertools (z. B. chain, islice)", "points": 1},
    136       {"text": "enumerate", "points": 1},
    137       {"text": "re (Regex) als Iteratorersatz", "points": 0},
    138       {"text": "sum zur Konkatenation von Listen", "points": 0}
    139     ]},
    140     { "domain": "PY-STREAMS", "type": "radio", "text": "Welcher Ausdruck ist am speicherschonendsten?", "options": [
    141       {"text": "sum(x for x in range(1_000_000))", "points": 1},
    142       {"text": "sum([x for x in range(1_000_000)])", "points": 0},
    143       {"text": "list(range(1_000_000))", "points": 0}
    144     ]},
    145 
    146     { "domain": "PY-FUNCTOOLS", "type": "radio", "text": "Wozu dient `functools.lru_cache`?", "options": [
    147       {"text": "Ergebnisse teurer Funktionsaufrufe zwischenspeichern.", "points": 1},
    148       {"text": "Dateien schneller lesen.", "points": 0},
    149       {"text": "Asynchronität bereitstellen.", "points": 0}
    150     ]},
    151     { "domain": "PY-FUNCTOOLS", "type": "checkbox", "text": "Welche Aussagen zu `partial` stimmen?", "options": [
    152       {"text": "`partial` bindet einige Argumente vor.", "points": 1},
    153       {"text": "Die neue Funktion verhält sich wie die ursprüngliche mit den gebundenen Werten.", "points": 1},
    154       {"text": "`partial` ersetzt kwargs vollständig.", "points": 0},
    155       {"text": "`partial` ist ein Decorator für Klassen.", "points": 0}
    156     ]},
    157     { "domain": "PY-FUNCTOOLS", "type": "radio", "text": "Was macht `@wraps` beim Schreiben eigener Decorators?", "options": [
    158       {"text": "Überträgt Metadaten (Name, Docstring) auf die Wrapper-Funktion.", "points": 1},
    159       {"text": "Beschleunigt die Funktion automatisch.", "points": 0},
    160       {"text": "Erstellt eine Klasse.", "points": 0}
    161     ]},
    162     { "domain": "PY-FUNCTOOLS", "type": "checkbox", "text": "Welche Aussagen zu `singledispatch` sind korrekt?", "options": [
    163       {"text": "Erzeugt generische Funktionen je nach Typ des ersten Arguments.", "points": 1},
    164       {"text": "Weitere Varianten werden mit @<func>.register definiert.", "points": 1},
    165       {"text": "Funktioniert nicht mit Klassenmethoden/Methoden-Workarounds.", "points": 0},
    166       {"text": "Ist Teil der Stdlib.", "points": 1}
    167     ]},
    168     { "domain": "PY-FUNCTOOLS", "type": "radio", "text": "Wie leert man den Cache einer mit `lru_cache` dekorierten Funktion?", "options": [
    169       {"text": "funktion.cache_clear()", "points": 1},
    170       {"text": "del funktion", "points": 0},
    171       {"text": "Cache leert sich automatisch bei Programmstart nicht", "points": 0}
    172     ]},
    173 
    174     { "domain": "PY-COLLECTIONS", "type": "radio", "text": "Welche Struktur eignet sich für schnelle Anhänge am Ende?", "options": [
    175       {"text": "list.append", "points": 1},
    176       {"text": "tuple +=", "points": 0},
    177       {"text": "str +=", "points": 0}
    178     ]},
    179     { "domain": "PY-COLLECTIONS", "type": "checkbox", "text": "Welche Aussagen zu Mengen (set) sind korrekt?", "options": [
    180       {"text": "Speichern einzigartige, hashbare Elemente.", "points": 1},
    181       {"text": "Unterstützen Vereinigungs-/Schnittmengen-Operationen.", "points": 1},
    182       {"text": "Erhalten die Einfügereihenfolge garantiert vor 3.7.", "points": 0},
    183       {"text": "Doppelte Einträge bleiben erhalten.", "points": 0}
    184     ]},
    185     { "domain": "PY-COLLECTIONS", "type": "radio", "text": "Wofür ist `collections.Counter` gut?", "options": [
    186       {"text": "Häufigkeiten von Elementen zählen.", "points": 1},
    187       {"text": "Dateien öffnen.", "points": 0},
    188       {"text": "Zahlen runden.", "points": 0}
    189     ]},
    190     { "domain": "PY-COLLECTIONS", "type": "checkbox", "text": "Welche Datenstrukturen sind unveränderlich (immutable)?", "options": [
    191       {"text": "tuple", "points": 1},
    192       {"text": "frozenset", "points": 1},
    193       {"text": "list", "points": 0},
    194       {"text": "dict", "points": 0}
    195     ]},
    196     { "domain": "PY-COLLECTIONS", "type": "radio", "text": "Was bewirkt `defaultdict(list)`?", "options": [
    197       {"text": "Fehlende Schlüssel bekommen automatisch eine leere Liste.", "points": 1},
    198       {"text": "Fehlende Schlüssel werfen KeyError.", "points": 0},
    199       {"text": "Es ist identisch zu dict.", "points": 0}
    200     ]},
    201 
    202     { "domain": "PY-DATETIME", "type": "radio", "text": "Welche Bibliothek liefert Zeitzonen ohne Zusatzpakete (ab 3.9)?", "options": [
    203       {"text": "zoneinfo", "points": 1},
    204       {"text": "pytz", "points": 0},
    205       {"text": "tzlocal", "points": 0}
    206     ]},
    207     { "domain": "PY-DATETIME", "type": "checkbox", "text": "Welche Aussagen zu datetime sind korrekt?", "options": [
    208       {"text": "Naive datetimes haben keine Zeitzone.", "points": 1},
    209       {"text": "Aware datetimes tragen eine tzinfo.", "points": 1},
    210       {"text": "UTC ist eine sinnvolle interne Normalform.", "points": 1},
    211       {"text": "time.time() ist immer monoton.", "points": 0}
    212     ]},
    213     { "domain": "PY-DATETIME", "type": "radio", "text": "Wie misst man kurze Zeitdauern zuverlässig?", "options": [
    214       {"text": "time.perf_counter()", "points": 1},
    215       {"text": "datetime.now()", "points": 0},
    216       {"text": "time.sleep()", "points": 0}
    217     ]},
    218     { "domain": "PY-DATETIME", "type": "checkbox", "text": "Welche Formate sind für Logs/Datenaustausch robust?", "options": [
    219       {"text": "ISO-8601 (z. B. 2025-10-20T15:00:00Z)", "points": 1},
    220       {"text": "Ortsabhängige Datumsstrings", "points": 0},
    221       {"text": "Unix-Timestamps (Sekunden seit Epoch)", "points": 1},
    222       {"text": "Freitext ohne Norm", "points": 0}
    223     ]},
    224     { "domain": "PY-DATETIME", "type": "radio", "text": "Wie erstellt man 'jetzt in UTC' als aware datetime?", "options": [
    225       {"text": "datetime.now(timezone.utc)", "points": 1},
    226       {"text": "datetime.utcnow()", "points": 0},
    227       {"text": "datetime.now()", "points": 0}
    228     ]},
    229 
    230     { "domain": "PY-NAMESPACES", "type": "radio", "text": "Wofür steht LEGB?", "options": [
    231       {"text": "Local, Enclosing, Global, Builtins (Namensauflösung)", "points": 1},
    232       {"text": "Library, Env, Git, Bytecode", "points": 0},
    233       {"text": "List, Eval, Generator, Bytes", "points": 0}
    234     ]},
    235     { "domain": "PY-NAMESPACES", "type": "checkbox", "text": "Welche Aussagen zu `global`/`nonlocal` sind korrekt?", "options": [
    236       {"text": "`global` bezieht sich auf das Modul-Namespace.", "points": 1},
    237       {"text": "`nonlocal` greift auf die nächst-äußere Funktionsvariable zu.", "points": 1},
    238       {"text": "`nonlocal` kann globale Variablen binden.", "points": 0},
    239       {"text": "`global` wirkt nur in Klassenmethoden.", "points": 0}
    240     ]},
    241     { "domain": "PY-NAMESPACES", "type": "radio", "text": "Wie markiert man interne Modulnamen konventionell?", "options": [
    242       {"text": "Mit führendem Unterstrich: _name", "points": 1},
    243       {"text": "Mit Großschreibung", "points": 0},
    244       {"text": "Mit @internal", "points": 0}
    245     ]},
    246     { "domain": "PY-NAMESPACES", "type": "checkbox", "text": "Welche sind gute Praktiken, um Namenskonflikte zu vermeiden?", "options": [
    247       {"text": "Aussagekräftige Modul-/Paketnamen", "points": 1},
    248       {"text": "Kein `from x import *`", "points": 1},
    249       {"text": "Alles in eine Datei schreiben", "points": 0},
    250       {"text": "Konstante Namen in UPPER_CASE", "points": 1}
    251     ]},
    252     { "domain": "PY-NAMESPACES", "type": "radio", "text": "Wozu dient `__all__`?", "options": [
    253       {"text": "Steuert Exporte bei `from mod import *`.", "points": 1},
    254       {"text": "Definiert die Versionsnummer.", "points": 0},
    255       {"text": "Erzeugt automatisch Docstrings.", "points": 0}
    256     ]},
    257 
    258     { "domain": "PY-RECURSION", "type": "radio", "text": "Warum ist tiefe Rekursion in Python oft problematisch?", "options": [
    259       {"text": "Begrenzter Call-Stack; keine Tail-Call-Optimierung.", "points": 1},
    260       {"text": "Python erlaubt Rekursion gar nicht.", "points": 0},
    261       {"text": "Listen unterstützen keine Rekursion.", "points": 0}
    262     ]},
    263     { "domain": "PY-RECURSION", "type": "checkbox", "text": "Wie kann man eine rekursive Lösung vereinfachen?", "options": [
    264       {"text": "Iterative Variante mit explizitem Stack/Queue.", "points": 1},
    265       {"text": "Memoization bei sich wiederholenden Teilproblemen.", "points": 1},
    266       {"text": "Zufällige Abbrüche einbauen.", "points": 0},
    267       {"text": "Auf unendliche Rekursion hoffen.", "points": 0}
    268     ]},
    269     { "domain": "PY-RECURSION", "type": "radio", "text": "Welche Variante ist für Fibonacci(n) effizienter?", "options": [
    270       {"text": "Iterativ oder mit lru_cache.", "points": 1},
    271       {"text": "Naiv rekursiv ohne Cache.", "points": 0},
    272       {"text": "Mit eval auf Zeichenketten.", "points": 0}
    273     ]},
    274     { "domain": "PY-RECURSION", "type": "checkbox", "text": "Welche Probleme treten bei Graph-Rekursion auf?", "options": [
    275       {"text": "Zyklen → unendliche Rekursion ohne Visited-Set.", "points": 1},
    276       {"text": "Große Tiefe → RecursionError.", "points": 1},
    277       {"text": "Listen sind nicht iterierbar.", "points": 0},
    278       {"text": "Rekursion verbraucht keinen Stack.", "points": 0}
    279     ]},
    280     { "domain": "PY-RECURSION", "type": "radio", "text": "Wie traversiert man einen Baum iterativ (DFS)?", "options": [
    281       {"text": "Mit explizitem Stack (list) und Schleife.", "points": 1},
    282       {"text": "Mit globaler Variablen.", "points": 0},
    283       {"text": "Gar nicht möglich.", "points": 0}
    284     ]},
    285 
    286     { "domain": "PY-MODULES", "type": "radio", "text": "Was macht Code auf Modul-Top-Level beim Import?", "options": [
    287       {"text": "Er wird genau einmal ausgeführt (Initialisierung).", "points": 1},
    288       {"text": "Er wird jedes Mal neu ausgeführt.", "points": 0},
    289       {"text": "Er wird ignoriert.", "points": 0}
    290     ]},
    291     { "domain": "PY-MODULES", "type": "checkbox", "text": "Welche Aussagen zu Imports sind korrekt?", "options": [
    292       {"text": "Module werden in sys.modules gecacht.", "points": 1},
    293       {"text": "Relative Importe funktionieren nur innerhalb von Paketen.", "points": 1},
    294       {"text": "`if __name__ == '__main__':` trennt Skript-Start vom Import.", "points": 1},
    295       {"text": "`from x import *` ist immer empfohlen.", "points": 0}
    296     ]},
    297     { "domain": "PY-MODULES", "type": "radio", "text": "Wie bindet man einen Paket-Logger korrekt?", "options": [
    298       {"text": "logging.getLogger(__name__)", "points": 1},
    299       {"text": "print statt Logging", "points": 0},
    300       {"text": "logging.getLogger()", "points": 0}
    301     ]},
    302     { "domain": "PY-MODULES", "type": "checkbox", "text": "Welche Werkzeuge sind für Paket-Ressourcen nützlich?", "options": [
    303       {"text": "importlib.resources", "points": 1},
    304       {"text": "pkgutil", "points": 1},
    305       {"text": "random.resources", "points": 0},
    306       {"text": "sys.path zum Lesen von Dateien", "points": 0}
    307     ]},
    308     { "domain": "PY-MODULES", "type": "radio", "text": "Wie führt man ein Modul als Skript aus?", "options": [
    309       {"text": "python -m paket.modul", "points": 1},
    310       {"text": "python paket/modul", "points": 0},
    311       {"text": "run paket.modul", "points": 0}
    312     ]},
    313 
    314     { "domain": "PY-OOP", "type": "radio", "text": "Wofür steht `@property`?", "options": [
    315       {"text": "Liest/erzeugt berechnete Attribute wie ein Getter.", "points": 1},
    316       {"text": "Markiert eine Klassenmethode.", "points": 0},
    317       {"text": "Erstellt automatisch __init__.", "points": 0}
    318     ]},
    319     { "domain": "PY-OOP", "type": "checkbox", "text": "Welche Aussagen zu Klassenmethoden/staticmethods stimmen?", "options": [
    320       {"text": "@classmethod erhält die Klasse als erstes Argument (cls).", "points": 1},
    321       {"text": "@staticmethod erhält weder self noch cls.", "points": 1},
    322       {"text": "@classmethod und @staticmethod sind identisch.", "points": 0},
    323       {"text": "Beide können auf der Klasse aufgerufen werden.", "points": 1}
    324     ]},
    325     { "domain": "PY-OOP", "type": "radio", "text": "Wie vergleicht man zwei Instanzen auf Wertgleichheit korrekt?", "options": [
    326       {"text": "__eq__ implementieren und ggf. __hash__ bei Immutables.", "points": 1},
    327       {"text": "Nur __repr__ implementieren.", "points": 0},
    328       {"text": "Nur __hash__ implementieren.", "points": 0}
    329     ]},
    330     { "domain": "PY-OOP", "type": "checkbox", "text": "Welche Methoden sind für Containerklassen nützlich?", "options": [
    331       {"text": "__len__", "points": 1},
    332       {"text": "__iter__", "points": 1},
    333       {"text": "__getitem__", "points": 1},
    334       {"text": "__cmp__", "points": 0}
    335     ]},
    336     { "domain": "PY-OOP", "type": "radio", "text": "Welche Basisklasse nutzt man für abstrakte Klassen?", "options": [
    337       {"text": "abc.ABC", "points": 1},
    338       {"text": "typing.Any", "points": 0},
    339       {"text": "objectonly", "points": 0}
    340     ]},
    341 
    342     { "domain": "PY-EXCEPTIONS", "type": "radio", "text": "Wie fängt man eine konkrete Exception?", "options": [
    343       {"text": "try: ... except ValueError:", "points": 1},
    344       {"text": "try: ... catch ValueError:", "points": 0},
    345       {"text": "except(ValueError): ... ohne try", "points": 0}
    346     ]},
    347     { "domain": "PY-EXCEPTIONS", "type": "checkbox", "text": "Welche Blöcke kann ein try-Statement enthalten?", "options": [
    348       {"text": "except", "points": 1},
    349       {"text": "else", "points": 1},
    350       {"text": "finally", "points": 1},
    351       {"text": "then", "points": 0}
    352     ]},
    353     { "domain": "PY-EXCEPTIONS", "type": "radio", "text": "Wie wirft man dieselbe Exception im except-Block erneut?", "options": [
    354       {"text": "raise  (ohne Argumente)", "points": 1},
    355       {"text": "return e", "points": 0},
    356       {"text": "throw e", "points": 0}
    357     ]},
    358     { "domain": "PY-EXCEPTIONS", "type": "checkbox", "text": "Welche sind gute Praktiken beim Fehler-Handling?", "options": [
    359       {"text": "Nur spezifische Exceptions abfangen.", "points": 1},
    360       {"text": "Kontext mit `raise ... from e` erhalten.", "points": 1},
    361       {"text": "Bare `except:` nur in seltenen Ausnahmefällen.", "points": 1},
    362       {"text": "Alle Fehler still ignorieren.", "points": 0}
    363     ]},
    364     { "domain": "PY-EXCEPTIONS", "type": "radio", "text": "Welche Ausnahmen sind KEINE Unterklassen von Exception?", "options": [
    365       {"text": "SystemExit/KeyboardInterrupt/GeneratorExit", "points": 1},
    366       {"text": "RuntimeError", "points": 0},
    367       {"text": "ValueError", "points": 0}
    368     ]},
    369 
    370     { "domain": "PY-CNTXTMNGR", "type": "radio", "text": "Wozu dient ein Context Manager (with)?", "options": [
    371       {"text": "Ressourcen sicher öffnen/schließen (z. B. Dateien).", "points": 1},
    372       {"text": "Nur fürs Logging.", "points": 0},
    373       {"text": "Nur in Tests nutzbar.", "points": 0}
    374     ]},
    375     { "domain": "PY-CNTXTMNGR", "type": "checkbox", "text": "Welche bereitgestellten Kontexte sind nützlich?", "options": [
    376       {"text": "contextlib.suppress", "points": 1},
    377       {"text": "contextlib.redirect_stdout", "points": 1},
    378       {"text": "contextlib.ExitStack", "points": 1},
    379       {"text": "contextlib.magic", "points": 0}
    380     ]},
    381     { "domain": "PY-CNTXTMNGR", "type": "radio", "text": "Welche Methoden implementiert ein eigener Context-Manager (Klasse)?", "options": [
    382       {"text": "__enter__ und __exit__", "points": 1},
    383       {"text": "__open__ und __close__", "points": 0},
    384       {"text": "__start__ und __stop__", "points": 0}
    385     ]},
    386     { "domain": "PY-CNTXTMNGR", "type": "checkbox", "text": "Welche Aussage zu `__exit__(exc_type, exc, tb)` stimmt?", "options": [
    387       {"text": "True zurückgeben unterdrückt die Exception.", "points": 1},
    388       {"text": "False/None propagiert die Exception weiter.", "points": 1},
    389       {"text": "Die Parameter sind immer None, auch bei Fehlern.", "points": 0},
    390       {"text": "__exit__ wird nicht aufgerufen, wenn ein Fehler auftritt.", "points": 0}
    391     ]},
    392     { "domain": "PY-CNTXTMNGR", "type": "radio", "text": "Wie kombiniert man mehrere Kontexte flexibel?", "options": [
    393       {"text": "Mit contextlib.ExitStack()", "points": 1},
    394       {"text": "Gar nicht möglich.", "points": 0},
    395       {"text": "Mit globalen Variablen.", "points": 0}
    396     ]},
    397 
    398     { "domain": "PY-REGEXP", "type": "radio", "text": "Welche Stdlib nutzt man für reguläre Ausdrücke?", "options": [
    399       {"text": "re", "points": 1},
    400       {"text": "regexlib", "points": 0},
    401       {"text": "rx", "points": 0}
    402     ]},
    403     { "domain": "PY-REGEXP", "type": "checkbox", "text": "Welche Konstrukte gehören zur re-Syntax?", "options": [
    404       {"text": "Gruppen: (...)", "points": 1},
    405       {"text": "Benannte Gruppen: (?P<name>...)", "points": 1},
    406       {"text": "Quantifizierer: *, +, ?", "points": 1},
    407       {"text": "Operator := für Teilmuster", "points": 0}
    408     ]},
    409     { "domain": "PY-REGEXP", "type": "radio", "text": "Wie findet man alle nicht-überlappenden Treffer in einem Text?", "options": [
    410       {"text": "re.findall(pattern, text)", "points": 1},
    411       {"text": "re.match überall aufrufen", "points": 0},
    412       {"text": "re.compile(pattern).group()", "points": 0}
    413     ]},
    414     { "domain": "PY-REGEXP", "type": "checkbox", "text": "Welche Flags sind korrekt zugeordnet?", "options": [
    415       {"text": "re.IGNORECASE: Groß/Kleinschreibung ignorieren", "points": 1},
    416       {"text": "re.MULTILINE: ^ und $ auf Zeilen anwenden", "points": 1},
    417       {"text": "re.DOTALL: Punkt matcht auch Newlines", "points": 1},
    418       {"text": "re.GLOBAL: existiert in re", "points": 0}
    419     ]},
    420     { "domain": "PY-REGEXP", "type": "radio", "text": "Wie ersetzt man Texte mit einer Funktion je Match?", "options": [
    421       {"text": "re.sub(pattern, func, text)", "points": 1},
    422       {"text": "re.replace(pattern, func, text)", "points": 0},
    423       {"text": "text.replace_re(func)", "points": 0}
    424     ]},
    425 
    426     { "domain": "PY-PARALLEL", "type": "radio", "text": "Wofür eignen sich Threads in CPython besonders?", "options": [
    427       {"text": "I/O-gebundene Aufgaben (z. B. Netzwerk, Dateien).", "points": 1},
    428       {"text": "Schwere CPU-Berechnungen parallelisieren.", "points": 0},
    429       {"text": "Ohne Synchronisation immer korrekt.", "points": 0}
    430     ]},
    431     { "domain": "PY-PARALLEL", "type": "checkbox", "text": "Welche Bibliotheken gehören zur Stdlib für Parallelität/Konkurrenz?", "options": [
    432       {"text": "threading", "points": 1},
    433       {"text": "multiprocessing", "points": 1},
    434       {"text": "concurrent.futures", "points": 1},
    435       {"text": "numpy.threads", "points": 0}
    436     ]},
    437     { "domain": "PY-PARALLEL", "type": "radio", "text": "Welche Klasse erstellt einen Prozess-Pool einfach?", "options": [
    438       {"text": "concurrent.futures.ProcessPoolExecutor", "points": 1},
    439       {"text": "threading.Pool", "points": 0},
    440       {"text": "os.pool", "points": 0}
    441     ]},
    442     { "domain": "PY-PARALLEL", "type": "checkbox", "text": "Welche Aussagen zu Locks sind richtig?", "options": [
    443       {"text": "Locks schützen kritische Abschnitte.", "points": 1},
    444       {"text": "Ohne Locks kann es zu Race Conditions kommen.", "points": 1},
    445       {"text": "Mit GIL sind Locks nie nötig.", "points": 0},
    446       {"text": "RLock erlaubt denselben Thread mehrfach zu sperren.", "points": 1}
    447     ]},
    448     { "domain": "PY-PARALLEL", "type": "radio", "text": "Was beschreibt `asyncio` korrekt?", "options": [
    449       {"text": "Kooperative Nebenläufigkeit per Event-Loop und await.", "points": 1},
    450       {"text": "Preemptives Scheduling wie OS-Threads.", "points": 0},
    451       {"text": "Automatische Mehrkern-Parallelisierung.", "points": 0}
    452     ]},
    453 
    454     { "domain": "PY-NETWORK", "type": "radio", "text": "Welche Bibliothek ist der Low-Level-Baustein für TCP/UDP?", "options": [
    455       {"text": "socket", "points": 1},
    456       {"text": "email", "points": 0},
    457       {"text": "http.client", "points": 0}
    458     ]},
    459     { "domain": "PY-NETWORK", "type": "checkbox", "text": "Welche Aussagen zu HTTP in der Stdlib sind korrekt?", "options": [
    460       {"text": "urllib.request kann einfache HTTP-Requests senden.", "points": 1},
    461       {"text": "http.client ist sehr Low-Level.", "points": 1},
    462       {"text": "ssl unterstützt TLS-Konfiguration.", "points": 1},
    463       {"text": "email ist für HTTP-Requests gedacht.", "points": 0}
    464     ]},
    465     { "domain": "PY-NETWORK", "type": "radio", "text": "Wie setzt man ein Timeout für einen Socket?", "options": [
    466       {"text": "sock.settimeout(seconds)", "points": 1},
    467       {"text": "socket.timeout=True", "points": 0},
    468       {"text": "sock.timeout(seconds)", "points": 0}
    469     ]},
    470     { "domain": "PY-NETWORK", "type": "checkbox", "text": "Welche Helfer gibt es zum Arbeiten mit URLs?", "options": [
    471       {"text": "urllib.parse (urlsplit, urlencode, parse_qs)", "points": 1},
    472       {"text": "json.parse_url", "points": 0},
    473       {"text": "urllib.robotparser für robots.txt", "points": 1},
    474       {"text": "re.url", "points": 0}
    475     ]},
    476     { "domain": "PY-NETWORK", "type": "radio", "text": "Wie validiert man standardmäßig TLS-Zertifikate mit urllib?", "options": [
    477       {"text": "Standard-Handler prüfen Zertifikate, wenn CA-Store korrekt ist.", "points": 1},
    478       {"text": "urllib kann TLS nie prüfen.", "points": 0},
    479       {"text": "Man muss verify=False setzen.", "points": 0}
    480     ]},
    481 
    482     { "domain": "PY-PACKAGING", "type": "radio", "text": "Welche Datei beschreibt moderne Projekt-Metadaten/Build-System?", "options": [
    483       {"text": "pyproject.toml", "points": 1},
    484       {"text": "requirements.txt", "points": 0},
    485       {"text": "Pipfile.lock", "points": 0}
    486     ]},
    487     { "domain": "PY-PACKAGING", "type": "checkbox", "text": "Welche Felder sind typisch in pyproject.toml (PEP 621)?", "options": [
    488       {"text": "project.name / version / dependencies", "points": 1},
    489       {"text": "project.scripts für CLI-Einträge", "points": 1},
    490       {"text": "build-system (Backend/Requires)", "points": 1},
    491       {"text": "kernel.modules", "points": 0}
    492     ]},
    493     { "domain": "PY-PACKAGING", "type": "radio", "text": "Was ist ein Wheel (.whl)?", "options": [
    494       {"text": "Vorgebaute Distributionsdatei für schnelle Installation.", "points": 1},
    495       {"text": "Quellpaket (sdist).", "points": 0},
    496       {"text": "Virtuelle Umgebung.", "points": 0}
    497     ]},
    498     { "domain": "PY-PACKAGING", "type": "checkbox", "text": "Welche Tools helfen beim Bauen/Veröffentlichen?", "options": [
    499       {"text": "`python -m build` (extern)", "points": 1},
    500       {"text": "`pip install .`", "points": 1},
    501       {"text": "twine für Uploads", "points": 1},
    502       {"text": "setup.py zwingend in jedem Projekt", "points": 0}
    503     ]},
    504     { "domain": "PY-PACKAGING", "type": "radio", "text": "Wo definiert man ein Konsolen-Skript ohne setup.py?", "options": [
    505       {"text": "In pyproject.toml unter project.scripts (Backend-abhängig).", "points": 1},
    506       {"text": "In requirements.txt", "points": 0},
    507       {"text": "In README.md", "points": 0}
    508     ]}
     6    {
     7      "domain": "PY_ARITHM",
     8      "qtype": "single_choice",
     9      "text": "Welcher Operator liefert in Python 3 die Ganzzahl-Division (Abrunden Richtung −∞)?",
     10      "options": [
     11        {
     12          "text": "//",
     13          "points": 1,
     14          "id": "A"
     15        },
     16        {
     17          "text": "/",
     18          "points": 0,
     19          "id": "B"
     20        },
     21        {
     22          "text": "%",
     23          "points": 0,
     24          "id": "C"
     25        }
     26      ]
     27    },
     28    {
     29      "domain": "PY_ARITHM",
     30      "qtype": "multiple_choice",
     31      "text": "Welche Aussagen über Ganzzahlen (int) in Python sind korrekt?",
     32      "options": [
     33        {
     34          "text": "int kann beliebig groß werden (nur durch RAM begrenzt).",
     35          "points": 1,
     36          "id": "A"
     37        },
     38        {
     39          "text": "int hat fest 64-Bit.",
     40          "points": 0,
     41          "id": "B"
     42        },
     43        {
     44          "text": "Negatives Ergebnis bei -7 // 3 ist -3.",
     45          "points": 1,
     46          "id": "C"
     47        },
     48        {
     49          "text": "Bei 7 % 3 ist der Rest 1.",
     50          "points": 1,
     51          "id": "D"
     52        }
     53      ]
     54    },
     55    {
     56      "domain": "PY_ARITHM",
     57      "qtype": "single_choice",
     58      "text": "Welches Ergebnis hat 2 ** 3 ** 2?",
     59      "options": [
     60        {
     61          "text": "512 (weil Exponentiation rechtsassoziativ ist).",
     62          "points": 1,
     63          "id": "A"
     64        },
     65        {
     66          "text": "64",
     67          "points": 0,
     68          "id": "B"
     69        },
     70        {
     71          "text": "Fehler, da unklar.",
     72          "points": 0,
     73          "id": "C"
     74        }
     75      ]
     76    },
     77    {
     78      "domain": "PY_ARITHM",
     79      "qtype": "multiple_choice",
     80      "text": "Welche Funktionen helfen beim Runden/Umwandeln?",
     81      "options": [
     82        {
     83          "text": "round(x)",
     84          "points": 1,
     85          "id": "A"
     86        },
     87        {
     88          "text": "int(x)",
     89          "points": 1,
     90          "id": "B"
     91        },
     92        {
     93          "text": "float.to_int(x)",
     94          "points": 0,
     95          "id": "C"
     96        },
     97        {
     98          "text": "abs(x)",
     99          "points": 1,
     100          "id": "D"
     101        }
     102      ]
     103    },
     104    {
     105      "domain": "PY_ARITHM",
     106      "qtype": "single_choice",
     107      "text": "Was liefert 0.1 + 0.2 in Python typischerweise?",
     108      "options": [
     109        {
     110          "text": "Einen Wert nahe 0.3 wegen Binär-Float, daher vergleicht man besser mit math.isclose.",
     111          "points": 1,
     112          "id": "A"
     113        },
     114        {
     115          "text": "Genau 0.3, immer.",
     116          "points": 0,
     117          "id": "B"
     118        },
     119        {
     120          "text": "Eine Exception.",
     121          "points": 0,
     122          "id": "C"
     123        }
     124      ]
     125    },
     126    {
     127      "domain": "PY_CNTRLSTRCT",
     128      "qtype": "single_choice",
     129      "text": "Wie wiederholt man Code, bis eine Bedingung erfüllt ist?",
     130      "options": [
     131        {
     132          "text": "while-Schleife",
     133          "points": 1,
     134          "id": "A"
     135        },
     136        {
     137          "text": "if-Anweisung",
     138          "points": 0,
     139          "id": "B"
     140        },
     141        {
     142          "text": "import-Anweisung",
     143          "points": 0,
     144          "id": "C"
     145        }
     146      ]
     147    },
     148    {
     149      "domain": "PY_CNTRLSTRCT",
     150      "qtype": "multiple_choice",
     151      "text": "Welche Schlüsselwörter passen zu Schleifen?",
     152      "options": [
     153        {
     154          "text": "break (Schleife vorzeitig beenden)",
     155          "points": 1,
     156          "id": "A"
     157        },
     158        {
     159          "text": "continue (nächste Iteration)",
     160          "points": 1,
     161          "id": "B"
     162        },
     163        {
     164          "text": "nextloop (existiert nicht)",
     165          "points": 0,
     166          "id": "C"
     167        },
     168        {
     169          "text": "else: läuft, wenn kein break passierte",
     170          "points": 1,
     171          "id": "D"
     172        }
     173      ]
     174    },
     175    {
     176      "domain": "PY_CNTRLSTRCT",
     177      "qtype": "single_choice",
     178      "text": "Wie iteriert man idiomatisch über Index und Element einer Liste?",
     179      "options": [
     180        {
     181          "text": "for i, x in enumerate(items):",
     182          "points": 1,
     183          "id": "A"
     184        },
     185        {
     186          "text": "for i in items: x = items[i]",
     187          "points": 0,
     188          "id": "B"
     189        },
     190        {
     191          "text": "for (i, x) in range(items):",
     192          "points": 0,
     193          "id": "C"
     194        }
     195      ]
     196    },
     197    {
     198      "domain": "PY_CNTRLSTRCT",
     199      "qtype": "multiple_choice",
     200      "text": "Welche Teile gehören zur if-Syntax?",
     201      "options": [
     202        {
     203          "text": "if …:",
     204          "points": 1,
     205          "id": "A"
     206        },
     207        {
     208          "text": "elif …:",
     209          "points": 1,
     210          "id": "B"
     211        },
     212        {
     213          "text": "elseif …:",
     214          "points": 0,
     215          "id": "C"
     216        },
     217        {
     218          "text": "else:",
     219          "points": 1,
     220          "id": "D"
     221        }
     222      ]
     223    },
     224    {
     225      "domain": "PY_CNTRLSTRCT",
     226      "qtype": "single_choice",
     227      "text": "Wofür steht der Walrus-Operator (:=)?",
     228      "options": [
     229        {
     230          "text": "Zuweisung in einem Ausdruck (z. B. while (n := input()) != '')",
     231          "points": 1,
     232          "id": "A"
     233        },
     234        {
     235          "text": "Vergleich auf Gleichheit",
     236          "points": 0,
     237          "id": "B"
     238        },
     239        {
     240          "text": "Exponentiation",
     241          "points": 0,
     242          "id": "C"
     243        }
     244      ]
     245    },
     246    {
     247      "domain": "PY_FILES",
     248      "qtype": "single_choice",
     249      "text": "Wie öffnet man eine Textdatei sicher zum Lesen in UTF-8?",
     250      "options": [
     251        {
     252          "text": "open(path, 'r', encoding='utf-8')",
     253          "points": 1,
     254          "id": "A"
     255        },
     256        {
     257          "text": "open(path)",
     258          "points": 0,
     259          "id": "B"
     260        },
     261        {
     262          "text": "open('utf-8', path)",
     263          "points": 0,
     264          "id": "C"
     265        }
     266      ]
     267    },
     268    {
     269      "domain": "PY_FILES",
     270      "qtype": "multiple_choice",
     271      "text": "Welche Vorteile hat der with-Block beim Datei-I/O?",
     272      "options": [
     273        {
     274          "text": "Automatisches Schließen der Datei.",
     275          "points": 1,
     276          "id": "A"
     277        },
     278        {
     279          "text": "Weniger Risiko für Ressourcen-Leaks.",
     280          "points": 1,
     281          "id": "B"
     282        },
     283        {
     284          "text": "Er macht das Lesen schneller per se.",
     285          "points": 0,
     286          "id": "C"
     287        },
     288        {
     289          "text": "Kompakterer Code.",
     290          "points": 1,
     291          "id": "D"
     292        }
     293      ]
     294    },
     295    {
     296      "domain": "PY_FILES",
     297      "qtype": "single_choice",
     298      "text": "Welcher Modus legt eine neue Datei an und wirft Fehler, falls sie existiert?",
     299      "options": [
     300        {
     301          "text": "'x'",
     302          "points": 1,
     303          "id": "A"
     304        },
     305        {
     306          "text": "'w'",
     307          "points": 0,
     308          "id": "B"
     309        },
     310        {
     311          "text": "'a'",
     312          "points": 0,
     313          "id": "C"
     314        }
     315      ]
     316    },
     317    {
     318      "domain": "PY_FILES",
     319      "qtype": "multiple_choice",
     320      "text": "Welche Bibliotheken sind für Pfade hilfreich?",
     321      "options": [
     322        {
     323          "text": "pathlib.Path",
     324          "points": 1,
     325          "id": "A"
     326        },
     327        {
     328          "text": "os.path",
     329          "points": 1,
     330          "id": "B"
     331        },
     332        {
     333          "text": "sys.path zum Dateischreiben",
     334          "points": 0,
     335          "id": "C"
     336        },
     337        {
     338          "text": "glob für Dateimuster",
     339          "points": 1,
     340          "id": "D"
     341        }
     342      ]
     343    },
     344    {
     345      "domain": "PY_FILES",
     346      "qtype": "single_choice",
     347      "text": "Wie liest man große Dateien speicherschonend Zeile für Zeile?",
     348      "options": [
     349        {
     350          "text": "for line in f:",
     351          "points": 1,
     352          "id": "A"
     353        },
     354        {
     355          "text": "lines = f.readlines()",
     356          "points": 0,
     357          "id": "B"
     358        },
     359        {
     360          "text": "text = f.read()",
     361          "points": 0,
     362          "id": "C"
     363        }
     364      ]
     365    },
     366    {
     367      "domain": "PY_TYPES",
     368      "qtype": "single_choice",
     369      "text": "Wozu dienen Typannotationen in Python?",
     370      "options": [
     371        {
     372          "text": "Sie unterstützen Tools/IDE/Typchecker; zur Laufzeit optional.",
     373          "points": 1,
     374          "id": "A"
     375        },
     376        {
     377          "text": "Sie erzwingen strikt die Typen zur Laufzeit.",
     378          "points": 0,
     379          "id": "B"
     380        },
     381        {
     382          "text": "Sie ersetzen Docstrings.",
     383          "points": 0,
     384          "id": "C"
     385        }
     386      ]
     387    },
     388    {
     389      "domain": "PY_TYPES",
     390      "qtype": "multiple_choice",
     391      "text": "Welche sind gültige Typannotationen (Beispiele ab Python 3.9)?",
     392      "options": [
     393        {
     394          "text": "list[int]",
     395          "points": 1,
     396          "id": "A"
     397        },
     398        {
     399          "text": "dict[str, int]",
     400          "points": 1,
     401          "id": "B"
     402        },
     403        {
     404          "text": "Optional[int] bzw. int | None",
     405          "points": 1,
     406          "id": "C"
     407        },
     408        {
     409          "text": "tuple<int>",
     410          "points": 0,
     411          "id": "D"
     412        }
     413      ]
     414    },
     415    {
     416      "domain": "PY_TYPES",
     417      "qtype": "single_choice",
     418      "text": "Welche Annotation beschreibt 'Funktion erhält int und gibt bool zurück'?",
     419      "options": [
     420        {
     421          "text": "Callable[[int], bool]",
     422          "points": 1,
     423          "id": "A"
     424        },
     425        {
     426          "text": "function(int)->bool",
     427          "points": 0,
     428          "id": "B"
     429        },
     430        {
     431          "text": "call[int->bool]",
     432          "points": 0,
     433          "id": "C"
     434        }
     435      ]
     436    },
     437    {
     438      "domain": "PY_TYPES",
     439      "qtype": "multiple_choice",
     440      "text": "Welche Stdlib-Hilfen definieren einfache Datenobjekte?",
     441      "options": [
     442        {
     443          "text": "dataclasses.dataclass",
     444          "points": 1,
     445          "id": "A"
     446        },
     447        {
     448          "text": "typing.TypedDict",
     449          "points": 1,
     450          "id": "B"
     451        },
     452        {
     453          "text": "collections.namedtuple",
     454          "points": 1,
     455          "id": "C"
     456        },
     457        {
     458          "text": "functools.singledispatch",
     459          "points": 0,
     460          "id": "D"
     461        }
     462      ]
     463    },
     464    {
     465      "domain": "PY_TYPES",
     466      "qtype": "single_choice",
     467      "text": "Was bedeutet `Any`?",
     468      "options": [
     469        {
     470          "text": "Beliebiger Typ (Typchecker schränkt kaum ein).",
     471          "points": 1,
     472          "id": "A"
     473        },
     474        {
     475          "text": "Nur Ganzzahlen erlaubt.",
     476          "points": 0,
     477          "id": "B"
     478        },
     479        {
     480          "text": "Ist identisch zu `object` in jeder Hinsicht.",
     481          "points": 0,
     482          "id": "C"
     483        }
     484      ]
     485    },
     486    {
     487      "domain": "PY_STREAMS",
     488      "qtype": "single_choice",
     489      "text": "Was gibt eine Listen-Comprehension typischerweise zurück?",
     490      "options": [
     491        {
     492          "text": "Eine neue Liste",
     493          "points": 1,
     494          "id": "A"
     495        },
     496        {
     497          "text": "Einen Generator",
     498          "points": 0,
     499          "id": "B"
     500        },
     501        {
     502          "text": "Ein Tupel",
     503          "points": 0,
     504          "id": "C"
     505        }
     506      ]
     507    },
     508    {
     509      "domain": "PY_STREAMS",
     510      "qtype": "multiple_choice",
     511      "text": "Welche sind Lazy-Konstrukte?",
     512      "options": [
     513        {
     514          "text": "(x for x in seq)  (Generator-Expression)",
     515          "points": 1,
     516          "id": "A"
     517        },
     518        {
     519          "text": "map(func, seq)",
     520          "points": 1,
     521          "id": "B"
     522        },
     523        {
     524          "text": "filter(func, seq)",
     525          "points": 1,
     526          "id": "C"
     527        },
     528        {
     529          "text": "[x for x in seq]",
     530          "points": 0,
     531          "id": "D"
     532        }
     533      ]
     534    },
     535    {
     536      "domain": "PY_STREAMS",
     537      "qtype": "single_choice",
     538      "text": "Wofür steht `yield` in einer Funktion?",
     539      "options": [
     540        {
     541          "text": "Es macht die Funktion zu einem Generator (liefert Werte schrittweise).",
     542          "points": 1,
     543          "id": "A"
     544        },
     545        {
     546          "text": "Es beendet das Programm.",
     547          "points": 0,
     548          "id": "B"
     549        },
     550        {
     551          "text": "Es importiert ein Modul.",
     552          "points": 0,
     553          "id": "C"
     554        }
     555      ]
     556    },
     557    {
     558      "domain": "PY_STREAMS",
     559      "qtype": "multiple_choice",
     560      "text": "Welche Tools sind nützlich für Iterables?",
     561      "options": [
     562        {
     563          "text": "itertools (z. B. chain, islice)",
     564          "points": 1,
     565          "id": "A"
     566        },
     567        {
     568          "text": "enumerate",
     569          "points": 1,
     570          "id": "B"
     571        },
     572        {
     573          "text": "re (Regex) als Iteratorersatz",
     574          "points": 0,
     575          "id": "C"
     576        },
     577        {
     578          "text": "sum zur Konkatenation von Listen",
     579          "points": 0,
     580          "id": "D"
     581        }
     582      ]
     583    },
     584    {
     585      "domain": "PY_STREAMS",
     586      "qtype": "single_choice",
     587      "text": "Welcher Ausdruck ist am speicherschonendsten?",
     588      "options": [
     589        {
     590          "text": "sum(x for x in range(1_000_000))",
     591          "points": 1,
     592          "id": "A"
     593        },
     594        {
     595          "text": "sum([x for x in range(1_000_000)])",
     596          "points": 0,
     597          "id": "B"
     598        },
     599        {
     600          "text": "list(range(1_000_000))",
     601          "points": 0,
     602          "id": "C"
     603        }
     604      ]
     605    },
     606    {
     607      "domain": "PY_FUNCTOOLS",
     608      "qtype": "single_choice",
     609      "text": "Wozu dient `functools.lru_cache`?",
     610      "options": [
     611        {
     612          "text": "Ergebnisse teurer Funktionsaufrufe zwischenspeichern.",
     613          "points": 1,
     614          "id": "A"
     615        },
     616        {
     617          "text": "Dateien schneller lesen.",
     618          "points": 0,
     619          "id": "B"
     620        },
     621        {
     622          "text": "Asynchronität bereitstellen.",
     623          "points": 0,
     624          "id": "C"
     625        }
     626      ]
     627    },
     628    {
     629      "domain": "PY_FUNCTOOLS",
     630      "qtype": "multiple_choice",
     631      "text": "Welche Aussagen zu `partial` stimmen?",
     632      "options": [
     633        {
     634          "text": "`partial` bindet einige Argumente vor.",
     635          "points": 1,
     636          "id": "A"
     637        },
     638        {
     639          "text": "Die neue Funktion verhält sich wie die ursprüngliche mit den gebundenen Werten.",
     640          "points": 1,
     641          "id": "B"
     642        },
     643        {
     644          "text": "`partial` ersetzt kwargs vollständig.",
     645          "points": 0,
     646          "id": "C"
     647        },
     648        {
     649          "text": "`partial` ist ein Decorator für Klassen.",
     650          "points": 0,
     651          "id": "D"
     652        }
     653      ]
     654    },
     655    {
     656      "domain": "PY_FUNCTOOLS",
     657      "qtype": "single_choice",
     658      "text": "Was macht `@wraps` beim Schreiben eigener Decorators?",
     659      "options": [
     660        {
     661          "text": "Überträgt Metadaten (Name, Docstring) auf die Wrapper-Funktion.",
     662          "points": 1,
     663          "id": "A"
     664        },
     665        {
     666          "text": "Beschleunigt die Funktion automatisch.",
     667          "points": 0,
     668          "id": "B"
     669        },
     670        {
     671          "text": "Erstellt eine Klasse.",
     672          "points": 0,
     673          "id": "C"
     674        }
     675      ]
     676    },
     677    {
     678      "domain": "PY_FUNCTOOLS",
     679      "qtype": "multiple_choice",
     680      "text": "Welche Aussagen zu `singledispatch` sind korrekt?",
     681      "options": [
     682        {
     683          "text": "Erzeugt generische Funktionen je nach Typ des ersten Arguments.",
     684          "points": 1,
     685          "id": "A"
     686        },
     687        {
     688          "text": "Weitere Varianten werden mit @<func>.register definiert.",
     689          "points": 1,
     690          "id": "B"
     691        },
     692        {
     693          "text": "Funktioniert nicht mit Klassenmethoden/Methoden-Workarounds.",
     694          "points": 0,
     695          "id": "C"
     696        },
     697        {
     698          "text": "Ist Teil der Stdlib.",
     699          "points": 1,
     700          "id": "D"
     701        }
     702      ]
     703    },
     704    {
     705      "domain": "PY_FUNCTOOLS",
     706      "qtype": "single_choice",
     707      "text": "Wie leert man den Cache einer mit `lru_cache` dekorierten Funktion?",
     708      "options": [
     709        {
     710          "text": "funktion.cache_clear()",
     711          "points": 1,
     712          "id": "A"
     713        },
     714        {
     715          "text": "del funktion",
     716          "points": 0,
     717          "id": "B"
     718        },
     719        {
     720          "text": "Cache leert sich automatisch bei Programmstart nicht",
     721          "points": 0,
     722          "id": "C"
     723        }
     724      ]
     725    },
     726    {
     727      "domain": "PY_COLLECTIONS",
     728      "qtype": "single_choice",
     729      "text": "Welche Struktur eignet sich für schnelle Anhänge am Ende?",
     730      "options": [
     731        {
     732          "text": "list.append",
     733          "points": 1,
     734          "id": "A"
     735        },
     736        {
     737          "text": "tuple +=",
     738          "points": 0,
     739          "id": "B"
     740        },
     741        {
     742          "text": "str +=",
     743          "points": 0,
     744          "id": "C"
     745        }
     746      ]
     747    },
     748    {
     749      "domain": "PY_COLLECTIONS",
     750      "qtype": "multiple_choice",
     751      "text": "Welche Aussagen zu Mengen (set) sind korrekt?",
     752      "options": [
     753        {
     754          "text": "Speichern einzigartige, hashbare Elemente.",
     755          "points": 1,
     756          "id": "A"
     757        },
     758        {
     759          "text": "Unterstützen Vereinigungs-/Schnittmengen-Operationen.",
     760          "points": 1,
     761          "id": "B"
     762        },
     763        {
     764          "text": "Erhalten die Einfügereihenfolge garantiert vor 3.7.",
     765          "points": 0,
     766          "id": "C"
     767        },
     768        {
     769          "text": "Doppelte Einträge bleiben erhalten.",
     770          "points": 0,
     771          "id": "D"
     772        }
     773      ]
     774    },
     775    {
     776      "domain": "PY_COLLECTIONS",
     777      "qtype": "single_choice",
     778      "text": "Wofür ist `collections.Counter` gut?",
     779      "options": [
     780        {
     781          "text": "Häufigkeiten von Elementen zählen.",
     782          "points": 1,
     783          "id": "A"
     784        },
     785        {
     786          "text": "Dateien öffnen.",
     787          "points": 0,
     788          "id": "B"
     789        },
     790        {
     791          "text": "Zahlen runden.",
     792          "points": 0,
     793          "id": "C"
     794        }
     795      ]
     796    },
     797    {
     798      "domain": "PY_COLLECTIONS",
     799      "qtype": "multiple_choice",
     800      "text": "Welche Datenstrukturen sind unveränderlich (immutable)?",
     801      "options": [
     802        {
     803          "text": "tuple",
     804          "points": 1,
     805          "id": "A"
     806        },
     807        {
     808          "text": "frozenset",
     809          "points": 1,
     810          "id": "B"
     811        },
     812        {
     813          "text": "list",
     814          "points": 0,
     815          "id": "C"
     816        },
     817        {
     818          "text": "dict",
     819          "points": 0,
     820          "id": "D"
     821        }
     822      ]
     823    },
     824    {
     825      "domain": "PY_COLLECTIONS",
     826      "qtype": "single_choice",
     827      "text": "Was bewirkt `defaultdict(list)`?",
     828      "options": [
     829        {
     830          "text": "Fehlende Schlüssel bekommen automatisch eine leere Liste.",
     831          "points": 1,
     832          "id": "A"
     833        },
     834        {
     835          "text": "Fehlende Schlüssel werfen KeyError.",
     836          "points": 0,
     837          "id": "B"
     838        },
     839        {
     840          "text": "Es ist identisch zu dict.",
     841          "points": 0,
     842          "id": "C"
     843        }
     844      ]
     845    },
     846    {
     847      "domain": "PY_DATETIME",
     848      "qtype": "single_choice",
     849      "text": "Welche Bibliothek liefert Zeitzonen ohne Zusatzpakete (ab 3.9)?",
     850      "options": [
     851        {
     852          "text": "zoneinfo",
     853          "points": 1,
     854          "id": "A"
     855        },
     856        {
     857          "text": "pytz",
     858          "points": 0,
     859          "id": "B"
     860        },
     861        {
     862          "text": "tzlocal",
     863          "points": 0,
     864          "id": "C"
     865        }
     866      ]
     867    },
     868    {
     869      "domain": "PY_DATETIME",
     870      "qtype": "multiple_choice",
     871      "text": "Welche Aussagen zu datetime sind korrekt?",
     872      "options": [
     873        {
     874          "text": "Naive datetimes haben keine Zeitzone.",
     875          "points": 1,
     876          "id": "A"
     877        },
     878        {
     879          "text": "Aware datetimes tragen eine tzinfo.",
     880          "points": 1,
     881          "id": "B"
     882        },
     883        {
     884          "text": "UTC ist eine sinnvolle interne Normalform.",
     885          "points": 1,
     886          "id": "C"
     887        },
     888        {
     889          "text": "time.time() ist immer monoton.",
     890          "points": 0,
     891          "id": "D"
     892        }
     893      ]
     894    },
     895    {
     896      "domain": "PY_DATETIME",
     897      "qtype": "single_choice",
     898      "text": "Wie misst man kurze Zeitdauern zuverlässig?",
     899      "options": [
     900        {
     901          "text": "time.perf_counter()",
     902          "points": 1,
     903          "id": "A"
     904        },
     905        {
     906          "text": "datetime.now()",
     907          "points": 0,
     908          "id": "B"
     909        },
     910        {
     911          "text": "time.sleep()",
     912          "points": 0,
     913          "id": "C"
     914        }
     915      ]
     916    },
     917    {
     918      "domain": "PY_DATETIME",
     919      "qtype": "multiple_choice",
     920      "text": "Welche Formate sind für Logs/Datenaustausch robust?",
     921      "options": [
     922        {
     923          "text": "ISO-8601 (z. B. 2025-10-20T15:00:00Z)",
     924          "points": 1,
     925          "id": "A"
     926        },
     927        {
     928          "text": "Ortsabhängige Datumsstrings",
     929          "points": 0,
     930          "id": "B"
     931        },
     932        {
     933          "text": "Unix-Timestamps (Sekunden seit Epoch)",
     934          "points": 1,
     935          "id": "C"
     936        },
     937        {
     938          "text": "Freitext ohne Norm",
     939          "points": 0,
     940          "id": "D"
     941        }
     942      ]
     943    },
     944    {
     945      "domain": "PY_DATETIME",
     946      "qtype": "single_choice",
     947      "text": "Wie erstellt man 'jetzt in UTC' als aware datetime?",
     948      "options": [
     949        {
     950          "text": "datetime.now(timezone.utc)",
     951          "points": 1,
     952          "id": "A"
     953        },
     954        {
     955          "text": "datetime.utcnow()",
     956          "points": 0,
     957          "id": "B"
     958        },
     959        {
     960          "text": "datetime.now()",
     961          "points": 0,
     962          "id": "C"
     963        }
     964      ]
     965    },
     966    {
     967      "domain": "PY_NAMESPACES",
     968      "qtype": "single_choice",
     969      "text": "Wofür steht LEGB?",
     970      "options": [
     971        {
     972          "text": "Local, Enclosing, Global, Builtins (Namensauflösung)",
     973          "points": 1,
     974          "id": "A"
     975        },
     976        {
     977          "text": "Library, Env, Git, Bytecode",
     978          "points": 0,
     979          "id": "B"
     980        },
     981        {
     982          "text": "List, Eval, Generator, Bytes",
     983          "points": 0,
     984          "id": "C"
     985        }
     986      ]
     987    },
     988    {
     989      "domain": "PY_NAMESPACES",
     990      "qtype": "multiple_choice",
     991      "text": "Welche Aussagen zu `global`/`nonlocal` sind korrekt?",
     992      "options": [
     993        {
     994          "text": "`global` bezieht sich auf das Modul-Namespace.",
     995          "points": 1,
     996          "id": "A"
     997        },
     998        {
     999          "text": "`nonlocal` greift auf die nächst-äußere Funktionsvariable zu.",
     1000          "points": 1,
     1001          "id": "B"
     1002        },
     1003        {
     1004          "text": "`nonlocal` kann globale Variablen binden.",
     1005          "points": 0,
     1006          "id": "C"
     1007        },
     1008        {
     1009          "text": "`global` wirkt nur in Klassenmethoden.",
     1010          "points": 0,
     1011          "id": "D"
     1012        }
     1013      ]
     1014    },
     1015    {
     1016      "domain": "PY_NAMESPACES",
     1017      "qtype": "single_choice",
     1018      "text": "Wie markiert man interne Modulnamen konventionell?",
     1019      "options": [
     1020        {
     1021          "text": "Mit führendem Unterstrich: _name",
     1022          "points": 1,
     1023          "id": "A"
     1024        },
     1025        {
     1026          "text": "Mit Großschreibung",
     1027          "points": 0,
     1028          "id": "B"
     1029        },
     1030        {
     1031          "text": "Mit @internal",
     1032          "points": 0,
     1033          "id": "C"
     1034        }
     1035      ]
     1036    },
     1037    {
     1038      "domain": "PY_NAMESPACES",
     1039      "qtype": "multiple_choice",
     1040      "text": "Welche sind gute Praktiken, um Namenskonflikte zu vermeiden?",
     1041      "options": [
     1042        {
     1043          "text": "Aussagekräftige Modul-/Paketnamen",
     1044          "points": 1,
     1045          "id": "A"
     1046        },
     1047        {
     1048          "text": "Kein `from x import *`",
     1049          "points": 1,
     1050          "id": "B"
     1051        },
     1052        {
     1053          "text": "Alles in eine Datei schreiben",
     1054          "points": 0,
     1055          "id": "C"
     1056        },
     1057        {
     1058          "text": "Konstante Namen in UPPER_CASE",
     1059          "points": 1,
     1060          "id": "D"
     1061        }
     1062      ]
     1063    },
     1064    {
     1065      "domain": "PY_NAMESPACES",
     1066      "qtype": "single_choice",
     1067      "text": "Wozu dient `__all__`?",
     1068      "options": [
     1069        {
     1070          "text": "Steuert Exporte bei `from mod import *`.",
     1071          "points": 1,
     1072          "id": "A"
     1073        },
     1074        {
     1075          "text": "Definiert die Versionsnummer.",
     1076          "points": 0,
     1077          "id": "B"
     1078        },
     1079        {
     1080          "text": "Erzeugt automatisch Docstrings.",
     1081          "points": 0,
     1082          "id": "C"
     1083        }
     1084      ]
     1085    },
     1086    {
     1087      "domain": "PY_RECURSION",
     1088      "qtype": "single_choice",
     1089      "text": "Warum ist tiefe Rekursion in Python oft problematisch?",
     1090      "options": [
     1091        {
     1092          "text": "Begrenzter Call-Stack; keine Tail-Call-Optimierung.",
     1093          "points": 1,
     1094          "id": "A"
     1095        },
     1096        {
     1097          "text": "Python erlaubt Rekursion gar nicht.",
     1098          "points": 0,
     1099          "id": "B"
     1100        },
     1101        {
     1102          "text": "Listen unterstützen keine Rekursion.",
     1103          "points": 0,
     1104          "id": "C"
     1105        }
     1106      ]
     1107    },
     1108    {
     1109      "domain": "PY_RECURSION",
     1110      "qtype": "multiple_choice",
     1111      "text": "Wie kann man eine rekursive Lösung vereinfachen?",
     1112      "options": [
     1113        {
     1114          "text": "Iterative Variante mit explizitem Stack/Queue.",
     1115          "points": 1,
     1116          "id": "A"
     1117        },
     1118        {
     1119          "text": "Memoization bei sich wiederholenden Teilproblemen.",
     1120          "points": 1,
     1121          "id": "B"
     1122        },
     1123        {
     1124          "text": "Zufällige Abbrüche einbauen.",
     1125          "points": 0,
     1126          "id": "C"
     1127        },
     1128        {
     1129          "text": "Auf unendliche Rekursion hoffen.",
     1130          "points": 0,
     1131          "id": "D"
     1132        }
     1133      ]
     1134    },
     1135    {
     1136      "domain": "PY_RECURSION",
     1137      "qtype": "single_choice",
     1138      "text": "Welche Variante ist für Fibonacci(n) effizienter?",
     1139      "options": [
     1140        {
     1141          "text": "Iterativ oder mit lru_cache.",
     1142          "points": 1,
     1143          "id": "A"
     1144        },
     1145        {
     1146          "text": "Naiv rekursiv ohne Cache.",
     1147          "points": 0,
     1148          "id": "B"
     1149        },
     1150        {
     1151          "text": "Mit eval auf Zeichenketten.",
     1152          "points": 0,
     1153          "id": "C"
     1154        }
     1155      ]
     1156    },
     1157    {
     1158      "domain": "PY_RECURSION",
     1159      "qtype": "multiple_choice",
     1160      "text": "Welche Probleme treten bei Graph-Rekursion auf?",
     1161      "options": [
     1162        {
     1163          "text": "Zyklen → unendliche Rekursion ohne Visited-Set.",
     1164          "points": 1,
     1165          "id": "A"
     1166        },
     1167        {
     1168          "text": "Große Tiefe → RecursionError.",
     1169          "points": 1,
     1170          "id": "B"
     1171        },
     1172        {
     1173          "text": "Listen sind nicht iterierbar.",
     1174          "points": 0,
     1175          "id": "C"
     1176        },
     1177        {
     1178          "text": "Rekursion verbraucht keinen Stack.",
     1179          "points": 0,
     1180          "id": "D"
     1181        }
     1182      ]
     1183    },
     1184    {
     1185      "domain": "PY_RECURSION",
     1186      "qtype": "single_choice",
     1187      "text": "Wie traversiert man einen Baum iterativ (DFS)?",
     1188      "options": [
     1189        {
     1190          "text": "Mit explizitem Stack (list) und Schleife.",
     1191          "points": 1,
     1192          "id": "A"
     1193        },
     1194        {
     1195          "text": "Mit globaler Variablen.",
     1196          "points": 0,
     1197          "id": "B"
     1198        },
     1199        {
     1200          "text": "Gar nicht möglich.",
     1201          "points": 0,
     1202          "id": "C"
     1203        }
     1204      ]
     1205    },
     1206    {
     1207      "domain": "PY_MODULES",
     1208      "qtype": "single_choice",
     1209      "text": "Was macht Code auf Modul-Top-Level beim Import?",
     1210      "options": [
     1211        {
     1212          "text": "Er wird genau einmal ausgeführt (Initialisierung).",
     1213          "points": 1,
     1214          "id": "A"
     1215        },
     1216        {
     1217          "text": "Er wird jedes Mal neu ausgeführt.",
     1218          "points": 0,
     1219          "id": "B"
     1220        },
     1221        {
     1222          "text": "Er wird ignoriert.",
     1223          "points": 0,
     1224          "id": "C"
     1225        }
     1226      ]
     1227    },
     1228    {
     1229      "domain": "PY_MODULES",
     1230      "qtype": "multiple_choice",
     1231      "text": "Welche Aussagen zu Imports sind korrekt?",
     1232      "options": [
     1233        {
     1234          "text": "Module werden in sys.modules gecacht.",
     1235          "points": 1,
     1236          "id": "A"
     1237        },
     1238        {
     1239          "text": "Relative Importe funktionieren nur innerhalb von Paketen.",
     1240          "points": 1,
     1241          "id": "B"
     1242        },
     1243        {
     1244          "text": "`if __name__ == '__main__':` trennt Skript-Start vom Import.",
     1245          "points": 1,
     1246          "id": "C"
     1247        },
     1248        {
     1249          "text": "`from x import *` ist immer empfohlen.",
     1250          "points": 0,
     1251          "id": "D"
     1252        }
     1253      ]
     1254    },
     1255    {
     1256      "domain": "PY_MODULES",
     1257      "qtype": "single_choice",
     1258      "text": "Wie bindet man einen Paket-Logger korrekt?",
     1259      "options": [
     1260        {
     1261          "text": "logging.getLogger(__name__)",
     1262          "points": 1,
     1263          "id": "A"
     1264        },
     1265        {
     1266          "text": "print statt Logging",
     1267          "points": 0,
     1268          "id": "B"
     1269        },
     1270        {
     1271          "text": "logging.getLogger()",
     1272          "points": 0,
     1273          "id": "C"
     1274        }
     1275      ]
     1276    },
     1277    {
     1278      "domain": "PY_MODULES",
     1279      "qtype": "multiple_choice",
     1280      "text": "Welche Werkzeuge sind für Paket-Ressourcen nützlich?",
     1281      "options": [
     1282        {
     1283          "text": "importlib.resources",
     1284          "points": 1,
     1285          "id": "A"
     1286        },
     1287        {
     1288          "text": "pkgutil",
     1289          "points": 1,
     1290          "id": "B"
     1291        },
     1292        {
     1293          "text": "random.resources",
     1294          "points": 0,
     1295          "id": "C"
     1296        },
     1297        {
     1298          "text": "sys.path zum Lesen von Dateien",
     1299          "points": 0,
     1300          "id": "D"
     1301        }
     1302      ]
     1303    },
     1304    {
     1305      "domain": "PY_MODULES",
     1306      "qtype": "single_choice",
     1307      "text": "Wie führt man ein Modul als Skript aus?",
     1308      "options": [
     1309        {
     1310          "text": "python -m paket.modul",
     1311          "points": 1,
     1312          "id": "A"
     1313        },
     1314        {
     1315          "text": "python paket/modul",
     1316          "points": 0,
     1317          "id": "B"
     1318        },
     1319        {
     1320          "text": "run paket.modul",
     1321          "points": 0,
     1322          "id": "C"
     1323        }
     1324      ]
     1325    },
     1326    {
     1327      "domain": "PY_OOP",
     1328      "qtype": "single_choice",
     1329      "text": "Wofür steht `@property`?",
     1330      "options": [
     1331        {
     1332          "text": "Liest/erzeugt berechnete Attribute wie ein Getter.",
     1333          "points": 1,
     1334          "id": "A"
     1335        },
     1336        {
     1337          "text": "Markiert eine Klassenmethode.",
     1338          "points": 0,
     1339          "id": "B"
     1340        },
     1341        {
     1342          "text": "Erstellt automatisch __init__.",
     1343          "points": 0,
     1344          "id": "C"
     1345        }
     1346      ]
     1347    },
     1348    {
     1349      "domain": "PY_OOP",
     1350      "qtype": "multiple_choice",
     1351      "text": "Welche Aussagen zu Klassenmethoden/staticmethods stimmen?",
     1352      "options": [
     1353        {
     1354          "text": "@classmethod erhält die Klasse als erstes Argument (cls).",
     1355          "points": 1,
     1356          "id": "A"
     1357        },
     1358        {
     1359          "text": "@staticmethod erhält weder self noch cls.",
     1360          "points": 1,
     1361          "id": "B"
     1362        },
     1363        {
     1364          "text": "@classmethod und @staticmethod sind identisch.",
     1365          "points": 0,
     1366          "id": "C"
     1367        },
     1368        {
     1369          "text": "Beide können auf der Klasse aufgerufen werden.",
     1370          "points": 1,
     1371          "id": "D"
     1372        }
     1373      ]
     1374    },
     1375    {
     1376      "domain": "PY_OOP",
     1377      "qtype": "single_choice",
     1378      "text": "Wie vergleicht man zwei Instanzen auf Wertgleichheit korrekt?",
     1379      "options": [
     1380        {
     1381          "text": "__eq__ implementieren und ggf. __hash__ bei Immutables.",
     1382          "points": 1,
     1383          "id": "A"
     1384        },
     1385        {
     1386          "text": "Nur __repr__ implementieren.",
     1387          "points": 0,
     1388          "id": "B"
     1389        },
     1390        {
     1391          "text": "Nur __hash__ implementieren.",
     1392          "points": 0,
     1393          "id": "C"
     1394        }
     1395      ]
     1396    },
     1397    {
     1398      "domain": "PY_OOP",
     1399      "qtype": "multiple_choice",
     1400      "text": "Welche Methoden sind für Containerklassen nützlich?",
     1401      "options": [
     1402        {
     1403          "text": "__len__",
     1404          "points": 1,
     1405          "id": "A"
     1406        },
     1407        {
     1408          "text": "__iter__",
     1409          "points": 1,
     1410          "id": "B"
     1411        },
     1412        {
     1413          "text": "__getitem__",
     1414          "points": 1,
     1415          "id": "C"
     1416        },
     1417        {
     1418          "text": "__cmp__",
     1419          "points": 0,
     1420          "id": "D"
     1421        }
     1422      ]
     1423    },
     1424    {
     1425      "domain": "PY_OOP",
     1426      "qtype": "single_choice",
     1427      "text": "Welche Basisklasse nutzt man für abstrakte Klassen?",
     1428      "options": [
     1429        {
     1430          "text": "abc.ABC",
     1431          "points": 1,
     1432          "id": "A"
     1433        },
     1434        {
     1435          "text": "typing.Any",
     1436          "points": 0,
     1437          "id": "B"
     1438        },
     1439        {
     1440          "text": "objectonly",
     1441          "points": 0,
     1442          "id": "C"
     1443        }
     1444      ]
     1445    },
     1446    {
     1447      "domain": "PY_EXCEPTIONS",
     1448      "qtype": "single_choice",
     1449      "text": "Wie fängt man eine konkrete Exception?",
     1450      "options": [
     1451        {
     1452          "text": "try: ... except ValueError:",
     1453          "points": 1,
     1454          "id": "A"
     1455        },
     1456        {
     1457          "text": "try: ... catch ValueError:",
     1458          "points": 0,
     1459          "id": "B"
     1460        },
     1461        {
     1462          "text": "except(ValueError): ... ohne try",
     1463          "points": 0,
     1464          "id": "C"
     1465        }
     1466      ]
     1467    },
     1468    {
     1469      "domain": "PY_EXCEPTIONS",
     1470      "qtype": "multiple_choice",
     1471      "text": "Welche Blöcke kann ein try-Statement enthalten?",
     1472      "options": [
     1473        {
     1474          "text": "except",
     1475          "points": 1,
     1476          "id": "A"
     1477        },
     1478        {
     1479          "text": "else",
     1480          "points": 1,
     1481          "id": "B"
     1482        },
     1483        {
     1484          "text": "finally",
     1485          "points": 1,
     1486          "id": "C"
     1487        },
     1488        {
     1489          "text": "then",
     1490          "points": 0,
     1491          "id": "D"
     1492        }
     1493      ]
     1494    },
     1495    {
     1496      "domain": "PY_EXCEPTIONS",
     1497      "qtype": "single_choice",
     1498      "text": "Wie wirft man dieselbe Exception im except-Block erneut?",
     1499      "options": [
     1500        {
     1501          "text": "raise  (ohne Argumente)",
     1502          "points": 1,
     1503          "id": "A"
     1504        },
     1505        {
     1506          "text": "return e",
     1507          "points": 0,
     1508          "id": "B"
     1509        },
     1510        {
     1511          "text": "throw e",
     1512          "points": 0,
     1513          "id": "C"
     1514        }
     1515      ]
     1516    },
     1517    {
     1518      "domain": "PY_EXCEPTIONS",
     1519      "qtype": "multiple_choice",
     1520      "text": "Welche sind gute Praktiken beim Fehler-Handling?",
     1521      "options": [
     1522        {
     1523          "text": "Nur spezifische Exceptions abfangen.",
     1524          "points": 1,
     1525          "id": "A"
     1526        },
     1527        {
     1528          "text": "Kontext mit `raise ... from e` erhalten.",
     1529          "points": 1,
     1530          "id": "B"
     1531        },
     1532        {
     1533          "text": "Bare `except:` nur in seltenen Ausnahmefällen.",
     1534          "points": 1,
     1535          "id": "C"
     1536        },
     1537        {
     1538          "text": "Alle Fehler still ignorieren.",
     1539          "points": 0,
     1540          "id": "D"
     1541        }
     1542      ]
     1543    },
     1544    {
     1545      "domain": "PY_EXCEPTIONS",
     1546      "qtype": "single_choice",
     1547      "text": "Welche Ausnahmen sind KEINE Unterklassen von Exception?",
     1548      "options": [
     1549        {
     1550          "text": "SystemExit/KeyboardInterrupt/GeneratorExit",
     1551          "points": 1,
     1552          "id": "A"
     1553        },
     1554        {
     1555          "text": "RuntimeError",
     1556          "points": 0,
     1557          "id": "B"
     1558        },
     1559        {
     1560          "text": "ValueError",
     1561          "points": 0,
     1562          "id": "C"
     1563        }
     1564      ]
     1565    },
     1566    {
     1567      "domain": "PY_CNTXTMNGR",
     1568      "qtype": "single_choice",
     1569      "text": "Wozu dient ein Context Manager (with)?",
     1570      "options": [
     1571        {
     1572          "text": "Ressourcen sicher öffnen/schließen (z. B. Dateien).",
     1573          "points": 1,
     1574          "id": "A"
     1575        },
     1576        {
     1577          "text": "Nur fürs Logging.",
     1578          "points": 0,
     1579          "id": "B"
     1580        },
     1581        {
     1582          "text": "Nur in Tests nutzbar.",
     1583          "points": 0,
     1584          "id": "C"
     1585        }
     1586      ]
     1587    },
     1588    {
     1589      "domain": "PY_CNTXTMNGR",
     1590      "qtype": "multiple_choice",
     1591      "text": "Welche bereitgestellten Kontexte sind nützlich?",
     1592      "options": [
     1593        {
     1594          "text": "contextlib.suppress",
     1595          "points": 1,
     1596          "id": "A"
     1597        },
     1598        {
     1599          "text": "contextlib.redirect_stdout",
     1600          "points": 1,
     1601          "id": "B"
     1602        },
     1603        {
     1604          "text": "contextlib.ExitStack",
     1605          "points": 1,
     1606          "id": "C"
     1607        },
     1608        {
     1609          "text": "contextlib.magic",
     1610          "points": 0,
     1611          "id": "D"
     1612        }
     1613      ]
     1614    },
     1615    {
     1616      "domain": "PY_CNTXTMNGR",
     1617      "qtype": "single_choice",
     1618      "text": "Welche Methoden implementiert ein eigener Context-Manager (Klasse)?",
     1619      "options": [
     1620        {
     1621          "text": "__enter__ und __exit__",
     1622          "points": 1,
     1623          "id": "A"
     1624        },
     1625        {
     1626          "text": "__open__ und __close__",
     1627          "points": 0,
     1628          "id": "B"
     1629        },
     1630        {
     1631          "text": "__start__ und __stop__",
     1632          "points": 0,
     1633          "id": "C"
     1634        }
     1635      ]
     1636    },
     1637    {
     1638      "domain": "PY_CNTXTMNGR",
     1639      "qtype": "multiple_choice",
     1640      "text": "Welche Aussage zu `__exit__(exc_type, exc, tb)` stimmt?",
     1641      "options": [
     1642        {
     1643          "text": "True zurückgeben unterdrückt die Exception.",
     1644          "points": 1,
     1645          "id": "A"
     1646        },
     1647        {
     1648          "text": "False/None propagiert die Exception weiter.",
     1649          "points": 1,
     1650          "id": "B"
     1651        },
     1652        {
     1653          "text": "Die Parameter sind immer None, auch bei Fehlern.",
     1654          "points": 0,
     1655          "id": "C"
     1656        },
     1657        {
     1658          "text": "__exit__ wird nicht aufgerufen, wenn ein Fehler auftritt.",
     1659          "points": 0,
     1660          "id": "D"
     1661        }
     1662      ]
     1663    },
     1664    {
     1665      "domain": "PY_CNTXTMNGR",
     1666      "qtype": "single_choice",
     1667      "text": "Wie kombiniert man mehrere Kontexte flexibel?",
     1668      "options": [
     1669        {
     1670          "text": "Mit contextlib.ExitStack()",
     1671          "points": 1,
     1672          "id": "A"
     1673        },
     1674        {
     1675          "text": "Gar nicht möglich.",
     1676          "points": 0,
     1677          "id": "B"
     1678        },
     1679        {
     1680          "text": "Mit globalen Variablen.",
     1681          "points": 0,
     1682          "id": "C"
     1683        }
     1684      ]
     1685    },
     1686    {
     1687      "domain": "PY_REGEXP",
     1688      "qtype": "single_choice",
     1689      "text": "Welche Stdlib nutzt man für reguläre Ausdrücke?",
     1690      "options": [
     1691        {
     1692          "text": "re",
     1693          "points": 1,
     1694          "id": "A"
     1695        },
     1696        {
     1697          "text": "regexlib",
     1698          "points": 0,
     1699          "id": "B"
     1700        },
     1701        {
     1702          "text": "rx",
     1703          "points": 0,
     1704          "id": "C"
     1705        }
     1706      ]
     1707    },
     1708    {
     1709      "domain": "PY_REGEXP",
     1710      "qtype": "multiple_choice",
     1711      "text": "Welche Konstrukte gehören zur re-Syntax?",
     1712      "options": [
     1713        {
     1714          "text": "Gruppen: (...)",
     1715          "points": 1,
     1716          "id": "A"
     1717        },
     1718        {
     1719          "text": "Benannte Gruppen: (?P<name>...)",
     1720          "points": 1,
     1721          "id": "B"
     1722        },
     1723        {
     1724          "text": "Quantifizierer: *, +, ?",
     1725          "points": 1,
     1726          "id": "C"
     1727        },
     1728        {
     1729          "text": "Operator := für Teilmuster",
     1730          "points": 0,
     1731          "id": "D"
     1732        }
     1733      ]
     1734    },
     1735    {
     1736      "domain": "PY_REGEXP",
     1737      "qtype": "single_choice",
     1738      "text": "Wie findet man alle nicht-überlappenden Treffer in einem Text?",
     1739      "options": [
     1740        {
     1741          "text": "re.findall(pattern, text)",
     1742          "points": 1,
     1743          "id": "A"
     1744        },
     1745        {
     1746          "text": "re.match überall aufrufen",
     1747          "points": 0,
     1748          "id": "B"
     1749        },
     1750        {
     1751          "text": "re.compile(pattern).group()",
     1752          "points": 0,
     1753          "id": "C"
     1754        }
     1755      ]
     1756    },
     1757    {
     1758      "domain": "PY_REGEXP",
     1759      "qtype": "multiple_choice",
     1760      "text": "Welche Flags sind korrekt zugeordnet?",
     1761      "options": [
     1762        {
     1763          "text": "re.IGNORECASE: Groß/Kleinschreibung ignorieren",
     1764          "points": 1,
     1765          "id": "A"
     1766        },
     1767        {
     1768          "text": "re.MULTILINE: ^ und $ auf Zeilen anwenden",
     1769          "points": 1,
     1770          "id": "B"
     1771        },
     1772        {
     1773          "text": "re.DOTALL: Punkt matcht auch Newlines",
     1774          "points": 1,
     1775          "id": "C"
     1776        },
     1777        {
     1778          "text": "re.GLOBAL: existiert in re",
     1779          "points": 0,
     1780          "id": "D"
     1781        }
     1782      ]
     1783    },
     1784    {
     1785      "domain": "PY_REGEXP",
     1786      "qtype": "single_choice",
     1787      "text": "Wie ersetzt man Texte mit einer Funktion je Match?",
     1788      "options": [
     1789        {
     1790          "text": "re.sub(pattern, func, text)",
     1791          "points": 1,
     1792          "id": "A"
     1793        },
     1794        {
     1795          "text": "re.replace(pattern, func, text)",
     1796          "points": 0,
     1797          "id": "B"
     1798        },
     1799        {
     1800          "text": "text.replace_re(func)",
     1801          "points": 0,
     1802          "id": "C"
     1803        }
     1804      ]
     1805    },
     1806    {
     1807      "domain": "PY_PARALLEL",
     1808      "qtype": "single_choice",
     1809      "text": "Wofür eignen sich Threads in CPython besonders?",
     1810      "options": [
     1811        {
     1812          "text": "I/O-gebundene Aufgaben (z. B. Netzwerk, Dateien).",
     1813          "points": 1,
     1814          "id": "A"
     1815        },
     1816        {
     1817          "text": "Schwere CPU-Berechnungen parallelisieren.",
     1818          "points": 0,
     1819          "id": "B"
     1820        },
     1821        {
     1822          "text": "Ohne Synchronisation immer korrekt.",
     1823          "points": 0,
     1824          "id": "C"
     1825        }
     1826      ]
     1827    },
     1828    {
     1829      "domain": "PY_PARALLEL",
     1830      "qtype": "multiple_choice",
     1831      "text": "Welche Bibliotheken gehören zur Stdlib für Parallelität/Konkurrenz?",
     1832      "options": [
     1833        {
     1834          "text": "threading",
     1835          "points": 1,
     1836          "id": "A"
     1837        },
     1838        {
     1839          "text": "multiprocessing",
     1840          "points": 1,
     1841          "id": "B"
     1842        },
     1843        {
     1844          "text": "concurrent.futures",
     1845          "points": 1,
     1846          "id": "C"
     1847        },
     1848        {
     1849          "text": "numpy.threads",
     1850          "points": 0,
     1851          "id": "D"
     1852        }
     1853      ]
     1854    },
     1855    {
     1856      "domain": "PY_PARALLEL",
     1857      "qtype": "single_choice",
     1858      "text": "Welche Klasse erstellt einen Prozess-Pool einfach?",
     1859      "options": [
     1860        {
     1861          "text": "concurrent.futures.ProcessPoolExecutor",
     1862          "points": 1,
     1863          "id": "A"
     1864        },
     1865        {
     1866          "text": "threading.Pool",
     1867          "points": 0,
     1868          "id": "B"
     1869        },
     1870        {
     1871          "text": "os.pool",
     1872          "points": 0,
     1873          "id": "C"
     1874        }
     1875      ]
     1876    },
     1877    {
     1878      "domain": "PY_PARALLEL",
     1879      "qtype": "multiple_choice",
     1880      "text": "Welche Aussagen zu Locks sind richtig?",
     1881      "options": [
     1882        {
     1883          "text": "Locks schützen kritische Abschnitte.",
     1884          "points": 1,
     1885          "id": "A"
     1886        },
     1887        {
     1888          "text": "Ohne Locks kann es zu Race Conditions kommen.",
     1889          "points": 1,
     1890          "id": "B"
     1891        },
     1892        {
     1893          "text": "Mit GIL sind Locks nie nötig.",
     1894          "points": 0,
     1895          "id": "C"
     1896        },
     1897        {
     1898          "text": "RLock erlaubt denselben Thread mehrfach zu sperren.",
     1899          "points": 1,
     1900          "id": "D"
     1901        }
     1902      ]
     1903    },
     1904    {
     1905      "domain": "PY_PARALLEL",
     1906      "qtype": "single_choice",
     1907      "text": "Was beschreibt `asyncio` korrekt?",
     1908      "options": [
     1909        {
     1910          "text": "Kooperative Nebenläufigkeit per Event-Loop und await.",
     1911          "points": 1,
     1912          "id": "A"
     1913        },
     1914        {
     1915          "text": "Preemptives Scheduling wie OS-Threads.",
     1916          "points": 0,
     1917          "id": "B"
     1918        },
     1919        {
     1920          "text": "Automatische Mehrkern-Parallelisierung.",
     1921          "points": 0,
     1922          "id": "C"
     1923        }
     1924      ]
     1925    },
     1926    {
     1927      "domain": "PY_NETWORK",
     1928      "qtype": "single_choice",
     1929      "text": "Welche Bibliothek ist der Low-Level-Baustein für TCP/UDP?",
     1930      "options": [
     1931        {
     1932          "text": "socket",
     1933          "points": 1,
     1934          "id": "A"
     1935        },
     1936        {
     1937          "text": "email",
     1938          "points": 0,
     1939          "id": "B"
     1940        },
     1941        {
     1942          "text": "http.client",
     1943          "points": 0,
     1944          "id": "C"
     1945        }
     1946      ]
     1947    },
     1948    {
     1949      "domain": "PY_NETWORK",
     1950      "qtype": "multiple_choice",
     1951      "text": "Welche Aussagen zu HTTP in der Stdlib sind korrekt?",
     1952      "options": [
     1953        {
     1954          "text": "urllib.request kann einfache HTTP-Requests senden.",
     1955          "points": 1,
     1956          "id": "A"
     1957        },
     1958        {
     1959          "text": "http.client ist sehr Low-Level.",
     1960          "points": 1,
     1961          "id": "B"
     1962        },
     1963        {
     1964          "text": "ssl unterstützt TLS-Konfiguration.",
     1965          "points": 1,
     1966          "id": "C"
     1967        },
     1968        {
     1969          "text": "email ist für HTTP-Requests gedacht.",
     1970          "points": 0,
     1971          "id": "D"
     1972        }
     1973      ]
     1974    },
     1975    {
     1976      "domain": "PY_NETWORK",
     1977      "qtype": "single_choice",
     1978      "text": "Wie setzt man ein Timeout für einen Socket?",
     1979      "options": [
     1980        {
     1981          "text": "sock.settimeout(seconds)",
     1982          "points": 1,
     1983          "id": "A"
     1984        },
     1985        {
     1986          "text": "socket.timeout=True",
     1987          "points": 0,
     1988          "id": "B"
     1989        },
     1990        {
     1991          "text": "sock.timeout(seconds)",
     1992          "points": 0,
     1993          "id": "C"
     1994        }
     1995      ]
     1996    },
     1997    {
     1998      "domain": "PY_NETWORK",
     1999      "qtype": "multiple_choice",
     2000      "text": "Welche Helfer gibt es zum Arbeiten mit URLs?",
     2001      "options": [
     2002        {
     2003          "text": "urllib.parse (urlsplit, urlencode, parse_qs)",
     2004          "points": 1,
     2005          "id": "A"
     2006        },
     2007        {
     2008          "text": "json.parse_url",
     2009          "points": 0,
     2010          "id": "B"
     2011        },
     2012        {
     2013          "text": "urllib.robotparser für robots.txt",
     2014          "points": 1,
     2015          "id": "C"
     2016        },
     2017        {
     2018          "text": "re.url",
     2019          "points": 0,
     2020          "id": "D"
     2021        }
     2022      ]
     2023    },
     2024    {
     2025      "domain": "PY_NETWORK",
     2026      "qtype": "single_choice",
     2027      "text": "Wie validiert man standardmäßig TLS-Zertifikate mit urllib?",
     2028      "options": [
     2029        {
     2030          "text": "Standard-Handler prüfen Zertifikate, wenn CA-Store korrekt ist.",
     2031          "points": 1,
     2032          "id": "A"
     2033        },
     2034        {
     2035          "text": "urllib kann TLS nie prüfen.",
     2036          "points": 0,
     2037          "id": "B"
     2038        },
     2039        {
     2040          "text": "Man muss verify=False setzen.",
     2041          "points": 0,
     2042          "id": "C"
     2043        }
     2044      ]
     2045    },
     2046    {
     2047      "domain": "PY_PACKAGING",
     2048      "qtype": "single_choice",
     2049      "text": "Welche Datei beschreibt moderne Projekt-Metadaten/Build-System?",
     2050      "options": [
     2051        {
     2052          "text": "pyproject.toml",
     2053          "points": 1,
     2054          "id": "A"
     2055        },
     2056        {
     2057          "text": "requirements.txt",
     2058          "points": 0,
     2059          "id": "B"
     2060        },
     2061        {
     2062          "text": "Pipfile.lock",
     2063          "points": 0,
     2064          "id": "C"
     2065        }
     2066      ]
     2067    },
     2068    {
     2069      "domain": "PY_PACKAGING",
     2070      "qtype": "multiple_choice",
     2071      "text": "Welche Felder sind typisch in pyproject.toml (PEP 621)?",
     2072      "options": [
     2073        {
     2074          "text": "project.name / version / dependencies",
     2075          "points": 1,
     2076          "id": "A"
     2077        },
     2078        {
     2079          "text": "project.scripts für CLI-Einträge",
     2080          "points": 1,
     2081          "id": "B"
     2082        },
     2083        {
     2084          "text": "build-system (Backend/Requires)",
     2085          "points": 1,
     2086          "id": "C"
     2087        },
     2088        {
     2089          "text": "kernel.modules",
     2090          "points": 0,
     2091          "id": "D"
     2092        }
     2093      ]
     2094    },
     2095    {
     2096      "domain": "PY_PACKAGING",
     2097      "qtype": "single_choice",
     2098      "text": "Was ist ein Wheel (.whl)?",
     2099      "options": [
     2100        {
     2101          "text": "Vorgebaute Distributionsdatei für schnelle Installation.",
     2102          "points": 1,
     2103          "id": "A"
     2104        },
     2105        {
     2106          "text": "Quellpaket (sdist).",
     2107          "points": 0,
     2108          "id": "B"
     2109        },
     2110        {
     2111          "text": "Virtuelle Umgebung.",
     2112          "points": 0,
     2113          "id": "C"
     2114        }
     2115      ]
     2116    },
     2117    {
     2118      "domain": "PY_PACKAGING",
     2119      "qtype": "multiple_choice",
     2120      "text": "Welche Tools helfen beim Bauen/Veröffentlichen?",
     2121      "options": [
     2122        {
     2123          "text": "`python -m build` (extern)",
     2124          "points": 1,
     2125          "id": "A"
     2126        },
     2127        {
     2128          "text": "`pip install .`",
     2129          "points": 1,
     2130          "id": "B"
     2131        },
     2132        {
     2133          "text": "twine für Uploads",
     2134          "points": 1,
     2135          "id": "C"
     2136        },
     2137        {
     2138          "text": "setup.py zwingend in jedem Projekt",
     2139          "points": 0,
     2140          "id": "D"
     2141        }
     2142      ]
     2143    },
     2144    {
     2145      "domain": "PY_PACKAGING",
     2146      "qtype": "single_choice",
     2147      "text": "Wo definiert man ein Konsolen-Skript ohne setup.py?",
     2148      "options": [
     2149        {
     2150          "text": "In pyproject.toml unter project.scripts (Backend-abhängig).",
     2151          "points": 1,
     2152          "id": "A"
     2153        },
     2154        {
     2155          "text": "In requirements.txt",
     2156          "points": 0,
     2157          "id": "B"
     2158        },
     2159        {
     2160          "text": "In README.md",
     2161          "points": 0,
     2162          "id": "C"
     2163        }
     2164      ]
     2165    }
    5092166  ]
    5102167}
  • gui/detail_panel.py

    re77bfb3 rfe7d338  
    6868
    6969    # ───────────────────────────────────────────────────────────────────────
    70     def update_info(self, question=None, catalog_title=None,
     70    def refresh_details(self, question=None, catalog_title=None,
    7171                    clipboard_action=None, clipboard_count=0):
    7272        """
  • gui/gui.py

    re77bfb3 rfe7d338  
    55from pathlib import Path
    66from datetime import datetime
    7 from flexoentity import EntityState, EntityType, FlexOID
     7from flexoentity import FlexOID
    88from builder.exam import Exam
    9 from builder.question_factory import question_factory
    109from builder.question_catalog import QuestionCatalog
    1110from builder.media_items import NullMediaItem
     
    1312from builder.exam_manager import ExamManager
    1413from builder.exam_elements import SingleChoiceQuestion, MultipleChoiceQuestion, InstructionBlock
     14from .menu import AppMenu
     15from .actions_panel import ActionsPanel
     16from .select_panel import SelectPanel
    1517from .detail_panel import DetailPanel
    1618from .option_question_editor import OptionQuestionEditorDialog
     
    2628        self.catalog_manager = CatalogManager()
    2729        self.exam_manager = ExamManager()
    28         self.title(f"flex-o-grader – [{self.catalog_manager.get_active_title()}]")
     30        self.title("flex-o-grader")
    2931
    3032        self.geometry("1000x600")
    3133        default_font = font.nametofont("TkDefaultFont")
    3234        default_font.configure(size=12)  # increase from default (usually 9–10)
    33         # Optional: change family
    34         # default_font.configure(family="Helvetica")
    3535
    3636        # Apply same size to other common named fonts
     
    4343        self.status_var = tk.StringVar(value="No catalog loaded")
    4444        self.recent_actions_var = tk.StringVar(value="")  # new right-side text
    45         self.current_qtype_var = tk.StringVar(value="Radio")
     45        self.current_qtype_var = tk.StringVar(value="single_choice")
    4646        self.target_exam_var = tk.StringVar(value="")
    4747
     
    5151        self.clipboard_action = None  # "copy"
    5252        self.clipboard_source_catalog = None
    53         self.create_menu()
     53        self.config(menu=AppMenu(self))
    5454        self.create_widgets()
    5555
     
    6060        geometry = self.session.load()
    6161        self.geometry(geometry)
     62        self.after(60000, lambda: self.session.save(self.geometry()))
     63        self.protocol("WM_DELETE_WINDOW", self.on_quit)
     64        self.refresh_all()
     65
     66    def refresh_all(self):
    6267        self.refresh_catalog_list()
    6368        self.refresh_exam_list()
    6469        self.refresh_question_tree()
    6570        self.refresh_status_bar()
    66        
    67         self.after(60000, lambda: self.session.save(self.geometry()))
    68         self.protocol("WM_DELETE_WINDOW", self.on_quit)
    69  
     71        if self.question_tree.selection():
     72            self.detail_panel.refresh_details(self.require_selected_question(),
     73                                              self.require_active_catalog())
    7074    @property
    7175    def active_catalog(self):
     
    8084        return active
    8185
    82     def create_menu(self):
    83         menubar = tk.Menu(self)
    84         filemenu = tk.Menu(menubar, tearoff=0)
    85         filemenu.add_command(label="Import from exam", command=self.import_exam)
    86         filemenu.add_separator()
    87         filemenu.add_command(label="Exit", command=self.quit)
    88         menubar.add_cascade(label="File", menu=filemenu)
    89 
    90         catalog_menu = tk.Menu(menubar, tearoff=0)
    91         catalog_menu.add_command(label="Create Catalog", command=self.create_catalog)
    92         catalog_menu.add_command(label="Open Catalog", command=self.open_catalog)
    93         menubar.add_cascade(label="Catalogs", menu=catalog_menu)
    94 
    95         domain_menu = tk.Menu(menubar, tearoff=0)
    96         domain_menu.add_command(label="Add domain", command=self.add_domain)
    97         menubar.add_cascade(label="Domains", menu=domain_menu)
    98 
    99         exam_menu = tk.Menu(menubar, tearoff=0)
    100         exam_menu.add_command(label="Create new exam", command=self.create_exam_dialog)
    101         exam_menu.add_command(label="Layout existing exam", command=self.layout_exam)
    102         exam_menu.add_command(label="Load existing exam", command=self.load_exam)
    103         menubar.add_cascade(label="Exams", menu=exam_menu)
    104         help_menu = tk.Menu(menubar, tearoff=0)
    105         help_menu.add_command(label="Help / FAQ", command=self.show_help)
    106         menubar.add_cascade(label="Help", menu=help_menu)
    107         self.config(menu=menubar)
    108 
    10986    def create_widgets(self):
    11087        self.columnconfigure(0, weight=2, uniform="columns")
     
    11289        self.rowconfigure(0, weight=1)
    11390
    114         frame_left = ttk.Frame(self)
    115         frame_left.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
    116         frame_left.rowconfigure(1, weight=1)
    117         frame_left.columnconfigure(0, weight=1)
    118         frame_left.grid_propagate(True)
    119 
    120         ttk.Label(frame_left, text="Questions").grid(
    121             row=0, column=0, sticky="w", pady=(0, 5)
    122         )
    123 
    124         # Listbox + Scrollbar frame
    125         list_frame = ttk.Frame(frame_left)
    126         list_frame.grid(row=1, column=0, sticky="nsew")
    127         list_frame.columnconfigure(0, weight=1)
    128         list_frame.rowconfigure(0, weight=1)
    129 
    130         # --- Treeview replacing Listbox ---
    131         self.question_tree = ttk.Treeview(
    132             list_frame,
    133             columns=("text", "state"),
    134             show="headings",
    135             selectmode="browse",
    136         )
    137         self.question_tree.heading("text", text="Question")
    138         self.question_tree.heading("state", text="State")
    139         self.question_tree.column("text", anchor="w", width=500)
    140         self.question_tree.column("state", anchor="center", width=100)
    141         self.question_tree.grid(row=0, column=0, sticky="nsew")
    142 
    143         # Attach scrollbars
    144         vscrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=self.question_tree.yview)
    145         vscrollbar.grid(row=0, column=1, sticky="ns")
    146         hscrollbar = ttk.Scrollbar(list_frame, orient="horizontal", command=self.question_tree.xview)
    147         hscrollbar.grid(row=1, column=0, sticky="ew")
    148         self.question_tree.configure(yscrollcommand=vscrollbar.set, xscrollcommand=hscrollbar.set)
    149 
    150         # Bind events (similar to Listbox)
    151         self.question_tree.bind("<<TreeviewSelect>>", self.on_select_question)
    152         self.question_tree.bind("<Double-Button-1>", lambda e: self.edit_selected_question())
    153         self.question_tree.tag_configure("cut", foreground="gray60", font=("", 10, "italic"))
    154         # Create catalog switcher
    155         frm_catalog = ttk.Frame(frame_left)
    156         frm_catalog.grid(row=0, column=0, sticky="ew", pady=(4, 8))
    157         frm_catalog.columnconfigure(1, weight=1)  # allow dropdown to expand horizontally
    158 
    159         # ─ Label ─
    160         lbl_catalog = ttk.Label(frm_catalog, text="Catalog:")
    161         lbl_catalog.grid(row=0, column=0, sticky="w", padx=(0, 6))
    162         self.catalog_var = tk.StringVar()
    163         self.catalog_dropdown = ttk.Combobox(
    164             frm_catalog,
    165             textvariable=self.catalog_var,
    166             values=self.catalog_manager.list_titles(),
    167             state="readonly",
    168         )
    169         self.catalog_dropdown.bind("<<ComboboxSelected>>", self.on_catalog_selected)
    170         self.catalog_dropdown.grid(row=0, column=1, sticky="ew", pady=(0, 5))
    171 
    172         # Bottom button frame
    173         frame_buttons = ttk.Frame(frame_left)
    174         frame_buttons.grid(row=2, column=0, sticky="ew", pady=(5, 0))
    175 
    176         # Define 6 columns: Add+Type, Copy, Cut, Paste, Delete, Add-to-exam
    177         for i in range(6):
    178             frame_buttons.columnconfigure(i, weight=1)
    179 
    180         frame_buttons.columnconfigure(0, weight=2)  # Add + Type (wider)
    181         frame_buttons.columnconfigure(5, weight=2)  # Add-to-exam area (wider)
    182 
    183         # --- Add + Type ---
    184         add_frame = ttk.Frame(frame_buttons)
    185         add_frame.grid(row=0, column=0, sticky="ew", padx=2)
    186         add_frame.columnconfigure(0, weight=0)
    187         add_frame.columnconfigure(1, weight=1)
    188 
    189         ttk.Button(add_frame, text="Add", command=self.add_question).grid(
    190             row=0, column=0, padx=(4, 6)
    191         )
    192         self.qtype_dropdown = ttk.Combobox(
    193             add_frame,
    194             textvariable=self.current_qtype_var,
    195             values=["Instruction", "Text", "Radio", "Multiple_Choice"],
    196             state="readonly",
    197             width=16,
    198         )
    199         self.qtype_dropdown.grid(row=0, column=1, sticky="ew")
    200 
    201         # --- Copy / Cut / Paste / Delete ---
    202         ttk.Button(frame_buttons, text="Copy",
    203                    command=self.copy_selected_question).grid(row=0, column=1, padx=2, sticky="ew")
    204         ttk.Button(frame_buttons, text="Paste",
    205                    command=self.paste_selected_question).grid(row=0, column=3, padx=2, sticky="ew")
    206         ttk.Button(frame_buttons, text="Delete",
    207                    command=self.delete_selected_question).grid(row=0, column=4, padx=2, sticky="ew")
    208 
    209         # --- Add to exam + Dropdown ---
    210         exam_frame = ttk.Frame(frame_buttons)
    211         exam_frame.grid(row=0, column=5, sticky="ew", padx=(12, 6))
    212         exam_frame.columnconfigure(0, weight=0)
    213         exam_frame.columnconfigure(1, weight=1)
    214 
    215         ttk.Button(exam_frame, text="Add to exam",
    216                    command=self.add_selected_question_to_exam).grid(row=0, column=0, padx=(0, 4))
    217         self.target_exam_dropdown = ttk.Combobox(
    218             exam_frame,
    219             textvariable=self.target_exam_var,
    220             values=[],
    221             state="readonly",
    222             width=18,
    223         )
    224         self.target_exam_dropdown.grid(row=0, column=1, sticky="ew")
    225         self.target_exam_dropdown.bind("<<ComboboxSelected>>", self.on_exam_selected)
     91        self.selection_panel = SelectPanel(self)
     92
     93        self.action_panel = ActionsPanel(self)
    22694
    22795        self.detail_panel = DetailPanel(self)
     
    23199        status_frame.grid(row=1, column=0, columnspan=2, sticky="ew")
    232100
    233         # Columns:
    234         # 0 = main status (expands)
    235         # 1 = right-aligned recent actions (fixed)
    236101        status_frame.columnconfigure(0, weight=1)
    237102        status_frame.columnconfigure(1, weight=0)
     
    263128    def export_catalog(self):
    264129        """Export the currently active catalog to a JSON file."""
    265         catalog = self.catalog_manager.get_active_catalog()
     130        catalog = self.catalog_manager.get_active()
    266131        if not catalog:
    267132            messagebox.showwarning("No Catalog", "No active catalog to export.")
     
    294159
    295160        catalog = QuestionCatalog.from_dict(data)
    296         catalog.title = "Test"
     161        title = simpledialog.askstring("New Catalog", "Enter title:", parent=self)
     162        catalog.title = title
    297163        self.catalog_manager.add_catalog(catalog)
    298164        self.catalog_manager.set_active(catalog.flexo_id)
    299         self.refresh_catalog_list()
     165        self.refresh_all()
    300166
    301167    def refresh_catalog_list(self):
     
    312178        version = current_catalog.version
    313179        status = (
    314             f"Catalog: {title} | Version: {version} | Questions: {q_count} | Status: {current_catalog.status_text}"
     180            f"Catalog: {title} | Version: {version} | Questions: {q_count} | "
     181            f"Status: {current_catalog.status_text}"
    315182        )
    316183        self.status_var.set(status)
     
    336203        catalog.title = title
    337204        catalog.author =author
    338         catalog._update_fingerprint
     205        catalog._update_fingerprint()
    339206        self.catalog_manager.add_catalog(catalog)
    340207        self.catalog_manager.set_active(did)
     
    345212
    346213        self.log_action(f"Created - New catalog '{title}' is now active.")
    347         self.refresh_catalog_list()
    348         self.refresh_question_tree()
    349         self.title(f"flex-o-grader – [{self.catalog_manager.get_active_title()}]")
    350         self.refresh_status_bar()
     214        self.refresh_all()
    351215
    352216    def create_exam_dialog(self):
     
    357221                author=dialog.result["author"]
    358222            )
    359             self.refresh_exam_list()
     223            self.refresh_all()
    360224
    361225    def on_catalog_selected(self, event=None):
     
    364228        if not title:
    365229            return
    366         catalog = self.catalog_manager.set_active_by_title(title)
    367         if catalog:
    368             self.refresh_status_bar()
    369             # Refresh question list display
    370         self.refresh_question_tree()
    371 
     230        self.catalog_manager.set_active_by_title(title)
     231
     232        self.refresh_all()
    372233        self.log_action(f"Catalog Switched - Active catalog: {title}")
    373234
     
    387248        """
    388249        Return the appropriate editor dialog instance for a given question.
    389 
    390250        This acts as a factory so the main GUI doesn’t need isinstance() checks.
    391251        """
    392         if question.qtype in ("radio", "multiple_choice"):
     252        if question.qtype in ("single_choice", "multiple_choice"):
    393253            return OptionQuestionEditorDialog(parent, question, available_domains)
    394         elif question.qtype in ("instruction", "text"):
     254        if question.qtype in ("instruction", "text"):
    395255            # even though InstructionBlock may subclass Question,
    396256            # it doesn’t need options
     
    427287
    428288        if current_question:
    429             # refresh display
    430289            current_question.flexo_id = FlexOID.generate(
    431290                current_question.domain,
     
    435294            )
    436295            active_catalog.add_questions([current_question])
    437             self.refresh_question_tree()
     296            self.refresh_all()
    438297            self.log_action(f"Updated - Question {q.id} updated.")
    439298            self.log_action(f"New Question ID - Assigned ID: {current_question.id}")
     
    447306        question = self.require_selected_question()
    448307        exam.add_question(question)
    449         #  (f"Added {question.flexo_id} to exam {exam.title}")
    450308
    451309    def delete_selected_question(self):
     
    459317        try:
    460318            if self.active_catalog.remove(q.id):
    461                 self.refresh_question_tree()
     319                self.refresh_all()
    462320                self.log_action(f"Deleted - Question {q.id} removed.")
    463321        except ValueError as e:
     
    473331        catalog = self.catalog_manager.get_active()
    474332        catalog_title = catalog.title if catalog else None
    475         self.detail_panel.update_info(question=q, catalog_title=catalog_title,
     333        self.detail_panel.refresh_details(question=q, catalog_title=catalog_title,
    476334                                      clipboard_action=self.clipboard_action,
    477335                                      clipboard_count=len(self.clipboard)
     
    485343        if current_question:
    486344            self.active_catalog.add_questions([current_question])
    487             self.refresh_question_tree()
     345            self.refresh_all()
    488346            self.log_action(f"Updated - Question {q.id} updated.")
    489347
     
    495353        print("Duplicate IDs:", duplicates)
    496354
    497        # Create a temporary catalog
     355        # Create a temporary catalog
    498356        temp_id = "TEMP_" + datetime.utcnow().strftime("%H%M%S")
    499357        # FIXME: Check flexo_id creation
     
    503361            questions=exam.questions
    504362        )
    505 #         self.domain_set.update(temp_catalog.domains)
    506363        self.catalog_manager.add_catalog(temp_catalog)
    507364        self.catalog_manager.set_active(temp_id)
    508365
    509         self.refresh_catalog_list()
    510         self.refresh_question_tree()
    511         self.refresh_status_bar()
     366        self.refresh_all()
    512367        messagebox.showinfo(
    513368            "Import Complete",
     
    524379        dlg = ExamLayoutDialog(self, exam)
    525380        self.wait_window(dlg)
    526         # self.log_action(f"Layout updated for {exam.title}")
    527381
    528382    def load_exam(self):
     
    534388            return
    535389        try:
    536             self.exam = self.import_exam_as_temp_catalog(path)
     390            self.import_exam_as_temp_catalog(path)
    537391        except Exception as e:
    538392            messagebox.showerror("Error", f"Could not load exam:\n{e}")
     
    554408            return None
    555409
    556         print("QID", qid)
    557         print(active.questions)
    558410        q = active.find(qid)
    559411        if not q:
     
    586438            # Clean up the question text (short preview)
    587439            text = q.text.strip().replace("\n", " ")
    588             if len(text) > 100:
    589                 text = text # [:97] + "..."
    590440
    591441            # Try to display a readable status name
     
    613463            return
    614464
    615         catalogs = self.catalog_manager.list_catalogs()
     465        catalogs = self.catalog_manager.catalogs()
    616466        if len(catalogs) < 2:
    617467            messagebox.showinfo(action.title(), "You need at least two catalogs.")
     
    651501
    652502        target_catalog.add_questions(transferred)
    653         self.refresh_question_tree()
     503        self.refresh_all()
    654504
    655505        msg = f"{action.title()}d {len(transferred)} question(s) to catalog '{target_id}'."
     
    726576        self.clipboard_source_catalog = None
    727577
    728         self.refresh_question_tree()
     578        self.refresh_all()
    729579        self.log_action(f"Pasted {len(transferred)} question(s) to '{target.title}'.")
    730580
     
    736586
    737587        entry = simpledialog.askstring("Add Domain",
    738                                        "Enter domain in 'ABBREV_Fullname' format (e.g. ELEK_Elektrotechnik):",
     588                                       "Enter domain (e.g. ELEK_Elektrotechnik):",
    739589                                       parent=self)
    740590        if not entry:
  • gui/option_question_editor.py

    re77bfb3 rfe7d338  
    2121        self.question = question
    2222        self.available_domains = sorted(available_domains or [])
    23         self.domain_var = tk.StringVar(value=self.question.domain if self.question else "")
     23        self.domain_var = tk.StringVar(value=self.question.domain_code if self.question else "")
    2424        self.state_var = tk.StringVar(value=self.question.state.name if self.question else EntityState.DRAFT.name)
    2525
     
    7979
    8080    def on_ok(self):
     81        # FIXME: Use from dict here
    8182        text = self.txt_question.get("1.0", tk.END).strip()
    8283        if not text:
     
    8586
    8687        self.question.text = text
    87         self.question.domain = self.cmb_domain.get()
     88        self.question.domain_code = self.cmb_domain.get()
    8889        new_state = EntityState[self.cmb_status.get()]
    8990        self.question.apply_state_change(new_state)
Note: See TracChangeset for help on using the changeset viewer.