Programm überprüfen – Hab ich schlimme Fehler gemacht?

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
Hellstorm
User
Beiträge: 231
Registriert: Samstag 22. Juni 2013, 15:01

Hallo,

ich bin gerade an einem kleinen Hochlade-Programm dran. Obwohl es noch relativ im Anfangsstadium ist, wollte ich fragen, ob jemand kurz darüber schauen und sagen könnte, ob ich irgendwo eklatante Fehler gemacht habe (Jetzt ist der Code ja noch halbwegs übersichtlich, so dass das noch möglich ist)?

Im Moment sieht es so aus:

Bild

Das Programm soll Dateien per Kommandozeilenparameter übergeben bekommen, diese dann auf einen FTP-Server hochladen und in eine Datenbank mit „Ablaufdatum“ eintragen. Beim nächsten Start soll dann geschaut werden, welche Dateien abgelaufen sind (im Bild rot markiert) und diese dann gelöscht werden. So soll verhindert werden, dass der FTP-Server eine vermüllte Datenhalde wird.

Also ich habe das Programm in mehrere Dateien aufgeteilt:

uploader.py (Das Hauptprogramm):

Code: Alles auswählen

import sys
import gui
from PyQt4 import QtGui, QtCore

def start(argv):
    app = QtGui.QApplication(argv)
    QtGui.QApplication.setApplicationName("Uploader")
    
    #Translation
    myappTranslator = QtCore.QTranslator()
    language = QtCore.QLocale.system().name()
    myappTranslator.load("uploader_{0}".format(language), "i18n")
    app.installTranslator(myappTranslator)
    
    #Initialise window
    dialog = gui.MainWindow()
    dialog.show()
    sys.exit(app.exec_())
    
if __name__ == "__main__":
    start(sys.argv)
Macht eigentlich nichts groß außer das Fenster anzuzeigen und die Übersetzung zu laden.


dann gui.py:

Code: Alles auswählen

import sys
import time, datetime
import os

from background import *

from PyQt4 import QtCore, QtGui
from src.Ui_mainwindow import Ui_mainwindow

def extractfilename(file):
    """Extrahiert den Dateinamen aus einem kompletten Pfad"""
    os.path.normpath(file)
    return(os.path.split(file)[1])


class MainWindow(QtGui.QDialog, Ui_mainwindow):
    def __init__(self):
        """Creates the main window and connects to the database and calls
        functions to populate the servers and files list"""
        
        QtGui.QDialog.__init__(self)
        self.setupUi(self)
        
        #Connect to database
        databasepath = os.path.split(sys.argv[0])[0]
        databasefile = os.path.join(databasepath,  "files.sqlite3")
        self.databaseconnection = Database(databasefile)

        #Load servers and files from database
        self.listservers()
        self.readarguments()
        self.listfiles()
        
        #Slots
        self.connect(self.buttonClose, QtCore.SIGNAL("clicked()"), self.onClose)
        self.connect
        
    def listservers(self):
        """Create server objects from every entry in the database"""
        
        self.servers=[]
        for server in self.databaseconnection.readserver():
            #Appends a new server object with the settings from the DB 
            self.servers.append(Server(server))
            self.cmbServer.insertItem(self.servers[-1].serverid, self.servers[-1].name)
            
        self.cmbServer.insertItem(50, self.tr("Create a new server..."))
        #TODO: Hier eine bessere Lösung anstatt der 50 finden.
    
    def listfiles(self):
        """Inserts the current files listed in the database"""
        
        red = QtGui.QColor("red")
        
        self.files = []
        for file in self.databaseconnection.readfiles():
            self.files.append(DatabaseFile(file))
        
        #Inserts files into QTableWidget
        self.tblFiles.setRowCount(len(self.files))

        row = 0
        for file in self.files:            
            column = 0
            
            #Converts expiry date from timestamp to readable
            expiry_readable = datetime.datetime.fromtimestamp(file.expirydate).strftime(self.tr("%Y-%m-%d %H:%M:%S"))
            
            #Converts server number to server name
            for server in self.servers:
                if server.serverid == file.serverid:
                    server_readable = server.name
                    break
            
            for item in (file.name, expiry_readable, server_readable):
                widgetitem = QtGui.QTableWidgetItem(item)
                
                if file.expired == True:
                    widgetitem.setTextColor(red)

                self.tblFiles.setItem(row, column, widgetitem)
                
                column += 1
            row += 1
            
    def readarguments(self):
        """Reads the files from the arguments and saves them into the database"""
        
        if len(sys.argv) > 1:
            for file in sys.argv[1:]:
                filename = extractfilename(file)
                self.databaseconnection.fileentry(filename, int(time.time()) + 500, 1)
        
            
    def onClose(self):
        self.databaseconnection.close()
        self.close()
Also hier wird das Hauptfenster aufgebaut. In einem ersten Schritt wird zur Datenbank verbunden (Zeile 24). Ist etwas umständlich, da er die SQLite-DB im Skript-Ordner sucht (Wird in Zukunft noch auf den Home-Ordner geändert).
Dann wird die Funktion listservers() aufgerufen,welche die Datenbank ausliest und dort je Server-Objekt mit den verschiedenen Einstellungen erstellt. Hier direkt meine erste Frage: Lohnt es sich hier überhaupt, das mit einer Klasse zu machen? Ich finde es ganz nett, dass je ein Server eine Klasse hat und so alle Einstellungen gebündelt sind, aber ist das irgendwie problematisch (oder ressourcenverschwenderisch)? Für jeden Server wird dann auch ein Eintrag in der QComboBox cmbServer gemacht.

Anschließend liest er in readarguments() die Kommandozeilenargumente aus, die dem Programm übergeben wurden. Der Dateiname selber wird getrennt (also z.b. d:\uploader.py -> uploader.py) und dann dem Datenbankobjekt in der Funktion „fileentry“ übergeben. Die Funktion speichert dann die Datei in der Datenbank zusammen mit dem Ablaufdatum (im Moment noch fix jetzige Zeit + 500 Sekunden) und der Server-ID (im Moment noch fest). Im Moment noch nicht implementiert, allerdings soll readarguments() nachher auch noch die Dateien hochladen.

Anschließend wird listfiles() aufgerufen. Hierbei liest er alle Dateien, die in der Datenbank gespeichert sind, ein, erstellt für jede Datei ein Objekt und listet dies anschließend in dem QTableWidget tblFiles. Hierbei hätte ich mehrere Fragen:
1. Lohnt es sich, für jeden Dateieintrag eine Klasse zu machen? Ich habe mir überlegt, dass ich nachher z.B. der Datenbank oder der Server-Klasse o.ä. dann sehr einfach einfach jeweils ein Dateiobjekt übergeben könnte, wodurch dann intern z.B. der Dateiname extrahiert wird, die Datei von der Festplatte gelesen wird usw. blabla. Das könnte man dann soweit einfach dort intern verstecken und bräuchte immer nur das Dateiobjekt übergeben.
2. Meine Schleife um das QTableWidget zu füllen sieht mir etwas blöd aus. Im Moment geht er halt praktisch erst jede Spalte durch, erhöht dann column um 1, und nachdem eine Zeile fertig ist, geht er in die nächste Spalte. Ist das soweit in Ordnung?

Ok, und hier dann noch die Hintergrundklassen in der Datei background.py:

Code: Alles auswählen

import sqlite3
import ftplib
import time

class Server(object):
    """Class of servers
    
    A class of a server, which connects to FTP and uploads
    files. """
    
    def __init__(self, settings):
        """Reads multiple settings (in the setting of a SQLite entry
        and creates a new server object"""
        self.serverid = settings[0]
        self.name = settings[1]
        self.server = settings[2]
        self.port = settings[3]
        self.encrypted = settings[4]
        self.user = settings[5]
        self.password= settings[6]
        self.remotefolder = settings[7]
        #self.setting = settings
        
    def connectftp(self):
        self.ftp = ftplib.FTP(self.server, self.user,  self.password)
        self.ftp.cwd(self.remotefolder)
        #TODO: Verschlüsselte Verbindungen unterstützen
    
    def closeftp(self):
        self.ftp.quit()
        
    def upload(self, file,  filename):
        """Uploads a file
        
        Accepts a file (a complete path, e.g. d:\file.pdf) and the filename
        (file.pdf) and uploads it.
        """
        with open(file, "rb") as f:
            self.ftp.storbinary("STOR {}".format(filename), f)
        #TODO: Unicode unterstützen.

    def delete(self, filename):
        try:
            self.ftp.delete(filename)
        except:
            print(self.tr("{0} was not on the server".format(filename)))
    
    def listfiles(self):
        pass
        
class DatabaseFile(object):
    def __init__(self, databaseentry):
        self.fileid = databaseentry[0]
        self.name = databaseentry[1]
        self.expirydate = databaseentry[2]
        self.serverid = databaseentry[3]
        
        if self.expirydate < int(time.time()):
            self.expired = True
        else:
            self.expired = False
        
class Database(object):
    def __init__(self, databasefile):
        """Connects to SQLite database"""
        self.database = sqlite3.connect(databasefile)
        self.cursor = self.database.cursor()
        self.createtable()

    def close(self):
        """Closes the SQLite database"""
        self.database.close()        

    def createtable(self):
        tblcmd = """CREATE TABLE IF NOT EXISTS files 
                    (fileid INTEGER PRIMARY KEY, url TEXT, expire INT, serverid INT)"""
        self.cursor.execute(tblcmd)
        tblcmd = """CREATE TABLE IF NOT EXISTS servers 
                    (serverid INTEGER PRIMARY KEY, 
                    name TEXT,
                    server TEXT, 
                    port INT, 
                    encrypted INT,
                    user TEXT, 
                    password TEXT, 
                    remotefolder TEXT)"""
        self.cursor.execute(tblcmd)
        self.database.commit()

    def fileentry(self, file, expire, serverid):
        """Inserts a new file with expiry date into the webserver"""
        
        tblcmd = "INSERT INTO files(url, expire, serverid) VALUES(?, ?, ?)"
        self.cursor.execute(tblcmd,  (file,  expire, serverid))
        self.database.commit()
        #TODO: Keine bereits vorhanden Einträge zulassen.
    
    def saveserver(self, settings):
        tblcmd = """INSERT INTO servers 
                    (name, server, port, encrypted, user, password, remotefolder) 
                    VALUES(?, ?, ?, ?, ?, ?, ?)"""
        self.cursor.execute(tblcmd, settings)
        self.database.commit()

    def readserver(self):
        """Reads all saved server entries from the database"""
        
        self.cursor.execute("SELECT * FROM servers")
        servers = self.cursor.fetchall()
        return servers

    def readfiles(self):
        """Displays a list of all files stored in the database """
        self.cursor.execute("SELECT * FROM files")
        files = self.cursor.fetchall()
        return files
        
    def checkifold(self, time):
        """Returns old files. The current time must be supplied."""
        
        self.cursor.execute("SELECT fileid, url FROM files WHERE expire < ?",  (int(time),))
        oldfiles = self.cursor.fetchall()
        return oldfiles
        
    def deletefiles(self, fileid):
        """Deletes a file from the database"""
        
        self.cursor.execute("DELETE FROM files WHERE fileid = ?",  (fileid,))
        self.database.commit()
Hier wird halt zuerst eine Klasse Server erstellt, die die Einstellungen eines Servers aufnimmt. Außerdem kann die Server-Klasse einige FTP-Verbindungen durchführen (hochladen, löschen, Dateien auflisten).
Dann die Klasse DatabaseFile, die die Einstellungen einer Datei aus der Datenbank aufnehmen soll, z.B. wie der Dateiname ist und wann sie abläuft. Ich will da noch einige andere Sachen implementieren.
Außerdem die Klasse Database, die eben eine Datenbank mit den entsprechenden Tabellen erstellt und Einträge dort einträgt.

Danke an jeden, der sich den ganzen Text durchliest :)
BlackJack

@Hellstorm: `myappTranslator` könnte man auch einfach `tanslator` nennen. Immer beim Präfix `my` bei einem Namen sollte man sich IMHO überlegen ob das ein guter Name ist, denn meistens ist das `my` ein Zeichen, dass man sich nichts passendes ausdenken wollte. Denn dieser Präfix sagt ja im Grunde nichts aus.

Sternchenimporte sind keine gute Idee weil man sich damit alle Namen aus einem anderen Modul in das aktuelle Modul holt. Auch solche die man am Ende gar nicht verwendet. Ausserdem wird der Quelltext unübersichtlich weil man nicht mehr so leicht sieht wo ein Name herkommt. Wenn man mehrere Module so importiert kann es auch zu Namenskollisionen kommen.

Ich glaube das hatten wir schon einmal: `src` ist kein Name für ein Package. Überhaupt nicht.

Quelltext für die GUI zu generieren ist nicht mehr zeitgemäss. Diesen unnötigen Zwischenschritt kann man sich ganz einfach mit dem `PyQt4.uic`-Modul sparen.

Bei vielen Funktions-/Methodennamen fehlt ein UnterstrichumdieWortezutrennen. ;-)

Die erste Code-Zeile bei `extract_filename()` hat keinen Effekt, kann also ersatzlos gestrichen werden. `file` ist der Name eines eingebauten Datentypen und ausserdem passt der noch nicht einmal weil es sich um einen Datei*namen* beziehungsweise um einen Pfad handelt.

``return`` ist keine Funktion, also sollte man das auch nicht schreiben als wäre es eine.

Zum Verbinden von Signalen mit Slots gibt es eine bessere, weil kürzere und typsicherere Methode:

Code: Alles auswählen

        # Anstatt:
        self.connect(self.buttonClose, QtCore.SIGNAL("clicked()"), self.onClose)
        # Besser:
        self.buttonClose.clicked.connect(self.onClose)
Die letzte Zeile von `MainWindow.__init__()` hat keinen Effekt, kann also gestrichen werden.

Die Trennung zwischen GUI, Geschäftslogik, und Datenbank finde ich ein wenig „verwischt”. Warum werden in der GUI Objekte der Geschäftslogik aus „rohen” Datenbankergebnissen erstellt? Und warum müssen die Objekte der Geschäftslogik wissen wie der Datensatz aus der Datenbank aufgebaut ist?

Ich würde hier auch ein ORM wie SQLAlchemy ins Auge fassen. Das macht es einfacher Datensätze und Objekte aufeinander abzubilden und man muss sich nicht mehr direkt mit SQL herum schlagen und mit den feinen Unterschieden zwische verschiedenen SQL-DBMS.

In der GUI sollte man auch schauen ob man wirklich die „einfachen” Anzeigewidgets verwenden muss, wo man alles selber machen muss, oder ob man nicht doch besser die `*View*`-Widgets verwendet und entsprechende Modelklassen für die Daten schreibt.

Noch eine letzte Anmerkung zur GUI-Klasse: Attribute sollte man in der `__init__()` einführen. Sonst ist das für Leser irgendwann nicht mehr nachvollziehbar welche Attribute ein Objekt nun eigentlich hat.
Hellstorm
User
Beiträge: 231
Registriert: Samstag 22. Juni 2013, 15:01

Klasse, danke für die Antwort :) Ich werde das mal durchgehen und den Code entsprechend verbessern :)

Manche Sachen habe ich so aus irgendwelchen Tutorials/Büchern abgeschrieben, deswegen wirkt das manchmal etwas komisch. Danke schön!
Antworten