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
- 2. Gestión de paquetes en Python y librerías externas
- 3. Librerías incluidas en el paquete estándar
- 4. Comenzando con el desarrollo
- 5. Conclusiones
- 6. Referencias
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:
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.
[…] de más aprender las cosas básicas de un lenguage tan conocido como Python. Podéis empezar por aquí o por […]