Konstruktive Kritik zu pathlib und HTML Mail Versand Verwendung

Registriere dich jetzt, um exklusive Vorteile zu genießen! Als registriertes Mitglied kannst du Inhalte herunterladen und profitierst von einem werbefreien Forum.
Mach mit und werde Teil unserer Community!
  • Hallo,

    heute war es so weit und ich rang mich durch erstmals anstatt os pathlib zu verwenden.

    Da man neben dem selber Machen am meisten durch konstruktive Kritik lernt, bitte ich um Tipps für folgenden Code:



    Die Vermischung von englisch und deutsch ist mir bewusst, bekomm ich aber nicht (mehr) raus. Die Formatierung von Black gefällt mir nicht. Die 2 Punkte könnt ihr also außer acht lassen.

    Der Code funktioniert und erledigt was er machen soll.

    2 spezielle Frage von mir:

    • Wie formatier ich das in Zeile 111-113 am besten?
    • Setzt man die HTML eMail iwie anders besser zusammen?


    Was macht der Code überhaupt?

    Da die QNAP HBS Backup App bei mir irgendwie nicht richtig mit der Versionskontrolle funktioniert, bzw. unlogisch groß das Backup wird, habe ich mir in einem anderen Skript eine eigene Versionierung geschrieben um Backups von verschiedenen Zeiträumen (Tage, Wochen, Monate) vorzuhalten.

    Dieser Code hier soll mir einmal täglich eine eMail schicken, in der steht wie viele Backups im entsprechenden Tage, Wochen, Monatordner vorzufinden sind und deren Namen und Größe in einer Tabelle darzustellen.


    Wie gesagt es funktioniert :thumbup:



    EDIT:

    Was mir gerade selbst noch aufgefallen ist, die Ausgabe gehört chronologisch sortiert und geprüft ob nicht nur die Anzahl, sondern auch vom Datum her die Backups vorhanden sind.


    EDITEDIT:

    Gerade noch einen Fehler gefunden, die Größe der Ordner bei Tagesbackup stimmt nicht behoben

  • Hi,


    vorab deine zwei Fragen kann ich leider nicht beantworten und ich kenne auch nicht alles was du verwendest.

    Mir sind nur folgende Sachen aufgefallen:


    'SKRIPTPFAD' nutzt du eigentlich nur um 'CONFIG' zu erstellen. Da auch auf Modulebene kein ausführbarer Code stehen sollte, würde ich gleich eine Konstante erstellen, die 'config.toml' schon enthält. Dann wäre 'CONFIG' bei mir allerdings auch keine Konstante mehr. Ich würde in der 'main' - Funktion dann 'load_backup_config' aufrufen.

    Die 'backuppfade_tage', 'backuppfad_wochen' und 'backuppfad_monate' würde ich ich in einer extra Funktion definieren. Auch um Programmfunktionen unabhängig einfacher testen zu können.

    Die Anzahl der Dateien kann man auch ohne 'enumerate' etwas kürzer zählen und mit 'sum' gleich zusammen rechnen lassen. 'itertools' hat alternativ auch noch eine 'count' Funktion.

    Hab das mal eingebaut:


    Beim anschauen des Codes fand ich etwas unübersichtlich, dass die Klasse immer wieder Funktionen außerhalb der Klasse aufruft und dann wieder zurück in der Klasse weiter gearbeitet wird. Habe ehrlich gesagt aber gerade einen zu langen Tag hinter mir um die Logik weiters zu hinerfragen oder eine Alternative aufzustellen. (Vielleicht sehe ich das morgen auch wieder anders ^^ )


    Grüße

    Dennis

    🎧 Mein Auto springt, mein Toaster kocht, es zwickt mich im Genick. Meine Frau ist eingelocht, die Spülmaschine tickt. Meine Telefonapperat brüllt mich seit Tagen an, er ist schon lange abgestellt im Bett liegt Peter Pan. Die Uhr geht falsch, die Haustür singt, mein Spiegel schlägt zurück - Ich werde noch verrückt, was solls ich bin entzückt. Die Badewanne zieht nicht ab ihr glaubt nicht was ich seh' - Sie ist voll Himbeerengelee 🎧

  • Ich hätte noch etwas zur HTML-Ausgabe zu sagen. Soweit ich das richtig sehe, werden alle Zeilen und Zellen der Tabelle in der Funktion tabelle_erstellen() in eine Zeile geschrieben. Das ist dem Frontend zwar sehr wahrscheinlich egal, aber trotzdem sollte wenigstens pro Tabellenzeile (<tr> ... </tr>) ein Zeilenumbruch \n rein.


    Besser weil übersichtlicher wäre allerdings eine neue Zeile pro "Tabellenendtag". Das wären </tr> für Zeilenende, </td> für Zellenende und </th> für Kopfzellenende. Das würde anhand der z.B. Tabellenzeile so </tr>\n aussehen.

  • Hofei: So beim drüber schauen ist mir am deutlichsten aufgefallen, dass da einiges an Code dreimal wiederholt wird für Tage, Wochen, und Monate, was sich offenbar hauptsächlich durch Attributnamen und Pfade unterscheidet, aber sonst das gleiche macht. Das sollte vielleicht in eine Klasse herausgezogen werden.


    Auf der anderen Seite ist da die `Controller`-Klasse deren Sinn ich nicht so ganz verstehe, was vielleicht auch den nichtssagenden Namen erklärt. Vielleicht auch ein Übersetzungsproblem, denn das Exemplar heisst dann `backup_kontrolle`: „control“ heisst „Steuerung“ und nicht „Kontrolle“.


    Und dann Listen die immer genau ein und nach einem bestimmten Verarbeitungsschritt genau zwei Elemente enthalten. Und dann die obligatorischen magischen Indizes 0 und 1 für den Zugriff was dem Leser so nicht wirklich irgendwas über die Bedeutung dieser Werte verrät.


    Die Daten werden unnötigerweise zweimal vom Dateisystem geholt, einmal um sie zu zählen und dann um ein Wörterbuch zu erstellen. Man könnte auch das Wörterbuch erstellen und dann mit `len()` die Anzahl ermitteln.


    Die `*_ok`-Attribute enthalten redundante Daten. Das ist immer unschön, weil man aufpassen muss, dass die immer mit dem Rest übereinstimmen. Wie schon gesagt ist das eigentlich ein simpler Vergleich zwischen der Anzahl der Tage und der Dateien in dem Wörterbuch. Man braucht den Wert also nicht als Wert speichern, sondern kann den leicht bei Bedarf prüfen. Ein `property` bietet sich da an. Genau wie für die Anzahl der Dateien.


    `get_file_name_and_size()` ist eigentlich ein einziger Ausdruck, eine „dict comprehension“.


    In `load_backup_config()` wird erst die Datei geladen und dann dekodiert, wo `toml` doch selbst eine Funktion hat, die das Laden übernimmt. Die Funktion kann man als alias definieren: ``load_backup_config = toml.load``. Oder weglassen.


    `count_files()` ist umständlich ausgedrückt mit dem -1 und +1. Man kann `enumerate()` auch einen Startwert geben. Aber kürzer wäre der Einzeiler ``return sum(1 for _ in pfad.iterdir())`` gewesen.


    `dict_laenge_angleichen()` sollte man sich mit `itertools.zip_longest()` sparen können.


    In `tabelle_erstellen()` sieht man ein „anti pattern“ eine Zeichenkette zu erweitern. In jedem Schritt wo etwas dazu kommt wird eine neue Zeichenkette erstellt in die die alte Zeichenkette und die neue kopiert wird. Das ist superineffizient. Idiomatisch wäre es die Einzelteile in einer Liste zu sammeln und die am Ende mit `join()` zusammenzusetzen.


    Dateinamen können Sachen enthalten, die das HTML durcheinander bringen. Minimum wäre das zu escapen, wobei ich persönlich da gleich zu einer Template-Engine wie Jinja2 greifen würde.


    `get_html_farbe()` kann man mit einem bedingten Ausdruck in eine Zeile bringen. Und da sollte so ein `*_ok`-Attribut verwendet werden, denn die werden gar nicht benutzt im Moment.


    Ungetestet:

    „Eat the rich — the poor are full of preservatives.“ — Rebecca ”Becky” Connor, The Connors

  • Hallo,

    vielen Dank euch Dreien für euren Input. :thumbup: Natürlich hatte ich euere Beiträge sofort gelesen. Jetzt musste ich aber erst einiges Nachlesen und mit Schritt für Schritt debuggen etwas Klarheit schaffen.


    'SKRIPTPFAD' nutzt du eigentlich nur um 'CONFIG' zu erstellen. Da auch auf Modulebene kein ausführbarer Code stehen sollte, würde ich gleich eine Konstante erstellen, die 'config.toml' schon enthält.

    An anderer Stelle, leider kann ich mich nicht mehr erinnern durch wem und welcher Thread, wurde mir mal empfohlen, diese Art an Config als Konstante zu definieren, denn der Inhalt der Variable Config ist definiert durch den Inhalt der Datei und wird nur einmalig eingelesen und während dem Skript nicht mehr verändert und darf als Konstante definiert werden.


    wo `toml` doch selbst eine Funktion hat, die das Laden übernimmt.

    :blush: wie oft ich mittlerweile toml verwendet hatte kann ich gar nicht mehr sagen, aber ich verwendete bisher nur loads() und nicht load():danke_ATDE: betriebsblind :geek:



    Die 'backuppfade_tage', 'backuppfad_wochen' und 'backuppfad_monate' würde ich ich in einer extra Funktion definieren.

    Da gebe ich dir völlig recht, das macht das ganze flexibler :thumbup:


    Das würde anhand der z.B. Tabellenzeile so </tr>\n aussehen.

    Auch dir stimme ich zu, sollte man wirklich mal den Quelltext lesen wollen/müssen, ist es besser, wenn man diesen auch lesbar gestaltet und nicht alles in eine Zeile knallt.

    Dateinamen können Sachen enthalten, die das HTML durcheinander bringen. Minimum wäre das zu escapen, wobei ich persönlich da gleich zu einer Template-Engine wie Jinja2 greifen würde.

    Das minimum hast du umgesetzt mit markupsafe.escape_silent() korrekt? HTML durcheinander bringen? Wie darf ich mir das vorstellen? Webzeugs bin ich noch ganz schwach, wobei ich auch hier vor einem guten Monat mal ein erstes kleines PHP Spagetti Projekt (von einem Freund erstellt) in Python mit Hilfe von Flask überführt hatte und dabei auch erstmals mit Jinja2 durch Flask in Berührung kam. Das ich Jinja2 hier auch standalone nutzen hätte können, der Gedanke kam mir nicht.


    In `tabelle_erstellen()` sieht man ein „anti pattern“ eine Zeichenkette zu erweitern. In jedem Schritt wo etwas dazu kommt wird eine neue Zeichenkette erstellt in die die alte Zeichenkette und die neue kopiert wird. Das ist superineffizient. Idiomatisch wäre es die Einzelteile in einer Liste zu sammeln und die am Ende mit `join()` zusammenzusetzen.

    Genau so, wie es in deinem Code jetzt ist habe ich mir das vorgestellt das es sein müsste, also logischer aufgebaut. Nur das wie fehlte mir. Aber immerhin kam mir meine Erstellung schon "merkwürdig" vor.

    Setzt man die HTML eMail iwie anders besser zusammen?



    dass da einiges an Code dreimal wiederholt wird für Tage, Wochen, und Monate, was sich offenbar hauptsächlich durch Attributnamen und Pfade unterscheidet, aber sonst das gleiche macht. Das sollte vielleicht in eine Klasse herausgezogen werden.

    Ja, jetzt wo du es sagst fallen mir die Blätter von den Augen. Warum ich das selbst nicht erkannte :denker: . Genau dafür ist ja so eine Klasse praktisch. 3 Sachen vom Schema gleich, nur der Inhalt ist anders. Erstelle eine Klasse.


    Zu dem Code von __blackjack__ noch gezielt ein paar Fragen:


    • Zeile 33: cls fungiert als self in einer @classmethod korrekt und ist für die bessere Unterscheidung nur anders benannt?
    • Zeile 114: self.is_ok hat schon schon Wert beim Schritt für Schritt debuggen direkt nach der Instanzierung, bevor die Methode überhaupt aufgerufen worden ist, ebenso bei self.dateianzahl, warum?


    Vielen Dank nochmals für eure Zeit und Mühe.

    Mir hat es sich wieder gezeigt, gerade wenn man es nur zum Hobby macht, ist es wichtig immer wieder um konstruktive Kritik zum Code zu bitten. Nur so lernt man neue Sachen kennen und verbessert den eigenen Codestil. Hoffentlich kann ich das hier gezeigte das nächste Mal schon eigenständig umsetzen und verfalle nicht in das alte Muster. Auch habe ich wieder jede Menge neuer Module kennen gelernt :danke_ATDE:

  • Hallo,


    Quote


    HTML durcheinander bringen? Wie darf ich mir das vorstellen?

    Wenn im Dateinamen irgendwas drin wäre, was in HTML eine Bedeutung hat, im Sinne von das der HTML-Parser im Browser es interpreitert, könnte das zu einer falschen Darstellung führen. Z.B. wenn & oder < oder > im Dateinamen sind. Was je nach Betriebssystem ja gehen würde.


    Quote

    Das minimum hast du umgesetzt mit markupsafe.escape_silent()

    Richtig.


    Quote

    Das ich Jinja2 hier auch standalone nutzen hätte können, der Gedanke kam mir nicht.

    Jinja2 sieht an zwar meistens im HTML-Kontext, aber grundsätzlich kannst du damit beliebige Tempates mit beliebigem Inhalt erstellen, in Sinne von dass damit alles möglich ist, was textbasiert ist. Du könnstest damit z.B. auch Markdown oder LaTeX erstellen.


    Gruß, noisefloor

  • Hofei: Zum HTML hat noisefloor ja schon was geschrieben.


    `self` ist per Konvention der Name eines Objekts das aus der Klasse erstellt wurde. Klassenmethoden bekommen aber die Klasse selbst als erstes Argument. Darum ist da der Name `cls`. Auch per Konvention. Die üblichen IDEs/Linter sollten da auch warnen wenn man das anders nennt.


    Bezüglich Properties: Wenn ich die Frage richtig verstehe: Weil das keine Methode ist sondern ein `property()`. Also es ist ursprünglich eine Methode gewesen, aber der `property()`-Dekorator ersetzt die durch ein Attribut, das schon beim Abfragen vom Objekt etwas ausführt, statt einfach nur einen festen Wert zu liefern. Und was ausgeführt wird, ist die Methode die `property()` übergeben wird. Technisch gesehen ist das Stichwort „Deskriptoren“, wenn man sich `property()` selber schreiben wollen würde. Das ist aber etwas das man selten wirklich selbst braucht. Ungefähr so esoterisch wie Metaklassen oder die `__new__()`-Methode.

    „Eat the rich — the poor are full of preservatives.“ — Rebecca ”Becky” Connor, The Connors

  • Post by Hofei ().

    The post was deleted, no further information is available.
  • EDIT:

    Was mir gerade selbst noch aufgefallen ist, die Ausgabe gehört chronologisch sortiert

    Die Lösung das die Ausgabe sortiert war, erreichte ich in meinem Code mit dieser Funktion

    Code
    def sort_dict(dict_):
        return dict(sorted(dict_.items(), key=lambda item: item[0]))


    Dies lässt sich jetzt aber nicht mehr Anwenden wegen dem frozen bei attrs.

    Schaffe es aber auch nicht zu integrieren zum Zeitpunkt der Erstellung. Kann mir hier noch wer auf die Sprünge helfen.

  • Also würde die Funktion dann so aussehen?

    Python
    def sort_dict(dict_):
        return dict(sorted(dict_.items(), key=dict_.itemgetter()))

    Da bekomme ich dann folgenden Traceback:


    Aber mein Problem aus #9 behebt das noch nicht.

    Das die Ausgabe sortiert ist erreichte ich mit folgender Änderung:

    Jetzt habe ich noch das Problem, das die Dateigröße nicht stimmt wenn es sich um einen Ordner handelt.

    `get_file_name_and_size()` ist eigentlich ein einziger Ausdruck, eine „dict comprehension“.

    dateien = {pfad.name: pfad.stat().st_size for pfad in backup_pfad.iterdir()} das ermittelt nicht korrekt die Gesamtgröße des Ordners

  • dateien = {pfad.name: pfad.stat().st_size for pfad in backup_pfad.iterdir()} das ermittelt nicht korrekt die Gesamtgröße des Ordners

    So, der Fehler liegt wohl darin, dass ich in meinem Edit in #1 zwar erwähnte dass der Code eine fehlerhafte Ordnergröße im Tagesordner ausgibt, aber meinen orginalen Code nicht mehr anpasste. Somit kam das verbesserte Codebeispiel auf einen von mir fehlerhafte Codebasis.


    Mein aktueller Code sieht nun so aus:


    Zeile 41 -59 ist das hinzugefügte von mir um die korrekte Größe zu erhalten und in einer sortierten Ausgabe.

    ( DeaD_EyE habe das mit dem lambda jetzt mal bewusst drin gelassen, da ich mir so einen import spare)

    Meine Lösung ist jetzt halt wieder keine Comprehension mehr, sollte aber auch nicht weiter schlimm sein?

    Wie lassen sich die Zeilen noch weiter verbessern?

  • Hallo,


    aus dem Bauch heraus, geht folgendes?

    Code
    def get_folder_size(pfad):
        # 'pfad' ist in meinem Beispiel schon ein Path-Objekt
        return sum([file_.stat().st_size for file_ in pfad.rglob('*')])


    Grüße

    Dennis

    🎧 Mein Auto springt, mein Toaster kocht, es zwickt mich im Genick. Meine Frau ist eingelocht, die Spülmaschine tickt. Meine Telefonapperat brüllt mich seit Tagen an, er ist schon lange abgestellt im Bett liegt Peter Pan. Die Uhr geht falsch, die Haustür singt, mein Spiegel schlägt zurück - Ich werde noch verrückt, was solls ich bin entzückt. Die Badewanne zieht nicht ab ihr glaubt nicht was ich seh' - Sie ist voll Himbeerengelee 🎧

  • Dennis89 Das kann man auch mit `iterdir()` machen und `sum()` kann man auch ein Generatorobjekt geben, also ohne die eckigen Klammern die erst eine komplette Liste mit den Grössen erstellen.

    Python
    def get_folder_size(pfad):
        return sum(sub_path.stat().st_size for sub_path in pfad.iterdir())

    Wobei das dann auch nur die Grössen direkt in dem Ordner aufaddiert, aber nicht die in Ordnern die in dem gegeben Ordner stecken. Keine Ahnung wie weit Hofei da gehen möchte.

    „Eat the rich — the poor are full of preservatives.“ — Rebecca ”Becky” Connor, The Connors

  • Danke für den Hinweis.

    `sum()` kann man auch ein Generatorobjekt geben,

    das blöde/peinliche daran ist, dass du mir das schonmal gesagt hast :blush:


    Grüße

    Dennis


    Edit: Das 'rglob' habe ich einfach so übernommen.

    🎧 Mein Auto springt, mein Toaster kocht, es zwickt mich im Genick. Meine Frau ist eingelocht, die Spülmaschine tickt. Meine Telefonapperat brüllt mich seit Tagen an, er ist schon lange abgestellt im Bett liegt Peter Pan. Die Uhr geht falsch, die Haustür singt, mein Spiegel schlägt zurück - Ich werde noch verrückt, was solls ich bin entzückt. Die Badewanne zieht nicht ab ihr glaubt nicht was ich seh' - Sie ist voll Himbeerengelee 🎧

  • Dennis89 und __blackjack__ glob, rglob und iterdir liefern auch Verzeichnisse.

    Verzeichnisse belegen 4096 Bytes (Metadaten) unter Linux.

    D.h. wenn sich dort auch Verzeichnisse befinden, stimmt die Größe nicht mehr.


    Code
    file_size_1 = sum(element.stat().st_size for element in Path("Downloads").rglob("*"))
    file_size_2 = sum(element.stat().st_size for element in Path("Downloads").rglob("*") if element.is_file())

    PS: Der Generator benötigt weniger Speicher. Bei 1e6 Dateien kann sich das schon bemerkbar machen. Wenn man stattdessen erst eine List Comprehension macht, wird für die Liste Speicher allokiert. Nachdem die gesammte Liste vollständig ist, wird die Summe gebildet. Macht man das mit einem Generator, geschieht die Addition iterativ und der Speicherverbrauch ist viel geringer.


    PPS: Generatoren können Generatoren konsumieren. Der Code von Dennis ist die Funktion sum mit einer List Comprehension als Argument. Also auch ein Speicherfresser. Bei 12 Dateien denkt man aber nicht lange nach und kann auch ruhig die List Comprehension verwenden.

  • Keine Ahnung wie weit Hofei da gehen möchte.

    Am besten lässt sich das wohl mit der tatsächlichen Situation zeigen:


    Vor meiner Korrektur wurden nur die 4,0K von dem Ordner ausgegeben (erster Block). Ich möchte aber die Ausgabe der Gesamtgröße aller Ordner und Dateien innerhalb der einzelnen Ordner vom ersten Block. Und der Ordnername auf oberster Ebene soll als Zelleninhalt in der eMail stehen (aber das funktioniert ja eh schon)

    Auch mit meiner Korrektur funktioniert es schon. Jetzt muss ich es nur mit euren Vorschläge mittels * Comprehension probieren.


    Das wäre sowieso meine nächste Frage gewesen, warum ihr immer lieber alles, wo möglich, über eine Comprehension macht. Aber das hat DeaD_EyE wohl schneller beantwortet als ich die Frage stellen konnte

    PS: Der Generator benötigt weniger Speicher

    Ich persönlich bin noch kein großer Freund der * Comprehensions, da ich zum einen in der IDE beim Schritt für Schritt debuggen nicht seh was passiert (oder liegt das nur an PyCharm?) und zum anderen der Code "rückwärts" gelesen werden muss. Da komm ich noch nicht ganz so klar damit.