Importe von Geschwisterpaketen

199

Ich habe versucht, Fragen zu Geschwisterimporten und sogar zur Paketdokumentation durchzulesen , aber ich habe noch keine Antwort gefunden.

Mit folgender Struktur:

├── LICENSE.md
├── README.md
├── api
   ├── __init__.py
   ├── api.py
   └── api_key.py
├── examples
   ├── __init__.py
   ├── example_one.py
   └── example_two.py
└── tests
   ├── __init__.py
   └── test_one.py

Wie können die Skripte in den Verzeichnissen examplesund testsaus dem apiModul importiert und über die Befehlszeile ausgeführt werden?

Außerdem möchte ich den hässlichen sys.path.insertHack für jede Datei vermeiden . Sicherlich kann das in Python gemacht werden, oder?

Zachwill
quelle
7
Ich empfehle, alle sys.pathHacks zu überspringen und die einzige tatsächliche Lösung zu lesen , die bisher veröffentlicht wurde (nach 7 Jahren!).
Aran-Fey
1
Übrigens gibt es noch Raum für eine weitere gute Lösung: Trennung von ausführbarem Code von Bibliothekscode; die meiste Zeit ein Skript in einem Paket soll nicht sein ausführbar zu beginnen.
Aran-Fey
Dies ist sehr hilfreich, sowohl die Frage als auch die Antworten. Ich bin nur neugierig, warum ist "Akzeptierte Antwort" nicht dasselbe wie die, die in diesem Fall das Kopfgeld erhalten hat?
Indominus
@ Aran-Fey Das ist eine unterschätzte Erinnerung in diesen Fragen und Antworten zu relativen Importfehlern. Ich habe die ganze Zeit nach einem Hack gesucht, aber tief im Inneren wusste ich, dass es einen einfachen Weg gibt, mich aus dem Problem herauszuarbeiten. Um nicht zu sagen, dass es die Lösung für alle hier ist, die lesen, aber es ist eine gute Erinnerung, wie es für viele sein könnte.
Colorlace

Antworten:

69

Sieben Jahre später

Seit ich die Antwort unten geschrieben habe, ist das Ändern sys.pathimmer noch ein schneller und schmutziger Trick, der für private Skripte gut funktioniert, aber es wurden einige Verbesserungen vorgenommen

  • Wenn Sie das Paket installieren (in einer virtuellen Umgebung oder nicht), erhalten Sie das, was Sie möchten. Ich würde jedoch empfehlen, pip zu verwenden, anstatt setuptools direkt zu verwenden (und setup.cfgdie Metadaten zu speichern).
  • Das -mFlag zu verwenden und als Paket auszuführen funktioniert ebenfalls (wird jedoch etwas umständlich, wenn Sie Ihr Arbeitsverzeichnis in ein installierbares Paket konvertieren möchten).
  • Insbesondere für die Tests kann pytest in dieser Situation das API-Paket finden und kümmert sich sys.pathfür Sie um die Hacks

Es kommt also wirklich darauf an, was Sie tun möchten. In Ihrem Fall ist die Installation durch, da es anscheinend Ihr Ziel ist, irgendwann ein richtiges Paket zu erstellen, pip -ewahrscheinlich die beste Wahl, auch wenn es noch nicht perfekt ist.

Alte Antwort

Wie bereits an anderer Stelle erwähnt, ist die schreckliche Wahrheit, dass Sie hässliche Hacks durchführen müssen, um Importe von Geschwistermodulen oder Elternpaketen von einem __main__Modul zu ermöglichen. Das Problem wird in PEP 366 beschrieben . PEP 3122 hat versucht, Importe rationaler zu handhaben, aber Guido hat dies abgelehnt

Der einzige Anwendungsfall scheint darin zu bestehen, Skripte auszuführen, die sich zufällig im Verzeichnis eines Moduls befinden, das ich immer als Antimuster gesehen habe.

( hier )

Allerdings verwende ich dieses Muster regelmäßig mit

# Ugly hack to allow absolute import from the root folder
# whatever its name is. Please forgive the heresy.
if __name__ == "__main__" and __package__ is None:
    from sys import path
    from os.path import dirname as dir

    path.append(dir(path[0]))
    __package__ = "examples"

import api

Hier path[0]ist der übergeordnete Ordner Ihres laufenden Skripts und dir(path[0])Ihr Ordner der obersten Ebene.

Ich war zwar immer noch nicht in der Lage, relative Importe zu verwenden, aber es erlaubt absolute Importe von der obersten Ebene (im apiübergeordneten Ordner Ihres Beispiels ).

Evpok
quelle
3
Sie müssen nicht , wenn Sie aus einem Projektverzeichnis mit -mFormular ausführen oder wenn Sie das Paket installieren (pip und virtualenv machen es einfach)
jfs
2
Wie findet pytest das API-Paket für Sie? Amüsanterweise habe ich diesen Thread gefunden, weil ich auf dieses Problem speziell beim Importieren von Pytest- und Geschwisterpaketen stoße.
JuniorIncanter
1
Ich habe bitte zwei Fragen. 1. Dein Muster scheint __package__ = "examples"für mich ohne zu funktionieren . Warum benutzt du es? 2. In welcher Situation ist, __name__ == "__main__"aber __package__nicht None?
actual_panda
@actual_panda Die Einstellung __packages__hilft, wenn Sie einen absoluten Pfad möchten, z. B. examples.apium iirc zu arbeiten (aber es ist lange her, seit ich das das letzte Mal getan habe), und zu überprüfen, ob das Paket nicht None ist, war meistens ausfallsicher für seltsame Situationen und Zukunftssicherheit.
Evpok
165

Müde von sys.path Hacks?

Es gibt viele sys.path.append-hacks, aber ich habe einen alternativen Weg gefunden, um das Problem in der Hand zu lösen.

Zusammenfassung

  • Wickeln Sie den Code in einen Ordner (zB packaged_stuff)
  • Verwenden Sie das Erstellungsskript, setup.pyin dem Sie setuptools.setup () verwenden .
  • Pip installiert das Paket im bearbeitbaren Zustand mit pip install -e <myproject_folder>
  • Importieren mit from packaged_stuff.modulename import function_name

Konfiguration

Der Ausgangspunkt ist die von Ihnen bereitgestellte Dateistruktur, die in einen Ordner mit dem Namen eingeschlossen ist myproject.

.
└── myproject
    ├── api
       ├── api_key.py
       ├── api.py
       └── __init__.py
    ├── examples
       ├── example_one.py
       ├── example_two.py
       └── __init__.py
    ├── LICENCE.md
    ├── README.md
    └── tests
        ├── __init__.py
        └── test_one.py

Ich werde den .Stammordner aufrufen und in meinem Beispiel befindet er sich unter C:\tmp\test_imports\.

api.py.

Verwenden wir als Testfall die folgende ./api/api.py

def function_from_api():
    return 'I am the return value from api.api!'

test_one.py

from api.api import function_from_api

def test_function():
    print(function_from_api())

if __name__ == '__main__':
    test_function()

Versuchen Sie, test_one auszuführen:

PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
Traceback (most recent call last):
  File ".\myproject\tests\test_one.py", line 1, in <module>
    from api.api import function_from_api
ModuleNotFoundError: No module named 'api'

Auch der Versuch, relative Importe durchzuführen, funktioniert nicht:

Die Verwendung from ..api.api import function_from_apiwürde dazu führen

PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
Traceback (most recent call last):
  File ".\tests\test_one.py", line 1, in <module>
    from ..api.api import function_from_api
ValueError: attempted relative import beyond top-level package

Schritte

  1. Erstellen Sie eine setup.py-Datei im Stammverzeichnis

Der Inhalt für setup.pywäre *

from setuptools import setup, find_packages

setup(name='myproject', version='1.0', packages=find_packages())
  1. Verwenden Sie eine virtuelle Umgebung

Wenn Sie mit virtuellen Umgebungen vertraut sind, aktivieren Sie eine und fahren Sie mit dem nächsten Schritt fort. Die Verwendung von virtuellen Umgebungen sind nicht unbedingt erforderlich, aber sie werden wirklich Sie auf lange Sicht helfen (wenn Sie mehr als 1 Projekt laufenden haben ..). Die grundlegendsten Schritte sind (im Stammordner ausführen)

  • Erstellen Sie eine virtuelle Umgebung
    • python -m venv venv
  • Aktivieren Sie die virtuelle Umgebung
    • source ./venv/bin/activate(Linux, macOS) oder ./venv/Scripts/activate(Win)

Um mehr darüber zu erfahren, googeln Sie einfach "python virtual env tutorial" oder ähnliches. Sie benötigen wahrscheinlich nie andere Befehle als das Erstellen, Aktivieren und Deaktivieren.

Nachdem Sie eine virtuelle Umgebung erstellt und aktiviert haben, sollte Ihre Konsole den Namen der virtuellen Umgebung in Klammern angeben

PS C:\tmp\test_imports> python -m venv venv
PS C:\tmp\test_imports> .\venv\Scripts\activate
(venv) PS C:\tmp\test_imports>

und dein Ordnerbaum sollte so aussehen **

.
├── myproject
   ├── api
      ├── api_key.py
      ├── api.py
      └── __init__.py
   ├── examples
      ├── example_one.py
      ├── example_two.py
      └── __init__.py
   ├── LICENCE.md
   ├── README.md
   └── tests
       ├── __init__.py
       └── test_one.py
├── setup.py
└── venv
    ├── Include
    ├── Lib
    ├── pyvenv.cfg
    └── Scripts [87 entries exceeds filelimit, not opening dir]
  1. pip Installieren Sie Ihr Projekt im bearbeitbaren Zustand

Installieren Sie Ihr Top-Level-Paket myprojectmit pip. Der Trick besteht darin, -ebei der Installation das Flag zu verwenden . Auf diese Weise wird es in einem bearbeitbaren Zustand installiert, und alle an den .py-Dateien vorgenommenen Änderungen werden automatisch in das installierte Paket aufgenommen.

Führen Sie im Stammverzeichnis aus

pip install -e . (Beachten Sie den Punkt, er steht für "aktuelles Verzeichnis")

Sie können auch sehen, dass es mithilfe von installiert wird pip freeze

(venv) PS C:\tmp\test_imports> pip install -e .
Obtaining file:///C:/tmp/test_imports
Installing collected packages: myproject
  Running setup.py develop for myproject
Successfully installed myproject
(venv) PS C:\tmp\test_imports> pip freeze
myproject==1.0
  1. Fügen Sie myproject.Ihren Importen hinzu

Beachten Sie, dass Sie myproject.nur Importe hinzufügen müssen , die sonst nicht funktionieren würden. Importe, die ohne setup.py& pip installfunktionierten, funktionieren weiterhin einwandfrei. Siehe ein Beispiel unten.


Testen Sie die Lösung

Testen wir nun die Lösung mit api.pyden oben test_one.pydefinierten und unten definierten.

test_one.py

from myproject.api.api import function_from_api

def test_function():
    print(function_from_api())

if __name__ == '__main__':
    test_function()

Test ausführen

(venv) PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
I am the return value from api.api!

* Weitere ausführliche Beispiele für setup.py finden Sie in den setuptools-Dokumenten .

** In Wirklichkeit können Sie Ihre virtuelle Umgebung überall auf Ihrer Festplatte platzieren.

np8
quelle
13
Danke für den ausführlichen Beitrag. Hier ist mein Problem. Wenn ich alles tue, was du gesagt hast, und ein Pip-Freeze mache, bekomme ich eine Zeile. -e git+https://[email protected]/folder/myproject.git@f65466656XXXXX#egg=myprojectIrgendeine Idee, wie man das löst?
Si Mo
2
Warum funktioniert die relative Importlösung nicht? Ich glaube dir, aber ich versuche, Pythons verschlungenes System zu verstehen.
Jared Nielsen
8
Hat jemand Probleme mit einem ModuleNotFoundError? Ich habe 'myproject' gemäß diesen Schritten in einer virtuellen Umgebung installiert. Wenn ich eine interpretierte Sitzung betrete und ausführe, import myprojecterhalte ich ModuleNotFoundError: No module named 'myproject'? pip list installed | grep myprojectzeigt , dass es da ist, ist das Verzeichnis korrekt ist , und sowohl die Verison von pipund pythonsind korrekt verifiziert werden.
ThoseKind
2
Hey @ np8, es funktioniert, ich habe es versehentlich in venv und in os installiert :) pip listzeigt Pakete, während pip freezezeigt seltsame Namen, wenn mit flag -e
Grzegorz Krug
3
Verbrachte ungefähr 2 Stunden damit, herauszufinden, wie relative Importe funktionieren, und diese Antwort war die, die schließlich tatsächlich etwas Vernünftiges tat. 👍👍
Graham Lea
43

Hier ist eine weitere Alternative, die ich oben in die Python-Dateien im testsOrdner einfüge:

# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))
Cenk Alti
quelle
1
+1 wirklich einfach und es hat perfekt funktioniert. Sie müssen die übergeordnete Klasse zum Import hinzufügen (ex api.api, examples.example_two), aber ich bevorzuge es so.
Evan Plaice
10
Ich denke, es ist erwähnenswert für Neulinge (wie mich), dass ..hier relativ zu dem Verzeichnis steht, aus dem Sie ausführen - nicht zu dem Verzeichnis, das diese Test- / Beispieldatei enthält. Ich führe aus dem Projektverzeichnis aus und brauchte ./stattdessen. Hoffe das hilft jemand anderem.
Joshua Detwiler
@ JoshDetwiler, ja absolut. Das war mir nicht bewusst. Vielen Dank.
Doak
1
Dies ist eine schlechte Antwort. Das Hacken des Pfades ist keine gute Praxis. Es ist skandalös, wie viel es in der Python-Welt verwendet wird. Einer der Hauptpunkte dieser Frage war zu sehen, wie Importe durchgeführt werden können, während diese Art von Hack vermieden wird.
jtcotton63
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))@JoshuaDetwiler
vldbnc
31

Sie brauchen und sollten nicht hacken, es sys.pathsei denn, es ist notwendig und in diesem Fall nicht. Verwenden:

import api.api_key # in tests, examples

Aus dem Projektverzeichnis ausführen : python -m tests.test_one.

Sie sollten sich wahrscheinlich nach testsinnen bewegen (wenn es sich um API-Unittests handelt) apiund ausführen python -m api.test, um alle Tests auszuführen (vorausgesetzt, es gibt sie __main__.py) oder stattdessen python -m api.test.test_oneauszuführen test_one.

Sie können auch __init__.pyaus examples(es ist kein Python-Paket) entfernen und die Beispiele in einer virtuellen Umgebung ausführen, in der sie apiinstalliert ist, z. B. pip install -e .in einer virtuellen Umgebung, die ein Inplace- apiPaket installiert , wenn Sie über das richtige verfügen setup.py.

jfs
quelle
@Alex Die Antwort geht nicht davon aus, dass es sich bei Tests um API-Tests handelt, mit Ausnahme des Absatzes, in dem ausdrücklich angegeben ist, "ob es sich um API-Unittests handelt" .
JFS
Leider stecken Sie dann mit dem Ausführen aus dem Root-Verzeichnis fest und PyCharm findet die Datei immer noch nicht für ihre netten Funktionen
mhstnsc
@mhstnsc: es ist nicht korrekt. Sie sollten python -m api.test.test_onevon überall aus ausgeführt werden können, wenn die virtuelle Umgebung aktiviert ist. Wenn Sie PyCharm nicht für die Ausführung Ihrer Tests konfigurieren können, versuchen Sie, eine neue Frage zum Stapelüberlauf zu stellen (wenn Sie zu diesem Thema keine vorhandene Frage finden).
JFS
@jfs Ich habe den virtuellen Env-Pfad verpasst, aber ich möchte nicht mehr als die Shebang-Zeile verwenden, um dieses Zeug jemals aus einem Verzeichnis auszuführen. Es geht nicht darum, mit PyCharm zu laufen. Entwickler mit PyCharm würden auch wissen, dass sie fertig sind und durch Funktionen springen, die ich mit keiner Lösung zum Laufen bringen könnte.
mhstnsc
@mhstnsc ein geeigneter Shebang ist in vielen Fällen ausreichend (zeigen Sie es auf die Python-Binärdatei virtualenv. Jede anständige Python-IDE sollte eine virtualenv unterstützen.
jfs
9

Ich habe noch nicht das Verständnis der Pythonologie, das erforderlich ist, um die beabsichtigte Art des Code-Austauschs zwischen nicht verwandten Projekten ohne einen Geschwister- / relativen Import-Hack zu erkennen. Bis zu diesem Tag ist dies meine Lösung. Für examplesoder testszum Importieren von Sachen ..\apiwürde es so aussehen:

import sys.path
import os.path
# Import from sibling directory ..\api
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/..")
import api.api
import api.api_key
user1330131
quelle
Dies würde Ihnen weiterhin das übergeordnete API-Verzeichnis geben und Sie würden die Verkettung "/ .." nicht benötigen sys.path.append (os.path.dirname (os.path.dirname (os.path.abspath ( Datei ))) )
Camilo Sanchez
4

Für den Import von Geschwisterpaketen können Sie entweder die Methode insert oder append des Moduls [sys.path] [2] verwenden :

if __name__ == '__main__' and if __package__ is None:
    import sys
    from os import path
    sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
    import api

Dies funktioniert, wenn Sie Ihre Skripte wie folgt starten:

python examples/example_one.py
python tests/test_one.py

Auf der anderen Seite können Sie auch den relativen Import verwenden:

if __name__ == '__main__' and if __package__ is not None:
    import ..api.api

In diesem Fall müssen Sie Ihr Skript mit dem Argument '-m' starten (beachten Sie, dass Sie in diesem Fall die Erweiterung '.py' nicht angeben dürfen ):

python -m packageName.examples.example_one
python -m packageName.tests.test_one

Natürlich können Sie die beiden Ansätze mischen, damit Ihr Skript funktioniert, egal wie es heißt:

if __name__ == '__main__':
    if __package__ is None:
        import sys
        from os import path
        sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
        import api
    else:
        import ..api.api
Paolo Rovelli
quelle
Ich habe das Click-Framework verwendet, das nicht __file__global ist, daher musste ich Folgendes verwenden: sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0]))))Aber es funktioniert jetzt in jedem Verzeichnis
GammaGames
3

TLDR

Diese Methode erfordert keine Setuptools, Pfad-Hacks, zusätzlichen Befehlszeilenargumente oder die Angabe der obersten Ebene des Pakets in jeder einzelnen Datei Ihres Projekts.

Erstellen Sie einfach ein Skript im übergeordneten Verzeichnis, in dem Sie sich befinden, __main__und führen Sie alles von dort aus aus. Zur weiteren Erklärung lesen Sie weiter.

Erläuterung

Dies kann erreicht werden, ohne einen neuen Pfad zusammen zu hacken, zusätzliche Befehlszeilenargumente zu erstellen oder jedem Ihrer Programme Code hinzuzufügen, um seine Geschwister zu erkennen.

Der Grund, warum dies fehlschlägt, wie ich glaube, bereits erwähnt wurde, ist, dass die aufgerufenen Programme ihre __name__Einstellung als haben __main__. In diesem Fall akzeptiert das aufgerufene Skript, dass es sich auf der obersten Ebene des Pakets befindet, und weigert sich, Skripte in Geschwisterverzeichnissen zu erkennen.

Alles unter der obersten Ebene des Verzeichnisses erkennt jedoch noch ALLES ANDERE unter der obersten Ebene. Dies bedeutet , das nur , was Sie zu tun haben , Dateien zu erhalten in Geschwister Verzeichnissen , sie zu erkennen / nutzen ist , sie von einem Skript in ihrem übergeordneten Verzeichnis zu nennen.

Proof of Concept In einem Verzeichnis mit folgender Struktur:

.
|__Main.py
|
|__Siblings
   |
   |___sib1
   |   |
   |   |__call.py
   |
   |___sib2
       |
       |__callsib.py

Main.py enthält den folgenden Code:

import sib1.call as call


def main():
    call.Call()


if __name__ == '__main__':
    main()

sib1 / call.py enthält:

import sib2.callsib as callsib


def Call():
    callsib.CallSib()


if __name__ == '__main__':
    Call()

und sib2 / Callsib.py enthält:

def CallSib():
    print("Got Called")

if __name__ == '__main__':
    CallSib()

Wenn Sie dieses Beispiel reproduzieren, werden Sie feststellen, dass beim Anruf Main.py"Get Called" gedruckt wird, wie in definiert sib2/callsib.py , obwohl er sib2/callsib.pyangerufen wurde sib1/call.py. Wenn man jedoch direkt aufruft sib1/call.py(nachdem man entsprechende Änderungen an den Importen vorgenommen hat), wird eine Ausnahme ausgelöst. Obwohl es funktioniert hat, wenn es vom Skript in seinem übergeordneten Verzeichnis aufgerufen wurde, funktioniert es nicht, wenn es glaubt, auf der obersten Ebene des Pakets zu sein.

Donnerwald
quelle
2

Ich habe ein Beispielprojekt erstellt, um zu demonstrieren, wie ich damit umgegangen bin. Dies ist in der Tat ein weiterer sys.path-Hack, wie oben angegeben. Beispiel für den Import von Python-Geschwistern , das sich auf Folgendes stützt:

if __name__ == '__main__': import os import sys sys.path.append(os.getcwd())

Dies scheint ziemlich effektiv zu sein, solange Ihr Arbeitsverzeichnis im Stammverzeichnis des Python-Projekts verbleibt. Wenn jemand dies in einer realen Produktionsumgebung einsetzt, wäre es schön zu hören, ob es auch dort funktioniert.

ADataGMan
quelle
Dies funktioniert nur, wenn Sie aus dem übergeordneten Verzeichnis des Skripts
ausführen
1

Sie müssen nachsehen, wie die Importanweisungen im zugehörigen Code geschrieben sind. Wenn examples/example_one.pydie folgende Importanweisung verwendet wird:

import api.api

... dann erwartet es, dass sich das Stammverzeichnis des Projekts im Systempfad befindet.

Der einfachste Weg, dies ohne Hacks zu unterstützen (wie Sie sagen), besteht darin, die Beispiele wie folgt aus dem Verzeichnis der obersten Ebene auszuführen:

PYTHONPATH=$PYTHONPATH:. python examples/example_one.py 
AJ.
quelle
Mit Python 2.7.1 bekomme ich folgendes : $ python examples/example.py Traceback (most recent call last): File "examples/example.py", line 3, in <module> from api.api import API ImportError: No module named api.api. Ich bekomme auch das gleiche mit import api.api.
Zachwill
Meine Antwort wurde aktualisiert ... Sie müssen das aktuelle Verzeichnis zum Importpfad hinzufügen, daran führt kein Weg vorbei.
AJ.
1

Nur für den Fall, dass jemand, der Pydev in Eclipse verwendet, hier landet: Sie können den übergeordneten Pfad des Geschwisters (und damit den übergeordneten Pfad des aufrufenden Moduls) als externen Bibliotheksordner hinzufügen, indem Sie Projekt-> Eigenschaften verwenden und externe Bibliotheken im linken Menü Pydev-PYTHONPATH festlegen . Dann können Sie von Ihrem Geschwister importieren, z from sibling import some_class.

Lord Henry Wotton
quelle
-3

Erstens sollten Sie vermeiden, Dateien mit demselben Namen wie das Modul selbst zu haben. Es kann andere Importe brechen.

Wenn Sie eine Datei importieren, überprüft der Interpreter zuerst das aktuelle Verzeichnis und durchsucht dann globale Verzeichnisse.

Drinnen examplesoder testsSie können anrufen:

from ..api import api

quelle
Ich bekomme folgendes mit Python 2.7.1:Traceback (most recent call last): File "example_one.py", line 3, in <module> from ..api import api ValueError: Attempted relative import in non-package
Zachwill
2
Oh, dann sollten Sie __init__.pydem Verzeichnis der obersten Ebene eine Datei hinzufügen . Andernfalls kann Python es nicht als Modul behandeln
8
Es wird nicht funktionieren. Das Problem ist nicht , dass der übergeordnete Ordner nicht ein Paket ist, ist es , dass , da die Modul __name__ist __main__statt package.module, Python nicht ihre Mutter Paket sehen kann, so .Punkte zu nichts.
Evpok