Python-Abhängigkeitschaos

Kennen Sie die Geschichte der Python-Verpackung? Navigieren Sie in Paketformaten? Wissen Sie, dass Sie das Gewirr der Abhängigkeiten auflösen müssen, auch wenn es so aussieht, als wäre es ein Wunder - eine Null-Abhängigkeit? Ich bin mir sicher, dass sie damit nicht so vertraut sind wie der Autor der DepHell-Bibliothek.



Ich habe es geschafft, mit Nikita Voronov , besser bekannt als Gram oder Orsinium , zu sprechen und ihn nach dem Thema eines zukünftigen Berichts, den Schmerzen von Lösungen zur Auflösung von Abhängigkeiten, DepHell, Pip, dem ersten Match-Wins-Prinzip, Guido, Pipfile, der inkrementellen Python-Entwicklung und der Zukunft des Ökosystems zu fragen.

- In Moscow Python Conf ++ werden Sie über Abhängigkeiten und alles, was sich neben ihnen befindet, sprechen. Warum haben Sie gerade ein solches Thema für den Bericht ausgewählt?

Weil diese Frage alle meine Erfahrungen mit Python durchläuft. Als ich mein erstes Paket erstellt und den ersten Code geschrieben habe, habe ich darüber nachgedacht, wie ich anderen helfen kann, damit sie es installieren können, und habe setup.py ausgeführt. Dann arbeitete er in einer Firma, in einer anderen, in einer dritten, die Aufgabe war kompliziert und entwickelt. Zuerst gab es nur eine Requirements.txt-Datei, dann erkannte ich, dass ich die Abhängigkeiten beheben musste, Pip-Tools, eine Sperrdatei erschien. Später bekamen wir Pipenv und dann Poetry.

Nach und nach öffneten sich immer mehr Probleme, ich versank tiefer in dieses Chaos. Infolgedessen habe ich begonnen, DepHell zu implementieren, ein Projekt zum Verwalten von Abhängigkeiten, mit dem Abhängigkeiten in einem anderen Format aufgelöst und gelesen werden können. Während ich mit allen Arten von Formaten gearbeitet habe, hatte ich genug von ihrem Inneren gesehen und jetzt weiß ich, wie viel darin angeordnet ist, aber ich lerne jeden Tag etwas Neues. Daher kann ich Ihnen viele interessante Dinge über Schmerzen und schlechte Entscheidungen erzählen.

- Schmerz ist immer interessant. Was denken Sie, sind die Probleme, die derzeit in diesem Teil von Python auftreten?

JS verfügt über ein node_modules Verzeichnis, in dem für jede Abhängigkeit eigene Abhängigkeiten gestapelt sind. In Python ist dies nicht der Fall. Beispielsweise wird ein Paket in derselben Umgebung installiert und alle Pakete, die es verwenden, verwenden dieselbe Version dieses Pakets. Dazu müssen Sie die Abhängigkeiten korrekt auflösen - wählen Sie die Version dieses Pakets aus, die im Allgemeinen allen Paketen in dieser Umgebung entspricht. Die Aufgabe ist nicht trivial: Pakete hängen voneinander ab, alles ist miteinander verflochten und das Auflösen von Abhängigkeiten ist schwierig. In Python gibt es praktisch keine Resolver. Clever Resolver gibt es nur in Poetry und DepHell.

All dies wird sehr kompliziert durch die Tatsache, dass pypi.org häufig keine Informationen über Paketabhängigkeiten bereitstellt, da diese Informationen vom Client angegeben werden müssen und der PyPI-Server sie nicht selbst herausfinden kann. Wenn PyPI angibt, dass das Paket keine Abhängigkeiten aufweist, können Sie ihm daher nicht vertrauen. Sie müssen die gesamte Version herunterladen, die Paketabhängigkeiten entpacken und analysieren von setup.py. Dies ist ein langer Prozess, daher kann Resolver in Python nicht schnell sein.

Es gibt nicht nur wenige Resolver in Python, sie sind auch von Natur aus langsam.

In meinem Bericht möchte ich erläutern, wie die Auflösung von DepHell funktioniert: Wie ein Abhängigkeitsdiagramm erstellt wird, wie dieses Diagramm aussieht, warum die meisten wissenschaftlichen Artikel sich mit dem Auflösen von Abhängigkeiten und der Arbeit mit diesem Diagramm befassen. Natürlich gibt es Whitepapers, wie das alles funktionieren sollte. Kluge Köpfe haben Artikel mit Algorithmen geschrieben, aber meistens funktionieren sie nicht für Python. Daher beschreibe ich in DepHell, wie ich in der Praxis mit der Abhängigkeitsauflösung arbeite.

- Ich höre oft von Programmierern, dass sie pip verwenden und alles gut für sie funktioniert. Was machen sie falsch?

Sie haben Glück und stoßen nicht auf einen Abhängigkeitskonflikt. Obwohl das Problem auftreten kann, wenn Sie nur zwei Pakete in einer sauberen Umgebung ablegen. Kürzlich gab es ein Release des Coverage 5.0-Pakets, und wenn Sie nur pip install pytest-cov coveralls angeben, wird pip install pytest-cov coveralls Reihenfolge ausgeführt und für das erste Paket wird die neueste Version des Coverages ausgewählt, nämlich 5.0. Das Prinzip von First Match Wins funktioniert in Pip. Selbst wenn die Version nicht mit dem zweiten Paket kompatibel ist, wird sie bereits für das erste Paket behoben. Oft funktioniert dieser Ansatz, aber nicht immer.

Darüber hinaus gibt es eine Frage mit reproduzierbaren Umgebungen. Da pip immer die neueste Version verwendet, können die Versionen in der lokalen Umgebung und in der Produktion abweichen. Um dieses Problem zu lösen, ist es üblich, die Abhängigkeiten zu beheben. Und wenn die Abhängigkeiten bereits behoben sind, werden die spezifischen Versionen angezeigt, die pip installieren soll. Dann funktioniert pip bereits einwandfrei. Pip hat keinen Resolver, aber es ist möglich, wenn jemand anderes Abhängigkeiten dafür auflöst, wie z. B. DepHell oder Poetry.

- Warum gewinnt dieses Thema jetzt so an Relevanz, wie Sie denken? Warum ist vorher nichts passiert, aber jetzt ist es weg und sogar in verschiedene Richtungen?

Erstens wächst das Python-Ökosystem. Es gibt mehr Pakete, sie müssen mehr installiert werden und es treten viel mehr Probleme auf. Zweitens gibt es schon lange Probleme mit Dateiformaten, die schon lange diskutiert werden.

Setup.py kann in der Regel nicht analysiert, sondern nur ausgeführt werden. Wenn wir zum Beispiel einen Server in Go schreiben möchten, um Pakete für Python schnell zu verteilen, können wir setup.py nicht einfach abrufen und lesen, da es sich um eine ausführbare Datei handelt. Dementsprechend benötigen Sie für die Ausführung Python und eine vollständige Umgebung, und häufig auch, damit das gesamte Projekt in der Nähe liegt und einige spezifische Abhängigkeiten installiert werden. Abgesehen von all diesen Schwierigkeiten kann die Ausführung von setup.py gefährlich sein, da auf Ihrem Computer ein anderer Code ausgeführt wird. Es ist sogar beängstigend, Code unter dem aktuellen Benutzer auszuführen, denn wenn er beispielsweise meinen privaten SSH-Schlüssel empfängt und irgendwohin sendet, ist dies eine große Tragödie.

Die zweite Option zum Definieren von Abhängigkeiten, die es schon lange gibt und mit denen jeder arbeitet, ist die Datei requirements.txt. Es ist auch fast unmöglich, auf die gleiche Weise zu analysieren. Pip kann, aber es macht es sehr, sehr schwierig: Die Funktionen, die die Funktionen aufrufen, Iteratoren, alles ist gemischt. Außerdem kann pip einige seiner Schlüssel aus der Datei requirements.txt lesen, beispielsweise kann ein Index zum Herunterladen angegeben werden. Dies funktioniert jedoch nicht mit allen Schlüsseln.

Um die Datei requirements.txt zu analysieren, müssen Sie entweder pip oder eine Drittanbieterlösung verwenden. Bei allen Lösungen von Drittanbietern handelt es sich im Wesentlichen um Gabeln, bei denen Annahmen über die Datei getroffen werden. Nicht jede knifflige Requirements.txt-Datei, die Pip lesen kann, kann diese Gabeln lesen.

Pip selbst ist nicht als Bibliothek gedacht. Dies ist ein exklusives CLI-Tool, das nur von der Konsole aus verwendet werden kann. Der gesamte Pip-Quellcode ist hinter _internal verborgen, und die Entwickler sagen direkt: "Verwenden Sie dies nicht!". Und jede Veröffentlichung bricht die Abwärtskompatibilität. Sie garantieren ehrlich gesagt keine Kompatibilität und können jederzeit etwas ändern. Und genau das passiert - jedes Mal, wenn ein neues Release mit pip kommt, erfahre ich davon von einem kaputten CI in DepHell.

- Wie wäre es in anderen Sprachen? Ist es dort genauso schlimm oder werden all diese Probleme irgendwo gelöst?

Guido van Rossum wurde kürzlich mit dem Dijkstra-Preis ausgezeichnet. Ich besuchte seine Vorlesungen und fragte ihn nach Python-Abhängigkeiten. Guido sagte, dass Sucht in allen Sprachen Chaos ist, er versucht, nicht hineinzukommen und vertraut der Gemeinschaft, um dieses Problem zu lösen.

Daher wird die Arbeit mit Abhängigkeiten in Python nach und nach von der Community organisiert. Es entstehen neue Lösungen. Als Distutils in Python gebaut wurde, stellten die Leute fest, dass es viele Probleme gab, Add-On-Setuptools. easy_Install wurde später entwickelt, um Pakete zu installieren, hatte aber auch Probleme. Um sie zu lösen, erstellt pip. Jetzt hat pip viele Probleme. Seine Quellen ändern sich ständig, es gibt keine Architektur, es gibt überhaupt keine Schnittstelle.

Die Community versucht, sich etwas auszudenken. Zum Beispiel gab es eine lange Diskussion über das Thema Anforderungen 2.0, bei dem es darum ging, Anforderungen sowohl für Benutzer (hier ist die Version, hier sind Markierungen) als auch für Programmierer aus anderen Sprachen verständlich zu machen.

Sie haben ein Pipfile erstellt, aber da pip sehr verwirrend ist, konnten sie keine Pipfile-Unterstützung hinzufügen.

Entwickler möchten dies natürlich tun. Höchstwahrscheinlich werden sie eines Tages in der Lage sein, aber Pip kann Pipfile bisher nicht unterstützen. Deshalb haben wir pipenv für die Arbeit mit Pipfile und der virtuellen Umgebung entwickelt, einige andere Wrapper für die Umgebung. Aber auch in Pipenv ist alles durcheinander und verwirrt.

Für andere Sprachen gefällt mir, wie das Abhängigkeitsmanagement in Go implementiert ist. Bisher gab es keine Versionierung, es wurde go get , in der Sie angeben, aus welchem ​​Repository welches Paket heruntergeladen werden soll. Aus Einsteigersicht ist dies praktisch: Sie schreiben einfach go get und das Paket ist bereits im System. Und wenn Sie anfangen, mit Python zu arbeiten, bricht sofort ein ganzer Haufen von allem zusammen: einige Versionen, PyPI, pip, requirements.txt, setup.py, jetzt Pipfile, Poetry, __pymodules__ usw.

Während sich Python schrittweise und mithilfe der Community weiterentwickelt, wird das Erbe im Ökosystem aufgebaut. Los war nur go get , aber wieder trat das Problem auf, dass Abhängigkeiten behoben werden mussten, damit insbesondere die Umgebung reproduzierbar war.

Eine abspielbare Umgebung kann mithilfe eines Docker-Containers erstellt werden, in dem alle Abhängigkeiten installiert sind. Manchmal müssen Sie jedoch einzelne Abhängigkeiten aktualisieren. Zum Beispiel sind wir möglicherweise nicht bereit, alles zu aktualisieren, da das Projekt nicht über genügend Tests verfügt, um zu beweisen, dass nach dem Update alles noch funktioniert. Möglicherweise muss jedoch eine bestimmte Abhängigkeit aktualisiert werden, da beispielsweise eine Sicherheitslücke gefunden wurde. Zu diesem Zweck ist es besser, kein Docker-Image zu haben, sondern eine Datei mit der Aufschrift "Eine bestimmte Version eines bestimmten Pakets installieren".

In Go gab es so etwas nicht, und es wurde eine Vendorisierung durchgeführt: Alle Abhängigkeiten wurden nur genommen und in ein Verzeichnis gestellt. Dies ist eine node_modules Lösung, ähnlich wie node_modules , die in Go seit einiger Zeit mithilfe von Lösungen von Drittanbietern implementiert wird. In Python wird dieser Ansatz auch verwendet, z. B. hat pip ein vendor . Wenn Sie pip installieren, werden keine Abhängigkeiten hergestellt, und Sie könnten denken, dass alles sehr cool ist und es überhaupt keine Abhängigkeiten gibt, aber tatsächlich sind sie alle innerhalb des vendor .

Vor ungefähr einem Jahr erschien go.mod (Go Modules) in Go. Dies ist ein neues integriertes Tool, aber go get auch unterstützt. Das Projekt enthält zwei Dateien:

  • man beschreibt die Abhängigkeiten, mit denen das Projekt direkt arbeitet;
  • Die andere ist eine Sperrdatei, die absolut alle Abhängigkeiten und ihre spezifischen Versionen beschreibt.

Dies ist eine coole zentralisierte Lösung.

Was wichtig ist, bestehen sie darauf, dass bestimmte Dinge auf eine bestimmte Weise aussehen sollten. In Go sollte die Version beispielsweise eine semantische Version sein.

Python hat auch eine Spezifikation, wie die Version aussehen soll. Dafür gibt es PEP 440. Aber zum einen ist die Spezifikation sehr kompliziert: Es gibt nicht nur drei Versionskomponenten (Nummern), sondern auch Vorabversion, Post-Release und Ära (wenn sich die Art der Versionierung ändert). Zweitens wurde PEP 440 nicht sofort akzeptiert, sondern es wurde auch inkrementell darauf zugegriffen. Daher wird die ältere Version unterstützt, was bedeutet, dass alles als Version verwendet werden kann - jede Zeile wie „Hallo Welt!“.

- Sie sagten, dass die Community die Sprache inkrementell entwickelt, sodass es eine Vielzahl von Lösungen gibt. Aber warum nicht den ganzen Müll loswerden? Warum nicht die Distutils wegwerfen, das alte und unnötige, das niemand benutzt, aufgeben und stattdessen aktiv neue Praktiken und Werkzeuge einführen?

All dies beizubehalten ist sinnvoll, damit Sie die alten Pakete trotzdem installieren können. Es ist unmöglich darauf zu bestehen, dass es notwendig ist, genau das zu tun, und nicht anders, weil die Entscheidung von der Gemeinschaft getroffen wird. Keiner der Core Python-Entwickler kommt und sagt: "Das war's, wir machen jetzt alles und keine Nägel."

Go hat alles, was Sie brauchen, um mit Abhängigkeiten sofort zu arbeiten. In Python müssen Sie alles von außen neu installieren, und Sie müssen immer noch verstehen, was genau. Meistens reicht pip aus, aber jetzt werden andere Optionen angezeigt.

Auf der Site mit offiziellen Paketempfehlungen der Python Packaging Authority, der Gruppe, die pip, pipenv und PyPI herstellt, ist die Verwendung von pipenv vorgesehen. Mit pipenv ist eine andere Geschichte. Erstens hat es eine schlechte Auflösung. Zweitens gab es lange Zeit keine Veröffentlichungen und die Community wartet bereits darauf, dass die Macher ehrlich zugeben, dass dieses Projekt tot ist. Das dritte Problem mit pipenv ist, dass es nur für Projekte geeignet ist, nicht jedoch für Pakete: Sie können Projektabhängigkeiten in pipenv angeben, aber Sie können den Namen und die Version nicht angeben und es dementsprechend in ein Paket zum Herunterladen in PyPI einfügen. Es stellt sich heraus, dass es immer noch nicht ausreicht, den Empfehlungen der Python Packaging Authority zu folgen und pipenv zu verwenden, um dies herauszufinden.

Die Poesie versucht, revolutionär zu sein. Die Datei setup.py wird im Grunde genommen nicht generiert, was aus Gründen der Abwärtskompatibilität nützlich wäre, da Poetry das neue und einzige Format für alles sein möchte. Er weiß, wie man Pakete sammelt, und er hat eine Sperrdatei, die für Projekte benötigt wird. Trotzdem hat Poetry eine Menge seltsamer Dinge, viele bekannte Funktionen werden nicht unterstützt.

- Wie sehen Sie die Zukunft des Ökosystems im Umgang mit Abhängigkeiten? Deine Vorhersage.

Alles wird mehr oder weniger besser. Ich habe zum Beispiel eine Stelle in pip gesehen, und einem Entwickler, der sie in Ordnung bringt, wird viel Geld versprochen. Vielleicht wird pip eine universellere Lösung. Aber Sie brauchen jemanden, der es ernst nimmt: Kommen Sie und sagen Sie, dass wir es so machen, jetzt folgen wir einem strengeren PEP und werden darauf bestehen (weil PEP nur eine Empfehlung ist, die niemand wirklich tut nicht erforderlich zu folgen).

Zum Beispiel hatten wir eine solche Geschichte: Eine bestimmte Version von PyYAML wurde in der Sperrdatei gesperrt. Eines Tages werden die Tests für CI bestanden, wir stellen sie für die Produktion bereit, und alles fällt dort ab, weil die PyYAML-Version nicht gefunden wurde. Die Sache war, dass die gesperrte Version von pypi.org gelöscht wurde. Alle waren empört, aktualisierten die Sperrdatei, überlebten irgendwie, aber das Sediment blieb.

Vor nicht allzu langer Zeit ist PEP 592 erschienen, es wurde bereits übernommen und wird in pip gepflegt, in dem ruckartige Releases erschienen sind. Yank bedeutet, dass die Veröffentlichung noch nicht vollständig von pypi.org entfernt wurde - sie ist versteckt. Wenn Sie also angeben, dass Sie beispielsweise eine PyYAML-Version größer als 3.0 benötigen, überspringt pip die ausgelassenen Versionen und installiert die neueste verfügbare Version. Wenn jedoch in der Sperrdatei eine bestimmte Version angegeben ist und diese Version "yank" ist, wird sie von pip trotzdem installiert. Auf diese Weise werden Sperrdateien und die Bereitstellung nicht unterbrochen, aber wenn möglich, wird die alte Version nicht verwendet.

Das zweite interessante ist PEP für __pymodules__ . Hierbei handelt es sich pip install schlanke virtuelle Umgebungen: Sie öffnen das Projektverzeichnis, schreiben die pip install PyYAML und PyYAML wird nicht global installiert, sondern im Verzeichnis __pymodules__ . Wenn Python in diesem Verzeichnis gestartet wird, importiert es PyYAML nicht global, sondern aus diesem Verzeichnis.

Ich nenne es zumindest virtuelle Umgebungen, weil es weniger Isolation gibt. Beispielsweise gibt es keinen Zugriff auf Binärdateien. Wenn eine virtuelle Umgebung mit installiertem pytest aktiviert ist, kann sie von der Konsole aus verwendet werden: Schreiben Sie einfach pytest und führen Sie eine Aktion aus. Mit __pymodules__ importiert werden, jedoch keine Binärdateien, da diese nicht installiert werden.

Dieses PEP wurde entwickelt, um es Anfängern leicht zu machen. Damit sie sich nicht mit den Feinheiten virtueller Umgebungen befassen müssen, sondern einfach alles installieren, was Sie für __pymodules__ per Pip-Installation benötigen.

- Nun, die Zukunft in Ihrer Prognose ist heller als jetzt.

Ja, aber wie gesagt, wenn niemand kommt und sagt, dass wir das Erbe wiederholen und versuchen, es wegzuwerfen, bleiben die Probleme bestehen. Jetzt häufen wir und häufen Werkzeuge an, und es wird in naher Zukunft unmöglich sein, sie vollständig loszuwerden.

- Was denken Sie, warum keiner der Entwickler Abhängigkeiten aktualisieren kann? Fast überall - weder in Unternehmen noch in Open Source - wurde der Prozess der Arbeit mit Sicherheitsreleases im Prinzip mit neuen Neben- oder Hauptreleases aufgebaut. Wo sehen Sie die Probleme hier?

Zumindest, wenn Sie Abhängigkeiten aktualisieren möchten, ist es beängstigend, alle Abhängigkeiten zu aktualisieren, da es nicht so ist, dass alles funktioniert, selbst wenn Sie die Tests bestehen. Beispielsweise tritt diese Situation häufig bei Sellerie auf, da Sellerie in Tests nicht vollständig getestet werden kann. Sie können etwas sperren, etwas vereinfachen, aber die Tatsache, dass die Mitarbeiter ausgeführt werden, kann nicht überprüft werden.

Die Arbeit mit Tests ist gut implementiert. Selbst in Lernprogrammen zu Go Modules wird beschrieben, wie Abhängigkeiten aktualisiert werden: Sie aktualisieren bestimmte Abhängigkeiten und führen Tests aus. Darüber hinaus laufen die Tests nicht nur bei Ihnen, sondern auch in dieser Abhängigkeit.

Ein interessanter Aspekt ist noch zu erwähnen: Sollen Tests in Paketen in Python sein? Sollten beim Herunterladen eines Pakets von pypi.org Tests durchgeführt werden? Theoretisch sollten sie und haben sogar einen Mechanismus, um sie auszuführen: In setup.py können Sie angeben, wie Tests ausgeführt werden und welche Abhängigkeiten sie haben.

Aber erstens wissen viele Leute nicht, wie man sie ausführt, und führen keine Tests durch, die abhängig sind. Daher werden sie oft nicht benötigt. Zweitens haben diese Tests häufig sehr schwierige Vorrichtungen, und daher bedeutet das Einschließen von Tests in eine Packung, die Packung 6 bis 10 mal größer zu machen.

Es wäre toll, ein Paket mit und ohne Tests herunterladen zu können. Aber jetzt gibt es keine solche Möglichkeit, sodass sich Tests in Paketen oft nicht summieren. Es herrscht Chaos und ich weiß nicht einmal, ob es möglich ist, diese Abhängigkeiten beim Aktualisieren von Abhängigkeiten zu testen.

Dieser Aspekt scheint meistens übersehen zu werden. Insbesondere in einigen anderen Sprachen wird Go als bewährte Methode angesehen. Aktualisieren Sie ein Paket in der Umgebung, und führen Sie sofort Tests durch, um sicherzustellen, dass dieses Paket in dieser Umgebung einwandfrei funktioniert.

- Warum sind Ihrer Meinung nach in Python Werkzeuge für die automatische semantische Versionierung nicht beliebt?

Ich denke eines der Probleme ist, dass die Version an vielen Stellen beschrieben werden kann. Am häufigsten gibt es drei davon: das Format der Projekt-Metadatenbeschreibung (pypi.org, poety, setup.py usw.), innerhalb des Projekts selbst und in der Dokumentation. Ein Upgrade einer Version an drei Stellen ist nicht sehr schwierig, aber leicht zu vergessen.

DepHell hat ein Team für Versions-Upgrades. DepHell , , . semantic version, compatible version .. , , .

Flit. Flit — , . : init , build , publish install . , , PyPI — . Flit , . docstring . , .

DepHell Flit . description, , , .

, .

DepHell import , , , , . , , .

, Moscow Python Conf++ 27 . DepHell backend, web, , AI/ML, , DevOps, , IoT, infosec . , , Moscow Python Conf++.

Source: https://habr.com/ru/post/de483920/


All Articles