Keine sqlite-Abfrage möglich bei Autostart des Scripts

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Onomatopoesie
User
Beiträge: 41
Registriert: Montag 12. August 2019, 07:52

Liebe Pythoner,
ich habe ein Script, dass eine Datenbankabfrage tätigt. Das funktioniert wunderbar. Starte ich das Script aber per Autostart, dann scheitert die Datenbankabfrage. Ein time.sleep(10) am Anfang des Scripts hat nichts genützt - ich dachte, dass vielleicht Rasbian nicht vollständig hochgefahren wäre, aber das scheint es nicht zu sein.
Betriebssystem: Rasbian Buster auf dem Pi 3 B+
Mein Problem dabei ist auch, dass ich gar keine Meldung über die Datenbank erhalten, weil das Terminalfenster nicht erscheint, dass ich von Python gewohnt bin, wenn das Script per Autostart ausgeführt wird. Es wird nur das von tkinter erzeugte Fenster gezeigt.

Gekürztest Script (auf das Kernproblem reduziert):

Code: Alles auswählen

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *
from tkinter import messagebox
import sqlite3

root = Tk()
root.geometry("480x320")
#root.attributes("-fullscreen", True)

connection = sqlite3.connect("artikel.db")
cursor=connection.cursor()

# Funktionen
def onReturn(*event):
    str_artikelnr=textinput.get()
    textinput.delete(0,"end")
    cursor.execute("SELECT * FROM artikelliste WHERE artikelnr="
    + str_artikelnr)
    result=cursor.fetchone()
    messagebox.showinfo(title="DB Abfrage",
        message=str(result))
    # Messagebox wird gar nicht ausgeführt!

# Input
textinput=Entry(root, width=12)
textinput.focus_set()
root.bind("<Return>", onReturn)
textinput.pack()

root.mainloop()
connection.close()
Autostart-Datei im Verzeichnis "/home/pi/.config/autostart":

Code: Alles auswählen

[Desktop Entry]
Encoding=UTF-8
Type=Application
Name=Kasse
Exec=sudo python3 /home/pi/python/kasse.py
StartupNotify=false
Terminal=true
Hidden=false
Für hilfreiche Tipps wäre ich dankbar, weil ich ratlos bin, was ich unternehmen könnte ...
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Wie in nahezu 100% aller solchen Fälle benutzt du relative Pfade. Womit die Datenbank irgendwo versucht wird anzulegen, wo sie nicht gefunden wird. Du musst dafür also einen absoluten Pfad angeben.

Und so wie dein Programm aussieht, braucht es keine root Rechte. Das sudo sieht eher nach “wenn’s nicht geht, schreib sudo davor, vielleicht geht’s dann” aus. Wenn ja: werd das los. Denn beim nächsten Fehler dieser Art überschreibst oder löschst du vielleicht aus versehen system-relevante Dateien. Und dann ist Essig.
Onomatopoesie
User
Beiträge: 41
Registriert: Montag 12. August 2019, 07:52

@_deets_: Die Datenbank liegt doch im selben Verzeichnis wie die Python-Datei. Warum muss ich dort absolute Pfade verwenden? Das Script funktioniert ja, wenn ich es manuell starte ...
Bitte verstehe mich nicht falsch: Ich werde es natürlich jetzt gleich einmal mit absoluten Pfaden versuchen, aber ich würde es eben gerne verstehen, warum es einen Unterschied macht, ob ich es manuell oder per Script anstupse.
Das Script für die Autostart-Datei habe ich direkt von einer Internetseite übernommen und dachte, dass immer sudo davor gehört, weil es sonst keine Lese-/Schreibrechte gibt usw. Ich habe meinen Pi ja erst seit ein paar Tagen und keine Linux-Erfahrung, daher weiß ich noch nicht, was sinnvoll ist und was nicht ... :-(
Onomatopoesie
User
Beiträge: 41
Registriert: Montag 12. August 2019, 07:52

@_deets_: Ja - du hast vollkommen recht. Das hat das Problem gelöst. Ich danke dir, deets. Magst du mir nur grob erklären, warum es per Autostart mit den relativen Pfaden nicht klappt?
Benutzeravatar
sparrow
User
Beiträge: 4201
Registriert: Freitag 17. April 2009, 10:28

Weil der Pfad nicht relativ zum Verzeichnis ist, in dem fas Script liegt, sondern relativ zum workingdir.
Ruf doch in der Konsole mal das Script auf, wenn du nicht vorher mit cd in das Veezeichnis gewechselt bist.
Und woher weißt du bei dem Autostart, was das workingdir ist?
Benutzeravatar
__blackjack__
User
Beiträge: 13131
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Onomatopoesie: Wenn die Datenbankdatei immer im gleichen Verzeichnis liegen soll wie die *.py-Datei, dann kannst Du über `__file__` und `pathlib` auch den Pfad zur Laufzeit ermitteln.

Neben dem ``sudo`` würde ich ja auch noch das ``python3`` aus der Desktop-Datei werfen. Nicht vergessen das Skript ausführbar zu machen.
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
Onomatopoesie
User
Beiträge: 41
Registriert: Montag 12. August 2019, 07:52

Alles klar. Das verstehe ich. Ich dachte immer, dass die relativen Pfade relativ zum Script sind. Das erschien mir naheliegend. Nun weiß ich, dass das nicht so ist und werde zukünftig immer absolute Pfade verwenden bzw. den Pfad vorgeben und dann relativ arbeiten.
Vielen Dank für die Hilfe und die Hinweise.
Onomatopoesie
User
Beiträge: 41
Registriert: Montag 12. August 2019, 07:52

@_blackjack_: Warum soll ich das "python3" entfernen? Soweit ich weiß, ist auf dem Pi auch python2 und ich befürchte, dass dann python2 zum Ausführen verwendet wird, oder nicht? Wie kann ich denn ein Script ausführbar machen? Durch den python3-Interpreter wird das Script doch ausgeführt? Ich dachte, dass Python nichts kompiliert, sondern immer das Script verwendet (bzw. zum schnelleren Ausführen den Bytecode)?
Benutzeravatar
sparrow
User
Beiträge: 4201
Registriert: Freitag 17. April 2009, 10:28

Weil die Shebang-Zeile im Script sagt, welcher Interpreter genommen werden soll.
Benutzeravatar
__blackjack__
User
Beiträge: 13131
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Onomatopoesie: Noch ein paar Anmerkungen zum Quelltext:

Der Coding-Kommentar ist bei Python 3 überflüssig, weil UTF-8 dort die Vorannahme von Python ist.

Sternchen-Importe sind Böse™. Damit holt man sich bei `tkinter` deutlich mehr als 100 Namen ins Modul von denen nur ein Bruchteil tatsächlich benötigt wird. Und auch Namen die gar nicht in `tkinter` definiert sind, sondern von `tkinter` selbst aus anderen Modulen importiert werden. Üblich ist es das Modul unter dem Namen `tk` zu importieren und dann darüber auf die Werte darin zuzugreifen.

Auf Modulebene gehört nur Code der Konstanten, Funktionen, und Klassen definiert. Das unübersichtlichste was man machen kann ist Hauptprogramm und Funktionsdefinitionen auf Modulebene mischen. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Funktionen und Methoden verwenden ausser Konstanten nichts was sie nicht als Argument(e) übergeben bekommen. Das bedeutet das man bei fast jeder nicht-trivialen GUI objektproentierte Programmierung (OOP) braucht!

Um binäre Operatoren, Zuweisungszeichen ausserhalb von Argumentlisten und nach Kommas erhöhen Leerzeichen die Lesbarkeit.

Cursor-Objekte sind etwas kurzlebiges, die hebt man nicht für die gesamte Laufzeit eines Programms auf, sondern erzeugt sie dann wenn man sie braucht und räumt sie danach auch sauber wieder ab (`close()`).

Das saubere Abräumen von Objekten die selbst keine Kontextmanager sind, aber eine `close()`-Methode haben, kann man mit ``with`` und `contextlib.closing()` sicherstellen.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase). Also `on_return()` statt `onReturn()`.

Für Zeichenketten mit einer festen Bedeutung für Tk hat `tkinter` Konstanten definiert.

Ganz wichtig: Keine Werte in SQL-Anfragen mit Zeichenkettenoperationen formatieren. Schon überhaupt gar nicht wenn diese Werte vom Benutzer kommen! Das ist gefährlich! In die SQL-Anfrage gehören für variable Werte Platzhalter und die Werte selbst werden als zweites Argument an `execute()` übergeben.

Bei SELECT-Anfragen sollte man die Ergebnisspalten explizit angeben um unabhängig von Anzahl und Reihenfolge der Spalten in der Datenbank zu bleiben. Zudem weiss der Leser des Quelltextes dann auch was da abgefragt wird.

Tabellennamen vergebe ich immer in der Einzahl, weil eine Tabellendefinition beschreibt wie *ein* Datensatz aussieht. Das wird noch deutlicher wenn man mal ein ORM verwendet, denn dann wird in der Regel ja eine Tabelle auf eine Klasse abgebildet.

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
from contextlib import closing
from functools import partial
from pathlib import Path
import sqlite3
import tkinter as tk
from tkinter import messagebox

MODULE_PATH = Path(__file__).parent
ARTICLE_DB_FILENAME = "artikel.db"


def on_return(connection, text_entry, _event):
    article_number = text_entry.get()
    text_entry.delete(0, tk.END)
    with closing(connection.cursor()) as cursor:
        #
        # TODO Change * to explicit column names.
        #
        cursor.execute(
            "SELECT * FROM artikelliste WHERE artikelnr=?", [article_number]
        )
        result = cursor.fetchone()
    messagebox.showinfo("DB Abfrage", str(result))


def main():
    root = tk.Tk()
    root.geometry("480x320")
    # root.attributes("-fullscreen", True)

    with sqlite3.connect(MODULE_PATH / ARTICLE_DB_FILENAME) as connection:
        text_entry = tk.Entry(root, width=12)
        text_entry.focus_set()
        root.bind("<Return>", partial(on_return, connection, text_entry))
        text_entry.pack()

        root.mainloop()


if __name__ == "__main__":
    main()
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
Onomatopoesie
User
Beiträge: 41
Registriert: Montag 12. August 2019, 07:52

Vielen Dank für deine weiteren Hinweise, die ich erst einmal verdauen musste (und leider noch nicht alle ganz durchdrungen habe).

Ich wusste nicht, dass es blödsinnig ist, mit Stern zu importieren. Ich habe das aus Tutorials entnommen (und nicht nur in einem gesehen!). Dort war es sogar eine Empfehlung, um nicht immer tk.funktionsname schreiben zu müssen, sondern einfach nur funktionsname. Ich bin bisher davon ausgegangen, dass tkinter sozusagen referenziert wird durch Import *, nicht aber wirklich in den Speicher gezogen wird. Ich dachte also, man kann auf alles zugreifen, es würde aber auch nichts schaden, wenn man es nicht tut.

Für mich ist es neu, dass das eigentliche Hauptprogramm in eine Funktion gepackt werden sollte. Ich sehe in diesem Moment auch nicht den Vorteil davon. Warum ist es übersichtlicher, ein Programm in eine Funktion zu packen? Ich nahm an, dass Funktionen für wiederkehrende Aufgaben gedacht sind. Ein Vorgang, den man mit (anderem Input z.B.) wiederholt ausführen möchte. Aber das Hauptprogramm hat ja die Aufgabe, bestimmte Parameter vorzugeben, Dinge wie die GUI zu initialisieren usw. Warum das also in eine Funktion packen, wenn diese doch nur ein einziges Mal ausgeführt werden soll?

Ich habe leider auch nicht nachvollziehen können, warum ich das Cursorobjekt schließen sollte. Verstehe mich nicht falsch - ich zweifle deine Hinweise gar nicht an, sondern ich möchte verstehen und es nicht einfach nur stur befolgen. In dem Programm (ein kleines Kassenprogramm für meinen Sohn, wo er mit einem Handscanner seine Artikel einlesen können soll) werden doch immer wieder Artikelnummern mit der Datenbank abgeglichen. Der Datenbankabruf erfolgt also nicht ein einziges Mal, sondern fortlaufend. Nun habe ich es so verstanden, dass das Objekt "cursor" ja stets durch ein neues Objekt "cursor" überschrieben wird - so also, als wenn ich die Variable var1 == 5 mit var1 = "hallo" überschreibe (also nicht nur mit einem neuen Wert, sondern sogar einem anderen Typ, nämlich string statt integer). Ich habe "gelernt", dass das bei Python überhaupt kein Problem darstellt. Warum dann also ein close()? Und der letzte Cursor würde doch dann ohnehin durch das connection.close() geschlossen werden, sodass am Ende alles aufgeräumt wäre?
Benutzeravatar
snafu
User
Beiträge: 6744
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Ich möchte nicht auf alles eingehen, aber kurz zum Thema Funktionen: Die dienen nicht nur zum Wiederholen von Arbeitsschritten (ggf in leichter Variation mittels entsprechender Parameter), sondern auch generell zur Strukturierung. So wie man im echten Leben auch größere Abläufe in kleine Schritte unterteilen kann. Das Programm wird dann lesbarer und die Einzelschritte lassen sich leichter testen, sodass man einem möglichen Problem besser auf den Grund gehen kann. Du wirst das bei komplexeren Programmen sicherlich noch selbst bemerken...
Benutzeravatar
__blackjack__
User
Beiträge: 13131
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Onomatopoesie: Wenn man ein Modul importiert, egal wie, dann wird das in den Speicher geladen und der Code auf Modulebene wird ausgeführt. Die Frage ist welche Namen man sich in den Namensraum des importierenden Moduls holt. Mit einem * alle (wenn das importierte Modul keine speziellen Vorkehrungen trifft) und dadurch kann man schnell den Überblick verlieren oder sich auch lokale Namen überschreiben, die man dann nicht mehr verwenden kann. Ein ``from os import *`` würde beispielsweise die `open()`-Funktion durch eine andere `open()`-Funktion ersetzen die andere Argumente erwartet und auf viel niedrigerer Ebene ansetzt. Und bei einem *-Import bekommt man wie gesagt auch nicht nur die Namen die im importierten Modul direkt definiert werden, sondern auch alle Namen die das Modul seinerseits von woanders importiert, was die Grenzen noch mehr verwischt. Man macht damit den Sinn von Modulen als Namensräume zunichte.

Wenn man das Hauptprogramm nicht in eine Funktion steckt, dann kann man das Modul nicht importieren ohne dass das Hauptprogramm auch tatsächlich abläuft. Man kann es also auch nicht importieren um Fehlern in einzelnen Funktionen auf den Grund zu gehen in dem man die mal in einer interaktiven Python-Shell mit verschiedenen Argumenten aufruft. Man kann keine Dokumentation aus dem Modul ziehen, weil die üblichen Werkzeuge dafür (`pydoc` aus der Standardbibliothek und Sphinx) dazu das Modul importieren – und dann darf da nix loslaufen was nicht nur den Inhalt des Moduls definiert. Unittests kann man auf diese Weise nicht ausführen. Verschiedene Module funktionieren nicht oder nicht immer wenn man das nicht so macht. `multiprocessing` unter Windows beispielsweise.

Und über globale Variablen haben wir dabei jetzt noch gar nicht gesprochen.

Es geht nicht wirklich um Vorteile, sondern diese ganzen Nachteile zu vermeiden.

Cursor sollte man schliessen damit die Ressourcen auf *Seiten der Datenbank* aufgeräumt werden können. Das hat nichts mit Python zu tun, denn Python weiss ja gar nicht was die Datenbanksoftware da tut. Die Leute die die Datenbank programmieren gehen ebenfalls davon aus das der Benutzer Cursor-Objekte nicht längerfristig verwendet und treffen auf Grund dieser Annahmen Entscheidungen. Es kann sein, dass das bei einer bestimmten Datenbank/Python-Anbindungs-Kombination keine Rolle spielt, es kann aber auch sein das Du auf diese Weise zum Beispiel den Speicher vollmüllst, weil die Entwickler der DB in einem Cursor Speicher den sie mal angefordert haben erst vollständig freigeben bis der explizit geschlossen wird.

Man sollte sich nicht auf implizites Aufräumen von externen Ressourcen verlassen das irgendwann, vielleicht passiert.
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
Antworten