Parsování CLI v Pythonu po třech letech

K čemu že je dobré? Malou užitečnou funkcionalitu vtělíte do prográmku za patnáct minut. Pak je nutno nastavit rozhraní, kterým uživatelé budou váš program ovládat. Příjemná a v linuxu základní volba je skrz parametry v příkazové řádce.

program.py --texticek koloušek --cisilko False # zpracování CLI do proměnných v programu

Ouha, naše první volba, vestavěná knihovna argparse, vyžaduje poměrně zdlouhavé psaní. A nejen to, nabýváte s ní pocitu, že vám nespadly klacky pod nohy, ale že se derete křovím. Třeba proto, že vám IDE sotva napoví, které proměnné vystavujete, tudíž z vaší vypiplané dokumentace má profit jen uživatel, nikoli vy. Vám se programování zhoršilo.

Stáda nás kodérů se na tuhle problematiku vrhla, nadějné studentské projekty sbírají hvězdy na githubu a hltíme repozitář Pythonu více či méně domyšlenými pokusy.

Vyšel jsem z vynikající analýzy kolegy Zímy, který podrobně srovnával dlouhou řadu knihoven pro parsování CLI v Pythonu.

Tři roky uplynuly, ale při psaní programů stále zakopávám – v klání o CLI knihovnu je na místě grálu znepokojivé vakuum. Vždy si něco vyberu, ale po každém malém prográmku znovu očkem koukám, jestli se neurodil lepší projekt.

Tak co je nového na trhu CLI?

Jak vypadá trh

Dělí se do dvou proudů – jedny knihovny pracují s parametry funkcí, druhé s atributy třídy. Předhání se sliby o jednoduchosti, někdy však zůstane pouze u README.

Stabilní monolit argparse drží. Originální koncept docopt zažil renesanci. Typer, byť začíná ve svých hello-worldech zlehýnka, lapaje ptáčky, už při prvním help textu pořád pořádně přitvrzuje. Přesně jak psal Zíma, “the annotation turn rather quickly into very complex structure which completely obscures its original purpose”. Nechápu jeho monstrózní popularitu, asi mi něco uniká.

… Čím charakterizovat tuto rešerši? Širší záběr, menší detail, velké a konkrétní požadavky.

Širší záběr – Stanovil jsem si kritéria a prošel dobrých dvacet současných knihoven, v honbě za dokonalým.
Menší detail – Můžu se mýlit, někde jsem dokumentaci prolétl rychle. Nebo jsem nezvládl něco nastavit a v komentářích mne opravíte.
Požadavky – Nesplnil nikdo.

Behaviorální charakteristika jehly v kupce

Hledané řešení nemusí vykazovat nekonečnou robustnost. Stačí, když pokryje běžnou potřebu: předání textu, booleany a pár čísel.

Zjišťoval jsem, že nesmí být ukecané ani na straně programátora ani na straně uživatele – já nehodlám psát proměnnou do parametru, dekorátoru a potřetí znovu do dokumetace. A ani uživatele nebudu nutit, aby psal --flag True, když může napsat --flag.

K čemu jsem došel?

Hlavní požadavky:

  • Snadný start.

Chci doplnit chybějící dílek do skládačky, nikoli se stát závislým na regulích cizího frameworku.

  • Proměnné chci napsat právě jednou.

Některé knihovny nutí definovat proměnnou na více místech. Typicky click, nestačí, že ji mám v parametru, potřebuji ještě zvlášť napsat celý dlouhý dekorátor.

@click.option('--cisilko', help="My number.")
def main(cisilko: int):

  • Připojit k nim datový typ i komentář.

Anotace i nějakou formu docstringy podporují všechny.

  • IDE musí umět napovídat proměnné.

Docopt se báječně píše, ale protože jsou proměnné napsány v dokumentaci, IDE neumí napovědět. Práci, kterou jsem ušetřil, ve složitějším programu si vynahradím stálým nahlížením do zdrojového souboru.

  • K proměnným lze přímo přistoupit přes objekt.

Rozhodl jsem se, že chci konfigurační proměnné spravovat jako atributy třídy. Nechci je mít jako parametry funkce. Protože k takovým se dostanu pouze v dané funkci a jak program roste, vzniká balast, jak si části posílám sem a tam. Chci mít jeden konfigurační objekt, který snadno předám dál.

def main(cisilko: int) # definici pomocí parametru funkce nechci

@dataclass
class Config:
cisilko: int # definici pomocí atributu třídy chci

Vedlejší požadavky:

  • IDE musí umět napovídat proměnné včetně jejich dokumentace.

Řekl bych samozřejmé, pro většinu kandidátů však tato položka byla kamenem úrazu. Aby popis proměnné viděl jak uživatel při volání o pomoc program.py --help, tak programátor při práci, to je přeci základ! Proč má chudák programátor vidět jenom datový typ, když už k němu připsal komentář? Nebo má psát komentář dvakrát, jednou pro sebe a podruhé do dokumentace? Obtíže tkví v trochu nestandardním způsobu, jak Python očekává, že budete dokumentovat proměnné. Nikoli nad nimi, jak dělají spřátelené jazyky (JSDoc, PHPDoc, Javadoc), ani na daném řádku za znakem komentáře, ale v řádku pod.

atribut: str = "Ahoj" # toto není docstring
""" toto je docstring """

Tento spodní řádek zobrazí IDE, které udělá statickou analýzu… obtížně se k němu ale dostane za běhu program sám (aby ho vypsal přes --help). Zmíněný typer například řeší situaci použitím klíčového slova Annotated. Do něj umisťuje další funkce…

texticek: Annotated[str, typer.Option(help="Můj potřebný text.")] = ""

Uživatel nápovědu uvidí… Ale. Dlouze se to píše, špatně se to čte a IDE to stejně v současnosti vůbec nerozklíčuje, takže programátor ostrouhává. (Na téhle podmínce vypadává například jinak slibná dataclass-click.)

  • Jsou měnitelné z konfiguračního souboru.

Považuju za velmi potřebnou vlastnost, aby se při absenci CLI parametrů výchozí hodnoty čerpaly také z konfiguračního souboru. (Protože toto není obtížné doprogramovat, problém jsem zařadil mezi vedlejší požadavky.) Stále však platí hlavní podmínka, abych proměnnou mohl psát pouze jednou. Už jsem párkrát bloudil v programech, kde novou proměnnou bylo nutno přidat na pět různých míst, třeba právě do konfiguračního souboru. Jeho použití má pro uživatele zůstat nepovinné.

  • Blank → True parametry.

Chtěl bych mít parametry s elegantně implicitním True, které by se chovaly takto:

# definuji flag: str|bool = False
--flag # blank nabývá True
--flag "text" # "text"
# jinak nabývá False

Vede to i k 3stavovým booleanům. Ty se hodí uživateli, aby mohl vyjádřit kladnou i zápornou preferenci. (Například – vždycky pošli mail, nikdy neposílej mail.)

# definuji flag: bool|None = None
--flag # blank nabývá True
--flag False NEBO --no-flag # False
# jinak nabývá None

Žel vítězové tento typ zadávání nepodporují, možná mám příliš specifické choutky.

  • Bash completion.

Zrychlit CLI se dá pomocí doplňování. Některé knihovny automaticky přidávají možnost, jak do systému takové doplňování přidat či aspoň vygenerovat správný konfigurační soubor. Takový přístup zjednoduší programátorovi crcání v instalační souboru, bravo!

  • TUI/GUI.

Ještě lepší možnost jak zrychlit CLI, je ho nepoužít. Než nutit uživatele procházet manuály, rovnou poskytnout další rozhraní, kde si vybere přehledněji. Nejlépe plnokrevné okno, automaticky generované.
Ale pozor, tato výhoda se snadno pokazí tím, když se jí všechno podřídí. GUI ano, ale ne na úkor CLI. Windows by mohly vyprávět, jak skvěle se ladí pád aplikace, která nechrlí svůj výstup do terminálu. (Ve Windows žádná aplikace.) Ustrňte se na chvíli nad uživateli, kteří jsou doživotně odsouzeni k nějakému podnikovému klikátku, když už přesně ví, co chtějí, jen jen automatizovat proces nějakým pěkným parametrem z příkazové řádky… Která jauvej! chybí. Protože program CLI nemá.

Jakmile uživatel spustí můj program bez proměnných, ať se zobrazí prostředí, kde uživatel proměnné nakliká. Když nebude GUI, beru zavděk TUI – textovým rozhraním, které vypadá jako okno, ale zůstává v terminálu. Jeho výhodou je snadný provoz na vzdálených strojích bez monitoru. Podmínka je, aby uživatel mohl editovat všechny proměnné naráz, jako by byly na webové stránce. (Neuvažoval jsem projekty typu PyInquirer, které vedou uživatele sekvenčně.) V současnosti existují dechberoucí projekty jako textual či PyTermGUI… jen je spojit s procesem parsování.

V ideálním světě napíšu program a obalím jej nějakou funkcí. Nebastlím dalších deset řádků pro konfigurační soubor. A nebastlím dvacet dalších pro definici CLI. A hlavně nebastlím ani dalších dvě stě kvůli GUI, abych po dvou dnech měl pocit, že jsem datloval nějaké pitomé polofunkční okno, a vůbec ne, co jsem potřeboval. Tohle všechno mi přece zajistí ta jedna dokonalá funkce. Já si zpíchnu rešerši, projdu paklík houfně forkovaných, zralých projektů, které se budou lišit jakousi křížovou výpravou o tom, jestli mezery nebo taby, jestli podle Sphinx, Googlu nebo PEP123456, jestli lintovat Flake8 nebo Black…

Akorát žádná taková funkce pořád ještě není! Nenašel jsem dokonce ani upachtěné pokusy s opuštěnými verzemi jako 0.0.0.2. (S výjimkou nefunkčního oneFace.) Hlavně, že každý druhý nový projekt na webu je další webový framework. Jsem na světě sám?

Favorité – tyro & SimpleParsing

Konec lití horké kaše, můj favorit klání je tyro (pouhých 0.3 k٭ na Githubu).
Tato poměrně neznámá knihovna zvítězila především proto, že bravurně zvládá dokumentaci a je stručná. Ostatní berou popis proměnné buď:

  • Z parametru funkce, kterou přiřadíme: texticek: str = Field(default="", description="Můj potřebný text", cli=('-t', '--texticek'), ...)
  • Z anotace proměnné: texticek: Annotated[str, "..."] = ""

A nutí nás tím psát děsivosti buď před rovnítko, nebo za něj. Tyro ale nějakým kouzlem rozumí docstringu uvedeným pod proměnnou, díky čemuž jej IDE výborně zobrazuje. (Rozumí ale i komentáři na řádku či docstringu třídy.) Tím pádem uživatel o nic nepřijde (krásný help text) a programátor po stručné definici uvidí všechno.

Co je nutné přidat do programu? Jedinou věc: tyro.cli(Config)

Zvládá konverzi do YAMLu (s menší dopomocí programátora). Výběr YAMLu mi vyhovuje, je sice o maloučko složitější než TOML a pomalejší než JSON, ale má úžasnou vlastnost, kdy sestaví objekty přímo z konfiguračního souboru. Bash completion zvládá nativně. A šťavnatí se milou třešničkou – v konfiguraci pomocí dataclass umí udělat první parametr poziční, aby pro něj uživatel nemusel vypisovat program.py --flag run, ale rovnou psal program.py run.

Z vedlejších požadavků nesplňuje akorát mé Blank → True parametry. A GUI – jako nikdo.

Další finalisté – SimpleParsing a tap

  • SimpleParsing 0.3 k٭ – Rozkošná knihovna, velmi podobný přístup jako tyro, téže umí zpracovat docstringy. Z vedlejších parametrů pak nesplňuje především bash completion. Řešení konverze do YAMLu mi vyhovuje víc než v tyro, oproti němu však hůře zvládá například nápovědu --help: nevypočítává enum typy a je bezbarvá. Také iniciaci má delší, vychází více z argparse:
    parser = ArgumentParser()
    parser.add_arguments(Config, dest=“config”)
    # drobnost: musíme anotovat args, aby IDE napovídal proměnné
    args: Config = parser.parse_args().config
  • typed-argument-parser (tap) 0.4 k٭ – Během exportu do JSONu tap připojí několik užitečných parametrů, například řetězec, kterým je možno spustit danou konfiguraci z příkazového řádku. Třetí místo.

Následující projekty nesplňují zřejmě žádný vedlejší požadavek, ale ještě dodržely všechny hlavní. Je pozoruhodné, že mezi ně až patří marginální a neznámé projekty.

  • plumbum 3 k٭ – umí toho mnohem víc (možná je to overhead)
  • pydantic-cli 0.1 k٭ – Kromě hlavních požadavků si poradí s bash completion a konverzí do JSONu (s menší dopomocí uživatele).
  • dataclass-click ∅ – Lepidlo mezi vyzývaným panovníkem clickem a standardními dataclasses.
  • typed-args ∅ – Čtyři roky vyvíjený drobeček. Nezahltí množstvím informací, příklad použití a jede se.
  • jsonargparse 0.3 k٭ – Projekt, který se upřímně srovnává s ostatními a nevychází poraženě.

Ostatní účastníci

Dominantní koráby

  • argparse – Pokud proměnné napíšete jen jednou, IDE vám nenapoví.
  • fire 27 k٭ – Nápověda jen pomocí docstringu. Rozšiřuje parametry, nikoli atributy.
  • click 15 k٭ – Nutno proměnné psát 2× jako parametry hlavní funkce i jako položky dekorátoru.
  • typer 15 k٭– Skvělá bash completion. Hezky konvertuje funkci do CLI. Dlouhý zápis. Rozšiřuje parametry, nikoli atributy.
  • docopt 8 k٭, respektive docopt-ng – nejdřív se napíše dokumentace, asi tedy žádné IDE napovídání
  • twisted.python.usage 5k ٭– proměnné jsou jen text. Součástí většího frameworku (lze asi použít samostatně).

Menší bárky s komunitou

  • cement 1 k٭– Celý velký framework, nelze jen tak začít.
  • cleo 1 k٭– Spíše framework než argument parser. Nelze přistoupit přímo k proměnným (volají se přes string jména).
  • clize 0.5 k٭ – Asi nutno psát proměnné 2×.
  • plac 0.3 k٭ – Nutno proměnné psát 2×. Hezky konvertuje funkci do CLI. Rozšiřuje parametry, nikoli atributy.
  • arguably 0.3 k٭ – Rozšiřuje parametry, nikoli atributy.

Necky

  • anyfig ∅ – Nefunguje ani dokumentace.
  • arglite ∅ – Malinký projekt, docela nadějný, ale zřejmě nefunguje ani příklad z dokumentace.
  • recline ∅ – Nutno proměnné psát 2×.
  • cli3 ∅ – Rozšiřuje parametry, nikoli atributy.

Vraky

  • Gooey 20 k٭– Velká naděje pro GUI. Nešel nainstalovat, 2 roky žádná aktivita, zřejmě skončený projekt.
  • clint 0.1 k٭– Čerstvě skončený projekt.
  • interfacy-cli ∅ – Žádná dokumentace. Byl mi doporučen zbytečně.

Kudy plout dál?

Nepustil jsem se snu o automatickém GUI. Musím si vážně napsat všechno sám? To je přeci špatně. Nicméně díky téhle rešerši mi stačilo postavit lávku mezi jedním z favoritů SimpleParsing a tkinterem, výchozím GUI v Pythonu. (Proč ten ošklivý tkinter? Proč ne Qt? Pamatoval jsem si stesky, jak je hrozná migrace z Qt2. A jak je náročná migrace z Qt4. Uplynulo pár let a nalezl jsem stesky na migraci na Qt6. Qt vypadá krásně, ale podepište svůj projekt touhle krví, ne, díky.)

Té lávce říkám mininterface, je to jen proof-of-concept, výkop naslepo. Můžete se po ní také proběhnout. Budeme společně toužit po budoucnosti programování a příjemnějších interfacech.

Či se blýská jasná perla v moři projektů, které jsem nezohlednil, jako jsou glacier, clipstick, cyto, clippy, cliche? Kde je váš grál? Ach, poučte mne.

Autor:

Zanechte komentář

Všechny údaje jsou povinné. E-mail nebude zobrazen.

Tato stránka používá Akismet k omezení spamu. Podívejte se, jak vaše data z komentářů zpracováváme..