Desarrollando el paquete pypi perfecto con soporte para diferentes versiones de python

Este es un pequeño manual / historia sobre cómo crear un paquete pypi "perfecto" para python, que cualquiera puede instalar con el preciado comando:


pip install my-perfect-package 

Está dirigido a principiantes, pero insto a los profesionales a expresar su opinión sobre cómo mejorar el paquete "perfecto". Por lo tanto, pido gato.


¿Qué significa un paquete "ideal"?


Procederé de los siguientes requisitos:


  • Código abierto en github;
    Todos deberían poder contribuir al desarrollo y agradecer al autor.
  • Soporte para todas las versiones actuales / populares de python (2.7, 3.5, 3.6, 3.7, 3.8);
    Las pitones son diferentes, y en algún lugar todavía escriben activamente en 2.7.
  • 100% de cobertura con pruebas unitarias;
    Las pruebas unitarias mejoran la arquitectura y automatizan las comprobaciones de regresión.
    Una insignia con un número apreciado genera preguntas frecuentes y establece el listón para otros .
  • Usando CI:
    ¡Los controles automáticos son muy convenientes! Y un montón de insignias geniales
    • Ejecución de pruebas unitarias en todas las plataformas y en todas las versiones de python;
      No les creas a quienes afirman que Python y los paquetes instalados son multiplataforma , porque siempre puedes encontrar un error .
    • Verificación del código de estilo;
      Un estilo uniforme mejora la legibilidad y reduce el número de discusiones vacías en una revisión.
    • Analizador de código estático;
      ¿Búsqueda automática de errores en el código? Dame dos!
  • Documentación real;
    Ejemplos de trabajo con el paquete, descripción de métodos / clases y análisis de errores típicos: la experiencia documentada reducirá el umbral de entrada para principiantes.
  • Desarrollo multiplataforma;
    Desafortunadamente, es difícil hacer una contribución personal a los proyectos simplemente porque el desarrollador agudizó las herramientas para Unix. Por ejemplo, usé scripts bash para ensamblar.
  • El paquete es útil y hace del mundo un lugar mejor.
    Un requisito difícil, ya que a juzgar por el número de paquetes en pypi (~ 210k), los desarrolladores son altruistas salvajes y ya se ha escrito mucho.

Por donde empezar


No había buenas ideas, así que elegí un tema trillado y muy popular: trabajar con sistemas numéricos. La primera versión debería poder traducir números al romano y viceversa. Para el manual es más complicado y no necesario. Oh sí, lo más importante es el nombre: numsys: como descifrado de sistemas numéricos. numeral-system-py .


¿Cómo hacer la prueba?


Tomé python3.7 y primero escribí pruebas con stubs de función (todos somos TDD ) usando el módulo de pruebas unitarias estándar.


Realizo la siguiente estructura de proyecto:


 src/ numeral-system/ __init__.py roman.py tests __init__.py test_roman.py 

No pondré pruebas en el paquete, así que me separo paja . Inicialmente, no creé la carpeta src/ , pero el desarrollo posterior demostró que es más conveniente para mí operar. Esto no es necesario, por lo tanto, a voluntad.


Decidí usar pytest para el lanzamiento: sabe cómo funcionar perfectamente con las pruebas del módulo estándar. Puede parecer un poco ilógico, pero el módulo estándar para pruebas para mí parece Parecía un poco más cómodo. Ahora recomendaría usar el estilo de escritura pytest.


Pero, dicen que poner pytest (como cualquier otra dependencia) en el sistema python no es una idea muy inteligente ...


¿Cómo gestionar las dependencias?


Solo se pueden usar virtualenv y required.txt . Puedes ser progresivo y usar poesía . Yo, tal vez, usaré tox , una herramienta para simplificar la automatización y las pruebas, que también me permitirá administrar las dependencias.


Creo una configuración simple de tox.ini e instalo pytest :


 [tox] envlist = py37 ;     ,   python3.7 [testenv] ;     deps =  `deps`  ,   . -r requirements.txt ;     -r requirements-test.txt ; commands = pytest ;   

Inicialmente, indiqué explícitamente las dependencias, pero la práctica de integración con servicios de terceros demostró que la mejor manera sería almacenar las dependencias en el archivo require.txt.


Surge un momento muy sutil. ¿Corregir la versión actual en el momento del desarrollo o poner siempre la última?


Si se compromete, durante la instalación puede haber conflictos entre paquetes debido a las diferentes versiones de las dependencias utilizadas. Si no se compromete, el paquete puede dejar de funcionar repentinamente. La última situación es muy desagradable para los productos finales, cuando todas las compilaciones pueden "volverse rojas" en una noche debido a una actualización menor de la dependencia implícita. Y de acuerdo con la ley de Murphy, esto sucederá el día del lanzamiento.


Por lo tanto, desarrollé una regla para mí:


  1. Siempre corrija la versión para los productos finales, ya que las versiones a usar son su responsabilidad.
  2. No repare la versión utilizada para los paquetes instalados. O limítelo a un rango si la funcionalidad del paquete lo requiere.

Que sigue


Estoy escribiendo pruebas!


Completo el cuerpo de las funciones, agrego comentarios y hago que las pruebas se ejecuten correctamente.
En este punto, la mayoría de los desarrolladores generalmente se detienen (todavía creo que todos escriben tests =), publican el paquete y piratean errores. Pero sigo adelante y saltar a la madriguera del conejo .


¿Cómo trabajar con diferentes versiones de python?


En la configuración de tox , especifico el lanzamiento de pruebas en todas las versiones interesantes de python:


 [tox] envlist = py{27,35,36,37,38} 

Utilizando pyenv, entrego las versiones necesarias para mí localmente para que tox pueda encontrarlas y crear entornos de prueba.


¿Dónde están los apreciados al 100%?


Agregaré una medida de la cobertura del código; para esto hay un excelente paquete de cobertura y una integración no menos excelente con pytest: pytest-cov .
requirements-test.txt ahora se ve así:


 six=1.13.0 pytest=4.6.7 pytest-cov=2.8.1 parameterized=0.7.1 

De acuerdo con la regla anterior, reparo las versiones de los paquetes que se utilizan para ejecutar pruebas.


Cambio el comando de ejecución de prueba:


 deps = -r requirements.txt -r requirements-test.txt commands = pytest \ --cov=src/ \ --cov-config="{toxinidir}/tox.ini" \ --cov-append 

Estoy recopilando estadísticas de cobertura para todo el código de la carpeta src/ - el paquete en sí ( numeral_system / ) y requerido para el código de prueba ( tests / ) - ¿No quiero que las pruebas contengan partes no ejecutables?


--cov-append comando --cov-append todas las estadísticas recopiladas para cada llamada en una versión diferente de python en una, porque la cobertura para la segunda y tercera python puede ser diferente (¡hola código dependiente de la versión y módulo seis !), Pero en total da 100 % Un simple ejemplo:


 if sys.version_info > (3, 0): # Python 3 code in this block else: # Python 2 code in this block 

Agregue un nuevo entorno para crear un informe de cobertura.


 [testenv:coverage_report] deps = coverage commands = coverage html ;   ,   coverage report --include="src/*" --fail-under=100 -m ;    100%  

Y agrego a la lista de entornos después de ejecutar las pruebas en todas las versiones de python.


 [tox] envlist = py{27,35,36,37,38} coverage_report 

Después de ejecutar el comando tox , la carpeta tox contiene el archivo index.html con un hermoso informe debe aparecer en la raíz del proyecto.


Para la codiciada insignia, integro 100% con el servicio codecov , que ya se integrará con github y le permitirá ver el historial de cambios en la cobertura del código. Para hacer esto, por supuesto, tendrá que crear una cuenta allí.


El entorno de lanzamiento final es el siguiente:


 [testenv:coverage_report] deps = coverage==5.0.2 codecov==2.0.15 commands = coverage html coverage report --include="src/*" --fail-under=100 -m coverage xml codecov -f coverage.xml --token=2455dcfa-f9fc-4b3a-b94d-9765afe87f0f ;     codecov,    

Ahora solo queda agregar un enlace a la insignia en README.rst :


 |Code Coverage| .. |Code Coverage| image:: https://codecov.io/gh/zifter/numeral-system-py/branch/master/graph/badge.svg :target: https://codecov.io/gh/zifter/numeral-system-py 

¿Cómo formatear y analizar código?


Muchos analizadores no existen porque, en su mayor parte, se complementan entre sí. Por lo tanto, integraré analizadores estáticos populares que verificarán el cumplimiento de PEP8 , encontrarán problemas potenciales y arreglar todos los errores formatee uniformemente el código.


Debe considerar inmediatamente dónde especificar los parámetros para ajustar los analizadores. Para hacer esto, puede usar el archivo tox.ini , setup.cfg , un único archivo personalizado o archivos específicos para analizadores. Decidí usar tox.ini directamente, ya que puedes copiar tox.ini para futuros proyectos.


isort


isort es una utilidad para formatear importaciones.


Creo el siguiente entorno para ejecutar isort en modo de formato de código.


 [testenv:isort] changedir = {toxinidir}/src deps = isort==4.3.21 commands = isort -y -sp={toxinidir}/tox.ini 

Desafortunadamente, isort no puede especificar una carpeta para formatear. Por lo tanto, debe cambiar el directorio de inicio a través de changedir y especificar la ruta al archivo con la configuración -sp={toxinidir}/tox.ini . El -y es necesario para deshabilitar el modo interactivo.


Para ejecutar las pruebas, necesita un modo de verificación, para esto hay un indicador de --check-only verificación:


 [testenv:isort-check] changedir = {toxinidir}/src deps = isort==4.3.21 commands = isort --check-only -sp={toxinidir}/tox.ini 

negro


A continuación, me integro con el formateador de código negro . Lo hago por analogía con isort :


 [testenv:black] deps = black==19.10b0 commands = black src/ [testenv:black-check] deps = black==19.10b0 commands = black --check src/ 

Todo funciona bien, pero hay un conflicto con isort : hay una diferencia en el formato de las importaciones .


En uno de los comentarios encontré una configuración de isort mínimamente compatible, que utilicé:


 [isort] multi_line_output=3 include_trailing_comma=True force_grid_wrap=0 use_parentheses=True line_length=88 

escama8


A continuación, me integro con los analizadores estáticos flake8 .


 [testenv:flake8-check] deps = flake8==3.7.9 commands = flake8 --config=tox.ini src/ 

Hay problemas con la integración con black nuevamente. Tenemos que agregar un ajuste fino, que, de hecho, es recomendado por black propio black :


 [flake8] max-line-length=88 ignore=E203 

Lamentablemente, la primera vez no funcionó. Cayó con el error E231 missing whitespace after ',' , tuve que agregar este error para ignorar:


 [flake8] max-line-length=88 ignore=E203,E231 

pylint


Integrar con analizadores de código estático pylint


 [testenv:pylint-check] deps = {[testenv]deps} # pylint  ,     pylint==2.4.4 commands = pylint --rcfile=tox.ini src/ 

Inmediatamente me encuentro con restricciones extrañas: nombres de funciones de 30 caracteres (sí, escribo nombres muy largos de métodos de prueba) y advertencias sobre la presencia de TODO en el código.
Tengo que agregar un par de excepciones:


 [MESSAGES CONTROL] disable=fixme,invalid-name 

Además, el momento desagradable es que los desarrolladores de pylint ya pylint enterrado python2.7 y ya no están desarrollando un paquete para ello. Por lo tanto, las comprobaciones deben ejecutarse en el paquete actual para python3.7 .
Agregue la línea apropiada a la configuración:


 [tox] envlist = isort-check black-check flake8-check pylint-check py{27,35,36,37,38} coverage_report basepython = python3.7 

También es importante para ejecutar pruebas en diferentes plataformas, ya que la versión predeterminada de python en los sistemas CI es diferente.


¿Qué pasa con CI?


Transportador


Integrar con appveyor - CI para windows. La configuración inicial es simple: todo se puede hacer en la interfaz, luego descargue el archivo yaml y confírmelo en el repositorio.


 version: 0.0.{build} install: - cmd: >- C:\\Python37\\python -m pip install --upgrade pip C:\\Python37\\pip install tox build: off test_script: - cmd: C:\\Python37\\tox 

Aquí especifico explícitamente la versión de python3.7 , ya que python2.7 se usará por defecto (y tox también usará esta versión, aunque especifiqué explícitamente python3.7 ).
El enlace a la insignia, como de costumbre, se agrega a README.rst


 |Build status Appveyor| .. |Build status Appveyor| image:: https://ci.appveyor.com/api/projects/status/github/zifter/numeral-system-py?branch=master&svg=true :target: https://ci.appveyor.com/project/zifter/numeral-system-py 

Travis ci


Después de eso, me integro con Travis CI - CI en Linux (y en MacOS con Windows, pero las Python builds are not available on the macOS and Windows environments . La configuración es un poco más complicada, ya que el archivo de configuración se usará directamente desde el repositorio. Un par de iteraciones de prueba y error) la configuración está lista, aplasto en una hermosa confirmación y la solicitud de fusión está lista.


 language: python python: 3.8 # dist: xenial # required for Python 3.7 (travis-ci/travis-ci#9069) sudo: required # required for Python 3.7 (travis-ci/travis-ci#9069) addons: apt: sources: - deadsnakes packages: - python3.5 - python3.6 - python3.7 - pypy install: - pip install tox script: - tox 

( Pregunta retórica: ¿Y por qué a los proyectos de CI les gusta tanto el formato yaml? )


python3.8 la versión de python3.8 , ya que no funcionó correctamente a través del addon , y Travis CI crea con éxito virtualenv partir de la versión especificada.


Las personas familiarizadas con Travis CI pueden preguntar, ¿por qué no especificar explícitamente las versiones de Python de esta manera? Después de todo, Travis CI crea automáticamente virtualenv y ejecuta los comandos necesarios en él.


La razón es que necesitamos recopilar datos de cobertura de código de todas las versiones. Pero las pruebas se ejecutarán en diferentes trabajos en paralelo, por lo que no funcionará recopilar un informe de cobertura general.


Por supuesto, estoy seguro de que un poco más de comprensión y esto se puede solucionar.


Por tradición, también se agrega un enlace a una insignia a README.rst


 |Build Status Travis CI| .. |Build Status Travis CI| image:: https://travis-ci.org/zifter/numeral-system-py.svg?branch=master :target: https://travis-ci.org/zifter/numeral-system-py 

La documentación


Creo que cada desarrollador de Python ha usado el servicio al menos una vez: readthedocs.org . Me parece que este es el mejor servicio para alojar su documentación.
Usaré la herramienta estándar para generar la documentación de Sphinx . Sigo los pasos del manual de inicio y obtengo la siguiente estructura:


 src/ docs/ build/ #      html  source/ #     _static/ #   , ,  _templates/ #     conf.py #    index.rst #    make.bat Makefile # make     make 

A continuación, debe realizar los pasos mínimos para configurar:


  1. github por defecto sugiere crear un archivo README.md en formato Markdown , cuando como sphinx por defecto sugiere usar ReStructuredText .

Por lo tanto, tuve que reescribirlo en primer formato. Y si había leído el manual de inicio hasta el final al menos una vez, me di cuenta de que sphinx puede hacerlo en Markdown .
README.rst archivo index.rst en index.rst


 .. include:: ../../README.rst 

  1. Para generar automáticamente la documentación de los comentarios en la fuente, agrego la extensión sphinx.ext.autodoc .
  2. conf.py carpeta conf.py paquete a conf.py Esto permitirá que sphinx importe nuestro código para su análisis.
     import os import sys sys.path.insert(0, os.path.abspath('./../../src')) import numeral_system 
  3. Agrego la carpeta docs/source/api-docs y suelto el archivo de descripción para cada módulo allí. La documentación debe generarse automáticamente a partir de los comentarios:
     Roman numeral system ========================= .. automodule:: numeral_system.roman :members: 

Después de eso, el proyecto está listo para revelar su descripción al mundo. github crear una cuenta (preferiblemente a través de una cuenta en github ) e importar su proyecto, los pasos detallados se describen en las instrucciones .
Por tradición, creo un ambiente en tox :


 [testenv:gen_docs] deps = -r docs/requirements.txt commands = sphinx-build -b html docs/source/ docs/build/ 

Uso el sphinx-build explícitamente, en lugar de make , ya que no existe en Windows. Y no quiero violar el principio del desarrollo multiplataforma.


Tan pronto como se congelen los cambios, readthedocs.org recopilará automáticamente la documentación y la publicará.


Pero ... La Build failed . No comprometí las sphinx_rtd_theme sphinx y sphinx_rtd_theme , y esperaba que readthedocs.org tomara las versiones actuales. Pero esto no es así. Yo arreglo:


 sphinx==2.3.1 sphinx_rtd_theme==0.4.3 

Y creo un archivo de configuración especial .readthedocs.yml para readthedocs.org , en el que describo el entorno para iniciar la compilación:


 python: version: 3.7 install: - requirements: docs/requirements.txt - requirements: requirements.txt 

Aquí es donde el hecho fue útil de que las dependencias están en los archivos require.txt. Estoy esperando la compilación y la documentación estará disponible .


Agregue la insignia nuevamente:


 |Docs| .. |Docs| image:: https://readthedocs.org/projects/numeral-system-py/badge/?version=latest&style=flat :target: https://numeral-system-py.readthedocs.io/en/latest/ 

Licencia


Vale la pena considerar elegir una licencia para el paquete.
Este es un tema muy extenso, así que leí este artículo . Básicamente, la elección es entre MIT y Apache 2.0 . Me gustó la frase que se sacó de contexto con éxito:


 MIT       

Estoy completamente de acuerdo, lo haré. Si los planes cambian, puede cambiar fácilmente la licencia (aunque las versiones anteriores estarán bajo la anterior).


Nuevamente, agregue una insignia insignia de dios :


 |License| .. |License| image:: https://img.shields.io/badge/License-MIT-yellow.svg :target: https://opensource.org/licenses/MIT 

Aquí puede encontrar insignias para todas las licencias.


¿Cómo subir a pypi?


Primero necesitas crear una cuenta en pypi.org . Luego proceda con la preparación del paquete.


Creo setup.cfg


Es necesario describir correctamente la configuración para instalar / construir el paquete. Seguí las instrucciones . Es posible establecer datos a través de setup.py , pero no hay opciones para establecer algunos parámetros. Por lo tanto, use el archivo setup.cfg , en el que puede especificar todos los matices. Encontré una pequeña plantilla sobre cómo llenar este archivo. Como resultado, utilizo ese y ese archivo, es más conveniente.


Este archivo también se puede usar para configurar pylint , flake8 y otras configuraciones, pero no lo hice.


¿Cómo armar un paquete?


Nuevamente, estoy escribiendo un entorno que me ayudará a armar el paquete necesario:


 [testenv:build_wheel] skip_install = True deps = ;     wheel docutils pygments commands = python -c 'import shutil; (shutil.rmtree(p, ignore_errors=True) for p in ["build", "dist"]);' python setup.py sdist bdist_wheel 

¿Por qué estoy borrando carpetas usando Python? Quiero cumplir con el requisito para el desarrollo multiplataforma, no hay una forma conveniente de hacerlo en Windows y Unix.


Lanzo el entorno de prueba:


 tox -e build_wheel 

Como resultado, en la carpeta dist obtengo:


 dist/ numeral-system_py-0.1.0.tar.gz numeral_system-py-0.1.0-py2.py3-none-any.whl 

¡Lo completo!


En realidad no


Para empezar, vale la pena verificar que el paquete funcione correctamente. Lo subiré al repositorio del paquete de prueba. Por lo tanto, debe crear otra cuenta, pero ya en test.pypi.org .


Utilizo el paquete de hilo para esto, una herramienta para llenar artefactos en PyPi.


 [testenv:test_upload] skip_install = True deps = twine ;    commands = python -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* 

Inicialmente, el proyecto se llamaba numsys , pero cuando numsys de llenarlo, ¡me encontré con el hecho de que ya existe un paquete con el mismo nombre! Y lo que es más molesto: también sabe cómo convertir a números romanos :) No estaba muy molesto y cambió su nombre a numeral-system-py .


Ahora necesita instalar el paquete desde el entorno de prueba. La validación también debe automatizarse:


 [testenv:test_venv] skip_install = True ;         deps = ;   commands = pip install -i https://test.pypi.org/simple/ numeral-system-py 

Ahora solo necesitas ejecutar:


 tox -e test_venv ... test_venv: commands_succeeded congratulations :) 

Parece que funciona :)


Ahora definitivamente vierta!


Si


Creo un entorno para subir al repositorio de producción.


 [testenv:pypi_upload] skip_install = True deps = twine commands = python -m twine upload dist/* 

Y el entorno para la verificación de producción.


 [testenv:pypi_venv] skip_install = True deps = ;    commands = pip install numeral-system-py 

¿Funciona todo?


Compruebo con comandos simples:


 > virtualenv venv > source venv/bin/activate (venv) > pip install numeral-system-py (venv) > python >>> import numeral_system >>> numeral_system.roman.encode(7) 'VII' 

¡Todo es genial!


Corté el lanzamiento en github , recogí el paquete y lo rellené en el propi pypi.


Observaciones


Actualizaciones de dependencia


Durante la preparación de este artículo, se lanzó una nueva versión de pytest, en la que, de hecho, se eliminó el soporte para python 3.4 (en realidad en el paquete de colorama ). Había dos opciones:


  1. Comprometa la versión de colorama compatible con 3.4;
  2. Soltar el soporte 3.4 :)

A favor de la segunda opción, el último argumento fue que pip dejó de admitir 3.4 en la versión 19.1.


También hay dependencias fijas en forma de analizadores, formateadores y otros servicios. Estas dependencias se pueden actualizar al mismo tiempo. Si tiene suerte, solo obtendrá la versión de actualización; de lo contrario, tendrá que corregir el código o incluso agregar la configuración.


TravisCI


No es compatible con python para MacOS y Windows. Hay dificultades con ejecutar tox para todas las versiones de python dentro del mismo trabajo.


Versión del paquete


Es necesario cumplir con las versiones semánticas , es decir, el formato:


 MAJOR.MINOR.PATCH 

Duplicación de metainformación


Se debe especificar la versión del paquete y algunos otros parámetros para instalar el paquete (en setup.cfg o setup.py ) y en la documentación. Para evitar la duplicación, hizo una indicación solo en el paquete numeral_system/__init__.py :


 __version__ = '0.2.0' 

Y luego en setup.py explícitamente esta variable


 setup(version=numeral_system.__version__) 

Lo mismo es cierto para en docs/source/conf.py


 release = numeral_system.__version__ 

Lo anterior es cierto para cualquier metainformación: REAMDE.rst , descripciones de proyectos, licencias, nombres de autores y más.


Es cierto que esto lleva al hecho de que el paquete se está importando en el momento del ensamblaje, lo que puede ser indeseable.


Duplicación de dependencia


Al principio, estaba confundido por el hecho de que necesitaba especificar dependencias para el paquete en setup.cfg y setup.cfg .
, — setup.cfg .
. , requirements-dev.txt .



, src/ . :


  • ;
  • , , ;

:


  • PyCharm ( ?) — src , .


— .
, :


 Add badge with supported version Support for py38 

:


 Try fix py38 env creating Try fix py38 env creating Try fix py38 env creating Fix check 

, 3 ! Travis CI.


, . — , .


, . CHANGELOG .


La documentación


, Markdown . , rst .


Insignias


— , , , . , .
coverage .


-


, , , . six , , type hint , asyncio — .
python2.7 , . , python3.5+, . , , CI . .


?


, :



Conclusión


open source , .
, python github , .
stackoverflow.com issues github .
. . ?..


, , .
, .


github .


Gracias

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


All Articles