Zum Inhalt springen
aviral gupta

// I5.5 · ca. 40 Min. · Aufbau

pyproject.toml und ein installierbares Paket

Nach dieser Lektion machen Sie aus einem Ordner voller Code ein installierbares Paket mit pyproject.toml, src-Layout, Kommandozeilen-Einstieg und Tests, im editierbaren Modus installiert.

Lektion 5 von 5 in I5 Tests und Projektwerkzeuge

Ende des Moduls

Danach können Sie

  • pyproject.toml schreiben: [build-system], name, version, requires-python
  • Ein Paket unter src/ anlegen und mit pip install -e . editierbar installieren
  • Eine Funktion main(argv) -> int als Konsolenbefehl bereitstellen und testen
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus der vorigen Lektion: Welcher Befehl installiert jedes Paket aus der requirements.txt einer Kollegin?

  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen. Was wird ausgegeben?

    import tomllib
    
    text = """
    [project]
    name = "wordstat"
    version = "0.1.0"
    
    [project.scripts]
    wordstat = "wordstat.cli:main"
    """
    data = tomllib.loads(text)
    print(data["project"]["scripts"])
  3. Üben · Aufgabe 3 von 7

    Setzen Sie den Tabellennamen ein, der wordstat bei der Installation des Pakets zu einem Befehl macht.

    [project.____]
    wordstat = "wordstat.cli:main"
    [project.]
  4. Üben · Aufgabe 4 von 7

    Ordnen Sie jeder Datei des Projekts wordstat ihre Aufgabe zu.

  5. Üben · Aufgabe 5 von 7

    Ein Einstiegspunkt wird als modul:funktion geschrieben. Was gibt das aus?

    module, _, func = "wordstat.cli:main".partition(":")
    print(module, func)
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Welche Schlüssel hat die Tabelle project?

    import tomllib
    
    data = tomllib.loads("""
    [project]
    name = "wordstat"
    version = "0.1.0"
    
    [project.scripts]
    wordstat = "wordstat.cli:main"
    """)
    print(sorted(data["project"]))
  7. Anwenden · Aufgabe 7 von 7

    Kleine Aufgabe. Schreiben Sie die pyproject.toml für ein Projekt notes-cli: Das Paket notes_cli liegt in src/, braucht Python 3.10 oder neuer, wird mit setuptools gebaut und installiert einen Befehl notes, der main in notes_cli/cli.py aufruft. Führen Sie dann auf Ihrem Rechner in einer aktivierten Umgebung python -m pip install -e . aus und tippen Sie notes.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Ein Paketgerüst, gelesen wie von pip

Dieses Programm schreibt ein kleines Projekt im src-Layout in einen temporären Ordner: pyproject.toml, das Paket und einen Test. Es liest pyproject.toml mit tomllib und tut dann von Hand, was ein installierter Konsolenbefehl tut: Es setzt src auf sys.path, was python -m pip install -e . für Sie erledigen würde, teilt den Einstiegspunkt am Doppelpunkt, importiert das Modul und ruft die Funktion auf.

main.py

import importlib
import sys
import tempfile
import tomllib
from pathlib import Path

FILES = {
    "pyproject.toml": """[build-system]
requires = ["setuptools >= 77.0.3"]
build-backend = "setuptools.build_meta"

[project]
name = "wordstat"
version = "0.1.0"
requires-python = ">=3.10"

[project.scripts]
wordstat = "wordstat.cli:main"
""",
    "src/wordstat/__init__.py": '__version__ = "0.1.0"\n',
    "src/wordstat/cli.py": """def main() -> int:
    print("hello from wordstat")
    return 0
""",
    "tests/test_cli.py": """from wordstat.cli import main


def test_main_returns_0() -> None:
    assert main() == 0
""",
}

root = Path(tempfile.mkdtemp())
for name, text in FILES.items():
    (root / name).parent.mkdir(parents=True, exist_ok=True)
    (root / name).write_text(text, encoding="utf-8")
print("files:", sorted(p.relative_to(root).as_posix() for p in root.rglob("*") if p.is_file()))

# tomllib.load needs the file opened in binary mode.
with open(root / "pyproject.toml", "rb") as f:
    project = tomllib.load(f)["project"]
print("project:", project["name"], project["version"], project["requires-python"])

# pip install -e . makes src/ importable; here we add it to sys.path by hand.
sys.path.insert(0, str(root / "src"))
command, target = next(iter(project["scripts"].items()))
module_name, _, function_name = target.partition(":")
function = getattr(importlib.import_module(module_name), function_name)
print(f"the {command} command runs {module_name}.{function_name}()")
status = function()
print("exit status:", status)

Ausführen mit

python main.py

Ausgabe

files: ['pyproject.toml', 'src/wordstat/__init__.py', 'src/wordstat/cli.py', 'tests/test_cli.py']
project: wordstat 0.1.0 >=3.10
the wordstat command runs wordstat.cli.main()
hello from wordstat
exit status: 0
  • Nur pyproject.toml liegt mit den Tests im Projektordner; der importierbare Code liegt unter src/.
  • tomllib.load bekommt die mit "rb" geöffnete Datei; eine Datei im Textmodus löst TypeError aus.
  • Die letzten drei Zeilen sind das, was die Eingabe wordstat nach pip install -e . tut, nur dass dort sys.exit den Status bekommt.

Übungen

Übung 1 von 3

Schritt 1: die pyproject.toml

Bauschritt 1 von 3 des Pakets wordstat. PYPROJECT enthält den Text seiner pyproject.toml. Ergänzen Sie eine Tabelle [build-system] für setuptools, requires-python = ">=3.10" und eine Tabelle [project.scripts], die den Befehl wordstat wordstat.cli:main aufrufen lässt. Die Tests lesen sie mit tomllib. Speichern Sie den Text auf Ihrem Rechner als pyproject.toml im Projektordner.

Diese Übung braucht Python auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.

Hinweise
  1. Hinweis 1

    Tabellen beginnen mit einer Kopfzeile in eigener Zeile: [build-system], [project], [project.scripts].

  2. Hinweis 2

    Der Packaging User Guide nennt die Werte für setuptools: requires = ["setuptools >= 77.0.3"] und build-backend = "setuptools.build_meta".

  3. Hinweis 3

    Strings brauchen in TOML Anführungszeichen: requires-python = ">=3.10" und wordstat = "wordstat.cli:main".

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import tomllib

PYPROJECT = """
[build-system]
requires = ["setuptools >= 77.0.3"]
build-backend = "setuptools.build_meta"

[project]
name = "wordstat"
version = "0.1.0"
description = "Show the most common words in a text file"
requires-python = ">=3.10"

[project.scripts]
wordstat = "wordstat.cli:main"
"""

if __name__ == "__main__":
    data = tomllib.loads(PYPROJECT)
    print(data["project"]["name"], data["project"]["version"])
    print("scripts:", data["project"].get("scripts", {}))
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

import tomllib

# Add a [build-system] table for setuptools, requires-python = ">=3.10",
# and a [project.scripts] table with wordstat = "wordstat.cli:main".
PYPROJECT = """
[project]
name = "wordstat"
version = "0.1.0"
description = "Show the most common words in a text file"
"""

if __name__ == "__main__":
    data = tomllib.loads(PYPROJECT)
    print(data["project"]["name"], data["project"]["version"])
    print("scripts:", data["project"].get("scripts", {}))

test_main.py

import tomllib

import main


def load():
    try:
        return tomllib.loads(main.PYPROJECT)
    except tomllib.TOMLDecodeError as err:
        raise AssertionError(f"PYPROJECT ist kein gültiges TOML: {err}") from None


def test_build_system():
    """[build-system] nennt, was zu installieren ist, und das Backend zum Bauen"""
    build = load().get("build-system")
    assert build is not None, "ergänzen Sie eine Tabelle [build-system]"
    assert build.get("requires"), "[build-system] braucht requires, eine Liste wie [\"setuptools >= 77.0.3\"]"
    assert build.get("build-backend") == "setuptools.build_meta", "für setuptools ist build-backend \"setuptools.build_meta\""


def test_name_and_version():
    """[project] hat den Namen wordstat und eine Version"""
    project = load().get("project", {})
    assert project.get("name") == "wordstat", f"name ist {project.get('name')!r}; es muss \"wordstat\""
    assert project.get("version"), "ergänzen Sie eine Version, etwa \"0.1.0\""


def test_requires_python():
    """requires-python nennt das älteste Python, 3.10"""
    project = load().get("project", {})
    assert project.get("requires-python") == ">=3.10", f"requires-python ist {project.get('requires-python')!r}; nehmen Sie \">=3.10\""


def test_console_script():
    """[project.scripts] ordnet dem Befehl wordstat wordstat.cli:main zu"""
    scripts = load().get("project", {}).get("scripts", {})
    assert scripts.get("wordstat") == "wordstat.cli:main", f"[project.scripts] enthält {scripts}; ergänzen Sie wordstat = \"wordstat.cli:main\""

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Übung 2 von 3

Schritt 2: der Kommandozeilen-Einstieg

Schritt 2 von 3. main.py ist src/wordstat/cli.py; die Paketdateien enthalten top_words. Vervollständigen Sie main(argv): Ergänzen Sie eine Option -n/--top (ein int, Standard 3) und reichen Sie sie an top_words weiter; existiert die Datei nicht, geben Sie wordstat: no such file: PFAD auf stderr aus und liefern 1, statt abzustürzen. Bei Erfolg liefern Sie 0. Lokal, nach pip install -e ., starten Sie wordstat story.txt -n 2.

Diese Übung braucht Python auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.

Hinweise
  1. Hinweis 1

    parser.add_argument("-n", "--top", type=int, default=3) macht args.top zu einem int.

  2. Hinweis 2

    Legen Sie um den Aufruf von read_text ein try/except FileNotFoundError.

  3. Hinweis 3

    Im except-Block: print(f"wordstat: no such file: {args.path}", file=sys.stderr) und return 1.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import argparse
import sys
from pathlib import Path

from wordstat.core import top_words


def main(argv: list[str] | None = None) -> int:
    """Entry point of the wordstat command: wordstat = "wordstat.cli:main"."""
    parser = argparse.ArgumentParser(prog="wordstat", description="Show the most common words in a file.")
    parser.add_argument("path", help="the text file to read")
    parser.add_argument("-n", "--top", type=int, default=3, help="how many words to show")
    args = parser.parse_args(argv)
    try:
        text = Path(args.path).read_text(encoding="utf-8")
    except FileNotFoundError:
        print(f"wordstat: no such file: {args.path}", file=sys.stderr)
        return 1
    for word, count in top_words(text, args.top):
        print(f"{count} {word}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

import argparse
import sys
from pathlib import Path

from wordstat.core import top_words


def main(argv: list[str] | None = None) -> int:
    """Entry point of the wordstat command: wordstat = "wordstat.cli:main"."""
    parser = argparse.ArgumentParser(prog="wordstat", description="Show the most common words in a file.")
    parser.add_argument("path", help="the text file to read")
    # Add -n/--top (an int, default 3), and return 1 with a message on
    # stderr when the file does not exist.
    args = parser.parse_args(argv)
    text = Path(args.path).read_text(encoding="utf-8")
    for word, count in top_words(text):
        print(f"{count} {word}")
    return 0


if __name__ == "__main__":
    sys.exit(main())

test_main.py

import contextlib
import io
import tempfile
from pathlib import Path

import main

TEXT = "The cat sat. The cat ran! The end."


def run(*args):
    out, err = io.StringIO(), io.StringIO()
    with tempfile.TemporaryDirectory() as tmp:
        path = Path(tmp, "story.txt")
        path.write_text(TEXT, encoding="utf-8")
        argv = [str(path) if arg == "FILE" else arg for arg in args]
        with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
            status = main.main(argv)
    return status, out.getvalue(), err.getvalue()


def test_prints_top_three():
    """wordstat FILE gibt die drei häufigsten Wörter aus und liefert 0"""
    status, out, _ = run("FILE")
    assert out == "3 the\n2 cat\n1 sat\n", f"ausgegeben: {out!r}"
    assert status == 0, f"geliefert: {status!r}; liefern Sie bei Erfolg 0"


def test_top_option():
    """-n 1 und --top 2 wählen, wie viele Wörter ausgegeben werden"""
    _, out, _ = run("-n", "1", "FILE")
    assert out == "3 the\n", f"mit -n 1 ausgegeben: {out!r}"
    _, out, _ = run("--top", "2", "FILE")
    assert out == "3 the\n2 cat\n", f"mit --top 2 ausgegeben: {out!r}"


def test_missing_file():
    """Eine fehlende Datei gibt eine Meldung auf stderr aus und liefert 1"""
    try:
        status, out, err = run("no-such-file.txt")
    except FileNotFoundError:
        raise AssertionError("FileNotFoundError ist aus main entkommen; fangen Sie ihn ab und liefern Sie 1") from None
    assert status == 1, f"geliefert: {status!r}; liefern Sie 1, wenn die Datei fehlt"
    assert "no-such-file.txt" in err, f"stderr war {err!r}; nennen Sie dort die fehlende Datei"
    assert out == "", f"stdout sollte leer bleiben, erhalten: {out!r}"

wordstat/__init__.py

"""Show the most common words in a text."""

__version__ = "0.1.0"

wordstat/core.py

from collections import Counter


def top_words(text: str, n: int = 3) -> list[tuple[str, int]]:
    """The n most common words, lowercased and without punctuation, most common first."""
    words = [word.strip(".,!?;:\"'()").lower() for word in text.split()]
    return Counter(word for word in words if word).most_common(n)

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Übung 3 von 3

Schritt 3: die Tests

Schritt 3 von 3. main.py ist tests/test_core.py. Es hat einen Test für top_words; ergänzen Sie mindestens drei fokussierte Tests: dass Groß- und Kleinschreibung ignoriert wird, dass Satzzeichen entfernt werden und dass n begrenzt, wie viele Wörter zurückkommen. Die Prüfungen lassen Ihre Tests gegen das echte top_words laufen und gegen drei fehlerhafte Versionen, eine pro Regel. Lokal starten Sie sie nach pip install -e . im Projektordner mit python -m unittest.

Diese Übung braucht Python auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.

Hinweise
  1. Hinweis 1

    Ein Verhalten pro Test: def test_ignores_case(self) -> None: mit einem einzigen assertEqual.

  2. Hinweis 2

    Wählen Sie Eingaben, bei denen die Regel zählt: "The the THE" für die Schreibung, "end. end!" für Satzzeichen, "a b c d" mit n=2 für die Grenze.

  3. Hinweis 3

    self.assertEqual(top_words("The the THE", 1), [("the", 3)])

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import unittest

from wordstat.core import top_words


class TestTopWords(unittest.TestCase):
    def test_most_common_first(self) -> None:
        self.assertEqual(top_words("b a b", 2), [("b", 2), ("a", 1)])

    def test_ignores_case(self) -> None:
        self.assertEqual(top_words("The the THE", 1), [("the", 3)])

    def test_strips_punctuation(self) -> None:
        self.assertEqual(top_words("end. end!", 1), [("end", 2)])

    def test_n_limits_the_result(self) -> None:
        self.assertEqual(len(top_words("a b c d", 2)), 2)

    def test_empty_text(self) -> None:
        self.assertEqual(top_words("", 3), [])


if __name__ == "__main__":
    suite = unittest.TestLoader().loadTestsFromTestCase(TestTopWords)
    unittest.TextTestRunner(verbosity=2).run(suite)
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

import unittest

from wordstat.core import top_words


class TestTopWords(unittest.TestCase):
    # Add one focused test per behaviour: case is ignored, punctuation is
    # stripped, and n limits how many words come back.
    def test_most_common_first(self) -> None:
        self.assertEqual(top_words("b a b", 2), [("b", 2), ("a", 1)])


if __name__ == "__main__":
    suite = unittest.TestLoader().loadTestsFromTestCase(TestTopWords)
    unittest.TextTestRunner(verbosity=2).run(suite)

test_main.py

import unittest
from collections import Counter

import main


def run_against(implementation):
    saved = main.top_words
    main.top_words = implementation
    try:
        result = unittest.TestResult()
        unittest.TestLoader().loadTestsFromTestCase(main.TestTopWords).run(result)
    finally:
        main.top_words = saved
    return result


def keeps_case(text, n=3):
    words = [word.strip(".,!?;:\"'()") for word in text.split()]
    return Counter(word for word in words if word).most_common(n)


def keeps_punctuation(text, n=3):
    return Counter(word.lower() for word in text.split()).most_common(n)


def ignores_n(text, n=3):
    words = [word.strip(".,!?;:\"'()").lower() for word in text.split()]
    return Counter(word for word in words if word).most_common()


BROKEN = {
    "Groß- und Kleinschreibung nicht ignoriert wird": keeps_case,
    "Satzzeichen erhalten bleiben": keeps_punctuation,
    "n ignoriert wird": ignores_n,
}


def test_correct_code_passes():
    """Mindestens 4 Tests, und alle bestehen mit dem echten top_words"""
    result = run_against(main.top_words)
    failing = [test.id() for test, _ in result.failures + result.errors]
    assert result.wasSuccessful(), f"diese Tests scheitern an korrektem Code: {failing}"
    assert result.testsRun >= 4, f"{result.testsRun} Tests liefen; schreiben Sie mindestens 4 fokussierte Tests"


def test_every_bug_is_caught():
    """Jede der drei fehlerhaften Versionen lässt einen Test scheitern"""
    missed = [bug for bug, broken in BROKEN.items() if run_against(broken).wasSuccessful()]
    if missed:
        assert False, f"kein Test scheitert, wenn {missed[0]}; ergänzen Sie einen Test dafür"

wordstat/__init__.py

"""Show the most common words in a text."""

__version__ = "0.1.0"

wordstat/core.py

from collections import Counter


def top_words(text: str, n: int = 3) -> list[tuple[str, int]]:
    """The n most common words, lowercased and without punctuation, most common first."""
    words = [word.strip(".,!?;:\"'()").lower() for word in text.split()]
    return Counter(word for word in words if word).most_common(n)

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Häufige Fehler

Eine Version ohne Anführungszeichen

import tomllib

PYPROJECT = """
[project]
name = "wordstat"
version = 0.1.0
"""

print(tomllib.loads(PYPROJECT)["project"]["version"])

Was Python ausgibt

tomllib.TOMLDecodeError: Expected newline or end of document after a statement (at line 4, column 14)

Warum, und die Lösung

In TOML ist 0.1.0 kein Wert: 0.1 wird als Zahl gelesen, und der zweite Punkt ergibt danach keinen Sinn. Versionen, Namen und Versionsangaben sind Strings, setzen Sie sie also in Anführungszeichen: version = "0.1.0", requires-python = ">=3.10". Zeile und Spalte in der Meldung zeigen auf die Stelle.

pyproject.toml im Textmodus öffnen

import tomllib
from pathlib import Path

Path("pyproject.toml").write_text('[project]\nname = "wordstat"\n', encoding="utf-8")
with open("pyproject.toml") as f:
    print(tomllib.load(f)["project"]["name"])

Was Python ausgibt

TypeError: File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')`

Warum, und die Lösung

tomllib.load braucht eine im Binärmodus geöffnete Datei, damit es die Kodierung selbst behandeln kann: open("pyproject.toml", "rb"). Für Text, den Sie schon als str haben, nehmen Sie tomllib.loads.

Ein nicht installiertes Paket im src-Layout importieren

# Run from the project root, before python -m pip install -e .
from pathlib import Path

Path("src/wordstat").mkdir(parents=True, exist_ok=True)
Path("src/wordstat/__init__.py").write_text('__version__ = "0.1.0"\n', encoding="utf-8")

import wordstat

print(wordstat.__version__)

Was Python ausgibt

ModuleNotFoundError: No module named 'wordstat'

Warum, und die Lösung

Im src-Layout liegt das Paket in src/wordstat, und src liegt nicht auf sys.path, also findet ein Start aus dem Projektordner es nicht. Das ist Absicht: So testen Sie keine Kopie, die nicht dem entspricht, was Nutzer installieren. Aktivieren Sie Ihre Umgebung und führen Sie einmal python -m pip install -e . aus; danach funktioniert import wordstat überall in dieser Umgebung.

Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

pyproject.toml beschreibt das Projekt

pyproject.toml ist eine TOML-Datei im Projektordner. [build-system] nennt das Werkzeug, das das Paket baut: mit setuptools requires = ["setuptools >= 77.0.3"] und build-backend = "setuptools.build_meta". [project] enthält die Metadaten: name ist Pflicht, version ebenfalls, sofern nicht als dynamisch markiert, und requires-python = ">=3.10" nennt das älteste unterstützte Python. Python liest TOML mit tomllib: loads für einen String, load für eine im Binärmodus geöffnete Datei.

Das src-Layout und editierbare Installationen

Legen Sie das Paket nach src/wordstat/, neben pyproject.toml und tests/. Weil src nicht auf sys.path liegt, scheitert import wordstat im Projektordner, bis das Paket installiert ist; Ihre Tests laufen also gegen das installierte Paket, nicht gegen eine verirrte Kopie. In einer aktivierten Umgebung installiert python -m pip install -e . es im editierbaren Modus: Das Paket gilt als installiert, aber Ihre Änderungen in src wirken ohne Neuinstallation.

Konsolenbefehle

Eine Tabelle [project.scripts] mit wordstat = "wordstat.cli:main" lässt pip einen Befehl wordstat anlegen. Er importiert wordstat.cli, ruft main() auf und übergibt das Ergebnis an sys.exit; main sollte also 0 für Erfolg und eine andere Zahl für einen Fehler liefern. Geben Sie ihr die Signatur main(argv: list[str] | None = None) -> int: Der Befehl übergibt nichts, und argparse liest sys.argv, während Tests main(["story.txt"]) direkt aufrufen.

Quellen

Zuletzt geprüft am 29. September 2026