[{"data":1,"prerenderedAt":3415},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus\u002F":3,"content-directory":2867},{"id":4,"title":5,"body":6,"date":2853,"description":2854,"difficulty":2855,"draft":2856,"extension":2857,"meta":2858,"navigation":167,"path":2859,"seo":2860,"stem":2861,"tags":2862,"updated":2853,"__hash__":2866},"content\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus\u002Findex.md","Building Interactive Prompts and Menus in Python CLIs",{"type":7,"value":8,"toc":2836},"minimark",[9,33,38,59,63,67,98,102,105,126,1246,1928,1933,1943,1957,1970,1976,1986,2000,2030,2034,2037,2086,2090,2096,2714,2733,2737,2743,2747,2751,2763,2767,2770,2774,2790,2794,2797,2801,2832],[10,11,12,13,17,18,22,23,26,27,32],"p",{},"Some commands benefit from a conversation. Creating a new project, configuring credentials for the first time, choosing which environments to deploy to — tasks done rarely, with several decisions, by people who do not remember the flags. Typing a template name correctly from memory is harder than picking it from an arrow-key menu; ticking three features in a checkbox list is easier than recalling ",[14,15,16],"code",{},"--feature"," spelled three times. Typer and Click provide simple text prompts and confirmations; for menus, checkboxes and autocompletion, ",[19,20,21],"strong",{},"questionary"," (built on prompt_toolkit) is the usual choice. This guide builds a guided ",[14,24,25],{},"new"," command that asks only for what flags did not supply, validates as the user types, confirms a summary, prints the equivalent command for next time — and never prompts where nobody can answer. It belongs to the ",[28,29,31],"a",{"href":30},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002F","interactive terminal UI with Rich topic",".",[34,35,37],"h2",{"id":36},"prerequisites","Prerequisites",[39,40,41,52],"ul",{},[42,43,44,45,47,48,51],"li",{},"Python 3.10+, Typer and ",[14,46,21],{}," 2.x (",[14,49,50],{},"uv add questionary",").",[42,53,54,55,32],{},"A command whose inputs can all be given as flags — prompts are a layer on top, never a replacement, as argued in ",[28,56,58],{"href":57},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fchoosing-between-a-cli-a-prompt-flow-and-a-tui\u002F","choosing between a CLI, a prompt flow and a TUI",[34,60,62],{"id":61},"choosing-the-right-kind-of-question","Choosing the right kind of question",[64,65],"inline-diagram",{"name":66},"pm-widgets",[10,68,69,70,73,74,77,78,81,82,85,86,89,90,93,94,97],{},"Match the question to the answer. ",[19,71,72],{},"Text"," for free-form values such as names, always with validation. ",[19,75,76],{},"Confirm"," for a yes\u002Fno before doing something. ",[19,79,80],{},"Select"," — an arrow-key menu — for one choice from a short list, which removes typos entirely. ",[19,83,84],{},"Checkbox"," for several choices. ",[19,87,88],{},"Autocomplete"," for long lists where typing a few characters narrows the options. Typer's ",[14,91,92],{},"typer.prompt"," and ",[14,95,96],{},"typer.confirm"," cover the first two with no extra dependency; questionary adds the rest.",[34,99,101],{"id":100},"the-recipe-prompts-that-fill-gaps","The recipe: prompts that fill gaps",[64,103],{"name":104},"pm-flow",[10,106,107,108,111,112,115,116,115,119,93,122,125],{},"Keep the prompt logic separate from both the command and the prompt library. The flow is a function that receives an ",[14,109,110],{},"Asker"," — anything with ",[14,113,114],{},"text",", ",[14,117,118],{},"select",[14,120,121],{},"checkbox",[14,123,124],{},"confirm"," methods — so production code passes a questionary-backed implementation and tests pass a fake:",[127,128,133],"pre",{"className":129,"code":130,"language":131,"meta":132,"style":132},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fwizard.py\nfrom __future__ import annotations\n\nimport re\nimport shlex\nfrom dataclasses import dataclass\nfrom typing import Protocol\n\nTEMPLATES = {\"basic\": \"A minimal CLI\", \"service\": \"A CLI with an HTTP client\", \"data\": \"A data pipeline CLI\"}\nFEATURES = {\"docker\": \"Dockerfile\", \"ci\": \"GitHub Actions workflow\", \"docs\": \"MkDocs site\"}\nNAME_RE = re.compile(r\"^[a-z][a-z0-9-]{1,30}$\")\n\n\nclass Asker(Protocol):\n    def text(self, message: str, validate) -> str: ...\n    def select(self, message: str, choices: dict[str, str], default: str) -> str: ...\n    def checkbox(self, message: str, choices: dict[str, str]) -> list[str]: ...\n    def confirm(self, message: str) -> bool: ...\n\n\n@dataclass\nclass Answers:\n    name: str | None = None\n    template: str | None = None\n    features: list[str] | None = None\n\n    def command(self) -> str:\n        parts = [\"mytool\", \"new\", \"--name\", self.name or \"\", \"--template\", self.template or \"\"]\n        for f in self.features or []:\n            parts += [\"--feature\", f]\n        return shlex.join(parts)\n\n\ndef validate_name(value: str) -> bool | str:\n    return True if NAME_RE.fullmatch(value) else \"use 2-31 lowercase letters, digits and dashes\"\n\n\ndef fill_gaps(given: Answers, ask: Asker) -> Answers | None:\n    \"\"\"Ask only for what flags did not provide; None if the user declines at the end.\"\"\"\n    name = given.name or ask.text(\"Project name:\", validate=validate_name)\n    template = given.template or ask.select(\"Template:\", TEMPLATES, default=\"basic\")\n    features = given.features if given.features is not None else ask.checkbox(\"Extras:\", FEATURES)\n    answers = Answers(name, template, features)\n    summary = f\"Create {name!r} from the {template!r} template with {', '.join(features) or 'no extras'}?\"\n    return answers if ask.confirm(summary) else None\n\n\nclass QuestionaryAsker:\n    \"\"\"The real implementation. Imported lazily so non-interactive runs never load it.\"\"\"\n\n    def text(self, message, validate):\n        import questionary\n        return questionary.text(message, validate=validate).unsafe_ask()\n\n    def select(self, message, choices, default):\n        import questionary\n        opts = [questionary.Choice(f\"{k:\u003C8} {v}\", value=k) for k, v in choices.items()]\n        return questionary.select(message, choices=opts, default=next(o for o in opts if o.value == default)).unsafe_ask()\n\n    def checkbox(self, message, choices):\n        import questionary\n        return questionary.checkbox(message, choices=[questionary.Choice(v, value=k) for k, v in choices.items()]).unsafe_ask()\n\n    def confirm(self, message):\n        import questionary\n        return questionary.confirm(message, default=True).unsafe_ask()\n","python","",[14,134,135,144,162,169,178,186,199,212,217,262,302,333,338,343,362,387,422,452,473,478,483,489,500,519,535,555,560,575,633,656,673,682,687,692,717,741,746,751,768,774,804,838,877,888,947,964,969,974,984,990,995,1005,1014,1029,1034,1044,1051,1107,1154,1159,1169,1176,1206,1211,1221,1228],{"__ignoreMap":132},[136,137,140],"span",{"class":138,"line":139},"line",1,[136,141,143],{"class":142},"sJ8bj","# src\u002Fmytool\u002Fwizard.py\n",[136,145,147,151,155,158],{"class":138,"line":146},2,[136,148,150],{"class":149},"szBVR","from",[136,152,154],{"class":153},"sj4cs"," __future__",[136,156,157],{"class":149}," import",[136,159,161],{"class":160},"sVt8B"," annotations\n",[136,163,165],{"class":138,"line":164},3,[136,166,168],{"emptyLinePlaceholder":167},true,"\n",[136,170,172,175],{"class":138,"line":171},4,[136,173,174],{"class":149},"import",[136,176,177],{"class":160}," re\n",[136,179,181,183],{"class":138,"line":180},5,[136,182,174],{"class":149},[136,184,185],{"class":160}," shlex\n",[136,187,189,191,194,196],{"class":138,"line":188},6,[136,190,150],{"class":149},[136,192,193],{"class":160}," dataclasses ",[136,195,174],{"class":149},[136,197,198],{"class":160}," dataclass\n",[136,200,202,204,207,209],{"class":138,"line":201},7,[136,203,150],{"class":149},[136,205,206],{"class":160}," typing ",[136,208,174],{"class":149},[136,210,211],{"class":160}," Protocol\n",[136,213,215],{"class":138,"line":214},8,[136,216,168],{"emptyLinePlaceholder":167},[136,218,220,223,226,229,233,236,239,241,244,246,249,251,254,256,259],{"class":138,"line":219},9,[136,221,222],{"class":153},"TEMPLATES",[136,224,225],{"class":149}," =",[136,227,228],{"class":160}," {",[136,230,232],{"class":231},"sZZnC","\"basic\"",[136,234,235],{"class":160},": ",[136,237,238],{"class":231},"\"A minimal CLI\"",[136,240,115],{"class":160},[136,242,243],{"class":231},"\"service\"",[136,245,235],{"class":160},[136,247,248],{"class":231},"\"A CLI with an HTTP client\"",[136,250,115],{"class":160},[136,252,253],{"class":231},"\"data\"",[136,255,235],{"class":160},[136,257,258],{"class":231},"\"A data pipeline CLI\"",[136,260,261],{"class":160},"}\n",[136,263,265,268,270,272,275,277,280,282,285,287,290,292,295,297,300],{"class":138,"line":264},10,[136,266,267],{"class":153},"FEATURES",[136,269,225],{"class":149},[136,271,228],{"class":160},[136,273,274],{"class":231},"\"docker\"",[136,276,235],{"class":160},[136,278,279],{"class":231},"\"Dockerfile\"",[136,281,115],{"class":160},[136,283,284],{"class":231},"\"ci\"",[136,286,235],{"class":160},[136,288,289],{"class":231},"\"GitHub Actions workflow\"",[136,291,115],{"class":160},[136,293,294],{"class":231},"\"docs\"",[136,296,235],{"class":160},[136,298,299],{"class":231},"\"MkDocs site\"",[136,301,261],{"class":160},[136,303,305,308,310,313,316,319,322,325,328,330],{"class":138,"line":304},11,[136,306,307],{"class":153},"NAME_RE",[136,309,225],{"class":149},[136,311,312],{"class":160}," re.compile(",[136,314,315],{"class":149},"r",[136,317,318],{"class":231},"\"",[136,320,321],{"class":153},"^[a-z][a-z0-9-]",[136,323,324],{"class":149},"{1,30}",[136,326,327],{"class":153},"$",[136,329,318],{"class":231},[136,331,332],{"class":160},")\n",[136,334,336],{"class":138,"line":335},12,[136,337,168],{"emptyLinePlaceholder":167},[136,339,341],{"class":138,"line":340},13,[136,342,168],{"emptyLinePlaceholder":167},[136,344,346,349,353,356,359],{"class":138,"line":345},14,[136,347,348],{"class":149},"class",[136,350,352],{"class":351},"sScJk"," Asker",[136,354,355],{"class":160},"(",[136,357,358],{"class":351},"Protocol",[136,360,361],{"class":160},"):\n",[136,363,365,368,371,374,377,380,382,384],{"class":138,"line":364},15,[136,366,367],{"class":149},"    def",[136,369,370],{"class":351}," text",[136,372,373],{"class":160},"(self, message: ",[136,375,376],{"class":153},"str",[136,378,379],{"class":160},", validate) -> ",[136,381,376],{"class":153},[136,383,235],{"class":160},[136,385,386],{"class":153},"...\n",[136,388,390,392,395,397,399,402,404,406,408,411,413,416,418,420],{"class":138,"line":389},16,[136,391,367],{"class":149},[136,393,394],{"class":351}," select",[136,396,373],{"class":160},[136,398,376],{"class":153},[136,400,401],{"class":160},", choices: dict[",[136,403,376],{"class":153},[136,405,115],{"class":160},[136,407,376],{"class":153},[136,409,410],{"class":160},"], default: ",[136,412,376],{"class":153},[136,414,415],{"class":160},") -> ",[136,417,376],{"class":153},[136,419,235],{"class":160},[136,421,386],{"class":153},[136,423,425,427,430,432,434,436,438,440,442,445,447,450],{"class":138,"line":424},17,[136,426,367],{"class":149},[136,428,429],{"class":351}," checkbox",[136,431,373],{"class":160},[136,433,376],{"class":153},[136,435,401],{"class":160},[136,437,376],{"class":153},[136,439,115],{"class":160},[136,441,376],{"class":153},[136,443,444],{"class":160},"]) -> list[",[136,446,376],{"class":153},[136,448,449],{"class":160},"]: ",[136,451,386],{"class":153},[136,453,455,457,460,462,464,466,469,471],{"class":138,"line":454},18,[136,456,367],{"class":149},[136,458,459],{"class":351}," confirm",[136,461,373],{"class":160},[136,463,376],{"class":153},[136,465,415],{"class":160},[136,467,468],{"class":153},"bool",[136,470,235],{"class":160},[136,472,386],{"class":153},[136,474,476],{"class":138,"line":475},19,[136,477,168],{"emptyLinePlaceholder":167},[136,479,481],{"class":138,"line":480},20,[136,482,168],{"emptyLinePlaceholder":167},[136,484,486],{"class":138,"line":485},21,[136,487,488],{"class":351},"@dataclass\n",[136,490,492,494,497],{"class":138,"line":491},22,[136,493,348],{"class":149},[136,495,496],{"class":351}," Answers",[136,498,499],{"class":160},":\n",[136,501,503,506,508,511,514,516],{"class":138,"line":502},23,[136,504,505],{"class":160},"    name: ",[136,507,376],{"class":153},[136,509,510],{"class":149}," |",[136,512,513],{"class":153}," None",[136,515,225],{"class":149},[136,517,518],{"class":153}," None\n",[136,520,522,525,527,529,531,533],{"class":138,"line":521},24,[136,523,524],{"class":160},"    template: ",[136,526,376],{"class":153},[136,528,510],{"class":149},[136,530,513],{"class":153},[136,532,225],{"class":149},[136,534,518],{"class":153},[136,536,538,541,543,546,549,551,553],{"class":138,"line":537},25,[136,539,540],{"class":160},"    features: list[",[136,542,376],{"class":153},[136,544,545],{"class":160},"] ",[136,547,548],{"class":149},"|",[136,550,513],{"class":153},[136,552,225],{"class":149},[136,554,518],{"class":153},[136,556,558],{"class":138,"line":557},26,[136,559,168],{"emptyLinePlaceholder":167},[136,561,563,565,568,571,573],{"class":138,"line":562},27,[136,564,367],{"class":149},[136,566,567],{"class":351}," command",[136,569,570],{"class":160},"(self) -> ",[136,572,376],{"class":153},[136,574,499],{"class":160},[136,576,578,581,584,587,590,592,595,597,600,602,605,608,611,614,616,619,621,623,626,628,630],{"class":138,"line":577},28,[136,579,580],{"class":160},"        parts ",[136,582,583],{"class":149},"=",[136,585,586],{"class":160}," [",[136,588,589],{"class":231},"\"mytool\"",[136,591,115],{"class":160},[136,593,594],{"class":231},"\"new\"",[136,596,115],{"class":160},[136,598,599],{"class":231},"\"--name\"",[136,601,115],{"class":160},[136,603,604],{"class":153},"self",[136,606,607],{"class":160},".name ",[136,609,610],{"class":149},"or",[136,612,613],{"class":231}," \"\"",[136,615,115],{"class":160},[136,617,618],{"class":231},"\"--template\"",[136,620,115],{"class":160},[136,622,604],{"class":153},[136,624,625],{"class":160},".template ",[136,627,610],{"class":149},[136,629,613],{"class":231},[136,631,632],{"class":160},"]\n",[136,634,636,639,642,645,648,651,653],{"class":138,"line":635},29,[136,637,638],{"class":149},"        for",[136,640,641],{"class":160}," f ",[136,643,644],{"class":149},"in",[136,646,647],{"class":153}," self",[136,649,650],{"class":160},".features ",[136,652,610],{"class":149},[136,654,655],{"class":160}," []:\n",[136,657,659,662,665,667,670],{"class":138,"line":658},30,[136,660,661],{"class":160},"            parts ",[136,663,664],{"class":149},"+=",[136,666,586],{"class":160},[136,668,669],{"class":231},"\"--feature\"",[136,671,672],{"class":160},", f]\n",[136,674,676,679],{"class":138,"line":675},31,[136,677,678],{"class":149},"        return",[136,680,681],{"class":160}," shlex.join(parts)\n",[136,683,685],{"class":138,"line":684},32,[136,686,168],{"emptyLinePlaceholder":167},[136,688,690],{"class":138,"line":689},33,[136,691,168],{"emptyLinePlaceholder":167},[136,693,695,698,701,704,706,708,710,712,715],{"class":138,"line":694},34,[136,696,697],{"class":149},"def",[136,699,700],{"class":351}," validate_name",[136,702,703],{"class":160},"(value: ",[136,705,376],{"class":153},[136,707,415],{"class":160},[136,709,468],{"class":153},[136,711,510],{"class":149},[136,713,714],{"class":153}," str",[136,716,499],{"class":160},[136,718,720,723,726,729,732,735,738],{"class":138,"line":719},35,[136,721,722],{"class":149},"    return",[136,724,725],{"class":153}," True",[136,727,728],{"class":149}," if",[136,730,731],{"class":153}," NAME_RE",[136,733,734],{"class":160},".fullmatch(value) ",[136,736,737],{"class":149},"else",[136,739,740],{"class":231}," \"use 2-31 lowercase letters, digits and dashes\"\n",[136,742,744],{"class":138,"line":743},36,[136,745,168],{"emptyLinePlaceholder":167},[136,747,749],{"class":138,"line":748},37,[136,750,168],{"emptyLinePlaceholder":167},[136,752,754,756,759,762,764,766],{"class":138,"line":753},38,[136,755,697],{"class":149},[136,757,758],{"class":351}," fill_gaps",[136,760,761],{"class":160},"(given: Answers, ask: Asker) -> Answers ",[136,763,548],{"class":149},[136,765,513],{"class":153},[136,767,499],{"class":160},[136,769,771],{"class":138,"line":770},39,[136,772,773],{"class":231},"    \"\"\"Ask only for what flags did not provide; None if the user declines at the end.\"\"\"\n",[136,775,777,780,782,785,787,790,793,795,799,801],{"class":138,"line":776},40,[136,778,779],{"class":160},"    name ",[136,781,583],{"class":149},[136,783,784],{"class":160}," given.name ",[136,786,610],{"class":149},[136,788,789],{"class":160}," ask.text(",[136,791,792],{"class":231},"\"Project name:\"",[136,794,115],{"class":160},[136,796,798],{"class":797},"s4XuR","validate",[136,800,583],{"class":149},[136,802,803],{"class":160},"validate_name)\n",[136,805,807,810,812,815,817,820,823,825,827,829,832,834,836],{"class":138,"line":806},41,[136,808,809],{"class":160},"    template ",[136,811,583],{"class":149},[136,813,814],{"class":160}," given.template ",[136,816,610],{"class":149},[136,818,819],{"class":160}," ask.select(",[136,821,822],{"class":231},"\"Template:\"",[136,824,115],{"class":160},[136,826,222],{"class":153},[136,828,115],{"class":160},[136,830,831],{"class":797},"default",[136,833,583],{"class":149},[136,835,232],{"class":231},[136,837,332],{"class":160},[136,839,841,844,846,849,852,854,857,860,862,865,868,871,873,875],{"class":138,"line":840},42,[136,842,843],{"class":160},"    features ",[136,845,583],{"class":149},[136,847,848],{"class":160}," given.features ",[136,850,851],{"class":149},"if",[136,853,848],{"class":160},[136,855,856],{"class":149},"is",[136,858,859],{"class":149}," not",[136,861,513],{"class":153},[136,863,864],{"class":149}," else",[136,866,867],{"class":160}," ask.checkbox(",[136,869,870],{"class":231},"\"Extras:\"",[136,872,115],{"class":160},[136,874,267],{"class":153},[136,876,332],{"class":160},[136,878,880,883,885],{"class":138,"line":879},43,[136,881,882],{"class":160},"    answers ",[136,884,583],{"class":149},[136,886,887],{"class":160}," Answers(name, template, features)\n",[136,889,891,894,896,899,902,905,908,911,914,917,919,922,924,926,929,931,934,937,939,942,944],{"class":138,"line":890},44,[136,892,893],{"class":160},"    summary ",[136,895,583],{"class":149},[136,897,898],{"class":149}," f",[136,900,901],{"class":231},"\"Create ",[136,903,904],{"class":153},"{",[136,906,907],{"class":160},"name",[136,909,910],{"class":149},"!r",[136,912,913],{"class":153},"}",[136,915,916],{"class":231}," from the ",[136,918,904],{"class":153},[136,920,921],{"class":160},"template",[136,923,910],{"class":149},[136,925,913],{"class":153},[136,927,928],{"class":231}," template with ",[136,930,904],{"class":153},[136,932,933],{"class":231},"', '",[136,935,936],{"class":160},".join(features) ",[136,938,610],{"class":149},[136,940,941],{"class":231}," 'no extras'",[136,943,913],{"class":153},[136,945,946],{"class":231},"?\"\n",[136,948,950,952,955,957,960,962],{"class":138,"line":949},45,[136,951,722],{"class":149},[136,953,954],{"class":160}," answers ",[136,956,851],{"class":149},[136,958,959],{"class":160}," ask.confirm(summary) ",[136,961,737],{"class":149},[136,963,518],{"class":153},[136,965,967],{"class":138,"line":966},46,[136,968,168],{"emptyLinePlaceholder":167},[136,970,972],{"class":138,"line":971},47,[136,973,168],{"emptyLinePlaceholder":167},[136,975,977,979,982],{"class":138,"line":976},48,[136,978,348],{"class":149},[136,980,981],{"class":351}," QuestionaryAsker",[136,983,499],{"class":160},[136,985,987],{"class":138,"line":986},49,[136,988,989],{"class":231},"    \"\"\"The real implementation. Imported lazily so non-interactive runs never load it.\"\"\"\n",[136,991,993],{"class":138,"line":992},50,[136,994,168],{"emptyLinePlaceholder":167},[136,996,998,1000,1002],{"class":138,"line":997},51,[136,999,367],{"class":149},[136,1001,370],{"class":351},[136,1003,1004],{"class":160},"(self, message, validate):\n",[136,1006,1008,1011],{"class":138,"line":1007},52,[136,1009,1010],{"class":149},"        import",[136,1012,1013],{"class":160}," questionary\n",[136,1015,1017,1019,1022,1024,1026],{"class":138,"line":1016},53,[136,1018,678],{"class":149},[136,1020,1021],{"class":160}," questionary.text(message, ",[136,1023,798],{"class":797},[136,1025,583],{"class":149},[136,1027,1028],{"class":160},"validate).unsafe_ask()\n",[136,1030,1032],{"class":138,"line":1031},54,[136,1033,168],{"emptyLinePlaceholder":167},[136,1035,1037,1039,1041],{"class":138,"line":1036},55,[136,1038,367],{"class":149},[136,1040,394],{"class":351},[136,1042,1043],{"class":160},"(self, message, choices, default):\n",[136,1045,1047,1049],{"class":138,"line":1046},56,[136,1048,1010],{"class":149},[136,1050,1013],{"class":160},[136,1052,1054,1057,1059,1062,1065,1067,1069,1072,1075,1077,1079,1082,1084,1086,1088,1091,1093,1096,1099,1102,1104],{"class":138,"line":1053},57,[136,1055,1056],{"class":160},"        opts ",[136,1058,583],{"class":149},[136,1060,1061],{"class":160}," [questionary.Choice(",[136,1063,1064],{"class":149},"f",[136,1066,318],{"class":231},[136,1068,904],{"class":153},[136,1070,1071],{"class":160},"k",[136,1073,1074],{"class":149},":\u003C8",[136,1076,913],{"class":153},[136,1078,228],{"class":153},[136,1080,1081],{"class":160},"v",[136,1083,913],{"class":153},[136,1085,318],{"class":231},[136,1087,115],{"class":160},[136,1089,1090],{"class":797},"value",[136,1092,583],{"class":149},[136,1094,1095],{"class":160},"k) ",[136,1097,1098],{"class":149},"for",[136,1100,1101],{"class":160}," k, v ",[136,1103,644],{"class":149},[136,1105,1106],{"class":160}," choices.items()]\n",[136,1108,1110,1112,1115,1118,1120,1123,1125,1127,1130,1133,1135,1138,1140,1143,1145,1148,1151],{"class":138,"line":1109},58,[136,1111,678],{"class":149},[136,1113,1114],{"class":160}," questionary.select(message, ",[136,1116,1117],{"class":797},"choices",[136,1119,583],{"class":149},[136,1121,1122],{"class":160},"opts, ",[136,1124,831],{"class":797},[136,1126,583],{"class":149},[136,1128,1129],{"class":153},"next",[136,1131,1132],{"class":160},"(o ",[136,1134,1098],{"class":149},[136,1136,1137],{"class":160}," o ",[136,1139,644],{"class":149},[136,1141,1142],{"class":160}," opts ",[136,1144,851],{"class":149},[136,1146,1147],{"class":160}," o.value ",[136,1149,1150],{"class":149},"==",[136,1152,1153],{"class":160}," default)).unsafe_ask()\n",[136,1155,1157],{"class":138,"line":1156},59,[136,1158,168],{"emptyLinePlaceholder":167},[136,1160,1162,1164,1166],{"class":138,"line":1161},60,[136,1163,367],{"class":149},[136,1165,429],{"class":351},[136,1167,1168],{"class":160},"(self, message, choices):\n",[136,1170,1172,1174],{"class":138,"line":1171},61,[136,1173,1010],{"class":149},[136,1175,1013],{"class":160},[136,1177,1179,1181,1184,1186,1188,1191,1193,1195,1197,1199,1201,1203],{"class":138,"line":1178},62,[136,1180,678],{"class":149},[136,1182,1183],{"class":160}," questionary.checkbox(message, ",[136,1185,1117],{"class":797},[136,1187,583],{"class":149},[136,1189,1190],{"class":160},"[questionary.Choice(v, ",[136,1192,1090],{"class":797},[136,1194,583],{"class":149},[136,1196,1095],{"class":160},[136,1198,1098],{"class":149},[136,1200,1101],{"class":160},[136,1202,644],{"class":149},[136,1204,1205],{"class":160}," choices.items()]).unsafe_ask()\n",[136,1207,1209],{"class":138,"line":1208},63,[136,1210,168],{"emptyLinePlaceholder":167},[136,1212,1214,1216,1218],{"class":138,"line":1213},64,[136,1215,367],{"class":149},[136,1217,459],{"class":351},[136,1219,1220],{"class":160},"(self, message):\n",[136,1222,1224,1226],{"class":138,"line":1223},65,[136,1225,1010],{"class":149},[136,1227,1013],{"class":160},[136,1229,1231,1233,1236,1238,1240,1243],{"class":138,"line":1230},66,[136,1232,678],{"class":149},[136,1234,1235],{"class":160}," questionary.confirm(message, ",[136,1237,831],{"class":797},[136,1239,583],{"class":149},[136,1241,1242],{"class":153},"True",[136,1244,1245],{"class":160},").unsafe_ask()\n",[127,1247,1249],{"className":129,"code":1248,"language":131,"meta":132,"style":132},"# src\u002Fmytool\u002Fcli.py\nfrom __future__ import annotations\n\nimport sys\nfrom typing import Annotated\n\nimport typer\n\nfrom mytool.wizard import FEATURES, TEMPLATES, Answers, QuestionaryAsker, fill_gaps\n\napp = typer.Typer()\n\n\ndef interactive() -> bool:\n    return sys.stdin.isatty() and sys.stdout.isatty()\n\n\n@app.callback()\ndef main() -> None:\n    \"\"\"Project scaffolding.\"\"\"\n\n\n@app.command()\ndef new(\n    name: Annotated[str | None, typer.Option(help=\"Project name.\")] = None,\n    template: Annotated[str | None, typer.Option(help=f\"One of: {', '.join(TEMPLATES)}.\")] = None,\n    feature: Annotated[list[str] | None, typer.Option(help=f\"Extras: {', '.join(FEATURES)} (repeatable).\")] = None,\n    no_input: Annotated[bool, typer.Option(\"--no-input\", help=\"Never prompt.\")] = False,\n) -> None:\n    \"\"\"Create a project; prompts for anything not given as a flag.\"\"\"\n    given = Answers(name, template, feature)\n    complete = name is not None and template is not None\n    if not complete and (no_input or not interactive()):\n        typer.echo(\"error: --name and --template are required when not running interactively\", err=True)\n        raise typer.Exit(2)\n    if complete:\n        answers = Answers(name, template, feature or [])\n    else:\n        try:\n            answers = fill_gaps(given, QuestionaryAsker())\n        except KeyboardInterrupt:\n            typer.echo(\"\\ncancelled\", err=True)\n            raise typer.Exit(130)\n        if answers is None:\n            typer.echo(\"nothing created\", err=True)\n            raise typer.Exit(1)\n        typer.echo(f\"next time: {answers.command()}\", err=True)\n    typer.echo(f\"created {answers.name} ({answers.template}; {', '.join(answers.features) or 'no extras'})\")\n\n\nif __name__ == \"__main__\":\n    app()\n",[14,1250,1251,1256,1266,1270,1277,1288,1292,1299,1303,1322,1326,1336,1340,1344,1358,1371,1375,1379,1387,1401,1406,1410,1414,1421,1431,1463,1510,1557,1587,1595,1600,1610,1638,1660,1679,1692,1699,1714,1721,1728,1738,1748,1771,1783,1796,1813,1824,1852,1900,1904,1908,1923],{"__ignoreMap":132},[136,1252,1253],{"class":138,"line":139},[136,1254,1255],{"class":142},"# src\u002Fmytool\u002Fcli.py\n",[136,1257,1258,1260,1262,1264],{"class":138,"line":146},[136,1259,150],{"class":149},[136,1261,154],{"class":153},[136,1263,157],{"class":149},[136,1265,161],{"class":160},[136,1267,1268],{"class":138,"line":164},[136,1269,168],{"emptyLinePlaceholder":167},[136,1271,1272,1274],{"class":138,"line":171},[136,1273,174],{"class":149},[136,1275,1276],{"class":160}," sys\n",[136,1278,1279,1281,1283,1285],{"class":138,"line":180},[136,1280,150],{"class":149},[136,1282,206],{"class":160},[136,1284,174],{"class":149},[136,1286,1287],{"class":160}," Annotated\n",[136,1289,1290],{"class":138,"line":188},[136,1291,168],{"emptyLinePlaceholder":167},[136,1293,1294,1296],{"class":138,"line":201},[136,1295,174],{"class":149},[136,1297,1298],{"class":160}," typer\n",[136,1300,1301],{"class":138,"line":214},[136,1302,168],{"emptyLinePlaceholder":167},[136,1304,1305,1307,1310,1312,1315,1317,1319],{"class":138,"line":219},[136,1306,150],{"class":149},[136,1308,1309],{"class":160}," mytool.wizard ",[136,1311,174],{"class":149},[136,1313,1314],{"class":153}," FEATURES",[136,1316,115],{"class":160},[136,1318,222],{"class":153},[136,1320,1321],{"class":160},", Answers, QuestionaryAsker, fill_gaps\n",[136,1323,1324],{"class":138,"line":264},[136,1325,168],{"emptyLinePlaceholder":167},[136,1327,1328,1331,1333],{"class":138,"line":304},[136,1329,1330],{"class":160},"app ",[136,1332,583],{"class":149},[136,1334,1335],{"class":160}," typer.Typer()\n",[136,1337,1338],{"class":138,"line":335},[136,1339,168],{"emptyLinePlaceholder":167},[136,1341,1342],{"class":138,"line":340},[136,1343,168],{"emptyLinePlaceholder":167},[136,1345,1346,1348,1351,1354,1356],{"class":138,"line":345},[136,1347,697],{"class":149},[136,1349,1350],{"class":351}," interactive",[136,1352,1353],{"class":160},"() -> ",[136,1355,468],{"class":153},[136,1357,499],{"class":160},[136,1359,1360,1362,1365,1368],{"class":138,"line":364},[136,1361,722],{"class":149},[136,1363,1364],{"class":160}," sys.stdin.isatty() ",[136,1366,1367],{"class":149},"and",[136,1369,1370],{"class":160}," sys.stdout.isatty()\n",[136,1372,1373],{"class":138,"line":389},[136,1374,168],{"emptyLinePlaceholder":167},[136,1376,1377],{"class":138,"line":424},[136,1378,168],{"emptyLinePlaceholder":167},[136,1380,1381,1384],{"class":138,"line":454},[136,1382,1383],{"class":351},"@app.callback",[136,1385,1386],{"class":160},"()\n",[136,1388,1389,1391,1394,1396,1399],{"class":138,"line":475},[136,1390,697],{"class":149},[136,1392,1393],{"class":351}," main",[136,1395,1353],{"class":160},[136,1397,1398],{"class":153},"None",[136,1400,499],{"class":160},[136,1402,1403],{"class":138,"line":480},[136,1404,1405],{"class":231},"    \"\"\"Project scaffolding.\"\"\"\n",[136,1407,1408],{"class":138,"line":485},[136,1409,168],{"emptyLinePlaceholder":167},[136,1411,1412],{"class":138,"line":491},[136,1413,168],{"emptyLinePlaceholder":167},[136,1415,1416,1419],{"class":138,"line":502},[136,1417,1418],{"class":351},"@app.command",[136,1420,1386],{"class":160},[136,1422,1423,1425,1428],{"class":138,"line":521},[136,1424,697],{"class":149},[136,1426,1427],{"class":351}," new",[136,1429,1430],{"class":160},"(\n",[136,1432,1433,1436,1438,1440,1442,1445,1448,1450,1453,1456,1458,1460],{"class":138,"line":537},[136,1434,1435],{"class":160},"    name: Annotated[",[136,1437,376],{"class":153},[136,1439,510],{"class":149},[136,1441,513],{"class":153},[136,1443,1444],{"class":160},", typer.Option(",[136,1446,1447],{"class":797},"help",[136,1449,583],{"class":149},[136,1451,1452],{"class":231},"\"Project name.\"",[136,1454,1455],{"class":160},")] ",[136,1457,583],{"class":149},[136,1459,513],{"class":153},[136,1461,1462],{"class":160},",\n",[136,1464,1465,1468,1470,1472,1474,1476,1478,1480,1482,1485,1487,1489,1492,1494,1497,1499,1502,1504,1506,1508],{"class":138,"line":557},[136,1466,1467],{"class":160},"    template: Annotated[",[136,1469,376],{"class":153},[136,1471,510],{"class":149},[136,1473,513],{"class":153},[136,1475,1444],{"class":160},[136,1477,1447],{"class":797},[136,1479,583],{"class":149},[136,1481,1064],{"class":149},[136,1483,1484],{"class":231},"\"One of: ",[136,1486,904],{"class":153},[136,1488,933],{"class":231},[136,1490,1491],{"class":160},".join(",[136,1493,222],{"class":153},[136,1495,1496],{"class":160},")",[136,1498,913],{"class":153},[136,1500,1501],{"class":231},".\"",[136,1503,1455],{"class":160},[136,1505,583],{"class":149},[136,1507,513],{"class":153},[136,1509,1462],{"class":160},[136,1511,1512,1515,1517,1519,1521,1523,1525,1527,1529,1531,1534,1536,1538,1540,1542,1544,1546,1549,1551,1553,1555],{"class":138,"line":562},[136,1513,1514],{"class":160},"    feature: Annotated[list[",[136,1516,376],{"class":153},[136,1518,545],{"class":160},[136,1520,548],{"class":149},[136,1522,513],{"class":153},[136,1524,1444],{"class":160},[136,1526,1447],{"class":797},[136,1528,583],{"class":149},[136,1530,1064],{"class":149},[136,1532,1533],{"class":231},"\"Extras: ",[136,1535,904],{"class":153},[136,1537,933],{"class":231},[136,1539,1491],{"class":160},[136,1541,267],{"class":153},[136,1543,1496],{"class":160},[136,1545,913],{"class":153},[136,1547,1548],{"class":231}," (repeatable).\"",[136,1550,1455],{"class":160},[136,1552,583],{"class":149},[136,1554,513],{"class":153},[136,1556,1462],{"class":160},[136,1558,1559,1562,1564,1566,1569,1571,1573,1575,1578,1580,1582,1585],{"class":138,"line":577},[136,1560,1561],{"class":160},"    no_input: Annotated[",[136,1563,468],{"class":153},[136,1565,1444],{"class":160},[136,1567,1568],{"class":231},"\"--no-input\"",[136,1570,115],{"class":160},[136,1572,1447],{"class":797},[136,1574,583],{"class":149},[136,1576,1577],{"class":231},"\"Never prompt.\"",[136,1579,1455],{"class":160},[136,1581,583],{"class":149},[136,1583,1584],{"class":153}," False",[136,1586,1462],{"class":160},[136,1588,1589,1591,1593],{"class":138,"line":635},[136,1590,415],{"class":160},[136,1592,1398],{"class":153},[136,1594,499],{"class":160},[136,1596,1597],{"class":138,"line":658},[136,1598,1599],{"class":231},"    \"\"\"Create a project; prompts for anything not given as a flag.\"\"\"\n",[136,1601,1602,1605,1607],{"class":138,"line":675},[136,1603,1604],{"class":160},"    given ",[136,1606,583],{"class":149},[136,1608,1609],{"class":160}," Answers(name, template, feature)\n",[136,1611,1612,1615,1617,1620,1622,1624,1626,1629,1632,1634,1636],{"class":138,"line":684},[136,1613,1614],{"class":160},"    complete ",[136,1616,583],{"class":149},[136,1618,1619],{"class":160}," name ",[136,1621,856],{"class":149},[136,1623,859],{"class":149},[136,1625,513],{"class":153},[136,1627,1628],{"class":149}," and",[136,1630,1631],{"class":160}," template ",[136,1633,856],{"class":149},[136,1635,859],{"class":149},[136,1637,518],{"class":153},[136,1639,1640,1643,1645,1648,1650,1653,1655,1657],{"class":138,"line":689},[136,1641,1642],{"class":149},"    if",[136,1644,859],{"class":149},[136,1646,1647],{"class":160}," complete ",[136,1649,1367],{"class":149},[136,1651,1652],{"class":160}," (no_input ",[136,1654,610],{"class":149},[136,1656,859],{"class":149},[136,1658,1659],{"class":160}," interactive()):\n",[136,1661,1662,1665,1668,1670,1673,1675,1677],{"class":138,"line":694},[136,1663,1664],{"class":160},"        typer.echo(",[136,1666,1667],{"class":231},"\"error: --name and --template are required when not running interactively\"",[136,1669,115],{"class":160},[136,1671,1672],{"class":797},"err",[136,1674,583],{"class":149},[136,1676,1242],{"class":153},[136,1678,332],{"class":160},[136,1680,1681,1684,1687,1690],{"class":138,"line":719},[136,1682,1683],{"class":149},"        raise",[136,1685,1686],{"class":160}," typer.Exit(",[136,1688,1689],{"class":153},"2",[136,1691,332],{"class":160},[136,1693,1694,1696],{"class":138,"line":743},[136,1695,1642],{"class":149},[136,1697,1698],{"class":160}," complete:\n",[136,1700,1701,1704,1706,1709,1711],{"class":138,"line":748},[136,1702,1703],{"class":160},"        answers ",[136,1705,583],{"class":149},[136,1707,1708],{"class":160}," Answers(name, template, feature ",[136,1710,610],{"class":149},[136,1712,1713],{"class":160}," [])\n",[136,1715,1716,1719],{"class":138,"line":753},[136,1717,1718],{"class":149},"    else",[136,1720,499],{"class":160},[136,1722,1723,1726],{"class":138,"line":770},[136,1724,1725],{"class":149},"        try",[136,1727,499],{"class":160},[136,1729,1730,1733,1735],{"class":138,"line":776},[136,1731,1732],{"class":160},"            answers ",[136,1734,583],{"class":149},[136,1736,1737],{"class":160}," fill_gaps(given, QuestionaryAsker())\n",[136,1739,1740,1743,1746],{"class":138,"line":806},[136,1741,1742],{"class":149},"        except",[136,1744,1745],{"class":153}," KeyboardInterrupt",[136,1747,499],{"class":160},[136,1749,1750,1753,1755,1758,1761,1763,1765,1767,1769],{"class":138,"line":840},[136,1751,1752],{"class":160},"            typer.echo(",[136,1754,318],{"class":231},[136,1756,1757],{"class":153},"\\n",[136,1759,1760],{"class":231},"cancelled\"",[136,1762,115],{"class":160},[136,1764,1672],{"class":797},[136,1766,583],{"class":149},[136,1768,1242],{"class":153},[136,1770,332],{"class":160},[136,1772,1773,1776,1778,1781],{"class":138,"line":879},[136,1774,1775],{"class":149},"            raise",[136,1777,1686],{"class":160},[136,1779,1780],{"class":153},"130",[136,1782,332],{"class":160},[136,1784,1785,1788,1790,1792,1794],{"class":138,"line":890},[136,1786,1787],{"class":149},"        if",[136,1789,954],{"class":160},[136,1791,856],{"class":149},[136,1793,513],{"class":153},[136,1795,499],{"class":160},[136,1797,1798,1800,1803,1805,1807,1809,1811],{"class":138,"line":949},[136,1799,1752],{"class":160},[136,1801,1802],{"class":231},"\"nothing created\"",[136,1804,115],{"class":160},[136,1806,1672],{"class":797},[136,1808,583],{"class":149},[136,1810,1242],{"class":153},[136,1812,332],{"class":160},[136,1814,1815,1817,1819,1822],{"class":138,"line":966},[136,1816,1775],{"class":149},[136,1818,1686],{"class":160},[136,1820,1821],{"class":153},"1",[136,1823,332],{"class":160},[136,1825,1826,1828,1830,1833,1835,1838,1840,1842,1844,1846,1848,1850],{"class":138,"line":971},[136,1827,1664],{"class":160},[136,1829,1064],{"class":149},[136,1831,1832],{"class":231},"\"next time: ",[136,1834,904],{"class":153},[136,1836,1837],{"class":160},"answers.command()",[136,1839,913],{"class":153},[136,1841,318],{"class":231},[136,1843,115],{"class":160},[136,1845,1672],{"class":797},[136,1847,583],{"class":149},[136,1849,1242],{"class":153},[136,1851,332],{"class":160},[136,1853,1854,1857,1859,1862,1864,1867,1869,1872,1874,1877,1879,1882,1884,1886,1889,1891,1893,1895,1898],{"class":138,"line":976},[136,1855,1856],{"class":160},"    typer.echo(",[136,1858,1064],{"class":149},[136,1860,1861],{"class":231},"\"created ",[136,1863,904],{"class":153},[136,1865,1866],{"class":160},"answers.name",[136,1868,913],{"class":153},[136,1870,1871],{"class":231}," (",[136,1873,904],{"class":153},[136,1875,1876],{"class":160},"answers.template",[136,1878,913],{"class":153},[136,1880,1881],{"class":231},"; ",[136,1883,904],{"class":153},[136,1885,933],{"class":231},[136,1887,1888],{"class":160},".join(answers.features) ",[136,1890,610],{"class":149},[136,1892,941],{"class":231},[136,1894,913],{"class":153},[136,1896,1897],{"class":231},")\"",[136,1899,332],{"class":160},[136,1901,1902],{"class":138,"line":986},[136,1903,168],{"emptyLinePlaceholder":167},[136,1905,1906],{"class":138,"line":992},[136,1907,168],{"emptyLinePlaceholder":167},[136,1909,1910,1912,1915,1918,1921],{"class":138,"line":997},[136,1911,851],{"class":149},[136,1913,1914],{"class":153}," __name__",[136,1916,1917],{"class":149}," ==",[136,1919,1920],{"class":231}," \"__main__\"",[136,1922,499],{"class":160},[136,1924,1925],{"class":138,"line":1007},[136,1926,1927],{"class":160},"    app()\n",[1929,1930,1932],"h3",{"id":1931},"what-the-design-gives-you","What the design gives you",[10,1934,1935,1938,1939,1942],{},[19,1936,1937],{},"Flag parity."," Every question corresponds to a flag. ",[14,1940,1941],{},"mytool new --name billing --template service --feature ci"," runs without a single prompt, so documentation, scripts and CI use the same command a newcomer uses interactively.",[10,1944,1945,1948,1949,1952,1953,1956],{},[19,1946,1947],{},"Prompts fill gaps only."," ",[14,1950,1951],{},"fill_gaps"," asks only for values the flags did not provide. Someone who remembers the template but not the extras types ",[14,1954,1955],{},"mytool new --template service"," and is asked two questions, not four.",[10,1958,1959,1962,1963,1965,1966,1969],{},[19,1960,1961],{},"Validation while typing."," questionary runs the ",[14,1964,798],{}," callback on each keystroke and shows the message inline, so an invalid name never gets as far as the command. The same ",[14,1967,1968],{},"validate_name"," function is used for flags, keeping the rules identical.",[10,1971,1972,1975],{},[19,1973,1974],{},"A summary before acting."," The final confirmation restates what will happen, the moment to catch a wrong template before files are created.",[10,1977,1978,1981,1982,1985],{},[19,1979,1980],{},"The equivalent command, printed at the end."," \"next time: mytool new --name billing --template service --feature ci\" teaches the fast path from the slow one, and gives users something to paste into docs or scripts. ",[14,1983,1984],{},"shlex.join"," quotes it correctly for the shell.",[10,1987,1988,1991,1992,1995,1996,32],{},[19,1989,1990],{},"Lazy import."," questionary and prompt_toolkit are imported inside ",[14,1993,1994],{},"QuestionaryAsker","'s methods, so commands run from scripts never pay their import cost — consistent with ",[28,1997,1999],{"href":1998},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup\u002F","lazy-loading subcommands for faster startup",[10,2001,2002,2008,2009,2012,2013,1881,2015,2017,2018,2021,2022,2026,2027,2029],{},[19,2003,2004,2007],{},[14,2005,2006],{},"unsafe_ask()"," and Ctrl+C."," questionary's ",[14,2010,2011],{},"ask()"," swallows Ctrl+C and returns ",[14,2014,1398],{},[14,2016,2006],{}," raises ",[14,2019,2020],{},"KeyboardInterrupt",", which the command catches to print \"cancelled\" and exit 130 — the convention from ",[28,2023,2025],{"href":2024},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly\u002F","handling KeyboardInterrupt cleanly",". Returning ",[14,2028,1398],{}," from a prompt silently is how half-configured projects get created.",[34,2031,2033],{"id":2032},"ux-considerations","UX considerations",[64,2035],{"name":2036},"pm-terminal",[39,2038,2039,2054,2064,2070,2076],{},[42,2040,2041,2044,2045,2048,2049,2053],{},[19,2042,2043],{},"Never prompt without a terminal."," The command checks that stdin and stdout are TTYs and that ",[14,2046,2047],{},"--no-input"," was not given; otherwise it fails immediately, naming the missing flags. A prompt in CI hangs until the job times out. ",[28,2050,2052],{"href":2051},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells\u002F","Detecting CI environments and non-interactive shells"," covers the detection in detail.",[42,2055,2056,2059,2060,2063],{},[19,2057,2058],{},"Sensible defaults."," Pre-select the most common option (",[14,2061,2062],{},"default=\"basic\"",") so Enter does the usual thing.",[42,2065,2066,2069],{},[19,2067,2068],{},"Descriptions in menus."," \"service  A CLI with an HTTP client\" is more useful than \"service\" alone; menus are the one place where a short description costs nothing.",[42,2071,2072,2075],{},[19,2073,2074],{},"Keep flows short."," Three to five questions is comfortable; beyond that, write the answers to a config file the user can edit, rather than asking every time.",[42,2077,2078,2081,2082,2085],{},[19,2079,2080],{},"Prompts go to the terminal, not stdout."," questionary writes to the terminal directly, so a command's stdout remains clean data — the final ",[14,2083,2084],{},"created ..."," line here — even after an interactive session.",[34,2087,2089],{"id":2088},"testing-the-behaviour","Testing the behaviour",[10,2091,2092,2093,2095],{},"Because the flow depends on an ",[14,2094,110],{}," protocol, tests exercise it with a fake that records which questions were asked:",[127,2097,2099],{"className":129,"code":2098,"language":131,"meta":132,"style":132},"# tests\u002Ftest_wizard.py\nfrom typer.testing import CliRunner\n\nfrom mytool.cli import app\nfrom mytool.wizard import Answers, fill_gaps, validate_name\n\nrunner = CliRunner()\n\n\nclass FakeAsker:\n    def __init__(self, **answers):\n        self.answers = answers\n        self.asked: list[str] = []\n\n    def text(self, message, validate):\n        self.asked.append(\"name\")\n        value = self.answers[\"name\"]\n        assert validate(value) is True\n        return value\n\n    def select(self, message, choices, default):\n        self.asked.append(\"template\")\n        return self.answers[\"template\"]\n\n    def checkbox(self, message, choices):\n        self.asked.append(\"features\")\n        return self.answers[\"features\"]\n\n    def confirm(self, message):\n        return self.answers.get(\"confirm\", True)\n\n\ndef test_only_missing_values_are_asked():\n    ask = FakeAsker(template=\"service\", features=[\"ci\"])\n    result = fill_gaps(Answers(name=\"billing\"), ask)\n    assert ask.asked == [\"template\", \"features\"]\n    assert result.command() == \"mytool new --name billing --template service --feature ci\"\n\n\ndef test_declining_the_summary_returns_none():\n    assert fill_gaps(Answers(\"a1\", \"basic\", []), FakeAsker(confirm=False)) is None\n\n\ndef test_name_validation():\n    assert validate_name(\"billing-api\") is True\n    assert \"lowercase\" in validate_name(\"Billing API\")\n\n\ndef test_flags_alone_never_prompt():\n    result = runner.invoke(app, [\"new\", \"--name\", \"web\", \"--template\", \"basic\", \"--feature\", \"docker\"])\n    assert result.exit_code == 0 and \"created web (basic; docker)\" in result.output\n\n\ndef test_missing_values_without_a_terminal_fail_fast():\n    result = runner.invoke(app, [\"new\", \"--name\", \"web\"])\n    assert result.exit_code == 2 and \"--template\" in result.output\n",[14,2100,2101,2106,2118,2122,2134,2145,2149,2159,2163,2167,2176,2192,2205,2221,2225,2233,2245,2261,2274,2281,2285,2293,2304,2316,2320,2328,2339,2351,2355,2363,2381,2385,2389,2399,2430,2450,2470,2482,2486,2490,2499,2529,2533,2537,2546,2563,2580,2584,2588,2597,2635,2657,2661,2665,2674,2694],{"__ignoreMap":132},[136,2102,2103],{"class":138,"line":139},[136,2104,2105],{"class":142},"# tests\u002Ftest_wizard.py\n",[136,2107,2108,2110,2113,2115],{"class":138,"line":146},[136,2109,150],{"class":149},[136,2111,2112],{"class":160}," typer.testing ",[136,2114,174],{"class":149},[136,2116,2117],{"class":160}," CliRunner\n",[136,2119,2120],{"class":138,"line":164},[136,2121,168],{"emptyLinePlaceholder":167},[136,2123,2124,2126,2129,2131],{"class":138,"line":171},[136,2125,150],{"class":149},[136,2127,2128],{"class":160}," mytool.cli ",[136,2130,174],{"class":149},[136,2132,2133],{"class":160}," app\n",[136,2135,2136,2138,2140,2142],{"class":138,"line":180},[136,2137,150],{"class":149},[136,2139,1309],{"class":160},[136,2141,174],{"class":149},[136,2143,2144],{"class":160}," Answers, fill_gaps, validate_name\n",[136,2146,2147],{"class":138,"line":188},[136,2148,168],{"emptyLinePlaceholder":167},[136,2150,2151,2154,2156],{"class":138,"line":201},[136,2152,2153],{"class":160},"runner ",[136,2155,583],{"class":149},[136,2157,2158],{"class":160}," CliRunner()\n",[136,2160,2161],{"class":138,"line":214},[136,2162,168],{"emptyLinePlaceholder":167},[136,2164,2165],{"class":138,"line":219},[136,2166,168],{"emptyLinePlaceholder":167},[136,2168,2169,2171,2174],{"class":138,"line":264},[136,2170,348],{"class":149},[136,2172,2173],{"class":351}," FakeAsker",[136,2175,499],{"class":160},[136,2177,2178,2180,2183,2186,2189],{"class":138,"line":304},[136,2179,367],{"class":149},[136,2181,2182],{"class":153}," __init__",[136,2184,2185],{"class":160},"(self, ",[136,2187,2188],{"class":149},"**",[136,2190,2191],{"class":160},"answers):\n",[136,2193,2194,2197,2200,2202],{"class":138,"line":335},[136,2195,2196],{"class":153},"        self",[136,2198,2199],{"class":160},".answers ",[136,2201,583],{"class":149},[136,2203,2204],{"class":160}," answers\n",[136,2206,2207,2209,2212,2214,2216,2218],{"class":138,"line":340},[136,2208,2196],{"class":153},[136,2210,2211],{"class":160},".asked: list[",[136,2213,376],{"class":153},[136,2215,545],{"class":160},[136,2217,583],{"class":149},[136,2219,2220],{"class":160}," []\n",[136,2222,2223],{"class":138,"line":345},[136,2224,168],{"emptyLinePlaceholder":167},[136,2226,2227,2229,2231],{"class":138,"line":364},[136,2228,367],{"class":149},[136,2230,370],{"class":351},[136,2232,1004],{"class":160},[136,2234,2235,2237,2240,2243],{"class":138,"line":389},[136,2236,2196],{"class":153},[136,2238,2239],{"class":160},".asked.append(",[136,2241,2242],{"class":231},"\"name\"",[136,2244,332],{"class":160},[136,2246,2247,2250,2252,2254,2257,2259],{"class":138,"line":424},[136,2248,2249],{"class":160},"        value ",[136,2251,583],{"class":149},[136,2253,647],{"class":153},[136,2255,2256],{"class":160},".answers[",[136,2258,2242],{"class":231},[136,2260,632],{"class":160},[136,2262,2263,2266,2269,2271],{"class":138,"line":454},[136,2264,2265],{"class":149},"        assert",[136,2267,2268],{"class":160}," validate(value) ",[136,2270,856],{"class":149},[136,2272,2273],{"class":153}," True\n",[136,2275,2276,2278],{"class":138,"line":475},[136,2277,678],{"class":149},[136,2279,2280],{"class":160}," value\n",[136,2282,2283],{"class":138,"line":480},[136,2284,168],{"emptyLinePlaceholder":167},[136,2286,2287,2289,2291],{"class":138,"line":485},[136,2288,367],{"class":149},[136,2290,394],{"class":351},[136,2292,1043],{"class":160},[136,2294,2295,2297,2299,2302],{"class":138,"line":491},[136,2296,2196],{"class":153},[136,2298,2239],{"class":160},[136,2300,2301],{"class":231},"\"template\"",[136,2303,332],{"class":160},[136,2305,2306,2308,2310,2312,2314],{"class":138,"line":502},[136,2307,678],{"class":149},[136,2309,647],{"class":153},[136,2311,2256],{"class":160},[136,2313,2301],{"class":231},[136,2315,632],{"class":160},[136,2317,2318],{"class":138,"line":521},[136,2319,168],{"emptyLinePlaceholder":167},[136,2321,2322,2324,2326],{"class":138,"line":537},[136,2323,367],{"class":149},[136,2325,429],{"class":351},[136,2327,1168],{"class":160},[136,2329,2330,2332,2334,2337],{"class":138,"line":557},[136,2331,2196],{"class":153},[136,2333,2239],{"class":160},[136,2335,2336],{"class":231},"\"features\"",[136,2338,332],{"class":160},[136,2340,2341,2343,2345,2347,2349],{"class":138,"line":562},[136,2342,678],{"class":149},[136,2344,647],{"class":153},[136,2346,2256],{"class":160},[136,2348,2336],{"class":231},[136,2350,632],{"class":160},[136,2352,2353],{"class":138,"line":577},[136,2354,168],{"emptyLinePlaceholder":167},[136,2356,2357,2359,2361],{"class":138,"line":635},[136,2358,367],{"class":149},[136,2360,459],{"class":351},[136,2362,1220],{"class":160},[136,2364,2365,2367,2369,2372,2375,2377,2379],{"class":138,"line":658},[136,2366,678],{"class":149},[136,2368,647],{"class":153},[136,2370,2371],{"class":160},".answers.get(",[136,2373,2374],{"class":231},"\"confirm\"",[136,2376,115],{"class":160},[136,2378,1242],{"class":153},[136,2380,332],{"class":160},[136,2382,2383],{"class":138,"line":675},[136,2384,168],{"emptyLinePlaceholder":167},[136,2386,2387],{"class":138,"line":684},[136,2388,168],{"emptyLinePlaceholder":167},[136,2390,2391,2393,2396],{"class":138,"line":689},[136,2392,697],{"class":149},[136,2394,2395],{"class":351}," test_only_missing_values_are_asked",[136,2397,2398],{"class":160},"():\n",[136,2400,2401,2404,2406,2409,2411,2413,2415,2417,2420,2422,2425,2427],{"class":138,"line":694},[136,2402,2403],{"class":160},"    ask ",[136,2405,583],{"class":149},[136,2407,2408],{"class":160}," FakeAsker(",[136,2410,921],{"class":797},[136,2412,583],{"class":149},[136,2414,243],{"class":231},[136,2416,115],{"class":160},[136,2418,2419],{"class":797},"features",[136,2421,583],{"class":149},[136,2423,2424],{"class":160},"[",[136,2426,284],{"class":231},[136,2428,2429],{"class":160},"])\n",[136,2431,2432,2435,2437,2440,2442,2444,2447],{"class":138,"line":719},[136,2433,2434],{"class":160},"    result ",[136,2436,583],{"class":149},[136,2438,2439],{"class":160}," fill_gaps(Answers(",[136,2441,907],{"class":797},[136,2443,583],{"class":149},[136,2445,2446],{"class":231},"\"billing\"",[136,2448,2449],{"class":160},"), ask)\n",[136,2451,2452,2455,2458,2460,2462,2464,2466,2468],{"class":138,"line":743},[136,2453,2454],{"class":149},"    assert",[136,2456,2457],{"class":160}," ask.asked ",[136,2459,1150],{"class":149},[136,2461,586],{"class":160},[136,2463,2301],{"class":231},[136,2465,115],{"class":160},[136,2467,2336],{"class":231},[136,2469,632],{"class":160},[136,2471,2472,2474,2477,2479],{"class":138,"line":748},[136,2473,2454],{"class":149},[136,2475,2476],{"class":160}," result.command() ",[136,2478,1150],{"class":149},[136,2480,2481],{"class":231}," \"mytool new --name billing --template service --feature ci\"\n",[136,2483,2484],{"class":138,"line":753},[136,2485,168],{"emptyLinePlaceholder":167},[136,2487,2488],{"class":138,"line":770},[136,2489,168],{"emptyLinePlaceholder":167},[136,2491,2492,2494,2497],{"class":138,"line":776},[136,2493,697],{"class":149},[136,2495,2496],{"class":351}," test_declining_the_summary_returns_none",[136,2498,2398],{"class":160},[136,2500,2501,2503,2505,2508,2510,2512,2515,2517,2519,2522,2525,2527],{"class":138,"line":806},[136,2502,2454],{"class":149},[136,2504,2439],{"class":160},[136,2506,2507],{"class":231},"\"a1\"",[136,2509,115],{"class":160},[136,2511,232],{"class":231},[136,2513,2514],{"class":160},", []), FakeAsker(",[136,2516,124],{"class":797},[136,2518,583],{"class":149},[136,2520,2521],{"class":153},"False",[136,2523,2524],{"class":160},")) ",[136,2526,856],{"class":149},[136,2528,518],{"class":153},[136,2530,2531],{"class":138,"line":840},[136,2532,168],{"emptyLinePlaceholder":167},[136,2534,2535],{"class":138,"line":879},[136,2536,168],{"emptyLinePlaceholder":167},[136,2538,2539,2541,2544],{"class":138,"line":890},[136,2540,697],{"class":149},[136,2542,2543],{"class":351}," test_name_validation",[136,2545,2398],{"class":160},[136,2547,2548,2550,2553,2556,2559,2561],{"class":138,"line":949},[136,2549,2454],{"class":149},[136,2551,2552],{"class":160}," validate_name(",[136,2554,2555],{"class":231},"\"billing-api\"",[136,2557,2558],{"class":160},") ",[136,2560,856],{"class":149},[136,2562,2273],{"class":153},[136,2564,2565,2567,2570,2573,2575,2578],{"class":138,"line":966},[136,2566,2454],{"class":149},[136,2568,2569],{"class":231}," \"lowercase\"",[136,2571,2572],{"class":149}," in",[136,2574,2552],{"class":160},[136,2576,2577],{"class":231},"\"Billing API\"",[136,2579,332],{"class":160},[136,2581,2582],{"class":138,"line":971},[136,2583,168],{"emptyLinePlaceholder":167},[136,2585,2586],{"class":138,"line":976},[136,2587,168],{"emptyLinePlaceholder":167},[136,2589,2590,2592,2595],{"class":138,"line":986},[136,2591,697],{"class":149},[136,2593,2594],{"class":351}," test_flags_alone_never_prompt",[136,2596,2398],{"class":160},[136,2598,2599,2601,2603,2606,2608,2610,2612,2614,2617,2619,2621,2623,2625,2627,2629,2631,2633],{"class":138,"line":992},[136,2600,2434],{"class":160},[136,2602,583],{"class":149},[136,2604,2605],{"class":160}," runner.invoke(app, [",[136,2607,594],{"class":231},[136,2609,115],{"class":160},[136,2611,599],{"class":231},[136,2613,115],{"class":160},[136,2615,2616],{"class":231},"\"web\"",[136,2618,115],{"class":160},[136,2620,618],{"class":231},[136,2622,115],{"class":160},[136,2624,232],{"class":231},[136,2626,115],{"class":160},[136,2628,669],{"class":231},[136,2630,115],{"class":160},[136,2632,274],{"class":231},[136,2634,2429],{"class":160},[136,2636,2637,2639,2642,2644,2647,2649,2652,2654],{"class":138,"line":997},[136,2638,2454],{"class":149},[136,2640,2641],{"class":160}," result.exit_code ",[136,2643,1150],{"class":149},[136,2645,2646],{"class":153}," 0",[136,2648,1628],{"class":149},[136,2650,2651],{"class":231}," \"created web (basic; docker)\"",[136,2653,2572],{"class":149},[136,2655,2656],{"class":160}," result.output\n",[136,2658,2659],{"class":138,"line":1007},[136,2660,168],{"emptyLinePlaceholder":167},[136,2662,2663],{"class":138,"line":1016},[136,2664,168],{"emptyLinePlaceholder":167},[136,2666,2667,2669,2672],{"class":138,"line":1031},[136,2668,697],{"class":149},[136,2670,2671],{"class":351}," test_missing_values_without_a_terminal_fail_fast",[136,2673,2398],{"class":160},[136,2675,2676,2678,2680,2682,2684,2686,2688,2690,2692],{"class":138,"line":1036},[136,2677,2434],{"class":160},[136,2679,583],{"class":149},[136,2681,2605],{"class":160},[136,2683,594],{"class":231},[136,2685,115],{"class":160},[136,2687,599],{"class":231},[136,2689,115],{"class":160},[136,2691,2616],{"class":231},[136,2693,2429],{"class":160},[136,2695,2696,2698,2700,2702,2705,2707,2710,2712],{"class":138,"line":1046},[136,2697,2454],{"class":149},[136,2699,2641],{"class":160},[136,2701,1150],{"class":149},[136,2703,2704],{"class":153}," 2",[136,2706,1628],{"class":149},[136,2708,2709],{"class":231}," \"--template\"",[136,2711,2572],{"class":149},[136,2713,2656],{"class":160},[10,2715,2716,2717,2720,2721,2724,2725,2728,2729,2732],{},"The first test pins the most important behaviour — only missing values are asked — and the last two pin the non-interactive contract that keeps automation working. For end-to-end tests of the real questionary prompts, prompt_toolkit supports piped input: create one with ",[14,2718,2719],{},"prompt_toolkit.input.create_pipe_input()",", send keystrokes such as ",[14,2722,2723],{},"\"j\\r\""," (down, Enter), and pass it with ",[14,2726,2727],{},"output=DummyOutput()"," to ",[14,2730,2731],{},"questionary.select(..., input=..., output=...)",". Keep such tests few; the fake covers the logic.",[34,2734,2736],{"id":2735},"conclusion","Conclusion",[10,2738,2739,2740,2742],{},"Interactive prompts make rare, multi-decision tasks approachable, provided they sit on top of flags rather than replacing them. Ask only for what flags did not provide, choose the right question type — menus and checkboxes over free text where possible — validate as the user types, confirm a summary, and print the equivalent command at the end. Refuse to prompt without a terminal, raise on Ctrl+C instead of returning ",[14,2741,1398],{},", import the prompt library lazily, and keep the flow testable behind a small protocol.",[34,2744,2746],{"id":2745},"frequently-asked-questions","Frequently asked questions",[1929,2748,2750],{"id":2749},"questionary-inquirerpy-or-rich-prompts","questionary, InquirerPy or Rich prompts?",[10,2752,2753,2754,93,2757,2759,2760,2762],{},"Rich's ",[14,2755,2756],{},"Prompt",[14,2758,76],{}," handle text and yes\u002Fno with nice styling but no menus. questionary and InquirerPy both provide menus, checkboxes and fuzzy selection on prompt_toolkit; questionary has a simpler API and wide use. Pick one and wrap it, as the ",[14,2761,110],{}," protocol does, so switching later is local.",[1929,2764,2766],{"id":2765},"do-these-prompts-work-on-windows","Do these prompts work on Windows?",[10,2768,2769],{},"Yes — prompt_toolkit supports Windows consoles and Windows Terminal. Legacy consoles may render some glyphs (pointer symbols, checkbox marks) as plain characters, which remains usable.",[1929,2771,2773],{"id":2772},"how-do-i-prompt-for-passwords","How do I prompt for passwords?",[10,2775,2776,2777,2780,2781,2784,2785,2789],{},"Use ",[14,2778,2779],{},"questionary.password"," or ",[14,2782,2783],{},"typer.prompt(..., hide_input=True)",", and follow the rules in ",[28,2786,2788],{"href":2787},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely\u002F","prompting for passwords securely",": never echo, confirm new secrets, and always offer a non-interactive path.",[1929,2791,2793],{"id":2792},"can-i-show-a-menu-of-values-fetched-from-an-api","Can I show a menu of values fetched from an API?",[10,2795,2796],{},"Yes, but fetch before showing the prompt, with a spinner and a timeout, and cache the list if it is reused. A menu that freezes while loading feels broken.",[34,2798,2800],{"id":2799},"related","Related",[39,2802,2803,2809,2815,2821,2827],{},[42,2804,2805,2806],{},"Up: ",[28,2807,2808],{"href":30},"Interactive terminal UI with Rich",[42,2810,2811],{},[28,2812,2814],{"href":2813},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live\u002F","Live dashboards with Rich Live",[42,2816,2817],{},[28,2818,2820],{"href":2819},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently\u002F","Theming Rich output consistently",[42,2822,2823],{},[28,2824,2826],{"href":2825},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin\u002F","Testing interactive prompts and stdin",[42,2828,2829],{},[28,2830,2831],{"href":57},"Choosing between a CLI, a prompt flow and a TUI",[2833,2834,2835],"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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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);}",{"title":132,"searchDepth":146,"depth":146,"links":2837},[2838,2839,2840,2843,2844,2845,2846,2852],{"id":36,"depth":146,"text":37},{"id":61,"depth":146,"text":62},{"id":100,"depth":146,"text":101,"children":2841},[2842],{"id":1931,"depth":164,"text":1932},{"id":2032,"depth":146,"text":2033},{"id":2088,"depth":146,"text":2089},{"id":2735,"depth":146,"text":2736},{"id":2745,"depth":146,"text":2746,"children":2847},[2848,2849,2850,2851],{"id":2749,"depth":164,"text":2750},{"id":2765,"depth":164,"text":2766},{"id":2772,"depth":164,"text":2773},{"id":2792,"depth":164,"text":2793},{"id":2799,"depth":146,"text":2800},"2026-09-18","Add arrow-key menus, checkboxes and validated prompts to a Python CLI with questionary, keeping flag parity, non-interactive fallbacks and testable prompt logic.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus",{"title":5,"description":2854},"advanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus\u002Findex",[2863,21,2864,2865],"prompts","rich","terminal-ux","ByCow-siw0aOyqhMUseXNXA_OmU_7EFIB-qRyFRvocQ",[2868,2871,2874,2877,2880,2883,2886,2889,2892,2895,2898,2901,2904,2907,2910,2913,2916,2919,2922,2925,2928,2931,2934,2937,2940,2943,2946,2949,2952,2955,2958,2961,2964,2967,2970,2971,2974,2977,2980,2983,2986,2989,2992,2995,2998,3001,3004,3007,3010,3013,3016,3019,3022,3025,3028,3031,3034,3037,3040,3043,3046,3049,3052,3055,3058,3061,3064,3067,3070,3073,3076,3079,3082,3085,3088,3091,3094,3097,3100,3103,3106,3109,3112,3115,3118,3121,3124,3127,3130,3133,3136,3139,3142,3145,3148,3151,3154,3157,3160,3163,3166,3169,3172,3175,3178,3181,3184,3187,3190,3193,3196,3199,3202,3205,3208,3211,3214,3217,3220,3223,3226,3229,3232,3235,3238,3241,3244,3247,3250,3253,3256,3259,3262,3265,3268,3271,3274,3277,3280,3283,3286,3289,3292,3295,3298,3301,3304,3307,3310,3313,3316,3319,3322,3325,3328,3331,3334,3337,3340,3343,3346,3349,3352,3355,3358,3361,3364,3367,3370,3373,3376,3379,3382,3385,3388,3391,3394,3397,3400,3403,3406,3409,3412],{"path":2869,"title":2870},"\u002Fabout","About Python CLI Toolcraft",{"path":2872,"title":2873},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2875,"title":2876},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2878,"title":2879},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2881,"title":2882},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2884,"title":2885},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2887,"title":2888},"\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":2890,"title":2891},"\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":2893,"title":2894},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2896,"title":2897},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2899,"title":2900},"\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":2902,"title":2903},"\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":2905,"title":2906},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2908,"title":2909},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2911,"title":2912},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2914,"title":2915},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2917,"title":2918},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2920,"title":2921},"\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":2923,"title":2924},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2926,"title":2927},"\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":2929,"title":2930},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2932,"title":2933},"\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":2935,"title":2936},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2938,"title":2939},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2941,"title":2942},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2944,"title":2945},"\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":2947,"title":2948},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2950,"title":2951},"\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":2953,"title":2954},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2956,"title":2957},"\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":2959,"title":2960},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2962,"title":2963},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2965,"title":2966},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2968,"title":2969},"\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":2859,"title":5},{"path":2972,"title":2973},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2975,"title":2976},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2978,"title":2979},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2981,"title":2982},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2984,"title":2985},"\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":2987,"title":2988},"\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":2990,"title":2991},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2993,"title":2994},"\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":2996,"title":2997},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2999,"title":3000},"\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":3002,"title":3003},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":3005,"title":3006},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":3008,"title":3009},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":3011,"title":3012},"\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":3014,"title":3015},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":3017,"title":3018},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":3020,"title":3021},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":3023,"title":3024},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":3026,"title":3027},"\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":3029,"title":3030},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":3032,"title":3033},"\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":3035,"title":3036},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":3038,"title":3039},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":3041,"title":3042},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":3044,"title":3045},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":3047,"title":3048},"\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":3050,"title":3051},"\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":3053,"title":3054},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":3056,"title":3057},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":3059,"title":3060},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":3062,"title":3063},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":3065,"title":3066},"\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":3068,"title":3069},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":3071,"title":3072},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":3074,"title":3075},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":3077,"title":3078},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":3080,"title":3081},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":3083,"title":3084},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":3086,"title":3087},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":3089,"title":3090},"\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":3092,"title":3093},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":3095,"title":3096},"\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":3098,"title":3099},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":3101,"title":3102},"\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":3104,"title":3105},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":3107,"title":3108},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":3110,"title":3111},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":3113,"title":3114},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":3116,"title":3117},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":3119,"title":3120},"\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":3122,"title":3123},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":3125,"title":3126},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":3128,"title":3129},"\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":3131,"title":3132},"\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":3134,"title":3135},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":3137,"title":3138},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":3140,"title":3141},"\u002F","Python CLI Toolcraft",{"path":3143,"title":3144},"\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":3146,"title":3147},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":3149,"title":3150},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":3152,"title":3153},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":3155,"title":3156},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":3158,"title":3159},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":3161,"title":3162},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":3164,"title":3165},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":3167,"title":3168},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":3170,"title":3171},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":3173,"title":3174},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":3176,"title":3177},"\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":3179,"title":3180},"\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":3182,"title":3183},"\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":3185,"title":3186},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":3188,"title":3189},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":3191,"title":3192},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":3194,"title":3195},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":3197,"title":3198},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":3200,"title":3201},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":3203,"title":3204},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":3206,"title":3207},"\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":3209,"title":3210},"\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":3212,"title":3213},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":3215,"title":3216},"\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":3218,"title":3219},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":3221,"title":3222},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":3224,"title":3225},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":3227,"title":3228},"\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":3230,"title":3231},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":3233,"title":3234},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":3236,"title":3237},"\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":3239,"title":3240},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":3242,"title":3243},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":3245,"title":3246},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":3248,"title":3249},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":3251,"title":3252},"\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":3254,"title":3255},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":3257,"title":3258},"\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":3260,"title":3261},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":3263,"title":3264},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":3266,"title":3267},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":3269,"title":3270},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":3272,"title":3273},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":3275,"title":3276},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":3278,"title":3279},"\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":3281,"title":3282},"\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":3284,"title":3285},"\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":3287,"title":3288},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":3290,"title":3291},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":3293,"title":3294},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":3296,"title":3297},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":3299,"title":3300},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":3302,"title":3303},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":3305,"title":3306},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":3308,"title":3309},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":3311,"title":3312},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":3314,"title":3315},"\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":3317,"title":3318},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":3320,"title":3321},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":3323,"title":3324},"\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":3326,"title":3327},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":3329,"title":3330},"\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":3332,"title":3333},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":3335,"title":3336},"\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":3338,"title":3339},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":3341,"title":3342},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":3344,"title":3345},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":3347,"title":3348},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":3350,"title":3351},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":3353,"title":3354},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":3356,"title":3357},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":3359,"title":3360},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":3362,"title":3363},"\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":3365,"title":3366},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":3368,"title":3369},"\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":3371,"title":3372},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":3374,"title":3375},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":3377,"title":3378},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":3380,"title":3381},"\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":3383,"title":3384},"\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":3386,"title":3387},"\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":3389,"title":3390},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":3392,"title":3393},"\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":3395,"title":3396},"\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":3398,"title":3399},"\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":3401,"title":3402},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":3404,"title":3405},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":3407,"title":3408},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":3410,"title":3411},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":3413,"title":3414},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736905046]