#!/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? # # # #
[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! `