pytest für setup.py

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
Benutzeravatar
JakobDev
User
Beiträge: 63
Registriert: Mittwoch 17. Juli 2019, 17:20

Ich habe vor einiger Zeit ein klieines Modul erstellt. Für dieses habe ich jetzt folgenden Pytest geschrieben:

Code: Alles auswählen

from jdTranslationHelper import jdTranslationHelper
import locale
import os

def makeFiles(directory,first,second):
    localFile = open(os.path.join(directory,first),"w")
    localFile.write("testlocal=This is a Test\n")
    localFile.write("hello=World")
    localFile.close()
    defaultFile = open(os.path.join(directory,second),"a")
    defaultFile.write("hello=123\n")
    defaultFile.write("modul=jdTranslationHelper=Test123")
    defaultFile.close()

def doTest(translations):
    assert translations.translate("testlocal") == "This is a Test"
    assert translations.translate("hello") == "World"
    assert translations.translate("modul") == "jdTranslationHelper=Test123"
    assert translations.translate("noTranslation") == "noTranslation"
    assert translations.translate("noTranslation",default="Hello testpy") == "Hello testpy"

def test_normalUsage(tmpdir):
    makeFiles(str(tmpdir),locale.getlocale()[0] + ".lang","en_GB.lang")
    translations = jdTranslationHelper()
    translations.loadDirectory(str(tmpdir))
    doTest(translations)

def test_arguments(tmpdir):
    makeFiles(str(tmpdir),"selected.lang","default.lang")
    translations = jdTranslationHelper(lang="selected",defaultLanguage="default")
    translations.loadDirectory(str(tmpdir))

def test_noDefaultLang(tmpdir):
    makeFiles(str(tmpdir),"selected.lang","default.lang")
    translations = jdTranslationHelper(lang="noSelected",defaultLanguage="default")
    translations.loadDirectory(str(tmpdir))
    assert translations.translate("modul") == "jdTranslationHelper=Test123"
    assert translations.translate("hello") == "123"
    assert translations.translate("testlocal") == "testlocal"
Wenn ich das ganze mit pytest starte funktioniert alles. Der Test läuft fehlerfrei durch? Wie füge ich diesen Test jetzt aber der setup.py hinzu?Dazu habe ich im Internet nicht wirklich etwas gefunden.
__deets__
User
Beiträge: 14528
Registriert: Mittwoch 14. Oktober 2015, 14:29

Du solltest die Konventionen von PEP8 beachten was die Benamung von Dingen angeht. Das geht bei dir ziemlich durcheinander. Und in Tests sollte man besser nicht assert verwenden, sondern zB von unittest.TestCase ableiten und self.assertEqual verwenden, weil das deutlich bessere Fehlermeldungen produziert. Ggf. hat pytest da auch standalone Funktionen fuer. Und das all die Test-Funktionen ein Argument bekommen, anstatt sich ihr tempdir zB im setup anzulegen und dann in teardown zu loeschen sieht auch etwas seltsam aus. Wie bekommt der das denn? Und warum muss das die ganze Zeit mit str(...) behandelt werden? Dateien oeffnet und schliesst man implizit mit dem with-statement.

Und zur eigentlichen Frage: warum soll das in setup.py rein? setup.py ist schon so ein ziemliches Aergernis, denn statt nur Metadaten zu sein, ist es eben auch Code - und das macht das automatische und einfache Prozessieren schwierig bis unmoeglich. Darin automatisch Tests ablaufen zu lassen ist 100%ig NICHT die richtige Idee. Wenn du sowas willst, integrier zB Travis in deine github-hooks oder aehnliches. Dann wird fuer jeden commit oder auch nur fuer ausgewaehlte die Test-Suite abgefahren.
Benutzeravatar
JakobDev
User
Beiträge: 63
Registriert: Mittwoch 17. Juli 2019, 17:20

Ich verwende assert, weil es auf deren offizilen Webseite so steht. Auch tmpdir ist auf der Webseite so dokumentiert. Da tmpdir kein String, muss es erst mit str() in eine umgewandelt werden.

Viele fügen der stup.py tests hinzu, sodass sie setup.py test ausführen könne, wie z.B. hier auch wenn ich das nicht so ganz verstehe, wie ich das jetzt mit pytest machen soll, weshalb ich ja Frage.
__deets__
User
Beiträge: 14528
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich kenne pytest nicht so besonders gut. Das es das so temp-dir so automagisch macht - na gut. Allerdings ist es ein pathlib.Path-Objekt, und du solltest das vernuenftig benutzen, statt zu konvertieren. Also zb.

Code: Alles auswählen

def make_files(directory, first, second):
    with (directory / first).open("w") as outf:
        outf.write("testlocal=This is a Test\n")

Und ich habe gerade gesehen, dass die augenscheinlich irgendwie mit dem assert (oder dessen Vergleichsfunktion) spielen, die Ausgabe ist ja tatsaechlich besser. Wusste ich nicht, dass das geht. Die Bemerkung war also quatsch.

Das Tutorial auf das du dich da beziehst finde ich nicht gut. Es sagt ja von sich selbst, es sei "opinionated". Das faengt schon damit an, dass die Tests Teil des Packages sind. Das ist bloed. Denn dadurch muss man entweder sich einen abbrechen & die irgendwie aus dem distributierten Paket raushalten. Oder sie sind unnoetigerweise mitgeliefert. Und augenscheinlich enthaelt setuptools irgendwie support fuer den inzwischen veralteten nose testrunner.

Schau dir an was pytest selbst dazu sagt: http://doc.pytest.org/en/latest/goodpractices.html

Die integrieren das auch nicht mit setup.py, und ich wuerde das auch nicht machen. Das Ding ist schon magisch genug. Und im Grunde bringt das auch kaum was. Wenn man viel mit Tests arbeitet, will man alle moeglichen Dinge wie zB einzelne Tests laufen lassen, log-output sehen oder genau nicht, etc. All diese Dinge werden durch pytest zugreifbar, aber nicht durch ein einziges, generisches "python setup.py test".
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@JakobDev: Ist das Absicht das beim schreiben der beiden Dateien einmal "w" und einmal "a" als Dateimodus verwendet wird?

Warum ist `doTest()` eine eigene Funktion? Das wird ja nur in *einer* anderen Funktion aufgerufen.

`test_normalUsage()` ist problematisch bis fehlerhaft, weil der Test davon abhängt was denn tatsächlich die Locale auf dem Rechner ist, auf dem das ausgeführt wird. Tests sollten eher nicht von solchen externen Werten abhägig sein auf die man keinen Einfluss hat.

Du schreibst Dateien deren letzte Zeile nicht mit einem Zeilenendezeichen abgeschlossen sind. Das würde sogar beim gezeigten Code schon zu einem Problem führen wenn `first` und `second` den gleichen Wert haben, wegen dem "a"-Dateimodus.

Ich würde versuchen das so zu schreiben das man selbst Fixtures wie `tmpdir` hat und nicht immer `makeFiles()` aufrufen muss.

Wobei sich die Frage stellt warum überhaupt Dein Modul statt `gettext` aus der Standardbibliothek. 🙂

@__deets__: Tests muss man nicht aus der Distribution heraus halten. Ich habe die da sehr gerne drin, denn wenn da wo's installiert wurde etwas nicht funktioniert, ist das erste was man selbst machen oder den der benutzt bitten kann: die Testsuite mal laufen lassen.

Ich würde nicht einmal `open()` bemühen:

Code: Alles auswählen

def make_files(directory, first, second):
    (directory / first).write_text(
        "testlocal=This is a Test\nhello=World\n", encoding="utf-8"
    )
    (directory / second).write_text(
        "hello=123\nmodul=jdTranslationHelper=Test123\n", encoding="utf-8"
    )
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

@JakobDev: in Deinem verlinkten gitlab-Code: Module sollten nach Konvention klein_mit_unterstrich benannt sein, genauso wie Methoden, Variablennamen und Funktionen. Wenn ein Paket nur eine __init__.py hat, ist das eigentlich kein Paket und sollte ein einfaches Modul sein. Nach Komma kommt immer ein Leerzeichen. Keine nakten except benutzen und Strings nicht per + zusammenstückeln, sondern Stringformatierung benutzen. Es gibt 4 verschiedene Typen von Anführungszeichen für literale Strings, so dass man eigentlich nie ein Anführungszeichen im String escapen muß. In `loadDirectory` wird 5-mal der selbe Pfad zusammengebaut!

Code: Alles auswählen

import locale
import os

class jdTranslationHelper():
    def __init__(self, lang=None, default_language="en_GB"):
        self.selected_language = lang if lang is None else locale.getlocale()[0]
        self.default_language = default_language
        self.strings = {}

    def read_language_file(self, language_filename):
        with open(language_filename, encoding="utf-8") as lines:
            for line in lines:
                key, op, value = line.rstrip().partition('=')
                if not op:
                    print('Error loading line "{}" in file {}'.format(line, language_filename))
                else:
                    self.strings[key] = value

    def load_directory(self, path):
        language_filename = os.path.join(path, self.default_language + ".lang")
        if not os.path.isfile(language_filename):
            print("{} was not found".format(language_filename))
            return
        self.read_language_file(language_filename)
        language_filename = os.path.join(path, self.selected_language + ".lang")
        if os.path.isfile(language_filename):
            self.read_language_file(language_filename)

    def translate(self, key, default=None):
        if default is None:
            default = key
        return self.strings.get(key, default)
Benutzeravatar
JakobDev
User
Beiträge: 63
Registriert: Mittwoch 17. Juli 2019, 17:20

Erstmal danke für die Verbesserungsvorschläge! Die Benutzung der Variablennamen und ein paar andere Dinge sind einfach alte Gewohnheit von mir aus der Zeit vor Python. Aber ich werde versuchen, es mir abzugewöhnen.

Zu Frage nach gettext: Das liegt einfach daran, dass ich mit dem Dateiaufbau besser umgehen kann und sich das ganze besser in meinen Workflow einfügt. Bei gettext musste ich mich zudem erstmal in die Dokumentation reinarbeiten und welche Datei jetzt wohin gehört. Bis dahin hatte das kleine Modul schon längst geschrieben. Der Dateiaufbau ist zudem einfacher als bei gettext.

Aber jetzt zurück zu meiner Ausgansfrage: Wie baue ich die Tests in meine setup.py ein, so wie es z.B. chardet. Die benutzen auch pytest, aber ich verstehe den Aufbau nicht so ganz.
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

@JakobDev: chardet benutzt halt https://pypi.org/project/pytest-runner/, was verstehst Du daran nicht?

Besonders der Abschnitt in der Dokumentation ist interessant:
Remove ‘pytest-runner’ from your ‘setup_requires’, preferably removing the setup_requires option.
Remove ‘pytest’ and any other testing requirements from ‘tests_require’, preferably removing the setup_requires option.
Select a tool to bootstrap and then run tests such as tox
Der soviel aussagt, wie "Tests gehören nicht in setup.py".
Benutzeravatar
JakobDev
User
Beiträge: 63
Registriert: Mittwoch 17. Juli 2019, 17:20

Warum hat dann so gut wie jedes größere Projekt die tests in die Setup.py eingebaut, obwohl sie dort nicht hingehören?
__deets__
User
Beiträge: 14528
Registriert: Mittwoch 14. Oktober 2015, 14:29

Wir haben dir ne Reihe von Gruenden genannt, das *nicht* zu tun. Welches Problem denkst du denn wird dadurch geloest, das du im Moment nicht geloest bekommst? Und eine Datei wie setup.py ist etwas, das viele Leute einfach abschreiben. Das ist deswegen nicht notwendigerweise *gut*. Frag doch eines dieser groesseren Projekte, ob sie das aktiv nutzen.
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@JakobDev: Hat das jedes grössere Projekt? Was ist denn da Deine Basis für diese Aussage?

Falls es tatsächlich zutrifft, vielleicht aus historischen Gründen als man tatsächlich noch `setup.py` (direkt) verwendet hat zum bauen und installieren. Eben wie man das von ``make`` gewohnt ist, wo es ja dann auch oft ein Ziel für Tests gibt.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
JakobDev
User
Beiträge: 63
Registriert: Mittwoch 17. Juli 2019, 17:20

Ok, ich werde die Integration in die Setup.py sein lassen. Danke für eure Hilfe!
Antworten