Dynamisches setzten einer Klassenvariable mit Validierung

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
rbaert
User
Beiträge: 20
Registriert: Mittwoch 5. September 2018, 15:37

Hallo zusammen.

Ich habe eine Knacknuss, die ich nicht lösen kann.

Ich habe eine Klasse, bei der ich mit pydantic die Klassenvariabeln validieren will.

Code: Alles auswählen

from pydantic import BaseModel, Field, ValidationError, validator


class ConfigDefaults(BaseModel,
                     validate_assignment=True,
                     revalidate_instances='always',
                     extra='forbid'):
    file_base_path: str = os.path.join(
        os.path.dirname(
            os.path.dirname(
                os.path.dirname(
                    os.path.realpath(__file__)
                    )
                )
            ), 'log'
        )
    file_add_timestamp: bool = Field(validate_default=True, default=True)

    @validator('file_base_path')
    def validate_path(cls, v: str) -> str:
        return Path(v)

    def __iter__(self):
        for item in self.__dict__.items():
            yield item

    def set_value_by_key(self, key, value):
        try:
            self.__dict__[key] = value
        except ValidationError as e:
            print(e)
Soweit so gut, es funktioniert.
Nun möchte ich bei einer Instanz dieser Klasse, die Variabeln dynamisch ändern können. Also durch die Angabe der Variabelnamens und des neuen Wertes (siehe Methode: "set_value_by_key").

Den Wert ändern kann ich problemlos, allerdings funktioniert in diesem Fall der Validator nicht. Ich habe diverses ausprobiert aber ich komme nicht darauf wie ich das lösen könnte.

Hat jemand von euch einen Tipp, wie das funktionieren könnte.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Gar nicht. Statisches Type-Checking und dynamische Datentypen gehen nicht zusammen. Warum modellierst du das nicht einfach direct als dict? Denn das ist es ja effektiv.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Noch ein Nachtrag: das os.path-Modul sollte man in Zeiten von pathlib.Path nicht mehr verwenden. Dieser eher unglueckliche Ausdruck fuer die log-Datei sollte in etwas (ungetestet) so aussehen:

Code: Alles auswählen

Path(__file__).parent.parent.parent / "log"
Deutlich leichter zu begreifen, und kuerzer obendrein.
rbaert
User
Beiträge: 20
Registriert: Mittwoch 5. September 2018, 15:37

Hallo __deets___

Danke für deine Antwort.

Den Nachtrag nehme ich zur so zur Kenntnis und werde ich anpassen, danke.

Ich muss leider zugeben das ich anscheinend noch mehr Anfänger bin, als ich gedacht habe (sorry für die blöden Fragen).

Meinst du mit "modelieren als dict" innerhalb der Klasse "ConigDetails"? Damit ich die Validatoren von pydantic nutzen kann?

Oder effektiv als dict? Aber dann müsste ich die Validatoren selber schreiben?
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich kenne pydantic nicht gut, aber du baust dir hier selbst eine Funktionalitaet, die ein dict (setzen eines Wertes basierend auf einem Key) erlaubt. Und das sollte meines Erachtens nicht ueber so ein gewurschtel auf einem ansonsten durchdefinierten Objekt/Klasse sein, sondern explizit. Im Konzept so:

Code: Alles auswählen

class MeinModel(Pydantic-Kram):
    extra_info = Field(dict(string, any))
Dann kann pydantic das vielleich schon. Weiss ich nicht, aber in die Richtung wuerde ich mal suchen.
Sirius3
User
Beiträge: 18216
Registriert: Sonntag 21. Oktober 2012, 17:20

@__deets__: das schreibt man als

Code: Alles auswählen

Path(__file__).parents[3] / "log"
@rbaert: woher kommen denn die "Variablen"? Bei einer Configuration gibt es doch eigentlich einen festen Satz an möglichen Optionen.
rbaert
User
Beiträge: 20
Registriert: Mittwoch 5. September 2018, 15:37

Hallo Sirius

Ok, erstmal meine Ausgangslage, damit du verstehst, woran du bei mir bist:
Ich bin Wiedereinsteiger in Python. Bin aber mit anderen Programmiersprachen vertraut. In Python bin ich aber ein ziemlicher Anfänger.
Das Projekt um das es hier geht, ist mein Übungsprojekt. Ich versuche hier, alles möglichst "richtig" und elegant zu lösen, für den Lerneffekt.

Soweit dazu, nun zur eigentlichen Frage zurück:

Das Config-Objekt ist eigentlich eine reine Datenklasse, damit ich diese in mehreren Klassen verwenden kann. Diese wird einmalig beim Programmstart initialisiert und gefüllt, ab da sind diese statisch.
Die Daten können aus verschiedenen Quellen stammen:

Priorität 1: Die Default Werte aus der Klasse
Priorität 2: Die Werte aus einer .env Datei
Priorität 3: Aus einem Dict
Priorität 4: Aktuell nichts, aber es soll erweiterbar sein.

Ich möchte eigentlich erreichen, dass ich die Attribute definieren kann, ohne dass ich diese noch über die "Punkt-Schreibweise" ansteuern muss. Also im Idealfall, dass ich ein Dict übergeben kann, in dem alle keys mit den Attributen verglichen werden. Wenn ein solcher vorkommt, soll der Wert aus dem Dict übernommen werden. Wenn nicht, soll der Default-Wert genommen (behalten) werden.

Stand jetzt:
Wenn ich eine Instance aus der oben genannten Klasse erstelle, werden die Default-Werte gesetzt. Es ist also sichergestellt, dass alle benötigten Attribute vorhanden sind.
Nun möchte ich diese überschreiben, falls nötig.
Da ich nicht jeden Eintrag aus dem Dict mit der Punktschreibweise definieren will, müsste ich dies irgendwie Loop`en können. Also sowas wie:

Code: Alles auswählen

    def setkeys(self, *args, **kwargs):
        for key, value in kwargs.items():
            try:
                self.__dict__[key.lower()] = value
            except AttributeError:
                print(f"Attribute '{key}' not found.")
Leider funktioniert dabei der Validator nicht.
Ich bin aber inzwischen soweit wie __deets__. Ich muss dies wohl wirklich als Dict implementieren, oder?

Gruss und danke schon mal für deine (eure) Mühe
Benutzeravatar
__blackjack__
User
Beiträge: 13919
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@rbaert: Wie sieht das aus wenn Du da weniger Magie betreibst und den normalen Weg beschreitest um dynamisch Attributwerte zu setzen: `setattr()`‽

Warum hat die Methode das `args`-Argument wenn das nicht verwendet wird und warum die ``**kwargs``-Magie? Wenn Du da ein Wörterbuch übergeben willst, braucht man auch dafür ja wieder **-Magie für den Aufruf. Man könnte das also auch auf beiden Seiten einfach weg lassen.
“Java is a DSL to transform big Xml documents into long exception stack traces.”
— Scott Bellware
rbaert
User
Beiträge: 20
Registriert: Mittwoch 5. September 2018, 15:37

Hallo Blackjack

Wahrscheinlich ist genau das mein Problem... ich will zu viel. :-)

setattr() habe ich versucht. Dies übergeht leider auch den Validator.

Aber ich habe nun eine Lösung gefunden, die funktioniert und dazu noch einfach ist (ist es ja meistens, wenn man weiss wie...)

Ich übergebe einfach das Objekt beim initialisieren! So einfach wäre es gewesen:

Code: Alles auswählen


class ConfigDefaults(BaseModel,
                     validate_assignment=True,
                     revalidate_instances='always',
                     extra='forbid'):
    FILE_BASE_PATH: Path = Path(__file__).parents[2]
    FILE_FILENAME: str = Field(pattern=r'^[^<>:"\/\\|?*]+$', validate_default=True, default='log')
    FILE_EXTENSION: str = Field(pattern=r'^[^.<>:"\/\\|?*]+$', validate_default=True, default='log')
    FILE_ADD_HOSTNAME_BY_PATH: bool = Field(validate_default=True, default=True)
    FILE_ADD_USERNAME_BY_PATH: bool = Field(validate_default=True, default=False)
    FILE_ADD_TIMESTAMP_BY_FILENAME: bool = Field(validate_default=True, default=True)

    @validator('FILE_BASE_PATH')
    def validate_path(cls, v: Path) -> Path:
        return Path(v)

# ....            
# Dict holen / zusammenstellen
# ....            

ConfigDefaults(**myDict)

So funktioniert alles was ich will. Werte die im Dict nicht vorkommen werden auf den Default wert gesetzt.
Ich muss halt einfach das Dict beim initialisieren bereits haben, also nachträglich ändern funktioniert dann nicht (Brauche ich für dieses Szenario aber auch nicht).

Oh man, sorry für die Beübung Leute. Aber danke trotzdem.

(Muss ich hier was auf "gelöst" setzten?)
Antworten