Développement du package pypi parfait avec prise en charge de différentes versions de python

Ceci est un petit manuel / histoire sur la façon de créer un package pypi «parfait» pour python, que n'importe qui peut installer avec la commande chérie:


pip install my-perfect-package 

Il s'adresse aux débutants, mais j'exhorte les professionnels à exprimer leur avis sur la façon d'améliorer le package "parfait". Par conséquent, je demande un chat.


Que signifie un forfait «idéal»?


Je vais partir des exigences suivantes:


  • Open source sur github;
    Tout le monde devrait pouvoir contribuer au développement et remercier l'auteur.
  • Prise en charge de toutes les versions actuelles / populaires de python (2.7, 3.5, 3.6, 3.7, 3.8);
    Les pythons sont différents, et quelque part écrivent toujours activement sur 2.7.
  • Couverture à 100% avec tests unitaires;
    Les tests unitaires améliorent l'architecture et automatisent les contrôles de régression.
    Un badge avec un numéro cher soulève la FAQ et place la barre pour les autres .
  • Utilisation de CI:
    Les contrôles automatiques sont très pratiques! Et un tas de badges sympas
    • Exécution de tests unitaires sur toutes les plateformes et sur toutes les versions de python;
      Ne croyez pas ceux qui prétendent que python et les packages installés sont multiplates-formes , car vous pouvez toujours rencontrer un bogue .
    • Vérification du code de style;
      Un style uniforme améliore la lisibilité et réduit le nombre de discussions vides dans une révision.
    • Analyseur de code statique;
      Recherche automatique de bugs dans le code? Donnez-moi deux!
  • Documentation réelle;
    Exemples de travail avec le package, description des méthodes / classes et analyse des erreurs typiques - une expérience documentée réduira le seuil d'entrée pour les débutants.
  • Développement multiplateforme;
    Malheureusement, il est difficile d'apporter une contribution personnelle aux projets simplement parce que le développeur a affiné les outils pour Unix. Par exemple, j'ai utilisé des scripts bash pour l'assemblage.
  • Le package est utile et rend le monde meilleur.
    Une exigence difficile, car à en juger par le nombre de packages dans pypi (~ 210k), les développeurs sont des altruistes sauvages et beaucoup a déjà été écrit.

Par où commencer?


Il n'y avait pas de bonnes idées, j'ai donc choisi un sujet galvaudé et très populaire - travailler avec les systèmes numériques. La première version devrait pouvoir traduire les nombres en romain et vice versa. Pour le manuel c'est plus compliqué et pas nécessaire. Oh oui, le plus important est le nom: numsys - comme décryptage des systèmes numériques. numeral-system-py .


Comment tester?


J'ai pris python3.7 et j'ai d' abord écrit des tests avec des stubs de fonction (nous sommes tous des TDD ) en utilisant le module standard unittests .


Je fais la structure de projet suivante:


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

Je ne mettrai pas de tests dans le package, donc je sépare paillette . Au départ, src/ n'ai pas créé le dossier src/ , mais les développements ultérieurs ont montré qu'il était plus pratique pour moi d'opérer. Ce n'est donc pas nécessaire à volonté.


J'ai décidé d'utiliser pytest pour le lancement - il sait parfaitement fonctionner avec les tests du module standard. Cela peut sembler un peu illogique, mais le module standard pour les tests pour moi il semble semblait un peu plus confortable. Maintenant, je recommanderais d'utiliser le style d'écriture pytest.


Mais, ils disent que mettre pytest (comme toutes les autres dépendances) dans python système n'est pas une idée très intelligente ...


Comment gérer les dépendances?


Seuls virtualenv et requirements.txt peuvent être utilisés. Vous pouvez être progressiste et utiliser la poésie . J'utiliserai peut-être tox - un outil pour simplifier l'automatisation et les tests, ce qui me permettra également de gérer les dépendances.


Je crée une simple configuration tox.ini et installe pytest :


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

Initialement, j'ai indiqué explicitement les dépendances, mais la pratique de l'intégration avec des services tiers a montré que le meilleur moyen serait encore de stocker les dépendances dans le fichier requirements.txt .


Un moment très subtil survient. Réparer la version actuelle au moment du développement ou toujours mettre la dernière?


Si vous vous engagez, lors de l'installation, il peut y avoir des conflits entre les packages en raison des différentes versions des dépendances utilisées. Si vous ne vous engagez pas, le package peut soudainement cesser de fonctionner. Cette dernière situation est très désagréable pour les produits finaux, lorsque toutes les versions peuvent «virer au rouge» en une nuit en raison d'une mise à jour mineure de la dépendance implicite. Et selon la loi de Murphy, cela se produira le jour de la sortie.


Par conséquent, j'ai développé une règle pour moi-même:


  1. Fixez toujours la version des produits finaux, car les versions à utiliser sont de leur responsabilité.
  2. Ne corrigez pas la version utilisée pour les packages installés. Ou limitez-le à une plage si la fonctionnalité du package l'exige.

Et ensuite?


J'écris des tests!


Je remplis le corps des fonctions, ajoute des commentaires et fais les tests correctement.
À ce stade, la plupart des développeurs s'arrêtent généralement (je crois toujours que tout le monde écrit des tests =), publient le package et piratent les bogues. Mais je continue et saute dans le terrier du lapin .


Comment travailler avec différentes versions de python?


Dans la configuration tox , tox précise le lancement de tests sur toutes les versions intéressantes de python:


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

En utilisant pyenv, je me livre localement les versions nécessaires pour que tox puisse les trouver et créer des environnements de test.


Où sont les chéris à 100%?


J'ajouterai une mesure de la couverture du code - pour cela, il existe un excellent package de couverture et non moins une excellente intégration avec pytest - pytest-cov .
requirements-test.txt ressemble maintenant à ceci:


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

Selon la règle ci-dessus, je corrige les versions des packages utilisés pour exécuter les tests.


Je change la commande de test:


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

Je collecte des statistiques de couverture pour tout le code du dossier src/ - le package lui-même ( numeral_system / ) et requis pour le code de test ( tests / ) - Je ne veux pas que les tests eux-mêmes contiennent des parties non exécutables?


--cov-append commande --cov-append toutes les statistiques collectées pour chaque appel sous une version différente de python en une seule, car la couverture pour les deuxième et troisième python peut être différente (code dépendant de la version et module six !), Mais au total, donnez 100 % Un exemple simple:


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

Ajoutez un nouvel environnement pour créer un rapport de couverture.


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

Et j'ajoute à la liste des environnements après avoir exécuté les tests sur toutes les versions de python.


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

Après avoir exécuté la commande tox , le dossier tox contenant le fichier index.html avec un beau rapport doit apparaître à la racine du projet.


Pour le badge convoité, j'intègre à 100% le service codecov , lui-même déjà intégré à github et vous permet de visualiser l'historique des changements de couverture de code. Pour cela, vous devrez bien sûr y créer un compte.


L'environnement de lancement final est le suivant:


 [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,    

Il ne reste plus qu'à ajouter un lien vers le badge dans 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 

Comment formater et analyser le code?


De nombreux analyseurs n'existent pas, car ils se complètent pour la plupart. Par conséquent, j'intégrerai des analyseurs statiques populaires qui vérifieront la conformité au PEP8 , trouveront les problèmes potentiels et corriger tous les bugs formater uniformément le code.


Vous devez immédiatement déterminer où spécifier les paramètres de réglage fin des analyseurs. Pour ce faire, vous pouvez utiliser le fichier tox.ini , setup.cfg , un seul fichier personnalisé ou des fichiers spécifiques pour les analyseurs. J'ai décidé d'utiliser tox.ini directement, car vous pouvez simplement copier tox.ini pour de futurs projets.


isort


isort est un utilitaire pour formater les importations.


Je crée l'environnement suivant pour exécuter isort en mode format de code.


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

Malheureusement, isort ne peut pas spécifier de dossier pour le formatage. Par conséquent, vous devez modifier le répertoire de démarrage via changedir et spécifier le chemin d'accès au fichier avec les paramètres -sp={toxinidir}/tox.ini . Le commutateur -y est nécessaire pour désactiver le mode interactif.


Pour exécuter les tests, vous avez besoin d'un mode de vérification - pour cela, il y a un indicateur --check-only :


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

noir


Ensuite, je m'intègre avec le formateur de code noir . Je le fais par analogie avec isort :


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

Tout fonctionne bien, mais il y a un conflit avec isort - il y a une différence dans le formatage des importations .


Dans l' un des commentaires, j'ai trouvé un paramètre d' isort compatibilité minimale, que j'ai utilisé:


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

flake8


Ensuite, je m'intègre aux analyseurs statiques flake8 .


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

Il y a encore des problèmes avec l'intégration avec le black . Nous devons ajouter un réglage fin, qui, en fait, est recommandé par le black lui-même:


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

Malheureusement, la première fois, cela n'a pas fonctionné. Est tombé avec l'erreur E231 missing whitespace after ',' , j'ai dû ajouter cette erreur pour ignorer:


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

pylint


Intégration avec les analyseurs de code statique pylint


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

Je rencontre immédiatement d'étranges restrictions - des noms de fonction de 30 caractères (oui, j'écris des noms très longs de méthodes de test) et des avertissements pour la présence de TODO dans le code.
Je dois ajouter quelques exceptions:


 [MESSAGES CONTROL] disable=fixme,invalid-name 

De plus, le moment désagréable est que les développeurs de pylint déjà enterré python2.7 et ne développent plus de package pour cela. Par conséquent, les vérifications doivent être exécutées sur le package actuel pour python3.7 .
Ajoutez la ligne appropriée à la configuration:


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

Il est également important pour exécuter des tests sur différentes plates-formes, car la version par défaut de python dans les systèmes CI est différente.


Quoi de neuf avec CI?


Appveyor


Intégration avec appveyor - CI pour Windows. La configuration initiale est simple - tout peut être fait dans l'interface, puis téléchargez le fichier yaml et validez-le dans le référentiel.


 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 

Ici, je spécifie explicitement la version de python3.7 , car python2.7 sera utilisé par défaut (et tox utilisera également cette version, bien que j'aie explicitement spécifié python3.7 ).
Le lien vers le badge, comme d'habitude, est ajouté à 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


Après cela, je m'intègre à Travis CI - CI sous Linux (et sous MacOS avec Windows, mais les Python builds are not available on the macOS and Windows environments . La configuration est un peu plus compliquée, car le fichier de configuration sera utilisé directement à partir du référentiel. Quelques itérations d'essai et d'erreur - la configuration est prête, je squash dans un beau commit et la demande de fusion est prête.


 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 

( Question rhétorique: et pourquoi les projets CI aiment-ils tant le format yaml? )


python3.8 la version de python3.8 , car elle n'a pas fonctionné correctement via l' addon , et Travis CI réussi à créer virtualenv partir de la version spécifiée.


Les personnes familières avec Travis CI peuvent se demander pourquoi ne pas spécifier explicitement les versions de python de cette façon? Après tout, Travis CI crée automatiquement virtualenv et y exécute les commandes nécessaires.


La raison en est que nous devons collecter des données de couverture de code de toutes les versions. Mais les tests seront exécutés dans différents travaux en parallèle, c'est pourquoi il ne fonctionnera pas pour collecter un rapport de couverture générale.


Bien sûr, je suis sûr qu'un peu plus de compréhension et cela peut être corrigé.


Par tradition, un lien vers un badge est également ajouté à 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 documentation


Je pense que chaque développeur de python a utilisé le service au moins une fois - readthedocs.org . Il me semble que c'est le meilleur service pour héberger sa documentation.
J'utiliserai l'outil standard pour générer la documentation Sphinx . Je suis les étapes du manuel de démarrage et j'obtiens la structure suivante:


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

Ensuite, vous devez effectuer les étapes minimales pour configurer:


  1. github par défaut suggère de créer un fichier README.md au format Markdown , alors que comme sphinx par défaut, il suggère d'utiliser ReStructuredText .

Par conséquent, j'ai dû le réécrire au format .rst . Et si j'avais lu le manuel de démarrage jusqu'à la fin au moins une fois, je me suis rendu compte que sphinx peut le faire dans Markdown .
README.rst fichier index.rst dans index.rst


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

  1. Pour générer automatiquement la documentation à partir des commentaires dans la source, j'ajoute l'extension sphinx.ext.autodoc .
  2. conf.py dossier conf.py package à conf.py Cela permettra à sphinx d'importer notre code pour analyse.
     import os import sys sys.path.insert(0, os.path.abspath('./../../src')) import numeral_system 
  3. J'ajoute le dossier docs/source/api-docs et y dépose le fichier de description de chaque module. La documentation doit être générée automatiquement à partir des commentaires:
     Roman numeral system ========================= .. automodule:: numeral_system.roman :members: 

Après cela, le projet est prêt à révéler sa description au monde. Vous devez créer un compte (de préférence via un compte sur github ) et importer votre projet, les étapes détaillées sont décrites dans les instructions .
Par tradition, je crée un environnement en tox :


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

J'utilise explicitement la sphinx-build , au lieu de make , car elle n'existe pas sous Windows. Et je ne veux pas violer le principe du développement multiplateforme.


Dès que les modifications sont gelées, readthedocs.org collectera automatiquement la documentation et la publiera.


Mais ... La Build failed . Je n'ai pas sphinx_rtd_theme sphinx et sphinx_rtd_theme , et je m'attendais readthedocs.org ce que readthedocs.org prenne les versions actuelles. Mais ce n'est pas le cas. Je corrige:


 sphinx==2.3.1 sphinx_rtd_theme==0.4.3 

Et je crée un fichier de configuration spécial .readthedocs.yml pour readthedocs.org , dans lequel je décris l'environnement pour lancer la construction:


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

C'est là que le fait est devenu pratique que les dépendances se trouvent dans les fichiers requirements.txt . J'attends la construction et la documentation devient disponible .


Ajoutez à nouveau le badge:


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

Licence


Il vaut la peine d'envisager de choisir une licence pour le package.
Il s'agit d'un sujet très complet, j'ai donc lu cet article . Fondamentalement, le choix se fait entre MIT et Apache 2.0 . J'ai aimé la phrase qui a été sortie de son contexte avec succès:


 MIT       

Je suis tout à fait d'accord, je le ferai. Si les plans changent, vous pouvez facilement changer la licence (bien que les versions précédentes seront sous l'ancienne).


Encore une fois, ajoutez un badge insigne dieu :


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

Vous trouverez ici des badges pour toutes les licences.


Comment télécharger sur pypi?


Vous devez d'abord créer un compte sur pypi.org . Procédez ensuite à la préparation du colis.


Je crée setup.cfg


Il est nécessaire de décrire correctement la configuration pour l'installation / la construction du package. J'ai suivi les instructions . Il est possible de définir des données via setup.py , mais il n'y a pas d'options pour définir certains paramètres. Par conséquent, utilisez le fichier setup.cfg , dans lequel vous pouvez spécifier toutes les nuances. Trouvé un petit modèle sur la façon de remplir ce fichier. En conséquence, j'utilise à la fois cela et ce fichier - c'est plus pratique.


Ce fichier peut également être utilisé pour configurer pylint , flake8 et d'autres paramètres, mais je ne l'ai pas fait.


Comment assembler un colis?


Encore une fois, j'écris un environnement qui m'aidera à rassembler le package nécessaire:


 [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 

Pourquoi est-ce que je supprime des dossiers en utilisant python? Je veux me conformer à l'exigence de développement multiplateforme, il n'y a aucun moyen pratique de le faire sous Windows et Unix.


Je lance l'environnement de test:


 tox -e build_wheel 

En conséquence, dans le dossier dist , j'obtiens:


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

Je remplis!


Pas vraiment.


Pour commencer, il convient de vérifier que le package fonctionne correctement. Je vais le télécharger dans le référentiel du package de test. Par conséquent, vous devez créer un autre compte, mais déjà sur test.pypi.org .


J'utilise le paquet twine pour cela - un outil pour remplir les artefacts dans PyPi.


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

Au départ, le projet s'appelait numsys , mais en essayant de le remplir, je suis tombé sur le fait qu'un package du même nom existe déjà! Et ce qui est le plus ennuyeux - il sait aussi comment convertir en chiffres romains :) Il n'était pas très contrarié et renommé en numeral-system-py .


Vous devez maintenant installer le package à partir de l'environnement de test. La validation doit également être automatisée:


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

Il vous suffit maintenant d'exécuter:


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

Cela semble fonctionner :)


Maintenant, versez définitivement!


Oui


Je crée un environnement pour le téléchargement vers le référentiel de production.


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

Et l'environnement pour la vérification de la production.


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

Est-ce que tout fonctionne?


Je vérifie avec des commandes simples:


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

Tout est super!


Je coupe la version sur github , récupère le paquet et le remplis dans le propi pypi.


Remarques


Mises à jour des dépendances


Lors de la préparation de cet article, une nouvelle version de pytest a été publiée, dans laquelle, en fait, le support de python 3.4 a été abandonné (en fait dans le package colorama ). Il y avait deux options:


  1. Validez la version colorama compatible avec 3.4;
  2. Drop support 3.4 :)

En faveur de la deuxième option, le dernier argument était que pip abandonné le support 3.4 dans la version 19.1.


Il existe également des dépendances fixes sous la forme d'analyseurs, de formateurs et d'autres services. Ces dépendances peuvent être mises à jour en même temps. Si vous êtes chanceux, vous ne pourrez que vous mettre à niveau avec la version de mise à niveau; sinon, vous devrez corriger le code ou même ajouter les paramètres.


TravisCI


Ne prend pas en charge python pour MacOS et Windows. Il y a des difficultés à exécuter tox pour toutes les versions de python dans le même travail.


Version du package


Il est nécessaire de respecter le versioning sémantique , à savoir le format:


 MAJOR.MINOR.PATCH 

Duplication des méta-informations


La version du package et certains autres paramètres doivent être spécifiés pour installer le package (dans setup.cfg ou setup.py ) et dans la documentation. Pour éviter la duplication, il a fait une indication uniquement dans le paquet numeral_system/__init__.py :


 __version__ = '0.2.0' 

Et puis dans setup.py explicitement cette variable


 setup(version=numeral_system.__version__) 

Il en va de même pour dans docs/source/conf.py


 release = numeral_system.__version__ 

Ce qui précède est vrai pour toute méta-information - REAMDE.rst , descriptions de projet, licences, noms d'auteur, etc.


Certes, cela conduit au fait que le package est importé au moment de l'assemblage, ce qui peut être indésirable.


Duplication des dépendances


Au début, j'étais confus du fait que je devais spécifier des dépendances pour le package dans requirements.txt et 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 documentation


, Markdown . , rst .


Insignes


— , , , . , .
coverage .


-


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


?


, :



Conclusion


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


, , .
, .


github .


Je vous remercie!

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


All Articles