Entry in Datei speichern & prüfen ob Entry in Datei steht

Fragen zu Tkinter.
Antworten
Shiny Emerald
User
Beiträge: 8
Registriert: Samstag 13. Juli 2019, 09:49

Hallo zusammen.

Ich habe bis jetzt mit tkinter, zwei Skripte programmiert.
Im ersten Skript muss man seinen Namen eintragen. Dieser wird dann in einer Datei namens newplayers.txt gespeichert.
Im zweiten Skript muss man auch seinen Namen eintragen, welcher dann in players.txt gespeichert wird.

Nun habe ich zwei Probleme:

1. Ich möchte, dass der Name im zweiten Skript nur in die Datei players.txt geschrieben wird, wenn der gleiche Name auch schon in newplayers.txt steht. Falls das nicht der Fall sein sollte, baue ich da einfach ein window.quit() ein.

2. Außerdem habe ich bemerkt, dass wenn man in der ersten Datei zwei Namen hintereinander einträgt und dazwischen jeweils auf den Knopf drückt, der die Eingabe speichert, wird beim zweiten Mal der erste Name gelöscht und dann der zweite Name in der ersten Zeile eingetragen. Wie kriege ich das hin, dass der zweite Name dann in die nächste Zeile geschrieben wird, sodass der erste nicht gelöscht wird?

Hier ein Teil meines Codes: (1. Skript)

Code: Alles auswählen

Namecheck = StringVar()

tk.Entry(window, textvariable=Namecheck).grid(row=1, column=2)
ConfirmButton = tk.Button(window, text="Absenden", command=namecheck).grid(row=2, column=2)

def namecheck():
        namecheck = Namecheck.get()
        savename = open("newplayer.txt", "w")
        savename.write(namecheck)
        savename.close()

Das 2. Skript funktioniert nach demselben Prinzip.
Danke für eure Hilfe. Die zwei Skripte und Textdokumente sind auf dem selben Path.

Shiny_Emerald
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Shiny Emerald: Der Dateimodus "w" öffnet eine Datei zum Schreiben *und* löscht als erstes deren kompletten Inhalt. Schau Dir mal die anderen Modi an, die man da so angeben kann.

Wenn Du nur den Inhalt von `Namecheck` schreibst, dann hast Du zusätzlich noch das Problem, dass das nicht wirklich eine Zeile schreibt, sondern nur den Namen. Um daraus eine Zeile zu machen, braucht es noch das Zeilenendezeichen "\n" danach. Sonst würde der nächste Name den Du in die Datei schreibst, einfach ans Ende des ersten angehängt.

Eingerückt wird mit vier Leerzeichen pro Ebene. Nicht mit Tabs.

Namen schreibt man in Python klein_mit_unterstrichen. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase). `Namecheck` sollte also `namecheck` heissen, `ConfirmButton` sollte `confirm_button` heissen, und so weiter.

Wobei `namecheck` für ein `StringVar`-Objekt, eine Zeichenkette, und eine Funktion zu verwenden, die auch alle drei zusammen verwendet werden ist verwirrend, beziehungsweise geht ja auch gar nicht, denn die Zeichenkette und das `StringVar`-Objekt brauchst Du ja im gleichen Namensraum in der Funktion `namecheck()`.

Funktionen und Methoden werden üblicherweise nach der Tätigkeit benannt die sie durchführen. Also hier eher `check_name()`, weil das die Tätigkeit ist, aber auch das wäre falsch, denn wo wird denn da ein Name geprüft? Also wohl eher `save_name()`.

Ausserdem sollten Funktionen und Methoden alles was sie ausser Konstanten benötigen als Argument(e) übergeben bekommen. Die Funktion darf also nicht einfach so auf `Namecheck` zugreifen können, sondern das `StringVar`-Objekt als Argument übergeben bekommen. Wenn das so funktioniert wie es da steht, bedeutet es ja das `Namecheck` eine globale Variable ist. Die verwendet man aber nicht. Woraus folgt, das man für GUI-Programmierung mindestens `functools.partial()` benötigt, für jede nicht-triviale GUI aber auch sehr schnell nicht um objektorientierte Programmierung herum kommt.

Dateien öffnet/schliesst man am besten zusammen mit der ``with``-Anweisung.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Shiny Emerald: Wieso kannst Du einfach so `StringVar` schreiben während andere Werte aus dem `tkinter`-Modul per `tk` angesprochen werden?

`savename` ist kein guter Name für eine *Datei*. Einfach `file` würde reichen.

Bei Textdateien solle man immer explizit eine Kodierung angeben.
Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
from functools import partial
import tkinter as tk


def save_name(name_var):
    #
    # TODO Remove whitespace from entered name and check for empty name.
    # TODO Is it allowed to enter names already existent in file?
    #
    with open("newplayer.txt", "a", encoding="utf-8") as file:
        file.write(name_var.get() + "\n")


def main():
    window = tk.Tk()
    # ...
    name_var = tk.StringVar()

    tk.Entry(window, textvariable=name_var).grid(row=1, column=2)
    confirm_button = tk.Button(
        window, text="Absenden", command=partial(save_name, name_var)
    ).grid(row=2, column=2)
    # ...


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Shiny Emerald
User
Beiträge: 8
Registriert: Samstag 13. Juli 2019, 09:49

@_blackjack_: Wenn ich einen Namen eingebe, wird er zwar in das Dokument geschrieben. Wenn ich jedoch einen 2. Name eingebe, wird dieser nicht hineingeschrieben.

Warum hast du hier ein TODO?:

# TODO Remove whitespace from entered name and check for empty name.
# TODO Is it allowed to enter names already existent in file?

? Da weiß ich ja nicht, wie ich das machen soll. Warum gibst du mir dann meinen Code zwar verbessert (danke :-) ), aber das was ich eigentlich wollte, lässt du einfach weg?

Übrigens: Das mit dem tk.Label(window..., war mein Fehler, das tk, braucht man da ja nicht, wie du schon gesagt hast. Warum ich das da hingeschrieben habe? Anscheinend hab ich vergessen, dass es da gar nicht benötigt wird.

Anstelle von "w", kann man doch auch "r+" benutzen, oder?

"Eingerückt wird mit vier Leerzeichen pro Ebene. Nicht mit Tabs." Warum? Ein Tab entspricht doch 4 Leerzeichen. Warum sollte ich da dann 4 Leerzeichen verwenden, wenn es mit Tab viel schneller geht? Hä?

Was spielt es für eine Rolle, ob man Namen klein_mit_unterstrichen oder GROSS oder GeMixt schreibt? Das Programm funktioniert ja trotzdem. Meinst du, es ist sonst etwas unübersichtlich? Ok. Kann ich verstehen

"Wobei `namecheck` für ein `StringVar`-Objekt, eine Zeichenkette, und eine Funktion zu verwenden, die auch alle drei zusammen verwendet werden ist verwirrend, beziehungsweise geht ja auch gar nicht, denn die Zeichenkette und das `StringVar`-Objekt brauchst Du ja im gleichen Namensraum in der Funktion `namecheck()`."
Das bedeutet, dass ich das StringVar() in

def namecheck():

brauche?

"Also hier eher `check_name()`, weil das die Tätigkeit ist, aber auch das wäre falsch, denn wo wird denn da ein Name geprüft? Also wohl eher `save_name()`."
Ja, weil ich das ja noch machen wollte, ob der Name schon in der anderen Datei steht. Allerdings wusste ich nicht, wie ich das mache, deshalb hab ich ja hier gefragt. Dass ich später auch noch weiss, dass ich das da machen wollte, habe ich sie so benannt, sodass ich da dann später den nötigen Code einbauen kann und save_name davon trenne.

(übrigens sorry, dass ich deine Zitate nicht mit der Zitatfunktion zitiere...)

Danke
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Shiny Emerald: Der zweite (und jeder weitere) Name sollte aber in die Datei geschrieben werden bei meinem Code. Wenn das nicht der Fall ist, dann hast Du da immer noch den falschen Dateimodus.

Ich habe da TODOs als Anregungen was da noch passieren könnte oder müsste. Je nach dem ob das dem entspricht was Du da am Ende haben möchtest.

Für das zweite TODO, was Teil Deiner Frage für's zweite Programm ist, hast Du noch nicht verraten was da nicht funktioniert, also was Du versucht hast, und woran es scheitert. Wir bewegen uns hier auf ziemlich grundlegenden Niveau was Zeichenkettenoperationen und Umgang mit Dateien angeht.

Doch das `tk` braucht man. Wenn man das nicht braucht, hat man entweder alles explizit importiert, oder einen *-Import. *-Import macht man nicht, der ist Böse™. Da holt man sich gerade bei `tkinter` über 150 Namen ins Modul von denen man nur einen kleinen Bruchteil verwendet. Und da sind auch nicht nur Namen dabei die im `tkinter`-Modul definiert werden, sondern auch welche die im `tkinter`-Modul aus anderen Modulen importiert werden, und die man über diesen Weg erst recht nicht in das importierende Modul holen sollte.

Der Dateimodus "r+" macht eigentlich nie Sinn. "rb+" macht in seltenen Fällen Sinn.

Ein Tab entspricht gar keinem Leerzeichen sondern einem Tab. Das sagt, rücke bis zur nächsten Tabulatorposition vor. Wo die liegt? Keine Ahnung. Das hängt ganz und gar von dem ab der das interpetiert (Editor, Webbrowser, Terminal, Textverarbeitung). Die Tabulatorpositionen müssen dabei nicht einmal regelmässig sein. Das ein Tab vier Leerzeichen entspricht sieht mein Webbrowser beispielsweise nicht so. Das Terminal in der Regel auch nicht.

Tab-Zeichen sind auch nicht ”schneller” – man tippt ja nicht vier Leerzeichen (oder viermal Backspace) sondern einmal Tab (und einmal Backspace). Den Rest erledigt der entsprechend konfigurierte Texteditor. Sollte der das nicht können, ist er nicht zum Programmieren geeignet.

Die Namenschreibweise transportiert Informationen. Wenn ein Python-Programmierer den Namen `ConfirmButton` liest, dann weiss er, dass es sich um eine Klasse handelt, weil man nur Klassen in MixedCase schreibt. Und bei `ANSWER` weiss der Leser, dass es sich um einen Wert handelt der sich nie ändert, denn nur Konstanten werden KOMPLETT_GROSS geschrieben. Das passende Dokument dazu: Style Guide for Python Code.

Den `StringVar()`-Aufruf brauchst Du nicht in der Funktion, sondern den Wert den das zurück gibt, also das `StringVar`-Objekt was damit erstellt wird. In der Funktion musst Du ja den Wert abfragen den der Benutzer in das Eingabefeld geschrieben hat. Sieht man doch an meinem Code was da passieren muss.

Um zu prüfen ob ein Namen schon in der Datei ist, musst Du sie zum lesen öffnen und Zeile für Zeile prüfen ob die aktuelle Zeile der neu eingegebene Name ist. Dabei darauf achten, dass die Zeilen in der Datei nicht nur den Namen, sondern auch noch ein Zeilenende-Zeichen enthalten. Also muss man das für den Vergleich entweder entfernen, oder vorher eines an die Eingabe anhängen.

Die Programmlogik sollte man auch von der GUI trennen. Also das Testen ob ein Name bereits in der Datei ist und/oder das speichern eines zusätzlichen Namens in die Datei, sollte nichts von der GUI wissen müssen. Man braucht da also zwei Funktionen zum Speichern – eine auf GUI-Ebene, die den Wert aus der GUI holt und zum speichern aufbereitet und ggf. Prüfungen durchführt und den Benutzer über Probleme informiert. Und eine weitere auf Programmlogikebene die mit der GUI und generell Benutzerinteraktion nichts zu tun hat.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Shiny Emerald: Die Funktion zum Speichern von einem Namen in der Programmlogik kann man von der in der GUI durch ein `on_*()` im Namen unterscheiden, was ein gängiger Präfix für Rückruffunktionen ist, die auf Ergeignisse reagieren.

Code: Alles auswählen

NEW_PLAYER_FILENAME = "newplayer.txt"


def validate_name(name):
    if not name:
        raise ValueError("Name must not be empty")
    if name != name.strip() or not name:
        raise ValueError(f"Name {name!r} contains leading/trailing whitespace")
    return name


def contains_name(filename, name):
    validate_name(name)
    name += "\n"
    with open(filename, "r", encoding="urf-8") as lines:
        return any(name == line for line in lines)


def save_name(filename, name):
    validate_name(name)
    if contains_name(filename, name):
        raise ValueError(f"Name {name!r} already in file")
    with open(filename, "a", encoding="utf-8") as file:
        file.write(name + "\n")


def on_save_name(name_var):
    name = name_var.get().strip()
    if not name:
        # TODO Inform user that the name cannot be empty.
        return
    if contains_name(NEW_PLAYER_FILENAME, name):
        # TODO Inform user that this name already exists.
        return
    save_name(NEW_PLAYER_FILENAME, name)
Edit: `validate_name()` erweitert.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Shiny Emerald
User
Beiträge: 8
Registriert: Samstag 13. Juli 2019, 09:49

@_blackjack_ Danke.
aber warum bekomme ich da einen Error?:

Exception in Tkinter callback
Traceback (most recent call last):
File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/tkinter/__init__.py", line 1705, in __call__
return self.func(*args)
TypeError: save_name() missing 2 required positional arguments: 'filename' and 'name'
Benutzeravatar
sparrow
User
Beiträge: 4193
Registriert: Freitag 17. April 2009, 10:28

Code zu einer Fehlermeldung wäre schon hilfreich.
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Shiny Emerald: Du übergibst offenbar keine Argumente, also hast `save_name` direkt als `command`-Argument angegeben. Das ist a) die falsche Funktion, denn `save_name()` ist Programmlogik, nicht GUI, und b): Wo sollten denn die Werte für `filename` und `name` herkommen?

Hier mal der Testcode der mich zum Edit im letzten Beitrag bewogen hat:

Code: Alles auswählen

import pytest
from contextlib import ExitStack as does_not_raise


@pytest.mark.parametrize(
    "name,exception",
    [
        ("paul", does_not_raise()),
        ("", pytest.raises(ValueError, match="name must not be empty")),
        (" peter", pytest.raises(ValueError, match="leading/trailing")),
        ("mary\n", pytest.raises(ValueError, match="leading/trailing")),
        ("\talice\t", pytest.raises(ValueError, match="leading/trailing")),
    ],
)
def test_validate_name(name, exception):
    with exception:
        assert validate_name(name) == name
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Und die Validierungsfunktion um einen weiteren Test erweitert: Wenn Zeilenenden die Namen trennen, dann darf *im* Namen natürlich kein Zeilenende vorkommen:

Code: Alles auswählen

def validate_name(name):
    if not name:
        raise ValueError("name must not be empty")
    if name != name.strip():
        raise ValueError(f"name {name!r} contains leading/trailing whitespace")
    if "\n" in name:
        raise ValueError(f"name {name!r} contains '\\n' character")
    return name
Und Testcode für die gesamte Programmlogik:

Code: Alles auswählen

import pytest
from contextlib import ExitStack as does_not_raise

TEST_NAMEFILE_CONTENT = "peter\npaul\nmary\n"


@pytest.mark.parametrize(
    "name,exception",
    [
        ("paul", does_not_raise()),
        ("", pytest.raises(ValueError, match="name must not be empty")),
        (" peter", pytest.raises(ValueError, match="leading/trailing")),
        ("mary\n", pytest.raises(ValueError, match="leading/trailing")),
        ("\talice\t", pytest.raises(ValueError, match="leading/trailing")),
        ("bond\njames", pytest.raises(ValueError, match=r"contains '\\n'")),
    ],
)
def test_validate_name(name, exception):
    with exception:
        assert validate_name(name) == name


@pytest.fixture
def names_file_path(tmpdir):
    filename = tmpdir / NEW_PLAYER_FILENAME
    filename.write_text(TEST_NAMEFILE_CONTENT, "utf-8")
    return filename


@pytest.mark.parametrize(
    "name,expected", [("peter", True), ("mary", True), ("gandalf", False)]
)
def test_contains_name(name, expected, names_file_path):
    assert contains_name(names_file_path, name) == expected


@pytest.mark.parametrize(
    "name,exception",
    [
        ("gandalf", does_not_raise()),
        ("paul", pytest.raises(ValueError, match="already in file")),
    ],
)
def test_save_name(name, exception, names_file_path):
    with exception:
        save_name(names_file_path, name)
        assert (
            names_file_path.read_text("utf-8")
            == f"{TEST_NAMEFILE_CONTENT}{name}\n"
        )
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Shiny Emerald
User
Beiträge: 8
Registriert: Samstag 13. Juli 2019, 09:49

import pytest

@_blackjack_ :
Hast du den Code, den du mir davor gegeben hast so genannt, oder ist das ein Modul, welches bei mir nicht installiert ist?
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Das ist dann wohl bei Dir nicht installiert. Ich glaube Nose ist sanft entschlafen, womit pytest *das* „pythonische“ Testrahmenwerk ist.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten