Python backup programm

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
pyCoder
User
Beiträge: 3
Registriert: Sonntag 18. Februar 2024, 08:58

Hallo zusammen,
ich bin neu hier im Forum und auch anfänger als Python programmierer, darum brauche ich eure Hilfe.
Habe vor einigen Tagen angefangen, ein Backup Programm in Python zu schreiben, jedoch stosse ich immer wieder auf einige schwierigkeiten.
Ich will, wenn ich den Haken gesetzt habe, dass das Programm wen ich auf Copy gedruckt habe, das Programm nur die angeklickte Dateien kopiert.
Dies ist ein Programm mit GUI:

Code: Alles auswählen

#### IMPORT BIBLIOTHEK
from tkinter import *
from os import *
from tkinter import messagebox

import os
import shutil
import tkinter as tk

#### VARIABLES
bgColor = "#333333"
fgColor = "#fefefe"
titleFont = "Tahoma, 12"
stdFont = "Tahoma, 10"
chkBoxPadX = 40
chkBoxPadY = 2
getUsername = os.path.expanduser('~')

#### DEFINITIONS
def msgBox():
    msg = Label(mainForm, text="nothing selected").pack(padx=200, pady=200)

def copy_all():
        source_file = os.path.expanduser('~') + r"\\tmp\\copy_2\\"
        destination_file = os.path.expanduser('~') + r"\\tmp\\test\\"
        shutil.copytree(source_file, destination_file, symlinks=False, ignore=None, copy_function=shutil.copy2, ignore_dangling_symlinks=False, dirs_exist_ok=True)

#### MAIN FORM
mainForm = tk.Tk()
mainForm.geometry("800x600")
mainForm.title("Codat - Save files and copy back") :geek: 
mainForm['bg'] = bgColor
#mainForm.iconbitmap('icon.ico')
mainForm.resizable(False, False)

chkBox_1 = IntVar()
chkBox_2 = IntVar()

class CheckBoxes:
    chkBoxShow = 1
    def __init__(chkBox, chkBoxLabel):
        var = StringVar()
        lblCheckbox = Label(mainForm, text="Standard selected files", width=30, anchor=NW, bg=bgColor, fg=fgColor, highlightbackground=bgColor, highlightthickness=0, font=titleFont)
        lblCheckbox.grid(row=0, column=0, padx=20, pady=(20, 20), sticky=NW)

        chkBox = Checkbutton(mainForm, text=chkBoxLabel, variable=chkBox_1, onvalue=chkBoxLabel, offvalue="", command=copy_all)
        chkBox.config(bg=bgColor)
        chkBox.config(fg=fgColor)
        chkBox.config(highlightbackground=bgColor)
        chkBox.config(highlightthickness=0)
        chkBox.config(activebackground=bgColor)
        chkBox.config(activeforeground=fgColor)
        chkBox.grid(row=CheckBoxes.chkBoxShow, column=0, sticky=W, padx=50, pady=5)
        CheckBoxes.chkBoxShow+=1

CheckBoxes("Desktop")
CheckBoxes("Documents")
CheckBoxes("Favorite")

#### BUTTONS
btnCopy = tk.Button(mainForm, text="Copy", width=10, command=copy_all)
btnCopy.place(x=5, y=500)

btnExit = tk.Button(mainForm, text="Exit", width=15, command=mainForm.quit)
btnExit.place(x=675, y=550)

#### SCREEN POSITION
scrWidth = 800
scrHeight = 600
screenW = mainForm.winfo_screenwidth()
screenH = mainForm.winfo_screenheight()
screenX = (screenW/2) - (scrWidth/2)
screenY = (screenH/2) - (scrHeight/2)

#### MAIN FORM VIEW
mainForm.geometry('%dx%d+%d+%d' % (scrWidth, scrHeight, screenX, screenY))
mainForm.mainloop()

Hat jemand eini Idee wie ich das lösen kann bzw. habe ich hier was falsch gemacht und was?

Besten Dank und Gruss
pyCoder
Danke und Gruss
pyCoder
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Kommentare sollten einen Mehrwert bieten, und ein Kommentar, der das Offensichtliche beschreibt, kann weg. Das was mit VARIABLES überschrieben ist, sind eigentlich Konstanten. Konstanten werden in Python nach Konvention KOMPLETT_GROSS geschrieben, damit man sie als solche erkennt, Variablen und Funktionen werden dagegen komplett_klein geschrieben.
`getUsername` ist zudem nicht der Name einer Konstanten, sondern ist wie eine Funktion benannt.

*-Importe benutzt man nicht, zumal Du alle Namen auch auf anderem Wege importierst.
Globale Variablen darf man nicht benutzen, alles was eine Funktion braucht, bekommt sie über ihre Argumente.

Du benutzt os.path um das Home-Verzeichnis zu bekommen, nutzt aber dann Stringzusammenstückeln, um einen kompletten Pfad zu bauen, das ist wieder sehr inkonsequent und falsch. In neuen Programmen sollte man os.path eh vergessen und statt dessen pathlib benutzen.
Defaultargumente sind deshalb default, weil man sie auch weglassen kann.

Code: Alles auswählen

from pathlib import Path
def copy_all():
        source_path = Path.home() / "tmp" / "copy_2"
        destination_path = Path.home() / "tmp" / "test"
        shutil.copytree(source_path, destination_path, dirs_exist_ok=True)
Du benutzt `class` falsch. Das was Du da hast, ist gar keine Klasse, sondern eine komisch geschriebene Funktion, die wieder viele globalen Variablen benutzt.
Man setzt die Konfiguration gleich beim Erzeugen der Checkbox und nicht erst danach, so wie Du das ja beim Label auch tust. Mit Farben und Schriften sollte man aber bei den Defaults bleiben, weil der User ja vielleicht absichtlich "hohen Kontrast" eingestellt hat.
Variablen sollte man nicht durchnummerieren, sondern eine passende Datenstruktur (hier Wörterbuch) benutzen, chkBox_1 wird dreimal benutzt, chkBox_2 nie.

Zum Schreiben von GUI-Programmen sollte man Funktionen und Klassen schon gut verstanden haben. Geh also am besten nochmal ein paar Schritte zurück und lerne erst die Grundlagen.

place darf man nicht benutzen, zumal Du ja schon grid benutzt. Der Fenstermanager weiß am besten, wo es Fenster plazieren kann, das sollte man nicht selbst machen, und glauben, man könnte es besser.

Zum eigentlichen Problem: wenn Du auf die Checkboxen reagieren möchtest, dann mußt Du ihren Zustand auch benutzen, also auf die IntVar-Objekte zugreifen.
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@pyCoder: Da ist in Zeile 31 ein ganz offensichtlicher Syntaxfehler, weil da etwas steht, was da nicht rein gehört.

Eingerückt wird in Python per Konvention mit vier Leerzeichen pro Ebene.

`messagebox` wird importiert aber nirgends verwendet.

Sternchen-Importe sind Böse™. Da holt man sich gerade bei `tkinter` fast 140 Namen ins Modul von denen nur ein kleiner Bruchteil verwendet wird. Auch Namen die gar nicht in `tkinter` definiert werden, sondern ihrerseits von woanders importiert werden. Das macht Programme unnötig unübersichtlicher und fehleranfälliger und es besteht die Gefahr von Namenskollisionen.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (PascalCase).

Namen sollten keine kryptischen Abkürzungen enthalten oder gar nur daraus bestehen. Der Name soll dem Leser vermitteln was der Wert dahinter im Programm bedeutet, nicht zum rätseln zwingen.

Man nummeriert keine Namen. Dann will man sich entweder bessere Namen überlegen, oder gar keine Einzelnamen/-werte verwenden, sondern eine Datenstruktur. Oft eine Liste.

`stdFont`, `chkBoxPadX` , `chkBoxPadY`, `getUsername`, `msgBox` und `chkBox_2` werden definiert, aber nirgends verwendet.

`getUsername` (beziehunsgweise `get_username`) wäre ein guter Name für eine Funktion oder Methode, aber nicht für einen passiven Wert.

Kommentare werden mit *einem* # eingeleitet, nicht vier. Und man sollte da auch nicht nur Grossbuchstaben im Text verwenden. Kommentare sollen dem Leser einen Mehrwert über den Code geben. Faustregel: Kommentare beschreiben nicht *was* der Code macht, denn das steht da bereits als Code, sondern warum er das macht. Sofern das nicht offensichtlich ist. Offensichtlich ist in aller Regel auch was in der Dokumentation von Python und den verwendeten Bibliotheken steht.

Die Fenstergrösse ergibt sich automatisch aus dem Inhalt, und die Position sollte man nicht selbst vorgeben, sondern der Fensterverwaltung überlassen.

`place()` verwendet man nicht. Da ist nicht garantiert ob die Pixel-Angaben zu der Bildschirmgrösse, -auflösung, und den -einstellungen passt. Im besten Fall sieht's komisch aus, im schlechtesten Fall wird die Oberfläche unbedienbar wenn sich Elemente überlappen oder aus dem sichtbaren Fenster herausragen.

Die Klasse `CheckBoxes` ist unsinnig. Methoden bekommen als erstes Argument immer `self`. Das wird in der `__init__()` welche die einzige Methode ist, aber überhaupt gar nicht verwendet. Wenn eine Methode `self` nicht verwendet, dann ist das aber keine sinnvolle Methode, denn dann kann man das einfach als Funktion ausserhalb der Klasse definieren. Damit hätte die Klasse nur noch ein Attribut das verändert wird, also eine globale Variable. Da man keine globalen Variablen haben will, ist die Klasse also leer und damit unnötig. Das hätte auch auffallen können weil mit den Exemplaren auch überhaupt gar nichts gemacht wird.

Warum heisst ein Wert der für eine Zeile in einem Grid steht `chkBoxShow`?

`var` wird in der `__init__()` definiert, aber nicht verwendet.

"Standard selected files" wird für jede Checkbox erneut als Label erzeugt und immer übereinander in die gleiche Grid-Zelle gesteckt.

Ein Widget erstellen und danach dann mehrfach `config()` aufrufen ist umständlich, weil man das auch alles beim Erstellen hätte angeben können.

An der Stelle würde ich auch die Finger von den ganzen Farben lassen. Du bügelst da alle möglichen Farben für Aktiv und Ausgewählt mit den gleichen Werten platt. Die haben ja einen Sinn und machen GUI leichter bedienbar. Und auch von der Schriftart und Grösse würde ich die Finger lassen. Das sind Sachen die der Benutzer entscheided, und zwar manchmal auch weil er das *muss* um überhaupt mit dem Rechner arbeiten zu können. Beispielsweise im Alter/bei Sehberhinderungen. Schriftart, Grösse, und Kontraste sollte man nicht ändern weil man das ”schöner” findet.

Bei den Checkboxen sind `onvalue` und `offvalue` Zeichenketten und die `variable` eine `IntVar` — das geht natürlich nicht. Ausserdem verwenden alle Checkboxen das *selbe* `IntVar`-Objekt.

Es macht eher keinen Sinn jedes mal die Kopierfunktion aufzurufen wenn eine Checkbox an- oder abgehakt wurde.

Worte sollten in Namen die richtige Reihenfolge haben. Eine Button-Kopie ist was anderes als ein Kopier-Button.

Man muss auch nicht jedes Zwischenergebnis an einen Namen binden.

Man bastelt keine Pfade mit Zeichenkettenoperationen und hart kodierten Pfadtrennern zusammen, sondern verwendet dafür `pathlib`. Die Funktionen aus `os.path` nur wenn es tatsächlich keinen entsprechenden Weg mit `pathlib` gibt.

Defaultargumente sind dazu da, das man die gerade *nicht* beim Aufruf angeben muss.

Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python3
import shutil
import tkinter as tk
from pathlib import Path


def copy_all():
    base_path = Path.home() / "tmp"
    shutil.copytree(
        base_path / "copy_2", base_path / "test", dirs_exist_ok=True
    )


def create_checkbox(master, row, label):
    #
    # TODO Damit man damit später sinnvoll etwas machen kann, sollte das
    #   vielleicht der Rückgabewert dieser Funktion sein.
    #
    variable = tk.StringVar(master)
    checkbox = tk.Checkbutton(
        master, text=label, variable=variable, onvalue=label, offvalue=""
    )
    checkbox.grid(row=row, column=0, sticky=tk.W, padx=50, pady=5)


def main():
    root = tk.Tk()
    root.title("Codat - Save files and copy back")
    root.resizable(False, False)
    tk.Label(
        root,
        text="Standard selected files",
        anchor=tk.NW,
        font="TkHeadingFont",
    ).grid(row=0, column=0, padx=20, pady=20, sticky=tk.NW)
    create_checkbox(root, 1, "Desktop")
    create_checkbox(root, 2, "Documents")
    create_checkbox(root, 3, "Favorite")

    tk.Button(root, text="Copy", command=copy_all).grid(row=4, column=0)
    tk.Button(root, text="Exit", command=root.quit).grid(row=5, column=0)

    root.mainloop()


if __name__ == "__main__":
    main()
Falls die Datenmengen grösser sind, sollte man bedenken, dass die GUI einfriert solange die Kopierfunktion läuft, und das manche Betriebssysteme nach einer gewissen Wartezeit den Benutzer fragen ob die Anwendung beendet werden soll, weil sie nicht mehr reagiert.

Und man sollte sich überlegen ob Fehler entsprechend sinnvoll erkannt werden und behandelt werden, weil das bei Sicherungskopien ja doch wichtig ist, das der Benutzer mitbekommt, wenn etwas nicht funktioniert hat, und er keine Kopie hat.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
pyCoder
User
Beiträge: 3
Registriert: Sonntag 18. Februar 2024, 08:58

@Sirius3, danke Dir für die Kritik. Ich mache gerade Kurs über Python und kenne die Theorie noch nicht so gut, darum so viele Fehler :-)
Das mit den Variablen und Konsanten ist auch sehr guter Tipp, danke. Das mit dem os.path habe ich im Internet und auf Youtube videos gesehen, darum habe ich so verwendet.

@__blackjack__, danke Dir auch für die Ausführliche Erklärung und werde probieren, mich so umzustellen um dies weiter zu verwenden. Alle die benennungen habe ich aus dem Internet gelesen und auf Youtube gesehen und ist schwierig zu entscheiden, welches Weg ist der richtige.
Ich muss hier den aktuellen Benutzer ansprechen:

Code: Alles auswählen

os.path.expanduser('~')
Wie mache ich das mit dem pathlib so:

Code: Alles auswählen

pathlib.Path.home


Habt ihr evtl. einen guten Ansatz wo man am besten dies erlernen kann?

Besten Dank nochmals an alle.
Danke und Gruss
pyCoder
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@pyCoder: Was die Namensschreibweise angeht ist der Style Guide for Python Code der richtige Weg. Man sollte schon einen guten Grund haben davon abzuweichen.

Man findet im Internet und auf YouTube auch jede Menge schlechten Code und schlechte Tutorials. Oder auch einfach Sachen die veraltet sind, die man heute anders macht als in Python 1.4 in 1996, wo es schon `os.path.expanduser()` gab, aber kein `pathlib`-Modul.

Das Tutorial in der Python-Dokumentation sollte man mal durchgearbeitet haben.

Was `pathlib.Path.home()` macht kann man in der Dokumentation nachlesen, und auch einfach mal im interaktiven Interpreter ausprobieren. Und man kann sich auch den Quelltext anschauen.

Ich benutze als Python-Shell ganz gerne IPython, da kommt man sehr einfach an den Quelltext von in Python geschriebenen Funktionen und Methoden.

Code: Alles auswählen

In [199]: pathlib.Path.home()
Out[199]: PosixPath('/home/bj')

In [200]: pathlib.Path.home??
Signature: pathlib.Path.home()
Source:   
    @classmethod
    def home(cls):
        """Return a new path pointing to the user's home directory (as
        returned by os.path.expanduser('~')).
        """
        return cls("~").expanduser()
File:      /usr/local/lib/python3.10/pathlib.py
Type:      method
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
pyCoder
User
Beiträge: 3
Registriert: Sonntag 18. Februar 2024, 08:58

@Danke __blackjack__, habe die Lösung gefunden und es funktioniert

Code: Alles auswählen

Path.home()
Ich habe das Programm erweiter und stecke jetzt bei anderem Problem

Code: Alles auswählen

import os
import shutil
import tkinter as tk
from pathlib import Path
from tkinter import IntVar

bg_color = "#333333"
fg_color = "#fefefe"
title_font = "Tahoma, 12"
std_font = "Tahoma, 10"

def create_checkbox(master, row, label):
    variable = tk.StringVar(master)
    checkbox = tk.Checkbutton(master, text=label, variable=variable, onvalue=label, offvalue="")
    checkbox.grid(row=row, column=0, sticky=tk.W, padx=50, pady=5)

# Create GUI for program
def main():
    root = tk.Tk()
    root.geometry("800x600")
    root.title("Codat - Save files and copy back")
    root['bg'] = bg_color
    #root.iconbitmap('icon.ico')
    root.resizable(False, False)

    def copy_all():
        #
        #If checkbox selected, copy files from local user profile to user profile on the network drive, else nothing to do
        #
        if(checkbox_desktop.get() == 1)or(checkbox_documents.get() == 1)or(checkbox_favorites.get() == 1):
            local_path = Path.home()
            network_path = Path.home() / "tmp"
            shutil.copytree(local_path / "Desktop", network_path / "test" / "Desktop", dirs_exist_ok=True)
            shutil.copytree(local_path / "Documents", network_path / "test" / "Documents", dirs_exist_ok=True)
            shutil.copytree(local_path / "Favorites", network_path / "test" / "Favorites", dirs_exist_ok=True)
        else:
            tk.Message("nothing selected, nothing to copy")

        checkbox_desktop = IntVar()
        checkbox_documents = IntVar()
        checkbox_favorites = IntVar()

    tk.Label(
        root, text="Standard selected files", anchor=tk.NW, font=title_font, fg=fg_color, bg=bg_color
    ).grid(row=0, column=0, padx=20, pady=20, sticky=tk.NW)
    create_checkbox(root, 1, "Desktop")
    create_checkbox(root, 2, "Documents")
    create_checkbox(root, 3, "Favorites")

    tk.Button(root, text="Copy", width=10, command=copy_all).grid(row=15, column=0, padx=(300, 0), pady=(250, 0))
    tk.Button(root, text="Exit", width=15, command=root.quit).grid(row=16, column=0, padx=(650, 0), pady=(100, 0))

    root.mainloop()

if __name__ == "__main__":
    main()
Ich will das die Dateien kopiert werden, aber nur wen der Haken im Checkbox aktiviert wurde, ansonsten nichts machen:

Code: Alles auswählen

        if(checkbox_desktop.get() == 1)or(checkbox_documents.get() == 1)or(checkbox_favorites.get() == 1):
            local_path = Path.home()
            network_path = Path.home() / "tmp"
            shutil.copytree(local_path / "Desktop", network_path / "test" / "Desktop", dirs_exist_ok=True)
            shutil.copytree(local_path / "Documents", network_path / "test" / "Documents", dirs_exist_ok=True)
            shutil.copytree(local_path / "Favorites", network_path / "test" / "Favorites", dirs_exist_ok=True)
        else:
            tk.Message("nothing selected, nothing to copy")
Danke und Gruss
pyCoder
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@pyCoder: `Path.home()` wurde ja in meinem Zwischenstand bereits verwendet.

Man definiert normalerweise keine Funktionen *in* Funktionen. Das ist unübersichtlich, die haben Zugriff auf alles mögliche ausserhalb auf das sie keinen Zugriff haben sollten, und man kann die nicht separat aufrufen, zum Beispiel um sie zu testen, oder bei der Fehlersuche.

Bei jeder nicht-trivialen GUI kommt man um objektorientierte Programmierung (OOP) nicht herum. Also eigene Klassen schreiben. Denn man muss sich über Aufrufe hinweg Zustand merken, und dafür sind Klassen da.

Die `IntVar`-Objekte werden an einer falschen Stelle erstellt, und nicht wirklich verwendet, denn die werden ja nie irgendwo gesetzt, nur abgefragt.

Dafür werden die `tk.StringVar`-Objekte die von den Checkboxen gesetzt werden, nirgends abgefragt. Da hatte ich ja einen Kommentar geschrieben was man mit diesen Objekten machen sollte.

Was die Frage angeht: Wenn Du mit dem ``if`` alle drei Variablen abfragst, weisst Du natürlich nicht welche davon gesetzt waren. Du musst die einzeln abfragen. Und am besten auch nicht mit eigenem Code für jede einzelne, sondern die sollten in einer Liste stecken und in einer Schleife abgarbeitet werden. So braucht man nur einmal Code schreiben, der für beliebig viele Checkboxen/Variablen/Verzeichnisse funktioniert.

Da sind ein paar sehr komische pad-Werte. 300, 250, 650 — das sieht wieder so aus als sollte hier über einen Umweg etwas pixelgenau positioniert werden. Das funktioniert nicht wirklich, denn wie viel Platz 100 Pixel einnehmen hängt von der Bildschirmgrösse und -auflösung ab, und da gibt es sehr viel Spielraum heutzutage.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten