Primeros pasos con Python

1
7217

Primeros pasos con Python. Instalación del entorno, paquetería, y un ejemplo con expresiones regulares y por supuesto TESTS.

Índice de contenidos

1. Consideraciones iniciales

Dado que la versión considerada como oficial es la 3.x, en este tutorial trabajaremos sobre la versión 3.6.0 de Python. De manera extraoficial la versión 2.7 está ampliamente extendida, y es posible que encontremos muchas aplicaciones que están escritas en esta versión o librerías específicas que no estén soportadas en la nueva versión.

Debemos tener en cuenta que entre ambas versiones hay un cambio de major, lo que supone que es bastante probable que tengamos que hacer modificaciones al código original si necesitamos que corra bajo la nueva versión o plantearnos el utilizar la 2.7.

No obstante, casi todo lo explicado en este tutorial es fácilmente extrapolable a ambas versiones.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro Retina 15′ (2.5 Ghz Intel Core I7, 16GB DDR3).
  • Sistema Operativo: macOS Sierra 10.12.2
  • Entorno de desarrollo: PyCharm Professional 2017.1 EAP
  • Python 3.6

1.1. Objetivo del tutorial

En este tutorial aprenderemos a instalar el intérprete Python y su gestión de paquetes.

Además, crearemos un pequeño script que reciba argumentos de ejecución, y cuyo propósito es validar el formato de un fichero CSV de entrada utilizando expresiones regulares.

1.2. Instalación de Python

Lo primero que debemos instalar es el propio Python. Para ello, algo tan simple como dirigirnos a su página web oficial y descargarnos el paquete instalable de la versión que nos interese y que corresponda a nuestro sistema. Podéis acceder directamente desde aquí.

En algunos sistemas, por ejemplo en máquinas que ejecuten alguna distribución de Linux ya contaremos con una instalación de Python. La manera más sencilla de saberlo es ejecutar el comando

python3 --version

Si lo tenemos correctamente instalado podremos esperar un resultado similar a este:

Versión Python instalada

2. Gestión de paquetes en Python y librerías externas

Existen multitud de librerías que podemos incorporar a nuestros desarrollos para aumentar la funcionalidad de una manera sencilla. Existen algunas integradas en el propio Python, y podemos consultar la documentación en: The Python Standard Library

El propio Python cuenta con una herramienta para la gestión de paquetes, llamada pip. Esta es la herramienta recomendada, aunque más adelante comentaremos una segunda alternativa.

2.1. pip

Esta es la herramienta recomendada para la instalación de paquetes. Para la misma de la herramienta podremos dirigirnos a su página oficial: pip. Este paso no será necesario para distribuciones de Python a partir de la versión 3.4, ya que pip está incluido como parte del paquete oficial, y por lo tanto ya lo tendremos instalado. En cualquier caso, podemos consultar la documentación oficial sobre la instalación de paquetes en Installing Python Modules.

Instalar un paquete (o librería) es tan sencillo como ejecutar el comando:

pip install <paquete>

2.2. Macports

Podemos utilizar como alternativa específica para Mac la utilidad Macports. Esto nos servirá no sólo para este propósito, sino como gestor de paquetes en general. Para ello, necesitaremos tener instaladas las herramientas de XCode:

xcode-select --install

Cuando contemos con ello, procedemos a instalar el gestor de paquetes macports desde: http://www.macports.org/ y por fin podremos instalar los paquetes de Python necesarios mediante:

sudo port install <paquete>

3. Librerías incluidas en el paquete estándar

Disponemos de multitud de librerías incluidas en el paquete oficial, conocido como Python Standard Library (consultar en https://docs.python.org/3/library/). De las incluidas, las que utilizaremos en el tutorial son:

  • unittest: desarrollo de test unitarios
  • sys: parámetros y funciones del sistema. Especialmente útil a la hora de recuperar argumentos de ejecución, como veremos después.
  • re: manejo de expresiones regulares
  • argparse: parseo de argumentos
  • logging: gestión de logs
  • errno: errores estándar predefinidos
  • cobertura: informe de lineas cubiertas por nuestros tests (incluye integración con el IDE y CLI)

4. Comenzando con el desarrollo

Todo desarrollo es la solución a un problema planteado. En nuestro caso, y para comenzar por algo muy sencillo, nos centraremos en validar el formato de un fichero para evitar problemas a la hora de procesarlo. Dicho fichero se recibirá como parámetro de la ejecución. En caso de error, se indicará su tipo y la línea del fichero para poder corregirlo.

El formato de nuestro fichero es muy sencillo:
<nombre>;<apellido(s)>;<teléfono>;<email>
donde <email> es un valor opcional.

4.1. Al turrón

Crearemos un primer test encargado de comprobar si la expresión regular que hemos diseñado acorde con el formato esperado se comporta de forma correcta.

Con fines didácticos y para ilustrar el trabajo con cadenas, tomaremos la aproximación inicial de validar el formato de cada campo por separado. La solución óptima pasa por validar el registro en su conjunto aprovechando toda la potencia de las expresiones regulares.

Al desarrollar mediante TDD, podemos comprobar que los errores en tiempo de ejecución (recordad que Python es interpretado), son bastante explicativos. Haciendo uso de la librería unittest incluida, obtendremos el siguiente código:

format_validator_test.py
	"""
	Test for the different values of inputs to the regex validator
	"""

	import unittest

	from lesson_1.format_validator import FormatValidator


	# Test Suite in order to organize our tests by groups of functionality
	class FormatValidatorTest(unittest.TestSuite):
	    class NameTests(unittest.TestCase):  # Tests for Name string
	        def test_ShouldPassWhenValidNameFormat(self):
	            # given
	            name_ok = "Yair SegundoNombre"
	            validator = FormatValidator()

	            # when
	            validator.check_valid_name(name_ok)

	            # then
	            self.assertIsNone(validator.error, "Error detected")

	        def test_ShouldFailWhenEmptyName(self):
	            # given
	            empty_name = ""
	            validator = FormatValidator()

	            # when
	            validator.check_valid_name(empty_name)

	            # then
	            self.assertIsNotNone(validator.error, "No error found, but should!")
	            self.assertEqual(validator.error, "Name is empty", "Not recognized empty name")

	        def test_ShouldFailWhenNoneAsName(self):
	            # given
	            validator = FormatValidator()

	            # when
	            validator.check_valid_name(None)

	            # then
	            self.assertIsNotNone(validator.error, "No error found, but should!")
	            self.assertEqual(validator.error, "Name is None", "Not recognized none name")

	        def test_ShouldFailWhenNotValidName(self):
	            # given
	            ko_name = "_*("
	            validator = FormatValidator()

	            # when
	            validator.check_valid_name(ko_name)

	            # then
	            self.assertIsNotNone(validator.error, "No error found, but should!")
	            self.assertEqual(validator.error, "Name error", "Not recognized name error")

	    class SurnameTests(unittest.TestCase):  # Tests for Surname string
	        def test_ShouldPassWhenValidSurnameFormat(self):
	            # given
	            surname_ok = "Segura Albarracín"
	            validator = FormatValidator()

	            # when
	            validator.check_valid_surname(surname_ok)

	            # then
	            self.assertIsNone(validator.error, "Error detected")

	        def test_ShouldFailWhenEmptySurname(self):
	            # given
	            empty_surname = ""
	            validator = FormatValidator()

	            # when
	            validator.check_valid_surname(empty_surname)

	            # then
	            self.assertIsNotNone(validator.error, "No error found, but should!")
	            self.assertEqual(validator.error, "Surname is empty", "Not recognized empty surname")

	        def test_ShouldFailWhenNoneAsSurname(self):
	            # given
	            validator = FormatValidator()

	            # when
	            validator.check_valid_surname(None)

	            # then
	            self.assertIsNotNone(validator.error, "No error found, but should!")
	            self.assertEqual(validator.error, "Surname is None", "Not recognized none surname")

	        def test_ShouldFailWhenNotValidSurname(self):
	            # given
	            ko_surname = "_*("
	            validator = FormatValidator()

	            # when
	            validator.check_valid_surname(ko_surname)

	            # then
	            self.assertIsNotNone(validator.error, "No error found, but should!")
	            self.assertEqual(validator.error, "Surname error", "Not recognized surname error")

	    class PhoneNumberTests(unittest.TestCase):  # Tests for Phone number field
	        def test_ShouldPassWhenValidPhoneFormat(self):
	            # given
	            phone_ok = "912345678"
	            validator = FormatValidator()

	            # when
	            validator.check_valid_phone(phone_ok)

	            # then
	            self.assertIsNone(validator.error, "Error detected")

	        def test_ShouldFailWhenEmptyPhone(self):
	            # given
	            empty_phone = ""
	            validator = FormatValidator()

	            # when
	            validator.check_valid_phone(empty_phone)

	            # then
	            self.assertIsNotNone(validator.error, "No error found, but should!")
	            self.assertEqual(validator.error, "Phone number is empty", "Not recognized empty phone number")

	        def test_ShouldFailWhenNoneAsPhone(self):
	            # given
	            validator = FormatValidator()

	            # when
	            validator.check_valid_phone(None)

	            # then
	            self.assertIsNotNone(validator.error, "No error found, but should!")
	            self.assertEqual(validator.error, "Phone number is None", "Not recognized none phone number")

	        def test_ShouldFailWhenNotValidPhone(self):
	            # given
	            ko_phone = "_*("
	            validator = FormatValidator()

	            # when
	            validator.check_valid_phone(ko_phone)

	            # then
	            self.assertIsNotNone(validator.error, "No error found, but should!")
	            self.assertEqual(validator.error, "Phone number error", "Not recognized phone number error")

	    class EmailTests(unittest.TestCase):  # Tests for Email field
	        def test_ShouldPassWhenValidEmailFormat(self):
	            # given
	            email_ok = "foo@bar.com"
	            validator = FormatValidator()

	            # when
	            validator.check_valid_email(email_ok)

	            # then
	            self.assertIsNone(validator.error, "Error detected")

	        def test_ShouldPassWhenEmptyEmail(self):
	            # given
	            empty_email = ""
	            validator = FormatValidator()

	            # when
	            validator.check_valid_email(empty_email)

	            # then
	            self.assertIsNone(validator.error, "Error detected")

	        def test_ShouldPassWhenNoneAsEmail(self):
	            # given
	            validator = FormatValidator()

	            # when
	            validator.check_valid_email(None)

	            # then
	            self.assertIsNone(validator.error, "Error detected")

	        def test_ShouldFailWhenNotValidEmail(self):
	            # given
	            ko_email = "foo.bar@com"
	            validator = FormatValidator()

	            # when
	            validator.check_valid_email(ko_email)

	            # then
	            self.assertIsNotNone(validator.error, "No error found, but should!")
	            self.assertEqual(validator.error, "Email error", "Not recognized email error")


	if __name__ == "__main__":
	    unittest.main(FormatValidatorTest)  # Executing our TestSuite

Y la clase que hace pasar los tests:

format_validator.py
	"""
	Format validation using regular expressions
	"""
	import re


	class FormatValidator(object):
	    def __init__(self):
	        self.error = None
	        self._text_regex = r"[A-Za-z\s]+"
	        self._phone_regex = r"\d{9,9}"
	        self._email_regex = r"[\w\d]+@[\w\d]+\.[\w\d]+"
	        return

	    def check_valid_name(self, name: str) -> bool:
	        return self.__check_field(name, self._text_regex, "Name")

	    def check_valid_surname(self, surname: str) -> bool:
	        return self.__check_field(surname, self._text_regex, "Surname")

	    def check_valid_phone(self, phone_number: str) -> bool:
	        return self.__check_field(phone_number, self._phone_regex, "Phone number")

	    def check_valid_email(self, email: str) -> bool:
	        if email is None or email is "":
	            return True
	        return self.__check4regex(pattern=self._email_regex, text=email, checked_field="Email")

	    def __check_field(self, field: str, pattern: str, field_name: str) -> bool:
	        if self.__check_input(text=field, checked_field=field_name):
	            return self.__check4regex(pattern=pattern, text=field, checked_field=field_name)

	    def __check_input(self, text: str, checked_field: str) -> bool:
	        if text is None:
	            self.error = ("{} is None".format(checked_field))
	            return False
	        elif text == "":
	            self.error = ("{} is empty".format(checked_field))
	            return False
	        return True

	    def __check4regex(self, pattern, text, checked_field: str) -> bool:
	        if re.match(pattern=pattern, string=text) is None:
	            self.error = ("{} error".format(checked_field))
	            return False
	        return True

Como consecuencia del diseño con TDD, obtenemos una cobertura de test muy alta, en este caso concreto, del 100%.

4.2. Trabajando con parámetros de entrada

Ahora que ya tenemos nuestra pieza de validación, lo único que nos queda por hacer es recibir un fichero como parámetro y hacer pasar su contenido por nuestro validador. Para ello, lo primero es poder tratar los parámetros de entrada.

Python provee una librería para estos menesteres, y mediante la cual podemos conseguir un resultado bastante profesional: argparse.

El código final, con su informe de cobertura:

file_check_test.py
"""
Test for the main program
"""

import errno
import unittest

from file_check import FileCheck


# Test Suite in order to organize our tests by groups of functionality
class FileCheckTest(unittest.TestSuite):
    class ParsingTests(unittest.TestCase):
        def test_ArgumentModelCreationOK(self):
            # given
            fc = FileCheck()
            # when

            # then
            self.assertIsNotNone(fc._args, "Object not initialized")
            self.assertTrue("file" in fc._args)

        def test_shouldFailWhenFileIsNone(self):
            # given
            fc = FileCheck()
            # when
            with self.assertRaises(SystemExit) as cm:
                fc.check_file()
            # then
            self.assertEqual(cm.exception.code, errno.EINVAL)

        def test_shouldFailWhenFileIsDirectory(self):
            # given
            fc = FileCheck()
            fc.file = "/"
            # when
            with self.assertRaises(SystemExit) as cm:
                fc.check_file()
            # then
            self.assertEqual(cm.exception.code, errno.EISDIR)

        def test_shouldFailWhenFileNotExist(self):
            # given
            fc = FileCheck()
            fc.file = "test_file.csv"
            # when
            with self.assertRaises(SystemExit) as cm:
                fc.check_file()
            # then
            self.assertEqual(cm.exception.code, errno.ENOENT)

        def test_shouldPassWhenCorrectFile(self):
            # given
            fc = FileCheck()
            fc.file = "test_ok.csv"
            # when

            # then
            self.assertTrue(fc.check_file())

        def test_shouldFailWhenIncorrectFile(self):
            # given
            fc = FileCheck()
            fc.file = "test_ko.csv"
            # when

            # then
            self.assertFalse(fc.check_file())


if __name__ == "__main__":
    unittest.main(FileCheckTest)  # Executing our TestSuite

file_check.py
"""
Document validation against predefined format
"""
import argparse
import csv
import errno
from pathlib import Path

from format_validator import FormatValidator


class FileCheck(object):

    def __init__(self):
        self.file = None
        # Parsing arguments of the invocation
        self.parser = argparse.ArgumentParser(description='Check csv file against predefined format')
        self.parser.add_argument('--file',
                                 help='file location')
        self._args = self.parser.parse_args()
        self.file = self._args.file

    def check_file(self) -> bool:
        if self.file is None:
            exit(errno.EINVAL)
        path = Path(str(self.file))
        if not path.exists():
            exit(errno.ENOENT)
        elif path.is_dir():
            exit(errno.EISDIR)

        # File correct
        fv = FormatValidator()
        with open(str(self.file), newline="") as csv_file:
            reader = csv.reader(csv_file, delimiter=";", quotechar="|")
            for row in reader:
                if not fv.check_valid_name(row[0]) or not fv.check_valid_surname(row[1]) \
                        or not fv.check_valid_phone(row[2]) or not fv.check_valid_email(row[3]):
                    return False
        return True


if __name__ == "__main__":
    fc = FileCheck()
    if fc.check_file():
        print("File format is valid")
        exit(0)
    else:
        print("File format not compliant")
        exit(1)


5. Conclusiones

Como habéis podido comprobar es tremendamente sencillo crear una aplicación desde 0 con este lenguaje. Además, el mismo lenguaje puede ser utilizado como script, orientado a objetos y según el paradigma de programación funcional.
Hay una comunidad muy activa de desarrolladores y un sin fin de librerías externas que nos permitiran aumentar su funcionalidad.

En futuros tutoriales ampliaremos el uso de Python en tareas de automatización, machine learning, etc.

El código fuente está disponible en el siguiente repositorio: https://github.com/ysegalb/python-tutorial.git
El mismo repositorio irá creciendo con las nuevas lecciones que vayamos añadiendo.

5.1. Deberes para casa

Hay muchas mejoras y “peoras” que podemos hacer con este ejemplo para cacharrear con el lenguaje. Desde aquí os animamos a trabajar los siguientes cambios para que profundicéis un poco más en este lenguaje:

  • Modificar el tratamiento de csv a fichero de texto y validar mediante una única expresión regular
  • Modificar la expresión regular para que el correo sea de un dominio específico (p.e. @autentia.com)
  • Añadir un nuevo campo URL opcional al fichero y validarlo
  • Mejorar la expresión regular correspondiente al e-mail
  • Todo aquello que se os ocurra para «divertiros» con este lenguaje

6. Referencias

El tutorial se ha escrito desde cero, partiendo de la documentación oficial y las referencias ahí incluidas.

1 COMENTARIO

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad