#!/usr/bin/env python # coding: utf-8 # ![Python packaging: Lo estás haciendo mal](img/cover.jpeg) # # ## 2021-03-04 @ Python Madrid # # Resumen # # 1. Introducción # 2. Brevísima historia de una pesadilla # 3. Cómo instalar paquetes # 4. Cómo especificar dependencias para aplicaciones # 5. Cómo distribuir código # 6. Conclusiones y preguntas # ### Preguntas hechas con antelación en el Meetup # # --- # # Casos de uso para: # # - Installation: python setup.py install # - Software dist: python setup.py sdist # - Binary dist: python setup.py bdist # # --- # # Compatibilidad poetry vs buildpacks, gracias! # # https://github.com/heroku/heroku-buildpack-python/issues/796 # # --- # # ¿Por qué todavía seguimos diferenciando entre librería y aplicación? # ¿Cuándo estará disponible para su uso general lo especificado en el PEP631? # ¿Qué es un Platypus? # # --- # # Cómo referenciar paquetes privados para que puedan instalarse con pip en distintos entornos de CI/CD # # --- # # Integración y despliegue continuo, cómo organizarlo en grandes empresas, artefactory, Jenkins, etc. convivencia con otros proyectos, puesta en producción. # # --- # # Instalación para Alpine linux mini root fs para containers! # # --- # # * tips para pasar un poyecto legacy en pip a poetry (requirements a toml). # * Manejo de multilenguaje y compilado de diccionarios en la instalación # # --- # # Como instalar una libreria de manera agnostica con el host?, me refiero a instalar una libreria en windows pero con arquitectura x86 linux. # # Ya que tuve un proyecto y la unica solucion fue descargar una ISO de un ubuntu server x86 y ahi hacer el pip install (en ese caso fue PARAMIKO) ya que donde yo ejecutaba mi Script tenian ese SO y yo desarrollaba en una MAC y me marcaba error por los HEADERS del OS. # # ¿Quién es este? # # ¡Yo! # # # # Sígueme! https://github.com/astrojuanlu/ # #
# # Una aclaración importante # # ![Paternalista](img/paternalista.jpg) # - "Lo estás haciendo mal" es un título condescendiente a propósito a modo de broma # - Hay valor en tener unas buenas prácticas comunes: reducir la fricción entre proyectos y poder dedicarnos a lo importante # # Brevísima historia de una pesadilla # # ## O «cómo aprendimos a odiar pip» # ![Jannis Leidel, 2009](img/jezdez2009.png) # ![Martin Natano, 2014](img/martinnatano2014.png) # !["Una abominación"](img/fperez_org2012.png) # ### 2013 # # ![¡Camaradas!](img/python_comrades-orig.png) # !["Malware"](img/dwf2015.png) # ### ??? # # ![¿Camaradas...?](img/python_comrades-edit.png) # https://www.slideshare.net/misterwang/pydata-past-present-future-pydata-sv-2014-keynote # # ![Vamos, un lío](img/this-packaging-problem.jpg) # ![Empecemos de nuevo](img/shaking-head-2.gif) # # Cómo instalar paquetes # # ![Nivel básico](img/charmander.webp) # ✨✨✨ _Instale el paquete deseado en dos sencillos pasos:_ ✨✨✨ # # ``` # $ source .venv/bin/activate # (.venv) $ python -m pip install ipython # ``` # > «¿Y no había otra cosa que se llamaba `easy`...?» # # Hagamos como que nunca existió 😇 # # ![easy_install ha muerto](img/codewithanthony2020.png) # > «Sí, pero... ¡es que yo lo quiero en uno!» # # Mal menor: pip nuevo ⚠️⚠️⚠️ # # ``` # $ pip install ipython # Hmmm... # ``` # # Por defecto se instala en `${HOME}/.local/lib` (a escondidas se ha usado `--user`). ¿Queremos eso? # ``` # In [1]: import sys # # In [2]: sys.prefix # Out[2]: '/usr' # # In [3]: import urllib3 # # In [4]: urllib3.__file__ # Out[4]: '/usr/lib/python3/dist-packages/urllib3/__init__.py' # # In [3]: import IPython # # In [4]: IPython.__file__ # Out[4]: '/home/juanlu/.local/lib/python3.8/site-packages/IPython/__init__.py' # ``` # # Ya tenemos dos localizaciones distintas 😨 # > «Sí, pero... ¡es que yo lo quiero en uno!» # # Catástrofe en ciernes: pip viejo 🚨🚨🚨 # # ``` # $ pip install urllib3 # Hmmm... # ``` #
[Errno 13] Permission denied: '/usr/lib/python3.8'
# ``` # $ sudo pip install urllib3 # NOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO # ``` # ![Catástrofe](img/nuclear-explosion.gif) # - Hacer `sudo pip install` 🚫 puede desestabilizar dependencias críticas de tu sistema # - Nótese que en Windows esto no es un problema # - Para desarrollo, **nunca jamás se usa el Python del sistema** # - ¡Siempre utilizar un **entorno virtual**! # Opciones: # # - `venv` https://docs.python.org/3/library/venv.html ¡en la biblioteca estándar desde Python 3.3 (2012)! # - `virtualenv` https://pypi.org/project/virtualenv/, más rápido y flexible ([recientemente resucitado por Bernát Gábor](https://discuss.python.org/t/virtualenv-20-0-0-beta1-is-available/3077?u=astrojuanlu)) # Otras opciones: # # - `virtualenvwrapper` no tiene actividad desde febrero de 2019 😓 (¡aunque aún funciona!) # - `pyenv` rompe el `$PATH` y algunos scripts 😓 https://github.com/pyenv/pyenv/issues/1112 # - `Pipenv` [trata de hacer demasiadas cosas](https://hynek.me/articles/python-app-deps-2018/#pipenv), [avanza con mucho esfuerzo](https://discuss.python.org/t/work-towards-next-pipenv-release/3781?u=astrojuanlu), y [tiene ciertos problemas](https://chriswarrick.com/blog/2018/07/17/pipenv-promises-a-lot-delivers-very-little/) # - `Poetry` nunca lo usé 🤔 # > ¿¿Y qué pasa con Alpine?? # # ``` # $ docker run -it --rm --name pip-wheel python:3.8-alpine sh # / # python -m pip install numpy # Collecting numpy # Downloading numpy-1.20.1.zip (7.8 MB) # |████████████████████████████████| 7.8 MB 10.8 MB/s # Installing build dependencies ... done # Getting requirements to build wheel ... done # Preparing wheel metadata ... error # ``` #

#     ERROR: Command errored out with exit status 1:
#      command: /usr/local/bin/python /usr/local/lib/python3.8/site-packages/pip/_vendor/pep517/_in_process.py prepare_metadata_for_build_wheel /tmp/tmpzz6m5yah
#          cwd: /tmp/pip-install-9mlwuba9/numpy
#     Complete output (226 lines):
# ...
#       File "numpy/core/setup.py", line 676, in get_mathlib_info
#         raise RuntimeError("Broken toolchain: cannot link a simple C program")
#     RuntimeError: Broken toolchain: cannot link a simple C program
# 
# # 🤯 # - Normalmente, pip descarga un [_Source Distribution_ o "sdist"](https://packaging.python.org/glossary/#term-Source-Distribution-or-sdist), que contiene todo lo necesario para instalar el paquete # - Sin embargo, "todo lo necesario" a veces puede incluir código en C, C++, FORTRAN, Rust, JavaScript... # - Y por tanto, ¡**necesito herramientas _que no se instalan con pip_**! # - [El PEP 427 define el formato `wheel`](https://www.python.org/dev/peps/pep-0427/) que resuelve este problema ofreciendo un [_Built Distribution_](https://packaging.python.org/glossary/#term-Built-Distribution) # - ¡Simplemente extraer archivos! Más rápido de instalar, y ✨ no necesita nada más que pip ✨ # - El problema es que ¡este formato no es multiplataforma! # El formato del nombre del archivo indica la compatibilidad: # #
{distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl
# # Es decir: # # - `distribution`: Nombre del paquete (`Django`, `numpy`) # - `version`: Versión (compatible con el [PEP 440](https://www.python.org/dev/peps/pep-0440/)) # - `python tag` y `abi tag`: Implementación de Python (`cp38`, `pp372-pypy3_72`) # - `platform`: **Plataforma** # Por ejemplo: # # ``` # numpy-1.20.1-cp38-cp38-manylinux1_x86_64.whl # ``` # ..."many _what_"??? # > For Linux, the situation is much more delicate. # # [PEP 513 -- A Platform Tag for Portable Linux Built Distributions](https://www.python.org/dev/peps/pep-0513/) # # - No hay "un" Linux, hay muchos # - Hay varias causas (explicadas en el PEP) de incompatibilidad entre binarios de Linux # - La más importante, _dependencia dinámica en diferentes versiones de la GNU C Library, `glibc`_ # - El PEP 513 estandariza unas versiones mínimas, basadas en CentOS 5.11, y le da el nombre `manylinux1` # - Después vinieron `manylinux2010` (CentOS 6), `manylinux2014` (CentOS 7), y vendrán más # - ...pero es que Alpine **no usa `glibc`**, sino `musl`! 😭😭😭 # - Hay un [pre-PEP para crear wheels para `musl`](https://discuss.python.org/t/pre-pep-platform-tag-for-linux-distributions-using-musl/7165?u=astrojuanlu), pero de momento hay que usar métodos alternativos # En resumen: # # - `python -m pip install` + `venv` = ❤️ # - pip está bien mantenido y está recibiendo muchas mejoras ❤️ # - ¡Cuidado con los wheels! # # Cómo especificar dependencias para aplicaciones # # ![Nivel intermedio](img/charmeleon.webp) # # ✨✨✨ _Especifique las dependencias deseadas en dos sencillos pasos:_ ✨✨✨ # # ``` # (.venv) $ echo "django<3" > requirements.in # (.venv) $ python -m pip install pip-tools && pip-compile # ``` # [pip-compile](https://pypi.org/project/pip-tools/) toma como entrada un archivo `requirements.in` y produce un `requirements.txt` con todas las versiones fijadas: # # ``` # (.venv) $ cat requirements.txt # # # # This file is autogenerated by pip-compile # # To update, run: # # # # pip-compile # # # django==2.2.19 # # via -r requirements.in # pytz==2021.1 # # via django # sqlparse==0.4.1 # # via django # ``` # # Perfecto para tener un entorno reproducible 💯 # Una vez fijadas, ¡por defecto no se actualizan! # # ``` # (.venv) $ echo "django" > requirements.in # (.venv) $ pip-compile # # # # This file is autogenerated by pip-compile # # To update, run: # # # # pip-compile # # # django==2.2.19 # # via -r requirements.in # pytz==2021.1 # # via django # sqlparse==0.4.1 # # via django # ``` # # Se acabaron los pipelines rotos por nuevas versiones ✨ # Solo se actualizan si lo pedimos: # # ``` # (.venv) $ pip-compile -P django # # # # This file is autogenerated by pip-compile # # To update, run: # # # # pip-compile # # # asgiref==3.3.1 # # via django # django==3.1.7 # # via -r requirements.in # pytz==2021.1 # # via django # sqlparse==0.4.1 # # via django # ``` # Y, para poner nuestro entorno al día: # # ``` # (.venv) $ pip-sync # Collecting asgiref==3.3.1 # Using cached asgiref-3.3.1-py3-none-any.whl (19 kB) # Collecting django==3.1.7 # Using cached Django-3.1.7-py3-none-any.whl (7.8 MB) # Collecting pytz==2021.1 # Using cached pytz-2021.1-py2.py3-none-any.whl (510 kB) # Collecting sqlparse==0.4.1 # Using cached sqlparse-0.4.1-py3-none-any.whl (42 kB) # Installing collected packages: sqlparse, pytz, asgiref, django # Successfully installed asgiref-3.3.1 django-3.1.7 pytz-2021.1 sqlparse-0.4.1 # ``` # ...que es casi lo mismo que hacer: # # ``` # (.venv) $ python -m pip install -r requirements.txt # Requirement already satisfied: asgiref==3.3.1 in ./.venv/lib/python3.8/site-packages (from -r requirements.txt (line 7)) (3.3.1) # Requirement already satisfied: django==3.1.7 in ./.venv/lib/python3.8/site-packages (from -r requirements.txt (line 9)) (3.1.7) # Requirement already satisfied: pytz==2021.1 in ./.venv/lib/python3.8/site-packages (from -r requirements.txt (line 11)) (2021.1) # Requirement already satisfied: sqlparse==0.4.1 in ./.venv/lib/python3.8/site-packages (from -r requirements.txt (line 13)) (0.4.1) # ``` # # 😍😍😍 # Otras alternativas: # # - Ya hemos hablado de `Pipenv` # - No puedo agregar mucho sobre `Poetry` # - ~`setup.py`~ ¡Calma! No hemos llegado aún, [y además no es exactamente para esto](https://caremad.io/posts/2013/07/setup-vs-requirement/) # En resumen: # # - `pip-compile` + `pip-sync` (o simplemente `pip`) = ❤️ # - _Todo el mundo_ entiende `requirements.txt`, ¡usémoslo! # - Básicamente, haced caso a Hynek https://hynek.me/articles/python-app-deps-2018/ # # Cómo distribuir código # # ![Nivel avanzado](img/charizard.webp) # ✨✨✨ _Distribuya su código en tres sencillos pasos:_ ✨✨✨ # # ``` # (.venv) $ cat saludo.py # Este no cuenta ;) # """ # Un saludo # """ # # __version__ = "0.1.0" # # print("¡Hola, mundo!") # (.venv) $ python -m pip install flit && flit init # (.venv) $ python -m pip install build && python -m build # (.venv) $ python -m pip install twine && twine upload dist/* # ``` # Pero, ¿qué es flit, y qué está pasando aquí? # # ``` # (.venv) $ ls # dist LICENSE pyproject.toml saludo.py # ``` # **¡No hay `setup.py`!** 😮 Rebobinemos por un momento... # - Tradicionalmente siempre se han usado archivos `setup.py` para especificar **los metadatos** del proyecto # - Estos archivos suelen empezar así: # # ```python # from setuptools import setup # # setup( # ... # ``` # # - ...pero, como hemos visto arriba, `setuptools` tiene _ejem_ algunos detractores # - Necesitamos eliminar el acoplamiento entre el _**backend**_ (`setuptools`) y el _**frontend**_ (`pip`) # > «¿No había otra cosa que se llamaba...?» # # Elevemos una oración por su alma 😇 # # ![distutils ha muerto](img/zooba2020.png) # - El [PEP 518](https://www.python.org/dev/peps/pep-0518/) estandariza una forma de especificar las **dependencias en tiempo de instalación**, que se escriben en el nuevo `pyproject.toml` # - ¡No confundir con las _dependencias en tiempo de ejecución_! # - Y el [PEP 517](https://www.python.org/dev/peps/pep-0517/) estandariza una forma de especificar **cómo el frontend tiene que llamar al backend** # # ```toml # [build-system] # requires = ["setuptools", "wheel"] # PEP 518 # build-backend = "setuptools.build_meta" # PEP 517 # ``` # # (Para más información, [Brett Cannon lo explica muy bien](https://snarky.ca/clarifying-pep-518/)) # Por tanto, en lugar de hacer el antiguo: # # ``` # (.venv) $ python setup.py install # Buuuuuuuuuuuuuu 👎 # ``` # ¡Ahora podemos hacer esto! # # ``` # (.venv) $ python -m pip install . # ``` # # Y `pip` ya sabe hacer el resto 🎉 # De paso, ¡`python -m pip uninstall` funciona! ✨ # Insisto: dejad de ejecutar `setup.py`. Haced caso a Paul, es muy majete. # # ![Nunca uses setup.py](img/pganssle2020.png) # Equivalencias: # # - `python setup.py install` -> `python -m pip install .` # - `python setup.py develop` -> `python -m pip install -e .` (["instalaciones editables"](https://pip.pypa.io/en/latest/reference/pip_install/#editable-installs), ¡muy útiles!) # - `python setup.py sdist bdist_wheel` -> `python -m build` (en seguida lo explicamos) # - Algunos `setup.py` son **endiablados** 😵 (por ejemplo el de [SciPy](https://github.com/scipy/scipy/blob/master/setup.py)) - por suerte, `setuptools` permite todo esto y más # - Si necesitas `setuptools`, es recomendable usar [metadatos estáticos en `setup.cfg`](https://setuptools.readthedocs.io/en/latest/userguide/quickstart.html#basic-use): # # ``` # [metadata] # name = mypackage # version = 0.0.1 # # [options] # packages = mypackage # install_requires = # requests # importlib; python_version == "2.6" # ``` # # - Sin embargo, ¡muchos proyectos no necesitan esa complejidad! # Y para eso tenemos [`flit`](https://flit.readthedocs.io/): # # ``` # (.venv) $ flit init # Module name [saludo]: # Author [Juan Luis Cano Rodríguez]: # Author email [hello@juanlu.space]: # Home page [https://github.com/astrojuanlu/python-saludo]: # Choose a license (see http://choosealicense.com/ for more info) # 1. MIT - simple and permissive # 2. Apache - explicitly grants patent rights # 3. GPL - ensures that code based on this is shared with the same terms # 4. Skip - choose a license later # Enter 1-4 [1]: # # Written pyproject.toml; edit that file to add optional extra info. # ``` # ``` # (.venv) $ cat pyproject.toml # [build-system] # requires = ["flit_core >=2,<4"] # build-backend = "flit_core.buildapi" # # [tool.flit.metadata] # module = "saludo" # author = "Juan Luis Cano Rodríguez" # author-email = "hello@juanlu.space" # home-page = "https://github.com/astrojuanlu/python-saludo" # classifiers = [ "License :: OSI Approved :: MIT License",] # ``` # # ¡Y nada más! ✨ # Y ha llegado el momento de la verdad: generamos nuestras _Source Distribution_ y _Built Distribution_ usando [`build`](https://pypa-build.readthedocs.io/): # # ``` # (.venv) $ python -m build # (.venv) $ ls dist/ # saludo-0.1.0-py2.py3-none-any.whl saludo-0.1.0.tar.gz # ``` # ...¡y las subimos a [PyPI](https://pypi.org/) usando [`twine`](https://twine.readthedocs.io/)! # # ``` # (.venv) $ twine upload dist/* # ``` # # ![PyPI](img/pypi-logo.svg) # (¡o al [PyPI interno de tu empresa](https://pypi.org/project/pypiserver/)!) # > «¿Significa eso que, dependiendo de si elijo `setuptools`, `flit`, o `poetry`, tengo que especificar los metadatos de una manera totalmente distinta?» # # Sí 😓 # ...¡pero no por mucho tiempo! # # ![PEP 621](img/brettsky2021.png) # - El [PEP 621](https://www.python.org/dev/peps/pep-0621/) estandariza [cómo especificar los metadatos del proyecto en `pyproject.toml`](https://packaging.python.org/specifications/declaring-project-metadata/) # - ¡Se escribió con `setuptools`, `flit`, y `poetry` en mente! # - Aún no tiene implementación de referencia... # - ...pero [Thomas Kluyver abrió un pull request para `flit` hace pocos días](https://github.com/takluyver/flit/pull/393) 😍 # Alternativas: # # - ¡`Poetry`! # - `enscons` (poca actividad en GitHub) # - `scikit-build` o `mesonpep517` para código con extensiones en C, C++, FORTRAN u otros # - ? # En resumen: # # - `flit` + `build` + `twine` = ❤️ # - Para las cosas complicadas, siempre estará `setuptools` # - **No importa qué sistema usemos, pronto `pyproject.toml` será parecido en todos** # ✨ Resumen del resumen: https://github.com/astrojuanlu/cookiecutter-pylib ✨ # # Conclusiones # # - ¡Larga vida a `pip`! 🎉 Y nunca jamás nombrar a `easy_install` 🚫 # - ¡Larga vida a `venv`! 🎉 Y nunca jamás hacer `sudo pip install` 🚫 # - ¡Larga vida a los wheels! 🎉 Y si no hay para tu plataforma, tendrás que compilarlo o buscar una alternativa 🔨 # - ¡Larga vida a `flit`! 🎉 Y, si lo necesitas, puedes seguir usando `setuptools` ✔️ # - No te compliques la vida 😉 https://github.com/astrojuanlu/cookiecutter-pylib # - ¡Y nos mantenemos en contacto! `` # # ## ¡Muchas gracias! # # [![El ornitorrinco de la paquetería](img/platypus.png)](https://discuss.python.org/t/the-packaging-platypus/1939?u=astrojuanlu)