[{"data":1,"prerenderedAt":2566},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli\u002F":3,"content-directory":2019},{"id":4,"title":5,"body":6,"date":2005,"description":2006,"difficulty":2007,"draft":2008,"extension":2009,"meta":2010,"navigation":127,"path":2011,"seo":2012,"stem":2013,"tags":2014,"updated":2005,"__hash__":2018},"content\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli\u002Findex.md","End-to-End Testing an Installed Python CLI",{"type":7,"value":8,"toc":1986},"minimark",[9,32,37,55,59,63,69,72,76,83,86,1059,1062,1102,1106,1727,1733,1747,1752,1755,1803,1807,1810,1860,1864,1883,1887,1893,1897,1909,1915,1919,1922,1926,1941,1945,1948,1952,1982],[10,11,12,16,17,21,22,25,26,31],"p",{},[13,14,15],"code",{},"CliRunner"," tests are fast and thorough, and they should be the backbone of a CLI's test suite. But they run your command in-process, from the source tree, with stdout and stderr replaced by in-memory buffers and no real terminal, pipe or signal in sight. A class of bugs lives exactly in that gap: a console-script entry point that points at the wrong function, a command that behaves differently when its output is a pipe, an exit status that is correct in-process and wrong through the launcher, state that leaks between invocations in one process and never in real use. End-to-end tests close the gap by running the ",[18,19,20],"strong",{},"installed"," command as a ",[18,23,24],{},"separate process",", the way users and scripts do. This guide builds a small pytest fixture that installs your CLI once per session into an isolated environment, a helper for running it, and a focused set of end-to-end tests. It belongs to the ",[27,28,30],"a",{"href":29},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002F","testing Python CLI applications topic",".",[33,34,36],"h2",{"id":35},"prerequisites","Prerequisites",[38,39,40,52],"ul",{},[41,42,43,44,47,48,31],"li",{},"A CLI packaged with a ",[13,45,46],{},"[project.scripts]"," entry point and a solid in-process suite, as in ",[27,49,51],{"href":50},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner\u002F","testing Click commands with CliRunner",[41,53,54],{},"uv available on the test machine (used here to build and install quickly).",[33,56,58],{"id":57},"where-end-to-end-tests-fit","Where end-to-end tests fit",[60,61],"inline-diagram",{"name":62},"e2e-pyramid",[10,64,65,66,68],{},"Most tests should stay where they are: unit tests of core functions, and ",[13,67,15],{}," tests of commands. End-to-end tests are the thin top layer — a few dozen at most — chosen to cover what only a real process can show:",[60,70],{"name":71},"e2e-catches",[33,73,75],{"id":74},"the-recipe-install-once-run-many","The recipe: install once, run many",[10,77,78,79,82],{},"The expensive part is building and installing the package; do it ",[18,80,81],{},"once per test session"," and share the result:",[60,84],{"name":85},"e2e-flow",[87,88,93],"pre",{"className":89,"code":90,"language":91,"meta":92,"style":92},"language-python shiki shiki-themes github-light github-dark","# tests\u002Fe2e\u002Fconftest.py\nfrom __future__ import annotations\n\nimport os\nimport subprocess\nimport sys\nfrom collections.abc import Callable\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\nimport pytest\n\nPROJECT = Path(__file__).resolve().parents[2]\nCOMMAND = \"mytool\"\n\n\n@dataclass(frozen=True)\nclass Run:\n    code: int\n    stdout: str\n    stderr: str\n\n\n@pytest.fixture(scope=\"session\")\ndef installed_cli(tmp_path_factory: pytest.TempPathFactory) -> Path:\n    \"\"\"Build the wheel and install it into a fresh virtual environment; return the executable.\"\"\"\n    root = tmp_path_factory.mktemp(\"e2e\")\n    dist, venv = root \u002F \"dist\", root \u002F \"venv\"\n    subprocess.run([\"uv\", \"build\", \"--wheel\", \"--out-dir\", str(dist)], cwd=PROJECT, check=True,\n                   capture_output=True)\n    wheel = next(dist.glob(\"*.whl\"))\n    subprocess.run([\"uv\", \"venv\", \"--quiet\", str(venv)], check=True)\n    python = venv \u002F (\"Scripts\u002Fpython.exe\" if os.name == \"nt\" else \"bin\u002Fpython\")\n    subprocess.run([\"uv\", \"pip\", \"install\", \"--quiet\", \"--python\", str(python), str(wheel)],\n                   check=True)\n    exe = venv \u002F (\"Scripts\" if os.name == \"nt\" else \"bin\") \u002F (COMMAND + (\".exe\" if os.name == \"nt\" else \"\"))\n    assert exe.exists(), f\"console script {exe} was not installed\"\n    return exe\n\n\n@pytest.fixture\ndef cli(installed_cli: Path, tmp_path: Path) -> Callable[..., Run]:\n    \"\"\"Run the installed command in an isolated directory with a controlled environment.\"\"\"\n    home = tmp_path \u002F \"home\"\n    home.mkdir()\n\n    def run(*args: str, input: str | None = None, env: dict[str, str] | None = None,\n            timeout: float = 30) -> Run:\n        base = {\"PATH\": os.environ.get(\"PATH\", \"\"), \"HOME\": str(home), \"USERPROFILE\": str(home),\n                \"XDG_CONFIG_HOME\": str(home \u002F \".config\"), \"NO_COLOR\": \"1\",\n                \"SYSTEMROOT\": os.environ.get(\"SYSTEMROOT\", \"\")}\n        proc = subprocess.run([str(installed_cli), *args], cwd=tmp_path, input=input,\n                              capture_output=True, text=True, timeout=timeout,\n                              env={**base, **(env or {})})\n        return Run(proc.returncode, proc.stdout, proc.stderr)\n\n    return run\n","python","",[13,94,95,104,122,129,138,146,154,167,180,193,198,206,211,235,247,252,257,280,292,301,310,318,323,328,346,358,364,380,405,457,469,489,521,560,598,610,672,699,708,713,718,724,741,747,763,769,774,831,848,896,926,944,981,1010,1037,1046,1051],{"__ignoreMap":92},[96,97,100],"span",{"class":98,"line":99},"line",1,[96,101,103],{"class":102},"sJ8bj","# tests\u002Fe2e\u002Fconftest.py\n",[96,105,107,111,115,118],{"class":98,"line":106},2,[96,108,110],{"class":109},"szBVR","from",[96,112,114],{"class":113},"sj4cs"," __future__",[96,116,117],{"class":109}," import",[96,119,121],{"class":120},"sVt8B"," annotations\n",[96,123,125],{"class":98,"line":124},3,[96,126,128],{"emptyLinePlaceholder":127},true,"\n",[96,130,132,135],{"class":98,"line":131},4,[96,133,134],{"class":109},"import",[96,136,137],{"class":120}," os\n",[96,139,141,143],{"class":98,"line":140},5,[96,142,134],{"class":109},[96,144,145],{"class":120}," subprocess\n",[96,147,149,151],{"class":98,"line":148},6,[96,150,134],{"class":109},[96,152,153],{"class":120}," sys\n",[96,155,157,159,162,164],{"class":98,"line":156},7,[96,158,110],{"class":109},[96,160,161],{"class":120}," collections.abc ",[96,163,134],{"class":109},[96,165,166],{"class":120}," Callable\n",[96,168,170,172,175,177],{"class":98,"line":169},8,[96,171,110],{"class":109},[96,173,174],{"class":120}," dataclasses ",[96,176,134],{"class":109},[96,178,179],{"class":120}," dataclass\n",[96,181,183,185,188,190],{"class":98,"line":182},9,[96,184,110],{"class":109},[96,186,187],{"class":120}," pathlib ",[96,189,134],{"class":109},[96,191,192],{"class":120}," Path\n",[96,194,196],{"class":98,"line":195},10,[96,197,128],{"emptyLinePlaceholder":127},[96,199,201,203],{"class":98,"line":200},11,[96,202,134],{"class":109},[96,204,205],{"class":120}," pytest\n",[96,207,209],{"class":98,"line":208},12,[96,210,128],{"emptyLinePlaceholder":127},[96,212,214,217,220,223,226,229,232],{"class":98,"line":213},13,[96,215,216],{"class":113},"PROJECT",[96,218,219],{"class":109}," =",[96,221,222],{"class":120}," Path(",[96,224,225],{"class":113},"__file__",[96,227,228],{"class":120},").resolve().parents[",[96,230,231],{"class":113},"2",[96,233,234],{"class":120},"]\n",[96,236,238,241,243],{"class":98,"line":237},14,[96,239,240],{"class":113},"COMMAND",[96,242,219],{"class":109},[96,244,246],{"class":245},"sZZnC"," \"mytool\"\n",[96,248,250],{"class":98,"line":249},15,[96,251,128],{"emptyLinePlaceholder":127},[96,253,255],{"class":98,"line":254},16,[96,256,128],{"emptyLinePlaceholder":127},[96,258,260,264,267,271,274,277],{"class":98,"line":259},17,[96,261,263],{"class":262},"sScJk","@dataclass",[96,265,266],{"class":120},"(",[96,268,270],{"class":269},"s4XuR","frozen",[96,272,273],{"class":109},"=",[96,275,276],{"class":113},"True",[96,278,279],{"class":120},")\n",[96,281,283,286,289],{"class":98,"line":282},18,[96,284,285],{"class":109},"class",[96,287,288],{"class":262}," Run",[96,290,291],{"class":120},":\n",[96,293,295,298],{"class":98,"line":294},19,[96,296,297],{"class":120},"    code: ",[96,299,300],{"class":113},"int\n",[96,302,304,307],{"class":98,"line":303},20,[96,305,306],{"class":120},"    stdout: ",[96,308,309],{"class":113},"str\n",[96,311,313,316],{"class":98,"line":312},21,[96,314,315],{"class":120},"    stderr: ",[96,317,309],{"class":113},[96,319,321],{"class":98,"line":320},22,[96,322,128],{"emptyLinePlaceholder":127},[96,324,326],{"class":98,"line":325},23,[96,327,128],{"emptyLinePlaceholder":127},[96,329,331,334,336,339,341,344],{"class":98,"line":330},24,[96,332,333],{"class":262},"@pytest.fixture",[96,335,266],{"class":120},[96,337,338],{"class":269},"scope",[96,340,273],{"class":109},[96,342,343],{"class":245},"\"session\"",[96,345,279],{"class":120},[96,347,349,352,355],{"class":98,"line":348},25,[96,350,351],{"class":109},"def",[96,353,354],{"class":262}," installed_cli",[96,356,357],{"class":120},"(tmp_path_factory: pytest.TempPathFactory) -> Path:\n",[96,359,361],{"class":98,"line":360},26,[96,362,363],{"class":245},"    \"\"\"Build the wheel and install it into a fresh virtual environment; return the executable.\"\"\"\n",[96,365,367,370,372,375,378],{"class":98,"line":366},27,[96,368,369],{"class":120},"    root ",[96,371,273],{"class":109},[96,373,374],{"class":120}," tmp_path_factory.mktemp(",[96,376,377],{"class":245},"\"e2e\"",[96,379,279],{"class":120},[96,381,383,386,388,391,394,397,400,402],{"class":98,"line":382},28,[96,384,385],{"class":120},"    dist, venv ",[96,387,273],{"class":109},[96,389,390],{"class":120}," root ",[96,392,393],{"class":109},"\u002F",[96,395,396],{"class":245}," \"dist\"",[96,398,399],{"class":120},", root ",[96,401,393],{"class":109},[96,403,404],{"class":245}," \"venv\"\n",[96,406,408,411,414,417,420,422,425,427,430,432,435,438,441,443,445,447,450,452,454],{"class":98,"line":407},29,[96,409,410],{"class":120},"    subprocess.run([",[96,412,413],{"class":245},"\"uv\"",[96,415,416],{"class":120},", ",[96,418,419],{"class":245},"\"build\"",[96,421,416],{"class":120},[96,423,424],{"class":245},"\"--wheel\"",[96,426,416],{"class":120},[96,428,429],{"class":245},"\"--out-dir\"",[96,431,416],{"class":120},[96,433,434],{"class":113},"str",[96,436,437],{"class":120},"(dist)], ",[96,439,440],{"class":269},"cwd",[96,442,273],{"class":109},[96,444,216],{"class":113},[96,446,416],{"class":120},[96,448,449],{"class":269},"check",[96,451,273],{"class":109},[96,453,276],{"class":113},[96,455,456],{"class":120},",\n",[96,458,460,463,465,467],{"class":98,"line":459},30,[96,461,462],{"class":269},"                   capture_output",[96,464,273],{"class":109},[96,466,276],{"class":113},[96,468,279],{"class":120},[96,470,472,475,477,480,483,486],{"class":98,"line":471},31,[96,473,474],{"class":120},"    wheel ",[96,476,273],{"class":109},[96,478,479],{"class":113}," next",[96,481,482],{"class":120},"(dist.glob(",[96,484,485],{"class":245},"\"*.whl\"",[96,487,488],{"class":120},"))\n",[96,490,492,494,496,498,501,503,506,508,510,513,515,517,519],{"class":98,"line":491},32,[96,493,410],{"class":120},[96,495,413],{"class":245},[96,497,416],{"class":120},[96,499,500],{"class":245},"\"venv\"",[96,502,416],{"class":120},[96,504,505],{"class":245},"\"--quiet\"",[96,507,416],{"class":120},[96,509,434],{"class":113},[96,511,512],{"class":120},"(venv)], ",[96,514,449],{"class":269},[96,516,273],{"class":109},[96,518,276],{"class":113},[96,520,279],{"class":120},[96,522,524,527,529,532,534,537,540,543,546,549,552,555,558],{"class":98,"line":523},33,[96,525,526],{"class":120},"    python ",[96,528,273],{"class":109},[96,530,531],{"class":120}," venv ",[96,533,393],{"class":109},[96,535,536],{"class":120}," (",[96,538,539],{"class":245},"\"Scripts\u002Fpython.exe\"",[96,541,542],{"class":109}," if",[96,544,545],{"class":120}," os.name ",[96,547,548],{"class":109},"==",[96,550,551],{"class":245}," \"nt\"",[96,553,554],{"class":109}," else",[96,556,557],{"class":245}," \"bin\u002Fpython\"",[96,559,279],{"class":120},[96,561,563,565,567,569,572,574,577,579,581,583,586,588,590,593,595],{"class":98,"line":562},34,[96,564,410],{"class":120},[96,566,413],{"class":245},[96,568,416],{"class":120},[96,570,571],{"class":245},"\"pip\"",[96,573,416],{"class":120},[96,575,576],{"class":245},"\"install\"",[96,578,416],{"class":120},[96,580,505],{"class":245},[96,582,416],{"class":120},[96,584,585],{"class":245},"\"--python\"",[96,587,416],{"class":120},[96,589,434],{"class":113},[96,591,592],{"class":120},"(python), ",[96,594,434],{"class":113},[96,596,597],{"class":120},"(wheel)],\n",[96,599,601,604,606,608],{"class":98,"line":600},35,[96,602,603],{"class":269},"                   check",[96,605,273],{"class":109},[96,607,276],{"class":113},[96,609,279],{"class":120},[96,611,613,616,618,620,622,624,627,629,631,633,635,637,640,643,645,647,649,652,654,657,659,661,663,665,667,670],{"class":98,"line":612},36,[96,614,615],{"class":120},"    exe ",[96,617,273],{"class":109},[96,619,531],{"class":120},[96,621,393],{"class":109},[96,623,536],{"class":120},[96,625,626],{"class":245},"\"Scripts\"",[96,628,542],{"class":109},[96,630,545],{"class":120},[96,632,548],{"class":109},[96,634,551],{"class":245},[96,636,554],{"class":109},[96,638,639],{"class":245}," \"bin\"",[96,641,642],{"class":120},") ",[96,644,393],{"class":109},[96,646,536],{"class":120},[96,648,240],{"class":113},[96,650,651],{"class":109}," +",[96,653,536],{"class":120},[96,655,656],{"class":245},"\".exe\"",[96,658,542],{"class":109},[96,660,545],{"class":120},[96,662,548],{"class":109},[96,664,551],{"class":245},[96,666,554],{"class":109},[96,668,669],{"class":245}," \"\"",[96,671,488],{"class":120},[96,673,675,678,681,684,687,690,693,696],{"class":98,"line":674},37,[96,676,677],{"class":109},"    assert",[96,679,680],{"class":120}," exe.exists(), ",[96,682,683],{"class":109},"f",[96,685,686],{"class":245},"\"console script ",[96,688,689],{"class":113},"{",[96,691,692],{"class":120},"exe",[96,694,695],{"class":113},"}",[96,697,698],{"class":245}," was not installed\"\n",[96,700,702,705],{"class":98,"line":701},38,[96,703,704],{"class":109},"    return",[96,706,707],{"class":120}," exe\n",[96,709,711],{"class":98,"line":710},39,[96,712,128],{"emptyLinePlaceholder":127},[96,714,716],{"class":98,"line":715},40,[96,717,128],{"emptyLinePlaceholder":127},[96,719,721],{"class":98,"line":720},41,[96,722,723],{"class":262},"@pytest.fixture\n",[96,725,727,729,732,735,738],{"class":98,"line":726},42,[96,728,351],{"class":109},[96,730,731],{"class":262}," cli",[96,733,734],{"class":120},"(installed_cli: Path, tmp_path: Path) -> Callable[",[96,736,737],{"class":113},"...",[96,739,740],{"class":120},", Run]:\n",[96,742,744],{"class":98,"line":743},43,[96,745,746],{"class":245},"    \"\"\"Run the installed command in an isolated directory with a controlled environment.\"\"\"\n",[96,748,750,753,755,758,760],{"class":98,"line":749},44,[96,751,752],{"class":120},"    home ",[96,754,273],{"class":109},[96,756,757],{"class":120}," tmp_path ",[96,759,393],{"class":109},[96,761,762],{"class":245}," \"home\"\n",[96,764,766],{"class":98,"line":765},45,[96,767,768],{"class":120},"    home.mkdir()\n",[96,770,772],{"class":98,"line":771},46,[96,773,128],{"emptyLinePlaceholder":127},[96,775,777,780,783,785,788,791,793,796,798,801,804,806,808,811,813,815,817,820,823,825,827,829],{"class":98,"line":776},47,[96,778,779],{"class":109},"    def",[96,781,782],{"class":262}," run",[96,784,266],{"class":120},[96,786,787],{"class":109},"*",[96,789,790],{"class":120},"args: ",[96,792,434],{"class":113},[96,794,795],{"class":120},", input: ",[96,797,434],{"class":113},[96,799,800],{"class":109}," |",[96,802,803],{"class":113}," None",[96,805,219],{"class":109},[96,807,803],{"class":113},[96,809,810],{"class":120},", env: dict[",[96,812,434],{"class":113},[96,814,416],{"class":120},[96,816,434],{"class":113},[96,818,819],{"class":120},"] ",[96,821,822],{"class":109},"|",[96,824,803],{"class":113},[96,826,219],{"class":109},[96,828,803],{"class":113},[96,830,456],{"class":120},[96,832,834,837,840,842,845],{"class":98,"line":833},48,[96,835,836],{"class":120},"            timeout: ",[96,838,839],{"class":113},"float",[96,841,219],{"class":109},[96,843,844],{"class":113}," 30",[96,846,847],{"class":120},") -> Run:\n",[96,849,851,854,856,859,862,865,867,869,872,875,878,881,883,886,889,891,893],{"class":98,"line":850},49,[96,852,853],{"class":120},"        base ",[96,855,273],{"class":109},[96,857,858],{"class":120}," {",[96,860,861],{"class":245},"\"PATH\"",[96,863,864],{"class":120},": os.environ.get(",[96,866,861],{"class":245},[96,868,416],{"class":120},[96,870,871],{"class":245},"\"\"",[96,873,874],{"class":120},"), ",[96,876,877],{"class":245},"\"HOME\"",[96,879,880],{"class":120},": ",[96,882,434],{"class":113},[96,884,885],{"class":120},"(home), ",[96,887,888],{"class":245},"\"USERPROFILE\"",[96,890,880],{"class":120},[96,892,434],{"class":113},[96,894,895],{"class":120},"(home),\n",[96,897,899,902,904,906,909,911,914,916,919,921,924],{"class":98,"line":898},50,[96,900,901],{"class":245},"                \"XDG_CONFIG_HOME\"",[96,903,880],{"class":120},[96,905,434],{"class":113},[96,907,908],{"class":120},"(home ",[96,910,393],{"class":109},[96,912,913],{"class":245}," \".config\"",[96,915,874],{"class":120},[96,917,918],{"class":245},"\"NO_COLOR\"",[96,920,880],{"class":120},[96,922,923],{"class":245},"\"1\"",[96,925,456],{"class":120},[96,927,929,932,934,937,939,941],{"class":98,"line":928},51,[96,930,931],{"class":245},"                \"SYSTEMROOT\"",[96,933,864],{"class":120},[96,935,936],{"class":245},"\"SYSTEMROOT\"",[96,938,416],{"class":120},[96,940,871],{"class":245},[96,942,943],{"class":120},")}\n",[96,945,947,950,952,955,957,960,962,965,967,969,972,975,977,979],{"class":98,"line":946},52,[96,948,949],{"class":120},"        proc ",[96,951,273],{"class":109},[96,953,954],{"class":120}," subprocess.run([",[96,956,434],{"class":113},[96,958,959],{"class":120},"(installed_cli), ",[96,961,787],{"class":109},[96,963,964],{"class":120},"args], ",[96,966,440],{"class":269},[96,968,273],{"class":109},[96,970,971],{"class":120},"tmp_path, ",[96,973,974],{"class":269},"input",[96,976,273],{"class":109},[96,978,974],{"class":113},[96,980,456],{"class":120},[96,982,984,987,989,991,993,996,998,1000,1002,1005,1007],{"class":98,"line":983},53,[96,985,986],{"class":269},"                              capture_output",[96,988,273],{"class":109},[96,990,276],{"class":113},[96,992,416],{"class":120},[96,994,995],{"class":269},"text",[96,997,273],{"class":109},[96,999,276],{"class":113},[96,1001,416],{"class":120},[96,1003,1004],{"class":269},"timeout",[96,1006,273],{"class":109},[96,1008,1009],{"class":120},"timeout,\n",[96,1011,1013,1016,1018,1020,1023,1026,1028,1031,1034],{"class":98,"line":1012},54,[96,1014,1015],{"class":269},"                              env",[96,1017,273],{"class":109},[96,1019,689],{"class":120},[96,1021,1022],{"class":109},"**",[96,1024,1025],{"class":120},"base, ",[96,1027,1022],{"class":109},[96,1029,1030],{"class":120},"(env ",[96,1032,1033],{"class":109},"or",[96,1035,1036],{"class":120}," {})})\n",[96,1038,1040,1043],{"class":98,"line":1039},55,[96,1041,1042],{"class":109},"        return",[96,1044,1045],{"class":120}," Run(proc.returncode, proc.stdout, proc.stderr)\n",[96,1047,1049],{"class":98,"line":1048},56,[96,1050,128],{"emptyLinePlaceholder":127},[96,1052,1054,1056],{"class":98,"line":1053},57,[96,1055,704],{"class":109},[96,1057,1058],{"class":120}," run\n",[10,1060,1061],{},"Three isolation decisions matter:",[38,1063,1064,1078,1084],{},[41,1065,1066,1069,1070,1073,1074,1077],{},[18,1067,1068],{},"A fresh virtual environment with only the wheel installed."," No development dependencies, no editable install, no source tree on ",[13,1071,1072],{},"sys.path",". If a runtime dependency is missing from ",[13,1075,1076],{},"pyproject.toml",", the command fails here — as it would for users.",[41,1079,1080,1083],{},[18,1081,1082],{},"The working directory is a temporary directory",", so Python cannot import the package from the checkout by accident.",[41,1085,1086,1089,1090,1093,1094,1097,1098,1101],{},[18,1087,1088],{},"A controlled environment."," A throwaway ",[13,1091,1092],{},"HOME"," (and ",[13,1095,1096],{},"USERPROFILE"," on Windows) keeps tests from reading or writing the developer's real config, keyring or cache; ",[13,1099,1100],{},"NO_COLOR"," makes output deterministic.",[33,1103,1105],{"id":1104},"the-recipe-tests-that-need-a-real-process","The recipe: tests that need a real process",[87,1107,1109],{"className":89,"code":1108,"language":91,"meta":92,"style":92},"# tests\u002Fe2e\u002Ftest_installed.py\nimport json\nimport re\nimport signal\nimport subprocess\nimport sys\nimport time\n\nimport pytest\n\npytestmark = pytest.mark.e2e\n\n\ndef test_version_and_help(cli):\n    r = cli(\"--version\")\n    assert r.code == 0 and re.fullmatch(r\"mytool \\d+\\.\\d+\\.\\d+.*\\n\", r.stdout)\n    assert cli(\"--help\").code == 0\n\n\ndef test_usage_error_exit_status(cli):\n    r = cli(\"--no-such-flag\")\n    assert r.code == 2\n    assert r.stdout == \"\" and \"No such option\" in r.stderr     # errors on stderr, not stdout\n\n\ndef test_json_output_is_clean_stdout(cli):\n    r = cli(\"site\", \"list\", \"--json\", \"-v\")\n    assert r.code == 0\n    json.loads(r.stdout)                                      # verbose chatter went to stderr\n\n\ndef test_reads_stdin_from_a_pipe(cli):\n    r = cli(\"lint\", \"-\", input=\"name: web\\nreplicas: 2\\n\")\n    assert r.code == 0\n\n\ndef test_config_is_written_under_home(cli, tmp_path):\n    assert cli(\"config\", \"set\", \"region\", \"eu\").code == 0\n    assert (tmp_path \u002F \"home\" \u002F \".config\" \u002F \"mytool\" \u002F \"config.toml\").exists()\n\n\n@pytest.mark.skipif(sys.platform == \"win32\", reason=\"POSIX signals\")\ndef test_sigint_exits_130(installed_cli, tmp_path):\n    proc = subprocess.Popen([str(installed_cli), \"watch\", str(tmp_path)],\n                            stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)\n    time.sleep(1.0)\n    proc.send_signal(signal.SIGINT)\n    _, err = proc.communicate(timeout=10)\n    assert proc.returncode == 130\n    assert \"Traceback\" not in err\n",[13,1110,1111,1116,1123,1130,1137,1143,1149,1156,1160,1166,1170,1180,1184,1188,1198,1213,1273,1290,1294,1298,1307,1320,1331,1356,1360,1364,1373,1401,1411,1419,1423,1427,1436,1472,1482,1486,1490,1500,1530,1560,1564,1568,1593,1603,1627,1661,1671,1681,1700,1712],{"__ignoreMap":92},[96,1112,1113],{"class":98,"line":99},[96,1114,1115],{"class":102},"# tests\u002Fe2e\u002Ftest_installed.py\n",[96,1117,1118,1120],{"class":98,"line":106},[96,1119,134],{"class":109},[96,1121,1122],{"class":120}," json\n",[96,1124,1125,1127],{"class":98,"line":124},[96,1126,134],{"class":109},[96,1128,1129],{"class":120}," re\n",[96,1131,1132,1134],{"class":98,"line":131},[96,1133,134],{"class":109},[96,1135,1136],{"class":120}," signal\n",[96,1138,1139,1141],{"class":98,"line":140},[96,1140,134],{"class":109},[96,1142,145],{"class":120},[96,1144,1145,1147],{"class":98,"line":148},[96,1146,134],{"class":109},[96,1148,153],{"class":120},[96,1150,1151,1153],{"class":98,"line":156},[96,1152,134],{"class":109},[96,1154,1155],{"class":120}," time\n",[96,1157,1158],{"class":98,"line":169},[96,1159,128],{"emptyLinePlaceholder":127},[96,1161,1162,1164],{"class":98,"line":182},[96,1163,134],{"class":109},[96,1165,205],{"class":120},[96,1167,1168],{"class":98,"line":195},[96,1169,128],{"emptyLinePlaceholder":127},[96,1171,1172,1175,1177],{"class":98,"line":200},[96,1173,1174],{"class":120},"pytestmark ",[96,1176,273],{"class":109},[96,1178,1179],{"class":120}," pytest.mark.e2e\n",[96,1181,1182],{"class":98,"line":208},[96,1183,128],{"emptyLinePlaceholder":127},[96,1185,1186],{"class":98,"line":213},[96,1187,128],{"emptyLinePlaceholder":127},[96,1189,1190,1192,1195],{"class":98,"line":237},[96,1191,351],{"class":109},[96,1193,1194],{"class":262}," test_version_and_help",[96,1196,1197],{"class":120},"(cli):\n",[96,1199,1200,1203,1205,1208,1211],{"class":98,"line":249},[96,1201,1202],{"class":120},"    r ",[96,1204,273],{"class":109},[96,1206,1207],{"class":120}," cli(",[96,1209,1210],{"class":245},"\"--version\"",[96,1212,279],{"class":120},[96,1214,1215,1217,1220,1222,1225,1228,1231,1234,1237,1241,1244,1247,1251,1253,1255,1257,1259,1261,1263,1265,1268,1270],{"class":98,"line":254},[96,1216,677],{"class":109},[96,1218,1219],{"class":120}," r.code ",[96,1221,548],{"class":109},[96,1223,1224],{"class":113}," 0",[96,1226,1227],{"class":109}," and",[96,1229,1230],{"class":120}," re.fullmatch(",[96,1232,1233],{"class":109},"r",[96,1235,1236],{"class":245},"\"",[96,1238,1240],{"class":1239},"sA_wV","mytool ",[96,1242,1243],{"class":113},"\\d",[96,1245,1246],{"class":109},"+",[96,1248,1250],{"class":1249},"snhLl","\\.",[96,1252,1243],{"class":113},[96,1254,1246],{"class":109},[96,1256,1250],{"class":1249},[96,1258,1243],{"class":113},[96,1260,1246],{"class":109},[96,1262,31],{"class":113},[96,1264,787],{"class":109},[96,1266,1267],{"class":1249},"\\n",[96,1269,1236],{"class":245},[96,1271,1272],{"class":120},", r.stdout)\n",[96,1274,1275,1277,1279,1282,1285,1287],{"class":98,"line":259},[96,1276,677],{"class":109},[96,1278,1207],{"class":120},[96,1280,1281],{"class":245},"\"--help\"",[96,1283,1284],{"class":120},").code ",[96,1286,548],{"class":109},[96,1288,1289],{"class":113}," 0\n",[96,1291,1292],{"class":98,"line":282},[96,1293,128],{"emptyLinePlaceholder":127},[96,1295,1296],{"class":98,"line":294},[96,1297,128],{"emptyLinePlaceholder":127},[96,1299,1300,1302,1305],{"class":98,"line":303},[96,1301,351],{"class":109},[96,1303,1304],{"class":262}," test_usage_error_exit_status",[96,1306,1197],{"class":120},[96,1308,1309,1311,1313,1315,1318],{"class":98,"line":312},[96,1310,1202],{"class":120},[96,1312,273],{"class":109},[96,1314,1207],{"class":120},[96,1316,1317],{"class":245},"\"--no-such-flag\"",[96,1319,279],{"class":120},[96,1321,1322,1324,1326,1328],{"class":98,"line":320},[96,1323,677],{"class":109},[96,1325,1219],{"class":120},[96,1327,548],{"class":109},[96,1329,1330],{"class":113}," 2\n",[96,1332,1333,1335,1338,1340,1342,1344,1347,1350,1353],{"class":98,"line":325},[96,1334,677],{"class":109},[96,1336,1337],{"class":120}," r.stdout ",[96,1339,548],{"class":109},[96,1341,669],{"class":245},[96,1343,1227],{"class":109},[96,1345,1346],{"class":245}," \"No such option\"",[96,1348,1349],{"class":109}," in",[96,1351,1352],{"class":120}," r.stderr     ",[96,1354,1355],{"class":102},"# errors on stderr, not stdout\n",[96,1357,1358],{"class":98,"line":330},[96,1359,128],{"emptyLinePlaceholder":127},[96,1361,1362],{"class":98,"line":348},[96,1363,128],{"emptyLinePlaceholder":127},[96,1365,1366,1368,1371],{"class":98,"line":360},[96,1367,351],{"class":109},[96,1369,1370],{"class":262}," test_json_output_is_clean_stdout",[96,1372,1197],{"class":120},[96,1374,1375,1377,1379,1381,1384,1386,1389,1391,1394,1396,1399],{"class":98,"line":366},[96,1376,1202],{"class":120},[96,1378,273],{"class":109},[96,1380,1207],{"class":120},[96,1382,1383],{"class":245},"\"site\"",[96,1385,416],{"class":120},[96,1387,1388],{"class":245},"\"list\"",[96,1390,416],{"class":120},[96,1392,1393],{"class":245},"\"--json\"",[96,1395,416],{"class":120},[96,1397,1398],{"class":245},"\"-v\"",[96,1400,279],{"class":120},[96,1402,1403,1405,1407,1409],{"class":98,"line":382},[96,1404,677],{"class":109},[96,1406,1219],{"class":120},[96,1408,548],{"class":109},[96,1410,1289],{"class":113},[96,1412,1413,1416],{"class":98,"line":407},[96,1414,1415],{"class":120},"    json.loads(r.stdout)                                      ",[96,1417,1418],{"class":102},"# verbose chatter went to stderr\n",[96,1420,1421],{"class":98,"line":459},[96,1422,128],{"emptyLinePlaceholder":127},[96,1424,1425],{"class":98,"line":471},[96,1426,128],{"emptyLinePlaceholder":127},[96,1428,1429,1431,1434],{"class":98,"line":491},[96,1430,351],{"class":109},[96,1432,1433],{"class":262}," test_reads_stdin_from_a_pipe",[96,1435,1197],{"class":120},[96,1437,1438,1440,1442,1444,1447,1449,1452,1454,1456,1458,1461,1463,1466,1468,1470],{"class":98,"line":523},[96,1439,1202],{"class":120},[96,1441,273],{"class":109},[96,1443,1207],{"class":120},[96,1445,1446],{"class":245},"\"lint\"",[96,1448,416],{"class":120},[96,1450,1451],{"class":245},"\"-\"",[96,1453,416],{"class":120},[96,1455,974],{"class":269},[96,1457,273],{"class":109},[96,1459,1460],{"class":245},"\"name: web",[96,1462,1267],{"class":113},[96,1464,1465],{"class":245},"replicas: 2",[96,1467,1267],{"class":113},[96,1469,1236],{"class":245},[96,1471,279],{"class":120},[96,1473,1474,1476,1478,1480],{"class":98,"line":562},[96,1475,677],{"class":109},[96,1477,1219],{"class":120},[96,1479,548],{"class":109},[96,1481,1289],{"class":113},[96,1483,1484],{"class":98,"line":600},[96,1485,128],{"emptyLinePlaceholder":127},[96,1487,1488],{"class":98,"line":612},[96,1489,128],{"emptyLinePlaceholder":127},[96,1491,1492,1494,1497],{"class":98,"line":674},[96,1493,351],{"class":109},[96,1495,1496],{"class":262}," test_config_is_written_under_home",[96,1498,1499],{"class":120},"(cli, tmp_path):\n",[96,1501,1502,1504,1506,1509,1511,1514,1516,1519,1521,1524,1526,1528],{"class":98,"line":701},[96,1503,677],{"class":109},[96,1505,1207],{"class":120},[96,1507,1508],{"class":245},"\"config\"",[96,1510,416],{"class":120},[96,1512,1513],{"class":245},"\"set\"",[96,1515,416],{"class":120},[96,1517,1518],{"class":245},"\"region\"",[96,1520,416],{"class":120},[96,1522,1523],{"class":245},"\"eu\"",[96,1525,1284],{"class":120},[96,1527,548],{"class":109},[96,1529,1289],{"class":113},[96,1531,1532,1534,1537,1539,1542,1545,1547,1549,1552,1554,1557],{"class":98,"line":710},[96,1533,677],{"class":109},[96,1535,1536],{"class":120}," (tmp_path ",[96,1538,393],{"class":109},[96,1540,1541],{"class":245}," \"home\"",[96,1543,1544],{"class":109}," \u002F",[96,1546,913],{"class":245},[96,1548,1544],{"class":109},[96,1550,1551],{"class":245}," \"mytool\"",[96,1553,1544],{"class":109},[96,1555,1556],{"class":245}," \"config.toml\"",[96,1558,1559],{"class":120},").exists()\n",[96,1561,1562],{"class":98,"line":715},[96,1563,128],{"emptyLinePlaceholder":127},[96,1565,1566],{"class":98,"line":720},[96,1567,128],{"emptyLinePlaceholder":127},[96,1569,1570,1573,1576,1578,1581,1583,1586,1588,1591],{"class":98,"line":726},[96,1571,1572],{"class":262},"@pytest.mark.skipif",[96,1574,1575],{"class":120},"(sys.platform ",[96,1577,548],{"class":109},[96,1579,1580],{"class":245}," \"win32\"",[96,1582,416],{"class":120},[96,1584,1585],{"class":269},"reason",[96,1587,273],{"class":109},[96,1589,1590],{"class":245},"\"POSIX signals\"",[96,1592,279],{"class":120},[96,1594,1595,1597,1600],{"class":98,"line":743},[96,1596,351],{"class":109},[96,1598,1599],{"class":262}," test_sigint_exits_130",[96,1601,1602],{"class":120},"(installed_cli, tmp_path):\n",[96,1604,1605,1608,1610,1613,1615,1617,1620,1622,1624],{"class":98,"line":749},[96,1606,1607],{"class":120},"    proc ",[96,1609,273],{"class":109},[96,1611,1612],{"class":120}," subprocess.Popen([",[96,1614,434],{"class":113},[96,1616,959],{"class":120},[96,1618,1619],{"class":245},"\"watch\"",[96,1621,416],{"class":120},[96,1623,434],{"class":113},[96,1625,1626],{"class":120},"(tmp_path)],\n",[96,1628,1629,1632,1634,1637,1640,1642,1645,1647,1649,1651,1653,1655,1657,1659],{"class":98,"line":765},[96,1630,1631],{"class":269},"                            stdout",[96,1633,273],{"class":109},[96,1635,1636],{"class":120},"subprocess.",[96,1638,1639],{"class":113},"PIPE",[96,1641,416],{"class":120},[96,1643,1644],{"class":269},"stderr",[96,1646,273],{"class":109},[96,1648,1636],{"class":120},[96,1650,1639],{"class":113},[96,1652,416],{"class":120},[96,1654,995],{"class":269},[96,1656,273],{"class":109},[96,1658,276],{"class":113},[96,1660,279],{"class":120},[96,1662,1663,1666,1669],{"class":98,"line":771},[96,1664,1665],{"class":120},"    time.sleep(",[96,1667,1668],{"class":113},"1.0",[96,1670,279],{"class":120},[96,1672,1673,1676,1679],{"class":98,"line":776},[96,1674,1675],{"class":120},"    proc.send_signal(signal.",[96,1677,1678],{"class":113},"SIGINT",[96,1680,279],{"class":120},[96,1682,1683,1686,1688,1691,1693,1695,1698],{"class":98,"line":833},[96,1684,1685],{"class":120},"    _, err ",[96,1687,273],{"class":109},[96,1689,1690],{"class":120}," proc.communicate(",[96,1692,1004],{"class":269},[96,1694,273],{"class":109},[96,1696,1697],{"class":113},"10",[96,1699,279],{"class":120},[96,1701,1702,1704,1707,1709],{"class":98,"line":850},[96,1703,677],{"class":109},[96,1705,1706],{"class":120}," proc.returncode ",[96,1708,548],{"class":109},[96,1710,1711],{"class":113}," 130\n",[96,1713,1714,1716,1719,1722,1724],{"class":98,"line":898},[96,1715,677],{"class":109},[96,1717,1718],{"class":245}," \"Traceback\"",[96,1720,1721],{"class":109}," not",[96,1723,1349],{"class":109},[96,1725,1726],{"class":120}," err\n",[10,1728,1729,1730,1732],{},"Each test checks something ",[13,1731,15],{}," cannot: the installed launcher and version metadata, the real separation of stdout and stderr, stdin from an actual pipe, files written relative to a real home directory, and signal handling in a real process. Adjust the commands to your own tool; the categories are what matter.",[10,1734,1735,1736,1739,1740,1742,1743,1746],{},"Mark them (",[13,1737,1738],{},"pytest.mark.e2e",") and register the marker in ",[13,1741,1076],{},", so developers can run ",[13,1744,1745],{},"pytest -m \"not e2e\""," for a fast loop and CI runs everything.",[1748,1749,1751],"h3",{"id":1750},"keeping-end-to-end-tests-deterministic","Keeping end-to-end tests deterministic",[10,1753,1754],{},"Process-level tests are more exposed to the machine they run on than in-process ones, and flakiness here quickly erodes trust in the whole suite. A few rules keep them stable:",[38,1756,1757,1771,1777,1783,1793],{},[41,1758,1759,1762,1763,1766,1767,1770],{},[18,1760,1761],{},"Control every input."," The environment dictionary in the ",[13,1764,1765],{},"cli"," fixture starts almost empty on purpose: no inherited ",[13,1768,1769],{},"MYTOOL_*"," variables, no developer config, no proxy settings leaking in. Add variables explicitly per test.",[41,1772,1773,1776],{},[18,1774,1775],{},"Never depend on the network."," If a command talks to an API, give the CLI a base-URL setting and point it at a local fake server started by a fixture, so tests pass on an aeroplane and in locked-down CI.",[41,1778,1779,1782],{},[18,1780,1781],{},"Avoid fixed sleeps where possible."," The signal test above sleeps for a second to let the process start; for anything more complex, have the command print a \"ready\" line and wait for it on the pipe instead.",[41,1784,1785,1788,1789,1792],{},[18,1786,1787],{},"Set timeouts on every subprocess call."," A command that unexpectedly waits for input will otherwise hang the whole suite; ",[13,1790,1791],{},"timeout=30"," turns it into a clear failure.",[41,1794,1795,1798,1799,31],{},[18,1796,1797],{},"Run on every platform you ship to."," End-to-end tests are where Windows path, encoding and launcher differences surface, so include them in the Windows and macOS jobs of the matrix in ",[27,1800,1802],{"href":1801},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions\u002F","testing a CLI across Python versions with GitHub Actions",[33,1804,1806],{"id":1805},"ux-considerations","UX considerations",[10,1808,1809],{},"End-to-end tests protect the experience users actually have:",[38,1811,1812,1830,1836,1850],{},[41,1813,1814,1817,1818,1821,1822,1825,1826,31],{},[18,1815,1816],{},"Streams."," A test that parses ",[13,1819,1820],{},"--json"," output with ",[13,1823,1824],{},"-v"," enabled proves that diagnostics never contaminate data — the property scripts depend on, explained in ",[27,1827,1829],{"href":1828},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002F","working with stdin, stdout and pipes",[41,1831,1832,1835],{},[18,1833,1834],{},"Exit statuses."," Scripts branch on them; the real process's status is the only one that counts.",[41,1837,1838,1841,1842,1845,1846,31],{},[18,1839,1840],{},"No tracebacks."," Asserting that stderr contains no ",[13,1843,1844],{},"Traceback"," for expected failures and interrupts guards the friendly-error behaviour described in ",[27,1847,1849],{"href":1848},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks\u002F","friendly error messages and tracebacks",[41,1851,1852,1855,1856,1859],{},[18,1853,1854],{},"Startup time."," An end-to-end test can time ",[13,1857,1858],{},"--help"," and fail if it regresses past a budget — something users feel on every invocation. Keep the budget generous to avoid flaky failures on slow CI machines.",[33,1861,1863],{"id":1862},"testing-the-behaviour","Testing the behaviour",[10,1865,1866,1867,1870,1871,1875,1876,1879,1880,1882],{},"The end-to-end suite is itself the test, but two habits keep it trustworthy. First, ",[18,1868,1869],{},"run it against the artefact CI will publish"," — in CI, point the fixture at the wheel built by the build job rather than building again, exactly as in ",[27,1872,1874],{"href":1873},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci\u002F","smoke-testing the built wheel in CI",". Second, ",[18,1877,1878],{},"prove it can fail",": temporarily break the entry point in ",[13,1881,1076],{}," and confirm the session fixture fails with a clear message. An end-to-end suite that has never failed may not be testing the installed command at all.",[33,1884,1886],{"id":1885},"conclusion","Conclusion",[10,1888,1889,1890,1892],{},"Keep most CLI tests in-process and fast, and add a thin layer of end-to-end tests that run the installed command as a real process. Build and install once per session into an isolated environment with only runtime dependencies, run each test in a temporary directory with a controlled ",[13,1891,1092],{}," and environment, and use those tests for what only a process can show: entry points, exit statuses, stream separation, pipes, files in real locations and signals. They take seconds, and they catch the bugs users would otherwise find first.",[33,1894,1896],{"id":1895},"frequently-asked-questions","Frequently asked questions",[1748,1898,1900,1901,1904,1905,1908],{"id":1899},"why-not-just-use-subprocess-with-python-m-mytool","Why not just use ",[13,1902,1903],{},"subprocess"," with ",[13,1906,1907],{},"python -m mytool","?",[10,1910,1911,1914],{},[13,1912,1913],{},"python -m"," bypasses the console-script launcher and usually runs against the source tree, which is exactly what end-to-end tests should avoid. Run the installed executable.",[1748,1916,1918],{"id":1917},"how-slow-is-this","How slow is this?",[10,1920,1921],{},"Building and installing takes a few seconds once per session with uv; each test then costs a process start — typically 100–300 ms for a Python CLI. Twenty end-to-end tests add a few seconds in total.",[1748,1923,1925],{"id":1924},"can-i-test-interactive-prompts-end-to-end","Can I test interactive prompts end to end?",[10,1927,1928,1929,1932,1933,1936,1937,31],{},"For simple prompts, pass ",[13,1930,1931],{},"input="," and set an environment variable your tool honours to force prompting without a TTY. For real terminal behaviour, ",[13,1934,1935],{},"pexpect"," runs the command under a pseudo-terminal. Most prompt logic is better covered in-process, as in ",[27,1938,1940],{"href":1939},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin\u002F","testing interactive prompts and stdin",[1748,1942,1944],{"id":1943},"should-end-to-end-tests-hit-real-services","Should end-to-end tests hit real services?",[10,1946,1947],{},"No — point the CLI at a local fake (a small HTTP server fixture, or a base URL environment variable aimed at one) so tests stay fast and deterministic. Tests against real staging services belong in a separate, explicitly triggered job.",[33,1949,1951],{"id":1950},"related","Related",[38,1953,1954,1960,1965,1971,1977],{},[41,1955,1956,1957],{},"Up: ",[27,1958,1959],{"href":29},"Testing Python CLI applications",[41,1961,1962],{},[27,1963,1964],{"href":50},"Testing Click commands with CliRunner",[41,1966,1967],{},[27,1968,1970],{"href":1969},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis\u002F","Property-based testing CLI arguments with Hypothesis",[41,1972,1973],{},[27,1974,1976],{"href":1975},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output\u002F","Snapshot testing CLI output",[41,1978,1979],{},[27,1980,1981],{"href":1873},"Smoke-testing the built wheel in CI",[1983,1984,1985],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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":92,"searchDepth":106,"depth":106,"links":1987},[1988,1989,1990,1991,1994,1995,1996,1997,2004],{"id":35,"depth":106,"text":36},{"id":57,"depth":106,"text":58},{"id":74,"depth":106,"text":75},{"id":1104,"depth":106,"text":1105,"children":1992},[1993],{"id":1750,"depth":124,"text":1751},{"id":1805,"depth":106,"text":1806},{"id":1862,"depth":106,"text":1863},{"id":1885,"depth":106,"text":1886},{"id":1895,"depth":106,"text":1896,"children":1998},[1999,2001,2002,2003],{"id":1899,"depth":124,"text":2000},"Why not just use subprocess with python -m mytool?",{"id":1917,"depth":124,"text":1918},{"id":1924,"depth":124,"text":1925},{"id":1943,"depth":124,"text":1944},{"id":1950,"depth":106,"text":1951},"2026-09-18","Test a Python CLI the way users run it: build the wheel once, install it in an isolated environment, run the real command in subprocesses and check codes, streams and files.","intermediate",false,"md",{},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli",{"title":5,"description":2006},"modern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli\u002Findex",[2015,2016,1903,2017],"testing","pytest","packaging","BdYxvNu9kz1_jJJlyDXFFPk8LwDse5A5Unn8tjSw_gM",[2020,2023,2026,2029,2032,2035,2038,2041,2044,2047,2050,2053,2056,2059,2062,2065,2068,2071,2074,2077,2080,2083,2086,2089,2092,2095,2098,2101,2104,2107,2110,2113,2116,2119,2122,2125,2128,2131,2134,2137,2140,2143,2146,2149,2152,2155,2158,2161,2164,2167,2170,2173,2176,2179,2182,2185,2188,2191,2194,2197,2200,2203,2206,2209,2212,2215,2218,2221,2224,2227,2230,2233,2236,2239,2242,2245,2248,2251,2254,2257,2260,2263,2266,2269,2272,2275,2278,2281,2284,2287,2290,2293,2295,2298,2301,2304,2307,2310,2313,2316,2319,2322,2325,2328,2331,2334,2337,2340,2343,2346,2349,2352,2355,2358,2361,2364,2367,2370,2373,2376,2379,2380,2383,2386,2389,2392,2395,2398,2401,2404,2407,2410,2413,2416,2419,2422,2425,2428,2431,2434,2437,2440,2443,2446,2449,2452,2455,2458,2461,2464,2467,2470,2473,2476,2479,2482,2485,2488,2491,2494,2497,2500,2503,2506,2509,2512,2515,2518,2521,2524,2527,2530,2533,2536,2539,2542,2545,2548,2551,2554,2557,2560,2563],{"path":2021,"title":2022},"\u002Fabout","About Python CLI Toolcraft",{"path":2024,"title":2025},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2027,"title":2028},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2030,"title":2031},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2033,"title":2034},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2036,"title":2037},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2039,"title":2040},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbuilding-your-first-textual-app","Building Your First Textual App for a Python CLI",{"path":2042,"title":2043},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fchoosing-between-a-cli-a-prompt-flow-and-a-tui","Choosing Between a CLI, a Prompt Flow and a TUI",{"path":2045,"title":2046},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2048,"title":2049},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2051,"title":2052},"\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":2054,"title":2055},"\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":2057,"title":2058},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2060,"title":2061},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2063,"title":2064},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2066,"title":2067},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2069,"title":2070},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2072,"title":2073},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Ffixing-unicode-and-encoding-errors-on-windows","Fixing Unicode and Encoding Errors on Windows in Python CLIs",{"path":2075,"title":2076},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2078,"title":2079},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Frespecting-no-color-and-force-color","Respecting NO_COLOR and FORCE_COLOR in Python CLIs",{"path":2081,"title":2082},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2084,"title":2085},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdesigning-an-exception-hierarchy-for-a-cli","Designing an Exception Hierarchy for a Python CLI",{"path":2087,"title":2088},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2090,"title":2091},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2093,"title":2094},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2096,"title":2097},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Freporting-machine-readable-errors-in-json-mode","Reporting Machine-Readable Errors in JSON Mode",{"path":2099,"title":2100},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2102,"title":2103},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fdiscovering-project-config-files-by-walking-up-directories","Discovering Project Config Files by Walking Up Directories",{"path":2105,"title":2106},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2108,"title":2109},"\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":2111,"title":2112},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2114,"title":2115},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2117,"title":2118},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2120,"title":2121},"\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":2123,"title":2124},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2126,"title":2127},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2129,"title":2130},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2132,"title":2133},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2135,"title":2136},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2138,"title":2139},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fdynamic-completion-values-from-apis-and-files","Dynamic Completion Values from APIs and Files",{"path":2141,"title":2142},"\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":2144,"title":2145},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2147,"title":2148},"\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":2150,"title":2151},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2153,"title":2154},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-trace-ids-and-context-to-cli-logs","Adding Trace IDs and Context to Python CLI Logs",{"path":2156,"title":2157},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2159,"title":2160},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2162,"title":2163},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2165,"title":2166},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fwriting-rotating-log-files-from-a-cli","Writing Rotating Log Files from a Python CLI",{"path":2168,"title":2169},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2171,"title":2172},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2174,"title":2175},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2177,"title":2178},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2180,"title":2181},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fprocessing-large-files-and-ndjson-streams","Processing Large Files and NDJSON Streams in Python CLIs",{"path":2183,"title":2184},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2186,"title":2187},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fbuilding-an-api-client-cli-with-httpx","Building an API Client CLI with httpx",{"path":2189,"title":2190},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2192,"title":2193},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2195,"title":2196},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2198,"title":2199},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2201,"title":2202},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls","Retries and Backoff for CLI HTTP Calls",{"path":2204,"title":2205},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fcancelling-async-tasks-on-ctrl-c","Cancelling Async Tasks on Ctrl+C in Python CLIs",{"path":2207,"title":2208},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2210,"title":2211},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2213,"title":2214},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2216,"title":2217},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2219,"title":2220},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frunning-async-code-in-typer-and-click","Running Async Code in Typer and Click",{"path":2222,"title":2223},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2225,"title":2226},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2228,"title":2229},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2231,"title":2232},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2234,"title":2235},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2237,"title":2238},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2240,"title":2241},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2243,"title":2244},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fbuilding-a-watch-mode-with-watchfiles","Building a Watch Mode with watchfiles in Python",{"path":2246,"title":2247},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2249,"title":2250},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhealth-checks-and-heartbeats-for-long-running-clis","Health Checks and Heartbeats for Long-Running CLIs",{"path":2252,"title":2253},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2255,"title":2256},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-on-a-schedule-with-cron-and-systemd","Running a Python CLI on a Schedule with cron and systemd",{"path":2258,"title":2259},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2261,"title":2262},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2264,"title":2265},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2267,"title":2268},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2270,"title":2271},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2273,"title":2274},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli","Wrapping git and Other Tools from a Python CLI",{"path":2276,"title":2277},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2279,"title":2280},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2282,"title":2283},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files","Reading Secrets from Env Vars and Files in CLIs",{"path":2285,"title":2286},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs","Redacting Secrets from CLI Output and Logs",{"path":2288,"title":2289},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2291,"title":2292},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":393,"title":2294},"Python CLI Toolcraft",{"path":2296,"title":2297},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fcaching-expensive-work-between-cli-runs","Caching Expensive Work Between Python CLI Runs",{"path":2299,"title":2300},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2302,"title":2303},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2305,"title":2306},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2308,"title":2309},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2311,"title":2312},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2314,"title":2315},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2317,"title":2318},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2320,"title":2321},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2323,"title":2324},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2326,"title":2327},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2329,"title":2330},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fadding-dry-run-and-confirmation-to-destructive-commands","Adding Dry-Run and Confirmation to Destructive Commands",{"path":2332,"title":2333},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Ffollowing-posix-and-gnu-argument-conventions","Following POSIX and GNU Argument Conventions in Python",{"path":2335,"title":2336},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fglobal-options-vs-per-command-options","Global Options vs Per-Command Options in Python CLIs",{"path":2338,"title":2339},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2341,"title":2342},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2344,"title":2345},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2347,"title":2348},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2350,"title":2351},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2353,"title":2354},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2356,"title":2357},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2359,"title":2360},"\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":2362,"title":2363},"\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":2365,"title":2366},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2368,"title":2369},"\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":2371,"title":2372},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2374,"title":2375},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2377,"title":2378},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2011,"title":5},{"path":2381,"title":2382},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2384,"title":2385},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2387,"title":2388},"\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":2390,"title":2391},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2393,"title":2394},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2396,"title":2397},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2399,"title":2400},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2402,"title":2403},"\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":2405,"title":2406},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2408,"title":2409},"\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":2411,"title":2412},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2414,"title":2415},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2417,"title":2418},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2420,"title":2421},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2423,"title":2424},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2426,"title":2427},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2429,"title":2430},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-to-pypi-with-trusted-publishing","Publishing a CLI to PyPI with Trusted Publishing",{"path":2432,"title":2433},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci","Smoke-Testing the Built Wheel of a Python CLI in CI",{"path":2435,"title":2436},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions","Testing a CLI Across Python Versions in GitHub Actions",{"path":2438,"title":2439},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2441,"title":2442},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2444,"title":2445},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2447,"title":2448},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2450,"title":2451},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2453,"title":2454},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2456,"title":2457},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2459,"title":2460},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2462,"title":2463},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2465,"title":2466},"\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":2468,"title":2469},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2471,"title":2472},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2474,"title":2475},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fenforcing-import-boundaries-in-a-cli-codebase","Enforcing Import Boundaries in a Python CLI Codebase",{"path":2477,"title":2478},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2480,"title":2481},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ftype-checking-click-and-typer-code-with-mypy","Type-Checking Click and Typer Code with mypy",{"path":2483,"title":2484},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2486,"title":2487},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs","Deriving CLI Versions from Git Tags with hatch-vcs",{"path":2489,"title":2490},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2492,"title":2493},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2495,"title":2496},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2498,"title":2499},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2501,"title":2502},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2504,"title":2505},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2507,"title":2508},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2510,"title":2511},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2513,"title":2514},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fwriting-pyproject-toml-metadata-for-a-cli","Writing pyproject.toml Metadata for a Python CLI",{"path":2516,"title":2517},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2519,"title":2520},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmigrating-a-cli-from-poetry-to-uv","Migrating a Python CLI from Poetry to uv",{"path":2522,"title":2523},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2525,"title":2526},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2528,"title":2529},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2531,"title":2532},"\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":2534,"title":2535},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fshipping-your-cli-as-a-pre-commit-hook","Shipping Your Python CLI as a pre-commit Hook",{"path":2537,"title":2538},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python","Writing Local pre-commit Hooks in Python",{"path":2540,"title":2541},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2543,"title":2544},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Frunning-one-off-cli-scripts-with-uv-run","Running One-Off CLI Scripts with uv run and PEP 723",{"path":2546,"title":2547},"\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":2549,"title":2550},"\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":2552,"title":2553},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2555,"title":2556},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2558,"title":2559},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2561,"title":2562},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2564,"title":2565},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736907442]