Ícono del sitio Ingeniería Resuelve

Tutorial básico para publicar un módulo en Pypi

Normalmente el conocimiento base de todo desarrollador llega hasta la solución puntual de un problema, pero cuando este problema resulta siendo bastante común en proyectos propios, del equipo, empresa o ecosistema, entonces es un buen momento para crear una librería. En esta entrada vamos a dar un ejemplo de cómo crear un proyecto con ciertos criterios de calidad, pero al mismo tiempo básico.

En este tutorial vamos a crear una clase Snake en un módulo llamado animals, la cual va a estar documentada con Sphinx y con tests unitarios usando unittest, para ser publicada en pypi.

La clase Snake para este ejemplo va a ser

# snake.py

class Snake:
    def eat(self, animal):
        return 'Delicious!!!' if animal is 'mouse' else 'No thanks'

Estructura de archivos

A continuación vamos a mostrar la estructura propuesta, vamos a ir explicando la finalidad de los demás archivos.

 .
├ animals
|  ├ __init__.py
|  └ snake.py
├ setup.py
├ requirements.txt
├ README.md
└ .gitignore

En el módulo de animals está la clase en el archivo snake.py y un archivo para configurar el módulo, en donde se importan los archivos y clases a usar, como se muestra en el ejemplo

# __init__.py

from .snake import Snake

__name__ = 'animals'
__version__ = '0.0.0'
__all__ = ['Snake']

En el archivo requirements.txt se agregan las dependencias del proyecto, especificando versiones si es necesario, como por ejemplo

# requirements.txt

pandas==0.25.0
sklearn

Este archivo es tomado por el archivo de configuración del módulo, el cual es setup.py, para la configuración de dependencias. Este archivo puede ser de la siguiente forma

# setup.py

import setuptools
import animals

with open('README.md', 'r') as fh:
    long_description = fh.read()

setuptools.setup(
    name='animals',
    version=animals.__version__,
    author='Santa Claus',
    author_email='santa@claus.com',
    description='Please describe this',
    long_description=long_description,
    long_description_content_type='text/markdown',
    url='https://github.com/resuelve/animals',
    packages=setuptools.find_packages(exclude=['sphinx_docs', 'docs', 'tests']),
    python_requires='~=3.5',
    install_requires=[
        i.replace('\n', '')
        for i in open('requirements.txt', 'r').readlines()
    ],
    extras_require={
        'dev': ['setuptools', 'wheel', 'twine', 'Sphinx'],
    },
    classifiers=[
        'Development Status :: 3 - Alpha',
        'Intended Audience :: Developers',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.6',
        'Programming Language :: Python :: 3.7',
        'Topic :: Software Development',
        'License :: OSI Approved :: MIT License',
        'Operating System :: OS Independent',
    ],
)

Para terminar, el archivo .gitignore puede ser el mismo generado por github, solo recomendamos que al menos estén estas reglas

# .gitignore

*.egg-info/
build/
dist/
__pycache__/

Pruebas unitarias

Se agregan unas pruebas unitarias usando unittesting. La idea es que sean lo más concisas posibles. Este archivo para este caso va a estar dentro de la carpeta tests.

# test_snake.py

import unittest
from animals.snake import Snake

class TestSnake(unittest.TestCase):

    def test_eat(self):
        snake = Snake()
        self.assertEqual(snake.eat('mouse'), 'Delicious!!!')
        self.assertEqual(snake.eat('elephant'), 'No thanks')


if __name__ == '__main__':
    unittest.main()

La forma de revisar que estén pasando todos los tests es corriendo en consola, el cual solo funciona si los tests están ubicados dentro de la carpeta de tests y comienzan con la palabra test_ , como por ejemplo, test_snake.py

python3 -m unittest tests/test_*

Documentación

Para generar una página web con los comentarios en python nosotros usamos Sphinx. Un ejemplo de la forma de documentar usando la estructura de sphinx se puede ver en la siguiente forma

# snake.py

class Snake:
    ''' Esta es la clase snake, de tener unos parámetros de constructor, acá irían'''
    
    def eat(self, animal):
        ''' Toda serpiente necesita comer
        :param animal: Hoy que animal va a comer
        :type animal: str
        :return: Si la serpiente está dispuesta a comer o no
        :rtype: str
        '''
        return 'Delicious!!!' if animal is 'mouse' else 'No thanks'

Para esto se agrega una carpeta donde se guarde la estructura de los archivos a generar. En este punto la estructura de carpetas debería estar de la siguiente manera

.
├ animals
|  ├ __init__.py
|  └ snake.py
├ tests
|  └ test_snake.py
├ sphinx_docs
|  ├ conf.py
|  ├ index.rst
|  └ snake.rst
├ setup.py
├ requirements.txt
├ MANIFEST.in
├ README.md
└ .gitignore

En conf.py está la configuración y demás formas de revisar el código para generar el HTML, en index.rst está la entrada inicial del proyecto y cómo se va a ver representada y en snake.rst están las directivas de cómo leer el archivo de snake.py

# conf.py

# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# http://www.sphinx-doc.org/en/master/config

# -- Path setup --------------------------------------------------------------

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath('..'))


# -- Project information -----------------------------------------------------

project = 'animals'
copyright = '2019, Resuelve'
author = 'Santa Claus'

# The full version, including alpha/beta/rc tags
release = '0.0.0'


# -- General configuration ---------------------------------------------------

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
    'sphinx.ext.autodoc',
    'sphinx.ext.viewcode',
    'sphinx.ext.intersphinx',
    'sphinx.ext.autosummary',
]

# Include Python objects as they appear in source files
# Default: alphabetically ('alphabetical')
autodoc_member_order = 'bysource'
# Default flags used by autodoc directives
autodoc_default_flags = ['members', 'show-inheritance']
# This value contains a list of modules to be mocked up
autodoc_mock_imports = [
    # all the dependencies that you want to ignore
]
# Generate autodoc stubs with summaries from code
autosummary_generate = True

# Add any paths that contain templates here, relative to this directory.
# templates_path = ['_templates']

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []


# -- Options for HTML output -------------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# alabater theme opitons
html_theme_options = {
    'github_button': True,
    'github_type': 'star&v=2',
    'github_user': 'resuelve',
    'github_repo': 'silk-ml',
}

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named 'default.css' will overwrite the builtin 'default.css'.
html_static_path = ['_static']

# Sidebars configuration for alabaster theme

html_sidebars = {
    '**': [
        'about.html',
        'navigation.html',
        'searchbox.html',
    ]
}

# I don't like links to page reST sources
html_show_sourcelink = True

# Add Python version number to the default address to correctly reference
# the Python standard library
intersphinx_mapping = {'https://docs.python.org/3.7': None}
.. index.rst

*******
animals
*******

.. image:: https://img.shields.io/pypi/v/animals.svg
   :target: https://pypi.python.org/pypi/animals
   :alt: PyPI Version
.. image:: https://img.shields.io/pypi/pyversions/animals.svg
   :target: https://pypi.python.org/pypi/animals
   :alt: PyPI python Version

Simple Intelligent Learning Kit (SILK) for Machine learning

Welcome to silk_ml's documentation!
===================================

:Source code: `github.com project <https://github.com/resuelve/animals>`_
:Bug tracker: `github.com issues <https://github.com/resuelve/animals/issues>`_
:Generated: |today|
:License: MIT
:Version: |release|

Project Modules
===============

List of project's modules

.. toctree::
   :maxdepth: 2
   
   _autosummary/snake.rst

Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
.. snake.rst

snake
========

.. automodule:: animals.snake
   :members:

Generación de binarios

Primero se necesita instalar las herramientas necesarias para poder realizar la compilación del código

python3 -m pip install --upgrade setuptools wheel twine

Ya teniendo esto instalado, primero se compilan y crean los binarios usando el mismo archivo de configuración setup.py, y se suben a pypi usando twine. Vale destacar que primero es necesario haber creado una cuenta en pypi.

python3 setup.py sdist bdist_wheel
python3 -m twine upload dist/*


Salir de la versión móvil