The core in modell/kjerne/ is an unmodified copy of sim.py, diagnose.py and forlop.py from den-dynamiske-rettsstaten @2badc25, pinned by sha256. kjor.py is the single entry point; kontroll.py reproduces the note's 22 data files and printed results. Skills: diagnose and forlop (Norwegian). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
432 lines
18 KiB
Python
432 lines
18 KiB
Python
# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE.
|
|
"""Gjenskapingskontroll: sjekk at modellen i denne mappa regner som notatet.
|
|
|
|
Bruk:
|
|
python3 kjor.py kontroll [--rask]
|
|
python3 kontroll.py [--rask] (med en Python som har numpy og scipy)
|
|
|
|
Fire kontroller, hver med én linje dom og detaljer bare ved avvik:
|
|
1. Kjernen: sha256 for filene i kjerne/ mot kjerne/KILDE.json.
|
|
2. diagnose.py på begge eksempelfilene mot fasit/ (tekst).
|
|
3. forlop.py på begge eksempelfilene mot fasit/ (tekst; trenger numpy og scipy).
|
|
4. Full kjøring av sim.py i en midlertidig mappe: datafilene mot fasit/data/ byte for byte,
|
|
med største absolutte avvik per kolonne når en fil er ulik, og utskriften mot
|
|
fasit/sim_utskrift.txt. Om lag 1-2 min; --rask hopper over den.
|
|
I tillegg kjøres grensetilfelle() i hver utvidelse i utvidelser/; hvert avvik skal være eksakt 0.
|
|
|
|
Nivået til slutt:
|
|
- «bit for bit som notatet»: alt er likt byte for byte.
|
|
- «innenfor toleranse»: teksten er lik, og hvert tall avviker med høyst én enhet i siste
|
|
viste siffer (det som kan skje med andre versjoner av numpy og scipy, eller en annen maskin).
|
|
- «endret»: alt annet. I en arbeidskopi (mappa over modell/ har LES-MEG.md, som
|
|
arbeidskopi.py lager) er det en opplysning, ikke en feil: detaljene sier hvilke av
|
|
notatets resultater som har flyttet seg, og hvor mye.
|
|
|
|
Exit-kode: 0 for bit for bit og innenfor toleranse, 1 for endret, 3 når numpy eller scipy
|
|
mangler for noe av kontrollen (det som kunne kjøres, er kjørt), 2 for feil bruk.
|
|
|
|
Grensesnitt: main(argv: list[str]) -> int, som kjor.py kaller.
|
|
Utvidelser: utvidelser/<navn>.py (ikke filer som begynner med _) med
|
|
grensetilfelle() -> list av (beskrivelse, største absolutte avvik mot kjernen). Hver
|
|
kjøres i en egen prosess med modell/kjerne og modell/ først i sys.path.
|
|
Bare standardbiblioteket her; kjernen kjøres som egne prosesser med samme Python.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import difflib
|
|
import hashlib
|
|
import importlib
|
|
import json
|
|
import math
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
MODELL = Path(__file__).resolve().parent
|
|
KJERNE = MODELL / "kjerne"
|
|
ROT = MODELL.parent
|
|
FASIT = ROT / "fasit"
|
|
EKSEMPLER = ROT / "eksempler"
|
|
EKSEMPELFILER = ("eksempel_domstol", "norge_2025")
|
|
ARBEIDSKOPI_MERKE = "LES-MEG.md"
|
|
|
|
LIK, TOLERANSE, ENDRET, IKKE_KJORT = "lik", "toleranse", "endret", "ikke kjørt"
|
|
RANG = {LIK: 0, TOLERANSE: 1, IKKE_KJORT: 2, ENDRET: 3}
|
|
MAKS_LINJER = 40 # linjer med forskjeller som vises per sammenligning
|
|
SLINGRING = 1e-9 # relativ slingring når avviket sammenlignes med én enhet i siste siffer
|
|
|
|
TALL = re.compile(r"[-+]?\d+(?:[.,]\d+)?(?:[eE][-+]?\d+)?")
|
|
|
|
MILJO = dict(os.environ, PYTHONIOENCODING="utf-8", PYTHONDONTWRITEBYTECODE="1")
|
|
|
|
UTVIDELSE_KJORER = """\
|
|
import importlib.util, json, sys
|
|
modell, fil = sys.argv[1], sys.argv[2]
|
|
sys.path[:0] = [modell + "/kjerne", modell]
|
|
spec = importlib.util.spec_from_file_location("utvidelse", fil)
|
|
modul = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(modul)
|
|
print()
|
|
print(json.dumps([[str(b), float(a)] for b, a in modul.grensetilfelle()]))
|
|
"""
|
|
|
|
|
|
@dataclass
|
|
class Dom:
|
|
"""Resultatet av én kontroll."""
|
|
|
|
status: str
|
|
linje: str
|
|
detaljer: list[str] = field(default_factory=list)
|
|
avvik: float = 0.0
|
|
|
|
|
|
@dataclass
|
|
class Sammenligning:
|
|
"""Resultatet av å sammenligne en fil eller en utskrift med fasit."""
|
|
|
|
status: str
|
|
avvik: float = 0.0
|
|
detaljer: list[str] = field(default_factory=list)
|
|
|
|
|
|
# ---------------- sammenligning ----------------
|
|
|
|
def enhet(tall: str) -> float:
|
|
"""Én enhet i siste viste siffer: «1,28» -> 0,01, «7.21» -> 0,01, «1.11e-15» -> 1e-17."""
|
|
mantisse, _, eksponent = tall.lower().partition("e")
|
|
desimaler = len(mantisse.replace(",", ".").partition(".")[2])
|
|
return float(10.0 ** (int(eksponent or "0") - desimaler))
|
|
|
|
|
|
def verdi(tall: str) -> float:
|
|
"""Tallet i en tekst, med punktum eller komma som desimaltegn."""
|
|
return float(tall.replace(",", "."))
|
|
|
|
|
|
def innenfor(fasit: str, naa: str) -> tuple[bool, float]:
|
|
"""Er to tall like innenfor én enhet i siste viste siffer? Returnerer også avviket."""
|
|
try:
|
|
a, b = verdi(fasit), verdi(naa)
|
|
except ValueError:
|
|
return fasit == naa, 0.0 if fasit == naa else math.inf
|
|
avvik = abs(a - b) if math.isfinite(a) and math.isfinite(b) else (0.0 if a == b else math.inf)
|
|
return avvik <= max(enhet(fasit), enhet(naa)) * (1 + SLINGRING), avvik
|
|
|
|
|
|
def vis_tall(x: float) -> str:
|
|
"""Et avvik med norsk desimaltegn."""
|
|
if x == 0:
|
|
return "0"
|
|
if math.isinf(x):
|
|
return "uendelig"
|
|
return f"{x:.1e}".replace(".", ",")
|
|
|
|
|
|
def forskjeller(fasit: list[str], naa: list[str]) -> list[str]:
|
|
"""Linjene som er ulike, som en kort diff (notatet / nå)."""
|
|
linjer = list(difflib.unified_diff(fasit, naa, "notatet", "nå", lineterm="", n=0))
|
|
if len(linjer) > MAKS_LINJER:
|
|
linjer = [*linjer[:MAKS_LINJER], f"... ({len(linjer) - MAKS_LINJER} linjer til)"]
|
|
return linjer
|
|
|
|
|
|
def sammenlign_tekst(fasit: str, naa: str) -> Sammenligning:
|
|
"""Sammenlign en utskrift med fasit: lik, innenfor toleranse eller endret."""
|
|
if fasit == naa:
|
|
return Sammenligning(LIK)
|
|
a, b = fasit.splitlines(), naa.splitlines()
|
|
detaljer = forskjeller(a, b)
|
|
if len(a) != len(b):
|
|
return Sammenligning(ENDRET, math.inf, detaljer)
|
|
storst = 0.0
|
|
for linje_a, linje_b in zip(a, b):
|
|
if linje_a == linje_b:
|
|
continue
|
|
if TALL.sub("#", linje_a) != TALL.sub("#", linje_b):
|
|
return Sammenligning(ENDRET, math.inf, detaljer)
|
|
for x, y in zip(TALL.findall(linje_a), TALL.findall(linje_b)):
|
|
ok, avvik = innenfor(x, y)
|
|
if not ok:
|
|
return Sammenligning(ENDRET, avvik, detaljer)
|
|
storst = max(storst, avvik)
|
|
return Sammenligning(TOLERANSE, storst, detaljer)
|
|
|
|
|
|
def les_dat(tekst: str) -> tuple[list[str], list[list[str]]]:
|
|
"""Kolonnenavn og rader i en datafil fra sim.py (første linje er kolonnenavnene)."""
|
|
linjer = [linje.split() for linje in tekst.splitlines() if linje.strip()]
|
|
if not linjer:
|
|
return [], []
|
|
return linjer[0], linjer[1:]
|
|
|
|
|
|
def sammenlign_dat(fasit: str, naa: str) -> Sammenligning:
|
|
"""Sammenlign en datafil med fasit; ved avvik største absolutte avvik per kolonne."""
|
|
if fasit == naa:
|
|
return Sammenligning(LIK)
|
|
kol_a, rader_a = les_dat(fasit)
|
|
kol_b, rader_b = les_dat(naa)
|
|
if kol_a != kol_b:
|
|
return Sammenligning(ENDRET, math.inf,
|
|
[f"kolonnene er ulike: notatet {' '.join(kol_a)}, nå {' '.join(kol_b)}"])
|
|
if len(rader_a) != len(rader_b) or any(len(x) != len(y) for x, y in zip(rader_a, rader_b)):
|
|
return Sammenligning(ENDRET, math.inf,
|
|
[f"antall rader er ulikt: notatet {len(rader_a)}, nå {len(rader_b)}"])
|
|
per_kolonne = [0.0] * len(kol_a)
|
|
alle_innenfor = True
|
|
for rad_a, rad_b in zip(rader_a, rader_b):
|
|
for j, (x, y) in enumerate(zip(rad_a, rad_b)):
|
|
if x == y:
|
|
continue
|
|
ok, avvik = innenfor(x, y)
|
|
alle_innenfor = alle_innenfor and ok
|
|
per_kolonne[j] = max(per_kolonne[j], avvik)
|
|
deler = [f"{navn} {vis_tall(avvik)}" for navn, avvik in zip(kol_a, per_kolonne) if avvik > 0]
|
|
detaljer = ["største absolutte avvik per kolonne: " + (", ".join(deler) or "ingen (bare formatering)")]
|
|
return Sammenligning(TOLERANSE if alle_innenfor else ENDRET, max(per_kolonne), detaljer)
|
|
|
|
|
|
def verst(statuser: list[str]) -> str:
|
|
"""Den dårligste statusen i en liste."""
|
|
return max(statuser, key=RANG.__getitem__, default=LIK)
|
|
|
|
|
|
# ---------------- miljø og prosesser ----------------
|
|
|
|
def har_numpy_scipy() -> bool:
|
|
"""Sjekk om denne Python har numpy 2.0 eller nyere (numpy.trapezoid) og scipy."""
|
|
try:
|
|
numpy = importlib.import_module("numpy")
|
|
importlib.import_module("scipy.optimize")
|
|
except ImportError:
|
|
return False
|
|
return hasattr(numpy, "trapezoid")
|
|
|
|
|
|
def kjor_python(args: list[str], cwd: Path) -> subprocess.CompletedProcess[str]:
|
|
"""Kjør denne Python med args i mappa cwd og fang utskriften."""
|
|
return subprocess.run([sys.executable, *args], cwd=str(cwd), env=MILJO, capture_output=True, text=True,
|
|
encoding="utf-8", check=False)
|
|
|
|
|
|
def feilmelding(prosess: subprocess.CompletedProcess[str]) -> list[str]:
|
|
"""De siste linjene på stderr, til detaljene."""
|
|
linjer = prosess.stderr.strip().splitlines()
|
|
return [f"stoppet med exit-kode {prosess.returncode}:", *linjer[-MAKS_LINJER // 2:]]
|
|
|
|
|
|
def sha256(fil: Path) -> str:
|
|
"""sha256 av en fil."""
|
|
return hashlib.sha256(fil.read_bytes()).hexdigest()
|
|
|
|
|
|
# ---------------- kontrollene ----------------
|
|
|
|
def kontroll_kjerne() -> Dom:
|
|
"""1. Kjernen mot sha256 i KILDE.json."""
|
|
kildefil = KJERNE / "KILDE.json"
|
|
if not kildefil.is_file():
|
|
return Dom(ENDRET, f"Kjernen: fant ikke {kildefil}, så kjernen kan ikke sammenlignes med notatet.")
|
|
kilde = json.loads(kildefil.read_text(encoding="utf-8"))
|
|
endret = []
|
|
for navn, info in kilde["filer"].items():
|
|
fil = KJERNE / navn
|
|
if not fil.is_file():
|
|
endret.append(f"{navn} mangler")
|
|
elif sha256(fil) != info["sha256"]:
|
|
endret.append(f"{navn} er endret")
|
|
versjon = f"{kilde['repo']} @ {kilde['commit'][:7]}"
|
|
if not endret:
|
|
return Dom(LIK, f"Kjernen: uendret fra notatet ({len(kilde['filer'])} filer, {versjon}).")
|
|
return Dom(ENDRET, f"Kjernen: endret fra notatet ({versjon}): {', '.join(endret)}.")
|
|
|
|
|
|
def kontroll_utskrift(skript: str, mappe: Path) -> Dom:
|
|
"""2 og 3. Utskriften fra diagnose.py eller forlop.py på begge eksempelfilene mot fasit."""
|
|
statuser, detaljer, storst = [], [], 0.0
|
|
for eksempel in EKSEMPELFILER:
|
|
fasitfil = FASIT / f"{skript}_{eksempel}.txt"
|
|
inndata = EKSEMPLER / f"{eksempel}.json"
|
|
mangler = [str(f) for f in (fasitfil, inndata) if not f.is_file()]
|
|
if mangler:
|
|
statuser.append(ENDRET)
|
|
detaljer.append(f"{eksempel}: fant ikke {', '.join(mangler)}")
|
|
continue
|
|
prosess = kjor_python([str(KJERNE / f"{skript}.py"), str(inndata)], mappe)
|
|
if prosess.returncode != 0:
|
|
statuser.append(ENDRET)
|
|
detaljer += [f"{eksempel}:", *(" " + linje for linje in feilmelding(prosess))]
|
|
continue
|
|
resultat = sammenlign_tekst(fasitfil.read_text(encoding="utf-8"), prosess.stdout)
|
|
statuser.append(resultat.status)
|
|
storst = max(storst, resultat.avvik)
|
|
if resultat.status != LIK:
|
|
detaljer += [f"{eksempel}:", *(" " + linje for linje in resultat.detaljer)]
|
|
status = verst(statuser)
|
|
begge = " og ".join(EKSEMPELFILER)
|
|
linje = {
|
|
LIK: f"{skript}: lik notatet for {begge}.",
|
|
TOLERANSE: f"{skript}: innenfor toleranse for {begge} (største avvik {vis_tall(storst)}).",
|
|
ENDRET: f"{skript}: endret (se under).",
|
|
}[status]
|
|
return Dom(status, linje, detaljer, storst)
|
|
|
|
|
|
def kontroll_full(mappe: Path) -> Dom:
|
|
"""4. Full kjøring av sim.py i en midlertidig mappe mot fasit/data og fasit/sim_utskrift.txt."""
|
|
print(" (kjører sim.py, om lag 1-2 minutter ...)", flush=True)
|
|
prosess = kjor_python([str(KJERNE / "sim.py")], mappe)
|
|
if prosess.returncode != 0:
|
|
return Dom(ENDRET, "Full kjøring: sim.py stoppet med en feil.", feilmelding(prosess), math.inf)
|
|
fasit_data = FASIT / "data"
|
|
navn_fasit = sorted(f.name for f in fasit_data.glob("*.dat"))
|
|
navn_naa = sorted(f.name for f in (mappe / "data").glob("*.dat"))
|
|
statuser, detaljer, storst, like = [], [], 0.0, 0
|
|
for navn in sorted(set(navn_fasit) | set(navn_naa)):
|
|
if navn not in navn_naa or navn not in navn_fasit:
|
|
statuser.append(ENDRET)
|
|
detaljer.append(f"{navn}: " + ("mangler i kjøringen" if navn not in navn_naa else "finnes ikke i fasit"))
|
|
continue
|
|
resultat = sammenlign_dat((fasit_data / navn).read_text(encoding="utf-8"),
|
|
(mappe / "data" / navn).read_text(encoding="utf-8"))
|
|
statuser.append(resultat.status)
|
|
storst = max(storst, resultat.avvik)
|
|
if resultat.status == LIK:
|
|
like += 1
|
|
else:
|
|
dom = "innenfor toleranse" if resultat.status == TOLERANSE else "endret"
|
|
detaljer.append(f"{navn} ({dom}): {'; '.join(resultat.detaljer)}")
|
|
utskriftfil = FASIT / "sim_utskrift.txt"
|
|
if utskriftfil.is_file():
|
|
utskrift = sammenlign_tekst(utskriftfil.read_text(encoding="utf-8"), prosess.stdout)
|
|
else:
|
|
utskrift = Sammenligning(ENDRET, math.inf, [f"fant ikke {utskriftfil}"])
|
|
statuser.append(utskrift.status)
|
|
storst = max(storst, utskrift.avvik)
|
|
if utskrift.status != LIK:
|
|
detaljer += ["utskriften fra sim.py:", *(" " + linje for linje in utskrift.detaljer)]
|
|
status = verst(statuser)
|
|
tekst_utskrift = {LIK: "lik", TOLERANSE: "innenfor toleranse", ENDRET: "endret"}[utskrift.status]
|
|
linje = (f"Full kjøring: {like} av {len(navn_fasit)} datafiler like byte for byte med notatet; "
|
|
f"utskriften {tekst_utskrift}.")
|
|
return Dom(status, linje, detaljer, storst)
|
|
|
|
|
|
def kontroll_utvidelser(mappe: Path, *, regnemiljo: bool) -> Dom:
|
|
"""Grensetilfellet i hver utvidelse skal gjenskape kjernen eksakt (avvik 0)."""
|
|
utvidelser = sorted(f for f in (MODELL / "utvidelser").glob("*.py") if not f.name.startswith("_"))
|
|
if not utvidelser:
|
|
return Dom(LIK, "Utvidelser: ingen å kontrollere.")
|
|
if not regnemiljo:
|
|
return Dom(IKKE_KJORT, f"Utvidelser: ikke kjørt ({len(utvidelser)} stk.), numpy og scipy mangler.")
|
|
statuser, detaljer, storst = [], [], 0.0
|
|
for fil in utvidelser:
|
|
prosess = kjor_python(["-c", UTVIDELSE_KJORER, str(MODELL), str(fil)], mappe)
|
|
try:
|
|
tilfeller = json.loads(prosess.stdout.strip().splitlines()[-1]) if prosess.returncode == 0 else None
|
|
except (ValueError, IndexError):
|
|
tilfeller = None
|
|
if tilfeller is None:
|
|
statuser.append(ENDRET)
|
|
detaljer += [f"{fil.stem}: grensetilfelle() kunne ikke kjøres", *(" " + linje for linje in
|
|
feilmelding(prosess))]
|
|
continue
|
|
if not tilfeller:
|
|
statuser.append(ENDRET)
|
|
detaljer.append(f"{fil.stem}: grensetilfelle() ga ingen tilfeller; en utvidelse skal ha minst ett.")
|
|
continue
|
|
for beskrivelse, avvik in tilfeller:
|
|
storst = max(storst, abs(avvik))
|
|
if avvik != 0.0:
|
|
statuser.append(ENDRET)
|
|
detaljer.append(f"{fil.stem}: {beskrivelse}: avvik {vis_tall(abs(avvik))}, skal være eksakt 0")
|
|
else:
|
|
statuser.append(LIK)
|
|
status = verst(statuser)
|
|
navn = ", ".join(f.stem for f in utvidelser)
|
|
linje = (f"Utvidelser: grensetilfellene gjenskaper kjernen eksakt ({navn})." if status == LIK
|
|
else f"Utvidelser: grensetilfellet gjenskaper ikke kjernen eksakt ({navn}; se under).")
|
|
return Dom(status, linje, detaljer, storst)
|
|
|
|
|
|
# ---------------- rapporten ----------------
|
|
|
|
def niva(dommer: list[Dom], *, rask: bool, arbeidskopi: bool) -> list[str]:
|
|
"""Linjene om nivået som er oppnådd."""
|
|
kjort = [d for d in dommer if d.status != IKKE_KJORT]
|
|
status = verst([d.status for d in kjort])
|
|
deler = (["uten full kjøring, --rask"] if rask else []) + (
|
|
["bare det som er kjørt"] if any(d.status == IKKE_KJORT for d in dommer) else [])
|
|
hva = f" ({'; '.join(deler)})" if deler else ""
|
|
if status == LIK:
|
|
linjer = [f"Nivå{hva}: bit for bit som notatet."]
|
|
elif status == TOLERANSE:
|
|
storst = max(d.avvik for d in kjort)
|
|
linjer = [(f"Nivå{hva}: innenfor toleranse (største avvik {vis_tall(storst)}): hvert tall avviker med "
|
|
"høyst én enhet i siste viste siffer.")]
|
|
else:
|
|
endret = [d.linje.split(":")[0] for d in kjort if d.status == ENDRET]
|
|
linjer = [f"Nivå{hva}: endret: {', '.join(endret)}."]
|
|
if arbeidskopi:
|
|
linjer.append("Dette er en arbeidskopi, så det er en opplysning, ikke en feil: detaljene over viser "
|
|
"hvilke av notatets resultater som har flyttet seg, og hvor mye.")
|
|
else:
|
|
linjer.append("Pluginens modell skal regne som notatet, så dette er en feil.")
|
|
if any(d.status == IKKE_KJORT for d in dommer):
|
|
linjer.append(f"Ikke kjørt fordi numpy eller scipy mangler i {sys.executable}. "
|
|
"«kjor.py miljo --opprett» installerer dem i en egen mappe.")
|
|
return linjer
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
"""Kjør kontrollene og skriv dommen. Returnerer exit-koden."""
|
|
if argv not in ([], ["--rask"]):
|
|
print("Bruk: kjor.py kontroll [--rask]", file=sys.stderr)
|
|
return 2
|
|
rask = argv == ["--rask"]
|
|
arbeidskopi = (ROT / ARBEIDSKOPI_MERKE).is_file()
|
|
regnemiljo = har_numpy_scipy()
|
|
print(f"Kontroll av modellen i {MODELL}" + (" (arbeidskopi)" if arbeidskopi else "") + "\n", flush=True)
|
|
dommer: list[Dom] = []
|
|
|
|
def vis(nummer: str, dom: Dom) -> None:
|
|
dommer.append(dom)
|
|
print(f"{nummer} {dom.linje}")
|
|
for linje in dom.detaljer:
|
|
print(f" {linje}")
|
|
sys.stdout.flush()
|
|
|
|
with tempfile.TemporaryDirectory(prefix="dynamisk-rettsstat-") as tmp:
|
|
mappe = Path(tmp)
|
|
vis("1.", kontroll_kjerne())
|
|
vis("2.", kontroll_utskrift("diagnose", mappe))
|
|
if regnemiljo:
|
|
vis("3.", kontroll_utskrift("forlop", mappe))
|
|
else:
|
|
vis("3.", Dom(IKKE_KJORT, "forlop: ikke kjørt, numpy og scipy mangler."))
|
|
if rask:
|
|
print("4. Full kjøring: hoppet over (--rask).")
|
|
elif regnemiljo:
|
|
vis("4.", kontroll_full(mappe))
|
|
else:
|
|
vis("4.", Dom(IKKE_KJORT, "Full kjøring: ikke kjørt, numpy og scipy mangler."))
|
|
vis("5.", kontroll_utvidelser(mappe, regnemiljo=regnemiljo))
|
|
print()
|
|
print("\n".join(niva(dommer, rask=rask, arbeidskopi=arbeidskopi)))
|
|
statuser = [d.status for d in dommer]
|
|
if ENDRET in statuser:
|
|
return 1
|
|
if IKKE_KJORT in statuser:
|
|
return 3
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.dont_write_bytecode = True
|
|
sys.exit(main(sys.argv[1:]))
|