setuptools: Datei- und Ordnerstruktur "bündeln"

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
syntor
User
Beiträge: 88
Registriert: Donnerstag 2. Dezember 2010, 03:56

Moin

Ich frage mich, wie ich bei Setuptools am besten Dateien/Ordner so mitgebe, dass ich diese dann einfach über einen Pfad ansperechen kann.

Stellt euch vor, ihr hab ein Script, dass, wenn aufgerufen, eine bestimmte Orderstruktur plus zugehörige Dateien im gegenwärtigen Verzeichnis erstellen soll, so wie z.B. `git init` die Struktur für ein leeres Git-Repository anlegt.

In Linux gibt es ja z.B. bei vielen Distributionen den Ordner `/etc/skel` der alle Dateien beinhaltet, die beim Erstellen eines neuen Benutzers in dessen Home-Verzeichnis kopiert werden. Nun möchte ich alle Dateien, die mein Script anlegen soll, in einem solchen Ordner verwalten, diese dann mittels setuptools "packen", und auf dem Zielsystem an einen Ort bringen, sodass ich von meinem Script aus einfach per `shutils.copy` die Dateien in ein Verzeichnis kopieren kann.

Leider kann ich in der setuptools Dokumentation keinen wirklichen Ansatz finden, wie ich das bewerkstelligen könnte. Ich habe versucht, über ein MANIFEST.in mit dem Inhalt `recursive-inlcude skel *` die Dateien "mitzugeben", jedoch scheint das nichts zu bewirken.

Habt ihr eine Idee wie ich das am besten anstellen könnte?
lunar

Lies in der "distutils"-Dokumentation über "package data" nach. Solche Daten wird im Paketverzeichnis installiert, so dass Du zur Laufzeit über "pkgutil.get_data()", "pkg_resources" oder auch direkt mit "os.path" und "__file__" an die Daten gelangen kannst.
syntor
User
Beiträge: 88
Registriert: Donnerstag 2. Dezember 2010, 03:56

Danke für die rasche Antwort.

Ich habe nun versucht, das Verzeichnis über package_data wie folgt einzubinden:

Code: Alles auswählen

package_data = {'mein_paket' : ['data/*']},
Das Problem, das ich jetzt habe, ist folgendes: Da die Pfadangaben glob-Patterns sind, geht das natürlich nicht und wird mit "error: can't copy [...]: doesn't exist or not a regular file" quittiert.

Muss ich wirklich eine Liste aller Dateien bauen (natürlich via Script) und diese dann mitgeben? Oder kann ich setuptools irgendwie klarmachen, dass ich einen ganzen Verzeichnisbaum "mitpacken" möchte?

Ein weiterer Nebeneffekt der mir nicht wirklich gefallen will ist, dass ich die Dateien in den Ordner des Pakets stecken muss, wobei ich diese doch lieber auf Projektebene, also dort wo auch `setup.py` ist, haben möchte. (Ja, das ist jetzt ganz subjektiv aber es stört mich irgendwie :) )
lunar

"distutils" interpretiert Wildcards in "package_data", so dass die gezeigte Zeile durchaus alle Daten in "data/" einschließt. Also das Übliche: Exakter Quelltext und exakte Fehlermeldung bitte, denn offenbar hast Du irgendwo einen Fehler in Deiner "setup.py".

Wenn es Dich stört, dass die Daten dann im Paket liegen, dann ist es besser, damit leben zu lernen. Es gibt jedenfalls meines Wissens keinen einfachen Weg, diese "Einschränkung" zu überwinden, denn dafür musst Du wohl das distutils-Kommando, welches die Installation der Paketdaten vornimmt, überschreiben und entsprechend anpassen. Das ist kein sonderlich amüsanter Zeitvertreib, denn die distutils-API ist weder sonderlich schön noch allzu intuitiv, und entbehrt zudem jeglicher Dokumentation, was man auch nicht durch die Lektüre des Quelltexts ausgleichen kann, da dieser die Qualität der API nochmals locker unterbietet. Alles in allem kann man seine Zeit sinnvoller verwenden ;)
syntor
User
Beiträge: 88
Registriert: Donnerstag 2. Dezember 2010, 03:56

Das war der Quelltext und die effektive Fehlermeldung, ich habe bloss den Paketnamen verallgemeinert.
In your package_data, your '*' glob will match package/dir itself, and try to copy that dir as a file, resulting in a failure. Find a glob that won't match the directory package/dir, rewriting your setup.py along these lines:
Dieser Antwort bei Stackoverflow auf dieselbe Fehlermeldung entnehme ich, dass durch die Wildcards keine Verzeichnisse "matchen" dürfen. (Was ist der deutsche Ausdruck hierfür?)

Wenn ich die Unterverzeichnisse entferne, dann klappt das auch.

Ich habe mir nun das hier gebastelt:

Code: Alles auswählen

def discover_data(path, pattern = "*", prefix = ""):
    """
    Iterator to build  a list of files suitable for processing
    by distutil's package_data.

    The individual paths returned will be relative to the path that is
    passed as first argument. Use the prefix parameter to adjust the
    returned paths.

    """
    cwd_backup = os.getcwd()
    os.chdir(path)
    matches = glob(pattern)
    for match in matches:
        if os.path.isdir(match):
            subdirectory = discover_data(match, pattern = pattern)
            try:
                for file in subdirectory:
                    yield prefix + match + "/" + file
            except StopIteration:
                continue
        else:
            yield prefix + match
    os.chdir(cwd_backup)
#edit: hmm.. mit `os.walk`wäre dies wohl einfacher gewesen :)
#edit2: Oder auch nicht, dann müsste ich die Pfade manuell korrigieren, da distutils forwardslashes will, nicht? Zudem kann ich so einfache glob-patterns verwenden, anstelle von Regex - wobei das natürlich ein Vor- und Nachteil ist.

#edit3: Was für ein Nachteil habe ich, wenn ich zip_safe auf False stelle? Denn dann könnte ich einfach die Dateien kopieren und müsste nicht selektiv aus dem Zip-Archiv entpacken.
lunar

Du kannst mithilfe des "fnmatch"-Moduls auch Zeichenketten direkt gegen einfache Glob-Muster verwenden. Außerdem kannst Du den Schrägstrich ja auch am Schluss nochmal anhängen. Nachbearbeiten musst Du die Pfade sowieso, da Distutils nämlich zwecks Plattformunabhängigkeit darauf besteht, dass in "setup.py" alle Pfade grundsätzlich mit Schrägstrichen nach Unix-Art angegeben werden. Insofern kannst Du ruhig auch "os.walk()" nutzen, wenn das den Quelltext vereinfacht.

Du solltest aber vor allem "os.chdir()" loswerden, den gerade distutils ist zu großen Teilen darauf angewiesen, dass das Arbeitsverzeichnis stimmt. Tritt in Deinem Quelltext eine Ausnahme auf, wird das Arbeitsverzeichnis nicht zurückgesetzt, was weitere Fehler zur Folge haben kann, die dann in der Folge die ursprüngliche Fehlerursache verschleiern. Um relative Pfade zu erhalten, ist es daher meines Erachtens ratsamer, das Arbeitsverzeichnis nicht zu wechseln, sondern stattdessen einfach das ungewollte Präfix mittels Slicing oder den Funktions aus "os.path" abzuschneiden.

"zip_safe=False" birgt per se keine Nachteile. Du verlierst dadurch lediglich die Möglichkeit, den Paket als "egg" in einem einzigen Archiv auszuliefern. Der theoretische Vorteil ist, dass man solche Eggs einfach direkt in "site-packages" kopieren und sie danach sofort benutzen kann, ohne erst "python setup.py install" oder "pip install" (bzw. "easy_install") zu bemühen. In der Praxis werden Eggs selten genutzt. Sie bieten keine bedeutenden Vorteile, aber diverse Nachteile. Allenfalls unter Windows spielen sie eine Rolle bei der Verteilung von Erweiterungsmodulen, oder Modulen, die von nativen Bibliotheken abhängen kann, da man diese dann in Form eines Eggs vorkompiliert verteilen kann. Besonders häufig ist das allerdings auch nicht. Im Allgemeinen sind Eggs mittlerweile von so geringer Bedeutung, dass "pip", ein moderner Ersatz für das allzu merkwürdige "easy_install", sie bereits gar nicht mehr unterstützt. Es spricht also nichts dagegen, "zip_safe" auf "False" zu setzen, und dann einfach direkt über "__file__" auf die Daten zuzugreifen.

Es ist aber eigentlich auch überflüssig. Enthält ein Paket neben Python-Modulen noch Daten, dann nimmt setuptools pauschal an, dass Deine Anwendung nicht "zip_safe" ist, da es ja nicht erraten kann, wie auf diese Daten zugegriffen wird.
Zuletzt geändert von lunar am Samstag 1. Januar 2011, 20:33, insgesamt 1-mal geändert.
syntor
User
Beiträge: 88
Registriert: Donnerstag 2. Dezember 2010, 03:56

lunar hat geschrieben:Du kannst mithilfe des "fnmatch"-Moduls auch Zeichenketten direkt gegen einfache Glob-Muster verwenden. Außerdem kannst Du den Schrägstrich ja auch am Schluss nochmal anhängen. Nachbearbeiten musst Du die Pfade sowieso, da Distutils nämlich zwecks Plattformunabhängigkeit darauf besteht, dass in "setup.py" alle Pfade grundsätzlich mit Schrägstrichen nach Unix-Art angegeben werden. Insofern kannst Du ruhig auch "os.walk()" nutzen, wenn das den Quelltext vereinfacht.
Den Slash am Schluss nochmal anhängen? Weshalb, damit er auch einen Ordner kopieren könnte? Dann würde es ja aber mit Dateien nicht mehr gehen oder?
So wie der Code jetzt ist, werden die Pfade immer mit Forwardslashes angegeben, ob auf Windows oder Linux. Ich bin mir also nicht sicher, was für einen Vorteil mir fnmatch bringen würde.
lunar hat geschrieben:Du solltest aber vor allem "os.chdir()" loswerden, den gerade distutils ist zu großen Teilen darauf angewiesen, dass das Arbeitsverzeichnis stimmt. Tritt in Deinem Quelltext eine Ausnahme auf, wird das Arbeitsverzeichnis nicht zurückgesetzt, was weitere Fehler zur Folge haben kann, die dann in der Folge die ursprüngliche Fehlerursache verschleiern. Um relative Pfade zu erhalten, ist es daher meines Erachtens ratsamer, das Arbeitsverzeichnis nicht zu wechseln, sondern stattdessen einfach das ungewollte Präfix mittels Slicing oder den Funktions aus "os.path" abzuschneiden.
Tritt in meinem Quelltext eine Exception auf, so sollte doch das Script ganz beendet werden, somit würde also distutils nie zum Zuge kommen, nicht wahr? Ich fange ja schliesslich nur die StopIteration-Exception ab.

lunar hat geschrieben:"zip_safe=False" birgt per se keine Nachteile. Du verlierst dadurch lediglich die Möglichkeit, den Paket als "egg" in einem einzigen Archiv auszuliefern. Der theoretische Vorteil ist, dass man solche Eggs einfach direkt in "site-packages" kopieren und sie danach sofort benutzen kann, ohne erst "python setup.py install" oder "pip install" (bzw. "easy_install") zu bemühen. In der Praxis werden Eggs selten genutzt. Sie bieten keine bedeutenden Vorteile, aber diverse Nachteile. Allenfalls unter Windows spielen sie eine Rolle bei der Verteilung von Erweiterungsmodulen, oder Modulen, die von nativen Bibliotheken abhängen kann, da man diese dann in Form eines Eggs vorkompiliert verteilen kann. Besonders häufig ist das allerdings auch nicht. Im Allgemeinen sind Eggs mittlerweile von so geringer Bedeutung, dass "pip", ein moderner Ersatz für das allzu merkwürdige "easy_install", sie bereits gar nicht mehr unterstützt. Es spricht also nichts dagegen, "zip_safe" auf "False" zu setzen, und dann einfach direkt über "__file__" auf die Daten zuzugreifen.

, ist aber eigentlich auch überflüssig. Enthält ein Paket neben Python-Modulen noch Daten, dann nimmt setuptools pauschal an, dass Deine Anwendung nicht "zip_safe" ist, da es ja nicht erraten kann, wie auf diese Daten zugegriffen wird.
Vielen Dank für die ausführliche Erklärung :)
lunar

Vergiss die Anmerkung über angehängte Schrägstriche, ich hatte Deinen Beitrag nicht richtig gelesen. Wenn ich den Rest allerdings verstanden habe, dann kannst auch "fnmatch" und "os.walk()" nutzen, das ist kürzer:

Code: Alles auswählen

def discover_data(directory, pattern="*"):
    for root, _, filenames in os.walk(directory):
        for filename in fnmatch.filter(filenames, pattern):
            yield os.path.join(root, filename).replace(os.sep, '/')
Im Bezug auf die Ausnahme hast Du natürlich recht, die Funktion wird ja aufgerufen, bevor "setup()" überhaupt ausgeführt wird. Wieso Du allerdings StopIteration abfängst, habe ich nicht ganz verstanden. Woher sollte diese Ausnahme denn kommen?
syntor
User
Beiträge: 88
Registriert: Donnerstag 2. Dezember 2010, 03:56

Nun... jetzt wo du das so explizit danach fragst... frage ich mich das auch. Irgendwie stand ich wohl unter der Illusion, der Generator würde am Ende eine StopIteration-Exception werfen. Da ich das ja aber über `for` iteriere, müsste ich das ja gar nicht abfangen... :roll:

Vielen Dank für den alternativen Ansatz, das sieht sehr pythonic aus.
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Du kannst auch einfach mit der MANIFEST.in festlegen was du alles im sdist build haben willst und was nicht. Da funktioniert dann auch globbing.
syntor
User
Beiträge: 88
Registriert: Donnerstag 2. Dezember 2010, 03:56

Ich habe versucht, über ein MANIFEST.in mit dem Inhalt `recursive-inlcude skel *` die Dateien "mitzugeben", jedoch scheint das nichts zu bewirken.
Wie würde ich das mit der MANIFEST.in-Datei denn korrekt anstellen?
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

syntor hat geschrieben:Wie würde ich das mit der MANIFEST.in-Datei denn korrekt anstellen?
Schau dir mal die Jinja2 MANIFEST.in an bzw. die von anderen Projekten.
lunar

@DasIch: Es geht um die "package_data"-Option, mit MANIFEST.in und sdist hat das nur mittelbar zu tun.
Antworten