[FIX] tui qemu: the form crashed on the resource totals
"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
This commit is contained in:
parent
3fbde848f8
commit
36fc4e319a
1 changed files with 24 additions and 5 deletions
|
|
@ -63,6 +63,19 @@ def disk_gb(value) -> int:
|
||||||
return int(parsed[:-1]) if parsed else 0
|
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):
|
def apply_profile(entries, profile, base_vcpus, host_cpu, custom=None):
|
||||||
"""Applique le profil de ressources aux entrées choisies.
|
"""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:
|
for e in entries:
|
||||||
if profile == "custom":
|
if profile == "custom":
|
||||||
cus = custom or {}
|
cus = custom or {}
|
||||||
ram = cus.get("ram") or e["ram"]
|
ram = positive_int(cus.get("ram"), e["ram"])
|
||||||
disk = cus.get("disk") or e["disk"]
|
disk = parse_disk(cus.get("disk")) or e["disk"]
|
||||||
vcpus = cus.get("vcpus") or base_vcpus
|
vcpus = positive_int(cus.get("vcpus"), base_vcpus)
|
||||||
else:
|
else:
|
||||||
mult = int(profile)
|
mult = int(profile)
|
||||||
ram = e["ram"] * mult
|
ram = e["ram"] * mult
|
||||||
|
|
@ -213,6 +226,12 @@ def run_deploy_form(ctx, run_app: bool = True):
|
||||||
Static,
|
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"]
|
catalog = ctx["catalog"]
|
||||||
arches = ctx["arches"]
|
arches = ctx["arches"]
|
||||||
domains = set(ctx.get("domains") or [])
|
domains = set(ctx.get("domains") or [])
|
||||||
|
|
@ -403,7 +422,7 @@ def run_deploy_form(ctx, run_app: bool = True):
|
||||||
)
|
)
|
||||||
yield Select(
|
yield Select(
|
||||||
[(lbl, i) for i, (lbl, _c) in enumerate(profiles)],
|
[(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,
|
allow_blank=not profiles,
|
||||||
id="f_profile_install",
|
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"}
|
mapping = {"f_vcpus": "vcpus", "f_ram": "ram", "f_disk": "disk"}
|
||||||
field = mapping.get(event.select.id)
|
field = mapping.get(event.select.id)
|
||||||
if field:
|
if field:
|
||||||
if event.value is not Select.BLANK:
|
if event.value is not SELECT_NULL:
|
||||||
self.custom[field] = event.value
|
self.custom[field] = event.value
|
||||||
self._recompute()
|
self._recompute()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue