
Python es un gran lenguaje para aplicaciones de consola, y destaca una gran cantidad de bibliotecas para estas tareas.
¿Pero qué bibliotecas existen? ¿Y cuál es mejor tomar? Este material compara herramientas populares y no muy útiles para el mundo de las consolas y trata de responder la segunda pregunta.
Para facilitar la lectura, la revisión se divide en dos publicaciones: la primera compara las seis bibliotecas más populares, la segunda, la menos popular y más específica, pero aún merece atención.
En cada ejemplo, una utilidad de consola para la biblioteca
todolib se escribirá en Python 3.7, con la que puede crear, ver, etiquetar y eliminar tareas. El resto se agregará sujeto a la simplicidad de implementación en un marco particular. Las tareas mismas se almacenan en un archivo json, que se guardará en una llamada separada, una condición adicional para los ejemplos.
Además de esto, se escribirá una prueba trivial para cada implementación. Pytest con los siguientes accesorios se tomó como marco de prueba:
@pytest.fixture(autouse=True) def db(monkeypatch): """ monkeypatch , """ value = {"tasks": []} monkeypatch.setattr(todolib.TodoApp, "get_db", lambda _: value) return value @pytest.yield_fixture(autouse=True) def check(db): """ """ yield assert db["tasks"] and db["tasks"][0]["title"] == "test" # , EXPECTED = "Task 'test' created with number 1.\n"
En principio, todo lo anterior será suficiente para demostrar las bibliotecas. El código fuente completo está disponible en
este repositorio.
argparse
Argparse tiene una ventaja innegable: está en la biblioteca estándar y su API no es difícil de aprender: hay un analizador, hay argumentos, los argumentos tienen
tipo ,
acción ,
dest ,
predeterminado y
ayuda . Y hay un
subparser : la capacidad de separar parte de los argumentos y la lógica en comandos separados.
Analizador
A primera vista, nada inusual, el analizador es como un analizador. Pero, en mi opinión, la legibilidad no es la mejor en comparación con otras bibliotecas, porque Los argumentos de los diferentes comandos se describen en un solo lugar.
código fuente def get_parser(): parser = argparse.ArgumentParser("Todo notes - argparse version") parser.add_argument( "--verbose", "-v", action="store_true", help="Enable verbose mode" ) parser.add_argument("--version", "-V", action="store_true", help="Show version") subparsers = parser.add_subparsers(title="Commands", dest="cmd") add = subparsers.add_parser("add", help="Add new task") add.add_argument("title", help="Todo title") show = subparsers.add_parser("show", help="Show tasks") show.add_argument( "--show-done", action="store_true", help="Include done tasks in the output" ) done = subparsers.add_parser("done", help="Mark task as done") done.add_argument("number", type=int, help="Task number") remove = subparsers.add_parser("remove", help="Remove task") remove.add_argument("number", type=int, help="Task number") return parser
principal
Y aquí lo mismo: el analizador, excepto los argumentos de análisis, no puede hacer nada más, por lo que la lógica tendrá que escribirse de forma independiente y en un solo lugar. Por un lado, es posible vivir, por el otro, es posible mejor, pero aún no está claro cómo.
UPD: Como se señaló foldr , de hecho, los subparsers pueden establecer funciones a través de set_defaults (func = foo), es decir, argparse le permite acortar main a tamaños pequeños. Vive y aprende.código fuente def main(raw_args=None): """ Argparse example entrypoint """ parser = get_parser() args = parser.parse_args(raw_args) logging.basicConfig() if args.verbose: logging.getLogger("todolib").setLevel(logging.INFO) if args.version: print(lib_version) exit(0) cmd = args.cmd if not cmd: parser.print_help() exit(1) with TodoApp.fromenv() as app: if cmd == "add": task = app.add_task(args.title) print(task, "created with number", task.number, end=".\n") elif cmd == "show": app.print_tasks(args.show_done) elif cmd == "done": task = app.task_done(args.number) print(task, "marked as done.") elif cmd == "remove": task = app.remove_task(args.number) print(task, "removed from list.")
Prueba
Para verificar la salida de la utilidad, se
utiliza el dispositivo
capsys , que da acceso al texto desde stdout y stderr.
def test_argparse(capsys): todo_argparse.main(["add", "test"]) out, _ = capsys.readouterr() assert out == EXPECTED
Resumen
De las ventajas: un buen conjunto de características para el análisis, la presencia de un módulo en la biblioteca estándar.
Contras - argparse solo está involucrado en analizar argumentos, la mayor parte de la lógica en main tuvo que ser escrita por mí mismo. Y no está claro cómo probar el código de salida en las pruebas.
docopt
docopt es un
analizador pequeño (<600 líneas, en comparación con 2500 con argparse), que te hará sonreír, citando una descripción en GitHub. La idea principal de docopt es describir la interfaz literalmente con texto, por ejemplo, en docstring.
En el mismo github, docopt> 6700 estrellas, se utiliza en al menos otros 22 mil proyectos. ¡Y esto es solo con la implementación de Python! La página del proyecto docopt tiene muchas opciones para diferentes lenguajes, desde C y PHP hasta CoffeeScript e incluso R. Tal plataforma cruzada solo puede explicarse por la compacidad y simplicidad del código.
Analizador
En comparación con argparse, este analizador es un gran paso adelante.
"""Todo notes on docopt. Usage: todo_docopt [-v | -vv ] add <task> todo_docopt [-v | -vv ] show --show-done todo_docopt [-v | -vv ] done <number> todo_docopt [-v | -vv ] remove <number> todo_docopt -h | --help todo_docopt --version Options: -h --help Show help. -v --verbose Enable verbose mode. """
principal
En general, todo es igual que con argparse, pero ahora puede tener varios valores (0-2), y el acceso a los argumentos es diferente: docopt no devuelve un espacio de nombres con atributos, sino solo un diccionario, donde se indica la elección de un comando a través de su booleano, como se ve en
si :
código fuente def main(argv=None): args = docopt(__doc__, argv=argv, version=lib_version) log.setLevel(levels[args["--verbose"]]) logging.basicConfig() log.debug("Arguments: %s", args) with TodoApp.fromenv() as app: if args["add"]: task = app.add_task(args["<task>"]) print(task, "created with number", task.number, end=".\n") elif args["show"]: app.print_tasks(args["--show-done"]) elif args["done"]: task = app.task_done(args["<number>"]) print(task, "marked as done.") elif args["remove"]: task = app.remove_task(args["<number>"]) print(task, "removed from list.")
Prueba
Similar a las pruebas de argparse:
def test_docopt(capsys): todo_docopt.main(["add", "test"]) out, _ = capsys.readouterr() assert out == EXPECTED
Resumen
De los beneficios: mucho menos código para el analizador, facilidad de descripción y lectura de comandos y argumentos, versión incorporada.
Contras, en primer lugar, lo mismo que argparse: mucha lógica en
main , no puede probar el código de salida. Además, la versión actual (0.6.2) de docopt aún no es estable y es poco probable que lo sea: el proyecto se desarrolló
activamente desde 2012 hasta finales de 2013, el último compromiso fue el 17 de diciembre. Y lo más desagradable en este momento es que algunos regulares de docopt provocan DeprecationWarning cuando realizan pruebas.
Haga clic en
Click es fundamentalmente diferente de argparse y docopt por la cantidad de características y el enfoque para describir comandos y parámetros a través de decoradores, y se propone que la lógica en sí misma se separe en funciones separadas en lugar de una gran
main . Los autores afirman que Click tiene muchas configuraciones, pero los parámetros estándar deberían ser suficientes. Entre las características, se enfatizan los comandos anidados y su carga diferida.
El proyecto es extremadamente popular: además de tener> 8100 estrellas y usarlo en al menos 174 mil (!) Proyectos, todavía se está desarrollando: la versión 7.0 se lanzó en otoño de 2018, y hasta el día de hoy aparecen nuevos compromisos y solicitudes de fusión. día
Analizador
En la página de documentación, encontré el decorador opción_confirmación, que solicita confirmación del usuario antes de ejecutar el comando. Para demostrarlo, se agregó el comando de borrado, que borra toda la lista de tareas.
código fuente levels = [logging.WARN, logging.INFO, logging.DEBUG] pass_app = click.make_pass_decorator(TodoApp) @click.group() @click.version_option(lib_version, prog_name="todo_click") @click.option("-v", "--verbose", count=True)
principal
Y aquí nos encontramos con la principal ventaja de Click: debido al hecho de que la lógica de los comandos está separada según sus funciones, casi nada queda en main. También se demuestra aquí la capacidad de la biblioteca para recibir argumentos y parámetros de las variables de entorno.
if __name__ == "__main__": cli(auto_envvar_prefix="TODO")
Prueba
En el caso de Click, no hay necesidad de interceptar sys.stdout, ya que hay un módulo
click.testing con un corredor para tales cosas. Y
CliRunner no solo intercepta la salida, sino que también le permite verificar el código de salida, que también es genial. Todo esto permite probar las utilidades de clics sin usar pytest y pasar por alto el módulo de prueba de unidad estándar.
import click.testing def test_click(): runner = click.testing.CliRunner() result = runner.invoke(todo_click.cli, ["add", "test"]) assert result.exit_code == 0 assert result.output == EXPECTED
Resumen
Esto es solo una pequeña parte de lo que Click puede hacer. Del resto de la API: validación de valores, integración con el terminal (colores, buscapersonas al menos, barra de progreso, etc.), devolución de llamadas de resultados, finalización automática y mucho más. Puedes ver sus ejemplos
aquí .
Pros: muchas herramientas para cualquier ocasión, un enfoque original, pero al mismo tiempo conveniente para describir equipos, facilidad de prueba y vida activa del proyecto.
Contras: ¿Cuáles son las desventajas de un "clic"? Esta es una pregunta difícil. ¿Quizás no sabe algo de lo que son capaces las siguientes bibliotecas?
Fuego
Fire no es solo una biblioteca joven (apareció en 2017) para interfaces de consola de Google, es una biblioteca para generar interfaces de consola, citando textualmente,
absolutamente cualquier objeto de Python.
Entre otras cosas, se afirma que fire ayuda en el desarrollo y la depuración de código, ayuda a adaptar el código existente en la CLI, facilita la transición de bash a Python y tiene su propia REPL para el trabajo interactivo. Vamos a ver?
Analizador y principal
fire.Fire es realmente capaz de aceptar cualquier objeto: un módulo, una instancia de clase, un diccionario con nombres de comandos y funciones correspondientes, etc.
Lo importante para nosotros es que Fire permite la transferencia de un objeto de clase. Por lo tanto, el constructor de la clase acepta argumentos comunes a todos los comandos, y sus métodos y atributos son comandos separados. Usaremos esto:
código fuente class Commands: def __init__(self, db=None, verbose=False): level = logging.INFO if verbose else logging.WARNING logging.basicConfig(level=level) logging.getLogger("todolib").setLevel(level) self._app = todolib.TodoApp.fromenv(db) atexit.register(self._app.save) def version(self): return todolib.__version__ def add(self, task): """Add new task.""" task = self._app.add_task(task) print(task, "created with number", task.number, end=".\n") def show(self, show_done=False): """ Show current tasks. """ self._app.print_tasks(show_done) def done(self, number): """ Mark task as done. """ task = self._app.task_done(number) print(task, "marked as done.") def remove(self, number): """ Removes task from the list. """ task = self._app.remove_task(number) print(task, "removed from the list.") def main(args=None): fire.Fire(Commands, command=args)
Banderas en línea
Fire tiene sus propias banderas con una sintaxis especial (deben pasarse después del "-"), que le permiten mirar debajo del capó del analizador y la aplicación en su conjunto:
ejemplos de llamadas $ ./todo_fire.py show -- --trace Fire trace: 1. Initial component 2. Instantiated class "Commands" (todo_fire.py:9) 3. Accessed property "show" (todo_fire.py:25) $ ./todo_fire.py -- --verbose | head -n 12
Prueba
Probar la función principal es similar a probar argparse y docopt, por lo que no veo el punto aquí.
Al mismo tiempo, vale la pena señalar que, debido a la naturaleza introspectiva de Fire, es igualmente posible probar la clase Comandos de inmediato.
Resumen
El fuego es una herramienta no menos interesante que el clic. No requiere enumerar muchas opciones en el analizador, la configuración es mínima, hay opciones para la depuración, y la biblioteca misma
vive y se desarrolla aún más activamente que el clic (60 confirmaciones este verano).
Contras: puede significativamente menos que el clic y otros analizadores; API inestable (la versión actual es 0.2.1).
Cemento
De hecho,
Cement no
es exactamente una biblioteca CLI, sino un marco para aplicaciones de consola, pero se afirma que es adecuado para scripts y aplicaciones complejas con diversas integraciones.
Analizador
El analizador en Cement parece inusual, pero si observa de cerca los parámetros, es fácil adivinar que el argumento familiar está bajo el capó. Pero tal vez esto sea lo mejor: no es necesario aprender nuevos parámetros.
código fuente from cement import Controller, ex class Base(Controller): class Meta: label = "base" arguments = [ ( ["-v", "--version"], {"action": "version", "version": f"todo_cement v{todolib.__version__}"}, ) ] def _default(self): """Default action if no sub-command is passed.""" self.app.args.print_help() @ex(help="Add new task", arguments=[(["task"], {"help": "Task title"})]) def add(self): title = self.app.pargs.task self.app.log.debug(f"Task title: {title!r}") task = self.app.todoobj.add_task(title) print(task, "created with number", task.number, end=".\n") @ex( help="Show current tasks", arguments=[ (["--show-done"], dict(action="store_true", help="Include done tasks")) ], ) def show(self): self.app.todoobj.print_tasks(self.app.pargs.show_done) @ex(help="Mark task as done", arguments=[(["number"], {"type": int})]) def done(self): task = self.app.todoobj.task_done(self.app.pargs.number) print(task, "marked as done.") @ex(help="Remove task from the list", arguments=[(["number"], {"type": int})]) def remove(self): task = self.app.todoobj.remove_task(self.app.pargs.number) print(task, "removed from the list.")
Aplicación y principal
El cemento, entre otras cosas, todavía envuelve señales en excepciones. Esto se demuestra aquí en la salida de código cero con SIGINT / SIGTERM.
código fuente class TodoApp(App): def __init__(self, argv=None): super().__init__(argv=argv) self.todoobj = None def load_db(self): self.todoobj = todolib.TodoApp.fromenv() def save(self): self.todoobj.save() class Meta:
Si tiene una idea de main, puede ver que cargar y guardar todolib.TodoApp también se puede hacer en el __enter __ / __ exit__ anulado, pero estas fases finalmente se separaron en métodos separados para demostrar los ganchos de cemento.
Prueba
Para las pruebas, puede usar la misma clase de aplicación:
def test_cement(capsys): with todo_cement.TodoApp(argv=["add", "test"]) as app: app.run() out, _ = capsys.readouterr() assert out == EXPECTED
Resumen
Pros: El conjunto de API es como un conjunto de cuchillos suizos, extensibilidad a través de ganchos y complementos, una interfaz estable y desarrollo activo.
Contras: en lugares documentación vacía; Los pequeños guiones basados en cemento pueden parecer un poco complicados.
Cleo
Cleo está lejos de ser un marco tan popular como los otros enumerados aquí (alrededor de 400 estrellas en GitHub en total), y sin embargo logré conocerlo cuando estudié cómo Poetry formateó la salida.
Entonces, Cleo es uno de los proyectos del autor del ya mencionado Poesía, una herramienta para administrar dependencias, virtualenvs y compilaciones de aplicaciones. Sobre Poesía en un habr ya más de una vez escribió, y sobre su parte de la consola - no.
Analizador
Cleo, como el cemento, se basa en principios de objeto, es decir los comandos se definen a través de la clase Command y su cadena de documentación, se accede a los parámetros a través del método option (), etc. Además, el método line (), que se utiliza para generar texto, admite estilos (es decir, colores) y filtrado de salida en función del número de marcas detalladas fuera de la caja. Cleo también tiene salida de tabla. Y también barras de progreso. Y sin embargo ... En general, vea:
código fuente from cleo import Command as BaseCommand
principal
Todo lo que se necesita es crear un objeto
cleo.Application y luego pasar comandos para agregarle comandos. Para no repetir durante la prueba, todo esto se transfirió de main al constructor:
from cleo import Application as BaseApplication class TodoApp(BaseApplication): def __init__(self): super().__init__(name="ToDo app - cleo version", version=todolib.__version__) self.add_commands(AddCommand(), ShowCommand(), DoneCommand(), RemoveCommand()) def main(args=None): TodoApp().run(args=args)
Prueba
Para probar comandos en Cleo, existe
CommandTester , que, como todos los
tíos adultos
del marco, intercepta las E / S y el código de salida:
def test_cleo(): app = todo_cleo.TodoApp() command = app.find("add") tester = cleo.CommandTester(command) tester.execute("test") assert tester.status_code == 0 assert tester.io.fetch_output() == "Task test created with number 0.\n"
Resumen
Pros: estructura de objetos con sugerencias de tipo, lo que simplifica el desarrollo (ya que muchos IDE y editores tienen un buen soporte para el código OOP y el módulo de escritura); Una buena cantidad de funcionalidad para trabajar no solo con argumentos, sino también con E / S.
Más o menos: su parámetro de verbosidad, que solo es compatible con I / O Cleo / CliKit. Aunque puede escribir un controlador personalizado para el módulo de registro, puede ser difícil mantenerlo junto con el desarrollo de cleo.
Contras: obviamente, una opinión personal, una API joven: el marco carece de otro usuario "grande", a excepción de Poetry, y Cleo se está desarrollando en paralelo con el desarrollo y las necesidades de uno; a veces la documentación está desactualizada (por ejemplo, los niveles de registro ahora no se encuentran en el módulo clikit, sino en clikit.api.io.flags), y en general es deficiente y no refleja la API completa.
Cleo, en comparación con Cement, está más centrado en la CLI, y él es el único que ha pensado en formatear (ocultar el seguimiento de pila predeterminado) de las excepciones en la salida predeterminada. Pero él, nuevamente una opinión personal, pierde ante Cement en su juventud y la estabilidad de la API.
En conclusión
En este punto, todos ya tienen su propia opinión, lo cual es mejor, pero la conclusión debería ser: me gustó Click, porque hay muchas cosas en él y es bastante fácil desarrollar y probar aplicaciones con él. Si intenta escribir código al mínimo, comience con Fire. Su script necesita acceso a Memcached, formateado con jinja y extensibilidad: tome Cement y no se arrepentirá. Tienes un proyecto favorito o quieres probar otra cosa: mira Cleo.