Perbandingan perpustakaan CLI yang kurang populer dan tidak terlalu: cliff, plac, plumbum, dan lain-lain (bagian 2)

Dalam ekosistem Python, ada banyak paket untuk aplikasi CLI, baik yang populer, seperti Klik, dan tidak banyak. Yang paling umum dipertimbangkan dalam artikel sebelumnya , tetapi sedikit diketahui, tapi tidak kalah menarik, akan ditampilkan di sini.



Seperti pada bagian pertama, skrip konsol untuk pustaka todolib akan ditulis untuk setiap pustaka dengan Python 3.7. Selain itu, tes sepele dengan perlengkapan ini akan ditulis untuk setiap implementasi:

@pytest.fixture(autouse=True) def db(monkeypatch): """ monkeypatch         ,         """ value = {"tasks": []} monkeypatch.setattr(todolib.TodoApp, "save", lambda _: ...) 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" 

Semua kode sumber tersedia di repositori ini .

tebing


Github
Dokumentasi
Banyak yang telah mendengar tentang OpenStack, platform open source untuk IaaS. Sebagian besar ditulis dalam Python, termasuk utilitas konsol yang telah mengulangi fungsi CLI untuk waktu yang lama. Ini berlanjut sampai tebing, atau Kerangka Kerja Formulasi Antarmuka Baris Perintah, muncul sebagai kerangka kerja umum. Dengan itu, pengembang Openstack menggabungkan paket-paket seperti python-novaclient, python-swiftclient dan python-keystoneclient menjadi satu program openstack .

Tim
Pendekatan untuk mendeklarasikan perintah menyerupai semen dan cleo: argparse sebagai parameter parser, dan perintah itu sendiri dibuat melalui pewarisan kelas Command. Pada saat yang sama, ada ekstensi kecil ke kelas Command, seperti Lister, yang secara mandiri memformat data.

kode sumber
 from cliff import command from cliff.lister import Lister class Command(command.Command): """Command with a parser shortcut.""" def get_parser(self, prog_name): parser = super().get_parser(prog_name) self.extend_parser(parser) return parser def extend_parser(self, parser): ... class Add(Command): """Add new task.""" def extend_parser(self, parser): parser.add_argument("title", help="Task title") def take_action(self, parsed_args): task = self.app.todoapp.add_task(parsed_args.title) print(task, "created with number", task.number, end=".\n") class Show(Lister, Command): """Show current tasks.""" def extend_parser(self, parser): parser.add_argument( "--show-done", action="store_true", help="Include done tasks" ) def take_action(self, parsed_args): tasks = self.app.todoapp.list_tasks(show_done=parsed_args.show_done) #     'there is no todos'   #      return ( ("Number", "Title", "Status"), [[task.number, task.title, "" if task.done else "✘"] for task in tasks], ) class Done(Command): """Mark task as done.""" def extend_parser(self, parser): parser.add_argument("number", type=int, help="Task number") def take_action(self, parsed_args): task = self.app.todoapp.task_done(number=parsed_args.number) print(task, "marked as done.") #   Done    class Remove(Done): """Remove task from the list.""" def take_action(self, parsed_args): task = self.app.todoapp.remove_task(number=parsed_args.number) print(task, "removed from the list.") 


Aplikasi dan utama

Kelas aplikasi memiliki metode initialize_app dan clean_up , dalam kasus kami, mereka menginisialisasi aplikasi dan menyimpan data.

kode sumber
 from cliff import app from cliff.commandmanager import CommandManager from todolib import TodoApp, __version__ class App(app.App): def __init__(self): #   add_command, CommandManager  #    setuptools entrypoint manager = CommandManager("todo_cliff") manager.add_command("add", Add) manager.add_command("show", Show) manager.add_command("done", Done) manager.add_command("remove", Remove) super().__init__( description="Todo notes on cliff", version=__version__, command_manager=manager, deferred_help=True, ) self.todoapp = None def initialize_app(self, argv): self.todoapp = TodoApp.fromenv() def clean_up(self, cmd, result, err): self.todoapp.save() def main(args=sys.argv[1:]) -> int: app = App() return app.run(argv=args) 


Contoh kerja

 igor$ ./todo_cliff.py add "sell the old laptop" Using database file /home/igor/.local/share/todoapp/db.json Task 'sell the old laptop' created with number 0. Saving database to a file /home/igor/.local/share/todoapp/db.json 

Keluar dari kotak! Dan jika Anda melihat di bawah tenda , Anda dapat melihat bahwa itu dilakukan dengan cara yang cerdas: info di stdout, dan peringatan / kesalahan di stderr, dan, jika perlu, dinonaktifkan oleh bendera --quiet .

 igor$ ./todo_cliff.py -q show +--------+----------------------+--------+ | Number | Title | Status | +--------+----------------------+--------+ | 1 | sell the old laptop | ✘ | +--------+----------------------+--------+ 

Seperti yang telah disebutkan, Lister memformat data, tetapi tabel tidak terbatas pada:

 igor$ ./todo_cliff.py -q show -f json --noindent [{"Number": 0, "Title": "sell old laptop", "Status": "\u2718"}] 

Selain json dan tabel, yaml dan csv juga tersedia.

Ada juga jejak tersembunyi default:

 igor$ ./todo_cliff.py -q remove 3 No such task. 

REPL dan pencarian fuzzy lain yang tersedia alias fuzzy search :

 igor$ ./todo_cliff.py -q (todo_cliff) help Shell commands (type help %topic%): =================================== alias exit history py quit shell unalias edit help load pyscript set shortcuts Application commands (type help %topic%): ========================================= add complete done help remove show (todo_cliff) whow todo_cliff: 'whow' is not a todo_cliff command. See 'todo_cliff --help'. Did you mean one of these? show 

Pengujian

Sederhana: objek App dibuat dan dijalankan () juga disebut, yang mengembalikan kode keluar.

 def test_cliff(capsys): app = todo_cliff.App() code = app.run(["add", "test"]) assert code == 0 out, _ = capsys.readouterr() assert out == EXPECTED 

Pro dan kontra

Pro:

  • Berbagai fasilitas di luar kotak;
  • Dikembangkan oleh OpenStack;
  • Mode interaktif;
  • Ekstensibilitas melalui entrypoint setuptools dan CommandHook;
  • Plugin Sphinx untuk dokumentasi otomatis ke CLI;
  • Perintah selesai (hanya bash);

Cons:

  • Dokumentasi kecil, yang pada dasarnya terdiri dari contoh yang terperinci tetapi tunggal;

Bug lain ditemukan: jika terjadi kesalahan saat menyembunyikan jejak stack, kode keluar selalu nol.

Plac


Github
Dokumentasi

Pada pandangan pertama, Plac tampaknya seperti Api, tetapi dalam kenyataannya itu seperti Api, yang menyembunyikan argparse yang sama dan lebih banyak di bawah tenda.

Plac mengikuti, mengutip dokumentasi, "prinsip kuno dunia komputer: Program seharusnya hanya menyelesaikan kasus-kasus biasa dan sederhana harus tetap sederhana, dan kompleks pada saat yang sama dapat dicapai ." Penulis kerangka kerja ini telah menggunakan Python selama lebih dari sembilan tahun dan menulisnya dengan harapan bisa menyelesaikan "99,9% tugas".

Perintah dan utama

Perhatian pada anotasi dalam acara dan metode yang dilakukan: inilah cara Plac mem-parsing parameter dan argumen masing-masing.

kode sumber
 import plac import todolib class TodoInterface: commands = "add", "show", "done", "remove" def __init__(self): self.app = todolib.TodoApp.fromenv() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.app.save() def add(self, task): """ Add new task. """ task = self.app.add_task(title=task) print(task, "created with number", task.number, end=".\n") def show(self, show_done: plac.Annotation("Include done tasks", kind="flag")): """ Show current tasks. """ self.app.print_tasks(show_done=show_done) def done(self, number: "Task number"): """ Mark task as done. """ task = self.app.task_done(number=int(number)) print(task, "marked as done.") def remove(self, number: "Task number"): """ Remove task from the list. """ task = self.app.remove_task(number=int(number)) print(task, "removed from the list.") if __name__ == "__main__": plac.Interpreter.call(TodoInterface) 


Pengujian

Sayangnya, menguji kode keluar, Plac tidak mengizinkan. Tetapi pengujian itu sendiri adalah nyata:

 def test_plac(capsys): plac.Interpreter.call(todo_plac.TodoInterface, arglist=["add", "test"]) out, _ = capsys.readouterr() assert out == EXPECTED 

Pro dan kontra

Pro:

  • Penggunaan sederhana;
  • Mode interaktif dengan dukungan readline;
  • API Stabil
  • Dokumentasi yang luar biasa;

Tetapi hal yang paling menarik tentang Plac tersembunyi di penggunaan lanjutan :

  • Eksekusi beberapa perintah di utas dan subproses;
  • Komputasi paralel;
  • server telnet;

Cons:

  • Anda tidak dapat menguji kode keluar;
  • Kehidupan proyek yang buruk .

Timah hitam


Github
Dokumentasi
Dokumentasi CLI
Timah hitam, pada kenyataannya, bukan kerangka kerja yang kurang dikenal - hampir 2000 bintang, dan ada sesuatu yang disukai untuk itu, karena, secara kasar, itu mengimplementasikan sintaks UNIX Shell. Nah, dengan aditif:

 >>> from plumbum import local >>> output = local["ls"]() >>> output.split("\n")[:3] ['console_examples.egg-info', '__pycache__', 'readme.md'] #  plumbum     cmd,      >>> from plumbum.cmd import rm, ls, grep, wc >>> rm["-r", "console_examples.egg-info"]() '' >>> chain = ls["-a"] | grep["-v", "\\.py"] | wc["-l"] >>> chain() '11\n' 

Perintah dan utama

Timah hitam juga memiliki alat untuk CLI, dan bukan untuk mengatakan bahwa mereka hanyalah tambahan: ada nargs, perintah, dan warna:

kode sumber
 from plumbum import cli, colors class App(cli.Application): """Todo notes on plumbum.""" VERSION = todolib.__version__ verbosity = cli.CountOf("-v", help="Increase verbosity") def main(self, *args): if args: print(colors.red | f"Unknown command: {args[0]!r}.") return 1 if not self.nested_command: # will be ``None`` if no sub-command follows print(colors.red | "No command given.") return 1 class Command(cli.Application): """Command with todoapp object""" def __init__(self, executable): super().__init__(executable) self.todoapp = todolib.TodoApp.fromenv() atexit.register(self.todoapp.save) def log_task(self, task, msg): print("Task", colors.green | task.title, msg, end=".\n") @App.subcommand("add") class Add(Command): """Add new task""" def main(self, task): task = self.todoapp.add_task(title=task) self.log_task(task, "added to the list") @App.subcommand("show") class Show(Command): """Show current tasks""" show_done = cli.Flag("--show-done", help="Include done tasks") def main(self): self.todoapp.print_tasks(self.show_done) @App.subcommand("done") class Done(Command): """Mark task as done""" def main(self, number: int): task = self.todoapp.task_done(number) self.log_task(task, "marked as done") @App.subcommand("remove") class Remove(Command): """Remove task from the list""" def main(self, number: int): task = self.todoapp.remove_task(number) self.log_task(task, "removed from the list.") if __name__ == '__main__': App.run() 


Pengujian

Menguji aplikasi pada Timah berbeda dari yang lain, kecuali bahwa kebutuhan untuk lulus juga nama aplikasi, mis. argumen pertama:

 def test_plumbum(capsys): _, code = todo_plumbum.App.run(["todo_plumbum", "add", "test"], exit=False) assert code == 0 out, _ = capsys.readouterr() assert out == "Task test created with number 0.\n" 

Pro dan kontra

Pro:

  • Toolkit yang sangat baik untuk bekerja dengan tim eksternal;
  • Dukungan untuk gaya dan warna;
  • API Stabil
  • Kehidupan aktif proyek;

Tidak ada kekurangan yang terlihat.

cmd2


Github
Dokumentasi

Pertama-tama, cmd2 adalah ekstensi lebih dari cmd dari perpustakaan standar,
yaitu Ini dimaksudkan untuk aplikasi interaktif. Namun demikian, ini termasuk dalam ulasan, karena juga dapat dikonfigurasi untuk mode CLI normal.

Perintah dan utama

cmd2 membutuhkan kepatuhan dengan aturan tertentu: perintah harus dimulai dengan awalan do_ , tetapi sebaliknya semuanya jelas:

mode interaktif
 import cmd2 import todolib class App(cmd2.Cmd): def __init__(self, **kwargs): super().__init__(**kwargs) self.todoapp = todolib.TodoApp.fromenv() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.todoapp.save() def do_add(self, title): """Add new task.""" task = self.todoapp.add_task(str(title)) self.poutput(f"{task} created with number {task.number}.") def do_show(self, show_done): """Show current tasks.""" self.todoapp.print_tasks(bool(show_done)) def do_done(self, number): """Mark task as done.""" task = self.todoapp.task_done(int(number)) self.poutput(f"{task} marked as done.") def do_remove(self, number): """Remove task from the list.""" task = self.todoapp.remove_task(int(number)) self.poutput(f"{task} removed from the list.") def main(**kwargs): with App(**kwargs) as app: app.cmdloop() if __name__ == '__main__': main() 


mode non-interaktif
Mode normal memerlukan sedikit gerakan ekstra.

Misalnya, Anda harus kembali ke argparse dan menulis logika untuk kasus ketika skrip dipanggil tanpa parameter. Dan sekarang perintahnya mendapatkan argparse.Namespace .

Parser diambil dari contoh argparse dengan tambahan kecil - sekarang sub-Parser adalah atribut dari ArgumentParser utama.

 import cmd2 from todo_argparse import get_parser parser = get_parser(progname="todo_cmd2_cli") class App(cmd2.Cmd): def __init__(self, **kwargs): super().__init__(**kwargs) self.todoapp = todolib.TodoApp.fromenv() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.todoapp.save() def do_add(self, args): """Add new task.""" task = self.todoapp.add_task(args.title) self.poutput(f"{task} created with number {task.number}.") def do_show(self, args): """Show current tasks.""" self.todoapp.print_tasks(args.show_done) def do_done(self, args): """Mark task as done.""" task = self.todoapp.task_done(args.number) self.poutput(f"{task} marked as done.") def do_remove(self, args): """Remove task from the list.""" task = self.todoapp.remove_task(args.number) self.poutput(f"{task} removed from the list.") parser.add.set_defaults(func=do_add) parser.show.set_defaults(func=do_show) parser.done.set_defaults(func=do_done) parser.remove.set_defaults(func=do_remove) @cmd2.with_argparser(parser) def do_base(self, args): func = getattr(args, "func", None) if func: func(self, args) else: print("No command provided.") print("Call with --help to get available commands.") def main(argv=None): with App() as app: app.do_base(argv or sys.argv[1:]) if __name__ == '__main__': main() 


Pengujian

Hanya skrip interaktif yang akan diuji.

Karena pengujian aplikasi interaktif membutuhkan sejumlah besar sumber daya manusia dan waktu, pengembang cmd2 mencoba memecahkan masalah ini dengan bantuan transkripsi - file teks dengan contoh input dan output yang diharapkan. Sebagai contoh:

 (Cmd) add test Task 'test' created with number 0. 

Dengan demikian, semua yang diperlukan adalah mentransfer daftar file dengan transkripsi ke Aplikasi :

 def test_cmd2(): todo_cmd2.main(transcript_files=["tests/transcript.txt"]) 

Pro dan kontra

Pro:

  • API yang baik untuk aplikasi interaktif;
  • Berkembang aktif dalam beberapa tahun terakhir;
  • Pendekatan asli untuk pengujian;
  • Dokumentasi yang bagus.

Cons:

  • Membutuhkan argparse dan kode tambahan saat menulis aplikasi non-interaktif;
  • API tidak stabil;
  • Di beberapa tempat, dokumentasinya kosong.

Bonus: Urwid


Github
Dokumentasi
Tutorial
Contoh Program

Urwid adalah kerangka kerja dari dunia yang sedikit berbeda - dari kutukan dan npyscreen, mis. Dari Console / Terminal UI. Namun demikian, itu termasuk dalam ulasan sebagai bonus, karena, menurut pendapat saya, itu patut diperhatikan.

Aplikasi dan tim

Urwid memiliki banyak widget, tetapi tidak memiliki konsep seperti jendela atau alat sederhana untuk mengakses widget tetangga. Jadi, jika Anda ingin mendapatkan hasil yang indah, Anda akan memerlukan desain yang bijaksana dan / atau menggunakan paket lain, jika tidak, Anda harus mentransfer data dalam atribut tombol, seperti di sini:

kode sumber
 import urwid from urwid import Button import todolib class App(urwid.WidgetPlaceholder): max_box_levels = 4 def __init__(self): super().__init__(urwid.SolidFill()) self.todoapp = None self.box_level = 0 def __enter__(self): self.todoapp = todolib.TodoApp.fromenv() self.new_menu( "Todo notes on urwid", #       ,  #      Button("New task", on_press=add), Button("List tasks", on_press=list_tasks), ) return self def __exit__(self, exc_type, exc_val, exc_tb): self.todoapp.save() def new_menu(self, title, *items): self.new_box(menu(title, *items)) def new_box(self, widget): self.box_level += 1 # overlay      , #     LineBox    self.original_widget = urwid.Overlay( # LineBox  unicode-    self.original_widget, align="center", width=30, valign="middle", height=10, ) def popup(self, text): self.new_menu(text, Button("To menu", on_press=lambda _: self.pop(levels=2))) def keypress(self, size, key): if key != "esc": super().keypress(size, key=key) elif self.box_level > 0: self.pop() def pop(self, levels=1): for _ in range(levels): self.original_widget = self.original_widget[0] self.box_level -= levels if self.box_level == 0: raise urwid.ExitMainLoop() 


tim
 app = App() def menu(title, *items) -> urwid.ListBox: body = [urwid.Text(title), urwid.Divider()] body.extend(items) return urwid.ListBox(urwid.SimpleFocusListWalker(body)) def add(button): edit = urwid.Edit("Title: ") def handle(button): text = edit.edit_text app.todoapp.add_task(text) app.popup("Task added") app.new_menu("New task", edit, Button("Add", on_press=handle)) def list_tasks(button): tasks = app.todoapp.list_tasks(show_done=True) buttons = [] for task in tasks: status = "done" if task.done else "not done" text = f"{task.title} [{status}]" #         button = Button(text, on_press=task_actions, user_data=task.number) buttons.append(button) app.new_menu("Task list", *buttons) def task_actions(button, number): def done(button, number): app.todoapp.task_done(number) app.popup("Task marked as done.") def remove(button, number): app.todoapp.remove_task(number) app.popup("Task removed from the list.") btn_done = Button("Mark as done", on_press=done, user_data=number) btn_remove = Button("Remove from the list", on_press=remove, user_data=number) app.new_menu("Actions", btn_done, btn_remove) 


utama
 if __name__ == "__main__": try: with app: urwid.MainLoop(app).run() except KeyboardInterrupt: pass 

Pro dan kontra

Pro:

  • API luar biasa untuk menulis berbagai aplikasi TUI;
  • Sejarah perkembangan panjang (sejak 2010) dan API stabil;
  • Arsitektur yang kompeten;
  • Dokumentasi yang bagus, ada contohnya.

Cons:

  • Cara tesnya tidak jelas. Hanya tombol kirim tmux yang terlintas dalam pikiran;
  • Kesalahan tidak informatif ketika widget tidak diatur dengan benar.

* * *


Tebing sangat mirip dengan Cleo dan Cement dan umumnya bagus untuk proyek besar.

Saya pribadi tidak akan berani menggunakan Plac, tetapi saya sarankan membaca kode sumber.

Timah memiliki toolkit CLI yang praktis dan API yang brilian untuk menjalankan perintah lain, jadi jika Anda menulis ulang skrip shell dengan Python, maka inilah yang Anda butuhkan.
cmd2 sangat cocok sebagai dasar untuk aplikasi interaktif dan bagi mereka yang ingin bermigrasi dari cmd standar.

Dan Urwid menampilkan aplikasi konsol yang cantik dan ramah pengguna.

Paket-paket berikut ini tidak termasuk dalam ulasan:

  • aioconsole - tidak ada mode non-interaktif;
  • pyCLI - tidak ada dukungan untuk sub-perintah;
  • Clint - tidak ada dukungan untuk sub-perintah, repositori dalam arsip;
  • commandline - terlalu lama (rilis terakhir tahun 2009) dan tidak menarik;
  • CLIArgs - old (rilis terakhir pada 2010)
  • opterator - relatif lama (rilis terakhir pada tahun 2015)

Source: https://habr.com/ru/post/id469093/


All Articles