Requirements vergleichen

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
Icke
User
Beiträge: 25
Registriert: Mittwoch 23. November 2022, 12:55

Hallo zusammen,

ich muss Listen mit requirements auf Kompatibilität prüfen, mir raucht der Kopf. Wer kann löschen? :)

Leider schlagen noch einige Tests fehl und ich bekomme es nicht hin. Ich sitze schon etliche Stunden dran und sehe schon bunte Farbbälle vor meinem geistigem Auge ( nein, ich nehme keine Drogen).

Hat da jemand Tipps für mich und sieht den/die Fehler? Mir raucht der Kopf

Der TestCase

Code: Alles auswählen

class TestCompareVersions(TestCase):
    def test_exact_match(self):
        self.assertTrue(compare_versions("==1.2.3", "==1.2.3"))
        self.assertFalse(compare_versions("==1.2.3", "==1.2.4"))

    def test_minimum_version(self):
        self.assertTrue(compare_versions("==2.1.0", ">=2.0.0"))
        self.assertFalse(compare_versions("==1.2.3", ">=2.0.0"))

    def test_maximum_version(self):
        self.assertTrue(compare_versions("==1.2.3", "<=2.0.0"))
        self.assertFalse(compare_versions("==2.1.0", "<=2.0.0"))

    def test_range_versions(self):
        self.assertTrue(compare_versions("==1.2.3", ">=1.2,<2.0"))
        self.assertFalse(compare_versions("==2.0.0", ">=1.2,<2.0"))

    def test_tilde_operator(self):
        self.assertTrue(compare_versions("==1.2.3", "~=1.2.0"))
        self.assertTrue(compare_versions("==1.2.4", "~=1.2.0"))
        self.assertFalse(compare_versions("==1.3.0", "~=1.2.0"))
        self.assertFalse(compare_versions("==2.0.0", "~=1.2.0"))

    def test_caret_operator(self):
        self.assertTrue(compare_versions("==1.2.3", "^1.2.0"))
        self.assertTrue(compare_versions("==1.2.4", "^1.2.0"))
        self.assertFalse(compare_versions("==2.0.0", "^1.2.0"))

    def test_invalid_syntax(self):
        # Hier werden Versionen mit ungültiger Syntax getestet
        self.assertFalse(compare_versions("==1.2.3", "abc"))
        self.assertFalse(compare_versions("==1.2.3", ">1.2,<=2.0"))
        self.assertFalse(compare_versions("==1.2.3", ">=1.2,<2.0,<=3.0"))

    def test_all_operators(self):
        # Teste alle Operatoren im ersten Parameter
        self.assertTrue(compare_versions("<=2.1.0", ">=2.0.0"))
        self.assertTrue(compare_versions("<=2.1.0", ">2.0.0"))
        self.assertTrue(compare_versions("<=2.1.0", "<3.0.0"))
        self.assertTrue(compare_versions("!=1.0.0", "<3.0.0"))
        self.assertFalse(compare_versions("!=1.0.0", "==1.0.0"))
        self.assertTrue(compare_versions("!=1.0.0", "!=1.0.1"))
        self.assertFalse(compare_versions("!=1.0.0", "!=1.0.0"))
        self.assertTrue(compare_versions("==1.2.3", "==1.2.3"))
        self.assertFalse(compare_versions("==1.2.3", "==1.2.4"))

    def test_combined_ranges(self):
        # Teste kombinierte Bereichsangaben
        self.assertTrue(compare_versions(">=1.2,<3.0", "~=2.5"))
        self.assertFalse(compare_versions(">=1.2,<3.0", "~=3.0"))
        self.assertTrue(compare_versions(">=1.2,<3.0", "^2.0"))
        self.assertFalse(compare_versions(">=1.2,<3.0", "^3.0"))

    def test_invalid_operator_combination(self):
        # Teste ungültige Operator-Kombinationen
        self.assertFalse(compare_versions("<=2.1.0", ">2.0.0"))
        self.assertFalse(compare_versions("~=1.2.3", "<3.0.0"))
        self.assertFalse(compare_versions("^2.1.0", "==2.1.0"))

Die Funktion

Code: Alles auswählen

def compare_versions(version, constraint):
    operators: List[str] = ["==", ">=", "<=", ">=", "~=", "=^", "<", ">"]

    def version_tuple(ver):
        try:
            return tuple(map(int, ver.split(".")))
        except ValueError:
            return ()

    # Verwenden von regulären Ausdrücken, um den Constraint-String zu analysieren
    match = re.match(r"^([<>=~^!]*)([\d+\.]+|\d+)$", constraint)
    if match:
        contraint_op, required_ver = match.groups()
    else:
        return False

    match = re.match(r"^([<>=~^!]*)([\d+\.]+|\d+)$", version)
    if match:
        version_op, version_ver = match.groups()
    else:
        return False

    # prüfe den Operator
    if contraint_op not in operators:
        return False
    if version_op not in operators:
        return False

    # Prüfen, ob die Version dem Constraint entspricht
    if contraint_op == "~":
        # Tilde-Operator
        tolerance = 0 if not required_ver.endswith(".0") else 1
        return all(
            abs(version_tuple(version_ver)[i] - int(ver)) <= tolerance
            for i, ver in enumerate(required_ver.split("."))
        )
    elif contraint_op == "^":
        # Caret-Operator
        tolerance = 0 if not required_ver.endswith(".0") else 1
        return all(
            version_tuple(version_ver)[i] <= int(ver) + tolerance
            for i, ver in enumerate(required_ver.split("."))
        )
    elif contraint_op == ">=":
        # Mindestversion
        return version_tuple(version_ver) >= version_tuple(required_ver)
    elif contraint_op == "<=":
        # Maximale Version
        return version_tuple(version_ver) <= version_tuple(required_ver)
    elif contraint_op == ">":
        return version_tuple(version_ver) > version_tuple(required_ver)
    elif contraint_op == "<":
        return version_tuple(version_ver) < version_tuple(required_ver)
    elif contraint_op == "":
        return version_ver == required_ver
    elif contraint_op == "!":
        return version_tuple(version_ver) != version_tuple(required_ver)
    elif contraint_op == "==":
        # Exakte Versionsnummer
        return version_tuple(version_ver) == version_tuple(required_ver)

    # Unterstützung für kombinierte Bereichsangaben
    if "," in required_ver:
        range_parts = required_ver.split(",")
        return all(
            compare_versions(version_ver, f"{contraint_op}{part}") for part in range_parts
        )

    return False
Error Log

Code: Alles auswählen

test_all_operators
    self.assertTrue(compare_versions("<=2.1.0", ">=2.0.0"))
AssertionError: False is not true

test_caret_operator
    self.assertTrue(compare_versions("1.2.3", "^1.2.0"))
AssertionError: False is not true


test_combined_ranges
    self.assertTrue(compare_versions(">=1.2,<3.0", "~=2.5"))
AssertionError: False is not true



test_invalid_operator_combination
    self.assertFalse(compare_versions("~=1.2.3", "<3.0.0"))
AssertionError: True is not false


test_range_versions
    self.assertTrue(compare_versions("1.2.3", ">=1.2,<2.0"))
AssertionError: False is not true


test_tilde_operator
    self.assertTrue(compare_versions("1.2.3", "~=1.2.0"))
AssertionError: False is not true

Greetings -=Icke=-

I do this just for fun, not professionell
only Python and absolute beginner
Sirius3
User
Beiträge: 18274
Registriert: Sonntag 21. Oktober 2012, 17:20

Das `operators` eine Liste von Strings ist, sieht man schon daran, dass der Variable eine Liste von Strings zugewiesen wird, die Typannotation ist absolut überflüssig und kann weg, zumal das ja die einzige im ganzen Code ist.
Warum ist version_tuple innerhalb der anderen Funktion definiert? Wie soll man die unabhängig testen können? Die gehört aus der Funktion raus.
Man benutzt keine kryptischen Abkürzungen, `ver` sollte überall `version` heißen.
Der Code zu version und constreint ist identisch, gehört also in eine eigene Funktion, um Codedopplungen zu vermeiden.
Du hast schon eine Liste an gültigen Operatoren, definierst Dir das aber im regulären Ausdruck erneut (auf andere Weise). Das ist eine potentielle Fehlerquelle. Nutze im Regulären Ausdruck `operators`.
Der reguläre Ausdruck passt nicht zu den Versionsangaben in den Testfällen. Wäre das eine eigene Funktion, könnte man den regulären Ausdruck getrennt vom Vergleich testen!
Du rufst zigmal version_tuple auf, das sollte man nur einmal tun.
Statt der langen if-Kaskade würde man eine passende Datenstruktur wählen, z.B. mit Hilfe des operators-Moduls.
Man benutzt keine direkten Indizes, in Deinem Fall gibt es zip.
Das Prüfen auf Kommas findet viel zu spät statt. Der Comperator in `version` wird gar nicht benutzt, ist das so korrekt?

Der Code könnte also ungefähr so aussehen (inklusive einiger der oben aufgeführten Fehler, die darst Du selbst noch beheben):

Code: Alles auswählen

import re
import operator

def compare_with_tolerance(used_version, required_version):
    tolerance = 0 if required_version[-1] == 0 else 1
    return all(
        abs(used_version_part - required_version_part) <= tolerance
        for used_version_part, required_version_part in zip(used_version, required_version)
    )

def compare_with_tolerance_in_used_version(used_version, required_version):
    tolerance = 0 if required_version[-1] == 0 else 1
    return all(
        used_version_part <= required_version_part + tolerance
        for used_version_part, required_version_part in zip(used_version, required_version)
    )


OPERATORS = {
    "~=": compare_with_tolerance,
    "^": compare_with_tolerance_in_used_version,
    ">=": operator.ge,
    "<=": operator.le,
    ">": operator.gt,
    "<": operator.lt,
    "": operator.eq,
    "==": operator.eq,
    "!=": operator.ne,
}

def version_tuple(version):
    try:
        return tuple(map(int, version.split(".")))
    except ValueError:
        return ()
        
def parse_version(version):
    # Verwenden von regulären Ausdrücken, um den Constraint-String zu analysieren
    operators = '|'.join(map(re.escape, OPERATORS))
    match = re.match(fr"^({operators})([\d+\.]+|\d+)$", version)
    if not match:
        raise ValueError("regex does not match")
    operator, version = match.groups()
    return operator, version_tuple(version)


def compare_versions(version, constraint):
    constraint_operator, required_version = parse_version(constraint)
    version_operator, used_version = parse_version(version)
    return OPERATORS[constraint_operator](used_version, required_version)
Benutzeravatar
__blackjack__
User
Beiträge: 14053
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Icke: `version_op` wird nicht wirklich verwendet, warum nicht?

Beim ersten Test der fehlschlägt ist der Test falsch. Da sollte in der Tat False bei heraus kommen und nicht True. Die Versionen die "<=2.1.0" erfüllen, können ">=2.0.0" erfüllen, müssen das aber nicht. Wenn man also nur `True` oder `False` als Antwortmöglichkeit hat, dann ist das `False`.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Benutzeravatar
grubenfox
User
Beiträge: 612
Registriert: Freitag 2. Dezember 2022, 15:49

von hier mal eine andere Frage zum Thema:

kann man die Testerei eigentlich so konfigurieren (im Zweifel mit einem anderen Modul als 'unittest') dass in der Ausgabe der fehlerhaften Tests auch ein wenig Traceback aufgeführt wird?

Das 'False' nicht 'True' ist...
Icke hat geschrieben: Dienstag 18. Juli 2023, 15:01 Error Log

Code: Alles auswählen

test_all_operators
    self.assertTrue(compare_versions("<=2.1.0", ">=2.0.0"))
AssertionError: False is not true
ist ja ganz nett (und stimmt ja auch), aber wenn man wüsste welche Zeile innerhalb von compare_versions nun das False eigentlich zurückgeliefert hat... im originalen Code vom ersten Posting gibt es ja genügend 'return'-Anweisungen die ein False zurückliefern könnten.
Sirius3
User
Beiträge: 18274
Registriert: Sonntag 21. Oktober 2012, 17:20

@grubenfox: Du sprichst das Problem an, die Funktion ist zu lang und hat zu viele verschiedene Pfade. Damit eine Funktion gut testbar ist, darf sie nur eine Sache machen, also entweder den Versionsausdruck parsen, oder einen Test durchführen.
Benutzeravatar
__blackjack__
User
Beiträge: 14053
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@grubenfox: Wie soll das gehen? Zu dem Zeitpunkt wo das Ergebnis der Funktion verglichen wird gibt es keinen Traceback mehr, weil die Funktion da ja bereits komplett abgearbeitet ist.

Die Funktion ist vielleicht auch einfach nicht gut formuliert mit den Massen an Fällen. Ich würde das eher so angehen, dass ich beide Angaben in Intervalle (start, end) umwandeln würde, und dann schauen würde ob das erste Intervall im zweiten enthalten (oder gleich) ist.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Benutzeravatar
grubenfox
User
Beiträge: 612
Registriert: Freitag 2. Dezember 2022, 15:49

__blackjack__ hat geschrieben: Dienstag 18. Juli 2023, 18:33 @grubenfox: Wie soll das gehen? Zu dem Zeitpunkt wo das Ergebnis der Funktion verglichen wird gibt es keinen Traceback mehr, weil die Funktion da ja bereits komplett abgearbeitet ist.
Theoretisch ganz einfach: vor jedem return irgendwo eine eindeutige Positionsangabe hinterlegen und beim assert die hinterlegte Info auswerten/ausgeben.
Es geht ja nur um eine Info welches return es dann nun war wo der Interpreter vorbei lief... kein richtiges Traceback.

die triviale manuelle Variante: mit einem Editor vor das erste 'return' ein 'print(1);' setzen, vor das zweite ein 'print(2)'....
Weil das durchnummerieren nun quälend langweilig ist, wäre es schön wenn man diese Aufgabe einem Python-Modul überlassen könnte.
Sirius3
User
Beiträge: 18274
Registriert: Sonntag 21. Oktober 2012, 17:20

Für diese Ausgaben benutzt man dann logging.debug.
Benutzeravatar
__blackjack__
User
Beiträge: 14053
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Oder bei so vielen ``return``\s vielleicht einfach das `trace`-Modul, auch wenn es vielleicht ein bisschen viel Ausgabe liefert.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Antworten