Perbandingan perpustakaan CLI populer untuk Python: klik, semen, api, dan lainnya



Python adalah bahasa yang bagus untuk aplikasi konsol, dan menyoroti sejumlah besar pustaka untuk tugas-tugas ini. Tapi perpustakaan apa yang ada? Dan mana yang lebih baik untuk diambil? Materi ini membandingkan alat yang populer dan tidak terlalu untuk dunia konsol dan memberikan upaya untuk menjawab pertanyaan kedua.

Untuk kemudahan membaca, ulasan dibagi menjadi dua tulisan: yang pertama membandingkan enam perpustakaan paling populer, yang kedua - yang kurang populer dan lebih spesifik, tetapi masih layak diperhatikan.

Pada masing-masing contoh, utilitas konsol untuk perpustakaan todolib akan ditulis dalam Python 3.7, yang dengannya Anda dapat membuat, melihat, menandai, dan menghapus tugas. Sisanya akan ditambahkan dengan kesederhanaan implementasi pada kerangka kerja tertentu. Tugas itu sendiri disimpan dalam file json, yang akan disimpan dalam panggilan terpisah - syarat tambahan untuk contoh.
Selain itu, tes sepele akan ditulis untuk setiap implementasi. Pytest dengan perlengkapan berikut diambil sebagai kerangka pengujian:

@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" 

Pada prinsipnya, semua hal di atas akan cukup untuk menunjukkan perpustakaan. Kode sumber lengkap tersedia di repositori ini .

argparse


Argparse memiliki keunggulan yang tidak dapat disangkal - ia ada di pustaka standar dan API-nya tidak sulit untuk dipelajari: ada parser, ada argumen, argumen memiliki tipe , action , dest , default , dan bantuan . Dan ada subparser - kemampuan untuk memisahkan bagian dari argumen dan logika menjadi perintah terpisah.

Parser


Sekilas - tidak ada yang aneh, pengurai seperti pengurai. Tetapi - menurut saya - keterbacaan bukan yang terbaik bila dibandingkan dengan perpustakaan lain, karena argumen untuk perintah yang berbeda dijelaskan di satu tempat.

kode sumber
 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 


utama


Dan di sini hal yang sama - parser kecuali untuk argumen parsing tidak bisa berbuat apa-apa lagi, jadi logika harus ditulis secara independen dan di satu tempat. Di satu sisi - mungkin untuk hidup, di sisi lain - mungkin lebih baik, tetapi belum jelas bagaimana.

UPD: Seperti yang dicatat foldr , sebenarnya, subparser dapat mengatur fungsi melalui set_defaults (func = foo), yaitu, argparse memungkinkan Anda untuk mempersingkat main menjadi ukuran kecil. Hidup dan belajar.

kode sumber
 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.") 


Pengujian


Untuk memeriksa output dari utilitas, fixture capsys digunakan , yang memberikan akses ke teks dari stdout dan stderr.

 def test_argparse(capsys): todo_argparse.main(["add", "test"]) out, _ = capsys.readouterr() assert out == EXPECTED 

Ringkasan


Dari kelebihan - seperangkat fitur yang baik untuk parsing, kehadiran modul di perpustakaan standar.

Cons - argparse hanya terlibat dalam argumen parsing, sebagian besar logika utama harus ditulis sendiri. Dan tidak jelas cara menguji kode keluar dalam tes.

docopt


docopt adalah parser kecil (<600 baris, dibandingkan dengan 2500 dengan argparse), yang akan membuat Anda tersenyum, mengutip deskripsi pada GitHub. Gagasan utama docopt adalah untuk menggambarkan antarmuka secara harfiah dengan teks, misalnya, dalam docstring.

Pada github yang sama, docopt> 6700 bintang, digunakan di setidaknya 22 ribu proyek lainnya. Dan ini hanya dengan implementasi python! Halaman proyek docopt memiliki banyak pilihan untuk bahasa yang berbeda, dari C dan PHP ke CoffeeScript dan bahkan R. Seperti cross-platform hanya dapat dijelaskan oleh kekompakan dan kesederhanaan kode.

Parser


Dibandingkan dengan argparse, parser ini merupakan langkah maju yang besar.

 """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. """ 

utama


Secara umum, semuanya sama dengan argparse, tetapi sekarang verbose dapat memiliki beberapa nilai (0-2), dan akses ke argumen berbeda: docopt mengembalikan bukan namespace dengan atribut, tetapi hanya kamus, di mana pilihan perintah ditunjukkan melalui booleannya, seperti yang terlihat di jika :

kode sumber
 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.") 


Pengujian


Mirip dengan pengujian argparse:
 def test_docopt(capsys): todo_docopt.main(["add", "test"]) out, _ = capsys.readouterr() assert out == EXPECTED 

Ringkasan


Dari manfaat - kode lebih sedikit untuk parser, kemudahan deskripsi dan membaca perintah dan argumen, versi bawaan.

Kontra, pertama, sama seperti argparse - banyak logika di utama , Anda tidak dapat menguji kode keluar. Selain itu, versi saat ini (0,6.2) dari docopt belum stabil dan tidak mungkin - proyek ini secara aktif berkembang dari 2012 hingga akhir 2013, komitmen terakhir adalah pada 17 Desember. Dan hal yang paling tidak menyenangkan saat ini adalah bahwa beberapa pelanggan tetap docopt memprovokasi DeprecationWarning ketika melakukan tes.

Klik


Klik pada dasarnya berbeda dari argparse dan docopt dengan jumlah fitur dan pendekatan untuk mendeskripsikan perintah dan parameter melalui dekorator, dan logika itu sendiri diusulkan untuk dipisahkan ke dalam fungsi-fungsi yang terpisah alih-alih utama yang besar. Para penulis mengklaim bahwa Klik memiliki banyak pengaturan, tetapi parameter standar harus cukup. Di antara fitur-fiturnya, perintah bersarang dan pemuatan malasnya ditekankan.

Proyek ini sangat populer: selain memiliki> 8100 bintang dan menggunakannya dalam setidaknya 174 ribu (!) Proyek, masih berkembang: versi 7.0 dirilis pada musim gugur 2018, dan komitmen baru serta permintaan gabungan muncul hingga hari ini hari

Parser


Pada halaman dokumentasi, saya menemukan dekorator confirm_option, yang meminta konfirmasi dari pengguna sebelum menjalankan perintah. Untuk mendemonstrasikannya, perintah penghapusan ditambahkan, yang menghapus seluruh daftar tugas.

kode sumber
 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) # click   ,     @click.option("--db", help="Path to the database file") @click.pass_context def cli(ctx, verbose, db): """Todo notes - click version.""" level = levels[min(verbose, 2)] logging.basicConfig(level=level) logging.getLogger("todolib").setLevel(level) ctx.obj = TodoApp.fromenv(db) atexit.register(ctx.obj.save) @cli.command() @click.argument("task") @pass_app def add(app, task): """ Add new task. """ task = app.add_task(task) click.echo(f"{task} created with number {task.number}.") @cli.command() @click.option("--show-done", is_flag=True, help="Include done tasks") @pass_app def show(app, show_done): """ Show current tasks. """ app.print_tasks(show_done) @cli.command() @click.argument("number", type=int) @pass_app def done(app, number): """ Mark task as done. """ task = app.task_done(number) click.echo(f"{task} marked as done.") @cli.command() @click.argument("number", type=int) @pass_app def remove(app, number): """ Remove task from the list. """ task = app.remove_task(number) click.echo(f"{task} removed from the list.") @cli.command() @click.confirmation_option(prompt="Are you sure you want to remove database") @pass_app def wipe(app): for task in app.list_tasks(): task.remove() 


utama


Dan di sini kita bertemu dengan keunggulan utama Click - karena fakta bahwa logika perintah ditempatkan sesuai dengan fungsinya, hampir tidak ada yang tersisa di utama. Juga ditunjukkan di sini adalah kemampuan perpustakaan untuk menerima argumen dan parameter dari variabel lingkungan.

 if __name__ == "__main__": cli(auto_envvar_prefix="TODO") 

Pengujian


Dalam kasus Klik, tidak perlu mencegat sys.stdout, karena ada modul click.testing dengan pelari untuk hal-hal seperti itu. Dan CliRunner sendiri tidak hanya mencegat output, tetapi juga memungkinkan Anda untuk memeriksa kode keluar, yang juga keren. Semua ini memungkinkan pengujian utilitas klik tanpa menggunakan pytest dan melewati modul unittest standar.

 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 

Ringkasan


Ini hanya sebagian kecil dari apa yang bisa dilakukan oleh Klik. Dari API lainnya - validasi nilai, integrasi dengan terminal (warna, pager a la less, progress bar, dll.), Hasil panggilan balik, pelengkapan otomatis dan banyak lagi. Anda dapat melihat contohnya di sini .

Pro: banyak alat untuk setiap kesempatan, asli, tetapi pada saat yang sama pendekatan yang mudah digunakan untuk menggambarkan tim, kemudahan pengujian dan kehidupan proyek yang aktif.

Cons: Apa kerugian dari "klik" - ini adalah pertanyaan yang sulit. Mungkin dia tidak tahu sesuatu dari apa yang mampu dimiliki perpustakaan berikut?

Api


Fire bukan hanya perpustakaan muda (muncul pada tahun 2017) untuk antarmuka konsol dari Google, itu adalah perpustakaan untuk menghasilkan antarmuka konsol dari, mengutip kata demi kata, benar-benar objek Python apa pun .
Antara lain, dinyatakan bahwa api membantu dalam pengembangan dan debugging kode, membantu untuk menyesuaikan kode yang ada di CLI, memfasilitasi transisi dari bash ke Python, dan memiliki REPL sendiri untuk pekerjaan interaktif. Bisakah kita melihat?

Parser dan main


fire.Fire benar-benar mampu menerima objek apa pun: modul, instance kelas, kamus dengan nama perintah dan fungsi terkait, dan sebagainya.

Yang penting bagi kami adalah bahwa Api memungkinkan transfer objek kelas. Dengan demikian, konstruktor kelas menerima argumen yang umum untuk semua perintah, dan metode dan atributnya adalah perintah yang terpisah. Kami akan menggunakan ini:

kode sumber
 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) 


Bendera sebaris


Fire memiliki benderanya sendiri dengan sintaks khusus (mereka harus dilewati setelah "-"), yang memungkinkan Anda untuk melihat di bawah kap parser dan aplikasi secara keseluruhan:

sebut contoh
 $ ./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 #    , ,  Commands._app NAME todo_fire.py - SYNOPSIS todo_fire.py - GROUP | COMMAND GROUPS GROUP is one of the following: _app Todo Application definition. $ ./todo_fire.py show -- --interactive Fire is starting a Python REPL with the following objects: Modules: atexit, fire, logging, todolib Objects: Commands, args, component, main, result, self, todo_fire.py, trace Python 3.7.4 (default, Aug 15 2019, 13:09:37) [GCC 7.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) >>> self <__main__.Commands object at 0x7fd0a6125bd0> >>> self._app.db {'tasks': [{'title': 'test', 'done': False}]} 


Pengujian


Menguji fungsi utama mirip dengan menguji argparse dan docopt, jadi saya tidak melihat intinya di sini.

Pada saat yang sama, perlu dicatat bahwa, karena sifat Api yang introspektif, sama-sama mungkin untuk menguji kelas Perintah dengan segera.

Ringkasan


Api adalah alat yang tidak kalah menarik dari klik. Tidak memerlukan daftar banyak opsi di parser, konfigurasi minimal, ada opsi untuk debugging, dan perpustakaan itu sendiri hidup dan berkembang bahkan lebih aktif daripada klik (60 dilakukan musim panas ini).

Cons: secara signifikan dapat kurang dari klik dan parser lainnya; API tidak stabil (versi saat ini adalah 0.2.1).

Semen


Faktanya, Cement bukanlah perpustakaan CLI, tetapi kerangka kerja untuk aplikasi konsol, tetapi diklaim cocok untuk skrip dan aplikasi kompleks dengan berbagai integrasi.

Parser


Parser di Semen terlihat tidak biasa, tetapi jika Anda melihat dengan cermat pada parameternya, mudah untuk menebak bahwa argparse yang sudah dikenal ada di bawah kap. Tapi mungkin ini yang terbaik - tidak perlu mempelajari parameter baru.

kode sumber
 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.") 


Aplikasi dan utama


Semen, antara lain, masih membungkus sinyal dalam pengecualian. Ini ditunjukkan di sini pada output kode nol dengan SIGINT / SIGTERM.

kode sumber
 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: # application label label = "todo_cement" # register handlers handlers = [Base] hooks = [("post_setup", lambda app: app.load_db()), ("pre_close", lambda app: app.save())] # call sys.exit() on close close_on_exit = True def main(): with TodoApp() as app: try: app.run() except CaughtSignal as e: if e.signum not in (signal.SIGINT, signal.SIGTERM): raise app.log.debug(f"\n{e}") app.exit_code = 0 


Jika Anda mendapatkan pemahaman utama, Anda dapat melihat bahwa memuat dan menyimpan todolib.TodoApp juga dapat dilakukan di __enter __ / __ keluar yang ditimpa, tetapi fase ini akhirnya dipisahkan menjadi metode terpisah untuk menunjukkan kait Semen.

Pengujian


Untuk pengujian, Anda dapat menggunakan kelas aplikasi yang sama:

 def test_cement(capsys): with todo_cement.TodoApp(argv=["add", "test"]) as app: app.run() out, _ = capsys.readouterr() assert out == EXPECTED #     jinja,       assert app.last_rendered is None 

Ringkasan


Pro: Perangkat API seperti set pisau Swiss, ekstensibilitas melalui kait dan plugin, antarmuka yang stabil dan pengembangan aktif.

Cons: Di tempat-tempat dokumentasi kosong; skrip kecil berbasis Semen mungkin tampak sedikit rumit.

Cleo


Cleo jauh dari kerangka yang populer seperti yang lain yang tercantum di sini (total sekitar 400 bintang di GitHub), namun saya berhasil mengetahuinya ketika saya mempelajari bagaimana Puisi memformat output.

Jadi, Cleo adalah salah satu proyek dari penulis Puisi yang telah disebutkan, alat untuk mengelola dependensi, virtualenvs dan aplikasi build. Tentang Puisi pada habr sudah lebih dari sekali menulis, dan tentang bagian konsolnya - tidak.

Parser


Cleo, seperti Semen, dibangun di atas prinsip-prinsip objek, mis. perintah didefinisikan melalui kelas Command dan docstring-nya, parameter diakses melalui metode option (), dan seterusnya. Selain itu, metode line (), yang digunakan untuk menampilkan teks, mendukung gaya (mis. Warna) dan pemfilteran output berdasarkan jumlah flag verbose di luar kotak. Cleo juga memiliki output tabel. Dan juga progress bar. Namun ... Secara umum, lihat:

kode sumber
 from cleo import Command as BaseCommand # cleo    clikit,          from clikit.api.io import flags as verbosity class Command(BaseCommand): def __init__(self): super().__init__() self.todoapp = None def handle(self): with todolib.TodoApp.fromenv() as app: self.todoapp = app self.do_handle() def do_handle(self): raise NotImplementedError class AddCommand(Command): """ Add new task. add {task : Task to add} """ def do_handle(self): title = self.argument("task") task = self.todoapp.add_task(title) # will be printed only on "-vvv" self.line(f"Title: {title}", style="comment", verbosity=verbosity.DEBUG) self.line(f"Task <info>{task.title}</> created with number {task.number}.") class ShowCommand(Command): """ Show current tasks. show {--show-done : Include tasks that are done.} """ def do_handle(self): tasks = self.todoapp.list_tasks(self.option("show-done")) if not tasks: self.line("There is no TODOs.", style="info") self.render_table( ["Number", "Title", "Status"], [ [str(task.number), task.title, "" if task.done else "✘"] for task in tasks ], ) class DoneCommand(Command): """ Mark task as done. done {number : Task number} """ def do_handle(self): task = self.todoapp.task_done(int(self.argument("number"))) self.line(f"Task <info>{task.title}</> marked as done.") class RemoveCommand(Command): """ Removes task from the list. remove {number : Task number} """ def do_handle(self): task = self.todoapp.remove_task(int(self.argument("number"))) self.line(f"Task <info>{task.title}</> removed from the list.") 


utama


Yang diperlukan hanyalah membuat objek cleo.Application lalu meneruskan perintah ke add_commands. Agar tidak mengulangi selama pengujian, semua ini ditransfer dari main ke konstruktor:

 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) 

Pengujian


Untuk menguji perintah di Cleo, ada CommandTester , yang, seperti semua paman dewasa dari kerangka kerja, memotong I / O dan kode keluar:

 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" 

Ringkasan


Kelebihan: struktur objek dengan petunjuk jenis, yang menyederhanakan pengembangan (karena banyak IDE dan editor memiliki dukungan yang baik untuk kode OOP dan modul pengetikan); Sejumlah fungsionalitas yang baik untuk bekerja tidak hanya dengan argumen, tetapi juga I / O.

Plus atau minus: parameter verbositasnya, yang hanya kompatibel dengan I / O Cleo / CliKit. Meskipun Anda bisa menulis custom handler untuk modul logging, bisa jadi sulit untuk mempertahankan seiring dengan pengembangan cleo.

Kontra: jelas - pendapat pribadi - API muda: kerangka kerja tidak memiliki pengguna "besar" yang lain, kecuali Puisi, dan Cleo berkembang secara paralel dengan pengembangan dan untuk kebutuhan satu; kadang-kadang dokumentasinya sudah usang (misalnya, level logging sekarang tidak terletak pada modul clikit, tetapi di clikit.api.io.flags), dan secara umum miskin dan tidak mencerminkan keseluruhan API.

Cleo, dibandingkan dengan Semen, lebih fokus pada CLI, dan dia adalah satu-satunya yang berpikir untuk memformat (menyembunyikan jejak tumpukan standar) pengecualian di output default. Tapi dia - lagi pendapat pribadi - kalah dari Semen di masa mudanya dan stabilitas API.

Kesimpulannya


Pada titik ini, semua orang sudah memiliki pendapat mereka sendiri, yang lebih baik, tetapi kesimpulannya adalah: Saya paling suka Klik, karena ada banyak hal di dalamnya dan sangat mudah untuk mengembangkan dan menguji aplikasi dengan itu. Jika Anda mencoba menulis kode seminimal mungkin - mulailah dengan Api. Script Anda membutuhkan akses ke Memcached, format dengan jinja dan ekstensibilitas - ambil Cement dan Anda tidak akan menyesalinya. Anda memiliki proyek kesayangan atau ingin mencoba sesuatu yang lain - lihat cleo.

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


All Articles