¿Conoces la historia del empaquetado de Python? ¿Navegas en formatos de paquete? ¿Sabe que tendrá que desentrañar la maraña de dependencias incluso cuando parezca un milagro: cero dependencia? Estoy seguro de que no están tan familiarizados con todo esto como el autor de la biblioteca DepHell.

Me las arreglé para hablar con
Nikita Voronov , más conocido como Gram u
orsinium , y preguntarle sobre el tema del informe futuro, los dolores de las malas decisiones de resolución de dependencia, DepHell, pip, el principio del primer partido gana, Guido, Pipfile, desarrollo incremental de Python y el futuro del ecosistema.
- En Moscow Python Conf ++ hablarás sobre las dependencias y todo lo que está al lado de ellas. ¿Por qué elegiste un tema así para el informe?Porque esta pregunta pasa por toda mi experiencia con Python. Cuando hice mi primer paquete, escribí el primer código, pensé en cómo ayudar a otras personas para que pudieran instalarlo, e hice setup.py. Luego trabajó en una empresa, en otra, en una tercera, la tarea fue complicada y desarrollada. Al principio solo había un archivo require.txt, luego me di cuenta de que necesitaba arreglar las dependencias, pip-tools, apareció un archivo de bloqueo. Más tarde tenemos Pipenv, y luego Poesía.
Poco a poco se abrieron más y más problemas, me estaba hundiendo más en este caos. Como resultado, comencé a implementar
DepHell , un proyecto para administrar dependencias que pueden resolverlas y leerlas en un formato diferente. Mientras trabajaba con todo tipo de formatos, había visto suficiente de su interior y ahora sé cuánto está dispuesto dentro, pero cada día aprendo algo nuevo. Por lo tanto, puedo contarte muchas cosas interesantes sobre el dolor y las malas decisiones.
- El dolor siempre es interesante. ¿Cuáles crees que son los problemas en este momento en esta parte de Python?JS tiene un directorio
node_modules
, y cada dependencia tiene sus propias dependencias apiladas dentro. En Python, este no es el caso. Por ejemplo, un paquete se instala en el mismo entorno y todos los paquetes que lo usan usan la misma versión de este paquete. Para hacer esto, debe resolver correctamente las dependencias: elija la versión de este paquete que satisfaga generalmente todos los paquetes en este entorno. La tarea no es trivial: los paquetes dependen unos de otros, todo está entrelazado y es difícil resolver dependencias. Prácticamente no hay resolvers en Python. El solucionador inteligente solo está en Poesía y DepHell.
Todo esto se complica en gran medida por el hecho de que pypi.org a menudo no proporciona información sobre las dependencias del paquete, ya que esta información debe ser especificada por el cliente, el servidor PyPI no puede resolverlo por sí mismo. Por lo tanto, cuando PyPI dice que el paquete no tiene dependencias, no puede confiar en él. Debe descargar la versión completa, descomprimir y analizar las dependencias del paquete desde setup.py. Este es un proceso largo, por lo que resolver en Python no puede ser rápido.
No solo hay pocos resolvers en Python, sino que también son lentos por diseño.
En mi
informe , quiero decir cómo funciona la resolución de DepHell: cómo construir un gráfico de dependencia, cómo se ve este gráfico, por qué la mayoría de los artículos científicos mienten sobre cómo resolver dependencias y trabajar con este gráfico. Por supuesto, hay documentos sobre cómo debería funcionar todo esto. Las personas inteligentes han escrito artículos con algoritmos, pero la mayoría de las veces no funcionan para Python. Por lo tanto, describiré cómo trabajo con la resolución de dependencias en la práctica en DepHell.
- A menudo escucho de los programadores que usan pip, y todo funciona bien para ellos. ¿Qué están haciendo mal?Tienen suerte, no encuentran un conflicto de dependencias. Aunque el problema puede surgir cuando coloca solo dos paquetes en un entorno limpio. Recientemente hubo un lanzamiento del paquete de cobertura 5.0, y si solo especifica
pip install pytest-cov coveralls
, entonces pip irá en orden y para el primer paquete seleccionará la última versión de cobertura, que es 5.0. El principio del
primer partido gana funciona en pip, por lo que incluso si la versión no es compatible con el segundo paquete, ya se solucionará para el primer paquete. A menudo, este enfoque funciona, pero no siempre.
Además, hay una pregunta con entornos reproducibles. Como pip siempre pone la última versión, las versiones en el entorno local y en producción pueden diferir. Para resolver este problema, es habitual corregir las dependencias. Y cuando las dependencias ya están arregladas, se indican las versiones específicas que pip debe instalar, luego pip ya funciona bien. Pip no tiene un solucionador, pero sí cuando alguien más resuelve dependencias para él, como DepHell o Poetry.
- ¿Por qué este tema ahora está ganando tanta relevancia, como piensas? ¿Por qué no sucedió nada antes, pero ahora se ha ido, e incluso en diferentes direcciones?Primero, el ecosistema de Python está creciendo. Hay más paquetes, deben instalarse más y aparecen muchos más problemas. En segundo lugar, los problemas con los formatos de archivo han existido durante mucho tiempo y se han discutido durante mucho tiempo.
Setup.py es generalmente imposible de analizar, solo se puede ejecutar. Si queremos, por ejemplo, escribir un servidor en Ir para distribuir rápidamente paquetes para Python, entonces no podemos simplemente tomar y leer setup.py, porque es un archivo ejecutable. En consecuencia, para ejecutarlo, necesita Python y un entorno completo, y a menudo también para que todo el proyecto se encuentre cerca y se instalen algunas dependencias específicas. Además de todas estas dificultades, ejecutar setup.py puede ser peligroso, porque se ejecutará algún otro código en su computadora. De hecho, es aterrador incluso ejecutar código de debajo del usuario actual, porque si, por ejemplo, recibe mi clave SSH privada y la envía a alguna parte, será una gran tragedia.
La segunda opción para definir dependencias, que existe desde hace mucho tiempo y todos trabajan con ella, es require.txt. También es casi imposible analizar de la misma manera. Pip puede, pero lo hace muy, muy difícil: las funciones que llaman funciones, iteradores, todo está mezclado. Además, pip puede leer algunas de sus claves de require.txt, por ejemplo, se puede especificar un índice para descargar. Pero esto no funciona con todas las claves.
Por lo tanto, para analizar requerimientos.txt, debe usar pip o alguna solución de terceros. Todas las soluciones de terceros son esencialmente tenedores y utilizan algún tipo de suposiciones sobre el archivo. No todos los pip del archivo de requisitos.txt difíciles de leer podrán leer estos tenedores.
Pip en sí no está destinado a ser utilizado como una biblioteca. Esta es una herramienta exclusiva de CLI que solo se puede usar desde la consola. Todo el código fuente de pip está oculto detrás de
_internal
, y los desarrolladores dicen directamente: "¡No uses esto!". Y cada lanzamiento rompe la compatibilidad con versiones anteriores. Sinceramente, no garantizan la compatibilidad y pueden cambiar cualquier cosa en cualquier momento. Y esto es lo que sucede: cada vez que un nuevo lanzamiento viene con pip, me entero de un CI roto en DepHell.
- ¿Qué tal en otros idiomas? ¿Es igual de malo allí o se resuelven todos estos problemas en alguna parte?Guido van Rossum recibió
recientemente el Premio Dijkstra. Asistí a sus conferencias y le pregunté sobre las dependencias de Python. Guido dijo que la adicción en todos los idiomas es un caos, trata de no entrar y confía en la comunidad para resolver este problema.
Por lo tanto, en Python, el trabajo con dependencias se organiza gradualmente por la comunidad. Nuevas soluciones están surgiendo. Una vez que Distutils se creó en Python, la gente se dio cuenta de que tenía muchos problemas, las herramientas de configuración adicionales.
easy_Install
se desarrolló más tarde para instalar paquetes, pero también tuvo problemas. Para resolverlos, creó pip. Ahora pip tiene muchos problemas. Sus fuentes cambian constantemente, no hay arquitectura, no hay interfaz en absoluto.
La comunidad está tratando de llegar a algo. Por ejemplo, hubo una larga discusión sobre el tema llamado requisitos 2.0 sobre cómo hacer que los requisitos sean comprensibles tanto para las personas (aquí está la versión, aquí hay marcadores) como programáticamente desde otros idiomas.
Crearon un Pipfile, pero dado que pip es muy confuso, no pudieron agregarle soporte para Pipfile.
Los desarrolladores quieren hacer esto, por supuesto. Lo más probable es que algún día puedan, pero hasta ahora pip no puede soportar Pipfile. Por lo tanto, hicimos que pipenv trabaje con Pipfile y el entorno virtual, algunos otros contenedores con el entorno. Pero en pipenv, también, todo está mezclado y confundido.
Para otros idiomas, me gusta cómo se implementa la gestión de dependencias en Go. Anteriormente, no había versiones en él, había
go get
, en el que indicaba desde qué repositorio qué paquete descargar. Desde el punto de vista de un principiante, esto es conveniente: simplemente escribe
go get
y el paquete ya está en el sistema. Y cuando comienzas a trabajar con Python, todo un montón de todo colapsa de inmediato: algunas versiones, PyPI, pip, require.txt, setup.py, ahora Pipfile, Poetry,
__pymodules__
, etc.
A medida que Python evoluciona gradualmente y con la ayuda de la comunidad, el legado se acumula en el ecosistema. Ir era solo
go get
, pero nuevamente surgió el problema de que las dependencias debían repararse para que, en particular, el entorno fuera reproducible.
Se puede crear un entorno jugable utilizando un contenedor acoplable con todas las dependencias instaladas. Pero a veces necesita actualizar dependencias individuales. Por ejemplo, es posible que no estemos listos para actualizar todo, porque el proyecto no tiene suficientes pruebas para demostrar que después de la actualización todo funciona. Pero una cierta dependencia puede necesitar ser actualizada, porque, por ejemplo, se encontró una vulnerabilidad en ella. Para hacer esto, es mejor no tener una imagen acoplable, sino un archivo que diga: "Instalar una versión específica de un paquete específico".
No había tal cosa en Go, y apareció la comercialización: todas las dependencias se tomaron y se colocaron en un solo directorio. Esta es una solución sucia, similar a
node_modules
, que en Go se ha implementado durante algún tiempo utilizando soluciones de terceros. En Python, este enfoque también se usa, por ejemplo, pip tiene un directorio de
vendor
. Cuando instala pip, no se crean dependencias, y puede pensar que todo es muy bueno y que no hay dependencias, pero de hecho, todas están dentro del
vendor
.
Hace aproximadamente un año apareció go.mod (módulos Go) en Go. Esta es una nueva herramienta incorporada, pero
go get
también
go get
compatible. El proyecto contiene dos archivos:
- uno describe las dependencias con las que el proyecto trabaja directamente;
- el otro es un archivo de bloqueo, que describe absolutamente todas las dependencias y sus versiones específicas.
Esta es una solución centralizada genial.
Lo que es importante, insisten en que ciertas cosas deben verse de cierta manera. Por ejemplo, en Go, la versión debe ser semántica.
Python también tiene una especificación sobre cómo debería verse la versión. Para esto, hay PEP 440. Pero, en primer lugar, la especificación es muy complicada: no solo hay tres componentes de versión (números), sino también pre-lanzamiento, post-lanzamiento y era (cuando cambia la forma de versionar). En segundo lugar, PEP 440 no fue aceptado de inmediato, sino que también lo hicieron de forma incremental, por lo tanto, la versión heredada es compatible, lo que significa que cualquier cosa se puede usar como versión, cualquier línea como "¡Hola mundo!".
- Dijiste que la comunidad desarrolla el lenguaje de forma incremental, por lo que hay una gran cantidad de soluciones. Pero, ¿por qué no deshacerse de toda esta basura? ¿Por qué no tirar los Distutils, abandonar lo viejo e innecesario que nadie está usando y, en cambio, introducir activamente nuevas prácticas y herramientas?Mantener todo esto tiene sentido, de modo que aún pueda instalar los paquetes antiguos. Es imposible insistir en que es necesario hacer eso, y no de otra manera, porque la decisión la toma la comunidad. Ninguno de los desarrolladores de Core Python viene y dice: "Eso es todo, estamos haciendo todo ahora y sin clavos".
Go tiene todo lo que necesita para trabajar con dependencias de inmediato. En Python, debe reinstalar todo desde el exterior, y aún debe comprender qué es exactamente. Muy a menudo, pip es suficiente, pero ahora aparecen otras opciones.
En el sitio con recomendaciones de paquetes oficiales de Python Packaging Authority, el grupo que hace pip, pipenv, PyPI, está escrito para usar pipenv. Con pipenv es otra historia. En primer lugar, tiene una resolución pobre. En segundo lugar, no hubo lanzamientos durante mucho tiempo y la comunidad ya está esperando que los creadores admitan honestamente que este proyecto está muerto. El tercer problema con pipenv es que es adecuado solo para proyectos, pero no para paquetes: puede especificar dependencias del proyecto en pipenv, pero no puede especificar su nombre, versión y, en consecuencia, ponerlo en un paquete para descargarlo en PyPI. Resulta que seguir las recomendaciones de Python Packaging Authority y usar pipenv todavía no es suficiente para resolverlo.
La poesía está tratando de ser revolucionaria. Básicamente no genera el archivo setup.py, lo que sería útil para la compatibilidad con versiones anteriores, porque Poetry quiere ser el formato nuevo y único para todo. Él sabe cómo recopilar paquetes, y tiene un archivo de bloqueo, que es necesario para los proyectos. Sin embargo, la poesía tiene muchas cosas extrañas, muchas características familiares no son compatibles.
- ¿Cuál crees que es el futuro del ecosistema en términos de trabajar con dependencias? Tu predicciónTodo está mejorando más o menos. Por ejemplo, vi una vacante en pip, y un desarrollador que lo pone en orden se le promete mucho dinero. Quizás pip se convertirá en una solución más universal. Pero necesita que alguien lo tome en serio: venga y diga que lo estamos haciendo de esa manera, ahora estamos siguiendo una PEP más estricta e insistiremos en su cumplimiento (porque la PEP es solo una recomendación de que nadie realmente no es obligatorio seguir)
Por ejemplo, teníamos una historia así: cierta versión de PyYAML estaba bloqueada en el archivo de bloqueo. Un día, las pruebas en CI pasan, implementamos en producción, y todo cae allí, porque no se encontró la versión PyYAML. La cuestión era que la versión bloqueada se eliminó de pypi.org. Todos estaban indignados, actualizaron el archivo de bloqueo, de alguna manera sobrevivieron, pero el sedimento permaneció.
No hace mucho tiempo, apareció PEP 592; ya se ha adoptado y se mantiene en pip, en el que aparecieron lanzamientos arrancados. Yank significa que el lanzamiento aún no se ha eliminado por completo de pypi.org, está oculto. Es decir, si especifica que necesita, por ejemplo, una versión de PyYAML superior a 3.0, pip se saltará las versiones arrancadas e instalará la última disponible. Pero si se indica una versión específica en el archivo de bloqueo, y esta versión es yank, entonces pip la instalará de todos modos. Por lo tanto, los archivos bloqueados y la implementación no se romperán, pero, si es posible, no se utilizará la versión anterior.
La segunda cosa interesante es PEP para
__pymodules__
. Estos son entornos virtuales livianos: abre el directorio del proyecto, escribe
pip install
PyYAML, y PyYAML se instala no globalmente, sino en el directorio
__pymodules__
. Cuando Python comienza en este directorio, importa PyYAML no globalmente, sino desde este directorio.
Lo llamo entornos virtuales como mínimo, porque hay menos aislamiento. Por ejemplo, no hay acceso a archivos binarios. Cuando se activa un entorno virtual con pytest instalado, se puede usar desde la consola: simplemente escriba pytest y haga algo. Con
__pymodules__
estarán disponibles para la importación, pero no los binarios, ya que en realidad no se instalarán.
Este PEP está diseñado para facilitar a los principiantes. Para que no tengan que lidiar con todas las complejidades de los entornos virtuales, simplemente instalen todo lo que necesitan en
__pymodules__
través de la instalación pip.
- Bueno, el futuro en tu pronóstico es más brillante que ahora.Sí, pero como dije, si nadie viene y dice que estamos rehaciendo e intentando desechar el legado, entonces los problemas continuarán. Ahora estamos acumulando y acumulando herramientas, y será imposible deshacerse de cualquiera de ellas en el futuro cercano.
- ¿Qué piensa usted? ¿Por qué ninguno de los desarrolladores puede actualizar las dependencias? Casi en ninguna parte, ni en las empresas ni en el código abierto, se ha construido el proceso de trabajar con versiones de seguridad, en principio, con nuevas versiones menores o mayores. ¿Dónde ves los problemas aquí?Como mínimo, cuando desea actualizar las dependencias, da miedo actualizar todas las dependencias, porque no es un hecho que incluso si pasa las pruebas, todo funcionará. Por ejemplo, a menudo esta situación surge con el apio, porque el apio no se puede probar completamente en las pruebas. Puede bloquear algo, simplificar algo, pero no se puede verificar el hecho de que los trabajadores se estén ejecutando.
Go work with tests está bien implementado, incluso en los tutoriales de Go Modules está escrito cómo actualizar dependencias: actualiza ciertas dependencias y ejecuta pruebas. Además, las pruebas ejecutan no solo las suyas, sino también esta dependencia.
Todavía vale la pena mencionar un aspecto interesante: ¿las pruebas deberían estar en paquetes en Python? Cuando descarga un paquete de pypi.org, ¿debería haber pruebas? En teoría, deberían, e incluso tener un mecanismo para ejecutarlos: en setup.py puede especificar cómo ejecutar pruebas, qué dependencias tienen.
Pero, en primer lugar, muchas personas no saben cómo ejecutarlos y no realizan pruebas que son dependientes. Por lo tanto, a menudo no son necesarios. En segundo lugar, a menudo estas pruebas tienen accesorios muy difíciles y, por lo tanto, incluir pruebas en un paquete significa hacer que el paquete sea 6-10 veces más grande.
Sería genial poder descargar un paquete con pruebas y sin pruebas. Pero ahora no existe tal posibilidad, por lo que las pruebas a menudo no cuadran dentro de los paquetes. Hay caos, y ni siquiera sé si es posible ejecutar pruebas de estas dependencias al actualizar las dependencias.
Este aspecto parece ser mayormente pasado por alto. Pero en algunos otros idiomas, en particular, Go se considera una buena práctica, actualizar un paquete en el entorno e inmediatamente ejecuta pruebas para asegurarse de que en este entorno este paquete funcione bien.
- ¿Por qué, en su opinión, en Python, las herramientas para las versiones semánticas automáticas no son populares?Creo que uno de los problemas es que la versión se puede describir en muchos lugares. La mayoría de las veces hay tres: el formato de descripción de metadatos del proyecto (pypi.org, poety, setup.py, etc.), dentro del proyecto en sí y en la documentación. Actualizar una versión en tres lugares no es muy difícil, pero fácil de olvidar.
DepHell tiene un equipo para actualizaciones de versiones. 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++.