مقارنة بين مكتبات CLI الأقل شيوعًا وليست جدًا: جرف ، plac ، برقوق وغيرها (جزء 2)

في نظام Python البيئي ، هناك العديد من الحزم لتطبيقات CLI ، كتلك الشائعة ، مثل Click ، ​​وليس الكثير. أكثر المقالات شيوعًا تم بحثها في مقال سابق ، ولكن سيتم عرض هنا غير معروف ، ولكن ليس أقل إثارة للاهتمام.



كما في الجزء الأول ، سيتم كتابة نص وحدة التحكم لمكتبة todolib لكل مكتبة في Python 3.7. بالإضافة إلى ذلك ، سيتم كتابة اختبار تافه مع هذه التركيبات لكل تطبيق:

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

كل شفرة المصدر متاحة في هذا المستودع .

جرف


جيثب
الوثائق
سمع الكثيرون عن OpenStack ، منصة مفتوحة المصدر لـ IaaS. تتم كتابة معظمها في Python ، بما في ذلك أدوات مساعدة وحدة التحكم التي كانت تكرر وظيفة CLI لفترة طويلة. استمر هذا الأمر حتى ظهر جرف ، أو إطار صياغة سطر الأوامر ، كإطار عمل مشترك. مع ذلك ، قام مطورو Openstack بدمج حزم مثل python-novaclient و python-swiftclient و python-keystoneclient في برنامج openstack واحد.

الأوامر
يشبه أسلوب التصريح عن الأوامر الاسمنت و cleo: argparse كمحلل للمعلمات ، ويتم إنشاء الأوامر نفسها من خلال وراثة فئة الأوامر. في الوقت نفسه ، هناك امتدادات صغيرة لفئة الأوامر ، مثل Lister ، والتي تقوم بتنسيق البيانات بشكل مستقل.

شفرة المصدر
 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.") 


التطبيق والرئيسي

فئة التطبيق لديها طرق تهيئة و تنظيف ، في حالتنا ، يقومون بتهيئة التطبيق وحفظ البيانات.

شفرة المصدر
 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) 


أمثلة العمل

 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 

تسجيل الخروج من المربع! وإذا نظرت إلى أسفل الغطاء ، يمكنك أن ترى أنه تم بطريقة ذكية: المعلومات في stdout ، والتحذير / الخطأ في stderr ، وإذا لزم الأمر ، يتم تعطيل بواسطة العلم الهادئ .

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

كما ذكرنا سابقًا ، تقوم Lister بتنسيق البيانات ، لكن الجدول لا يقتصر على:

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

بالإضافة إلى json and table ، تتوفر yaml و csv.

يوجد أيضًا تتبع مخفي افتراضي:

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

بحث REPL وبحث غامض آخر يُعرف أيضًا باسم بحث غامض :

 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 

تجريب

الأمر بسيط: يتم إنشاء كائن تطبيق ويُطلق عليه أيضًا () يُرجع رمز الخروج.

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

إيجابيات وسلبيات

الايجابيات:

  • وسائل الراحة المختلفة من خارج منطقة الجزاء.
  • التي وضعتها OpenStack.
  • الوضع التفاعلي
  • القابلية للتوسعة من خلال نقطة إدخال setuptools و CommandHook ؛
  • Sphinx plugin للتوثيق التلقائي لـ CLI ؛
  • إكمال القيادة (باش فقط) ؛

سلبيات:

  • وثائق صغيرة ، والتي تتكون أساسا من مثال مفصل ولكن واحد ؛

تم ملاحظة خطأ آخر: في حالة حدوث خطأ عند إخفاء تتبع المكدس ، يكون رمز الخروج دائمًا صفريًا.

PLAC


جيثب
الوثائق

للوهلة الأولى ، يبدو أن Plac يشبه Fire ، ولكنه في الواقع مثل Fire ، الذي يخفي نفس الحجة وأكثر من ذلك بكثير تحت الغطاء.

يتبع Plac ، مستشهداً بالوثائق ، "المبدأ القديم لعالم الكمبيوتر: يجب أن تحل البرامج فقط الحالات العادية والبساطة يجب أن تظل بسيطة ، ويمكن تحقيقها في نفس الوقت ." يستخدم مؤلف الإطار Python منذ أكثر من تسع سنوات وقد كتبه مع توقع حل "99.9٪ من المهام".

الأوامر والرئيسية

الانتباه إلى التعليقات التوضيحية في العرض وطرق التنفيذ: هذه هي الطريقة التي يوزع بها Plac المعلمات وتساعد الوسيطة ، على التوالي.

شفرة المصدر
 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) 


تجريب

لسوء الحظ ، لا يسمح اختبار رمز الخروج Plac. لكن اختبار نفسه حقيقي:

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

إيجابيات وسلبيات

الايجابيات:

  • استخدام بسيط
  • الوضع التفاعلي مع دعم readline ؛
  • API مستقرة
  • وثائق ممتازة

ولكن الشيء الأكثر إثارة للاهتمام حول Plac مخفي في الاستخدام المتقدم :

  • تنفيذ العديد من الأوامر في مؤشرات الترابط والعمليات الفرعية ؛
  • الحوسبة المتوازية
  • خادم التلنت

سلبيات:

  • لا يمكنك اختبار رمز الخروج ؛
  • سوء حياة المشروع.

بلومبوم plumbum


جيثب
الوثائق
وثائق CLI
بلومب ، في الواقع ، ليس مثل هذا الإطار غير المعروف - ما يقرب من 2000 نجم ، وهناك شيء ما يستحق العناء له ، لأنه ، على نحو تقريبي ، ينفذ بناء جملة UNIX Shell. حسنا ، مع إضافات:

 >>> 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' 

الأوامر والرئيسية

يحتوي Plumbum أيضًا على أدوات لـ CLI ، ولا يعني أنها مجرد إضافة: هناك nargs والأوامر والألوان:

شفرة المصدر
 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() 


تجريب

يختلف اختبار التطبيقات على Plumbum عن التطبيقات الأخرى ، فيما عدا الحاجة إلى تمرير اسم التطبيق أيضًا ، أي الحجة الأولى:

 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" 

إيجابيات وسلبيات

الايجابيات:

  • مجموعة أدوات ممتازة للعمل مع فرق خارجية ؛
  • دعم الأنماط والألوان ؛
  • API مستقرة
  • الحياة النشطة للمشروع ؛

لم يلاحظ أي عيوب.

CMD2


جيثب
الوثائق

بادئ ذي بدء ، cmd2 هو امتداد على cmd من المكتبة القياسية ،
أي الغرض منه هو للتطبيقات التفاعلية. ومع ذلك ، يتم تضمينه في المراجعة ، لأنه يمكن أيضًا تكوينه لوضع CLI العادي.

الأوامر والرئيسية

يتطلب cmd2 الامتثال لقاعدة معينة: يجب أن تبدأ الأوامر بالبادئة do_ ، ولكن خلاف ذلك ، كل شيء واضح:

الوضع التفاعلي
 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() 


وضع غير تفاعلي
الوضع العادي يتطلب حركة اضافية قليلا.

على سبيل المثال ، يجب عليك الرجوع إلى argparse وكتابة منطق الحالة عندما يتم استدعاء البرنامج النصي بدون معلمات. والآن تحصل الأوامر على argparse.Namespace .

يتم الحصول على المحلل اللغوي من مثال argparse مع الإضافات الصغيرة - الآن المحللون الفرعيون هم سمات ArgumentParser الرئيسية.

 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() 


تجريب

سيتم اختبار البرنامج النصي التفاعلي فقط.

نظرًا لأن اختبار التطبيقات التفاعلية يتطلب عددًا كبيرًا من الموارد البشرية والوقت ، فقد حاول مطورو cmd2 حل هذه المشكلة بمساعدة النسخ - الملفات النصية مع أمثلة المدخلات والمخرجات المتوقعة. على سبيل المثال:

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

وبالتالي ، كل ما هو مطلوب هو نقل قائمة الملفات مع النسخ إلى التطبيق :

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

إيجابيات وسلبيات

الايجابيات:

  • واجهة برمجة تطبيقات جيدة للتطبيقات التفاعلية ؛
  • النامية بنشاط في السنوات الأخيرة ؛
  • نهج الأصلي للاختبار.
  • وثائق جيدة.

سلبيات:

  • يتطلب argparse ورمز إضافي عند كتابة التطبيقات غير التفاعلية ؛
  • API غير مستقر
  • في بعض الأماكن تكون الوثائق فارغة.

المكافأة: Urwid


جيثب
الوثائق
البرنامج التعليمي
أمثلة البرنامج

Urwid هو إطار من عالم مختلف قليلاً - عن لعنات وشاشة npyscreen ، أي من وحدة التحكم / واجهة المستخدم الطرفية. ومع ذلك ، يتم تضمينه في الاستعراض كمكافأة ، لأنه ، في رأيي ، يستحق الاهتمام.

التطبيق والفرق

يحتوي Urwid على عدد كبير من عناصر واجهة المستخدم ، لكنه لا يحتوي على مفاهيم مثل نافذة أو أدوات بسيطة للوصول إلى عناصر واجهة التعامل المجاورة. وبالتالي ، إذا كنت ترغب في الحصول على نتيجة جميلة ، فستحتاج إلى تصميم و / أو استخدام حزم أخرى مدروس ، وإلا فسيتعين عليك نقل البيانات في سمات الأزرار ، كما يلي:

شفرة المصدر
 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() 


الأوامر
 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) 


رئيسي
 if __name__ == "__main__": try: with app: urwid.MainLoop(app).run() except KeyboardInterrupt: pass 

إيجابيات وسلبيات

الايجابيات:

  • واجهة برمجة تطبيقات رائعة لكتابة تطبيقات TUI المختلفة ؛
  • تاريخ التطوير الطويل (منذ 2010) و API المستقر ؛
  • العمارة المختصة
  • وثائق جيدة ، وهناك أمثلة.

سلبيات:

  • كيفية اختبار غير واضح. فقط tmux إرسال مفاتيح يتبادر إلى الذهن.
  • أخطاء غير إعلامية عندما لا يتم ترتيب الحاجيات بشكل صحيح.

* * *


يشبه Cliff إلى حد كبير Cleo و Cement وهو جيد عمومًا للمشاريع الكبيرة.

أنا شخصياً لا أجرؤ على استخدام Plac ، لكنني أوصي بقراءة التعليمات البرمجية المصدر.

يحتوي Plumbum على مجموعة أدوات CLI سهلة الاستخدام وواجهة برمجة تطبيقات رائعة لتنفيذ الأوامر الأخرى ، لذلك إذا كنت تعيد كتابة نصوص shell في Python ، فهذا هو ما تحتاجه.
cmd2 مناسب تمامًا كأساس للتطبيقات التفاعلية ولأولئك الذين يرغبون في الترحيل من cmd القياسي.

ويتميز Urwid بتطبيقات وحدة تحكم جميلة وسهلة الاستخدام.

لم يتم تضمين الحزم التالية في المراجعة:

  • aioconsole - لا يوجد وضع غير تفاعلي ؛
  • pyCLI - لا يوجد دعم للأوامر الفرعية ؛
  • Clint - لا يوجد دعم للأوامر الفرعية ، مستودع في الأرشيف ؛
  • سطر الأوامر - إنه قديم جدًا (الإصدار الأخير في 2009) وغير مهتم ؛
  • CLIArgs - القديمة (الإصدار الأخير في 2010)
  • مفتش - قديم نسبيا (الإصدار الأخير في 2015)

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


All Articles