[{"data":1,"prerenderedAt":1716},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin\u002F":3,"content-directory":1468},{"id":4,"title":5,"body":6,"date":1452,"description":1453,"difficulty":1454,"draft":1455,"extension":1456,"meta":1457,"navigation":117,"path":1458,"seo":1459,"stem":1460,"tags":1461,"updated":1452,"__hash__":1467},"content\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin\u002Findex.md","Testing Interactive Prompts and stdin",{"type":7,"value":8,"toc":1430},"minimark",[9,13,18,51,55,80,84,253,257,260,263,337,341,344,347,470,477,481,484,587,597,662,670,768,771,775,778,870,877,884,920,924,927,1058,1065,1069,1072,1086,1095,1101,1105,1108,1299,1302,1306,1309,1313,1318,1329,1333,1340,1344,1347,1351,1362,1366,1372,1376,1379,1383,1386,1390,1426],[10,11,12],"p",{},"Interactive prompts are pleasant for users and awkward for tests, because a test has nobody to\nanswer them. The runner solves that by supplying stdin — but the more important lesson is that\nmost tests should not go through the prompt at all.",[14,15,17],"h2",{"id":16},"tldr","TL;DR",[19,20,21,30,38,45,48],"ul",{},[22,23,24,25,29],"li",{},"Supply answers with ",[26,27,28],"code",{},"runner.invoke(app, argv, input=\"prod\\ny\\n\")",", one line per prompt, in\norder.",[22,31,32,33,37],{},"Test the ",[34,35,36],"strong",{},"prompt behaviour"," once; test everything else through the flag that skips it.",[22,39,40,41,44],{},"Always cover the ",[34,42,43],{},"non-interactive path",": no terminal means fail with a message naming the\nflag, never hang.",[22,46,47],{},"Passwords read without echoing, so the answer does not appear in captured output.",[22,49,50],{},"A missing input line aborts the command rather than hanging, which is what makes this testable.",[14,52,54],{"id":53},"prerequisites","Prerequisites",[10,56,57,58,61,62,61,65,61,68,71,72,75,76,79],{},"A command that prompts — ",[26,59,60],{},"typer.prompt",", ",[26,63,64],{},"typer.confirm",[26,66,67],{},"click.prompt",[26,69,70],{},"click.confirm"," or\nRich's ",[26,73,74],{},"Prompt.ask"," — plus pytest and the framework's ",[26,77,78],{},"CliRunner",".",[14,81,83],{"id":82},"supplying-answers","Supplying answers",[85,86,91],"pre",{"className":87,"code":88,"language":89,"meta":90,"style":90},"language-python shiki shiki-themes github-light github-dark","from typer.testing import CliRunner\n\nfrom mytool.cli import app\n\nrunner = CliRunner(mix_stderr=False)\n\ndef test_destroy_asks_for_confirmation():\n    result = runner.invoke(app, [\"destroy\", \"prod\"], input=\"y\\n\")\n\n    assert result.exit_code == 0\n    assert \"Really destroy prod?\" in result.stdout\n","python","",[26,92,93,112,119,132,137,162,167,180,219,224,239],{"__ignoreMap":90},[94,95,98,102,106,109],"span",{"class":96,"line":97},"line",1,[94,99,101],{"class":100},"szBVR","from",[94,103,105],{"class":104},"sVt8B"," typer.testing ",[94,107,108],{"class":100},"import",[94,110,111],{"class":104}," CliRunner\n",[94,113,115],{"class":96,"line":114},2,[94,116,118],{"emptyLinePlaceholder":117},true,"\n",[94,120,122,124,127,129],{"class":96,"line":121},3,[94,123,101],{"class":100},[94,125,126],{"class":104}," mytool.cli ",[94,128,108],{"class":100},[94,130,131],{"class":104}," app\n",[94,133,135],{"class":96,"line":134},4,[94,136,118],{"emptyLinePlaceholder":117},[94,138,140,143,146,149,153,155,159],{"class":96,"line":139},5,[94,141,142],{"class":104},"runner ",[94,144,145],{"class":100},"=",[94,147,148],{"class":104}," CliRunner(",[94,150,152],{"class":151},"s4XuR","mix_stderr",[94,154,145],{"class":100},[94,156,158],{"class":157},"sj4cs","False",[94,160,161],{"class":104},")\n",[94,163,165],{"class":96,"line":164},6,[94,166,118],{"emptyLinePlaceholder":117},[94,168,170,173,177],{"class":96,"line":169},7,[94,171,172],{"class":100},"def",[94,174,176],{"class":175},"sScJk"," test_destroy_asks_for_confirmation",[94,178,179],{"class":104},"():\n",[94,181,183,186,188,191,195,197,200,203,206,208,211,214,217],{"class":96,"line":182},8,[94,184,185],{"class":104},"    result ",[94,187,145],{"class":100},[94,189,190],{"class":104}," runner.invoke(app, [",[94,192,194],{"class":193},"sZZnC","\"destroy\"",[94,196,61],{"class":104},[94,198,199],{"class":193},"\"prod\"",[94,201,202],{"class":104},"], ",[94,204,205],{"class":151},"input",[94,207,145],{"class":100},[94,209,210],{"class":193},"\"y",[94,212,213],{"class":157},"\\n",[94,215,216],{"class":193},"\"",[94,218,161],{"class":104},[94,220,222],{"class":96,"line":221},9,[94,223,118],{"emptyLinePlaceholder":117},[94,225,227,230,233,236],{"class":96,"line":226},10,[94,228,229],{"class":100},"    assert",[94,231,232],{"class":104}," result.exit_code ",[94,234,235],{"class":100},"==",[94,237,238],{"class":157}," 0\n",[94,240,242,244,247,250],{"class":96,"line":241},11,[94,243,229],{"class":100},[94,245,246],{"class":193}," \"Really destroy prod?\"",[94,248,249],{"class":100}," in",[94,251,252],{"class":104}," result.stdout\n",[254,255],"inline-diagram",{"name":256},"prompt-input-anatomy",[10,258,259],{},"The string is consumed line by line as each prompt asks, so the order of the answers must match\nthe order of the questions. If a command asks two questions and you supply one line, the second\nprompt hits end-of-input and the command aborts — the exit code becomes 1, which is a much better\nfailure mode than a hanging test.",[10,261,262],{},"An empty line accepts the default:",[85,264,266],{"className":87,"code":265,"language":89,"meta":90,"style":90},"def test_environment_prompt_defaults_to_staging():\n    result = runner.invoke(app, [\"deploy\"], input=\"\\ny\\n\")     # accept default, then confirm\n\n    assert result.exit_code == 0\n    assert \"deploying to staging\" in result.stdout\n",[26,267,268,277,312,316,326],{"__ignoreMap":90},[94,269,270,272,275],{"class":96,"line":97},[94,271,172],{"class":100},[94,273,274],{"class":175}," test_environment_prompt_defaults_to_staging",[94,276,179],{"class":104},[94,278,279,281,283,285,288,290,292,294,296,298,301,303,305,308],{"class":96,"line":114},[94,280,185],{"class":104},[94,282,145],{"class":100},[94,284,190],{"class":104},[94,286,287],{"class":193},"\"deploy\"",[94,289,202],{"class":104},[94,291,205],{"class":151},[94,293,145],{"class":100},[94,295,216],{"class":193},[94,297,213],{"class":157},[94,299,300],{"class":193},"y",[94,302,213],{"class":157},[94,304,216],{"class":193},[94,306,307],{"class":104},")     ",[94,309,311],{"class":310},"sJ8bj","# accept default, then confirm\n",[94,313,314],{"class":96,"line":121},[94,315,118],{"emptyLinePlaceholder":117},[94,317,318,320,322,324],{"class":96,"line":134},[94,319,229],{"class":100},[94,321,232],{"class":104},[94,323,235],{"class":100},[94,325,238],{"class":157},[94,327,328,330,333,335],{"class":96,"line":139},[94,329,229],{"class":100},[94,331,332],{"class":193}," \"deploying to staging\"",[94,334,249],{"class":100},[94,336,252],{"class":104},[14,338,340],{"id":339},"test-the-prompt-once-then-skip-it","Test the prompt once, then skip it",[254,342],{"name":343},"stdin-test-paths",[10,345,346],{},"A test that supplies input to reach the behaviour it actually cares about is fragile: adding a\nconfirmation prompt to the command breaks every one of them. Cover the prompt itself in one or two\ntests, and give every other test the flag that bypasses it.",[85,348,350],{"className":87,"code":349,"language":89,"meta":90,"style":90},"def test_prompted_value_is_used():                       # about the prompt\n    result = runner.invoke(app, [\"deploy\"], input=\"prod\\ny\\n\")\n    assert \"deploying to prod\" in result.stdout\n\ndef test_deploy_scales_replicas(cli):                    # about the behaviour\n    result = cli.invoke(app, [\"deploy\", \"--env\", \"prod\", \"--yes\", \"--replicas\", \"5\"])\n    assert result.exit_code == 0\n",[26,351,352,365,394,405,409,422,460],{"__ignoreMap":90},[94,353,354,356,359,362],{"class":96,"line":97},[94,355,172],{"class":100},[94,357,358],{"class":175}," test_prompted_value_is_used",[94,360,361],{"class":104},"():                       ",[94,363,364],{"class":310},"# about the prompt\n",[94,366,367,369,371,373,375,377,379,381,384,386,388,390,392],{"class":96,"line":114},[94,368,185],{"class":104},[94,370,145],{"class":100},[94,372,190],{"class":104},[94,374,287],{"class":193},[94,376,202],{"class":104},[94,378,205],{"class":151},[94,380,145],{"class":100},[94,382,383],{"class":193},"\"prod",[94,385,213],{"class":157},[94,387,300],{"class":193},[94,389,213],{"class":157},[94,391,216],{"class":193},[94,393,161],{"class":104},[94,395,396,398,401,403],{"class":96,"line":121},[94,397,229],{"class":100},[94,399,400],{"class":193}," \"deploying to prod\"",[94,402,249],{"class":100},[94,404,252],{"class":104},[94,406,407],{"class":96,"line":134},[94,408,118],{"emptyLinePlaceholder":117},[94,410,411,413,416,419],{"class":96,"line":139},[94,412,172],{"class":100},[94,414,415],{"class":175}," test_deploy_scales_replicas",[94,417,418],{"class":104},"(cli):                    ",[94,420,421],{"class":310},"# about the behaviour\n",[94,423,424,426,428,431,433,435,438,440,442,444,447,449,452,454,457],{"class":96,"line":164},[94,425,185],{"class":104},[94,427,145],{"class":100},[94,429,430],{"class":104}," cli.invoke(app, [",[94,432,287],{"class":193},[94,434,61],{"class":104},[94,436,437],{"class":193},"\"--env\"",[94,439,61],{"class":104},[94,441,199],{"class":193},[94,443,61],{"class":104},[94,445,446],{"class":193},"\"--yes\"",[94,448,61],{"class":104},[94,450,451],{"class":193},"\"--replicas\"",[94,453,61],{"class":104},[94,455,456],{"class":193},"\"5\"",[94,458,459],{"class":104},"])\n",[94,461,462,464,466,468],{"class":96,"line":169},[94,463,229],{"class":100},[94,465,232],{"class":104},[94,467,235],{"class":100},[94,469,238],{"class":157},[10,471,472,473,476],{},"This is also a design pressure worth feeling: if there is no flag that skips the prompt, your\ncommand cannot be used in automation either. Adding ",[26,474,475],{},"--yes"," for the tests makes the tool better\nfor cron jobs at the same time.",[14,478,480],{"id":479},"the-non-interactive-path","The non-interactive path",[10,482,483],{},"The behaviour that matters most in production is what happens when nobody is there. A prompt in a\nCI job hangs the pipeline until it times out, and the log gives no clue why.",[85,485,487],{"className":87,"code":486,"language":89,"meta":90,"style":90},"def confirm(question: str, *, assume_yes: bool) -> bool:\n    if assume_yes:\n        return True\n    if not sys.stdin.isatty():\n        err.print(\"[red]refusing to prompt without a terminal — pass --yes[\u002F]\")\n        raise typer.Exit(2)\n    return typer.confirm(question, default=False)\n",[26,488,489,521,529,537,547,557,570],{"__ignoreMap":90},[94,490,491,493,496,499,502,504,507,510,513,516,518],{"class":96,"line":97},[94,492,172],{"class":100},[94,494,495],{"class":175}," confirm",[94,497,498],{"class":104},"(question: ",[94,500,501],{"class":157},"str",[94,503,61],{"class":104},[94,505,506],{"class":100},"*",[94,508,509],{"class":104},", assume_yes: ",[94,511,512],{"class":157},"bool",[94,514,515],{"class":104},") -> ",[94,517,512],{"class":157},[94,519,520],{"class":104},":\n",[94,522,523,526],{"class":96,"line":114},[94,524,525],{"class":100},"    if",[94,527,528],{"class":104}," assume_yes:\n",[94,530,531,534],{"class":96,"line":121},[94,532,533],{"class":100},"        return",[94,535,536],{"class":157}," True\n",[94,538,539,541,544],{"class":96,"line":134},[94,540,525],{"class":100},[94,542,543],{"class":100}," not",[94,545,546],{"class":104}," sys.stdin.isatty():\n",[94,548,549,552,555],{"class":96,"line":139},[94,550,551],{"class":104},"        err.print(",[94,553,554],{"class":193},"\"[red]refusing to prompt without a terminal — pass --yes[\u002F]\"",[94,556,161],{"class":104},[94,558,559,562,565,568],{"class":96,"line":164},[94,560,561],{"class":100},"        raise",[94,563,564],{"class":104}," typer.Exit(",[94,566,567],{"class":157},"2",[94,569,161],{"class":104},[94,571,572,575,578,581,583,585],{"class":96,"line":169},[94,573,574],{"class":100},"    return",[94,576,577],{"class":104}," typer.confirm(question, ",[94,579,580],{"class":151},"default",[94,582,145],{"class":100},[94,584,158],{"class":157},[94,586,161],{"class":104},[10,588,589,590,593,594,596],{},"Testing it needs one wrinkle: under the runner, stdin is a captured stream and ",[26,591,592],{},"isatty()"," is\nalready ",[26,595,158],{},", so the non-interactive branch is the default in tests.",[85,598,600],{"className":87,"code":599,"language":89,"meta":90,"style":90},"def test_refuses_to_prompt_without_a_terminal(cli):\n    result = cli.invoke(app, [\"destroy\", \"prod\"])          # no input supplied\n\n    assert result.exit_code == 2\n    assert \"--yes\" in result.stderr                        # the message names the fix\n",[26,601,602,612,632,636,647],{"__ignoreMap":90},[94,603,604,606,609],{"class":96,"line":97},[94,605,172],{"class":100},[94,607,608],{"class":175}," test_refuses_to_prompt_without_a_terminal",[94,610,611],{"class":104},"(cli):\n",[94,613,614,616,618,620,622,624,626,629],{"class":96,"line":114},[94,615,185],{"class":104},[94,617,145],{"class":100},[94,619,430],{"class":104},[94,621,194],{"class":193},[94,623,61],{"class":104},[94,625,199],{"class":193},[94,627,628],{"class":104},"])          ",[94,630,631],{"class":310},"# no input supplied\n",[94,633,634],{"class":96,"line":121},[94,635,118],{"emptyLinePlaceholder":117},[94,637,638,640,642,644],{"class":96,"line":134},[94,639,229],{"class":100},[94,641,232],{"class":104},[94,643,235],{"class":100},[94,645,646],{"class":157}," 2\n",[94,648,649,651,654,656,659],{"class":96,"line":139},[94,650,229],{"class":100},[94,652,653],{"class":193}," \"--yes\"",[94,655,249],{"class":100},[94,657,658],{"class":104}," result.stderr                        ",[94,660,661],{"class":310},"# the message names the fix\n",[10,663,664,665,669],{},"To exercise the ",[666,667,668],"em",{},"interactive"," branch you have to say so explicitly, which is a good argument for\nrouting the check through one helper you can override:",[85,671,673],{"className":87,"code":672,"language":89,"meta":90,"style":90},"def test_prompts_when_a_terminal_is_present(cli, monkeypatch):\n    monkeypatch.setattr(\"mytool.console.stdin_is_a_terminal\", lambda: True)\n\n    result = cli.invoke(app, [\"destroy\", \"prod\"], input=\"n\\n\")\n\n    assert result.exit_code == 1                           # declined\n    assert \"Really destroy\" in result.stdout\n",[26,674,675,685,706,710,739,743,757],{"__ignoreMap":90},[94,676,677,679,682],{"class":96,"line":97},[94,678,172],{"class":100},[94,680,681],{"class":175}," test_prompts_when_a_terminal_is_present",[94,683,684],{"class":104},"(cli, monkeypatch):\n",[94,686,687,690,693,695,698,701,704],{"class":96,"line":114},[94,688,689],{"class":104},"    monkeypatch.setattr(",[94,691,692],{"class":193},"\"mytool.console.stdin_is_a_terminal\"",[94,694,61],{"class":104},[94,696,697],{"class":100},"lambda",[94,699,700],{"class":104},": ",[94,702,703],{"class":157},"True",[94,705,161],{"class":104},[94,707,708],{"class":96,"line":121},[94,709,118],{"emptyLinePlaceholder":117},[94,711,712,714,716,718,720,722,724,726,728,730,733,735,737],{"class":96,"line":134},[94,713,185],{"class":104},[94,715,145],{"class":100},[94,717,430],{"class":104},[94,719,194],{"class":193},[94,721,61],{"class":104},[94,723,199],{"class":193},[94,725,202],{"class":104},[94,727,205],{"class":151},[94,729,145],{"class":100},[94,731,732],{"class":193},"\"n",[94,734,213],{"class":157},[94,736,216],{"class":193},[94,738,161],{"class":104},[94,740,741],{"class":96,"line":139},[94,742,118],{"emptyLinePlaceholder":117},[94,744,745,747,749,751,754],{"class":96,"line":164},[94,746,229],{"class":100},[94,748,232],{"class":104},[94,750,235],{"class":100},[94,752,753],{"class":157}," 1",[94,755,756],{"class":310},"                           # declined\n",[94,758,759,761,764,766],{"class":96,"line":169},[94,760,229],{"class":100},[94,762,763],{"class":193}," \"Really destroy\"",[94,765,249],{"class":100},[94,767,252],{"class":104},[254,769],{"name":770},"prompt-sequence",[14,772,774],{"id":773},"passwords-and-hidden-input","Passwords and hidden input",[10,776,777],{},"Hidden prompts read from the same stream but do not echo, so the answer never appears in the\ncaptured output:",[85,779,781],{"className":87,"code":780,"language":89,"meta":90,"style":90},"def test_login_accepts_a_password():\n    result = runner.invoke(app, [\"login\", \"--user\", \"ana\"], input=\"hunter2\\n\")\n\n    assert result.exit_code == 0\n    assert \"hunter2\" not in result.stdout                  # never echoed\n    assert \"logged in as ana\" in result.stdout\n",[26,782,783,792,828,832,842,859],{"__ignoreMap":90},[94,784,785,787,790],{"class":96,"line":97},[94,786,172],{"class":100},[94,788,789],{"class":175}," test_login_accepts_a_password",[94,791,179],{"class":104},[94,793,794,796,798,800,803,805,808,810,813,815,817,819,822,824,826],{"class":96,"line":114},[94,795,185],{"class":104},[94,797,145],{"class":100},[94,799,190],{"class":104},[94,801,802],{"class":193},"\"login\"",[94,804,61],{"class":104},[94,806,807],{"class":193},"\"--user\"",[94,809,61],{"class":104},[94,811,812],{"class":193},"\"ana\"",[94,814,202],{"class":104},[94,816,205],{"class":151},[94,818,145],{"class":100},[94,820,821],{"class":193},"\"hunter2",[94,823,213],{"class":157},[94,825,216],{"class":193},[94,827,161],{"class":104},[94,829,830],{"class":96,"line":121},[94,831,118],{"emptyLinePlaceholder":117},[94,833,834,836,838,840],{"class":96,"line":134},[94,835,229],{"class":100},[94,837,232],{"class":104},[94,839,235],{"class":100},[94,841,238],{"class":157},[94,843,844,846,849,851,853,856],{"class":96,"line":139},[94,845,229],{"class":100},[94,847,848],{"class":193}," \"hunter2\"",[94,850,543],{"class":100},[94,852,249],{"class":100},[94,854,855],{"class":104}," result.stdout                  ",[94,857,858],{"class":310},"# never echoed\n",[94,860,861,863,866,868],{"class":96,"line":164},[94,862,229],{"class":100},[94,864,865],{"class":193}," \"logged in as ana\"",[94,867,249],{"class":100},[94,869,252],{"class":104},[10,871,872,873,876],{},"That second assertion is worth keeping permanently. It fails the day somebody switches\n",[26,874,875],{},"hide_input=True"," off, or logs the value while debugging — which is the way secrets end up in CI\nlogs.",[10,878,879,880,883],{},"For confirmation-style password prompts (",[26,881,882],{},"confirmation_prompt=True","), supply the value twice:",[85,885,887],{"className":87,"code":886,"language":89,"meta":90,"style":90},"result = runner.invoke(app, [\"init\"], input=\"hunter2\\nhunter2\\n\")\n",[26,888,889],{"__ignoreMap":90},[94,890,891,894,896,898,901,903,905,907,909,911,914,916,918],{"class":96,"line":97},[94,892,893],{"class":104},"result ",[94,895,145],{"class":100},[94,897,190],{"class":104},[94,899,900],{"class":193},"\"init\"",[94,902,202],{"class":104},[94,904,205],{"class":151},[94,906,145],{"class":100},[94,908,821],{"class":193},[94,910,213],{"class":157},[94,912,913],{"class":193},"hunter2",[94,915,213],{"class":157},[94,917,216],{"class":193},[94,919,161],{"class":104},[14,921,923],{"id":922},"reading-piped-data-rather-than-prompting","Reading piped data rather than prompting",[10,925,926],{},"Some commands read a document from stdin rather than asking questions. The runner handles that\nidentically, and the interesting tests are the edge cases:",[85,928,930],{"className":87,"code":929,"language":89,"meta":90,"style":90},"def test_reads_a_json_document_from_stdin(cli):\n    result = cli.invoke(app, [\"apply\", \"-\"], input='{\"replicas\": 3}')\n\n    assert result.exit_code == 0\n    assert \"applied 1 change\" in result.stdout\n\ndef test_empty_stdin_is_a_clear_error(cli):\n    result = cli.invoke(app, [\"apply\", \"-\"], input=\"\")\n\n    assert result.exit_code == 65\n    assert \"no input on stdin\" in result.stderr\n",[26,931,932,941,968,972,982,993,997,1006,1031,1035,1046],{"__ignoreMap":90},[94,933,934,936,939],{"class":96,"line":97},[94,935,172],{"class":100},[94,937,938],{"class":175}," test_reads_a_json_document_from_stdin",[94,940,611],{"class":104},[94,942,943,945,947,949,952,954,957,959,961,963,966],{"class":96,"line":114},[94,944,185],{"class":104},[94,946,145],{"class":100},[94,948,430],{"class":104},[94,950,951],{"class":193},"\"apply\"",[94,953,61],{"class":104},[94,955,956],{"class":193},"\"-\"",[94,958,202],{"class":104},[94,960,205],{"class":151},[94,962,145],{"class":100},[94,964,965],{"class":193},"'{\"replicas\": 3}'",[94,967,161],{"class":104},[94,969,970],{"class":96,"line":121},[94,971,118],{"emptyLinePlaceholder":117},[94,973,974,976,978,980],{"class":96,"line":134},[94,975,229],{"class":100},[94,977,232],{"class":104},[94,979,235],{"class":100},[94,981,238],{"class":157},[94,983,984,986,989,991],{"class":96,"line":139},[94,985,229],{"class":100},[94,987,988],{"class":193}," \"applied 1 change\"",[94,990,249],{"class":100},[94,992,252],{"class":104},[94,994,995],{"class":96,"line":164},[94,996,118],{"emptyLinePlaceholder":117},[94,998,999,1001,1004],{"class":96,"line":169},[94,1000,172],{"class":100},[94,1002,1003],{"class":175}," test_empty_stdin_is_a_clear_error",[94,1005,611],{"class":104},[94,1007,1008,1010,1012,1014,1016,1018,1020,1022,1024,1026,1029],{"class":96,"line":182},[94,1009,185],{"class":104},[94,1011,145],{"class":100},[94,1013,430],{"class":104},[94,1015,951],{"class":193},[94,1017,61],{"class":104},[94,1019,956],{"class":193},[94,1021,202],{"class":104},[94,1023,205],{"class":151},[94,1025,145],{"class":100},[94,1027,1028],{"class":193},"\"\"",[94,1030,161],{"class":104},[94,1032,1033],{"class":96,"line":221},[94,1034,118],{"emptyLinePlaceholder":117},[94,1036,1037,1039,1041,1043],{"class":96,"line":226},[94,1038,229],{"class":100},[94,1040,232],{"class":104},[94,1042,235],{"class":100},[94,1044,1045],{"class":157}," 65\n",[94,1047,1048,1050,1053,1055],{"class":96,"line":241},[94,1049,229],{"class":100},[94,1051,1052],{"class":193}," \"no input on stdin\"",[94,1054,249],{"class":100},[94,1056,1057],{"class":104}," result.stderr\n",[10,1059,1060,1061,1064],{},"The empty-stream case is the one real users hit — an upstream command produced nothing — and the\ndefault failure without that check is a ",[26,1062,1063],{},"JSONDecodeError"," traceback that tells them nothing.",[14,1066,1068],{"id":1067},"ux-considerations","UX considerations",[10,1070,1071],{},"Three behaviours make a prompting command pleasant, and each is worth a test.",[10,1073,1074,1077,1078,1081,1082,1085],{},[34,1075,1076],{},"Offer a default and show it."," ",[26,1079,1080],{},"typer.confirm(\"Deploy now?\", default=False)"," renders as\n",[26,1083,1084],{},"[y\u002FN]",", which tells the user what pressing Enter does. A prompt with no default and no indication\nis a small cruelty.",[10,1087,1088,1091,1092,79],{},[34,1089,1090],{},"Re-ask on invalid input rather than failing."," Both frameworks do this for typed prompts, and it\nis worth confirming with a test that supplies a bad answer followed by a good one:\n",[26,1093,1094],{},"input=\"maybe\\ny\\n\"",[10,1096,1097,1100],{},[34,1098,1099],{},"Never prompt for something a flag could supply."," Every prompt should have a corresponding\noption, so the same command works interactively and in a script. That is the property that makes\nthe tool automatable, and the tests fall out of it naturally.",[14,1102,1104],{"id":1103},"testing-the-behaviour","Testing the behaviour",[10,1106,1107],{},"A compact suite for a prompting command looks like this:",[85,1109,1111],{"className":87,"code":1110,"language":89,"meta":90,"style":90},"@pytest.mark.parametrize(\"answer,expected_code\", [(\"y\\n\", 0), (\"n\\n\", 1)])\ndef test_confirmation_outcomes(cli, monkeypatch, answer, expected_code):\n    monkeypatch.setattr(\"mytool.console.stdin_is_a_terminal\", lambda: True)\n    assert cli.invoke(app, [\"destroy\", \"prod\"], input=answer).exit_code == expected_code\n\ndef test_yes_flag_skips_the_prompt(cli):\n    result = cli.invoke(app, [\"destroy\", \"prod\", \"--yes\"])\n    assert result.exit_code == 0\n    assert \"Really destroy\" not in result.stdout           # no question was asked\n\ndef test_no_terminal_and_no_flag_fails_fast(cli):\n    assert cli.invoke(app, [\"destroy\", \"prod\"]).exit_code == 2\n",[26,1112,1113,1155,1165,1181,1207,1211,1220,1240,1250,1266,1270,1279],{"__ignoreMap":90},[94,1114,1115,1118,1121,1124,1127,1129,1131,1133,1135,1138,1141,1143,1145,1147,1149,1152],{"class":96,"line":97},[94,1116,1117],{"class":175},"@pytest.mark.parametrize",[94,1119,1120],{"class":104},"(",[94,1122,1123],{"class":193},"\"answer,expected_code\"",[94,1125,1126],{"class":104},", [(",[94,1128,210],{"class":193},[94,1130,213],{"class":157},[94,1132,216],{"class":193},[94,1134,61],{"class":104},[94,1136,1137],{"class":157},"0",[94,1139,1140],{"class":104},"), (",[94,1142,732],{"class":193},[94,1144,213],{"class":157},[94,1146,216],{"class":193},[94,1148,61],{"class":104},[94,1150,1151],{"class":157},"1",[94,1153,1154],{"class":104},")])\n",[94,1156,1157,1159,1162],{"class":96,"line":114},[94,1158,172],{"class":100},[94,1160,1161],{"class":175}," test_confirmation_outcomes",[94,1163,1164],{"class":104},"(cli, monkeypatch, answer, expected_code):\n",[94,1166,1167,1169,1171,1173,1175,1177,1179],{"class":96,"line":121},[94,1168,689],{"class":104},[94,1170,692],{"class":193},[94,1172,61],{"class":104},[94,1174,697],{"class":100},[94,1176,700],{"class":104},[94,1178,703],{"class":157},[94,1180,161],{"class":104},[94,1182,1183,1185,1187,1189,1191,1193,1195,1197,1199,1202,1204],{"class":96,"line":134},[94,1184,229],{"class":100},[94,1186,430],{"class":104},[94,1188,194],{"class":193},[94,1190,61],{"class":104},[94,1192,199],{"class":193},[94,1194,202],{"class":104},[94,1196,205],{"class":151},[94,1198,145],{"class":100},[94,1200,1201],{"class":104},"answer).exit_code ",[94,1203,235],{"class":100},[94,1205,1206],{"class":104}," expected_code\n",[94,1208,1209],{"class":96,"line":139},[94,1210,118],{"emptyLinePlaceholder":117},[94,1212,1213,1215,1218],{"class":96,"line":164},[94,1214,172],{"class":100},[94,1216,1217],{"class":175}," test_yes_flag_skips_the_prompt",[94,1219,611],{"class":104},[94,1221,1222,1224,1226,1228,1230,1232,1234,1236,1238],{"class":96,"line":169},[94,1223,185],{"class":104},[94,1225,145],{"class":100},[94,1227,430],{"class":104},[94,1229,194],{"class":193},[94,1231,61],{"class":104},[94,1233,199],{"class":193},[94,1235,61],{"class":104},[94,1237,446],{"class":193},[94,1239,459],{"class":104},[94,1241,1242,1244,1246,1248],{"class":96,"line":182},[94,1243,229],{"class":100},[94,1245,232],{"class":104},[94,1247,235],{"class":100},[94,1249,238],{"class":157},[94,1251,1252,1254,1256,1258,1260,1263],{"class":96,"line":221},[94,1253,229],{"class":100},[94,1255,763],{"class":193},[94,1257,543],{"class":100},[94,1259,249],{"class":100},[94,1261,1262],{"class":104}," result.stdout           ",[94,1264,1265],{"class":310},"# no question was asked\n",[94,1267,1268],{"class":96,"line":226},[94,1269,118],{"emptyLinePlaceholder":117},[94,1271,1272,1274,1277],{"class":96,"line":241},[94,1273,172],{"class":100},[94,1275,1276],{"class":175}," test_no_terminal_and_no_flag_fails_fast",[94,1278,611],{"class":104},[94,1280,1282,1284,1286,1288,1290,1292,1295,1297],{"class":96,"line":1281},12,[94,1283,229],{"class":100},[94,1285,430],{"class":104},[94,1287,194],{"class":193},[94,1289,61],{"class":104},[94,1291,199],{"class":193},[94,1293,1294],{"class":104},"]).exit_code ",[94,1296,235],{"class":100},[94,1298,646],{"class":157},[10,1300,1301],{},"Four tests cover the whole surface: both answers, the bypass, and the automation path.",[14,1303,1305],{"id":1304},"conclusion","Conclusion",[10,1307,1308],{},"Prompts are testable, but they should not be a prerequisite for testing anything else. Pin the\nprompt behaviour in a couple of tests, give every prompt a flag that skips it, and make sure the\nnon-interactive path fails immediately with a message naming that flag. The result is a command\nthat is comfortable to use by hand and safe to put in a pipeline.",[14,1310,1312],{"id":1311},"frequently-asked-questions","Frequently asked questions",[1314,1315,1317],"h3",{"id":1316},"why-does-my-test-hang-instead-of-failing","Why does my test hang instead of failing?",[10,1319,1320,1321,1324,1325,1328],{},"Under the runner it should not — stdin is a captured stream, so a prompt with no input available\nraises an abort. A genuine hang usually means the command bypassed the framework and read from the\nreal terminal directly, for example with ",[26,1322,1323],{},"getpass.getpass()",", which opens ",[26,1326,1327],{},"\u002Fdev\u002Ftty"," rather than\nreading stdin.",[1314,1330,1332],{"id":1331},"how-do-i-supply-different-answers-to-different-prompts","How do I supply different answers to different prompts?",[10,1334,1335,1336,1339],{},"One line per prompt, in the order they are asked, in a single string:\n",[26,1337,1338],{},"input=\"prod\\n5\\ny\\n\"",". If the order is hard to keep straight, that is a signal the command asks\ntoo many questions — consider taking the values as options with sensible defaults instead.",[1314,1341,1343],{"id":1342},"can-i-assert-on-the-exact-prompt-text","Can I assert on the exact prompt text?",[10,1345,1346],{},"You can, since prompts echo into the captured output, but prefer asserting on a distinctive\nfragment. Pinning the whole line makes every wording improvement a test failure; asserting that the\nenvironment name appears in the question catches the mistakes that matter.",[1314,1348,1350],{"id":1349},"how-do-i-test-a-rich-prompt","How do I test a Rich prompt?",[10,1352,1353,1354,1357,1358,1361],{},"The same way — ",[26,1355,1356],{},"rich.prompt.Prompt.ask"," reads from the console's input stream, which the runner\nreplaces. If you constructed the console with an explicit ",[26,1359,1360],{},"file=",", pass the console in as a\nparameter so tests can supply one wired to a string buffer.",[1314,1363,1365],{"id":1364},"should-confirmation-be-a-prompt-or-a-flag","Should confirmation be a prompt or a flag?",[10,1367,1368,1369,1371],{},"Both. Destructive commands deserve a confirmation for interactive users and a ",[26,1370,475],{}," for\nautomation, and the flag should be the only way to proceed when there is no terminal. A tool that\ncan only be used interactively is a tool nobody can schedule, and being able to schedule the tool is usually most of the reason it exists at all.",[1314,1373,1375],{"id":1374},"should-the-confirmation-prompt-appear-in-the-tests-captured-output","Should the confirmation prompt appear in the tests' captured output?",[10,1377,1378],{},"Yes, and asserting on a fragment of it is worthwhile — it proves the question was actually asked\nrather than skipped by a default that changed. Keep the assertion to the distinctive part, such as\nthe resource name, so that rewording the prompt does not break it.",[1314,1380,1382],{"id":1381},"what-if-a-command-prompts-only-when-a-value-is-missing","What if a command prompts only when a value is missing?",[10,1384,1385],{},"Test both branches: one invocation supplying the value as a flag, asserting no prompt appeared,\nand one supplying it through stdin. That pair pins the rule \"prompt only for what was not given\",\nwhich is exactly the behaviour that makes the command usable in a pipeline.",[14,1387,1389],{"id":1388},"related","Related",[19,1391,1392,1400,1407,1413,1420],{},[22,1393,1394,1395],{},"Up: ",[1396,1397,1399],"a",{"href":1398},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002F","Testing Python CLI applications",[22,1401,1402,1403],{},"Sideways: ",[1396,1404,1406],{"href":1405},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner\u002F","Testing Click commands with CliRunner",[22,1408,1402,1409],{},[1396,1410,1412],{"href":1411},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmocking-filesystem-and-network-in-cli-tests\u002F","Mocking the filesystem and network in CLI tests",[22,1414,1415,1416],{},"Related: ",[1396,1417,1419],{"href":1418},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002F","Interactive terminal UI with Rich",[22,1421,1415,1422],{},[1396,1423,1425],{"href":1424},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks\u002F","Friendly error messages and tracebacks",[1427,1428,1429],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":90,"searchDepth":114,"depth":114,"links":1431},[1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1451],{"id":16,"depth":114,"text":17},{"id":53,"depth":114,"text":54},{"id":82,"depth":114,"text":83},{"id":339,"depth":114,"text":340},{"id":479,"depth":114,"text":480},{"id":773,"depth":114,"text":774},{"id":922,"depth":114,"text":923},{"id":1067,"depth":114,"text":1068},{"id":1103,"depth":114,"text":1104},{"id":1304,"depth":114,"text":1305},{"id":1311,"depth":114,"text":1312,"children":1443},[1444,1445,1446,1447,1448,1449,1450],{"id":1316,"depth":121,"text":1317},{"id":1331,"depth":121,"text":1332},{"id":1342,"depth":121,"text":1343},{"id":1349,"depth":121,"text":1350},{"id":1364,"depth":121,"text":1365},{"id":1374,"depth":121,"text":1375},{"id":1381,"depth":121,"text":1382},{"id":1388,"depth":114,"text":1389},"2026-08-01","Test Python CLI prompts, confirmations, passwords, and piped input - supply stdin through the runner and cover the non-interactive path properly.","intermediate",false,"md",{},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin",{"title":5,"description":1453},"modern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin\u002Findex",[1462,1463,1464,1465,1466],"testing","prompts","stdin","clirunner","pytest","S8W8PxqsRPeEJDIqqHEis_oE4yWKdRIuKBurPjwlcVM",[1469,1472,1475,1478,1481,1484,1487,1490,1493,1496,1499,1502,1505,1508,1511,1514,1517,1520,1523,1526,1529,1532,1535,1538,1541,1544,1547,1550,1553,1556,1559,1562,1565,1568,1571,1574,1577,1580,1583,1586,1589,1592,1595,1598,1601,1604,1607,1610,1613,1616,1619,1622,1625,1628,1629,1632,1635,1638,1641,1644,1647,1650,1653,1656,1659,1662,1665,1668,1671,1674,1677,1680,1683,1686,1689,1692,1695,1698,1701,1704,1707,1710,1713],{"path":1470,"title":1471},"\u002Fabout","About Python CLI Toolcraft",{"path":1473,"title":1474},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1476,"title":1477},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1479,"title":1480},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1482,"title":1483},"\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":1485,"title":1486},"\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":1488,"title":1489},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1491,"title":1492},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1494,"title":1495},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1497,"title":1498},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1500,"title":1501},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1503,"title":1504},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1506,"title":1507},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1509,"title":1510},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1512,"title":1513},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1515,"title":1516},"\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":1518,"title":1519},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1521,"title":1522},"\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":1524,"title":1525},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1527,"title":1528},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1530,"title":1531},"\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":1533,"title":1534},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1536,"title":1537},"\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":1539,"title":1540},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1542,"title":1543},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1545,"title":1546},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1548,"title":1549},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1551,"title":1552},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1554,"title":1555},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1557,"title":1558},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1560,"title":1561},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1563,"title":1564},"\u002F","Python CLI Toolcraft",{"path":1566,"title":1567},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1569,"title":1570},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1572,"title":1573},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1575,"title":1576},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1578,"title":1579},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1581,"title":1582},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1584,"title":1585},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1587,"title":1588},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1590,"title":1591},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1593,"title":1594},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1596,"title":1597},"\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":1599,"title":1600},"\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":1602,"title":1603},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1605,"title":1606},"\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":1608,"title":1609},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1611,"title":1612},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1614,"title":1615},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1617,"title":1618},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1620,"title":1621},"\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":1623,"title":1624},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1626,"title":1627},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1458,"title":5},{"path":1630,"title":1631},"\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":1633,"title":1634},"\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":1636,"title":1637},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1639,"title":1640},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1642,"title":1643},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1645,"title":1646},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1648,"title":1649},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1651,"title":1652},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1654,"title":1655},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1657,"title":1658},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1660,"title":1661},"\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":1663,"title":1664},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1666,"title":1667},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1669,"title":1670},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1672,"title":1673},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1675,"title":1676},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1678,"title":1679},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1681,"title":1682},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1684,"title":1685},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1687,"title":1688},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1690,"title":1691},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1693,"title":1694},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1696,"title":1697},"\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":1699,"title":1700},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1702,"title":1703},"\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":1705,"title":1706},"\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":1708,"title":1709},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1711,"title":1712},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1714,"title":1715},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690033]