From 36fc4e319af9a7dc69ecb73543f76500b67e28e9 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 06:57:23 -0400 Subject: [PATCH] [FIX] tui qemu: the form crashed on the resource totals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "TypeError: unsupported operand type(s) for +: 'int' and 'NoSelection'" as soon as the custom resource profile was picked. Textual 8 turned Select.BLANK into a deprecated alias worth False, the sentinel having become Select.NULL. The guard compared against False and filtered nothing; NoSelection, lacking __bool__ and thus truthy, sailed through "or default" into the plan totals. The sentinel is now resolved at runtime, and both summed fields are coerced to a positive int inside the pure function -- where the invariant belongs, whatever Textual version is installed. --- FR --- « TypeError: unsupported operand type(s) for +: 'int' and 'NoSelection' » dès qu'on choisissait le profil personnalisé. Textual 8 a ramené Select.BLANK à un alias déprécié valant False, le sentinelle étant devenu Select.NULL. La garde comparait donc à False et ne filtrait plus rien ; NoSelection, dépourvu de __bool__ et tenu pour vrai, traversait « ou valeur par défaut » jusqu'aux sommes du plan. Le sentinelle est résolu à l'exécution, et les deux champs additionnés sont ramenés à un entier positif dans la fonction pure — c'est là que l'invariant appartient, quelle que soit la version de Textual. Assisted-by: Claude Opus 5 --- script/todo/qemu_deploy_form.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index 76df668..e9b929c 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -63,6 +63,19 @@ def disk_gb(value) -> int: return int(parsed[:-1]) if parsed else 0 +def positive_int(value, fallback): + """Entier strictement positif, sinon `fallback`. + + Ces valeurs viennent de widgets : une liste déroulante sans choix rend un + sentinelle, une saisie libre rend du texte, éventuellement vide. Aucun des + deux ne doit atteindre les totaux, qui les additionnent.""" + try: + number = int(value) + except (TypeError, ValueError): + return fallback + return number if number > 0 else fallback + + def apply_profile(entries, profile, base_vcpus, host_cpu, custom=None): """Applique le profil de ressources aux entrées choisies. @@ -74,9 +87,9 @@ def apply_profile(entries, profile, base_vcpus, host_cpu, custom=None): for e in entries: if profile == "custom": cus = custom or {} - ram = cus.get("ram") or e["ram"] - disk = cus.get("disk") or e["disk"] - vcpus = cus.get("vcpus") or base_vcpus + ram = positive_int(cus.get("ram"), e["ram"]) + disk = parse_disk(cus.get("disk")) or e["disk"] + vcpus = positive_int(cus.get("vcpus"), base_vcpus) else: mult = int(profile) ram = e["ram"] * mult @@ -213,6 +226,12 @@ def run_deploy_form(ctx, run_app: bool = True): Static, ) + # Textual 8 a ramené Select.BLANK à un alias déprécié valant False ; le + # sentinelle « rien de choisi » est Select.NULL. Comparer à BLANK ne + # filtrait donc plus rien, et NoSelection — dépourvu de __bool__, donc + # tenu pour vrai — passait pour une valeur jusque dans les totaux. + SELECT_NULL = getattr(Select, "NULL", Select.BLANK) + catalog = ctx["catalog"] arches = ctx["arches"] domains = set(ctx.get("domains") or []) @@ -403,7 +422,7 @@ def run_deploy_form(ctx, run_app: bool = True): ) yield Select( [(lbl, i) for i, (lbl, _c) in enumerate(profiles)], - value=0 if profiles else Select.BLANK, + value=0 if profiles else SELECT_NULL, allow_blank=not profiles, id="f_profile_install", ) @@ -580,7 +599,7 @@ def run_deploy_form(ctx, run_app: bool = True): mapping = {"f_vcpus": "vcpus", "f_ram": "ram", "f_disk": "disk"} field = mapping.get(event.select.id) if field: - if event.value is not Select.BLANK: + if event.value is not SELECT_NULL: self.custom[field] = event.value self._recompute()