[{"data":1,"prerenderedAt":1649},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output\u002F":3,"content-directory":1402},{"id":4,"title":5,"body":6,"date":1386,"description":1387,"difficulty":1388,"draft":1389,"extension":1390,"meta":1391,"navigation":137,"path":1392,"seo":1393,"stem":1394,"tags":1395,"updated":1386,"__hash__":1401},"content\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output\u002Findex.md","Snapshot Testing CLI Output",{"type":7,"value":8,"toc":1363},"minimark",[9,13,18,50,54,65,69,73,88,91,95,408,491,494,501,507,566,572,576,579,582,863,866,873,925,932,936,939,1083,1095,1099,1102,1108,1114,1120,1124,1127,1217,1224,1228,1231,1235,1240,1243,1247,1253,1257,1268,1272,1275,1279,1288,1292,1295,1299,1302,1306,1312,1316,1319,1323,1359],[10,11,12],"p",{},"Some CLI output is documentation: the help screen, a generated config file, a report people read\nevery morning. Asserting on fragments of it misses drift, and asserting on the whole thing by hand\nis unmaintainable. Snapshot testing records the output once, commits it, and turns any later change\ninto a reviewable diff.",[14,15,17],"h2",{"id":16},"tldr","TL;DR",[19,20,21,30,36,39,47],"ul",{},[22,23,24,25,29],"li",{},"Snapshot output that is ",[26,27,28],"strong",{},"read by people and must not drift silently","; assert directly on\neverything else.",[22,31,32,35],{},[26,33,34],{},"Normalise"," timestamps, temporary paths, durations and ordering before comparing.",[22,37,38],{},"Force a fixed terminal width and disable colour so the snapshot is machine-independent.",[22,40,41,42,46],{},"Re-record deliberately (",[43,44,45],"code",{},"--snapshot-update","), review the diff, and commit it with the change\nthat caused it.",[22,48,49],{},"Keep each snapshot small enough that it fails for exactly one reason.",[14,51,53],{"id":52},"prerequisites","Prerequisites",[10,55,56,57,60,61,64],{},"A CLI with output worth pinning, pytest, and either ",[43,58,59],{},"syrupy"," (",[43,62,63],{},"pip install syrupy",") or twenty\nlines of your own. This guide shows both — the hand-rolled version first, because it makes the\nmechanism obvious.",[14,66,68],{"id":67},"when-a-snapshot-is-the-right-tool","When a snapshot is the right tool",[70,71],"inline-diagram",{"name":72},"snapshot-scope-decision",[10,74,75,76,79,80,83,84,87],{},"The test is whether a change to the output would surprise someone. Help screens qualify: people\ncopy commands out of them, and a silently reordered option list makes documentation wrong. A\ngenerated ",[43,77,78],{},"pyproject.toml"," from your ",[43,81,82],{},"init"," command qualifies. An internal debug line does not,\nand neither does a computed number — ",[43,85,86],{},"assert result.uploaded == 3"," is clearer than any snapshot.",[10,89,90],{},"Where snapshots go wrong is scope. One snapshot covering six behaviours fails whenever any of\nthem changes, and the diff no longer tells you which. Prefer several small snapshots over one\nlarge one.",[14,92,94],{"id":93},"a-minimal-snapshot-fixture","A minimal snapshot fixture",[96,97,102],"pre",{"className":98,"code":99,"language":100,"meta":101,"style":101},"language-python shiki shiki-themes github-light github-dark","# tests\u002Fconftest.py\nimport os\nimport pytest\n\n@pytest.fixture\ndef snapshot(request):\n    \"\"\"Compare text against tests\u002F__snapshots__\u002F\u003Ctest name>.txt.\"\"\"\n    directory = request.path.parent \u002F \"__snapshots__\"\n    directory.mkdir(exist_ok=True)\n    path = directory \u002F f\"{request.node.name}.txt\"\n\n    def compare(actual: str) -> None:\n        if os.environ.get(\"UPDATE_SNAPSHOTS\") or not path.exists():\n            path.write_text(actual, encoding=\"utf-8\")\n            if not os.environ.get(\"UPDATE_SNAPSHOTS\"):\n                pytest.skip(f\"recorded new snapshot: {path}\")\n            return\n        expected = path.read_text(encoding=\"utf-8\")\n        assert actual == expected, f\"snapshot mismatch; UPDATE_SNAPSHOTS=1 to re-record ({path})\"\n\n    return compare\n","python","",[43,103,104,113,124,132,139,146,158,165,183,202,233,238,262,286,302,317,340,346,365,394,399],{"__ignoreMap":101},[105,106,109],"span",{"class":107,"line":108},"line",1,[105,110,112],{"class":111},"sJ8bj","# tests\u002Fconftest.py\n",[105,114,116,120],{"class":107,"line":115},2,[105,117,119],{"class":118},"szBVR","import",[105,121,123],{"class":122},"sVt8B"," os\n",[105,125,127,129],{"class":107,"line":126},3,[105,128,119],{"class":118},[105,130,131],{"class":122}," pytest\n",[105,133,135],{"class":107,"line":134},4,[105,136,138],{"emptyLinePlaceholder":137},true,"\n",[105,140,142],{"class":107,"line":141},5,[105,143,145],{"class":144},"sScJk","@pytest.fixture\n",[105,147,149,152,155],{"class":107,"line":148},6,[105,150,151],{"class":118},"def",[105,153,154],{"class":144}," snapshot",[105,156,157],{"class":122},"(request):\n",[105,159,161],{"class":107,"line":160},7,[105,162,164],{"class":163},"sZZnC","    \"\"\"Compare text against tests\u002F__snapshots__\u002F\u003Ctest name>.txt.\"\"\"\n",[105,166,168,171,174,177,180],{"class":107,"line":167},8,[105,169,170],{"class":122},"    directory ",[105,172,173],{"class":118},"=",[105,175,176],{"class":122}," request.path.parent ",[105,178,179],{"class":118},"\u002F",[105,181,182],{"class":163}," \"__snapshots__\"\n",[105,184,186,189,193,195,199],{"class":107,"line":185},9,[105,187,188],{"class":122},"    directory.mkdir(",[105,190,192],{"class":191},"s4XuR","exist_ok",[105,194,173],{"class":118},[105,196,198],{"class":197},"sj4cs","True",[105,200,201],{"class":122},")\n",[105,203,205,208,210,213,215,218,221,224,227,230],{"class":107,"line":204},10,[105,206,207],{"class":122},"    path ",[105,209,173],{"class":118},[105,211,212],{"class":122}," directory ",[105,214,179],{"class":118},[105,216,217],{"class":118}," f",[105,219,220],{"class":163},"\"",[105,222,223],{"class":197},"{",[105,225,226],{"class":122},"request.node.name",[105,228,229],{"class":197},"}",[105,231,232],{"class":163},".txt\"\n",[105,234,236],{"class":107,"line":235},11,[105,237,138],{"emptyLinePlaceholder":137},[105,239,241,244,247,250,253,256,259],{"class":107,"line":240},12,[105,242,243],{"class":118},"    def",[105,245,246],{"class":144}," compare",[105,248,249],{"class":122},"(actual: ",[105,251,252],{"class":197},"str",[105,254,255],{"class":122},") -> ",[105,257,258],{"class":197},"None",[105,260,261],{"class":122},":\n",[105,263,265,268,271,274,277,280,283],{"class":107,"line":264},13,[105,266,267],{"class":118},"        if",[105,269,270],{"class":122}," os.environ.get(",[105,272,273],{"class":163},"\"UPDATE_SNAPSHOTS\"",[105,275,276],{"class":122},") ",[105,278,279],{"class":118},"or",[105,281,282],{"class":118}," not",[105,284,285],{"class":122}," path.exists():\n",[105,287,289,292,295,297,300],{"class":107,"line":288},14,[105,290,291],{"class":122},"            path.write_text(actual, ",[105,293,294],{"class":191},"encoding",[105,296,173],{"class":118},[105,298,299],{"class":163},"\"utf-8\"",[105,301,201],{"class":122},[105,303,305,308,310,312,314],{"class":107,"line":304},15,[105,306,307],{"class":118},"            if",[105,309,282],{"class":118},[105,311,270],{"class":122},[105,313,273],{"class":163},[105,315,316],{"class":122},"):\n",[105,318,320,323,326,329,331,334,336,338],{"class":107,"line":319},16,[105,321,322],{"class":122},"                pytest.skip(",[105,324,325],{"class":118},"f",[105,327,328],{"class":163},"\"recorded new snapshot: ",[105,330,223],{"class":197},[105,332,333],{"class":122},"path",[105,335,229],{"class":197},[105,337,220],{"class":163},[105,339,201],{"class":122},[105,341,343],{"class":107,"line":342},17,[105,344,345],{"class":118},"            return\n",[105,347,349,352,354,357,359,361,363],{"class":107,"line":348},18,[105,350,351],{"class":122},"        expected ",[105,353,173],{"class":118},[105,355,356],{"class":122}," path.read_text(",[105,358,294],{"class":191},[105,360,173],{"class":118},[105,362,299],{"class":163},[105,364,201],{"class":122},[105,366,368,371,374,377,380,382,385,387,389,391],{"class":107,"line":367},19,[105,369,370],{"class":118},"        assert",[105,372,373],{"class":122}," actual ",[105,375,376],{"class":118},"==",[105,378,379],{"class":122}," expected, ",[105,381,325],{"class":118},[105,383,384],{"class":163},"\"snapshot mismatch; UPDATE_SNAPSHOTS=1 to re-record (",[105,386,223],{"class":197},[105,388,333],{"class":122},[105,390,229],{"class":197},[105,392,393],{"class":163},")\"\n",[105,395,397],{"class":107,"line":396},20,[105,398,138],{"emptyLinePlaceholder":137},[105,400,402,405],{"class":107,"line":401},21,[105,403,404],{"class":118},"    return",[105,406,407],{"class":122}," compare\n",[96,409,411],{"className":98,"code":410,"language":100,"meta":101,"style":101},"def test_help_screen(cli, snapshot):\n    result = cli.invoke(app, [\"--help\"], env={\"COLUMNS\": \"80\", \"NO_COLOR\": \"1\"})\n\n    assert result.exit_code == 0\n    snapshot(result.stdout)\n",[43,412,413,423,469,473,486],{"__ignoreMap":101},[105,414,415,417,420],{"class":107,"line":108},[105,416,151],{"class":118},[105,418,419],{"class":144}," test_help_screen",[105,421,422],{"class":122},"(cli, snapshot):\n",[105,424,425,428,430,433,436,439,442,444,446,449,452,455,458,461,463,466],{"class":107,"line":115},[105,426,427],{"class":122},"    result ",[105,429,173],{"class":118},[105,431,432],{"class":122}," cli.invoke(app, [",[105,434,435],{"class":163},"\"--help\"",[105,437,438],{"class":122},"], ",[105,440,441],{"class":191},"env",[105,443,173],{"class":118},[105,445,223],{"class":122},[105,447,448],{"class":163},"\"COLUMNS\"",[105,450,451],{"class":122},": ",[105,453,454],{"class":163},"\"80\"",[105,456,457],{"class":122},", ",[105,459,460],{"class":163},"\"NO_COLOR\"",[105,462,451],{"class":122},[105,464,465],{"class":163},"\"1\"",[105,467,468],{"class":122},"})\n",[105,470,471],{"class":107,"line":126},[105,472,138],{"emptyLinePlaceholder":137},[105,474,475,478,481,483],{"class":107,"line":134},[105,476,477],{"class":118},"    assert",[105,479,480],{"class":122}," result.exit_code ",[105,482,376],{"class":118},[105,484,485],{"class":197}," 0\n",[105,487,488],{"class":107,"line":141},[105,489,490],{"class":122},"    snapshot(result.stdout)\n",[70,492],{"name":493},"snapshot-workflow",[10,495,496,497,500],{},"Two behaviours make this safe to live with. A missing snapshot is ",[26,498,499],{},"recorded and the test is\nskipped",", rather than silently passing — so a new snapshot is never mistaken for a passing\nassertion. And re-recording requires an explicit environment variable, so a stale snapshot cannot\nbe updated by accident during an ordinary run.",[10,502,503,504,506],{},"With ",[43,505,59],{}," the same test is shorter and the mechanics are identical:",[96,508,510],{"className":98,"code":509,"language":100,"meta":101,"style":101},"def test_help_screen(cli, snapshot):\n    result = cli.invoke(app, [\"--help\"], env={\"COLUMNS\": \"80\", \"NO_COLOR\": \"1\"})\n    assert result.stdout == snapshot\n",[43,511,512,520,554],{"__ignoreMap":101},[105,513,514,516,518],{"class":107,"line":108},[105,515,151],{"class":118},[105,517,419],{"class":144},[105,519,422],{"class":122},[105,521,522,524,526,528,530,532,534,536,538,540,542,544,546,548,550,552],{"class":107,"line":115},[105,523,427],{"class":122},[105,525,173],{"class":118},[105,527,432],{"class":122},[105,529,435],{"class":163},[105,531,438],{"class":122},[105,533,441],{"class":191},[105,535,173],{"class":118},[105,537,223],{"class":122},[105,539,448],{"class":163},[105,541,451],{"class":122},[105,543,454],{"class":163},[105,545,457],{"class":122},[105,547,460],{"class":163},[105,549,451],{"class":122},[105,551,465],{"class":163},[105,553,468],{"class":122},[105,555,556,558,561,563],{"class":107,"line":126},[105,557,477],{"class":118},[105,559,560],{"class":122}," result.stdout ",[105,562,376],{"class":118},[105,564,565],{"class":122}," snapshot\n",[10,567,568,571],{},[43,569,570],{},"pytest --snapshot-update"," re-records, and unused snapshots are reported so deleted tests do not\nleave orphans behind.",[14,573,575],{"id":574},"normalising-unstable-output","Normalising unstable output",[10,577,578],{},"A snapshot is only useful if the same input produces the same bytes on every machine. Four things\nroutinely break that.",[70,580],{"name":581},"snapshot-normalisation",[96,583,585],{"className":98,"code":584,"language":100,"meta":101,"style":101},"import re\n\nSUBSTITUTIONS = [\n    (re.compile(r\"\\d{4}-\\d{2}-\\d{2}T[\\d:.]+Z?\"), \"\u003CTIMESTAMP>\"),\n    (re.compile(r\"in \\d+\\.\\d+s\"), \"in \u003CDURATION>\"),\n    (re.compile(r\"\u002Ftmp\u002F[^\\s\\\"']+\"), \"\u003CTMPDIR>\"),\n    (re.compile(r\"mytool \\d+\\.\\d+\\.\\d+\"), \"mytool \u003CVERSION>\"),\n]\n\ndef normalise(text: str) -> str:\n    for pattern, replacement in SUBSTITUTIONS:\n        text = pattern.sub(replacement, text)\n    return text.replace(\"\\r\\n\", \"\\n\").rstrip() + \"\\n\"\n",[43,586,587,594,598,609,666,701,738,774,779,783,801,817,827],{"__ignoreMap":101},[105,588,589,591],{"class":107,"line":108},[105,590,119],{"class":118},[105,592,593],{"class":122}," re\n",[105,595,596],{"class":107,"line":115},[105,597,138],{"emptyLinePlaceholder":137},[105,599,600,603,606],{"class":107,"line":126},[105,601,602],{"class":197},"SUBSTITUTIONS",[105,604,605],{"class":118}," =",[105,607,608],{"class":122}," [\n",[105,610,611,614,617,619,622,625,629,631,634,636,638,640,643,646,649,652,655,657,660,663],{"class":107,"line":134},[105,612,613],{"class":122},"    (re.compile(",[105,615,616],{"class":118},"r",[105,618,220],{"class":163},[105,620,621],{"class":197},"\\d",[105,623,624],{"class":118},"{4}",[105,626,628],{"class":627},"sA_wV","-",[105,630,621],{"class":197},[105,632,633],{"class":118},"{2}",[105,635,628],{"class":627},[105,637,621],{"class":197},[105,639,633],{"class":118},[105,641,642],{"class":627},"T",[105,644,645],{"class":197},"[\\d:.]",[105,647,648],{"class":118},"+",[105,650,651],{"class":627},"Z",[105,653,654],{"class":118},"?",[105,656,220],{"class":163},[105,658,659],{"class":122},"), ",[105,661,662],{"class":163},"\"\u003CTIMESTAMP>\"",[105,664,665],{"class":122},"),\n",[105,667,668,670,672,674,677,679,681,685,687,689,692,694,696,699],{"class":107,"line":141},[105,669,613],{"class":122},[105,671,616],{"class":118},[105,673,220],{"class":163},[105,675,676],{"class":627},"in ",[105,678,621],{"class":197},[105,680,648],{"class":118},[105,682,684],{"class":683},"snhLl","\\.",[105,686,621],{"class":197},[105,688,648],{"class":118},[105,690,691],{"class":627},"s",[105,693,220],{"class":163},[105,695,659],{"class":122},[105,697,698],{"class":163},"\"in \u003CDURATION>\"",[105,700,665],{"class":122},[105,702,703,705,707,709,712,715,718,721,724,727,729,731,733,736],{"class":107,"line":148},[105,704,613],{"class":122},[105,706,616],{"class":118},[105,708,220],{"class":163},[105,710,711],{"class":627},"\u002Ftmp\u002F",[105,713,714],{"class":197},"[",[105,716,717],{"class":118},"^",[105,719,720],{"class":197},"\\s",[105,722,723],{"class":683},"\\\"",[105,725,726],{"class":197},"']",[105,728,648],{"class":118},[105,730,220],{"class":163},[105,732,659],{"class":122},[105,734,735],{"class":163},"\"\u003CTMPDIR>\"",[105,737,665],{"class":122},[105,739,740,742,744,746,749,751,753,755,757,759,761,763,765,767,769,772],{"class":107,"line":160},[105,741,613],{"class":122},[105,743,616],{"class":118},[105,745,220],{"class":163},[105,747,748],{"class":627},"mytool ",[105,750,621],{"class":197},[105,752,648],{"class":118},[105,754,684],{"class":683},[105,756,621],{"class":197},[105,758,648],{"class":118},[105,760,684],{"class":683},[105,762,621],{"class":197},[105,764,648],{"class":118},[105,766,220],{"class":163},[105,768,659],{"class":122},[105,770,771],{"class":163},"\"mytool \u003CVERSION>\"",[105,773,665],{"class":122},[105,775,776],{"class":107,"line":167},[105,777,778],{"class":122},"]\n",[105,780,781],{"class":107,"line":185},[105,782,138],{"emptyLinePlaceholder":137},[105,784,785,787,790,793,795,797,799],{"class":107,"line":204},[105,786,151],{"class":118},[105,788,789],{"class":144}," normalise",[105,791,792],{"class":122},"(text: ",[105,794,252],{"class":197},[105,796,255],{"class":122},[105,798,252],{"class":197},[105,800,261],{"class":122},[105,802,803,806,809,812,815],{"class":107,"line":235},[105,804,805],{"class":118},"    for",[105,807,808],{"class":122}," pattern, replacement ",[105,810,811],{"class":118},"in",[105,813,814],{"class":197}," SUBSTITUTIONS",[105,816,261],{"class":122},[105,818,819,822,824],{"class":107,"line":240},[105,820,821],{"class":122},"        text ",[105,823,173],{"class":118},[105,825,826],{"class":122}," pattern.sub(replacement, text)\n",[105,828,829,831,834,836,839,841,843,845,848,850,853,855,858,860],{"class":107,"line":264},[105,830,404],{"class":118},[105,832,833],{"class":122}," text.replace(",[105,835,220],{"class":163},[105,837,838],{"class":197},"\\r\\n",[105,840,220],{"class":163},[105,842,457],{"class":122},[105,844,220],{"class":163},[105,846,847],{"class":197},"\\n",[105,849,220],{"class":163},[105,851,852],{"class":122},").rstrip() ",[105,854,648],{"class":118},[105,856,857],{"class":163}," \"",[105,859,847],{"class":197},[105,861,862],{"class":163},"\"\n",[10,864,865],{},"The version substitution is the one people forget: without it, every release breaks every snapshot\nthat includes the help footer.",[10,867,868,869,872],{},"Terminal width is the other frequent culprit. Click and Rich both size output to the terminal, and\nthe runner inherits whatever ",[43,870,871],{},"COLUMNS"," happens to be. Pin it:",[96,874,876],{"className":98,"code":875,"language":100,"meta":101,"style":101},"result = cli.invoke(app, [\"status\"], env={\"COLUMNS\": \"100\", \"NO_COLOR\": \"1\", \"TERM\": \"dumb\"})\n",[43,877,878],{"__ignoreMap":101},[105,879,880,883,885,887,890,892,894,896,898,900,902,905,907,909,911,913,915,918,920,923],{"class":107,"line":108},[105,881,882],{"class":122},"result ",[105,884,173],{"class":118},[105,886,432],{"class":122},[105,888,889],{"class":163},"\"status\"",[105,891,438],{"class":122},[105,893,441],{"class":191},[105,895,173],{"class":118},[105,897,223],{"class":122},[105,899,448],{"class":163},[105,901,451],{"class":122},[105,903,904],{"class":163},"\"100\"",[105,906,457],{"class":122},[105,908,460],{"class":163},[105,910,451],{"class":122},[105,912,465],{"class":163},[105,914,457],{"class":122},[105,916,917],{"class":163},"\"TERM\"",[105,919,451],{"class":122},[105,921,922],{"class":163},"\"dumb\"",[105,924,468],{"class":122},[10,926,927,928,931],{},"Ordering matters too. If your command prints a set, or iterates over a dictionary populated from\nthe filesystem, sort before printing — not in the test, but in the command. Output that is stable\nfor tests is output that is stable for users piping it into ",[43,929,930],{},"diff",".",[14,933,935],{"id":934},"snapshotting-files-not-just-stdout","Snapshotting files, not just stdout",[10,937,938],{},"The same technique covers generated files, which are often the more valuable target:",[96,940,942],{"className":98,"code":941,"language":100,"meta":101,"style":101},"def test_init_generates_a_project(cli, snapshot, tmp_path):\n    result = cli.invoke(app, [\"init\", \"--name\", \"demo\", \"--dir\", str(tmp_path)])\n    assert result.exit_code == 0\n\n    generated = sorted(p.relative_to(tmp_path) for p in tmp_path.rglob(\"*\") if p.is_file())\n    snapshot(\"\\n\".join(str(p) for p in generated))            # the file list\n    snapshot_file(tmp_path \u002F \"pyproject.toml\")                # and one file's contents\n",[43,943,944,954,987,997,1001,1036,1067],{"__ignoreMap":101},[105,945,946,948,951],{"class":107,"line":108},[105,947,151],{"class":118},[105,949,950],{"class":144}," test_init_generates_a_project",[105,952,953],{"class":122},"(cli, snapshot, tmp_path):\n",[105,955,956,958,960,962,965,967,970,972,975,977,980,982,984],{"class":107,"line":115},[105,957,427],{"class":122},[105,959,173],{"class":118},[105,961,432],{"class":122},[105,963,964],{"class":163},"\"init\"",[105,966,457],{"class":122},[105,968,969],{"class":163},"\"--name\"",[105,971,457],{"class":122},[105,973,974],{"class":163},"\"demo\"",[105,976,457],{"class":122},[105,978,979],{"class":163},"\"--dir\"",[105,981,457],{"class":122},[105,983,252],{"class":197},[105,985,986],{"class":122},"(tmp_path)])\n",[105,988,989,991,993,995],{"class":107,"line":126},[105,990,477],{"class":118},[105,992,480],{"class":122},[105,994,376],{"class":118},[105,996,485],{"class":197},[105,998,999],{"class":107,"line":134},[105,1000,138],{"emptyLinePlaceholder":137},[105,1002,1003,1006,1008,1011,1014,1017,1020,1022,1025,1028,1030,1033],{"class":107,"line":141},[105,1004,1005],{"class":122},"    generated ",[105,1007,173],{"class":118},[105,1009,1010],{"class":197}," sorted",[105,1012,1013],{"class":122},"(p.relative_to(tmp_path) ",[105,1015,1016],{"class":118},"for",[105,1018,1019],{"class":122}," p ",[105,1021,811],{"class":118},[105,1023,1024],{"class":122}," tmp_path.rglob(",[105,1026,1027],{"class":163},"\"*\"",[105,1029,276],{"class":122},[105,1031,1032],{"class":118},"if",[105,1034,1035],{"class":122}," p.is_file())\n",[105,1037,1038,1041,1043,1045,1047,1050,1052,1055,1057,1059,1061,1064],{"class":107,"line":148},[105,1039,1040],{"class":122},"    snapshot(",[105,1042,220],{"class":163},[105,1044,847],{"class":197},[105,1046,220],{"class":163},[105,1048,1049],{"class":122},".join(",[105,1051,252],{"class":197},[105,1053,1054],{"class":122},"(p) ",[105,1056,1016],{"class":118},[105,1058,1019],{"class":122},[105,1060,811],{"class":118},[105,1062,1063],{"class":122}," generated))            ",[105,1065,1066],{"class":111},"# the file list\n",[105,1068,1069,1072,1074,1077,1080],{"class":107,"line":160},[105,1070,1071],{"class":122},"    snapshot_file(tmp_path ",[105,1073,179],{"class":118},[105,1075,1076],{"class":163}," \"pyproject.toml\"",[105,1078,1079],{"class":122},")                ",[105,1081,1082],{"class":111},"# and one file's contents\n",[10,1084,1085,1086,1090,1091,1094],{},"Snapshotting the ",[1087,1088,1089],"em",{},"file list"," separately from the ",[1087,1092,1093],{},"contents"," is worth doing: adding a file and\nchanging a file are different reviews, and separate snapshots keep the diffs meaningful.",[14,1096,1098],{"id":1097},"ux-considerations","UX considerations",[10,1100,1101],{},"Snapshots have a social cost as well as a technical one. Two habits keep them from becoming\nnoise nobody reads.",[10,1103,1104,1107],{},[26,1105,1106],{},"Re-record in the commit that caused the change."," A pull request that renames a flag should\ncontain both the code change and the updated snapshot, so the reviewer sees the user-visible effect\nnext to the cause. Updating snapshots in a separate \"fix tests\" commit throws that away.",[10,1109,1110,1113],{},[26,1111,1112],{},"Never re-record to make a failure go away."," The failure is the feature. If a snapshot changed\nand nobody intended it, the code changed in a way nobody intended — which is exactly what the\nsnapshot was for.",[10,1115,1116,1117,1119],{},"It is worth putting both of those in a comment at the top of the snapshot directory's README, or\nin the failure message itself, because the temptation to run ",[43,1118,45],{}," on a red build is\nstrong.",[14,1121,1123],{"id":1122},"testing-the-behaviour","Testing the behaviour",[10,1125,1126],{},"Snapshots complement targeted assertions rather than replacing them. A good pairing:",[96,1128,1130],{"className":98,"code":1129,"language":100,"meta":101,"style":101},"def test_status_table(cli, snapshot):\n    result = cli.invoke(app, [\"status\"], env={\"COLUMNS\": \"100\", \"NO_COLOR\": \"1\"})\n\n    assert result.exit_code == 0                  # behaviour: it worked\n    assert \"prod\" in result.stdout                # behaviour: the important row is present\n    snapshot(normalise(result.stdout))            # presentation: nothing else drifted\n",[43,1131,1132,1141,1175,1179,1193,1209],{"__ignoreMap":101},[105,1133,1134,1136,1139],{"class":107,"line":108},[105,1135,151],{"class":118},[105,1137,1138],{"class":144}," test_status_table",[105,1140,422],{"class":122},[105,1142,1143,1145,1147,1149,1151,1153,1155,1157,1159,1161,1163,1165,1167,1169,1171,1173],{"class":107,"line":115},[105,1144,427],{"class":122},[105,1146,173],{"class":118},[105,1148,432],{"class":122},[105,1150,889],{"class":163},[105,1152,438],{"class":122},[105,1154,441],{"class":191},[105,1156,173],{"class":118},[105,1158,223],{"class":122},[105,1160,448],{"class":163},[105,1162,451],{"class":122},[105,1164,904],{"class":163},[105,1166,457],{"class":122},[105,1168,460],{"class":163},[105,1170,451],{"class":122},[105,1172,465],{"class":163},[105,1174,468],{"class":122},[105,1176,1177],{"class":107,"line":126},[105,1178,138],{"emptyLinePlaceholder":137},[105,1180,1181,1183,1185,1187,1190],{"class":107,"line":134},[105,1182,477],{"class":118},[105,1184,480],{"class":122},[105,1186,376],{"class":118},[105,1188,1189],{"class":197}," 0",[105,1191,1192],{"class":111},"                  # behaviour: it worked\n",[105,1194,1195,1197,1200,1203,1206],{"class":107,"line":141},[105,1196,477],{"class":118},[105,1198,1199],{"class":163}," \"prod\"",[105,1201,1202],{"class":118}," in",[105,1204,1205],{"class":122}," result.stdout                ",[105,1207,1208],{"class":111},"# behaviour: the important row is present\n",[105,1210,1211,1214],{"class":107,"line":148},[105,1212,1213],{"class":122},"    snapshot(normalise(result.stdout))            ",[105,1215,1216],{"class":111},"# presentation: nothing else drifted\n",[10,1218,1219,1220,1223],{},"The first two assertions fail with a clear message when the feature breaks. The snapshot fails\nwhen the ",[1087,1221,1222],{},"presentation"," changes, which is a different signal and often a different fix.",[14,1225,1227],{"id":1226},"conclusion","Conclusion",[10,1229,1230],{},"Snapshot tests are the cheapest way to keep output that people depend on from drifting. Normalise\nthe unstable parts, keep each snapshot narrow, record intentionally, and treat an unexpected diff\nas the finding it is. For output nobody reads, an ordinary assertion is still the better tool.",[14,1232,1234],{"id":1233},"frequently-asked-questions","Frequently asked questions",[1236,1237,1239],"h3",{"id":1238},"should-snapshots-be-committed-to-the-repository","Should snapshots be committed to the repository?",[10,1241,1242],{},"Yes — that is the whole point. A snapshot in version control means a change to output appears in\nthe diff of the commit that caused it, and reviewers can see the user-visible effect without\nrunning anything.",[1236,1244,1246],{"id":1245},"how-do-i-stop-snapshots-from-becoming-a-wall-of-unreviewed-diffs","How do I stop snapshots from becoming a wall of unreviewed diffs?",[10,1248,1249,1250,1252],{},"Keep them small and few. One snapshot per meaningful output, not one per test; normalise\naggressively so unrelated changes do not touch them; and delete snapshots for tests you removed.\n",[43,1251,59],{}," reports orphaned snapshots, which makes the last part automatic.",[1236,1254,1256],{"id":1255},"what-about-colour-codes-in-the-output","What about colour codes in the output?",[10,1258,1259,1260,1263,1264,1267],{},"Disable them in tests with ",[43,1261,1262],{},"NO_COLOR=1"," and a fixed ",[43,1265,1266],{},"TERM",". Snapshotting escape sequences produces\nfiles nobody can read in a diff, and the colour is rarely the property you meant to pin. If colour\nitself matters, assert on it separately with a targeted test.",[1236,1269,1271],{"id":1270},"can-i-snapshot-json-output","Can I snapshot JSON output?",[10,1273,1274],{},"Yes, and it is a good use of the technique — but parse and re-serialise with sorted keys before\ncomparing, so key ordering cannot cause spurious diffs. Better still, assert on the parsed\nstructure for the fields you care about and snapshot the whole document for drift.",[1236,1276,1278],{"id":1277},"does-this-work-for-windows","Does this work for Windows?",[10,1280,1281,1282,1284,1285,1287],{},"With one normalisation: replace ",[43,1283,838],{}," with ",[43,1286,847],{}," before comparing, and use forward slashes in any\npath you include. Without that, a snapshot recorded on Linux fails on Windows for reasons that have\nnothing to do with your code.",[1236,1289,1291],{"id":1290},"what-is-the-right-size-for-a-snapshot","What is the right size for a snapshot?",[10,1293,1294],{},"Small enough that a failure names one thing. A single command's output at a fixed width is a good\nunit; the combined output of six commands is not. If a snapshot regularly changes for reasons the\ntest is not about, split it or normalise harder.",[1236,1296,1298],{"id":1297},"should-snapshot-tests-run-in-ci-or-only-locally","Should snapshot tests run in CI, or only locally?",[10,1300,1301],{},"In CI, and without the update flag available — a snapshot that can be re-recorded by the build is\nnot a check. Set the update mechanism behind an environment variable that CI never sets, so the\nonly way to change a snapshot is a deliberate local run and a commit.",[1236,1303,1305],{"id":1304},"how-do-snapshots-interact-with-parametrised-tests","How do snapshots interact with parametrised tests?",[10,1307,1308,1309,1311],{},"Each parameter set needs its own snapshot file, keyed on the test id. Both the fixture shown above\nand ",[43,1310,59],{}," handle this automatically because the node name includes the parameters — which is\nalso why parameter values should be short and readable rather than whole objects.",[1236,1313,1315],{"id":1314},"can-a-snapshot-replace-documentation-of-the-output-format","Can a snapshot replace documentation of the output format?",[10,1317,1318],{},"Not on its own, but it pairs well with it. A snapshot proves the format has not changed; the\ndocumentation says what the fields mean and which of them are guaranteed. Together they turn \"we\nthink this is stable\" into something a reviewer can verify in a diff.",[14,1320,1322],{"id":1321},"related","Related",[19,1324,1325,1333,1340,1346,1353],{},[22,1326,1327,1328],{},"Up: ",[1329,1330,1332],"a",{"href":1331},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002F","Testing Python CLI applications",[22,1334,1335,1336],{},"Sideways: ",[1329,1337,1339],{"href":1338},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner\u002F","Testing Click commands with CliRunner",[22,1341,1335,1342],{},[1329,1343,1345],{"href":1344},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage\u002F","Measuring CLI test coverage",[22,1347,1348,1349],{},"Related: ",[1329,1350,1352],{"href":1351},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002F","Interactive terminal UI with Rich",[22,1354,1348,1355],{},[1329,1356,1358],{"href":1357},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis\u002F","Structured JSON logging in Python CLIs",[1360,1361,1362],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html 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 .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 .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}html pre.shiki code .snhLl, html code.shiki .snhLl{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}",{"title":101,"searchDepth":115,"depth":115,"links":1364},[1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1385],{"id":16,"depth":115,"text":17},{"id":52,"depth":115,"text":53},{"id":67,"depth":115,"text":68},{"id":93,"depth":115,"text":94},{"id":574,"depth":115,"text":575},{"id":934,"depth":115,"text":935},{"id":1097,"depth":115,"text":1098},{"id":1122,"depth":115,"text":1123},{"id":1226,"depth":115,"text":1227},{"id":1233,"depth":115,"text":1234,"children":1375},[1376,1377,1378,1379,1380,1381,1382,1383,1384],{"id":1238,"depth":126,"text":1239},{"id":1245,"depth":126,"text":1246},{"id":1255,"depth":126,"text":1256},{"id":1270,"depth":126,"text":1271},{"id":1277,"depth":126,"text":1278},{"id":1290,"depth":126,"text":1291},{"id":1297,"depth":126,"text":1298},{"id":1304,"depth":126,"text":1305},{"id":1314,"depth":126,"text":1315},{"id":1321,"depth":115,"text":1322},"2026-08-01","Pin help screens, tables, and generated files with snapshot tests - normalise unstable values, review diffs, and re-record intentional changes safely.","intermediate",false,"md",{},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output",{"title":5,"description":1387},"modern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output\u002Findex",[1396,1397,1398,1399,1400],"testing","snapshots","pytest","output","cli","Ok48MEPKVpKmOxebRjFrEnT2aw6KBjXngd2n0N_Kx9k",[1403,1406,1409,1412,1415,1418,1421,1424,1427,1430,1433,1436,1439,1442,1445,1448,1451,1454,1457,1460,1463,1466,1469,1472,1475,1478,1481,1484,1487,1490,1493,1496,1498,1501,1504,1507,1510,1513,1516,1519,1522,1525,1528,1531,1534,1537,1540,1543,1546,1549,1552,1555,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,1631,1634,1637,1640,1643,1646],{"path":1404,"title":1405},"\u002Fabout","About Python CLI Toolcraft",{"path":1407,"title":1408},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1410,"title":1411},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1413,"title":1414},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1416,"title":1417},"\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":1419,"title":1420},"\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":1422,"title":1423},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1425,"title":1426},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1428,"title":1429},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1431,"title":1432},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1434,"title":1435},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1437,"title":1438},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1440,"title":1441},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1443,"title":1444},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1446,"title":1447},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1449,"title":1450},"\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":1452,"title":1453},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1455,"title":1456},"\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":1458,"title":1459},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1461,"title":1462},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1464,"title":1465},"\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":1467,"title":1468},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1470,"title":1471},"\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":1473,"title":1474},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1476,"title":1477},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1479,"title":1480},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1482,"title":1483},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1485,"title":1486},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1488,"title":1489},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1491,"title":1492},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1494,"title":1495},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":179,"title":1497},"Python CLI Toolcraft",{"path":1499,"title":1500},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1502,"title":1503},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1505,"title":1506},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1508,"title":1509},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1511,"title":1512},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1514,"title":1515},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1517,"title":1518},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1520,"title":1521},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1523,"title":1524},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1526,"title":1527},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1529,"title":1530},"\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":1532,"title":1533},"\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":1535,"title":1536},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1538,"title":1539},"\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":1541,"title":1542},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1544,"title":1545},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1547,"title":1548},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1550,"title":1551},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1553,"title":1554},"\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":1392,"title":5},{"path":1557,"title":1558},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1560,"title":1561},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1563,"title":1564},"\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":1566,"title":1567},"\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":1569,"title":1570},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1572,"title":1573},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1575,"title":1576},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1578,"title":1579},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1581,"title":1582},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1584,"title":1585},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1587,"title":1588},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1590,"title":1591},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1593,"title":1594},"\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":1596,"title":1597},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1599,"title":1600},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1602,"title":1603},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1605,"title":1606},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1608,"title":1609},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1611,"title":1612},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1614,"title":1615},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1617,"title":1618},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1620,"title":1621},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1623,"title":1624},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1626,"title":1627},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1629,"title":1630},"\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":1632,"title":1633},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1635,"title":1636},"\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":1638,"title":1639},"\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":1641,"title":1642},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1644,"title":1645},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1647,"title":1648},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690033]