[{"data":1,"prerenderedAt":1774},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner\u002F":3,"content-directory":1528},{"id":4,"title":5,"body":6,"date":1512,"description":1513,"difficulty":1514,"draft":1515,"extension":1516,"meta":1517,"navigation":161,"path":1518,"seo":1519,"stem":1520,"tags":1521,"updated":1512,"__hash__":1527},"content\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner\u002Findex.md","Testing Click Commands with CliRunner",{"type":7,"value":8,"toc":1488},"minimark",[9,17,22,80,84,87,134,137,141,320,324,346,353,433,439,472,476,479,574,589,592,596,602,666,669,672,675,736,745,749,752,873,883,887,890,1035,1038,1042,1045,1054,1102,1112,1176,1180,1187,1302,1321,1325,1330,1334,1339,1345,1349,1366,1370,1377,1381,1392,1400,1414,1422,1434,1438,1444,1448,1484],[10,11,12,16],"p",{},[13,14,15],"code",{},"CliRunner"," invokes a command the way a user would — a real argument list, real parsing, real\nexit code — without spawning a process. It is the single most useful testing tool in either\nframework, and most of the friction people hit with it comes from three or four behaviours that\nare easy to miss.",[18,19,21],"h2",{"id":20},"tldr","TL;DR",[23,24,25,36,51,63,70],"ul",{},[26,27,28,31,32,35],"li",{},[13,29,30],{},"runner.invoke(app, [\"sync\", \"--dry-run\", \"data\"])"," runs in-process; assert on\n",[13,33,34],{},"result.exit_code"," first.",[26,37,38,39,42,43,46,47,50],{},"Construct with ",[13,40,41],{},"CliRunner(mix_stderr=False)"," so ",[13,44,45],{},"result.stdout"," and ",[13,48,49],{},"result.stderr"," are\nseparate.",[26,52,53,54,58,59,62],{},"The runner ",[55,56,57],"strong",{},"never raises",": an escaped exception becomes ",[13,60,61],{},"result.exception"," with exit code 1.",[26,64,65,66,69],{},"Use ",[13,67,68],{},"catch_exceptions=False"," when debugging to get the real traceback.",[26,71,72,75,76,79],{},[13,73,74],{},"runner.isolated_filesystem()"," for commands that write relative paths; ",[13,77,78],{},"tmp_path"," otherwise.",[18,81,83],{"id":82},"prerequisites","Prerequisites",[10,85,86],{},"A Click or Typer application with at least one command, and pytest. Typer re-exports the runner,\nso the import differs but nothing else does:",[88,89,94],"pre",{"className":90,"code":91,"language":92,"meta":93,"style":93},"language-python shiki shiki-themes github-light github-dark","from typer.testing import CliRunner    # Typer\nfrom click.testing import CliRunner    # Click\n","python","",[13,95,96,119],{"__ignoreMap":93},[97,98,101,105,109,112,115],"span",{"class":99,"line":100},"line",1,[97,102,104],{"class":103},"szBVR","from",[97,106,108],{"class":107},"sVt8B"," typer.testing ",[97,110,111],{"class":103},"import",[97,113,114],{"class":107}," CliRunner    ",[97,116,118],{"class":117},"sJ8bj","# Typer\n",[97,120,122,124,127,129,131],{"class":99,"line":121},2,[97,123,104],{"class":103},[97,125,126],{"class":107}," click.testing ",[97,128,111],{"class":103},[97,130,114],{"class":107},[97,132,133],{"class":117},"# Click\n",[10,135,136],{},"Everything in this guide applies to both.",[18,138,140],{"id":139},"the-result-object","The result object",[88,142,144],{"className":90,"code":143,"language":92,"meta":93,"style":93},"from typer.testing import CliRunner\n\nfrom mytool.cli import app\n\nrunner = CliRunner(mix_stderr=False)\n\ndef test_sync_reports_uploads(tmp_path):\n    (tmp_path \u002F \"a.txt\").write_text(\"x\")\n\n    result = runner.invoke(app, [\"sync\", str(tmp_path)])\n\n    assert result.exit_code == 0\n    assert \"1 uploaded\" in result.stdout\n    assert result.stderr == \"\"\n",[13,145,146,157,163,176,181,206,211,224,245,250,273,278,293,307],{"__ignoreMap":93},[97,147,148,150,152,154],{"class":99,"line":100},[97,149,104],{"class":103},[97,151,108],{"class":107},[97,153,111],{"class":103},[97,155,156],{"class":107}," CliRunner\n",[97,158,159],{"class":99,"line":121},[97,160,162],{"emptyLinePlaceholder":161},true,"\n",[97,164,166,168,171,173],{"class":99,"line":165},3,[97,167,104],{"class":103},[97,169,170],{"class":107}," mytool.cli ",[97,172,111],{"class":103},[97,174,175],{"class":107}," app\n",[97,177,179],{"class":99,"line":178},4,[97,180,162],{"emptyLinePlaceholder":161},[97,182,184,187,190,193,197,199,203],{"class":99,"line":183},5,[97,185,186],{"class":107},"runner ",[97,188,189],{"class":103},"=",[97,191,192],{"class":107}," CliRunner(",[97,194,196],{"class":195},"s4XuR","mix_stderr",[97,198,189],{"class":103},[97,200,202],{"class":201},"sj4cs","False",[97,204,205],{"class":107},")\n",[97,207,209],{"class":99,"line":208},6,[97,210,162],{"emptyLinePlaceholder":161},[97,212,214,217,221],{"class":99,"line":213},7,[97,215,216],{"class":103},"def",[97,218,220],{"class":219},"sScJk"," test_sync_reports_uploads",[97,222,223],{"class":107},"(tmp_path):\n",[97,225,227,230,233,237,240,243],{"class":99,"line":226},8,[97,228,229],{"class":107},"    (tmp_path ",[97,231,232],{"class":103},"\u002F",[97,234,236],{"class":235},"sZZnC"," \"a.txt\"",[97,238,239],{"class":107},").write_text(",[97,241,242],{"class":235},"\"x\"",[97,244,205],{"class":107},[97,246,248],{"class":99,"line":247},9,[97,249,162],{"emptyLinePlaceholder":161},[97,251,253,256,258,261,264,267,270],{"class":99,"line":252},10,[97,254,255],{"class":107},"    result ",[97,257,189],{"class":103},[97,259,260],{"class":107}," runner.invoke(app, [",[97,262,263],{"class":235},"\"sync\"",[97,265,266],{"class":107},", ",[97,268,269],{"class":201},"str",[97,271,272],{"class":107},"(tmp_path)])\n",[97,274,276],{"class":99,"line":275},11,[97,277,162],{"emptyLinePlaceholder":161},[97,279,281,284,287,290],{"class":99,"line":280},12,[97,282,283],{"class":103},"    assert",[97,285,286],{"class":107}," result.exit_code ",[97,288,289],{"class":103},"==",[97,291,292],{"class":201}," 0\n",[97,294,296,298,301,304],{"class":99,"line":295},13,[97,297,283],{"class":103},[97,299,300],{"class":235}," \"1 uploaded\"",[97,302,303],{"class":103}," in",[97,305,306],{"class":107}," result.stdout\n",[97,308,310,312,315,317],{"class":99,"line":309},14,[97,311,283],{"class":103},[97,313,314],{"class":107}," result.stderr ",[97,316,289],{"class":103},[97,318,319],{"class":235}," \"\"\n",[321,322],"inline-diagram",{"name":323},"clirunner-result-object",[10,325,326,327,330,331,334,335,338,339,342,343,345],{},"The property that surprises people: ",[55,328,329],{},"the runner does not raise",". If your command throws an\nunhandled ",[13,332,333],{},"KeyError",", the test does not error out with a traceback — it gets a result whose\n",[13,336,337],{},"exit_code"," is 1 and whose ",[13,340,341],{},"exception"," holds the ",[13,344,333],{},". A test that only asserts on output\nwill fail with a confusing message about a missing string, and the actual cause is one attribute\naway.",[10,347,348,349,352],{},"That is why the exit-code assertion goes first, and why this helper is worth three lines in\n",[13,350,351],{},"conftest.py",":",[88,354,356],{"className":90,"code":355,"language":92,"meta":93,"style":93},"def invoke(runner, app, argv, **kwargs):\n    result = runner.invoke(app, argv, **kwargs)\n    if result.exception and not isinstance(result.exception, SystemExit):\n        raise result.exception          # surface the real traceback\n    return result\n",[13,357,358,374,388,414,425],{"__ignoreMap":93},[97,359,360,362,365,368,371],{"class":99,"line":100},[97,361,216],{"class":103},[97,363,364],{"class":219}," invoke",[97,366,367],{"class":107},"(runner, app, argv, ",[97,369,370],{"class":103},"**",[97,372,373],{"class":107},"kwargs):\n",[97,375,376,378,380,383,385],{"class":99,"line":121},[97,377,255],{"class":107},[97,379,189],{"class":103},[97,381,382],{"class":107}," runner.invoke(app, argv, ",[97,384,370],{"class":103},[97,386,387],{"class":107},"kwargs)\n",[97,389,390,393,396,399,402,405,408,411],{"class":99,"line":165},[97,391,392],{"class":103},"    if",[97,394,395],{"class":107}," result.exception ",[97,397,398],{"class":103},"and",[97,400,401],{"class":103}," not",[97,403,404],{"class":201}," isinstance",[97,406,407],{"class":107},"(result.exception, ",[97,409,410],{"class":201},"SystemExit",[97,412,413],{"class":107},"):\n",[97,415,416,419,422],{"class":99,"line":178},[97,417,418],{"class":103},"        raise",[97,420,421],{"class":107}," result.exception          ",[97,423,424],{"class":117},"# surface the real traceback\n",[97,426,427,430],{"class":99,"line":183},[97,428,429],{"class":103},"    return",[97,431,432],{"class":107}," result\n",[10,434,435,436,438],{},"When you are actively debugging, ",[13,437,68],{}," does the same thing for one call:",[88,440,442],{"className":90,"code":441,"language":92,"meta":93,"style":93},"result = runner.invoke(app, [\"sync\", \"data\"], catch_exceptions=False)\n",[13,443,444],{"__ignoreMap":93},[97,445,446,449,451,453,455,457,460,463,466,468,470],{"class":99,"line":100},[97,447,448],{"class":107},"result ",[97,450,189],{"class":103},[97,452,260],{"class":107},[97,454,263],{"class":235},[97,456,266],{"class":107},[97,458,459],{"class":235},"\"data\"",[97,461,462],{"class":107},"], ",[97,464,465],{"class":195},"catch_exceptions",[97,467,189],{"class":103},[97,469,202],{"class":201},[97,471,205],{"class":107},[18,473,475],{"id":474},"streams-and-why-they-must-be-separate","Streams, and why they must be separate",[10,477,478],{},"By default the runner mixes stderr into stdout, which makes it impossible to assert that an error\nmessage went to the right place. Since that is a real property scripts depend on, turn it off:",[88,480,482],{"className":90,"code":481,"language":92,"meta":93,"style":93},"runner = CliRunner(mix_stderr=False)\n\ndef test_missing_source_reports_on_stderr():\n    result = runner.invoke(app, [\"sync\", \"\u002Fdoes\u002Fnot\u002Fexist\"])\n\n    assert result.exit_code == 66\n    assert result.stdout == \"\"                      # nothing contaminated the results stream\n    assert \"does not exist\" in result.stderr\n",[13,483,484,500,504,514,532,536,547,562],{"__ignoreMap":93},[97,485,486,488,490,492,494,496,498],{"class":99,"line":100},[97,487,186],{"class":107},[97,489,189],{"class":103},[97,491,192],{"class":107},[97,493,196],{"class":195},[97,495,189],{"class":103},[97,497,202],{"class":201},[97,499,205],{"class":107},[97,501,502],{"class":99,"line":121},[97,503,162],{"emptyLinePlaceholder":161},[97,505,506,508,511],{"class":99,"line":165},[97,507,216],{"class":103},[97,509,510],{"class":219}," test_missing_source_reports_on_stderr",[97,512,513],{"class":107},"():\n",[97,515,516,518,520,522,524,526,529],{"class":99,"line":178},[97,517,255],{"class":107},[97,519,189],{"class":103},[97,521,260],{"class":107},[97,523,263],{"class":235},[97,525,266],{"class":107},[97,527,528],{"class":235},"\"\u002Fdoes\u002Fnot\u002Fexist\"",[97,530,531],{"class":107},"])\n",[97,533,534],{"class":99,"line":183},[97,535,162],{"emptyLinePlaceholder":161},[97,537,538,540,542,544],{"class":99,"line":208},[97,539,283],{"class":103},[97,541,286],{"class":107},[97,543,289],{"class":103},[97,545,546],{"class":201}," 66\n",[97,548,549,551,554,556,559],{"class":99,"line":213},[97,550,283],{"class":103},[97,552,553],{"class":107}," result.stdout ",[97,555,289],{"class":103},[97,557,558],{"class":235}," \"\"",[97,560,561],{"class":117},"                      # nothing contaminated the results stream\n",[97,563,564,566,569,571],{"class":99,"line":226},[97,565,283],{"class":103},[97,567,568],{"class":235}," \"does not exist\"",[97,570,303],{"class":103},[97,572,573],{"class":107}," result.stderr\n",[10,575,576,577,580,581,584,585,588],{},"The ",[13,578,579],{},"result.stdout == \"\""," assertion is the valuable one. It catches the stray ",[13,582,583],{},"print()"," that\nsomeone added while debugging and never removed — the kind of change that silently breaks\n",[13,586,587],{},"mytool export > data.json"," for every user.",[321,590],{"name":591},"runner-invocation-matrix",[18,593,595],{"id":594},"isolated-filesystems","Isolated filesystems",[10,597,598,599,601],{},"Commands that take explicit paths are easiest to test with ",[13,600,78],{},". Commands that write\nrelative to the working directory want the runner's own context manager:",[88,603,605],{"className":90,"code":604,"language":92,"meta":93,"style":93},"def test_init_creates_a_config_in_the_current_directory():\n    with runner.isolated_filesystem():\n        result = runner.invoke(app, [\"init\"])\n\n        assert result.exit_code == 0\n        assert Path(\".mytool.toml\").is_file()\n",[13,606,607,616,624,638,642,653],{"__ignoreMap":93},[97,608,609,611,614],{"class":99,"line":100},[97,610,216],{"class":103},[97,612,613],{"class":219}," test_init_creates_a_config_in_the_current_directory",[97,615,513],{"class":107},[97,617,618,621],{"class":99,"line":121},[97,619,620],{"class":103},"    with",[97,622,623],{"class":107}," runner.isolated_filesystem():\n",[97,625,626,629,631,633,636],{"class":99,"line":165},[97,627,628],{"class":107},"        result ",[97,630,189],{"class":103},[97,632,260],{"class":107},[97,634,635],{"class":235},"\"init\"",[97,637,531],{"class":107},[97,639,640],{"class":99,"line":178},[97,641,162],{"emptyLinePlaceholder":161},[97,643,644,647,649,651],{"class":99,"line":183},[97,645,646],{"class":103},"        assert",[97,648,286],{"class":107},[97,650,289],{"class":103},[97,652,292],{"class":201},[97,654,655,657,660,663],{"class":99,"line":208},[97,656,646],{"class":103},[97,658,659],{"class":107}," Path(",[97,661,662],{"class":235},"\".mytool.toml\"",[97,664,665],{"class":107},").is_file()\n",[321,667],{"name":668},"isolated-filesystem-flow",[10,670,671],{},"It creates a temporary directory, changes into it for the duration of the block, and removes it\nafterwards. Two caveats worth knowing: the directory is gone once the block exits, so assert\ninside it; and it changes the process working directory, so it does not compose with tests that\nrun commands in parallel threads.",[10,673,674],{},"For most projects a fixture combining both is the comfortable middle ground:",[88,676,678],{"className":90,"code":677,"language":92,"meta":93,"style":93},"@pytest.fixture\ndef cli(tmp_path, monkeypatch):\n    monkeypatch.chdir(tmp_path)                     # relative paths land in tmp_path\n    monkeypatch.delenv(\"MYTOOL_RETRIES\", raising=False)\n    return CliRunner(mix_stderr=False)\n",[13,679,680,685,695,703,722],{"__ignoreMap":93},[97,681,682],{"class":99,"line":100},[97,683,684],{"class":219},"@pytest.fixture\n",[97,686,687,689,692],{"class":99,"line":121},[97,688,216],{"class":103},[97,690,691],{"class":219}," cli",[97,693,694],{"class":107},"(tmp_path, monkeypatch):\n",[97,696,697,700],{"class":99,"line":165},[97,698,699],{"class":107},"    monkeypatch.chdir(tmp_path)                     ",[97,701,702],{"class":117},"# relative paths land in tmp_path\n",[97,704,705,708,711,713,716,718,720],{"class":99,"line":178},[97,706,707],{"class":107},"    monkeypatch.delenv(",[97,709,710],{"class":235},"\"MYTOOL_RETRIES\"",[97,712,266],{"class":107},[97,714,715],{"class":195},"raising",[97,717,189],{"class":103},[97,719,202],{"class":201},[97,721,205],{"class":107},[97,723,724,726,728,730,732,734],{"class":99,"line":183},[97,725,429],{"class":103},[97,727,192],{"class":107},[97,729,196],{"class":195},[97,731,189],{"class":103},[97,733,202],{"class":201},[97,735,205],{"class":107},[10,737,738,741,742,744],{},[13,739,740],{},"monkeypatch.chdir"," is undone automatically, the directory survives for post-test assertions, and\n",[13,743,78],{}," is printed in the failure output when something goes wrong.",[18,746,748],{"id":747},"environment-and-input","Environment and input",[10,750,751],{},"Both are constructor or call arguments rather than something to patch:",[88,753,755],{"className":90,"code":754,"language":92,"meta":93,"style":93},"def test_environment_variable_is_read(cli):\n    result = cli.invoke(app, [\"config\", \"show\"], env={\"MYTOOL_RETRIES\": \"9\"})\n    assert \"retries 9\" in result.stdout\n\ndef test_confirmation_can_be_answered(cli):\n    result = cli.invoke(app, [\"destroy\", \"prod\"], input=\"y\\n\")\n    assert result.exit_code == 0\n",[13,756,757,767,805,816,820,829,863],{"__ignoreMap":93},[97,758,759,761,764],{"class":99,"line":100},[97,760,216],{"class":103},[97,762,763],{"class":219}," test_environment_variable_is_read",[97,765,766],{"class":107},"(cli):\n",[97,768,769,771,773,776,779,781,784,786,789,791,794,796,799,802],{"class":99,"line":121},[97,770,255],{"class":107},[97,772,189],{"class":103},[97,774,775],{"class":107}," cli.invoke(app, [",[97,777,778],{"class":235},"\"config\"",[97,780,266],{"class":107},[97,782,783],{"class":235},"\"show\"",[97,785,462],{"class":107},[97,787,788],{"class":195},"env",[97,790,189],{"class":103},[97,792,793],{"class":107},"{",[97,795,710],{"class":235},[97,797,798],{"class":107},": ",[97,800,801],{"class":235},"\"9\"",[97,803,804],{"class":107},"})\n",[97,806,807,809,812,814],{"class":99,"line":165},[97,808,283],{"class":103},[97,810,811],{"class":235}," \"retries 9\"",[97,813,303],{"class":103},[97,815,306],{"class":107},[97,817,818],{"class":99,"line":178},[97,819,162],{"emptyLinePlaceholder":161},[97,821,822,824,827],{"class":99,"line":183},[97,823,216],{"class":103},[97,825,826],{"class":219}," test_confirmation_can_be_answered",[97,828,766],{"class":107},[97,830,831,833,835,837,840,842,845,847,850,852,855,858,861],{"class":99,"line":208},[97,832,255],{"class":107},[97,834,189],{"class":103},[97,836,775],{"class":107},[97,838,839],{"class":235},"\"destroy\"",[97,841,266],{"class":107},[97,843,844],{"class":235},"\"prod\"",[97,846,462],{"class":107},[97,848,849],{"class":195},"input",[97,851,189],{"class":103},[97,853,854],{"class":235},"\"y",[97,856,857],{"class":201},"\\n",[97,859,860],{"class":235},"\"",[97,862,205],{"class":107},[97,864,865,867,869,871],{"class":99,"line":213},[97,866,283],{"class":103},[97,868,286],{"class":107},[97,870,289],{"class":103},[97,872,292],{"class":201},[10,874,875,876,878,879,882],{},"Supplying ",[13,877,788],{}," sets those variables for the invocation only. It does not clear the rest of the\nenvironment, which is why a fixture that deletes your tool's variables is still worth having —\notherwise a developer with ",[13,880,881],{},"MYTOOL_REGION"," exported gets different results from CI.",[18,884,886],{"id":885},"testing-a-groups-callback","Testing a group's callback",[10,888,889],{},"Global options live on the callback, and it runs on every invocation. The way to test what it\nproduced is to look at what a command received:",[88,891,893],{"className":90,"code":892,"language":92,"meta":93,"style":93},"def test_config_flag_populates_the_context(cli, tmp_path):\n    (tmp_path \u002F \"custom.toml\").write_text('region = \"us-east-1\"\\n')\n    seen = {}\n\n    @app.command()\n    def _probe(ctx: typer.Context) -> None:          # a test-only command\n        seen[\"region\"] = ctx.obj.region\n\n    cli.invoke(app, [\"--config\", str(tmp_path \u002F \"custom.toml\"), \"_probe\"])\n\n    assert seen[\"region\"] == \"us-east-1\"\n",[13,894,895,905,926,936,940,948,968,984,988,1015,1019],{"__ignoreMap":93},[97,896,897,899,902],{"class":99,"line":100},[97,898,216],{"class":103},[97,900,901],{"class":219}," test_config_flag_populates_the_context",[97,903,904],{"class":107},"(cli, tmp_path):\n",[97,906,907,909,911,914,916,919,921,924],{"class":99,"line":121},[97,908,229],{"class":107},[97,910,232],{"class":103},[97,912,913],{"class":235}," \"custom.toml\"",[97,915,239],{"class":107},[97,917,918],{"class":235},"'region = \"us-east-1\"",[97,920,857],{"class":201},[97,922,923],{"class":235},"'",[97,925,205],{"class":107},[97,927,928,931,933],{"class":99,"line":165},[97,929,930],{"class":107},"    seen ",[97,932,189],{"class":103},[97,934,935],{"class":107}," {}\n",[97,937,938],{"class":99,"line":178},[97,939,162],{"emptyLinePlaceholder":161},[97,941,942,945],{"class":99,"line":183},[97,943,944],{"class":219},"    @app.command",[97,946,947],{"class":107},"()\n",[97,949,950,953,956,959,962,965],{"class":99,"line":208},[97,951,952],{"class":103},"    def",[97,954,955],{"class":219}," _probe",[97,957,958],{"class":107},"(ctx: typer.Context) -> ",[97,960,961],{"class":201},"None",[97,963,964],{"class":107},":          ",[97,966,967],{"class":117},"# a test-only command\n",[97,969,970,973,976,979,981],{"class":99,"line":213},[97,971,972],{"class":107},"        seen[",[97,974,975],{"class":235},"\"region\"",[97,977,978],{"class":107},"] ",[97,980,189],{"class":103},[97,982,983],{"class":107}," ctx.obj.region\n",[97,985,986],{"class":99,"line":226},[97,987,162],{"emptyLinePlaceholder":161},[97,989,990,993,996,998,1000,1003,1005,1007,1010,1013],{"class":99,"line":247},[97,991,992],{"class":107},"    cli.invoke(app, [",[97,994,995],{"class":235},"\"--config\"",[97,997,266],{"class":107},[97,999,269],{"class":201},[97,1001,1002],{"class":107},"(tmp_path ",[97,1004,232],{"class":103},[97,1006,913],{"class":235},[97,1008,1009],{"class":107},"), ",[97,1011,1012],{"class":235},"\"_probe\"",[97,1014,531],{"class":107},[97,1016,1017],{"class":99,"line":252},[97,1018,162],{"emptyLinePlaceholder":161},[97,1020,1021,1023,1026,1028,1030,1032],{"class":99,"line":275},[97,1022,283],{"class":103},[97,1024,1025],{"class":107}," seen[",[97,1027,975],{"class":235},[97,1029,978],{"class":107},[97,1031,289],{"class":103},[97,1033,1034],{"class":235}," \"us-east-1\"\n",[10,1036,1037],{},"Adding a probe command inside a test is slightly unusual and much clearer than reaching into\nClick's internals. The alternative — asserting on the output of a real command that happens to\nprint the region — couples the test to that command's formatting.",[18,1039,1041],{"id":1040},"ux-considerations","UX considerations",[10,1043,1044],{},"Two behaviours are worth pinning because they are what users meet first.",[10,1046,1047,1053],{},[55,1048,1049,1052],{},[13,1050,1051],{},"--help"," must work with no configuration present."," It is the most common first invocation, and\na tool that needs a valid config file to print help is frustrating in exactly the moment someone is\ntrying to learn it:",[88,1055,1057],{"className":90,"code":1056,"language":92,"meta":93,"style":93},"def test_help_works_without_config(cli):\n    result = cli.invoke(app, [\"--help\"])\n    assert result.exit_code == 0\n    assert \"sync\" in result.stdout\n",[13,1058,1059,1068,1081,1091],{"__ignoreMap":93},[97,1060,1061,1063,1066],{"class":99,"line":100},[97,1062,216],{"class":103},[97,1064,1065],{"class":219}," test_help_works_without_config",[97,1067,766],{"class":107},[97,1069,1070,1072,1074,1076,1079],{"class":99,"line":121},[97,1071,255],{"class":107},[97,1073,189],{"class":103},[97,1075,775],{"class":107},[97,1077,1078],{"class":235},"\"--help\"",[97,1080,531],{"class":107},[97,1082,1083,1085,1087,1089],{"class":99,"line":165},[97,1084,283],{"class":103},[97,1086,286],{"class":107},[97,1088,289],{"class":103},[97,1090,292],{"class":201},[97,1092,1093,1095,1098,1100],{"class":99,"line":178},[97,1094,283],{"class":103},[97,1096,1097],{"class":235}," \"sync\"",[97,1099,303],{"class":103},[97,1101,306],{"class":107},[10,1103,1104,1107,1108,1111],{},[55,1105,1106],{},"Usage errors must exit 2."," Both frameworks do this by default; the test exists to catch the\nday someone wraps the app in a ",[13,1109,1110],{},"try"," that swallows it:",[88,1113,1115],{"className":90,"code":1114,"language":92,"meta":93,"style":93},"@pytest.mark.parametrize(\"argv\", [[\"sync\"], [\"sync\", \"data\", \"--retries\", \"x\"]])\ndef test_usage_errors_exit_2(cli, argv):\n    assert cli.invoke(app, argv).exit_code == 2\n",[13,1116,1117,1154,1164],{"__ignoreMap":93},[97,1118,1119,1122,1125,1128,1131,1133,1136,1138,1140,1142,1144,1147,1149,1151],{"class":99,"line":100},[97,1120,1121],{"class":219},"@pytest.mark.parametrize",[97,1123,1124],{"class":107},"(",[97,1126,1127],{"class":235},"\"argv\"",[97,1129,1130],{"class":107},", [[",[97,1132,263],{"class":235},[97,1134,1135],{"class":107},"], [",[97,1137,263],{"class":235},[97,1139,266],{"class":107},[97,1141,459],{"class":235},[97,1143,266],{"class":107},[97,1145,1146],{"class":235},"\"--retries\"",[97,1148,266],{"class":107},[97,1150,242],{"class":235},[97,1152,1153],{"class":107},"]])\n",[97,1155,1156,1158,1161],{"class":99,"line":121},[97,1157,216],{"class":103},[97,1159,1160],{"class":219}," test_usage_errors_exit_2",[97,1162,1163],{"class":107},"(cli, argv):\n",[97,1165,1166,1168,1171,1173],{"class":99,"line":165},[97,1167,283],{"class":103},[97,1169,1170],{"class":107}," cli.invoke(app, argv).exit_code ",[97,1172,289],{"class":103},[97,1174,1175],{"class":201}," 2\n",[18,1177,1179],{"id":1178},"testing-the-behaviour","Testing the behaviour",[10,1181,1182,1183,1186],{},"Keep runner tests to the interface and push the rules down. A useful heuristic: if a test's name\ndescribes a domain rule (\"skips files that are already current\"), it should not need ",[13,1184,1185],{},"invoke",". If\nit describes an interface fact (\"--dry-run reaches the core\", \"a missing file exits 66\"), the\nrunner is the right tool.",[88,1188,1190],{"className":90,"code":1189,"language":92,"meta":93,"style":93},"def test_dry_run_flag_reaches_the_core(cli, monkeypatch):\n    seen = {}\n    monkeypatch.setattr(\n        \"mytool.commands.sync.core.sync_directory\",\n        lambda source, **kwargs: seen.update(kwargs) or SyncResult(0, 0),\n    )\n\n    assert cli.invoke(app, [\"sync\", \"data\", \"--dry-run\"]).exit_code == 0\n    assert seen[\"dry_run\"] is True\n",[13,1191,1192,1202,1210,1215,1223,1252,1257,1261,1285],{"__ignoreMap":93},[97,1193,1194,1196,1199],{"class":99,"line":100},[97,1195,216],{"class":103},[97,1197,1198],{"class":219}," test_dry_run_flag_reaches_the_core",[97,1200,1201],{"class":107},"(cli, monkeypatch):\n",[97,1203,1204,1206,1208],{"class":99,"line":121},[97,1205,930],{"class":107},[97,1207,189],{"class":103},[97,1209,935],{"class":107},[97,1211,1212],{"class":99,"line":165},[97,1213,1214],{"class":107},"    monkeypatch.setattr(\n",[97,1216,1217,1220],{"class":99,"line":178},[97,1218,1219],{"class":235},"        \"mytool.commands.sync.core.sync_directory\"",[97,1221,1222],{"class":107},",\n",[97,1224,1225,1228,1231,1233,1236,1239,1242,1245,1247,1249],{"class":99,"line":183},[97,1226,1227],{"class":103},"        lambda",[97,1229,1230],{"class":107}," source, ",[97,1232,370],{"class":103},[97,1234,1235],{"class":107},"kwargs: seen.update(kwargs) ",[97,1237,1238],{"class":103},"or",[97,1240,1241],{"class":107}," SyncResult(",[97,1243,1244],{"class":201},"0",[97,1246,266],{"class":107},[97,1248,1244],{"class":201},[97,1250,1251],{"class":107},"),\n",[97,1253,1254],{"class":99,"line":208},[97,1255,1256],{"class":107},"    )\n",[97,1258,1259],{"class":99,"line":213},[97,1260,162],{"emptyLinePlaceholder":161},[97,1262,1263,1265,1267,1269,1271,1273,1275,1278,1281,1283],{"class":99,"line":226},[97,1264,283],{"class":103},[97,1266,775],{"class":107},[97,1268,263],{"class":235},[97,1270,266],{"class":107},[97,1272,459],{"class":235},[97,1274,266],{"class":107},[97,1276,1277],{"class":235},"\"--dry-run\"",[97,1279,1280],{"class":107},"]).exit_code ",[97,1282,289],{"class":103},[97,1284,292],{"class":201},[97,1286,1287,1289,1291,1294,1296,1299],{"class":99,"line":247},[97,1288,283],{"class":103},[97,1290,1025],{"class":107},[97,1292,1293],{"class":235},"\"dry_run\"",[97,1295,978],{"class":107},[97,1297,1298],{"class":103},"is",[97,1300,1301],{"class":201}," True\n",[10,1303,1304,1305,1308,1309,1312,1313,1316,1317,1320],{},"Patch the name ",[55,1306,1307],{},"as imported by the command module",", not where it is defined. ",[13,1310,1311],{},"mytool.commands. sync.core.sync_directory"," works because the module holds a reference to ",[13,1314,1315],{},"core","; patching\n",[13,1318,1319],{},"mytool.core.sync.sync_directory"," after the command module has already bound the function is the\nclassic reason a patch appears to do nothing.",[18,1322,1324],{"id":1323},"conclusion","Conclusion",[10,1326,1327,1329],{},[13,1328,15],{}," gives you the full parsing and dispatch path at roughly a millisecond per invocation.\nAssert on the exit code, split the streams, re-raise unexpected exceptions so failures are\nreadable, and keep the tests focused on the interface — the behaviour belongs in cheaper tests one\nlayer down.",[18,1331,1333],{"id":1332},"frequently-asked-questions","Frequently asked questions",[1335,1336,1338],"h3",{"id":1337},"why-does-my-test-pass-when-the-command-clearly-failed","Why does my test pass when the command clearly failed?",[10,1340,1341,1342,1344],{},"Because the runner captured the exception instead of raising it, and your assertion happened to\nmatch text in the error output. Assert on ",[13,1343,34],{}," first in every test, and add the\nre-raising helper so unexpected exceptions surface with their traceback.",[1335,1346,1348],{"id":1347},"how-do-i-test-a-command-that-reads-from-stdin","How do I test a command that reads from stdin?",[10,1350,1351,1352,1355,1356,1358,1359,1362,1363,1365],{},"Pass ",[13,1353,1354],{},"input="," to ",[13,1357,1185],{},". It works both for prompts and for commands that read\n",[13,1360,1361],{},"sys.stdin.read()"," directly, since the runner replaces the stream for the duration of the call.\nFor binary input, ",[13,1364,1354],{}," accepts bytes.",[1335,1367,1369],{"id":1368},"can-i-invoke-a-subcommand-object-directly","Can I invoke a subcommand object directly?",[10,1371,1372,1373,1376],{},"You can — ",[13,1374,1375],{},"runner.invoke(sync, [\"data\"])"," invokes a single command without going through the\ngroup — but prefer invoking the root app with the full argument list. That is what users type, and\nit exercises the group callback, which is where global options and shared setup live.",[1335,1378,1380],{"id":1379},"does-the-runner-work-with-async-commands","Does the runner work with async commands?",[10,1382,1383,1384,1387,1388,1391],{},"Typer and Click commands are synchronous; if your command body runs an event loop with\n",[13,1385,1386],{},"asyncio.run",", the runner tests it like any other command. Testing the async functions themselves\nis better done directly with ",[13,1389,1390],{},"pytest-asyncio",", leaving the runner to cover the command wrapper.",[1335,1393,1395,1396,1399],{"id":1394},"why-is-resultoutput-empty-when-i-know-the-command-printed-something","Why is ",[13,1397,1398],{},"result.output"," empty when I know the command printed something?",[10,1401,1402,1403,1406,1407,1409,1410,1413],{},"Most often because the output went to stderr and you constructed the runner with\n",[13,1404,1405],{},"mix_stderr=False"," — check ",[13,1408,49],{},". Failing that, the command wrote to a stream it opened\nitself rather than to ",[13,1411,1412],{},"sys.stdout",", which the runner cannot capture; route all output through the\nframework's echo or a shared console object.",[1335,1415,1417,1418,1421],{"id":1416},"how-do-i-test-a-command-that-calls-sysexit-directly","How do I test a command that calls ",[13,1419,1420],{},"sys.exit"," directly?",[10,1423,1424,1425,1427,1428,1430,1431,1433],{},"The runner catches the resulting ",[13,1426,410],{}," and reports its code as ",[13,1429,34],{},", so the\ntest looks the same as any other. That said, a command calling ",[13,1432,1420],{}," itself is usually a\ndesign smell — raising a domain exception and mapping it in one boundary keeps the codes reviewable\nin a single place and makes the command testable as a plain function.",[1335,1435,1437],{"id":1436},"is-it-worth-asserting-on-help-text-for-every-command","Is it worth asserting on help text for every command?",[10,1439,1440,1441,1443],{},"No. Assert once that the root ",[13,1442,1051],{}," lists every command, which catches a registration that was\nremoved or never added. Beyond that, pin help output with a snapshot only if you have committed to\nkeeping it stable — otherwise every wording improvement becomes a test failure with no defect\nbehind it.",[18,1445,1447],{"id":1446},"related","Related",[23,1449,1450,1458,1465,1471,1478],{},[26,1451,1452,1453],{},"Up: ",[1454,1455,1457],"a",{"href":1456},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002F","Testing Python CLI applications",[26,1459,1460,1461],{},"Sideways: ",[1454,1462,1464],{"href":1463},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin\u002F","Testing interactive prompts and stdin",[26,1466,1460,1467],{},[1454,1468,1470],{"href":1469},"\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",[26,1472,1473,1474],{},"Related: ",[1454,1475,1477],{"href":1476},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-a-cli-with-subcommands-in-click\u002F","Building a CLI with subcommands in Click",[26,1479,1473,1480],{},[1454,1481,1483],{"href":1482},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools\u002F","Choosing exit codes for CLI tools",[1485,1486,1487],"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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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}",{"title":93,"searchDepth":121,"depth":121,"links":1489},[1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1511],{"id":20,"depth":121,"text":21},{"id":82,"depth":121,"text":83},{"id":139,"depth":121,"text":140},{"id":474,"depth":121,"text":475},{"id":594,"depth":121,"text":595},{"id":747,"depth":121,"text":748},{"id":885,"depth":121,"text":886},{"id":1040,"depth":121,"text":1041},{"id":1178,"depth":121,"text":1179},{"id":1323,"depth":121,"text":1324},{"id":1332,"depth":121,"text":1333,"children":1501},[1502,1503,1504,1505,1506,1508,1510],{"id":1337,"depth":165,"text":1338},{"id":1347,"depth":165,"text":1348},{"id":1368,"depth":165,"text":1369},{"id":1379,"depth":165,"text":1380},{"id":1394,"depth":165,"text":1507},"Why is result.output empty when I know the command printed something?",{"id":1416,"depth":165,"text":1509},"How do I test a command that calls sys.exit directly?",{"id":1436,"depth":165,"text":1437},{"id":1446,"depth":121,"text":1447},"2026-08-01","Drive Click and Typer commands in-process with CliRunner - separate streams, isolated filesystems, exit codes, and readable failures in pytest.","intermediate",false,"md",{},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner",{"title":5,"description":1513},"modern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner\u002Findex",[1522,1523,1524,1525,1526],"testing","clirunner","click","typer","pytest","KC2LxLlYITjbToHmd3uNU3QPUxF8DnJDoYl39WzMxUo",[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,1624,1627,1630,1633,1636,1639,1642,1645,1648,1651,1654,1657,1660,1663,1666,1669,1672,1675,1678,1681,1684,1685,1688,1690,1693,1696,1699,1702,1705,1708,1711,1714,1717,1720,1723,1726,1729,1732,1735,1738,1741,1744,1747,1750,1753,1756,1759,1762,1765,1768,1771],{"path":1530,"title":1531},"\u002Fabout","About Python CLI Toolcraft",{"path":1533,"title":1534},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1536,"title":1537},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1539,"title":1540},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1542,"title":1543},"\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":1545,"title":1546},"\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":1548,"title":1549},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1551,"title":1552},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1554,"title":1555},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1557,"title":1558},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1560,"title":1561},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1563,"title":1564},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1566,"title":1567},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1569,"title":1570},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1572,"title":1573},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1575,"title":1576},"\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":1578,"title":1579},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1581,"title":1582},"\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":1584,"title":1585},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1587,"title":1588},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1590,"title":1591},"\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":1593,"title":1594},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1596,"title":1597},"\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":1599,"title":1600},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1602,"title":1603},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1605,"title":1606},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1608,"title":1609},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1611,"title":1612},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1614,"title":1615},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1617,"title":1618},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1620,"title":1621},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":232,"title":1623},"Python CLI Toolcraft",{"path":1625,"title":1626},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1628,"title":1629},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1631,"title":1632},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1634,"title":1635},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1637,"title":1638},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1640,"title":1641},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1643,"title":1644},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1646,"title":1647},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1649,"title":1650},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1652,"title":1653},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1655,"title":1656},"\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":1658,"title":1659},"\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":1661,"title":1662},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1664,"title":1665},"\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":1667,"title":1668},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1670,"title":1671},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1673,"title":1674},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1676,"title":1677},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1679,"title":1680},"\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":1682,"title":1683},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1518,"title":5},{"path":1686,"title":1687},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1689,"title":1477},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-a-cli-with-subcommands-in-click",{"path":1691,"title":1692},"\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":1694,"title":1695},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1697,"title":1698},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1700,"title":1701},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1703,"title":1704},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1706,"title":1707},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1709,"title":1710},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1712,"title":1713},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1715,"title":1716},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1718,"title":1719},"\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":1721,"title":1722},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1724,"title":1725},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1727,"title":1728},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1730,"title":1731},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1733,"title":1734},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1736,"title":1737},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1739,"title":1740},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1742,"title":1743},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1745,"title":1746},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1748,"title":1749},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1751,"title":1752},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1754,"title":1755},"\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":1757,"title":1758},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1760,"title":1761},"\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":1763,"title":1764},"\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":1766,"title":1767},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1769,"title":1770},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1772,"title":1773},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690033]