[{"data":1,"prerenderedAt":2197},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci\u002F":3,"content-directory":1650},{"id":4,"title":5,"body":6,"date":1635,"description":1636,"difficulty":1637,"draft":1638,"extension":1639,"meta":1640,"navigation":172,"path":1641,"seo":1642,"stem":1643,"tags":1644,"updated":1635,"__hash__":1649},"content\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci\u002Findex.md","Smoke-Testing the Built Wheel of a Python CLI in CI",{"type":7,"value":8,"toc":1616},"minimark",[9,49,54,93,97,108,112,115,122,131,923,952,956,959,962,1239,1274,1289,1292,1296,1299,1350,1354,1360,1449,1455,1474,1478,1484,1488,1497,1503,1507,1514,1518,1535,1539,1561,1565,1578,1582,1612],[10,11,12,13,17,18,21,22,25,26,29,30,34,35,38,39,42,43,48],"p",{},"Every test passed. The release went out. The first user to run ",[14,15,16],"code",{},"mytool init"," got ",[14,19,20],{},"FileNotFoundError: templates\u002Fdefault.toml",", because the template directory was never included in the wheel. Or the entry point said ",[14,23,24],{},"mytool.cli:mian",". Or ",[14,27,28],{},"rich"," was listed under development dependencies, where it happened to be installed for every test run. These bugs share a cause: the tests ran against your ",[31,32,33],"strong",{},"source checkout",", where everything is present and importable, while users install a ",[31,36,37],{},"wheel",", which contains only what the build configuration says. A smoke test closes that gap by installing the artefact you are about to publish into a clean environment and running the command the way a user would. This guide builds that test for a CLI, including a ",[14,40,41],{},"doctor"," command that makes it thorough, and wires it into CI between the build and publish jobs. It is part of the ",[44,45,47],"a",{"href":46},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002F","CI\u002FCD pipelines topic",".",[50,51,53],"h2",{"id":52},"prerequisites","Prerequisites",[55,56,57,73,84],"ul",{},[58,59,60,61,64,65,68,69,48],"li",{},"A CLI that builds with ",[14,62,63],{},"uv build"," into ",[14,66,67],{},"dist\u002F",". See ",[44,70,72],{"href":71},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis\u002F","building wheels and sdists for Python CLIs",[58,74,75,76,79,80,83],{},"A console-script entry point in ",[14,77,78],{},"pyproject.toml"," (",[14,81,82],{},"[project.scripts]",").",[58,85,86,87,89,90,48],{},"A CI pipeline with a build job that uploads ",[14,88,67],{}," as an artefact, as in ",[44,91,92],{"href":46},"the topic overview",[50,94,96],{"id":95},"what-unit-tests-cannot-see","What unit tests cannot see",[10,98,99,100,103,104,107],{},"When pytest runs in your repository, ",[14,101,102],{},"import mytool"," resolves to ",[14,105,106],{},"src\u002Fmytool"," (or the editable install of it), every non-Python file in the tree is on disk next to the code, and the environment contains every development dependency. None of that is true for a user.",[109,110],"inline-diagram",{"name":111},"ci-src-vs-wheel",[10,113,114],{},"Each of those bugs is invisible to any test that imports from the checkout, and each is caught instantly by installing the wheel somewhere clean and running the command once. That is the whole idea of a smoke test: not a second test suite, just enough real usage of the real artefact to prove the package is whole.",[50,116,118,119,121],{"id":117},"the-recipe-part-1-a-doctor-command","The recipe, part 1: a ",[14,120,41],{}," command",[10,123,124,127,128,130],{},[14,125,126],{},"mytool --version"," proves the entry point and the top-level import work. It does not prove that the templates, schemas, plugins and lazily imported modules are all present. A small ",[14,129,41],{}," subcommand that touches every packaged resource makes the smoke test meaningful — and is useful to users debugging their installation too:",[132,133,138],"pre",{"className":134,"code":135,"language":136,"meta":137,"style":137},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fdoctor.py\nfrom __future__ import annotations\n\nimport importlib\nimport json\nfrom importlib import metadata, resources\n\nimport typer\n\nLAZY_MODULES = [\"mytool.commands.deploy\", \"mytool.commands.report\", \"mytool.render\"]\n\n\ndef check_templates() -> str:\n    root = resources.files(\"mytool\") \u002F \"templates\"\n    names = sorted(p.name for p in root.iterdir() if p.name.endswith(\".toml\"))\n    if not names:\n        raise RuntimeError(\"no templates packaged\")\n    return f\"{len(names)} found\"\n\n\ndef check_schema() -> str:\n    schema = json.loads((resources.files(\"mytool\") \u002F \"schema.json\").read_text(encoding=\"utf-8\"))\n    return f\"ok ({len(schema.get('properties', {}))} properties)\"\n\n\ndef check_modules() -> str:\n    for name in LAZY_MODULES:\n        importlib.import_module(name)\n    return f\"{len(LAZY_MODULES)} imported\"\n\n\ndef check_plugins() -> str:\n    eps = metadata.entry_points(group=\"mytool.plugins\")\n    for ep in eps:\n        ep.load()\n    return f\"{len(eps)} loaded\"\n\n\nCHECKS = {\"templates\": check_templates, \"config schema\": check_schema,\n          \"lazy modules\": check_modules, \"plugins\": check_plugins}\n\n\ndef doctor() -> None:\n    \"\"\"Check that this installation is complete.\"\"\"\n    failed = False\n    for label, check in CHECKS.items():\n        try:\n            typer.echo(f\"{label:\u003C14} {check()}\")\n        except Exception as exc:\n            failed = True\n            typer.secho(f\"{label:\u003C14} FAILED: {type(exc).__name__}: {exc}\", fg=\"red\", err=True)\n    typer.echo(f\"mytool {metadata.version('mytool')}\")\n    raise typer.Exit(1 if failed else 0)\n","python","",[14,139,140,149,167,174,183,191,204,209,217,222,252,257,262,281,305,343,355,373,397,402,407,421,455,481,486,491,505,521,527,550,555,560,574,595,608,614,633,638,643,666,681,686,691,706,712,723,739,747,780,795,806,870,897],{"__ignoreMap":137},[141,142,145],"span",{"class":143,"line":144},"line",1,[141,146,148],{"class":147},"sJ8bj","# src\u002Fmytool\u002Fdoctor.py\n",[141,150,152,156,160,163],{"class":143,"line":151},2,[141,153,155],{"class":154},"szBVR","from",[141,157,159],{"class":158},"sj4cs"," __future__",[141,161,162],{"class":154}," import",[141,164,166],{"class":165},"sVt8B"," annotations\n",[141,168,170],{"class":143,"line":169},3,[141,171,173],{"emptyLinePlaceholder":172},true,"\n",[141,175,177,180],{"class":143,"line":176},4,[141,178,179],{"class":154},"import",[141,181,182],{"class":165}," importlib\n",[141,184,186,188],{"class":143,"line":185},5,[141,187,179],{"class":154},[141,189,190],{"class":165}," json\n",[141,192,194,196,199,201],{"class":143,"line":193},6,[141,195,155],{"class":154},[141,197,198],{"class":165}," importlib ",[141,200,179],{"class":154},[141,202,203],{"class":165}," metadata, resources\n",[141,205,207],{"class":143,"line":206},7,[141,208,173],{"emptyLinePlaceholder":172},[141,210,212,214],{"class":143,"line":211},8,[141,213,179],{"class":154},[141,215,216],{"class":165}," typer\n",[141,218,220],{"class":143,"line":219},9,[141,221,173],{"emptyLinePlaceholder":172},[141,223,225,228,231,234,238,241,244,246,249],{"class":143,"line":224},10,[141,226,227],{"class":158},"LAZY_MODULES",[141,229,230],{"class":154}," =",[141,232,233],{"class":165}," [",[141,235,237],{"class":236},"sZZnC","\"mytool.commands.deploy\"",[141,239,240],{"class":165},", ",[141,242,243],{"class":236},"\"mytool.commands.report\"",[141,245,240],{"class":165},[141,247,248],{"class":236},"\"mytool.render\"",[141,250,251],{"class":165},"]\n",[141,253,255],{"class":143,"line":254},11,[141,256,173],{"emptyLinePlaceholder":172},[141,258,260],{"class":143,"line":259},12,[141,261,173],{"emptyLinePlaceholder":172},[141,263,265,268,272,275,278],{"class":143,"line":264},13,[141,266,267],{"class":154},"def",[141,269,271],{"class":270},"sScJk"," check_templates",[141,273,274],{"class":165},"() -> ",[141,276,277],{"class":158},"str",[141,279,280],{"class":165},":\n",[141,282,284,287,290,293,296,299,302],{"class":143,"line":283},14,[141,285,286],{"class":165},"    root ",[141,288,289],{"class":154},"=",[141,291,292],{"class":165}," resources.files(",[141,294,295],{"class":236},"\"mytool\"",[141,297,298],{"class":165},") ",[141,300,301],{"class":154},"\u002F",[141,303,304],{"class":236}," \"templates\"\n",[141,306,308,311,313,316,319,322,325,328,331,334,337,340],{"class":143,"line":307},15,[141,309,310],{"class":165},"    names ",[141,312,289],{"class":154},[141,314,315],{"class":158}," sorted",[141,317,318],{"class":165},"(p.name ",[141,320,321],{"class":154},"for",[141,323,324],{"class":165}," p ",[141,326,327],{"class":154},"in",[141,329,330],{"class":165}," root.iterdir() ",[141,332,333],{"class":154},"if",[141,335,336],{"class":165}," p.name.endswith(",[141,338,339],{"class":236},"\".toml\"",[141,341,342],{"class":165},"))\n",[141,344,346,349,352],{"class":143,"line":345},16,[141,347,348],{"class":154},"    if",[141,350,351],{"class":154}," not",[141,353,354],{"class":165}," names:\n",[141,356,358,361,364,367,370],{"class":143,"line":357},17,[141,359,360],{"class":154},"        raise",[141,362,363],{"class":158}," RuntimeError",[141,365,366],{"class":165},"(",[141,368,369],{"class":236},"\"no templates packaged\"",[141,371,372],{"class":165},")\n",[141,374,376,379,382,385,388,391,394],{"class":143,"line":375},18,[141,377,378],{"class":154},"    return",[141,380,381],{"class":154}," f",[141,383,384],{"class":236},"\"",[141,386,387],{"class":158},"{len",[141,389,390],{"class":165},"(names)",[141,392,393],{"class":158},"}",[141,395,396],{"class":236}," found\"\n",[141,398,400],{"class":143,"line":399},19,[141,401,173],{"emptyLinePlaceholder":172},[141,403,405],{"class":143,"line":404},20,[141,406,173],{"emptyLinePlaceholder":172},[141,408,410,412,415,417,419],{"class":143,"line":409},21,[141,411,267],{"class":154},[141,413,414],{"class":270}," check_schema",[141,416,274],{"class":165},[141,418,277],{"class":158},[141,420,280],{"class":165},[141,422,424,427,429,432,434,436,438,441,444,448,450,453],{"class":143,"line":423},22,[141,425,426],{"class":165},"    schema ",[141,428,289],{"class":154},[141,430,431],{"class":165}," json.loads((resources.files(",[141,433,295],{"class":236},[141,435,298],{"class":165},[141,437,301],{"class":154},[141,439,440],{"class":236}," \"schema.json\"",[141,442,443],{"class":165},").read_text(",[141,445,447],{"class":446},"s4XuR","encoding",[141,449,289],{"class":154},[141,451,452],{"class":236},"\"utf-8\"",[141,454,342],{"class":165},[141,456,458,460,462,465,467,470,473,476,478],{"class":143,"line":457},23,[141,459,378],{"class":154},[141,461,381],{"class":154},[141,463,464],{"class":236},"\"ok (",[141,466,387],{"class":158},[141,468,469],{"class":165},"(schema.get(",[141,471,472],{"class":236},"'properties'",[141,474,475],{"class":165},", {}))",[141,477,393],{"class":158},[141,479,480],{"class":236}," properties)\"\n",[141,482,484],{"class":143,"line":483},24,[141,485,173],{"emptyLinePlaceholder":172},[141,487,489],{"class":143,"line":488},25,[141,490,173],{"emptyLinePlaceholder":172},[141,492,494,496,499,501,503],{"class":143,"line":493},26,[141,495,267],{"class":154},[141,497,498],{"class":270}," check_modules",[141,500,274],{"class":165},[141,502,277],{"class":158},[141,504,280],{"class":165},[141,506,508,511,514,516,519],{"class":143,"line":507},27,[141,509,510],{"class":154},"    for",[141,512,513],{"class":165}," name ",[141,515,327],{"class":154},[141,517,518],{"class":158}," LAZY_MODULES",[141,520,280],{"class":165},[141,522,524],{"class":143,"line":523},28,[141,525,526],{"class":165},"        importlib.import_module(name)\n",[141,528,530,532,534,536,538,540,542,545,547],{"class":143,"line":529},29,[141,531,378],{"class":154},[141,533,381],{"class":154},[141,535,384],{"class":236},[141,537,387],{"class":158},[141,539,366],{"class":165},[141,541,227],{"class":158},[141,543,544],{"class":165},")",[141,546,393],{"class":158},[141,548,549],{"class":236}," imported\"\n",[141,551,553],{"class":143,"line":552},30,[141,554,173],{"emptyLinePlaceholder":172},[141,556,558],{"class":143,"line":557},31,[141,559,173],{"emptyLinePlaceholder":172},[141,561,563,565,568,570,572],{"class":143,"line":562},32,[141,564,267],{"class":154},[141,566,567],{"class":270}," check_plugins",[141,569,274],{"class":165},[141,571,277],{"class":158},[141,573,280],{"class":165},[141,575,577,580,582,585,588,590,593],{"class":143,"line":576},33,[141,578,579],{"class":165},"    eps ",[141,581,289],{"class":154},[141,583,584],{"class":165}," metadata.entry_points(",[141,586,587],{"class":446},"group",[141,589,289],{"class":154},[141,591,592],{"class":236},"\"mytool.plugins\"",[141,594,372],{"class":165},[141,596,598,600,603,605],{"class":143,"line":597},34,[141,599,510],{"class":154},[141,601,602],{"class":165}," ep ",[141,604,327],{"class":154},[141,606,607],{"class":165}," eps:\n",[141,609,611],{"class":143,"line":610},35,[141,612,613],{"class":165},"        ep.load()\n",[141,615,617,619,621,623,625,628,630],{"class":143,"line":616},36,[141,618,378],{"class":154},[141,620,381],{"class":154},[141,622,384],{"class":236},[141,624,387],{"class":158},[141,626,627],{"class":165},"(eps)",[141,629,393],{"class":158},[141,631,632],{"class":236}," loaded\"\n",[141,634,636],{"class":143,"line":635},37,[141,637,173],{"emptyLinePlaceholder":172},[141,639,641],{"class":143,"line":640},38,[141,642,173],{"emptyLinePlaceholder":172},[141,644,646,649,651,654,657,660,663],{"class":143,"line":645},39,[141,647,648],{"class":158},"CHECKS",[141,650,230],{"class":154},[141,652,653],{"class":165}," {",[141,655,656],{"class":236},"\"templates\"",[141,658,659],{"class":165},": check_templates, ",[141,661,662],{"class":236},"\"config schema\"",[141,664,665],{"class":165},": check_schema,\n",[141,667,669,672,675,678],{"class":143,"line":668},40,[141,670,671],{"class":236},"          \"lazy modules\"",[141,673,674],{"class":165},": check_modules, ",[141,676,677],{"class":236},"\"plugins\"",[141,679,680],{"class":165},": check_plugins}\n",[141,682,684],{"class":143,"line":683},41,[141,685,173],{"emptyLinePlaceholder":172},[141,687,689],{"class":143,"line":688},42,[141,690,173],{"emptyLinePlaceholder":172},[141,692,694,696,699,701,704],{"class":143,"line":693},43,[141,695,267],{"class":154},[141,697,698],{"class":270}," doctor",[141,700,274],{"class":165},[141,702,703],{"class":158},"None",[141,705,280],{"class":165},[141,707,709],{"class":143,"line":708},44,[141,710,711],{"class":236},"    \"\"\"Check that this installation is complete.\"\"\"\n",[141,713,715,718,720],{"class":143,"line":714},45,[141,716,717],{"class":165},"    failed ",[141,719,289],{"class":154},[141,721,722],{"class":158}," False\n",[141,724,726,728,731,733,736],{"class":143,"line":725},46,[141,727,510],{"class":154},[141,729,730],{"class":165}," label, check ",[141,732,327],{"class":154},[141,734,735],{"class":158}," CHECKS",[141,737,738],{"class":165},".items():\n",[141,740,742,745],{"class":143,"line":741},47,[141,743,744],{"class":154},"        try",[141,746,280],{"class":165},[141,748,750,753,756,758,761,764,767,769,771,774,776,778],{"class":143,"line":749},48,[141,751,752],{"class":165},"            typer.echo(",[141,754,755],{"class":154},"f",[141,757,384],{"class":236},[141,759,760],{"class":158},"{",[141,762,763],{"class":165},"label",[141,765,766],{"class":154},":\u003C14",[141,768,393],{"class":158},[141,770,653],{"class":158},[141,772,773],{"class":165},"check()",[141,775,393],{"class":158},[141,777,384],{"class":236},[141,779,372],{"class":165},[141,781,783,786,789,792],{"class":143,"line":782},49,[141,784,785],{"class":154},"        except",[141,787,788],{"class":158}," Exception",[141,790,791],{"class":154}," as",[141,793,794],{"class":165}," exc:\n",[141,796,798,801,803],{"class":143,"line":797},50,[141,799,800],{"class":165},"            failed ",[141,802,289],{"class":154},[141,804,805],{"class":158}," True\n",[141,807,809,812,814,816,818,820,822,824,827,830,833,836,839,841,844,846,848,850,853,855,858,860,863,865,868],{"class":143,"line":808},51,[141,810,811],{"class":165},"            typer.secho(",[141,813,755],{"class":154},[141,815,384],{"class":236},[141,817,760],{"class":158},[141,819,763],{"class":165},[141,821,766],{"class":154},[141,823,393],{"class":158},[141,825,826],{"class":236}," FAILED: ",[141,828,829],{"class":158},"{type",[141,831,832],{"class":165},"(exc).",[141,834,835],{"class":158},"__name__}",[141,837,838],{"class":236},": ",[141,840,760],{"class":158},[141,842,843],{"class":165},"exc",[141,845,393],{"class":158},[141,847,384],{"class":236},[141,849,240],{"class":165},[141,851,852],{"class":446},"fg",[141,854,289],{"class":154},[141,856,857],{"class":236},"\"red\"",[141,859,240],{"class":165},[141,861,862],{"class":446},"err",[141,864,289],{"class":154},[141,866,867],{"class":158},"True",[141,869,372],{"class":165},[141,871,873,876,878,881,883,886,889,891,893,895],{"class":143,"line":872},52,[141,874,875],{"class":165},"    typer.echo(",[141,877,755],{"class":154},[141,879,880],{"class":236},"\"mytool ",[141,882,760],{"class":158},[141,884,885],{"class":165},"metadata.version(",[141,887,888],{"class":236},"'mytool'",[141,890,544],{"class":165},[141,892,393],{"class":158},[141,894,384],{"class":236},[141,896,372],{"class":165},[141,898,900,903,906,909,912,915,918,921],{"class":143,"line":899},53,[141,901,902],{"class":154},"    raise",[141,904,905],{"class":165}," typer.Exit(",[141,907,908],{"class":158},"1",[141,910,911],{"class":154}," if",[141,913,914],{"class":165}," failed ",[141,916,917],{"class":154},"else",[141,919,920],{"class":158}," 0",[141,922,372],{"class":165},[10,924,925,926,929,930,933,934,937,938,942,943,947,948,951],{},"Register it with ",[14,927,928],{},"app.command()(doctor)",". Three details matter. Resources are read with ",[14,931,932],{},"importlib.resources.files()",", which works whether the package is a directory, a zip or a frozen binary — reading ",[14,935,936],{},"Path(__file__).parent \u002F \"templates\""," works from the checkout and fails in some install layouts; see ",[44,939,941],{"href":940},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources\u002F","bundling data files with importlib.resources",". Lazily imported subcommand modules are imported explicitly, because a CLI that ",[44,944,946],{"href":945},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup\u002F","lazy-loads subcommands"," would otherwise never import them during ",[14,949,950],{},"--help",". And the version comes from installed metadata, which proves the distribution's metadata is intact.",[50,953,955],{"id":954},"the-recipe-part-2-the-ci-job","The recipe, part 2: the CI job",[10,957,958],{},"The smoke job downloads the artefact built earlier, installs it into an isolated environment with no development dependencies, and runs it from a directory outside the checkout:",[109,960],{"name":961},"ci-smoke-flow",[132,963,967],{"className":964,"code":965,"language":966,"meta":137,"style":137},"language-yaml shiki shiki-themes github-light github-dark","  smoke:\n    needs: build\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n    runs-on: ${{ matrix.os }}\n    steps:\n      # Deliberately no checkout: the source tree must not be importable.\n      - uses: actions\u002Fdownload-artifact@v4\n        with:\n          name: dist\n          path: dist\n      - uses: astral-sh\u002Fsetup-uv@v6\n        with:\n          python-version: \"3.12\"\n      - name: Install the wheel as a tool\n        shell: bash\n        run: uv tool install dist\u002F*.whl\n      - name: Run it like a user would\n        shell: bash\n        working-directory: ${{ runner.temp }}\n        run: |\n          mytool --version\n          mytool --help > \u002Fdev\u002Fnull\n          mytool doctor\n      - name: The sdist must build and work too\n        shell: bash\n        run: |\n          uv tool uninstall mytool\n          uv tool install dist\u002F*.tar.gz\n          cd \"$RUNNER_TEMP\" && mytool doctor\n","yaml",[14,968,969,977,987,994,1001,1024,1034,1041,1046,1059,1066,1076,1085,1096,1102,1112,1124,1134,1144,1155,1163,1173,1182,1187,1192,1197,1208,1216,1224,1229,1234],{"__ignoreMap":137},[141,970,971,975],{"class":143,"line":144},[141,972,974],{"class":973},"s9eBZ","  smoke",[141,976,280],{"class":165},[141,978,979,982,984],{"class":143,"line":151},[141,980,981],{"class":973},"    needs",[141,983,838],{"class":165},[141,985,986],{"class":236},"build\n",[141,988,989,992],{"class":143,"line":169},[141,990,991],{"class":973},"    strategy",[141,993,280],{"class":165},[141,995,996,999],{"class":143,"line":176},[141,997,998],{"class":973},"      matrix",[141,1000,280],{"class":165},[141,1002,1003,1006,1009,1012,1014,1017,1019,1022],{"class":143,"line":185},[141,1004,1005],{"class":973},"        os",[141,1007,1008],{"class":165},": [",[141,1010,1011],{"class":236},"ubuntu-latest",[141,1013,240],{"class":165},[141,1015,1016],{"class":236},"windows-latest",[141,1018,240],{"class":165},[141,1020,1021],{"class":236},"macos-latest",[141,1023,251],{"class":165},[141,1025,1026,1029,1031],{"class":143,"line":193},[141,1027,1028],{"class":973},"    runs-on",[141,1030,838],{"class":165},[141,1032,1033],{"class":236},"${{ matrix.os }}\n",[141,1035,1036,1039],{"class":143,"line":206},[141,1037,1038],{"class":973},"    steps",[141,1040,280],{"class":165},[141,1042,1043],{"class":143,"line":211},[141,1044,1045],{"class":147},"      # Deliberately no checkout: the source tree must not be importable.\n",[141,1047,1048,1051,1054,1056],{"class":143,"line":219},[141,1049,1050],{"class":165},"      - ",[141,1052,1053],{"class":973},"uses",[141,1055,838],{"class":165},[141,1057,1058],{"class":236},"actions\u002Fdownload-artifact@v4\n",[141,1060,1061,1064],{"class":143,"line":224},[141,1062,1063],{"class":973},"        with",[141,1065,280],{"class":165},[141,1067,1068,1071,1073],{"class":143,"line":254},[141,1069,1070],{"class":973},"          name",[141,1072,838],{"class":165},[141,1074,1075],{"class":236},"dist\n",[141,1077,1078,1081,1083],{"class":143,"line":259},[141,1079,1080],{"class":973},"          path",[141,1082,838],{"class":165},[141,1084,1075],{"class":236},[141,1086,1087,1089,1091,1093],{"class":143,"line":264},[141,1088,1050],{"class":165},[141,1090,1053],{"class":973},[141,1092,838],{"class":165},[141,1094,1095],{"class":236},"astral-sh\u002Fsetup-uv@v6\n",[141,1097,1098,1100],{"class":143,"line":283},[141,1099,1063],{"class":973},[141,1101,280],{"class":165},[141,1103,1104,1107,1109],{"class":143,"line":307},[141,1105,1106],{"class":973},"          python-version",[141,1108,838],{"class":165},[141,1110,1111],{"class":236},"\"3.12\"\n",[141,1113,1114,1116,1119,1121],{"class":143,"line":345},[141,1115,1050],{"class":165},[141,1117,1118],{"class":973},"name",[141,1120,838],{"class":165},[141,1122,1123],{"class":236},"Install the wheel as a tool\n",[141,1125,1126,1129,1131],{"class":143,"line":357},[141,1127,1128],{"class":973},"        shell",[141,1130,838],{"class":165},[141,1132,1133],{"class":236},"bash\n",[141,1135,1136,1139,1141],{"class":143,"line":375},[141,1137,1138],{"class":973},"        run",[141,1140,838],{"class":165},[141,1142,1143],{"class":236},"uv tool install dist\u002F*.whl\n",[141,1145,1146,1148,1150,1152],{"class":143,"line":399},[141,1147,1050],{"class":165},[141,1149,1118],{"class":973},[141,1151,838],{"class":165},[141,1153,1154],{"class":236},"Run it like a user would\n",[141,1156,1157,1159,1161],{"class":143,"line":404},[141,1158,1128],{"class":973},[141,1160,838],{"class":165},[141,1162,1133],{"class":236},[141,1164,1165,1168,1170],{"class":143,"line":409},[141,1166,1167],{"class":973},"        working-directory",[141,1169,838],{"class":165},[141,1171,1172],{"class":236},"${{ runner.temp }}\n",[141,1174,1175,1177,1179],{"class":143,"line":423},[141,1176,1138],{"class":973},[141,1178,838],{"class":165},[141,1180,1181],{"class":154},"|\n",[141,1183,1184],{"class":143,"line":457},[141,1185,1186],{"class":236},"          mytool --version\n",[141,1188,1189],{"class":143,"line":483},[141,1190,1191],{"class":236},"          mytool --help > \u002Fdev\u002Fnull\n",[141,1193,1194],{"class":143,"line":488},[141,1195,1196],{"class":236},"          mytool doctor\n",[141,1198,1199,1201,1203,1205],{"class":143,"line":493},[141,1200,1050],{"class":165},[141,1202,1118],{"class":973},[141,1204,838],{"class":165},[141,1206,1207],{"class":236},"The sdist must build and work too\n",[141,1209,1210,1212,1214],{"class":143,"line":507},[141,1211,1128],{"class":973},[141,1213,838],{"class":165},[141,1215,1133],{"class":236},[141,1217,1218,1220,1222],{"class":143,"line":523},[141,1219,1138],{"class":973},[141,1221,838],{"class":165},[141,1223,1181],{"class":154},[141,1225,1226],{"class":143,"line":529},[141,1227,1228],{"class":236},"          uv tool uninstall mytool\n",[141,1230,1231],{"class":143,"line":552},[141,1232,1233],{"class":236},"          uv tool install dist\u002F*.tar.gz\n",[141,1235,1236],{"class":143,"line":557},[141,1237,1238],{"class":236},"          cd \"$RUNNER_TEMP\" && mytool doctor\n",[10,1240,1241,1242,1245,1246,1249,1250,1253,1254,1257,1258,1261,1262,1265,1266,1269,1270,1273],{},"Leaving out ",[14,1243,1244],{},"actions\u002Fcheckout"," is the strongest guarantee that nothing from the repository is on the import path. ",[14,1247,1248],{},"uv tool install"," creates a dedicated environment containing your package and its ",[31,1251,1252],{},"runtime"," dependencies only, and puts the ",[14,1255,1256],{},"mytool"," launcher on ",[14,1259,1260],{},"PATH"," — exactly what a user gets from ",[14,1263,1264],{},"uv tool install mytool"," or ",[14,1267,1268],{},"pipx install mytool",". Running from ",[14,1271,1272],{},"$RUNNER_TEMP"," avoids Python's habit of importing from the current directory.",[10,1275,1276,1277,1280,1281,1284,1285,1288],{},"The sdist step catches a different class of bug: a source distribution that is missing files needed to ",[31,1278,1279],{},"build"," (a ",[14,1282,1283],{},"README.md"," referenced by metadata, a ",[14,1286,1287],{},"LICENSE",", a build-time data file). Downstream packagers — Linux distributions, conda-forge, Homebrew — build from the sdist, so it is worth proving it works.",[109,1290],{"name":1291},"ci-smoke-terminal",[50,1293,1295],{"id":1294},"ux-considerations","UX considerations",[10,1297,1298],{},"The smoke test protects users directly, and a few choices make it more useful:",[55,1300,1301,1314,1324,1330,1344],{},[58,1302,1303,1309,1310,1313],{},[31,1304,1305,1306,1308],{},"Ship ",[14,1307,41],{}," as a public command."," When a user reports \"it doesn't work\", asking for ",[14,1311,1312],{},"mytool doctor"," output tells you in seconds whether the installation is broken, the environment is odd, or the bug is real.",[58,1315,1316,1319,1320,1323],{},[31,1317,1318],{},"Keep it fast and offline."," A doctor command that calls the network fails in air-gapped CI and on planes. Check local resources; offer a separate ",[14,1321,1322],{},"--online"," flag for connectivity checks if they are useful.",[58,1325,1326,1329],{},[31,1327,1328],{},"Make failures specific."," \"templates FAILED: FileNotFoundError: templates\" points straight at the packaging configuration.",[58,1331,1332,1335,1336,1339,1340,1343],{},[31,1333,1334],{},"Test the oldest supported Python in the smoke job too"," if your matrix is large; a wheel tagged ",[14,1337,1338],{},"py3-none-any"," should install everywhere ",[14,1341,1342],{},"requires-python"," allows.",[58,1345,1346,1349],{},[31,1347,1348],{},"Keep the list of lazy modules honest."," Generate it from your command registry rather than maintaining it by hand, so new subcommands are covered automatically.",[50,1351,1353],{"id":1352},"testing-the-behaviour","Testing the behaviour",[10,1355,1356,1357,1359],{},"The ",[14,1358,41],{}," command itself deserves ordinary unit tests, and you can reproduce the whole CI smoke test locally before pushing:",[132,1361,1365],{"className":1362,"code":1363,"language":1364,"meta":137,"style":137},"language-bash shiki shiki-themes github-light github-dark","# Build, then install into a throwaway tool environment and run from elsewhere.\nuv build\nuv tool install --force dist\u002Fmytool-*.whl\n(cd \"$(mktemp -d)\" && mytool --version && mytool doctor)\nuv tool uninstall mytool\n","bash",[14,1366,1367,1372,1380,1402,1437],{"__ignoreMap":137},[141,1368,1369],{"class":143,"line":144},[141,1370,1371],{"class":147},"# Build, then install into a throwaway tool environment and run from elsewhere.\n",[141,1373,1374,1377],{"class":143,"line":151},[141,1375,1376],{"class":270},"uv",[141,1378,1379],{"class":236}," build\n",[141,1381,1382,1384,1387,1390,1393,1396,1399],{"class":143,"line":169},[141,1383,1376],{"class":270},[141,1385,1386],{"class":236}," tool",[141,1388,1389],{"class":236}," install",[141,1391,1392],{"class":158}," --force",[141,1394,1395],{"class":236}," dist\u002Fmytool-",[141,1397,1398],{"class":158},"*",[141,1400,1401],{"class":236},".whl\n",[141,1403,1404,1406,1409,1412,1415,1418,1421,1424,1426,1429,1431,1433,1435],{"class":143,"line":176},[141,1405,366],{"class":165},[141,1407,1408],{"class":158},"cd",[141,1410,1411],{"class":236}," \"$(",[141,1413,1414],{"class":270},"mktemp",[141,1416,1417],{"class":158}," -d",[141,1419,1420],{"class":236},")\"",[141,1422,1423],{"class":165}," && ",[141,1425,1256],{"class":270},[141,1427,1428],{"class":158}," --version",[141,1430,1423],{"class":165},[141,1432,1256],{"class":270},[141,1434,698],{"class":236},[141,1436,372],{"class":165},[141,1438,1439,1441,1443,1446],{"class":143,"line":185},[141,1440,1376],{"class":270},[141,1442,1386],{"class":236},[141,1444,1445],{"class":236}," uninstall",[141,1447,1448],{"class":236}," mytool\n",[10,1450,1451,1452,1454],{},"To confirm the smoke test actually catches what it should, break the packaging on purpose once — exclude the templates directory in the build configuration, or misspell the entry point — rebuild, and watch ",[14,1453,41],{}," fail. A smoke test that has never been seen to fail has not been proven to work.",[10,1456,1457,1458,1461,1462,1465,1466,1469,1470,48],{},"For a pytest-based variant, mark end-to-end tests that call the installed command with ",[14,1459,1460],{},"@pytest.mark.installed",", skip them unless ",[14,1463,1464],{},"shutil.which(\"mytool\")"," resolves outside the repository, and run ",[14,1467,1468],{},"pytest -m installed"," in the smoke job after installing the wheel. The patterns for that are in ",[44,1471,1473],{"href":1472},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli\u002F","end-to-end testing an installed CLI",[50,1475,1477],{"id":1476},"conclusion","Conclusion",[10,1479,1480,1481,1483],{},"Unit tests prove your code works; a smoke test proves your package contains your code. Build once, install the artefact into a clean environment with only runtime dependencies, run it from outside the checkout on each operating system, and give it a ",[14,1482,41],{}," command that touches every resource, lazy module and plugin. It adds a minute to the pipeline and removes an entire category of \"works on my machine\" release bugs.",[50,1485,1487],{"id":1486},"frequently-asked-questions","Frequently asked questions",[1489,1490,1492,1493,1496],"h3",{"id":1491},"isnt-pip-install-in-the-test-job-enough","Isn't ",[14,1494,1495],{},"pip install ."," in the test job enough?",[10,1498,1499,1500,1502],{},"It is better than an editable install, but the test job usually still runs from the checkout (so ",[14,1501,102],{}," may find the source tree first) and has development dependencies installed. Installing the built wheel in a separate job, without the checkout, removes both effects.",[1489,1504,1506],{"id":1505},"should-the-smoke-test-run-on-every-push-or-only-for-releases","Should the smoke test run on every push or only for releases?",[10,1508,1509,1510,1513],{},"On every push to ",[14,1511,1512],{},"main"," and on pull requests that touch packaging files. It is cheap, and packaging bugs are easiest to fix in the change that introduced them.",[1489,1515,1517],{"id":1516},"how-do-i-smoke-test-a-standalone-binary","How do I smoke-test a standalone binary?",[10,1519,1520,1521,1524,1525,1527,1528,1530,1531,48],{},"The same way: download the PyInstaller or Nuitka artefact onto a clean runner with no Python installed, run ",[14,1522,1523],{},"--version"," and ",[14,1526,41],{},". Frozen binaries fail at runtime on missing hidden imports, which is exactly what ",[14,1529,41],{},"'s explicit imports catch. See ",[44,1532,1534],{"href":1533},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller\u002F","bundling a Python CLI with PyInstaller",[1489,1536,1538],{"id":1537},"does-the-smoke-test-need-network-access","Does the smoke test need network access?",[10,1540,1541,1542,1544,1545,1548,1549,1552,1553,1556,1557,1560],{},"Installing the wheel does, because ",[14,1543,1248],{}," fetches your runtime dependencies from PyPI — which is itself a useful check that every dependency and version constraint resolves for a user. If your CI runners are offline, point uv at an internal mirror with ",[14,1546,1547],{},"UV_INDEX_URL",", or pre-download dependency wheels in the build job with ",[14,1550,1551],{},"uv export"," plus ",[14,1554,1555],{},"uv pip download"," and install with ",[14,1558,1559],{},"--find-links dist --offline"," in the smoke job.",[1489,1562,1564],{"id":1563},"what-about-checking-the-wheels-contents-directly","What about checking the wheel's contents directly?",[10,1566,1567,1265,1570,1573,1574,1577],{},[14,1568,1569],{},"unzip -l dist\u002F*.whl",[14,1571,1572],{},"check-wheel-contents"," lists what was packaged and flags common mistakes such as duplicate files or a stray top-level ",[14,1575,1576],{},"tests"," package. It complements the smoke test; it does not replace running the command.",[50,1579,1581],{"id":1580},"related","Related",[55,1583,1584,1590,1596,1602,1607],{},[58,1585,1586,1587],{},"Up: ",[44,1588,1589],{"href":46},"CI\u002FCD pipelines for Python CLIs",[58,1591,1592],{},[44,1593,1595],{"href":1594},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-to-pypi-with-trusted-publishing\u002F","Publishing to PyPI with trusted publishing",[58,1597,1598],{},[44,1599,1601],{"href":1600},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags\u002F","Automating releases from git tags",[58,1603,1604],{},[44,1605,1606],{"href":940},"Bundling data files with importlib.resources",[58,1608,1609],{},[44,1610,1611],{"href":1472},"End-to-end testing an installed CLI",[1613,1614,1615],"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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":137,"searchDepth":151,"depth":151,"links":1617},[1618,1619,1620,1622,1623,1624,1625,1626,1634],{"id":52,"depth":151,"text":53},{"id":95,"depth":151,"text":96},{"id":117,"depth":151,"text":1621},"The recipe, part 1: a doctor command",{"id":954,"depth":151,"text":955},{"id":1294,"depth":151,"text":1295},{"id":1352,"depth":151,"text":1353},{"id":1476,"depth":151,"text":1477},{"id":1486,"depth":151,"text":1487,"children":1627},[1628,1630,1631,1632,1633],{"id":1491,"depth":169,"text":1629},"Isn't pip install . in the test job enough?",{"id":1505,"depth":169,"text":1506},{"id":1516,"depth":169,"text":1517},{"id":1537,"depth":169,"text":1538},{"id":1563,"depth":169,"text":1564},{"id":1580,"depth":151,"text":1581},"2026-09-18","Catch packaging bugs unit tests miss: install the built wheel of your Python CLI into a clean environment, run it from outside the repo, and add a doctor command.","intermediate",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci",{"title":5,"description":1636},"project-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci\u002Findex",[1645,1646,1647,1648],"packaging","ci","testing","wheels","-ZcLiowtMUZsSOet8GgX7LS-jayXQCE70-Nwfz9wHiA",[1651,1654,1657,1660,1663,1666,1669,1672,1675,1678,1681,1684,1687,1690,1693,1696,1699,1702,1705,1708,1711,1714,1717,1720,1723,1726,1729,1732,1735,1738,1741,1744,1747,1750,1753,1756,1759,1762,1765,1768,1771,1774,1777,1780,1783,1786,1789,1792,1795,1798,1801,1804,1807,1810,1813,1816,1819,1822,1825,1828,1831,1834,1837,1840,1843,1846,1849,1852,1855,1858,1861,1864,1867,1870,1873,1876,1879,1882,1885,1888,1891,1894,1897,1900,1903,1906,1909,1912,1915,1918,1921,1924,1926,1929,1932,1935,1938,1941,1944,1947,1950,1953,1956,1959,1962,1965,1968,1971,1974,1977,1980,1983,1986,1989,1992,1995,1998,2001,2004,2007,2010,2013,2016,2019,2022,2025,2028,2031,2034,2037,2040,2043,2046,2049,2052,2055,2058,2061,2064,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],{"path":1652,"title":1653},"\u002Fabout","About Python CLI Toolcraft",{"path":1655,"title":1656},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1658,"title":1659},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1661,"title":1662},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1664,"title":1665},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1667,"title":1668},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":1670,"title":1671},"\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":1673,"title":1674},"\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":1676,"title":1677},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":1679,"title":1680},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":1682,"title":1683},"\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":1685,"title":1686},"\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":1688,"title":1689},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1691,"title":1692},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1694,"title":1695},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1697,"title":1698},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":1700,"title":1701},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":1703,"title":1704},"\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":1706,"title":1707},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":1709,"title":1710},"\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":1712,"title":1713},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1715,"title":1716},"\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":1718,"title":1719},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1721,"title":1722},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1724,"title":1725},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1727,"title":1728},"\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":1730,"title":1731},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1733,"title":1734},"\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":1736,"title":1737},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1739,"title":1740},"\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":1742,"title":1743},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":1745,"title":1746},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":1748,"title":1749},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1751,"title":1752},"\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":1754,"title":1755},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":1757,"title":1758},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1760,"title":1761},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":1763,"title":1764},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1766,"title":1767},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":1769,"title":1770},"\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":1772,"title":1773},"\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":1775,"title":1776},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1778,"title":1779},"\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":1781,"title":1782},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":1784,"title":1785},"\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":1787,"title":1788},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1790,"title":1791},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1793,"title":1794},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1796,"title":1797},"\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":1799,"title":1800},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1802,"title":1803},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1805,"title":1806},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1808,"title":1809},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1811,"title":1812},"\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":1814,"title":1815},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1817,"title":1818},"\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":1820,"title":1821},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":1823,"title":1824},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":1826,"title":1827},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":1829,"title":1830},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":1832,"title":1833},"\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":1835,"title":1836},"\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":1838,"title":1839},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":1841,"title":1842},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":1844,"title":1845},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":1847,"title":1848},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":1850,"title":1851},"\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":1853,"title":1854},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":1856,"title":1857},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":1859,"title":1860},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":1862,"title":1863},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":1865,"title":1866},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":1868,"title":1869},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":1871,"title":1872},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":1874,"title":1875},"\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":1877,"title":1878},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":1880,"title":1881},"\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":1883,"title":1884},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":1886,"title":1887},"\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":1889,"title":1890},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":1892,"title":1893},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":1895,"title":1896},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":1898,"title":1899},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":1901,"title":1902},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":1904,"title":1905},"\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":1907,"title":1908},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":1910,"title":1911},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":1913,"title":1914},"\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":1916,"title":1917},"\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":1919,"title":1920},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":1922,"title":1923},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":301,"title":1925},"Python CLI Toolcraft",{"path":1927,"title":1928},"\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":1930,"title":1931},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1933,"title":1934},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1936,"title":1937},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1939,"title":1940},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1942,"title":1943},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1945,"title":1946},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1948,"title":1949},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1951,"title":1952},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1954,"title":1955},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":1957,"title":1958},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":1960,"title":1961},"\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":1963,"title":1964},"\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":1966,"title":1967},"\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":1969,"title":1970},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":1972,"title":1973},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":1975,"title":1976},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1978,"title":1979},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":1981,"title":1982},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":1984,"title":1985},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1987,"title":1988},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":1990,"title":1991},"\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":1993,"title":1994},"\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":1996,"title":1997},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1999,"title":2000},"\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":2002,"title":2003},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2005,"title":2006},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2008,"title":2009},"\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":2012},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli","End-to-End Testing an Installed Python CLI",{"path":2014,"title":2015},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2017,"title":2018},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2020,"title":2021},"\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":2023,"title":2024},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2026,"title":2027},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2029,"title":2030},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2032,"title":2033},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2035,"title":2036},"\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":2038,"title":2039},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2041,"title":2042},"\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":2044,"title":2045},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2047,"title":2048},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2050,"title":2051},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2053,"title":2054},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2056,"title":2057},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2059,"title":2060},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2062,"title":2063},"\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":1641,"title":5},{"path":2066,"title":2067},"\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":2069,"title":2070},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2072,"title":2073},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2075,"title":2076},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2078,"title":2079},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2081,"title":2082},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2084,"title":2085},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2087,"title":2088},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2090,"title":2091},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2093,"title":2094},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2096,"title":2097},"\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":2099,"title":2100},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2102,"title":2103},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2105,"title":2106},"\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":2108,"title":2109},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2111,"title":2112},"\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":2114,"title":2115},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2117,"title":2118},"\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":2120,"title":2121},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2123,"title":2124},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2126,"title":2127},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2129,"title":2130},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2132,"title":2133},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2135,"title":2136},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2138,"title":2139},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2141,"title":2142},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2144,"title":2145},"\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":2147,"title":2148},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2150,"title":2151},"\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":2153,"title":2154},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2156,"title":2157},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2159,"title":2160},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2162,"title":2163},"\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":2165,"title":2166},"\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":2168,"title":2169},"\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":2171,"title":2172},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2174,"title":2175},"\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":2177,"title":2178},"\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":2180,"title":2181},"\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":2183,"title":2184},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2186,"title":2187},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2189,"title":2190},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2192,"title":2193},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2195,"title":2196},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736907471]