Seite 1 von 1

Imports in der App und Pytest machen Probleme

Verfasst: Dienstag 22. Dezember 2020, 20:00
von pyko
Hallo zusammen,

ich bin Python- und Forum-Neuling. Nachdem ich meine erste Anwendung in einen funktional halbwegs brauchbaren Zustand gebracht habe, wird es Zeit, für ein bisschen Refactoring, um das Chaos zu beseitigen und Tests zu ergänzen.

Die Projekstruktur soll aussehen wie folgt und müsste damit auch den Empfehlungen folgen:

Code: Alles auswählen

App
  +- src
       + app.py
       + mod1
           __init__.py
           mod1_1.py
       + mod2
           __init__.py
           mod2_1.py
  +- tests
       + mod1
           test_mod1.py
Die Applikation läßt sich einfach ausführen mit "python src/app.py" oder auch aus dem src mit "python app.py".

In mod1_1.py steht nun allerdings ein "import mod2.mod2_1". Das funktioniert in der Applikation, aber durch pytest startet der Namespace immer bei src. Scheinbar unabhängig davon, ob ich die Tests mit "pytest" oder "python -m pytest" starte.

Wie muß ich pytest starten, damit der Applikationscode nach wie vor seine Abhängigkeiten auflösen kann? Ich lese seit zwei Tagen alle möglichen Tutorials und den stackoverflow, aber werde nicht schlauer bzw. sehe nicht, was eigentlich das Problem ist.

In den Tests muss ich übrigens die import-statements mit src prefixen, also z.b. "import src.mod1.mod1_1" verwenden. Das schaut mir auch falsch aus, oder?

Gruß
Heiko

Re: Imports in der App und Pytest machen Probleme

Verfasst: Mittwoch 23. Dezember 2020, 03:47
von Thants
Deine Pakete mod1 und mod2 sind nicht als generelle Python-Pakete installiert, die von jedem Python-Programm aus verfügbar wären, deshalb werden sie von den Tests nicht gefunden.

Wenn du ein Modul bzw. Paket per "import" importierst, muss Python nach der entsprechenden Datei suchen. Im Modul "sys" gibt es eine Variable "path", die eine Liste von Verzeichnissen enthält. In diesen Verzeichnissen sucht Python nach dem Paket/Modul, das du importieren willst.

Wenn du ein Python-Programm wie dein app.py startest, wird das Verzeichnis, in dem die Python-Datei liegt (also App/src) ganz vorne in der Liste eingefügt, deshalb wird mod1 und mod2 von deinem Programm gefunden, da diese Pakete ja direkt in App/src liegen.

Wenn du allerdings test_mod1.py startest, wird anstelle des Pfads App/src der Pfad App/tests/mod1 in sys.path eingetragen. App/src ist dann in der Liste nicht enthalten und deshalb wird mod1 und mod2 nicht gefunden.

Es gibt jetzt mehrere Möglichkeiten, das Problem zu lösen:
  1. sys.path kann in einem Programm verändert werden. Du könntest also in deinen Tests den Pfad zu App/src in sys.path eintragen, bevor du mod1 importierst (es würde sich dann ein relativer Pfad von App/tests/mod1 aus anbieten).
  2. Du kannst den Pfad zu App/src in der Umgebungsvariablen PYTHONPATH eintragen. PYTHONPATH kann eine List von Pfaden enthalten, die von Python zu sys.path hinzugefügt werden. Deine Tests können dann mod1/mod2 ganz normal importieren.
  3. Du könntest deine Tests direkt als Teil der mod1/mod2-Pakete ansehen und sie auch dort ablegen. Da sie dann Bestandteil der Pakete sind, können sie natürlich auch auf die anderen Module im Paket zugreifen.

Re: Imports in der App und Pytest machen Probleme

Verfasst: Mittwoch 23. Dezember 2020, 13:52
von pyko
Mit Variante 1 scheint mein Problem erstmal gelöst zu sein. Ich habe versucht, den sys.path zu setzen in einer conftest.py. Nach einigen Versuchen habe ich herausgefunden, daß die Datei neben dem Test liegen muß. Sieht jetzt so aus:

Code: Alles auswählen

App
  +- src
  +- tests
       + mod1
           conftest.py
           test_mod1.py
Und in der conftest.py steht:

Code: Alles auswählen

from  pathlib import Path
import sys

sys.path.insert(0, str(Path("src").resolve()))
Der Test muß die Package zwar mit "import src.mod1.mod1_1" importieren, aber damit kann ich erstmal leben.

Danke!

Re: Imports in der App und Pytest machen Probleme

Verfasst: Mittwoch 23. Dezember 2020, 13:59
von Sirius3
Warum hast Du überhaupt ein Verzeichnis src?

Re: Imports in der App und Pytest machen Probleme

Verfasst: Mittwoch 23. Dezember 2020, 14:31
von __blackjack__
@Sirius3: Warum nicht? Ist zum Beispiel sicherer bei Tests das noch einmal in einem Unterverzeichnis zu haben, damit man bei einem installierten Package wirklich das installierte Package testet und nicht den nicht-installierten Code der zufällig im Suchpfad liegt.

Was halt nicht sein sollte ist das man `src` dann tatsächlich als Modul/Package bei den Importen im Code stehen hat.

Re: Imports in der App und Pytest machen Probleme

Verfasst: Mittwoch 23. Dezember 2020, 19:40
von pyko
Sirius3 hat geschrieben: Mittwoch 23. Dezember 2020, 13:59 Warum hast Du überhaupt ein Verzeichnis src?
Ich bin das so gewohnt von C++/CMake Projekten. Python-Tutorials, z.B. https://blog.ionelmc.ro/2014/05/25/pyth ... -structure beschreiben diese Variante auch als gebräuchlich, also wollte ich das auch mit Python versuchen. Ich finde, es sieht auch aufgeräumter aus. :geek:

Irgendwie geht das Thema aber noch weiter. Die Module in meinem Beispiel heißen sowohl im src als auch im test "mod1". Das scheint die nächste schlechte Idee zu sein: Dadurch ich da ein __init__.py hatte, macht er aus dem tests/mod1 eine Package - und zwar ohne "tests" namespace. Das heißt, ein "import mod1" in den Sourcen macht Probleme, weil er die mod1 in den Tests findet und dort natürlich nichts brauchbares drin ist.

Ich schau mal, wie weit ich ohne das __init__ komme. So wie ich das verstanden habe, müssen dann nur die Testfunktionen alle unterschiedliche Namen haben.

Re: Imports in der App und Pytest machen Probleme

Verfasst: Mittwoch 23. Dezember 2020, 21:41
von Thants
@pyko: Wie lässt du eigentliche die Tests laufen (mit welchem Kommando und von welchem Verzeichnis aus)?

Ein "import src" funktioniert ja nur dann, wenn das App-Verzeichnis im Suchpfad ist. Das ist nur dann der Fall, wenn du ein Programm startest, das direkt in App liegt. Die Datei conftest.py wäre dann aber ja hinfällig, da du dort ja das src-Verzeichnis zu den Suchpfaden hinzufügst, aber gar kein import benutzt, der das Verzeichnis bräuchte.

Ausserdem müsste dann src ein Paket sein, d.h. dort müsste eine Datei __init__.py liegen (wobei ich gerade nach einem kurzen Test feststelle, dass Python 3 tatsächlich auch ein "nacktes" Verzeichnis als Paket akzeptiert. Seit wann ist das denn so? In der Doku steht immer noch, dass __init__.py zwingend notwendig ist. Python 2 liefert auch, wie erwartet, ein ImportError).

Übrigens, in conftest.py erstellst du einen Pfad, der nur dann funktioniert, wenn App das aktuelle Verzeichnis ist (andernfalls erzeugt resolve() einen falschen Pfad). Wenn du deine Tests von App aus startest, sollten deine imports auch ohne "src." am Anfang funktionieren. (du könntest übrigens in conftest.py die eingebaute Variable __file__ benutzen. Die enthält den Pfad von dem aktuellen Modul, was du als Basis für den src-Pfad verwenden könntest)

conftest.py muss nicht unbedingt auf der gleichen Ebene wie test_mod1.py liegen. Du könntest es auch eine Ebene höher ablegen und dann eben mit sowas wie "from .. import conftest" importieren. Das setzt allerdings voraus, dass der gesamte "test"-Baum ein Paket ist (das also auch wieder gefunden werden muss).

Wenn das test-Verzeichnis ein eigenes Paket ist, wäre das Verzeichnis test/mod1 auch separat von src/mod1. In Pythons Import-Namespace wäre das eine dann test.mod1 und das andere einfach nur mod1. (Wobei test/mod1 ja eigentlich sowieso gar kein Paket sein sollte, wenn da keine __init__.py liegt, aber da scheint es ja in Python 3 Änderungen gegeben zu haben, die mir nicht geläufig sind (wie oben schon erwähnt)).

Re: Imports in der App und Pytest machen Probleme

Verfasst: Donnerstag 24. Dezember 2020, 22:45
von pyko
Thants hat geschrieben: Mittwoch 23. Dezember 2020, 21:41 @pyko: Wie lässt du eigentliche die Tests laufen (mit welchem Kommando und von welchem Verzeichnis aus)?
Ich starte alles direkt vom Hauptverzeichnis, also Repository-Root. Sowohl pytest, python -m pythest, coverage ... usw. funktionieren.

Der pytest findet das conftest.py und nimmt das als "root" für die Tests. In der conftest.py wird dann der syspath wie vorgeschlagen geändert. Als Basis für das resolve() scheint aber immernoch das Repo-Root zu dienen, denn ich füge da nur "src" hinzu. Mit diesem Setup brauche ich dann weder in den Tests noch in den Sourcen den "src" prefix bei den Imports. Würde ich die Tests von /tests/ aus starten, muß ich in die conftest.py ein "../src" rein.
Thants hat geschrieben: Mittwoch 23. Dezember 2020, 21:41 Ausserdem müsste dann src ein Paket sein, d.h. dort müsste eine Datei __init__.py liegen
Das soll man vermeiden. Hier widersprechen sich diverse Dokus (die sagen, ja) und die Foren (sagen nein). Die App bzw src hat bei mir aktuell kein __init__.py mehr, und das funktioniert. Im src liegt nur das app.py.
Thants hat geschrieben: Mittwoch 23. Dezember 2020, 21:41 (wobei ich gerade nach einem kurzen Test feststelle, dass Python 3 tatsächlich auch ein "nacktes" Verzeichnis als Paket akzeptiert. Seit wann ist das denn so?
Scheinbar Python 3.3: https://www.python.org/dev/peps/pep-0420/

Dadurch ich ganz neu eingestiegen bin, bin ich gleich mit 3.9 gestartet. Hätte ich wohl auch hier gleich schreiben sollen. Man muß ziemlich aufpassen, auf welche Version sich die Dokus und Foreneinträge beziehen. Hat sich wohl viel geändert.
Thants hat geschrieben: Mittwoch 23. Dezember 2020, 21:41 Wenn das test-Verzeichnis ein eigenes Paket ist, wäre das Verzeichnis test/mod1 auch separat von src/mod1. In Pythons Import-Namespace wäre das eine dann test.mod1 und das andere einfach nur mod1. (Wobei test/mod1 ja eigentlich sowieso gar kein Paket sein sollte, wenn da keine __init__.py liegt, aber da scheint es ja in Python 3 Änderungen gegeben zu haben, die mir nicht geläufig sind (wie oben schon erwähnt)).
Hätte ich auch erwartet, aber wie oben beschrieben, starten mit meinem Setup beide Bäume ohne das "src" und das "tests". In meinem richtigen Projekt heißen die Module dann einfach anders, immer test_...py und das Problem ist gelöst.

Im großen und ganzen finde ich das Setup jetzt ganz gut. Eins meiner größten Probleme im Sourcecode sind rekursive imports, die finde ich hiermit ganz gut.

Schöne Feiertage!