Aufwärmen · Aufgabe 1 von 7
// 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
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
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"])Ü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.]Üben · Aufgabe 4 von 7
Ordnen Sie jeder Datei des Projekts wordstat ihre Aufgabe zu.
Ü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)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"]))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.pyAusgabe
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
Hinweis 1
Tabellen beginnen mit einer Kopfzeile in eigener Zeile: [build-system], [project], [project.scripts].
Hinweis 2
Der Packaging User Guide nennt die Werte für setuptools: requires = ["setuptools >= 77.0.3"] und build-backend = "setuptools.build_meta".
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.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.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
Hinweis 1
parser.add_argument("-n", "--top", type=int, default=3) macht args.top zu einem int.
Hinweis 2
Legen Sie um den Aufruf von read_text ein try/except FileNotFoundError.
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.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.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
Hinweis 1
Ein Verhalten pro Test: def test_ignores_case(self) -> None: mit einem einzigen assertEqual.
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.
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.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenHä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.