Fecha: 2021-05-28 Tiempo de lectura: 5 minutos Categoría: Miscelánea Tags: prototipo / python / cookiecutter / copier
Cada vez nos encontramos con el mismo problema; empezamos un nuevo proyecto y tenemos que crear toda la estructura del proyecto partiendo de cero, de un ejemplo, o haciendo copy-paste de otro anterior. Esto implica cambiar algunos nombres de ficheros y carpetas, o contenido de ciertos ficheros; es toda una invitación al desastre.
Hoy vamos a ver como podemos hacer estas plantillas usando dos alternativas escritas en python: cookiecutter y copier. Si no las tenemos, podemos instalárnoslas, por ejemplo, en un virtualenv.
gerard@arcadia:~/projects/bootstrap$ python3 -m venv env
gerard@arcadia:~/projects/bootstrap$
gerard@arcadia:~/projects/bootstrap$ . env/bin/activate
(env) gerard@arcadia:~/projects/bootstrap$
(env) gerard@arcadia:~/projects/bootstrap$ pip install cookiecutter copier
...
(env) gerard@arcadia:~/projects/bootstrap$
(env) gerard@arcadia:~/projects/bootstrap$ pip freeze | egrep "cookiecutter|copier"
cookiecutter==1.7.3
copier==5.1.0
(env) gerard@arcadia:~/projects/bootstrap$
A partir de ahora vamos a trabajar con un ejemplo bastante simple, pero con alto valor didáctico: vamos a empezar con un proyecto básico escrito usando el framework falcon.
(env) gerard@arcadia:~/projects/bootstrap$ tree ejemplo/
ejemplo/
├── myapi
│ └── __init__.py
├── requirements.txt
└── server.sh
1 directory, 3 files
(env) gerard@arcadia:~/projects/bootstrap$
Incluyo el contenido de todos los ficheros por tener el ejemplo completo, aunque no es muy relevante, cubre todas las necesidades de un prototipo más grande, aunque no más complejo.
(env) gerard@arcadia:~/projects/bootstrap$ cat ejemplo/myapi/__init__.py
import falcon
class HelloResource:
def on_get(self, req, resp, name):
resp.media = {'hello': name}
app = falcon.App()
app.add_route('/hello/{name}', HelloResource())
(env) gerard@arcadia:~/projects/bootstrap$
(env) gerard@arcadia:~/projects/bootstrap$ cat ejemplo/requirements.txt
gunicorn
falcon
(env) gerard@arcadia:~/projects/bootstrap$
(env) gerard@arcadia:~/projects/bootstrap$ cat ejemplo/server.sh
#!/bin/bash
export PYTHONDONTWRITEBYTECODE=" "
gunicorn --reload --bind 127.0.0.1:8080 myapi:app
(env) gerard@arcadia:~/projects/bootstrap$
La parte más importante a tener en cuenta es que hay cosas que van a cambiar entre diferentes instancias de esta plantilla; para este ejemplo tan simple, vamos a renombrar la carpeta a un nombre más descriptivo (es un package de python), y el script de servidor va a necesitar reflejar eso mismo en la aplicación que levanta.
Una plantilla de cookiecutter no es más que una carpeta contenedora, que incluye los metadatos necesarios y una carpeta con un nombre variable, que es lo que se crea cuando la instanciamos.
Es obligado que esta primera carpeta tenga un nombre variable, y para eso tiene
que llamarse algo como {{cookiecutter.variable}}. Esto es lo mismo que debemos
poner en los ficheros cada vez que queramos reemplazar algún contenido concreto.
Digamos que lo parametrizamos y lo dejamos así:
(env) gerard@arcadia:~/projects/bootstrap$ tree falcon_api.cookiecutter/
falcon_api.cookiecutter/
├── {{cookiecutter.folder_name}}
│ ├── {{cookiecutter.package_name}}
│ │ └── __init__.py
│ ├── requirements.txt
│ └── server.sh
└── cookiecutter.json
2 directories, 4 files
(env) gerard@arcadia:~/projects/bootstrap$
Las carpetas tienen nombre variable; el folder_name sería la carpeta
contenedora del proyecto final y el package_name sería el nombre del
paquete python, que referenciamos en el fichero server.sh en el
comando de ejecución:
(env) gerard@arcadia:~/projects/bootstrap$ cat falcon_api.cookiecutter/\{\{cookiecutter.folder_name\}\}/server.sh
#!/bin/bash
export PYTHONDONTWRITEBYTECODE=" "
gunicorn --reload --bind 127.0.0.1:8080 {{cookiecutter.package_name}}:app
(env) gerard@arcadia:~/projects/bootstrap$
NOTA: El resto de ficheros no ha cambiado desde el ejemplo inicial.
Las variables se declaran en el fichero cookiecutter.json, con sus
valores por defecto, que se nos van a preguntar interactivamente cuando
usemos la plantilla.
(env) gerard@arcadia:~/projects/bootstrap$ cat falcon_api.cookiecutter/cookiecutter.json
{
"folder_name": "myfolder",
"package_name": "myapi"
}
(env) gerard@arcadia:~/projects/bootstrap$
Solo necesitamos ejecutar el comando cookiecutter <ruta a la plantilla>
para crear una instancia en la carpeta actual:
(env) gerard@arcadia:~/projects/bootstrap$ cookiecutter falcon_api.cookiecutter/
folder_name [myfolder]: folder1
package_name [myapi]:
(env) gerard@arcadia:~/projects/bootstrap$
En este caso indicamos un folder_name nuevo, pero aceptamos el
package_name por defecto. Con estas variables, cookiecutter
genera todos los nombres de carpetas, ficheros y su contenido; por
supuesto con los valores reemplazados.
(env) gerard@arcadia:~/projects/bootstrap$ tree folder1/
folder1/
├── myapi
│ └── __init__.py
├── requirements.txt
└── server.sh
1 directory, 3 files
(env) gerard@arcadia:~/projects/bootstrap$
(env) gerard@arcadia:~/projects/bootstrap$ cat folder1/server.sh
#!/bin/bash
export PYTHONDONTWRITEBYTECODE=" "
gunicorn --reload --bind 127.0.0.1:8080 myapi:app
(env) gerard@arcadia:~/projects/bootstrap$
Las diferencias con el anterior son mínimas; la carpeta contenedora sigue siendo necesaria, pero los ficheros de la plantilla conviven en ella, mezclados con los metadatos de copier.
(env) gerard@arcadia:~/projects/bootstrap$ tree falcon_api.copier/
falcon_api.copier/
├── [[package_name]]
│ └── __init__.py
├── copier.yml
├── requirements.txt
└── server.sh.jinja
1 directory, 4 files
(env) gerard@arcadia:~/projects/bootstrap$
(env) gerard@arcadia:~/projects/bootstrap$ cat falcon_api.copier/server.sh.jinja
#!/bin/bash
export PYTHONDONTWRITEBYTECODE=" "
gunicorn --reload --bind 127.0.0.1:8080 [[package_name]]:app
(env) gerard@arcadia:~/projects/bootstrap$
A simple vista saltan dos cosas a la vista: el uso de [[variable]] en
vez de {{variable}} y la presencia de una extensión para “marcar” los
ficheros que son plantillas, para ahorrarnos procesado. Es interesante ver
que no cargamos con el prefijo cookiecutter.<variable> y simplificamos
nuestra plantilla.
NOTA: En versiones posteriores de copier es posible indicar un sufijo vacío para que se procesen todas, en caso de que lo veáis interesante.
La variables se indican en el fichero copier.yml, que es más simple
por ser un fichero YAML, y permite poner valores que afectan al resultado
del comando. En este caso, vamos a indicar otro sufijo (que sería .tmpl
por defecto en la versión usada).
(env) gerard@arcadia:~/projects/bootstrap$ cat falcon_api.copier/copier.yml
_templates_suffix: .jinja
package_name: myapi
(env) gerard@arcadia:~/projects/bootstrap$
Solo nos faltaría invocar a copier para crear una instancia de nuestro proyecto nuevo. En este caso, la carpeta destino no se indica como variable, sino como argumento en la invocación:
(env) gerard@arcadia:~/projects/bootstrap$ copier falcon_api.copier/ folder2
package_name? Format: yaml
🎤 [myapi]:
create requirements.txt
create server.sh
create myapi/
create myapi/__init__.py
(env) gerard@arcadia:~/projects/bootstrap$
Y obtenemos exactamente el mismo resultado de antes, sin sorpresas:
(env) gerard@arcadia:~/projects/bootstrap$ tree folder2/
folder2/
├── myapi
│ └── __init__.py
├── requirements.txt
└── server.sh
1 directory, 3 files
(env) gerard@arcadia:~/projects/bootstrap$
(env) gerard@arcadia:~/projects/bootstrap$ cat folder2/server.sh
#!/bin/bash
export PYTHONDONTWRITEBYTECODE=" "
gunicorn --reload --bind 127.0.0.1:8080 myapi:app
(env) gerard@arcadia:~/projects/bootstrap$
WARNING: Debido a un bug, no se conservan los permisos de los ficheros
originales; en este caso, hemos perdido el flag de ejecución de server.sh.
Ahora bien, hay algo que tiene copier que promete mucho: es capaz de actualizar un proyecto ya creado desde una plantilla (que posteriormente ha evolucionado), siempre que ambos estén versionados con git. Esto es algo que tengo que investigar en el futuro.