dynamisk-rettsstat/modell/kjor.py
Kjell Tore Guttormsen 776325c680
fix: findings from the independent review
Scenario runner: stops when overridden base parameters leave the start out
of balance (new field startlikevekt: krev, kalibrer, ubalanse); stops on
non-finite results; says that models with a court queue drift slowly even
without a shock and prints the drift before the shock; queue in months of
inflow; never deletes files it did not write; stricter limits.
Check: anchored on the note's own checksums for core and reference files,
runs the scenario files too, exact comparison for the diagnosis, honest
wording when parts were skipped or an extension is broken.
Launcher: unknown field names in input are rejected with a suggestion;
unwritable folders give a message, not a traceback.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 15:27:14 +01:00

687 lines
27 KiB
Python

# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE.
"""Én inngang til modellen bak notatet «Den dynamiske rettsstaten».
Bruk:
python3 kjor.py [--data <mappe>] [--modell <mappe>] <kommando> [...]
Bare standardbiblioteket, Python 3.9 eller nyere. Kommandoene står i HJELP under.
Exit-koder: 0 ok, 1 forskjeller funnet av kontroll, 2 feil i inndata eller bruk,
3 miljøet mangler (numpy eller scipy).
Valg av Python for kommandoene som trenger numpy og scipy: (1) denne tolken hvis den har
numpy 2.0 eller nyere (numpy.trapezoid) og scipy; (2) ellers Python i den egne mappa
<data>/venv, som kjører samme kommando på nytt; (3) ellers exit-kode 3 med en melding om
hva som mangler. Ingenting installeres uten «miljo --opprett».
Grensesnitt for modulene i modell-mappa (scenario.py, kontroll.py, arbeidskopi.py):
hver har ``main(argv: list[str]) -> int``, der argv er argumentene etter kommandonavnet.
Før main kalles, ligger modell-mappa og modell-mappa/kjerne først i sys.path, slik at
``import sim`` gir kjernen i samme modell-mappe. ``sys.exit("melding")`` gir exit-kode 2
og meldingen uendret; ``sys.exit(n)`` gir exit-kode n.
Før diagnose og forlop kalles, avvises ukjente feltnavn i hvert ledd og i "styring" (exit-kode
2, med «Mente du ...?»), fordi kjernen ser bort fra felt den ikke kjenner, og en skrivefeil
ellers gir en stille standardverdi. Gyldige felt i et ledd: "navn", kjernens diagnose.TALLFELT
(lest fra kjernen i modell-mappa når kommandoen kjøres) og merknadsfeltene i ANNOTASJONSFELT.
Gyldige felt i "styring": forlop.STYRINGSFELT hvis kjernen har den, ellers STYRINGSFELT her,
pluss merknadsfeltene. Andre felt på øverste nivå (som "merknad" og "kilder") er tillatt.
"""
from __future__ import annotations
import difflib
import importlib
import importlib.util
import json
import os
import platform
import shlex
import subprocess
import sys
import traceback
from pathlib import Path
from typing import TYPE_CHECKING, NamedTuple, TypedDict
if TYPE_CHECKING:
from collections.abc import Callable
from types import ModuleType
HER = Path(__file__).resolve().parent
VENV_MERKE = "DYNAMISK_RETTSSTAT_VENV" # satt når kjor.py er startet på nytt med Python fra den egne mappa
KJERNEMODULER = ("sim", "diagnose", "forlop")
MODELLMODULER = ("scenario", "kontroll", "arbeidskopi")
OK, FORSKJELLER, FEIL_BRUK, MILJO_MANGLER = 0, 1, 2, 3
HJELP = """\
Bruk: python3 kjor.py [--data <mappe>] [--modell <mappe>] <kommando> [...]
Kommandoer:
diagnose <fil> finn leddet som binder, fra egne tall (trenger bare Python)
forlop <fil> ["ledd"] stabilitetssjekk og forløp måned for måned (numpy og scipy)
scenario <fil> [--ut <mappe>] kjør en scenariofil (numpy og scipy)
notatet [--ut <mappe>] kjør notatets simuleringer uendret, om lag 1-2 min (numpy og scipy)
kontroll [--rask] kontroller at modellen regner som notatet
miljo [--opprett] vis Python, numpy og scipy; --opprett installerer dem i en egen mappe
arbeidskopi <mappe> lag en kopi av modellen som du kan endre
Valg før kommandoen:
--data <mappe> mappa der den egne Python-mappa (venv) med numpy og scipy ligger
(standard ~/.dynamisk-rettsstat)
--modell <mappe> bruk modellen i en arbeidskopi i stedet for pluginens
Exit-koder: 0 ok, 1 kontroll fant forskjeller, 2 feil i inndata eller bruk,
3 numpy eller scipy mangler.
"""
TREGER_REGNEMILJO = ("forlop", "scenario", "notatet")
ANNOTASJONSFELT = ("merknad", "kilde", "kilder") # fri tekst i et ledd eller i styring; leses ikke av regningen
STYRINGSFELT = ("forsinkelse", "reaktivitet") # det forlop.les_styring leser
KOMMANDOER = ("diagnose", "forlop", "scenario", "notatet", "kontroll", "miljo", "arbeidskopi")
class Bruksfeil(Exception):
"""Feil i kommandolinjen. Meldingen er til brukeren og gir exit-kode 2."""
class Valg(NamedTuple):
"""Kommandolinjen, tolket."""
argv: list[str] # hele kommandolinjen, slik den kom inn
data: Path # mappa der den egne venv ligger
data_oppgitt: bool # om --data ble oppgitt med en brukbar verdi
modell: Path # modell-mappa som brukes (pluginens eller en arbeidskopi)
kommando: str
args: list[str] # argumentene etter kommandoen
class Beskrivelse(TypedDict):
"""Hva en Python-tolk har av numpy og scipy."""
python: str
sti: str
numpy: str | None
trapezoid: bool
scipy: str | None
class Tolk(NamedTuple):
"""Hvilken Python som skal regne: «denne», «venv» eller «mangler»."""
hvor: str
python: Path | None
mangler: list[str]
# ---------------- kommandolinjen ----------------
def standard_datamappe() -> Path:
"""Mappa som brukes når --data ikke er oppgitt."""
return Path.home() / ".dynamisk-rettsstat"
def datamappe(verdi: str | None) -> tuple[Path, bool]:
"""Tom verdi, eller en verdi med «${» (ikke satt inn av Claude), betyr «ikke oppgitt»."""
if verdi is None or not verdi.strip() or "${" in verdi:
return standard_datamappe(), False
return Path(verdi).expanduser().resolve(), True
def modellmappe(verdi: str | None) -> Path:
"""Modell-mappa: pluginens egen, eller en arbeidskopi (mappa selv eller mappa over modell/)."""
if verdi is None:
return HER
if not verdi.strip():
melding = "--modell må ha en mappe etter seg."
raise Bruksfeil(melding)
sti = Path(verdi).expanduser().resolve()
for kandidat in (sti, sti / "modell"):
if (kandidat / "kjerne").is_dir():
return kandidat
melding = f"Fant ingen modell i {sti}: mappa må ha undermappa kjerne/ eller modell/kjerne/."
raise Bruksfeil(melding)
def tolk_argumenter(argv: list[str]) -> Valg:
"""Tolk de globale valgene og finn kommandoen."""
verdier: dict[str, str | None] = {"--data": None, "--modell": None}
i = 0
while i < len(argv) and argv[i].startswith("-"):
arg = argv[i]
if arg in {"-h", "--hjelp", "--help"}:
return Valg(argv=argv, data=standard_datamappe(), data_oppgitt=False, modell=HER, kommando="hjelp",
args=[])
navn, likhetstegn, verdi = arg.partition("=")
if navn not in verdier:
melding = f"Ukjent valg {arg}. Gyldige valg før kommandoen: --data <mappe>, --modell <mappe>."
raise Bruksfeil(melding)
if likhetstegn:
i += 1
elif i + 1 < len(argv):
verdi = argv[i + 1]
i += 2
else:
melding = f"{navn} må ha en mappe etter seg."
raise Bruksfeil(melding)
verdier[navn] = verdi
if i >= len(argv):
melding = "Mangler kommando."
raise Bruksfeil(melding)
kommando, args = argv[i], argv[i + 1:]
if kommando not in KOMMANDOER and kommando != "_beskriv":
melding = f"Ukjent kommando «{kommando}». Gyldige kommandoer: {', '.join(KOMMANDOER)}."
raise Bruksfeil(melding)
data, oppgitt = datamappe(verdier["--data"])
return Valg(argv, data, oppgitt, modellmappe(verdier["--modell"]), kommando, args)
# ---------------- Python-miljøet ----------------
def beskriv_denne() -> Beskrivelse:
"""Python, numpy og scipy i tolken som kjører nå."""
ut = Beskrivelse(python=platform.python_version(), sti=sys.executable, numpy=None, trapezoid=False, scipy=None)
try:
numpy = importlib.import_module("numpy")
except ImportError:
pass
else:
ut["numpy"] = str(getattr(numpy, "__version__", "ukjent versjon"))
ut["trapezoid"] = hasattr(numpy, "trapezoid")
try:
importlib.import_module("scipy.optimize")
scipy = importlib.import_module("scipy")
except ImportError:
pass
else:
ut["scipy"] = str(getattr(scipy, "__version__", "ukjent versjon"))
return ut
def mangler(beskrivelse: Beskrivelse) -> list[str]:
"""Det som mangler for å regne med numpy og scipy, i ord."""
ut = []
if beskrivelse["numpy"] is None:
ut.append("numpy")
elif not beskrivelse["trapezoid"]:
ut.append(f"numpy 2.0 eller nyere (har {beskrivelse['numpy']})")
if beskrivelse["scipy"] is None:
ut.append("scipy")
return ut
def beskriv_tolk(python: Path) -> Beskrivelse | None:
"""Beskriv en annen Python-tolk ved å la den kjøre denne fila. None hvis det ikke går."""
try:
resultat = subprocess.run(
[str(python), str(Path(__file__).resolve()), "_beskriv"],
capture_output=True, text=True, encoding="utf-8", check=False,
)
except OSError:
return None
if resultat.returncode != 0:
return None
try:
data = json.loads(resultat.stdout.strip().splitlines()[-1])
except (ValueError, IndexError):
return None
return Beskrivelse(
python=str(data["python"]), sti=str(data["sti"]), numpy=data["numpy"],
trapezoid=bool(data["trapezoid"]), scipy=data["scipy"],
)
def venv_mappe(data: Path) -> Path:
"""Den egne Python-mappa med numpy og scipy."""
return data / "venv"
def venv_python(data: Path) -> Path:
"""Python-tolken i den egne mappa."""
if os.name == "nt":
return venv_mappe(data) / "Scripts" / "python.exe"
return venv_mappe(data) / "bin" / "python"
def velg_tolk(data: Path) -> Tolk:
"""Velg Python for kommandoene som trenger numpy og scipy."""
mangel = mangler(beskriv_denne())
if not mangel:
return Tolk("denne", Path(sys.executable), [])
kandidat = venv_python(data)
if kandidat.exists() and os.environ.get(VENV_MERKE) != "1":
return Tolk("venv", kandidat, mangel)
return Tolk("mangler", None, mangel)
def kjor_paa_nytt(python: Path, argv: list[str]) -> int:
"""Kjør samme kommando med en annen Python og gi videre exit-koden."""
sys.stdout.flush()
miljo = dict(os.environ)
miljo[VENV_MERKE] = "1"
return subprocess.run([str(python), str(Path(__file__).resolve()), *argv], env=miljo, check=False).returncode
def python_navn(system: str = os.name) -> str:
"""Navnet brukeren skriver for Python: «python» på Windows, ellers «python3»."""
return "python" if system == "nt" else "python3"
def kommando_tekst(valg: Valg, *args: str, system: str = os.name) -> str:
"""Kommandolinjen brukeren kan skrive for å kjøre kjor.py med samme --data.
På Windows (system "nt") med «python» og Windows-sitering, ellers med «python3» og POSIX-sitering.
"""
deler = [python_navn(system), str(Path(__file__).resolve())]
if valg.data_oppgitt:
deler += ["--data", str(valg.data)]
deler += list(args)
if system == "nt":
return subprocess.list2cmdline(deler)
return " ".join(shlex.quote(d) for d in deler)
def melding_mangler(valg: Valg, tolk: Tolk) -> str:
"""Norsk melding for exit-kode 3."""
linjer = [
f"«{valg.kommando}» trenger numpy (2.0 eller nyere) og scipy. Mangler: {', '.join(tolk.mangler)}.",
f"Python som ble brukt: {sys.executable} ({platform.python_version()}).",
]
if os.environ.get(VENV_MERKE) == "1":
linjer.append(f"Den egne mappa {venv_mappe(valg.data)} mangler dem også. Kommandoen under installerer "
"dem der på nytt fra PyPI (krever internett):")
else:
linjer.append(f"Fant ingen egen mappe med numpy og scipy i {venv_mappe(valg.data)}. Kommandoen under "
"lager den og installerer numpy og scipy fra PyPI der (krever internett):")
linjer.append(f" {kommando_tekst(valg, 'miljo', '--opprett')}")
linjer.append("Ingenting installeres uten at du ber om det. diagnose virker uten numpy og scipy.")
return "\n".join(linjer)
# ---------------- å kjøre kjernen og modulene ----------------
def forbered_stier(modell: Path) -> None:
"""Legg modell-mappa og kjernen først i sys.path, og glem moduler lastet fra en annen mappe."""
sys.dont_write_bytecode = True # ingen __pycache__ i plugin-mappa
for navn in (*KJERNEMODULER, *MODELLMODULER):
sys.modules.pop(navn, None)
fjern = {str(modell), str(modell / "kjerne")}
if modell != HER:
fjern.add(str(HER))
sys.path[:] = [sti for sti in sys.path if str(Path(sti or ".").resolve()) not in fjern]
sys.path[:0] = [str(modell / "kjerne"), str(modell)]
def exit_kode(kode: object) -> int:
"""Exit-koden for et sys.exit(kode) fra kjernen eller en modul."""
if kode is None:
return OK
if isinstance(kode, int):
return kode
sys.stdout.flush()
print(kode, file=sys.stderr)
return FEIL_BRUK
def fang(funksjon: Callable[[], object], hva: str) -> int:
"""Kjør funksjonen og gjør sys.exit og uventede feil om til exit-koder."""
try:
resultat = funksjon()
except SystemExit as stopp:
return exit_kode(stopp.code)
except Exception: # noqa: BLE001 - alt som ikke er SystemExit, er en feil i koden, ikke i inndata
sys.stdout.flush()
traceback.print_exc()
print(f"Uventet feil i {hva} (se over).", file=sys.stderr)
return FEIL_BRUK
if isinstance(resultat, int) and not isinstance(resultat, bool):
return resultat
return OK
def last_kjerne(modell: Path, navn: str) -> ModuleType:
"""Last en kjernemodul fra modell-mappa/kjerne."""
forbered_stier(modell)
return importlib.import_module(navn)
def last_modul(modell: Path, navn: str) -> ModuleType | None:
"""Last modell-mappa/<navn>.py. None hvis fila ikke finnes."""
fil = modell / f"{navn}.py"
if not fil.is_file():
return None
forbered_stier(modell)
spec = importlib.util.spec_from_file_location(navn, fil)
if spec is None or spec.loader is None:
return None
modul = importlib.util.module_from_spec(spec)
sys.modules[navn] = modul
spec.loader.exec_module(modul)
return modul
def kjor_modul(valg: Valg, navn: str) -> int:
"""Kall main(argv) i modell-mappa/<navn>.py med argumentene etter kommandoen."""
fil = valg.modell / f"{navn}.py"
if not fil.is_file():
print(f"Kommandoen «{valg.kommando}» er ikke på plass i denne modellen ennå: fant ikke {fil}.",
file=sys.stderr)
return FEIL_BRUK
def start() -> object:
main = getattr(last_modul(valg.modell, navn), "main", None)
if not callable(main):
sys.exit(f"{fil} mangler funksjonen main(argv).")
return main(list(valg.args))
return fang(start, f"{navn}.py")
# ---------------- kommandoene ----------------
def feltmelding(hvor: str, felt: str, gyldige: tuple[str, ...]) -> str:
"""Melding om et ukjent feltnavn, med forslag når det ligner et gyldig."""
treff = difflib.get_close_matches(felt, gyldige, n=1, cutoff=0.6)
forslag = f' Mente du "{treff[0]}"?' if treff else ""
return (f'Feil i inndata, {hvor}: feltet "{felt}" finnes ikke.{forslag} '
f"Gyldige felt: {', '.join(gyldige)}.")
def ukjent_felt(fil: str, diagnose: object, forlop: object = None) -> str | None:
"""Melding om det første ukjente feltet i et ledd eller i styring, ellers None.
Fila leses bare for å finne feltnavn. Kan den ikke leses, eller har den feil form, sier
kjernen fra om det selv, med sin egen melding.
"""
try:
data = json.loads(Path(fil).read_text(encoding="utf-8"))
except (OSError, ValueError):
return None
if not isinstance(data, dict):
return None
tallfelt = tuple(str(f) for f in getattr(diagnose, "TALLFELT", ()))
gyldige_ledd = ("navn", *tallfelt, *ANNOTASJONSFELT)
alle_ledd = data.get("ledd")
for nr, ledd in enumerate(alle_ledd if isinstance(alle_ledd, list) else [], 1):
if not isinstance(ledd, dict):
continue
for felt in ledd:
if felt not in gyldige_ledd:
return feltmelding(f"ledd {nr} ({ledd.get('navn', 'uten navn')})", felt, gyldige_ledd)
styring = data.get("styring")
if isinstance(styring, dict):
gyldige_styring = (*(str(f) for f in getattr(forlop, "STYRINGSFELT", STYRINGSFELT)), *ANNOTASJONSFELT)
for felt in styring:
if felt not in gyldige_styring:
return feltmelding("styring", felt, gyldige_styring)
return None
def avvis_ukjente_felt(fil: str, diagnose: object, forlop: object = None) -> int | None:
"""Skriv meldingen og gi exit-kode 2 når fila har et ukjent felt; None når den er i orden."""
melding = ukjent_felt(fil, diagnose, forlop)
if melding is None:
return None
sys.stdout.flush()
print(melding, file=sys.stderr)
return FEIL_BRUK
def kommando_diagnose(valg: Valg) -> int:
"""Kjernens diagnose.main."""
if len(valg.args) != 1:
melding = "Bruk: kjor.py diagnose <inndata.json>"
raise Bruksfeil(melding)
diagnose = last_kjerne(valg.modell, "diagnose")
feil = avvis_ukjente_felt(valg.args[0], diagnose)
if feil is not None:
return feil
return fang(lambda: diagnose.main(valg.args[0]), "diagnose.py")
def kommando_forlop(valg: Valg) -> int:
"""Kjernens forlop.main."""
if len(valg.args) not in {1, 2}:
melding = 'Bruk: kjor.py forlop <inndata.json> ["navn på ledd"]'
raise Bruksfeil(melding)
forlop = last_kjerne(valg.modell, "forlop")
feil = avvis_ukjente_felt(valg.args[0], getattr(forlop, "diagnose", None), forlop)
if feil is not None:
return feil
return fang(lambda: forlop.main(*valg.args), "forlop.py")
def kan_ikke_skrive(mappe: Path, feil: OSError) -> int:
"""Norsk melding og exit-kode 2 når ut-mappa ikke kan lages eller skrives i."""
sys.stdout.flush()
print(f"Kan ikke skrive i {mappe}: {feil.strerror or feil}. Velg en annen mappe med --ut.", file=sys.stderr)
return FEIL_BRUK
def kommando_notatet(valg: Valg) -> int:
"""Kjør kjernens sim.py uendret, med ut-mappa som arbeidsmappe (sim.py skriver data/ der)."""
ut = Path.cwd() / "resultater" / "notatet"
args = list(valg.args)
if args:
if len(args) != 2 or args[0] != "--ut": # noqa: PLR2004 - «--ut <mappe>»
melding = "Bruk: kjor.py notatet [--ut <mappe>]"
raise Bruksfeil(melding)
ut = Path(args[1]).expanduser()
ut = ut.resolve()
if ut == valg.modell or valg.modell in ut.parents:
melding = f"Ut-mappa {ut} ligger inne i modell-mappa {valg.modell}. Velg en annen med --ut <mappe>."
raise Bruksfeil(melding)
if ut.exists() and not ut.is_dir():
melding = f"{ut} finnes og er ikke en mappe. Velg en annen med --ut <mappe>."
raise Bruksfeil(melding)
utskrift = ut / "sim_utskrift.txt"
try:
(ut / "data").mkdir(parents=True, exist_ok=True)
prove = ut / "data" / ".skriveprove"
prove.write_text("", encoding="utf-8")
prove.unlink()
logg = utskrift.open("w", encoding="utf-8")
except OSError as feil:
return kan_ikke_skrive(ut, feil)
print(f"Kjører notatets simuleringer (kjerne/sim.py, uendret) i {ut}. Det tar om lag 1-2 minutter.\n",
flush=True)
miljo = dict(os.environ, PYTHONIOENCODING="utf-8", PYTHONDONTWRITEBYTECODE="1", PYTHONUNBUFFERED="1")
with logg, subprocess.Popen(
[sys.executable, str(valg.modell / "kjerne" / "sim.py")],
cwd=str(ut), stdout=subprocess.PIPE, text=True, encoding="utf-8", env=miljo,
) as prosess:
try:
for linje in prosess.stdout or ():
sys.stdout.write(linje)
logg.write(linje)
except OSError as feil:
prosess.kill()
return kan_ikke_skrive(ut, feil)
if prosess.returncode != 0:
sys.stdout.flush()
print(f"Kjøringen av sim.py stoppet underveis (exit-kode {prosess.returncode}); se meldingene over. "
f"Datafilene i {ut / 'data'} er ikke fullstendige.", file=sys.stderr)
return FEIL_BRUK
antall = len(list((ut / "data").glob("*.dat")))
print(f"\nFerdig: {antall} datafiler i {ut / 'data'}, utskriften i {utskrift}.")
print("«kontroll» sammenligner pluginens egen kjøring med notatet.")
return OK
def laaste_versjoner(modell: Path) -> dict[str, str]:
"""Notatets låste versjoner fra modell/requirements.txt."""
ut = {}
fil = modell / "requirements.txt"
if fil.is_file():
for linje in fil.read_text(encoding="utf-8").splitlines():
navn, skille, versjon = linje.split("#")[0].strip().partition("==")
if skille:
ut[navn.strip().lower()] = versjon.strip()
return ut
def vis_tolk(beskrivelse: Beskrivelse, laast: dict[str, str]) -> list[str]:
"""Linjer som beskriver Python, numpy og scipy i en tolk."""
linjer = [f" Python {beskrivelse['python']} ({beskrivelse['sti']})"]
for navn in ("numpy", "scipy"):
versjon = beskrivelse["numpy"] if navn == "numpy" else beskrivelse["scipy"]
notatet = f" (notatet: {laast[navn]})" if navn in laast else ""
linjer.append(f" {navn}: {versjon or 'mangler'}{notatet}")
return linjer
def samme_som_notatet(beskrivelse: Beskrivelse, laast: dict[str, str]) -> bool:
"""Sjekk om tolken har notatets låste versjoner av numpy og scipy."""
return beskrivelse["numpy"] == laast.get("numpy") and beskrivelse["scipy"] == laast.get("scipy")
def rapport_miljo(valg: Valg) -> None:
"""Skriv ut hva denne Python og den egne mappa har."""
laast = laaste_versjoner(valg.modell)
denne = beskriv_denne()
print("Python som kjører nå:")
print("\n".join(vis_tolk(denne, laast)))
vpy = venv_python(valg.data)
print(f"\nEgen mappe for numpy og scipy: {venv_mappe(valg.data)}")
venv: Beskrivelse | None = None
if not vpy.exists():
print(" finnes ikke. «miljo --opprett» lager den og installerer numpy og scipy fra PyPI "
"(krever internett).")
else:
venv = beskriv_tolk(vpy)
if venv is None:
print(f" finnes, men {vpy} lot seg ikke starte. «miljo --opprett» prøver å lage den på nytt.")
else:
print("\n".join(vis_tolk(venv, laast)))
print()
regner = None
if not mangler(denne):
regner = denne
print("forlop, scenario, notatet og kontroll regner med Python som kjører nå.")
elif venv is not None and not mangler(venv):
regner = venv
print("forlop, scenario, notatet og kontroll regner med Python i den egne mappa.")
else:
hva = ", ".join(mangler(venv if venv is not None else denne))
print(f"forlop, scenario, notatet og kontroll kan ikke regne nå: {hva} mangler.")
if regner is not None:
if samme_som_notatet(regner, laast):
print("Versjonene av numpy og scipy er de samme som notatets, så kontroll kan gi bit for bit.")
else:
print("Versjonene av numpy og scipy er ikke de samme som notatets: kontroll sammenligner da "
"med toleranse, ikke bit for bit.")
def kjor_prosess(kommando: list[str]) -> int:
"""Kjør et program (pip, venv) med utskriften rett til skjermen. Exit-koden tilbake."""
sys.stdout.flush()
try:
return subprocess.run(kommando, check=False).returncode
except OSError as feil:
print(f"Kunne ikke starte {kommando[0]}: {feil}", file=sys.stderr)
return 1
def opprett_miljo(valg: Valg) -> int:
"""Lag den egne mappa og installer numpy og scipy: låste versjoner først, ellers nyeste."""
mappe = venv_mappe(valg.data)
vpy = venv_python(valg.data)
if not vpy.exists():
print(f"Lager egen Python-mappa {mappe} med {sys.executable} ({platform.python_version()}).")
valg.data.mkdir(parents=True, exist_ok=True)
if kjor_prosess([sys.executable, "-m", "venv", str(mappe)]) != 0 or not vpy.exists():
print(f"Kunne ikke lage {mappe}. På noen Linux-systemer må pakken python3-venv installeres først.",
file=sys.stderr)
return MILJO_MANGLER
pip = [str(vpy), "-m", "pip", "install", "--disable-pip-version-check"]
krav = valg.modell / "requirements.txt"
laast = laaste_versjoner(valg.modell)
print(f"\nInstallerer notatets versjoner ({', '.join(f'{n} {v}' for n, v in laast.items())}) fra PyPI.")
toleranse = False
if kjor_prosess([*pip, "-r", str(krav)]) != 0:
print("\nNotatets låste versjoner lot seg ikke installere for denne Python-versjonen. "
"Prøver numpy 2.0 eller nyere og scipy.")
toleranse = True
if kjor_prosess([*pip, "numpy>=2.0", "scipy"]) != 0:
print(f"\nInstallasjonen feilet. Se meldingene over. Mappa {mappe} kan slettes.", file=sys.stderr)
return MILJO_MANGLER
print()
rapport_miljo(valg)
if toleranse:
print("\nNumpy og scipy er ikke notatets låste versjoner. Resultatene kontrolleres da med toleranse, "
"ikke bit for bit.")
return OK
def kommando_miljo(valg: Valg) -> int:
"""Vis miljøet, eller lag den egne mappa med --opprett."""
if valg.args == ["--opprett"]:
return opprett_miljo(valg)
if valg.args:
melding = "Bruk: kjor.py miljo [--opprett]"
raise Bruksfeil(melding)
rapport_miljo(valg)
return OK
def kommando_beskriv(_valg: Valg) -> int:
"""Intern kommando for beskriv_tolk(): skriv beskrivelsen av denne tolken som JSON."""
print(json.dumps(beskriv_denne()))
return OK
def kommando_hjelp(_valg: Valg) -> int:
"""Skriv bruken."""
print(HJELP, end="")
return OK
def kommando_regner(valg: Valg) -> int:
"""Kommandoene som trenger numpy og scipy: velg Python først (se toppen av fila)."""
if valg.kommando == "scenario" and not (valg.modell / "scenario.py").is_file():
return kjor_modul(valg, "scenario") # sier at kommandoen ikke er på plass ennå
if valg.kommando == "forlop" and valg.args and not Path(valg.args[0]).exists():
print(f"Fant ikke fila {valg.args[0]}.", file=sys.stderr)
return FEIL_BRUK
tolk = velg_tolk(valg.data)
if tolk.hvor == "venv" and tolk.python is not None:
return kjor_paa_nytt(tolk.python, valg.argv)
if tolk.hvor == "mangler" and valg.kommando != "kontroll": # kontroll gjør det den kan uten
print(melding_mangler(valg, tolk), file=sys.stderr)
return MILJO_MANGLER
handlinger: dict[str, Callable[[Valg], int]] = {
"forlop": kommando_forlop, "notatet": kommando_notatet,
"scenario": lambda v: kjor_modul(v, "scenario"), "kontroll": lambda v: kjor_modul(v, "kontroll"),
}
return handlinger[valg.kommando](valg)
def utfor(valg: Valg) -> int:
"""Utfør kommandoen."""
handlinger: dict[str, Callable[[Valg], int]] = {
"hjelp": kommando_hjelp, "_beskriv": kommando_beskriv,
"diagnose": kommando_diagnose, "miljo": kommando_miljo,
"arbeidskopi": lambda v: kjor_modul(v, "arbeidskopi"),
}
return handlinger.get(valg.kommando, kommando_regner)(valg)
def main(argv: list[str]) -> int:
"""Inngangen. Returnerer exit-koden."""
try:
if not argv:
print(HJELP, end="", file=sys.stderr)
return FEIL_BRUK
return utfor(tolk_argumenter(argv))
except Bruksfeil as feil:
sys.stdout.flush()
print(f"{feil}\nSkriv «{python_navn()} kjor.py --hjelp» for bruk.", file=sys.stderr)
return FEIL_BRUK
except OSError as feil: # exit-kode 1 er forbeholdt kontroll som finner forskjeller
sys.stdout.flush()
print(f"Kan ikke lese eller skrive {feil.filename or ''}: {feil.strerror or feil}.", file=sys.stderr)
return FEIL_BRUK
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))