[{"data":1,"prerenderedAt":2713},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures\u002F":3,"content-directory":1868},{"id":4,"title":5,"body":6,"date":1852,"description":1853,"difficulty":1854,"draft":1855,"extension":1856,"meta":1857,"navigation":142,"path":1858,"seo":1859,"stem":1860,"tags":1861,"updated":1852,"__hash__":1867},"content\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures\u002Findex.md","Isolating Plugin Failures in an Extensible Python CLI",{"type":7,"value":8,"toc":1831},"minimark",[9,42,47,61,65,69,89,93,98,101,585,611,801,812,815,819,826,942,967,971,974,1003,1007,1022,1210,1218,1221,1225,1277,1281,1284,1709,1724,1728,1731,1735,1739,1746,1753,1766,1770,1781,1785,1792,1796,1827],[10,11,12,13,17,18,21,22,26,27,32,33,36,37,41],"p",{},"Plugins extend a CLI with code its maintainers never wrote, never reviewed and cannot fix. That code runs inside your process, with your users' credentials, and its bugs look like yours: a ",[14,15,16],"code",{},"ZeroDivisionError"," traceback ending in ",[14,19,20],{},"mytool\u002Fcli.py",", a hang, a corrupted JSON stream because a plugin printed debugging output to stdout. Users report those bugs to ",[23,24,25],"em",{},"you",". Safe loading — catching import errors when plugins are discovered — is the first line of defence, covered in ",[28,29,31],"a",{"href":30},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points\u002F","discovering plugins with entry points",". This guide covers what happens ",[23,34,35],{},"after"," a plugin has loaded: attributing runtime crashes to the plugin that caused them, a safe mode that starts the CLI with no plugins at all, switching off a single plugin, and — for code you do not trust — running plugins in a separate process. It belongs to the ",[28,38,40],{"href":39},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002F","plugin architectures topic",".",[43,44,46],"h2",{"id":45},"prerequisites","Prerequisites",[48,49,50,54],"ul",{},[51,52,53],"li",{},"A Typer (or Click) CLI that mounts plugin command groups from entry points.",[51,55,56,57,41],{},"The plugin API conventions from ",[28,58,60],{"href":59},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api\u002F","versioning a plugin API",[43,62,64],{"id":63},"where-plugins-fail","Where plugins fail",[66,67],"inline-diagram",{"name":68},"plugiso-stages",[10,70,71,72,76,77,80,81,84,85,88],{},"Plugin failures happen at three different moments, and each needs its own defence. At ",[73,74,75],"strong",{},"load time"," — the plugin's module raises on import, or exports the wrong object — the host catches the error, skips the plugin and warns. At ",[73,78,79],{},"run time"," — a plugin command raises — the host should report ",[23,82,83],{},"which plugin"," failed and where to report it, instead of a traceback that implicates the host. And ",[73,86,87],{},"ambiently"," — a plugin reconfigures logging, writes to stdout, patches a library, or is simply slow at import — where the only real remedies are switching the plugin off or running it out of process.",[43,90,92],{"id":91},"the-recipe","The recipe",[94,95,97],"h3",{"id":96},"attribute-runtime-crashes","Attribute runtime crashes",[10,99,100],{},"Wrap every command callback a plugin registers so exceptions carry the plugin's name and package:",[102,103,108],"pre",{"className":104,"code":105,"language":106,"meta":107,"style":107},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fplugin_guard.py\nfrom __future__ import annotations\n\nimport functools\nfrom collections.abc import Callable\n\nimport typer\n\n\nclass PluginCrashed(Exception):\n    def __init__(self, plugin: str, dist: str, original: BaseException) -> None:\n        super().__init__(\n            f\"plugin {plugin!r} ({dist}) crashed: {type(original).__name__}: {original}\")\n        self.plugin = plugin\n        self.dist = dist\n\n\ndef _guard(plugin: str, dist: str, func: Callable) -> Callable:\n    @functools.wraps(func)                      # keeps the signature Typer reads\n    def wrapper(*args, **kwargs):\n        try:\n            return func(*args, **kwargs)\n        except (typer.Exit, typer.Abort, KeyboardInterrupt):\n            raise                               # normal control flow, not crashes\n        except Exception as exc:\n            raise PluginCrashed(plugin, dist, exc) from exc\n    return wrapper\n\n\ndef isolate(plugin_app: typer.Typer, plugin: str, dist: str) -> typer.Typer:\n    \"\"\"Wrap every command (recursively) registered on a plugin's Typer app.\"\"\"\n    for info in plugin_app.registered_commands:\n        info.callback = _guard(plugin, dist, info.callback)\n    for group in plugin_app.registered_groups:\n        isolate(group.typer_instance, plugin, dist)\n    return plugin_app\n","python","",[14,109,110,119,137,144,153,166,171,179,184,189,208,243,258,318,333,346,351,356,377,389,411,419,437,451,460,474,487,496,501,506,526,532,547,558,571,577],{"__ignoreMap":107},[111,112,115],"span",{"class":113,"line":114},"line",1,[111,116,118],{"class":117},"sJ8bj","# src\u002Fmytool\u002Fplugin_guard.py\n",[111,120,122,126,130,133],{"class":113,"line":121},2,[111,123,125],{"class":124},"szBVR","from",[111,127,129],{"class":128},"sj4cs"," __future__",[111,131,132],{"class":124}," import",[111,134,136],{"class":135},"sVt8B"," annotations\n",[111,138,140],{"class":113,"line":139},3,[111,141,143],{"emptyLinePlaceholder":142},true,"\n",[111,145,147,150],{"class":113,"line":146},4,[111,148,149],{"class":124},"import",[111,151,152],{"class":135}," functools\n",[111,154,156,158,161,163],{"class":113,"line":155},5,[111,157,125],{"class":124},[111,159,160],{"class":135}," collections.abc ",[111,162,149],{"class":124},[111,164,165],{"class":135}," Callable\n",[111,167,169],{"class":113,"line":168},6,[111,170,143],{"emptyLinePlaceholder":142},[111,172,174,176],{"class":113,"line":173},7,[111,175,149],{"class":124},[111,177,178],{"class":135}," typer\n",[111,180,182],{"class":113,"line":181},8,[111,183,143],{"emptyLinePlaceholder":142},[111,185,187],{"class":113,"line":186},9,[111,188,143],{"emptyLinePlaceholder":142},[111,190,192,195,199,202,205],{"class":113,"line":191},10,[111,193,194],{"class":124},"class",[111,196,198],{"class":197},"sScJk"," PluginCrashed",[111,200,201],{"class":135},"(",[111,203,204],{"class":128},"Exception",[111,206,207],{"class":135},"):\n",[111,209,211,214,217,220,223,226,228,231,234,237,240],{"class":113,"line":210},11,[111,212,213],{"class":124},"    def",[111,215,216],{"class":128}," __init__",[111,218,219],{"class":135},"(self, plugin: ",[111,221,222],{"class":128},"str",[111,224,225],{"class":135},", dist: ",[111,227,222],{"class":128},[111,229,230],{"class":135},", original: ",[111,232,233],{"class":128},"BaseException",[111,235,236],{"class":135},") -> ",[111,238,239],{"class":128},"None",[111,241,242],{"class":135},":\n",[111,244,246,249,252,255],{"class":113,"line":245},12,[111,247,248],{"class":128},"        super",[111,250,251],{"class":135},"().",[111,253,254],{"class":128},"__init__",[111,256,257],{"class":135},"(\n",[111,259,261,264,268,271,274,277,280,283,285,288,290,293,296,299,302,305,307,310,312,315],{"class":113,"line":260},13,[111,262,263],{"class":124},"            f",[111,265,267],{"class":266},"sZZnC","\"plugin ",[111,269,270],{"class":128},"{",[111,272,273],{"class":135},"plugin",[111,275,276],{"class":124},"!r",[111,278,279],{"class":128},"}",[111,281,282],{"class":266}," (",[111,284,270],{"class":128},[111,286,287],{"class":135},"dist",[111,289,279],{"class":128},[111,291,292],{"class":266},") crashed: ",[111,294,295],{"class":128},"{type",[111,297,298],{"class":135},"(original).",[111,300,301],{"class":128},"__name__}",[111,303,304],{"class":266},": ",[111,306,270],{"class":128},[111,308,309],{"class":135},"original",[111,311,279],{"class":128},[111,313,314],{"class":266},"\"",[111,316,317],{"class":135},")\n",[111,319,321,324,327,330],{"class":113,"line":320},14,[111,322,323],{"class":128},"        self",[111,325,326],{"class":135},".plugin ",[111,328,329],{"class":124},"=",[111,331,332],{"class":135}," plugin\n",[111,334,336,338,341,343],{"class":113,"line":335},15,[111,337,323],{"class":128},[111,339,340],{"class":135},".dist ",[111,342,329],{"class":124},[111,344,345],{"class":135}," dist\n",[111,347,349],{"class":113,"line":348},16,[111,350,143],{"emptyLinePlaceholder":142},[111,352,354],{"class":113,"line":353},17,[111,355,143],{"emptyLinePlaceholder":142},[111,357,359,362,365,368,370,372,374],{"class":113,"line":358},18,[111,360,361],{"class":124},"def",[111,363,364],{"class":197}," _guard",[111,366,367],{"class":135},"(plugin: ",[111,369,222],{"class":128},[111,371,225],{"class":135},[111,373,222],{"class":128},[111,375,376],{"class":135},", func: Callable) -> Callable:\n",[111,378,380,383,386],{"class":113,"line":379},19,[111,381,382],{"class":197},"    @functools.wraps",[111,384,385],{"class":135},"(func)                      ",[111,387,388],{"class":117},"# keeps the signature Typer reads\n",[111,390,392,394,397,399,402,405,408],{"class":113,"line":391},20,[111,393,213],{"class":124},[111,395,396],{"class":197}," wrapper",[111,398,201],{"class":135},[111,400,401],{"class":124},"*",[111,403,404],{"class":135},"args, ",[111,406,407],{"class":124},"**",[111,409,410],{"class":135},"kwargs):\n",[111,412,414,417],{"class":113,"line":413},21,[111,415,416],{"class":124},"        try",[111,418,242],{"class":135},[111,420,422,425,428,430,432,434],{"class":113,"line":421},22,[111,423,424],{"class":124},"            return",[111,426,427],{"class":135}," func(",[111,429,401],{"class":124},[111,431,404],{"class":135},[111,433,407],{"class":124},[111,435,436],{"class":135},"kwargs)\n",[111,438,440,443,446,449],{"class":113,"line":439},23,[111,441,442],{"class":124},"        except",[111,444,445],{"class":135}," (typer.Exit, typer.Abort, ",[111,447,448],{"class":128},"KeyboardInterrupt",[111,450,207],{"class":135},[111,452,454,457],{"class":113,"line":453},24,[111,455,456],{"class":124},"            raise",[111,458,459],{"class":117},"                               # normal control flow, not crashes\n",[111,461,463,465,468,471],{"class":113,"line":462},25,[111,464,442],{"class":124},[111,466,467],{"class":128}," Exception",[111,469,470],{"class":124}," as",[111,472,473],{"class":135}," exc:\n",[111,475,477,479,482,484],{"class":113,"line":476},26,[111,478,456],{"class":124},[111,480,481],{"class":135}," PluginCrashed(plugin, dist, exc) ",[111,483,125],{"class":124},[111,485,486],{"class":135}," exc\n",[111,488,490,493],{"class":113,"line":489},27,[111,491,492],{"class":124},"    return",[111,494,495],{"class":135}," wrapper\n",[111,497,499],{"class":113,"line":498},28,[111,500,143],{"emptyLinePlaceholder":142},[111,502,504],{"class":113,"line":503},29,[111,505,143],{"emptyLinePlaceholder":142},[111,507,509,511,514,517,519,521,523],{"class":113,"line":508},30,[111,510,361],{"class":124},[111,512,513],{"class":197}," isolate",[111,515,516],{"class":135},"(plugin_app: typer.Typer, plugin: ",[111,518,222],{"class":128},[111,520,225],{"class":135},[111,522,222],{"class":128},[111,524,525],{"class":135},") -> typer.Typer:\n",[111,527,529],{"class":113,"line":528},31,[111,530,531],{"class":266},"    \"\"\"Wrap every command (recursively) registered on a plugin's Typer app.\"\"\"\n",[111,533,535,538,541,544],{"class":113,"line":534},32,[111,536,537],{"class":124},"    for",[111,539,540],{"class":135}," info ",[111,542,543],{"class":124},"in",[111,545,546],{"class":135}," plugin_app.registered_commands:\n",[111,548,550,553,555],{"class":113,"line":549},33,[111,551,552],{"class":135},"        info.callback ",[111,554,329],{"class":124},[111,556,557],{"class":135}," _guard(plugin, dist, info.callback)\n",[111,559,561,563,566,568],{"class":113,"line":560},34,[111,562,537],{"class":124},[111,564,565],{"class":135}," group ",[111,567,543],{"class":124},[111,569,570],{"class":135}," plugin_app.registered_groups:\n",[111,572,574],{"class":113,"line":573},35,[111,575,576],{"class":135},"        isolate(group.typer_instance, plugin, dist)\n",[111,578,580,582],{"class":113,"line":579},36,[111,581,492],{"class":124},[111,583,584],{"class":135}," plugin_app\n",[10,586,587,590,591,594,595,598,599,602,603,606,607,610],{},[14,588,589],{},"functools.wraps"," copies the wrapped function's metadata and sets ",[14,592,593],{},"__wrapped__",", which Typer follows when it inspects the signature — so the plugin's options and help are unchanged. ",[14,596,597],{},"typer.Exit",", ",[14,600,601],{},"typer.Abort"," and Ctrl-C pass through untouched, because they are how commands end normally. Call ",[14,604,605],{},"isolate()"," when mounting each plugin, and handle ",[14,608,609],{},"PluginCrashed"," at the top level:",[102,612,614],{"className":104,"code":613,"language":106,"meta":107,"style":107},"# src\u002Fmytool\u002Fmain.py\nimport typer\n\nfrom mytool.cli import app\nfrom mytool.plugin_guard import PluginCrashed\n\nEX_SOFTWARE = 70\n\n\ndef main() -> None:\n    try:\n        app()\n    except PluginCrashed as exc:\n        typer.echo(f\"error: {exc}\", err=True)\n        typer.echo(f\"This is a bug in the {exc.dist} plugin, not in mytool. \"\n                   f\"Run with MYTOOL_DEBUG=1 for the full traceback.\", err=True)\n        raise SystemExit(EX_SOFTWARE)\n",[14,615,616,621,627,631,643,655,659,670,674,678,692,699,704,717,750,769,787],{"__ignoreMap":107},[111,617,618],{"class":113,"line":114},[111,619,620],{"class":117},"# src\u002Fmytool\u002Fmain.py\n",[111,622,623,625],{"class":113,"line":121},[111,624,149],{"class":124},[111,626,178],{"class":135},[111,628,629],{"class":113,"line":139},[111,630,143],{"emptyLinePlaceholder":142},[111,632,633,635,638,640],{"class":113,"line":146},[111,634,125],{"class":124},[111,636,637],{"class":135}," mytool.cli ",[111,639,149],{"class":124},[111,641,642],{"class":135}," app\n",[111,644,645,647,650,652],{"class":113,"line":155},[111,646,125],{"class":124},[111,648,649],{"class":135}," mytool.plugin_guard ",[111,651,149],{"class":124},[111,653,654],{"class":135}," PluginCrashed\n",[111,656,657],{"class":113,"line":168},[111,658,143],{"emptyLinePlaceholder":142},[111,660,661,664,667],{"class":113,"line":173},[111,662,663],{"class":128},"EX_SOFTWARE",[111,665,666],{"class":124}," =",[111,668,669],{"class":128}," 70\n",[111,671,672],{"class":113,"line":181},[111,673,143],{"emptyLinePlaceholder":142},[111,675,676],{"class":113,"line":186},[111,677,143],{"emptyLinePlaceholder":142},[111,679,680,682,685,688,690],{"class":113,"line":191},[111,681,361],{"class":124},[111,683,684],{"class":197}," main",[111,686,687],{"class":135},"() -> ",[111,689,239],{"class":128},[111,691,242],{"class":135},[111,693,694,697],{"class":113,"line":210},[111,695,696],{"class":124},"    try",[111,698,242],{"class":135},[111,700,701],{"class":113,"line":245},[111,702,703],{"class":135},"        app()\n",[111,705,706,709,712,715],{"class":113,"line":260},[111,707,708],{"class":124},"    except",[111,710,711],{"class":135}," PluginCrashed ",[111,713,714],{"class":124},"as",[111,716,473],{"class":135},[111,718,719,722,725,728,730,733,735,737,739,743,745,748],{"class":113,"line":320},[111,720,721],{"class":135},"        typer.echo(",[111,723,724],{"class":124},"f",[111,726,727],{"class":266},"\"error: ",[111,729,270],{"class":128},[111,731,732],{"class":135},"exc",[111,734,279],{"class":128},[111,736,314],{"class":266},[111,738,598],{"class":135},[111,740,742],{"class":741},"s4XuR","err",[111,744,329],{"class":124},[111,746,747],{"class":128},"True",[111,749,317],{"class":135},[111,751,752,754,756,759,761,764,766],{"class":113,"line":335},[111,753,721],{"class":135},[111,755,724],{"class":124},[111,757,758],{"class":266},"\"This is a bug in the ",[111,760,270],{"class":128},[111,762,763],{"class":135},"exc.dist",[111,765,279],{"class":128},[111,767,768],{"class":266}," plugin, not in mytool. \"\n",[111,770,771,774,777,779,781,783,785],{"class":113,"line":348},[111,772,773],{"class":124},"                   f",[111,775,776],{"class":266},"\"Run with MYTOOL_DEBUG=1 for the full traceback.\"",[111,778,598],{"class":135},[111,780,742],{"class":741},[111,782,329],{"class":124},[111,784,747],{"class":128},[111,786,317],{"class":135},[111,788,789,792,795,797,799],{"class":113,"line":353},[111,790,791],{"class":124},"        raise",[111,793,794],{"class":128}," SystemExit",[111,796,201],{"class":135},[111,798,663],{"class":128},[111,800,317],{"class":135},[10,802,803,804,807,808,41],{},"The user now sees \"plugin 'aws' (mytool-aws 0.3.1) crashed: ZeroDivisionError: division by zero\" and a pointer to the right project. The traceback is still available — ",[14,805,806],{},"raise ... from exc"," keeps the chain — for anyone who asks for it, as in ",[28,809,811],{"href":810},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks\u002F","friendly error messages and tracebacks",[66,813],{"name":814},"plugiso-terminal",[94,816,818],{"id":817},"a-safe-mode-with-no-plugins","A safe mode with no plugins",[10,820,821,822,825],{},"When something is badly wrong — a plugin that breaks startup, slows every command to a crawl, or changes behaviour in a way nobody can explain — the fastest diagnosis is \"does it happen without plugins?\" Plugins are mounted before arguments are parsed, so a normal ",[14,823,824],{},"--no-plugins"," option arrives too late. An environment variable checked at mount time works:",[102,827,829],{"className":104,"code":828,"language":106,"meta":107,"style":107},"import os\n\n\ndef plugins_enabled() -> bool:\n    return os.environ.get(\"MYTOOL_NO_PLUGINS\", \"\") in (\"\", \"0\")\n\n\ndef disabled_plugins(config: dict) -> set[str]:\n    return set(config.get(\"plugins\", {}).get(\"disabled\", []))\n",[14,830,831,838,842,846,860,891,895,899,920],{"__ignoreMap":107},[111,832,833,835],{"class":113,"line":114},[111,834,149],{"class":124},[111,836,837],{"class":135}," os\n",[111,839,840],{"class":113,"line":121},[111,841,143],{"emptyLinePlaceholder":142},[111,843,844],{"class":113,"line":139},[111,845,143],{"emptyLinePlaceholder":142},[111,847,848,850,853,855,858],{"class":113,"line":146},[111,849,361],{"class":124},[111,851,852],{"class":197}," plugins_enabled",[111,854,687],{"class":135},[111,856,857],{"class":128},"bool",[111,859,242],{"class":135},[111,861,862,864,867,870,872,875,878,880,882,884,886,889],{"class":113,"line":155},[111,863,492],{"class":124},[111,865,866],{"class":135}," os.environ.get(",[111,868,869],{"class":266},"\"MYTOOL_NO_PLUGINS\"",[111,871,598],{"class":135},[111,873,874],{"class":266},"\"\"",[111,876,877],{"class":135},") ",[111,879,543],{"class":124},[111,881,282],{"class":135},[111,883,874],{"class":266},[111,885,598],{"class":135},[111,887,888],{"class":266},"\"0\"",[111,890,317],{"class":135},[111,892,893],{"class":113,"line":168},[111,894,143],{"emptyLinePlaceholder":142},[111,896,897],{"class":113,"line":173},[111,898,143],{"emptyLinePlaceholder":142},[111,900,901,903,906,909,912,915,917],{"class":113,"line":181},[111,902,361],{"class":124},[111,904,905],{"class":197}," disabled_plugins",[111,907,908],{"class":135},"(config: ",[111,910,911],{"class":128},"dict",[111,913,914],{"class":135},") -> set[",[111,916,222],{"class":128},[111,918,919],{"class":135},"]:\n",[111,921,922,924,927,930,933,936,939],{"class":113,"line":186},[111,923,492],{"class":124},[111,925,926],{"class":128}," set",[111,928,929],{"class":135},"(config.get(",[111,931,932],{"class":266},"\"plugins\"",[111,934,935],{"class":135},", {}).get(",[111,937,938],{"class":266},"\"disabled\"",[111,940,941],{"class":135},", []))\n",[10,943,944,947,948,951,952,955,956,959,960,962,963,966],{},[14,945,946],{},"MYTOOL_NO_PLUGINS=1 mytool status"," runs the bare host. A ",[14,949,950],{},"disabled = [\"aws\"]"," list in the configuration file switches off individual plugins without uninstalling them, which is gentler when the plugin is needed again next week. Both belong in the output of ",[14,953,954],{},"mytool plugins list",", so the current state is always visible. If you prefer a flag, peek at ",[14,957,958],{},"sys.argv"," for ",[14,961,824],{}," in ",[14,964,965],{},"main()"," before importing the module that mounts plugins, and remove it from the argument list.",[94,968,970],{"id":969},"ambient-misbehaviour","Ambient misbehaviour",[10,972,973],{},"Some plugin problems are not exceptions at all. Make the host robust where it can be, and document the rest as requirements for plugin authors:",[48,975,976,982,997],{},[51,977,978,981],{},[73,979,980],{},"Stdout discipline."," Plugins must write diagnostics to stderr. The host can enforce this for its own machine-readable modes by rendering JSON itself from data the plugin returns, rather than letting plugins print.",[51,983,984,987,988,991,992,996],{},[73,985,986],{},"No global configuration."," Plugins must not call ",[14,989,990],{},"logging.basicConfig",", install signal handlers or change the working directory at import. The audit-hook probe from ",[28,993,995],{"href":994},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Favoiding-import-time-side-effects\u002F","avoiding import-time side effects"," can be pointed at plugin modules in a plugin's own CI.",[51,998,999,1002],{},[73,1000,1001],{},"Import cost."," Load plugins lazily, so a slow plugin only costs time when its command runs.",[94,1004,1006],{"id":1005},"out-of-process-plugins-for-code-you-do-not-trust","Out-of-process plugins for code you do not trust",[10,1008,1009,1010,1013,1014,1017,1018,1021],{},"In-process isolation stops crashes from being misattributed; it does not stop a plugin from reading your users' tokens. When plugins come from anywhere, run them as separate programs. The model Git popularised — ",[14,1011,1012],{},"git foo"," runs an executable called ",[14,1015,1016],{},"git-foo"," found on ",[14,1019,1020],{},"PATH"," — needs no plugin API at all:",[102,1023,1025],{"className":104,"code":1024,"language":106,"meta":107,"style":107},"import os\nimport shutil\nimport subprocess\nimport sys\n\n\ndef run_external(name: str, args: list[str]) -> int:\n    exe = shutil.which(f\"mytool-{name}\")\n    if exe is None:\n        raise SystemExit(f\"error: unknown command {name!r}\")\n    env = {k: v for k, v in os.environ.items() if not k.startswith(\"MYTOOL_TOKEN\")}\n    return subprocess.run([exe, *args], env=env).returncode\n",[14,1026,1027,1033,1040,1047,1054,1058,1062,1087,1113,1129,1154,1190],{"__ignoreMap":107},[111,1028,1029,1031],{"class":113,"line":114},[111,1030,149],{"class":124},[111,1032,837],{"class":135},[111,1034,1035,1037],{"class":113,"line":121},[111,1036,149],{"class":124},[111,1038,1039],{"class":135}," shutil\n",[111,1041,1042,1044],{"class":113,"line":139},[111,1043,149],{"class":124},[111,1045,1046],{"class":135}," subprocess\n",[111,1048,1049,1051],{"class":113,"line":146},[111,1050,149],{"class":124},[111,1052,1053],{"class":135}," sys\n",[111,1055,1056],{"class":113,"line":155},[111,1057,143],{"emptyLinePlaceholder":142},[111,1059,1060],{"class":113,"line":168},[111,1061,143],{"emptyLinePlaceholder":142},[111,1063,1064,1066,1069,1072,1074,1077,1079,1082,1085],{"class":113,"line":173},[111,1065,361],{"class":124},[111,1067,1068],{"class":197}," run_external",[111,1070,1071],{"class":135},"(name: ",[111,1073,222],{"class":128},[111,1075,1076],{"class":135},", args: list[",[111,1078,222],{"class":128},[111,1080,1081],{"class":135},"]) -> ",[111,1083,1084],{"class":128},"int",[111,1086,242],{"class":135},[111,1088,1089,1092,1094,1097,1099,1102,1104,1107,1109,1111],{"class":113,"line":181},[111,1090,1091],{"class":135},"    exe ",[111,1093,329],{"class":124},[111,1095,1096],{"class":135}," shutil.which(",[111,1098,724],{"class":124},[111,1100,1101],{"class":266},"\"mytool-",[111,1103,270],{"class":128},[111,1105,1106],{"class":135},"name",[111,1108,279],{"class":128},[111,1110,314],{"class":266},[111,1112,317],{"class":135},[111,1114,1115,1118,1121,1124,1127],{"class":113,"line":186},[111,1116,1117],{"class":124},"    if",[111,1119,1120],{"class":135}," exe ",[111,1122,1123],{"class":124},"is",[111,1125,1126],{"class":128}," None",[111,1128,242],{"class":135},[111,1130,1131,1133,1135,1137,1139,1142,1144,1146,1148,1150,1152],{"class":113,"line":191},[111,1132,791],{"class":124},[111,1134,794],{"class":128},[111,1136,201],{"class":135},[111,1138,724],{"class":124},[111,1140,1141],{"class":266},"\"error: unknown command ",[111,1143,270],{"class":128},[111,1145,1106],{"class":135},[111,1147,276],{"class":124},[111,1149,279],{"class":128},[111,1151,314],{"class":266},[111,1153,317],{"class":135},[111,1155,1156,1159,1161,1164,1167,1170,1172,1175,1178,1181,1184,1187],{"class":113,"line":210},[111,1157,1158],{"class":135},"    env ",[111,1160,329],{"class":124},[111,1162,1163],{"class":135}," {k: v ",[111,1165,1166],{"class":124},"for",[111,1168,1169],{"class":135}," k, v ",[111,1171,543],{"class":124},[111,1173,1174],{"class":135}," os.environ.items() ",[111,1176,1177],{"class":124},"if",[111,1179,1180],{"class":124}," not",[111,1182,1183],{"class":135}," k.startswith(",[111,1185,1186],{"class":266},"\"MYTOOL_TOKEN\"",[111,1188,1189],{"class":135},")}\n",[111,1191,1192,1194,1197,1199,1202,1205,1207],{"class":113,"line":245},[111,1193,492],{"class":124},[111,1195,1196],{"class":135}," subprocess.run([exe, ",[111,1198,401],{"class":124},[111,1200,1201],{"class":135},"args], ",[111,1203,1204],{"class":741},"env",[111,1206,329],{"class":124},[111,1208,1209],{"class":135},"env).returncode\n",[10,1211,1212,1213,1217],{},"The external program cannot touch the host's memory, its crashes are its own exit codes, and the host decides which environment variables to pass — here, everything except the host's credentials. The cost is a weaker contract: data passes as arguments, environment and standard streams, so define those clearly, ideally as JSON over stdin and stdout. ",[28,1214,1216],{"href":1215},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess\u002F","Calling external commands safely with subprocess"," covers the invocation details.",[66,1219],{"name":1220},"plugiso-models",[43,1222,1224],{"id":1223},"ux-considerations","UX considerations",[48,1226,1227,1233,1249,1258,1267],{},[51,1228,1229,1232],{},[73,1230,1231],{},"Blame accurately."," Users deserve to know whether to report a bug to you or to a plugin author; include the plugin's package name and version, and its project URL from package metadata if it has one.",[51,1234,1235,1248],{},[73,1236,1237,1238,598,1241,1244,1245,41],{},"Never let a plugin break ",[14,1239,1240],{},"--help",[14,1242,1243],{},"--version"," or ",[14,1246,1247],{},"plugins list"," These are the commands people use to diagnose plugin problems.",[51,1250,1251,1254,1255,1257],{},[73,1252,1253],{},"Show state in one place."," ",[14,1256,1247],{}," with columns for loaded, failed and disabled plugins answers most support questions.",[51,1259,1260,1263,1264,1266],{},[73,1261,1262],{},"Use a distinct exit code"," for plugin crashes (70, ",[14,1265,663],{},", is conventional for internal errors) so automation can tell them apart.",[51,1268,1269,1272,1273,1276],{},[73,1270,1271],{},"Make safe mode discoverable."," Mention ",[14,1274,1275],{},"MYTOOL_NO_PLUGINS=1"," in the plugin crash message and in your troubleshooting docs.",[43,1278,1280],{"id":1279},"testing-the-behaviour","Testing the behaviour",[10,1282,1283],{},"Test the guard with a plugin defined inside the test:",[102,1285,1287],{"className":104,"code":1286,"language":106,"meta":107,"style":107},"# tests\u002Ftest_plugin_guard.py\nimport typer\nfrom typer.testing import CliRunner\n\nfrom mytool.plugin_guard import PluginCrashed, isolate\n\n\ndef make_host() -> typer.Typer:\n    plugin = typer.Typer()\n\n    @plugin.command()\n    def boom(n: int = 1) -> None:\n        print(1 \u002F (n - 1))\n\n    @plugin.command()\n    def stop() -> None:\n        raise typer.Exit(3)\n\n    host = typer.Typer()\n\n    @host.callback()\n    def main() -> None:\n        \"\"\"Host.\"\"\"\n\n    host.add_typer(isolate(plugin, \"aws\", \"mytool-aws 0.3.1\"), name=\"aws\")\n    return host\n\n\ndef test_crash_is_attributed_to_the_plugin():\n    result = CliRunner().invoke(make_host(), [\"aws\", \"boom\"])\n    assert isinstance(result.exception, PluginCrashed)\n    assert \"mytool-aws 0.3.1\" in str(result.exception)\n    assert isinstance(result.exception.__cause__, ZeroDivisionError)\n\n\ndef test_exit_passes_through_unchanged():\n    assert CliRunner().invoke(make_host(), [\"aws\", \"stop\"]).exit_code == 3\n\n\ndef test_signature_and_help_survive_wrapping():\n    result = CliRunner().invoke(make_host(), [\"aws\", \"boom\", \"--help\"])\n    assert result.exit_code == 0 and \"--n\" in result.output\n",[14,1288,1289,1294,1300,1312,1316,1327,1331,1335,1345,1355,1359,1367,1390,1414,1418,1424,1437,1449,1453,1462,1466,1473,1485,1490,1494,1518,1525,1529,1533,1543,1563,1574,1590,1603,1607,1611,1620,1643,1648,1653,1663,1685],{"__ignoreMap":107},[111,1290,1291],{"class":113,"line":114},[111,1292,1293],{"class":117},"# tests\u002Ftest_plugin_guard.py\n",[111,1295,1296,1298],{"class":113,"line":121},[111,1297,149],{"class":124},[111,1299,178],{"class":135},[111,1301,1302,1304,1307,1309],{"class":113,"line":139},[111,1303,125],{"class":124},[111,1305,1306],{"class":135}," typer.testing ",[111,1308,149],{"class":124},[111,1310,1311],{"class":135}," CliRunner\n",[111,1313,1314],{"class":113,"line":146},[111,1315,143],{"emptyLinePlaceholder":142},[111,1317,1318,1320,1322,1324],{"class":113,"line":155},[111,1319,125],{"class":124},[111,1321,649],{"class":135},[111,1323,149],{"class":124},[111,1325,1326],{"class":135}," PluginCrashed, isolate\n",[111,1328,1329],{"class":113,"line":168},[111,1330,143],{"emptyLinePlaceholder":142},[111,1332,1333],{"class":113,"line":173},[111,1334,143],{"emptyLinePlaceholder":142},[111,1336,1337,1339,1342],{"class":113,"line":181},[111,1338,361],{"class":124},[111,1340,1341],{"class":197}," make_host",[111,1343,1344],{"class":135},"() -> typer.Typer:\n",[111,1346,1347,1350,1352],{"class":113,"line":186},[111,1348,1349],{"class":135},"    plugin ",[111,1351,329],{"class":124},[111,1353,1354],{"class":135}," typer.Typer()\n",[111,1356,1357],{"class":113,"line":191},[111,1358,143],{"emptyLinePlaceholder":142},[111,1360,1361,1364],{"class":113,"line":210},[111,1362,1363],{"class":197},"    @plugin.command",[111,1365,1366],{"class":135},"()\n",[111,1368,1369,1371,1374,1377,1379,1381,1384,1386,1388],{"class":113,"line":245},[111,1370,213],{"class":124},[111,1372,1373],{"class":197}," boom",[111,1375,1376],{"class":135},"(n: ",[111,1378,1084],{"class":128},[111,1380,666],{"class":124},[111,1382,1383],{"class":128}," 1",[111,1385,236],{"class":135},[111,1387,239],{"class":128},[111,1389,242],{"class":135},[111,1391,1392,1395,1397,1400,1403,1406,1409,1411],{"class":113,"line":260},[111,1393,1394],{"class":128},"        print",[111,1396,201],{"class":135},[111,1398,1399],{"class":128},"1",[111,1401,1402],{"class":124}," \u002F",[111,1404,1405],{"class":135}," (n ",[111,1407,1408],{"class":124},"-",[111,1410,1383],{"class":128},[111,1412,1413],{"class":135},"))\n",[111,1415,1416],{"class":113,"line":320},[111,1417,143],{"emptyLinePlaceholder":142},[111,1419,1420,1422],{"class":113,"line":335},[111,1421,1363],{"class":197},[111,1423,1366],{"class":135},[111,1425,1426,1428,1431,1433,1435],{"class":113,"line":348},[111,1427,213],{"class":124},[111,1429,1430],{"class":197}," stop",[111,1432,687],{"class":135},[111,1434,239],{"class":128},[111,1436,242],{"class":135},[111,1438,1439,1441,1444,1447],{"class":113,"line":353},[111,1440,791],{"class":124},[111,1442,1443],{"class":135}," typer.Exit(",[111,1445,1446],{"class":128},"3",[111,1448,317],{"class":135},[111,1450,1451],{"class":113,"line":358},[111,1452,143],{"emptyLinePlaceholder":142},[111,1454,1455,1458,1460],{"class":113,"line":379},[111,1456,1457],{"class":135},"    host ",[111,1459,329],{"class":124},[111,1461,1354],{"class":135},[111,1463,1464],{"class":113,"line":391},[111,1465,143],{"emptyLinePlaceholder":142},[111,1467,1468,1471],{"class":113,"line":413},[111,1469,1470],{"class":197},"    @host.callback",[111,1472,1366],{"class":135},[111,1474,1475,1477,1479,1481,1483],{"class":113,"line":421},[111,1476,213],{"class":124},[111,1478,684],{"class":197},[111,1480,687],{"class":135},[111,1482,239],{"class":128},[111,1484,242],{"class":135},[111,1486,1487],{"class":113,"line":439},[111,1488,1489],{"class":266},"        \"\"\"Host.\"\"\"\n",[111,1491,1492],{"class":113,"line":453},[111,1493,143],{"emptyLinePlaceholder":142},[111,1495,1496,1499,1502,1504,1507,1510,1512,1514,1516],{"class":113,"line":462},[111,1497,1498],{"class":135},"    host.add_typer(isolate(plugin, ",[111,1500,1501],{"class":266},"\"aws\"",[111,1503,598],{"class":135},[111,1505,1506],{"class":266},"\"mytool-aws 0.3.1\"",[111,1508,1509],{"class":135},"), ",[111,1511,1106],{"class":741},[111,1513,329],{"class":124},[111,1515,1501],{"class":266},[111,1517,317],{"class":135},[111,1519,1520,1522],{"class":113,"line":476},[111,1521,492],{"class":124},[111,1523,1524],{"class":135}," host\n",[111,1526,1527],{"class":113,"line":489},[111,1528,143],{"emptyLinePlaceholder":142},[111,1530,1531],{"class":113,"line":498},[111,1532,143],{"emptyLinePlaceholder":142},[111,1534,1535,1537,1540],{"class":113,"line":503},[111,1536,361],{"class":124},[111,1538,1539],{"class":197}," test_crash_is_attributed_to_the_plugin",[111,1541,1542],{"class":135},"():\n",[111,1544,1545,1548,1550,1553,1555,1557,1560],{"class":113,"line":508},[111,1546,1547],{"class":135},"    result ",[111,1549,329],{"class":124},[111,1551,1552],{"class":135}," CliRunner().invoke(make_host(), [",[111,1554,1501],{"class":266},[111,1556,598],{"class":135},[111,1558,1559],{"class":266},"\"boom\"",[111,1561,1562],{"class":135},"])\n",[111,1564,1565,1568,1571],{"class":113,"line":528},[111,1566,1567],{"class":124},"    assert",[111,1569,1570],{"class":128}," isinstance",[111,1572,1573],{"class":135},"(result.exception, PluginCrashed)\n",[111,1575,1576,1578,1581,1584,1587],{"class":113,"line":534},[111,1577,1567],{"class":124},[111,1579,1580],{"class":266}," \"mytool-aws 0.3.1\"",[111,1582,1583],{"class":124}," in",[111,1585,1586],{"class":128}," str",[111,1588,1589],{"class":135},"(result.exception)\n",[111,1591,1592,1594,1596,1599,1601],{"class":113,"line":549},[111,1593,1567],{"class":124},[111,1595,1570],{"class":128},[111,1597,1598],{"class":135},"(result.exception.__cause__, ",[111,1600,16],{"class":128},[111,1602,317],{"class":135},[111,1604,1605],{"class":113,"line":560},[111,1606,143],{"emptyLinePlaceholder":142},[111,1608,1609],{"class":113,"line":573},[111,1610,143],{"emptyLinePlaceholder":142},[111,1612,1613,1615,1618],{"class":113,"line":579},[111,1614,361],{"class":124},[111,1616,1617],{"class":197}," test_exit_passes_through_unchanged",[111,1619,1542],{"class":135},[111,1621,1623,1625,1627,1629,1631,1634,1637,1640],{"class":113,"line":1622},37,[111,1624,1567],{"class":124},[111,1626,1552],{"class":135},[111,1628,1501],{"class":266},[111,1630,598],{"class":135},[111,1632,1633],{"class":266},"\"stop\"",[111,1635,1636],{"class":135},"]).exit_code ",[111,1638,1639],{"class":124},"==",[111,1641,1642],{"class":128}," 3\n",[111,1644,1646],{"class":113,"line":1645},38,[111,1647,143],{"emptyLinePlaceholder":142},[111,1649,1651],{"class":113,"line":1650},39,[111,1652,143],{"emptyLinePlaceholder":142},[111,1654,1656,1658,1661],{"class":113,"line":1655},40,[111,1657,361],{"class":124},[111,1659,1660],{"class":197}," test_signature_and_help_survive_wrapping",[111,1662,1542],{"class":135},[111,1664,1666,1668,1670,1672,1674,1676,1678,1680,1683],{"class":113,"line":1665},41,[111,1667,1547],{"class":135},[111,1669,329],{"class":124},[111,1671,1552],{"class":135},[111,1673,1501],{"class":266},[111,1675,598],{"class":135},[111,1677,1559],{"class":266},[111,1679,598],{"class":135},[111,1681,1682],{"class":266},"\"--help\"",[111,1684,1562],{"class":135},[111,1686,1688,1690,1693,1695,1698,1701,1704,1706],{"class":113,"line":1687},42,[111,1689,1567],{"class":124},[111,1691,1692],{"class":135}," result.exit_code ",[111,1694,1639],{"class":124},[111,1696,1697],{"class":128}," 0",[111,1699,1700],{"class":124}," and",[111,1702,1703],{"class":266}," \"--n\"",[111,1705,1583],{"class":124},[111,1707,1708],{"class":135}," result.output\n",[10,1710,1711,1712,1714,1715,1717,1718,1721,1722,41],{},"The help test protects the subtle part: if a refactor drops ",[14,1713,589],{},", plugin options silently disappear. Add a test for safe mode by setting ",[14,1716,1275],{}," with ",[14,1719,1720],{},"monkeypatch"," and asserting the plugin's command is absent from ",[14,1723,1240],{},[43,1725,1727],{"id":1726},"conclusion","Conclusion",[10,1729,1730],{},"Plugins fail at load time, at run time and ambiently, and a host CLI needs a defence for each. Catch load errors during discovery; wrap plugin commands so runtime crashes are attributed to the plugin and reported with its package and version; offer an environment-variable safe mode and per-plugin disabling; publish clear rules about stdout and global state; and for untrusted code, run plugins as separate programs with a controlled environment. Users then know whose bug they hit — and can keep working while it is fixed.",[43,1732,1734],{"id":1733},"frequently-asked-questions","Frequently asked questions",[94,1736,1738],{"id":1737},"can-a-plugin-still-crash-the-whole-process","Can a plugin still crash the whole process?",[10,1740,1741,1742,1745],{},"Yes — a plugin can call ",[14,1743,1744],{},"os._exit",", segfault in a C extension or exhaust memory, and nothing in-process prevents that. Only out-of-process plugins are isolated from those failures.",[94,1747,1749,1750,1752],{"id":1748},"should-the-host-catch-baseexception-in-plugins","Should the host catch ",[14,1751,233],{}," in plugins?",[10,1754,1755,1756,1759,1760,1762,1763,1765],{},"No. ",[14,1757,1758],{},"SystemExit"," and ",[14,1761,448],{}," are how programs end and how users interrupt them; swallowing them makes the CLI impossible to stop. Catch ",[14,1764,204],{}," and let control-flow exceptions through, as the guard does.",[94,1767,1769],{"id":1768},"how-do-i-show-plugin-authors-their-own-traceback","How do I show plugin authors their own traceback?",[10,1771,1772,1773,1776,1777,1780],{},"Honour a debug variable (",[14,1774,1775],{},"MYTOOL_DEBUG=1",") that re-raises or prints the chained exception with ",[14,1778,1779],{},"traceback.print_exception",". Plugin authors debugging their code want the full trace; users want the one-line attribution.",[94,1782,1784],{"id":1783},"is-pluggy-safer-than-entry-point-command-groups","Is pluggy safer than entry-point command groups?",[10,1786,1787,1788,41],{},"pluggy structures how hooks are called and combines results, which makes some failures easier to handle, but plugin code still runs in-process. The same attribution and safe-mode techniques apply; see ",[28,1789,1791],{"href":1790},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy\u002F","hook-based plugins with pluggy",[43,1793,1795],{"id":1794},"related","Related",[48,1797,1798,1804,1809,1815,1821],{},[51,1799,1800,1801],{},"Up: ",[28,1802,1803],{"href":39},"Plugin architectures for extensible CLIs",[51,1805,1806],{},[28,1807,1808],{"href":30},"Discovering plugins with entry points",[51,1810,1811],{},[28,1812,1814],{"href":1813},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Ftesting-plugins-against-the-host-cli\u002F","Testing plugins against the host CLI",[51,1816,1817],{},[28,1818,1820],{"href":1819},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdesigning-an-exception-hierarchy-for-a-cli\u002F","Designing an exception hierarchy for a CLI",[51,1822,1823],{},[28,1824,1826],{"href":1825},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis\u002F","Avoiding shell injection in Python CLIs",[1828,1829,1830],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":107,"searchDepth":121,"depth":121,"links":1832},[1833,1834,1835,1841,1842,1843,1844,1851],{"id":45,"depth":121,"text":46},{"id":63,"depth":121,"text":64},{"id":91,"depth":121,"text":92,"children":1836},[1837,1838,1839,1840],{"id":96,"depth":139,"text":97},{"id":817,"depth":139,"text":818},{"id":969,"depth":139,"text":970},{"id":1005,"depth":139,"text":1006},{"id":1223,"depth":121,"text":1224},{"id":1279,"depth":121,"text":1280},{"id":1726,"depth":121,"text":1727},{"id":1733,"depth":121,"text":1734,"children":1845},[1846,1847,1849,1850],{"id":1737,"depth":139,"text":1738},{"id":1748,"depth":139,"text":1848},"Should the host catch BaseException in plugins?",{"id":1768,"depth":139,"text":1769},{"id":1783,"depth":139,"text":1784},{"id":1794,"depth":121,"text":1795},"2026-10-02","Keep third-party plugins from taking a CLI down: attribute crashes to the plugin, offer a no-plugins safe mode, disable single plugins, and run untrusted ones out of process.","advanced",false,"md",{},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures",{"title":5,"description":1853},"modern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures\u002Findex",[1862,1863,1864,1865,1866],"plugins","reliability","error-handling","typer","architecture","TuKHBInN6cs_9Sbk1CHyxABQQaGvL763UAFgQg6xOMk",[1869,1872,1875,1878,1881,1884,1887,1890,1893,1896,1899,1902,1905,1908,1911,1914,1917,1920,1923,1926,1929,1932,1935,1938,1941,1944,1947,1950,1953,1956,1959,1962,1965,1968,1971,1974,1977,1980,1983,1986,1989,1992,1995,1998,2001,2004,2007,2010,2013,2016,2019,2022,2025,2028,2031,2034,2037,2040,2043,2046,2049,2052,2055,2058,2061,2064,2067,2070,2073,2076,2079,2082,2085,2088,2091,2094,2097,2100,2103,2106,2109,2112,2115,2118,2121,2124,2127,2130,2133,2136,2139,2142,2145,2148,2151,2154,2157,2160,2163,2166,2169,2172,2175,2178,2181,2184,2187,2190,2193,2196,2199,2202,2205,2208,2211,2214,2217,2220,2223,2226,2229,2232,2235,2238,2241,2244,2247,2250,2253,2256,2259,2262,2265,2268,2271,2274,2277,2280,2283,2286,2289,2292,2295,2298,2301,2304,2307,2310,2313,2316,2319,2322,2325,2328,2331,2334,2337,2340,2343,2346,2349,2352,2355,2358,2361,2364,2367,2370,2373,2376,2379,2382,2383,2386,2389,2392,2395,2398,2401,2404,2407,2410,2413,2416,2419,2422,2425,2428,2431,2434,2437,2440,2443,2446,2449,2452,2455,2458,2461,2464,2467,2470,2473,2476,2479,2482,2485,2488,2491,2494,2497,2500,2503,2506,2509,2512,2515,2518,2521,2524,2527,2530,2533,2536,2539,2542,2545,2548,2551,2554,2557,2560,2563,2566,2569,2572,2575,2578,2581,2584,2587,2590,2593,2596,2599,2602,2605,2608,2611,2614,2617,2620,2623,2626,2629,2632,2635,2638,2641,2644,2647,2650,2653,2656,2659,2662,2665,2668,2671,2674,2677,2680,2683,2686,2689,2692,2695,2698,2701,2704,2707,2710],{"path":1870,"title":1871},"\u002Fabout","About Python CLI Toolcraft",{"path":1873,"title":1874},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1876,"title":1877},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1879,"title":1880},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dates-and-durations-in-cli-arguments","Validating Dates and Durations in Python CLI Arguments",{"path":1882,"title":1883},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1885,"title":1886},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1888,"title":1889},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-urls-hosts-and-ports","Validating URLs, Hosts and Ports in Python CLI Arguments",{"path":1891,"title":1892},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":1894,"title":1895},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbrowsing-records-with-a-textual-datatable","Browsing Records with a Textual DataTable Picker",{"path":1897,"title":1898},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbuilding-your-first-textual-app","Building Your First Textual App for a Python CLI",{"path":1900,"title":1901},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fchoosing-between-a-cli-a-prompt-flow-and-a-tui","Choosing Between a CLI, a Prompt Flow and a TUI",{"path":1903,"title":1904},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":1906,"title":1907},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Frunning-background-work-in-textual-with-workers","Running Background Work in Textual with Workers",{"path":1909,"title":1910},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fstyling-textual-apps-with-tcss","Styling Textual Apps with TCSS",{"path":1912,"title":1913},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":1915,"title":1916},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fadding-examples-and-epilogs-to-help-output","Adding Examples and Epilogs to Help Output",{"path":1918,"title":1919},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fdocumenting-environment-variables-in-help","Documenting Environment Variables in CLI Help",{"path":1921,"title":1922},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fgenerating-man-pages-and-docs-from-a-cli","Generating Man Pages and Docs from a CLI",{"path":1924,"title":1925},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1927,"title":1928},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Frich-formatted-help-with-rich-click","Rich-Formatted Help for Click CLIs with rich-click",{"path":1930,"title":1931},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1933,"title":1934},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1936,"title":1937},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":1939,"title":1940},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":1942,"title":1943},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Ffixing-unicode-and-encoding-errors-on-windows","Fixing Unicode and Encoding Errors on Windows in Python CLIs",{"path":1945,"title":1946},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fhandling-ansi-escape-codes-on-windows-consoles","Handling ANSI Escape Codes on Windows Consoles",{"path":1948,"title":1949},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":1951,"title":1952},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Frespecting-no-color-and-force-color","Respecting NO_COLOR and FORCE_COLOR in Python CLIs",{"path":1954,"title":1955},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fsupporting-dumb-terminals-and-screen-readers","Supporting Dumb Terminals and Screen Readers in a Python CLI",{"path":1957,"title":1958},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1960,"title":1961},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdesigning-an-exception-hierarchy-for-a-cli","Designing an Exception Hierarchy for a Python CLI",{"path":1963,"title":1964},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdid-you-mean-suggestions-for-mistyped-input","Did You Mean…? Suggestions for Mistyped CLI Input",{"path":1966,"title":1967},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1969,"title":1970},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1972,"title":1973},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1975,"title":1976},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Freporting-machine-readable-errors-in-json-mode","Reporting Machine-Readable Errors in JSON Mode",{"path":1978,"title":1979},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fwriting-crash-reports-users-can-send","Writing Crash Reports Users Can Send",{"path":1981,"title":1982},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1984,"title":1985},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fdiscovering-project-config-files-by-walking-up-directories","Discovering Project Config Files by Walking Up Directories",{"path":1987,"title":1988},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1990,"title":1991},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Floading-yaml-configs-safely-in-cli-apps","Loading YAML configs safely in CLI apps",{"path":1993,"title":1994},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":1996,"title":1997},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":1999,"title":2000},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fvalidating-config-files-with-json-schema","Validating Config Files with JSON Schema in a Python CLI",{"path":2002,"title":2003},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fwriting-a-config-init-and-edit-command","Writing a Config Init and Edit Command for a Python CLI",{"path":2005,"title":2006},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2008,"title":2009},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fadding-progress-bars-and-spinners-to-python-clis","Progress Bars and Spinners for Python CLIs",{"path":2011,"title":2012},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2014,"title":2015},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-tree-views-with-rich","Building Tree Views with Rich in a Python CLI",{"path":2017,"title":2018},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2020,"title":2021},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2023,"title":2024},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-markdown-and-syntax-highlighting-with-rich","Rendering Markdown and Syntax Highlighting with Rich",{"path":2026,"title":2027},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2029,"title":2030},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2032,"title":2033},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fadding-a-format-flag-for-table-json-and-csv","Adding a Format Flag for Table, JSON and CSV Output",{"path":2035,"title":2036},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fcustom-output-templates-with-a-format-string","Custom Output Templates with a Format String in Python CLIs",{"path":2038,"title":2039},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fexporting-cli-results-to-files","Exporting CLI Results to Files from a Python CLI",{"path":2041,"title":2042},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis","Output Formats for Data-Heavy Python CLIs",{"path":2044,"title":2045},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fselecting-fields-and-columns-from-cli-output","Selecting Fields and Columns from Python CLI Output",{"path":2047,"title":2048},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fwriting-csv-and-tsv-output-correctly","Writing CSV and TSV Output Correctly from a Python CLI",{"path":2050,"title":2051},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fargcomplete-for-argparse-clis","Tab Completion for argparse CLIs with argcomplete",{"path":2053,"title":2054},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fdynamic-completion-values-from-apis-and-files","Dynamic Completion Values from APIs and Files",{"path":2056,"title":2057},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fenabling-tab-completion-in-click-and-typer","Enabling Tab Completion in Click and Typer",{"path":2059,"title":2060},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2062,"title":2063},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Finstalling-shell-completion-for-bash-zsh-fish","Installing Shell Completion for bash, zsh, fish",{"path":2065,"title":2066},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fshipping-completion-scripts-with-packages","Shipping Shell Completion Scripts with a Python CLI",{"path":2068,"title":2069},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2071,"title":2072},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-trace-ids-and-context-to-cli-logs","Adding Trace IDs and Context to Python CLI Logs",{"path":2074,"title":2075},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2077,"title":2078},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2080,"title":2081},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Flogging-with-structlog-in-a-cli","Logging with structlog in a Python CLI",{"path":2083,"title":2084},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fseparating-logs-from-program-output","Separating Logs from Program Output in a Python CLI",{"path":2086,"title":2087},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2089,"title":2090},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fwriting-rotating-log-files-from-a-cli","Writing Rotating Log Files from a Python CLI",{"path":2092,"title":2093},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2095,"title":2096},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2098,"title":2099},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2101,"title":2102},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2104,"title":2105},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fnull-delimited-input-and-xargs-compatibility","Null-Delimited Input and xargs Compatibility in Python CLIs",{"path":2107,"title":2108},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fprocessing-large-files-and-ndjson-streams","Processing Large Files and NDJSON Streams in Python CLIs",{"path":2110,"title":2111},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2113,"title":2114},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fbuilding-an-api-client-cli-with-httpx","Building an API Client CLI with httpx",{"path":2116,"title":2117},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2119,"title":2120},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2122,"title":2123},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fmocking-http-in-cli-tests-with-respx","Mocking HTTP in Python CLI Tests with respx",{"path":2125,"title":2126},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2128,"title":2129},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2131,"title":2132},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls","Retries and Backoff for CLI HTTP Calls",{"path":2134,"title":2135},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fuploading-files-with-multipart-and-progress","Uploading Files with Multipart and Progress in a Python CLI",{"path":2137,"title":2138},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fcancelling-async-tasks-on-ctrl-c","Cancelling Async Tasks on Ctrl+C in Python CLIs",{"path":2140,"title":2141},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2143,"title":2144},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2146,"title":2147},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2149,"title":2150},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2152,"title":2153},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frunning-async-code-in-typer-and-click","Running Async Code in Typer and Click",{"path":2155,"title":2156},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fshowing-progress-for-concurrent-tasks","Showing Progress for Concurrent Tasks in a Python CLI",{"path":2158,"title":2159},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis","Structured Concurrency with TaskGroups in Python CLIs",{"path":2161,"title":2162},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2164,"title":2165},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2167,"title":2168},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fhandling-file-permissions-and-umask-in-clis","Handling File Permissions and umask in Python CLIs",{"path":2170,"title":2171},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2173,"title":2174},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2176,"title":2177},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2179,"title":2180},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwalking-directory-trees-with-ignore-rules","Walking Directory Trees with Ignore Rules in a Python CLI",{"path":2182,"title":2183},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2185,"title":2186},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2188,"title":2189},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fcaching-http-responses-on-disk-in-a-cli","Caching HTTP Responses on Disk in a Python CLI",{"path":2191,"title":2192},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis","Local State and SQLite in Python CLIs",{"path":2194,"title":2195},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fmigrating-a-cli-sqlite-schema","Migrating a CLI’s SQLite Schema Between Releases",{"path":2197,"title":2198},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Frecording-and-querying-cli-run-history","Recording and Querying CLI Run History in SQLite",{"path":2200,"title":2201},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fstoring-cli-state-in-sqlite","Storing CLI State in SQLite with a Small Repository Class",{"path":2203,"title":2204},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fbuilding-a-watch-mode-with-watchfiles","Building a Watch Mode with watchfiles in Python",{"path":2206,"title":2207},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2209,"title":2210},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhealth-checks-and-heartbeats-for-long-running-clis","Health Checks and Heartbeats for Long-Running CLIs",{"path":2212,"title":2213},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2215,"title":2216},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Freloading-config-on-sighup","Reloading Configuration on SIGHUP in a Python CLI",{"path":2218,"title":2219},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-as-a-systemd-service","Running a Python CLI as a systemd Service",{"path":2221,"title":2222},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-on-a-schedule-with-cron-and-systemd","Running a Python CLI on a Schedule with cron and systemd",{"path":2224,"title":2225},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fsending-cli-logs-to-journald-and-syslog","Sending Python CLI Logs to journald and syslog",{"path":2227,"title":2228},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2230,"title":2231},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2233,"title":2234},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2236,"title":2237},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2239,"title":2240},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Flaunching-the-users-editor-from-a-cli","Launching the User’s Editor from a Python CLI",{"path":2242,"title":2243},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fpiping-between-subprocesses-in-python","Piping Between Subprocesses in Python Without a Shell",{"path":2245,"title":2246},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2248,"title":2249},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli","Wrapping git and Other Tools from a Python CLI",{"path":2251,"title":2252},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2254,"title":2255},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2257,"title":2258},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files","Reading Secrets from Env Vars and Files in CLIs",{"path":2260,"title":2261},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs","Redacting Secrets from CLI Output and Logs",{"path":2263,"title":2264},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Frefreshing-expired-tokens-automatically","Refreshing Expired Tokens Automatically in a Python CLI",{"path":2266,"title":2267},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2269,"title":2270},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":2272,"title":2273},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fchecking-pypi-for-a-newer-version","Checking PyPI for a Newer Version of Your Python CLI",{"path":2275,"title":2276},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis","Update Checks, Upgrade Commands and Telemetry for Python CLIs",{"path":2278,"title":2279},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fopt-in-usage-telemetry-for-python-clis","Opt-In Usage Telemetry for Python CLIs Done Responsibly",{"path":2281,"title":2282},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fself-upgrading-a-cli-installed-with-pipx-or-uv","Self-Upgrading a Python CLI Installed with pipx or uv",{"path":2284,"title":2285},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fshowing-non-blocking-update-notices","Showing Non-Blocking Update Notices in a Python CLI",{"path":2287,"title":2288},"\u002F","Python CLI Toolcraft",{"path":2290,"title":2291},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cleo","Building a Python CLI with Cleo Command Classes",{"path":2293,"title":2294},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts","Building a Type-Hinted Python CLI with Cyclopts",{"path":2296,"title":2297},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks","Beyond Click and Typer: Alternative Python CLI Frameworks",{"path":2299,"title":2300},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fquick-clis-from-functions-with-python-fire","Quick CLIs from Functions with Python Fire",{"path":2302,"title":2303},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fusage-string-driven-clis-with-docopt-ng","Usage-String Driven Python CLIs with docopt-ng",{"path":2305,"title":2306},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Favoiding-import-time-side-effects","Avoiding Import-Time Side Effects in a Python CLI",{"path":2308,"title":2309},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fcaching-expensive-work-between-cli-runs","Caching Expensive Work Between Python CLI Runs",{"path":2311,"title":2312},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests","Guarding CLI Startup with Import Tests",{"path":2314,"title":2315},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2317,"title":2318},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2320,"title":2321},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2323,"title":2324},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2326,"title":2327},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2329,"title":2330},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2332,"title":2333},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargument-groups-and-help-formatting-in-argparse","Argument Groups and Help Formatting in argparse",{"path":2335,"title":2336},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2338,"title":2339},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2341,"title":2342},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2344,"title":2345},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Freading-arguments-from-files-with-fromfile-prefix-chars","Reading Arguments from Files with argparse’s fromfile_prefix_chars",{"path":2347,"title":2348},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2350,"title":2351},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fadding-dry-run-and-confirmation-to-destructive-commands","Adding Dry-Run and Confirmation to Destructive Commands",{"path":2353,"title":2354},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fdesigning-idempotent-commands","Designing Idempotent Commands in a Python CLI",{"path":2356,"title":2357},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Ffollowing-posix-and-gnu-argument-conventions","Following POSIX and GNU Argument Conventions in Python",{"path":2359,"title":2360},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fglobal-options-vs-per-command-options","Global Options vs Per-Command Options in Python CLIs",{"path":2362,"title":2363},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2365,"title":2366},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2368,"title":2369},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fpositional-arguments-vs-options","Positional Arguments vs Options: Designing a CLI Signature",{"path":2371,"title":2372},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2374,"title":2375},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2377,"title":2378},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2380,"title":2381},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1858,"title":5},{"path":2384,"title":2385},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Ftesting-plugins-against-the-host-cli","Testing Plugins Against the Host CLI",{"path":2387,"title":2388},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2390,"title":2391},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fwriting-a-plugin-for-an-existing-cli","Writing a Plugin for an Existing CLI",{"path":2393,"title":2394},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points","Best practices for Python CLI entry points",{"path":2396,"title":2397},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2399,"title":2400},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fhow-to-structure-a-large-python-cli-project","Structuring a Large Python CLI Project",{"path":2402,"title":2403},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2405,"title":2406},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Foffering-a-python-api-alongside-your-cli","Offering a Python API Alongside Your CLI",{"path":2408,"title":2409},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fregistering-commands-from-modules-automatically","Registering CLI Commands from Modules Automatically",{"path":2411,"title":2412},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2414,"title":2415},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2417,"title":2418},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli","End-to-End Testing an Installed Python CLI",{"path":2420,"title":2421},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2423,"title":2424},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2426,"title":2427},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmocking-filesystem-and-network-in-cli-tests","Mocking the Filesystem and Network in CLI Tests",{"path":2429,"title":2430},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2432,"title":2433},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2435,"title":2436},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2438,"title":2439},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-commands-that-call-subprocesses","Testing Python CLI Commands That Call Subprocesses",{"path":2441,"title":2442},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2444,"title":2445},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftranscript-tests-for-cli-commands","Transcript Tests for Python CLI Commands",{"path":2447,"title":2448},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-a-cli-with-subcommands-in-click","Building a CLI with subcommands in Click",{"path":2450,"title":2451},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2453,"title":2454},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fchoices-and-enums-in-typer-and-click","Choices and Enums in Typer and Click Options",{"path":2456,"title":2457},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fclick-option-callbacks-and-eager-options","Click Option Callbacks and Eager Options Explained",{"path":2459,"title":2460},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fconverting-a-click-app-to-typer","Converting a Click App to Typer",{"path":2462,"title":2463},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2465,"title":2466},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Frich-markup-and-help-panels-in-typer","Rich Markup and Help Panels in Typer",{"path":2468,"title":2469},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2471,"title":2472},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2474,"title":2475},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2477,"title":2478},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2480,"title":2481},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2483,"title":2484},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-a-cli-docker-image-from-ci","Publishing a Python CLI as a Docker Image from CI",{"path":2486,"title":2487},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-to-pypi-with-trusted-publishing","Publishing a CLI to PyPI with Trusted Publishing",{"path":2489,"title":2490},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Frunning-cli-tests-on-windows-and-macos-runners","Running Python CLI Tests on Windows and macOS Runners",{"path":2492,"title":2493},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci","Smoke-Testing the Built Wheel of a Python CLI in CI",{"path":2495,"title":2496},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions","Testing a CLI Across Python Versions in GitHub Actions",{"path":2498,"title":2499},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2501,"title":2502},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2504,"title":2505},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2507,"title":2508},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2510,"title":2511},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftemplate-variables-and-conditional-files","Template Variables and Conditional Files in CLI Templates",{"path":2513,"title":2514},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftesting-a-project-template-with-pytest","Testing a CLI Project Template with pytest",{"path":2516,"title":2517},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fupdating-generated-projects-with-copier-update","Updating Generated CLI Projects with copier update",{"path":2519,"title":2520},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2522,"title":2523},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2525,"title":2526},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fcode-signing-and-notarizing-cli-binaries","Code Signing and Notarizing Python CLI Binaries",{"path":2528,"title":2529},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2531,"title":2532},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2534,"title":2535},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2537,"title":2538},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Freducing-pyinstaller-binary-size","Reducing the Size of a PyInstaller CLI Binary",{"path":2540,"title":2541},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fshipping-a-cli-as-a-zipapp-with-shiv","Shipping a CLI as a Zipapp with shiv",{"path":2543,"title":2544},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2546,"title":2547},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2549,"title":2550},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fenforcing-import-boundaries-in-a-cli-codebase","Enforcing Import Boundaries in a Python CLI Codebase",{"path":2552,"title":2553},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ffinding-unused-code-and-dependencies-with-vulture-and-deptry","Finding Unused Code and Dependencies with vulture and deptry",{"path":2555,"title":2556},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2558,"title":2559},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-pyright-in-strict-mode-on-a-cli","Running Pyright in Strict Mode on a Python CLI",{"path":2561,"title":2562},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-ruff-and-mypy-in-ci-with-annotations","Running Ruff and mypy in CI with Inline Annotations",{"path":2564,"title":2565},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ftype-checking-click-and-typer-code-with-mypy","Type-Checking Click and Typer Code with mypy",{"path":2567,"title":2568},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2570,"title":2571},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project","Bumping Versions Consistently Across a Python CLI Project",{"path":2573,"title":2574},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs","Deriving CLI Versions from Git Tags with hatch-vcs",{"path":2576,"title":2577},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2579,"title":2580},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2582,"title":2583},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2585,"title":2586},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fshipping-pre-releases-and-release-candidates","Shipping Pre-Releases and Release Candidates of a Python CLI",{"path":2588,"title":2589},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2591,"title":2592},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2594,"title":2595},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fchoosing-a-build-backend-for-a-python-cli","Choosing a Build Backend for a Python CLI",{"path":2597,"title":2598},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2600,"title":2601},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2603,"title":2604},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Foptional-dependencies-and-extras-for-clis","Optional Dependencies and Extras for Python CLIs",{"path":2606,"title":2607},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2609,"title":2610},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fwriting-pyproject-toml-metadata-for-a-cli","Writing pyproject.toml Metadata for a Python CLI",{"path":2612,"title":2613},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fdynamic-versioning-with-poetry-plugins","Dynamic Versioning for a Poetry CLI from Git Tags",{"path":2615,"title":2616},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2618,"title":2619},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmanaging-poetry-lock-files-for-cli-tools","Managing Poetry Lock Files for CLI Tools",{"path":2621,"title":2622},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmigrating-a-cli-from-poetry-to-uv","Migrating a Python CLI from Poetry to uv",{"path":2624,"title":2625},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2627,"title":2628},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2630,"title":2631},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpublishing-a-cli-with-poetry","Building and Publishing a Python CLI with Poetry",{"path":2633,"title":2634},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2636,"title":2637},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fkeeping-hook-versions-current-with-autoupdate","Keeping pre-commit Hook Versions Current with autoupdate",{"path":2639,"title":2640},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Frunning-pre-commit-in-ci","Running pre-commit in CI for a Python CLI Repository",{"path":2642,"title":2643},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fsetting-up-pre-commit-for-python-cli-repos","Setting up pre-commit for Python CLI repos",{"path":2645,"title":2646},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fshipping-your-cli-as-a-pre-commit-hook","Shipping Your Python CLI as a pre-commit Hook",{"path":2648,"title":2649},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fspeeding-up-slow-pre-commit-hooks","Speeding Up Slow pre-commit Hooks in a CLI Repository",{"path":2651,"title":2652},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python","Writing Local pre-commit Hooks in Python",{"path":2654,"title":2655},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fauditing-dependencies-with-pip-audit","Auditing a Python CLI’s Dependencies with pip-audit",{"path":2657,"title":2658},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fgenerating-an-sbom-for-a-python-cli","Generating an SBOM for a Python CLI Release",{"path":2660,"title":2661},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain","Securing the Supply Chain of a Python CLI",{"path":2663,"title":2664},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes","Pinning a Python CLI’s Dependencies with Hashes",{"path":2666,"title":2667},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases","Publishing Attestations and Verifying Python CLI Releases",{"path":2669,"title":2670},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fbuilding-and-publishing-a-cli-with-uv","Building and Publishing a Python CLI with uv",{"path":2672,"title":2673},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2675,"title":2676},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Flocking-and-syncing-cli-dependencies-with-uv","Locking and Syncing a Python CLI’s Dependencies with uv",{"path":2678,"title":2679},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Frunning-one-off-cli-scripts-with-uv-run","Running One-Off CLI Scripts with uv run and PEP 723",{"path":2681,"title":2682},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fusing-private-package-indexes-with-uv","Using Private Package Indexes with uv for Internal CLIs",{"path":2684,"title":2685},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-init-vs-poetry-init-for-cli-tools","uv init vs poetry init for CLI tools",{"path":2687,"title":2688},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-tool-install-vs-pipx-for-clis","uv tool install vs pipx for CLIs",{"path":2690,"title":2691},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2693,"title":2694},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fdebugging-wrong-python-and-wrong-venv-problems","Debugging Wrong-Python and Wrong-Venv Problems in CLIs",{"path":2696,"title":2697},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fexternally-managed-environments-and-pep-668","PEP 668 and Python CLIs: the externally-managed-environment Error",{"path":2699,"title":2700},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2702,"title":2703},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fisolating-cli-tools-from-project-dependencies","Isolating CLI Tools from Your Project’s Dependencies",{"path":2705,"title":2706},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2708,"title":2709},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2711,"title":2712},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1790967541367]