[{"data":1,"prerenderedAt":2407},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python\u002F":3,"content-directory":1861},{"id":4,"title":5,"body":6,"date":1846,"description":1847,"difficulty":1848,"draft":1849,"extension":1850,"meta":1851,"navigation":266,"path":1852,"seo":1853,"stem":1854,"tags":1855,"updated":1846,"__hash__":1860},"content\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python\u002Findex.md","Writing Local pre-commit Hooks in Python",{"type":7,"value":8,"toc":1826},"minimark",[9,49,54,71,75,79,87,99,118,126,130,136,403,408,415,1017,1021,1032,1330,1338,1342,1345,1348,1387,1391,1440,1443,1447,1454,1667,1682,1686,1703,1707,1711,1722,1726,1745,1749,1768,1772,1781,1785,1788,1792,1822],[10,11,12,13,17,18,21,22,25,26,30,31,34,35,38,39,42,43,48],"p",{},"Some checks only make sense for your repository. The generated CLI reference in ",[14,15,16],"code",{},"docs\u002F"," must match the current ",[14,19,20],{},"--help"," output. Every command module must be registered in the command table. Nobody should commit a ",[14,23,24],{},"breakpoint()",". The version in the changelog heading must be valid. No public hook exists for these, and writing a whole published package for a twenty-line check is overkill. pre-commit's ",[27,28,29],"strong",{},"local hooks"," fill the gap: hooks defined directly in your ",[14,32,33],{},".pre-commit-config.yaml"," under ",[14,36,37],{},"repo: local",", running a regular expression, a script in the repository, or a small Python program. This guide shows the four ways to write a local hook, when to use each, the contract a hook must follow, and how to test hooks so they do not become the flaky step everyone skips with ",[14,40,41],{},"--no-verify",". It belongs to the ",[44,45,47],"a",{"href":46},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002F","pre-commit hooks for CLI projects topic",".",[50,51,53],"h2",{"id":52},"prerequisites","Prerequisites",[55,56,57,65],"ul",{},[58,59,60,61,48],"li",{},"A repository already using pre-commit, as set up in ",[44,62,64],{"href":63},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fsetting-up-pre-commit-for-python-cli-repos\u002F","setting up pre-commit for Python CLI repos",[58,66,67,68,48],{},"Python 3.10+ and uv; the examples run the project's own CLI through ",[14,69,70],{},"uv run",[50,72,74],{"id":73},"four-ways-to-write-a-local-hook","Four ways to write a local hook",[76,77],"inline-diagram",{"name":78},"lh-languages",[10,80,81,86],{},[27,82,83],{},[14,84,85],{},"pygrep"," runs a Python regular expression over the matching files and fails on any match. No code and no environment — ideal for forbidding a pattern.",[10,88,89,94,95,98],{},[27,90,91],{},[14,92,93],{},"python"," builds an isolated virtual environment and runs an entry point in it, with any ",[14,96,97],{},"additional_dependencies"," you list. Reproducible and independent of the developer's setup, but it cannot import your project unless you install it into that environment.",[10,100,101,106,107,110,111,114,115,117],{},[27,102,103],{},[14,104,105],{},"system"," runs whatever command is on the developer's ",[14,108,109],{},"PATH",". It is the only way to run something ",[27,112,113],{},"inside your project's environment"," — for example your own CLI through ",[14,116,70],{}," — at the cost of depending on the developer having that environment.",[10,119,120,125],{},[27,121,122],{},[14,123,124],{},"script"," runs a script file from the repository, with no environment management. Handy for small shell or Python glue with no dependencies.",[50,127,129],{"id":128},"the-recipe","The recipe",[10,131,132,133,135],{},"Here is a ",[14,134,33],{}," with one local hook of each useful kind, all solving real problems in a CLI repository:",[137,138,143],"pre",{"className":139,"code":140,"language":141,"meta":142,"style":142},"language-yaml shiki shiki-themes github-light github-dark","# .pre-commit-config.yaml\nrepos:\n  - repo: local\n    hooks:\n      - id: no-breakpoints\n        name: no breakpoint() or pdb in source\n        language: pygrep\n        entry: '\\bbreakpoint\\(\\)|\\bimport pdb\\b|\\bpdb\\.set_trace\\('\n        types: [python]\n        exclude: ^tests\u002Ffixtures\u002F\n\n      - id: changelog-heading\n        name: changelog headings are valid versions\n        language: python\n        entry: python scripts\u002Fhooks\u002Fcheck_changelog.py\n        files: ^CHANGELOG\\.md$\n        additional_dependencies: [\"packaging>=24\"]\n\n      - id: cli-reference-up-to-date\n        name: CLI reference is up to date\n        language: system\n        entry: uv run --frozen python scripts\u002Fhooks\u002Fcheck_cli_docs.py\n        files: ^(src\u002Fmytool\u002F.*\\.py|docs\u002Freference\\.md)$\n        pass_filenames: false\n","yaml","",[14,144,145,154,165,181,189,203,214,225,236,250,261,268,280,290,300,310,321,334,339,351,361,371,381,391],{"__ignoreMap":142},[146,147,150],"span",{"class":148,"line":149},"line",1,[146,151,153],{"class":152},"sJ8bj","# .pre-commit-config.yaml\n",[146,155,157,161],{"class":148,"line":156},2,[146,158,160],{"class":159},"s9eBZ","repos",[146,162,164],{"class":163},"sVt8B",":\n",[146,166,168,171,174,177],{"class":148,"line":167},3,[146,169,170],{"class":163},"  - ",[146,172,173],{"class":159},"repo",[146,175,176],{"class":163},": ",[146,178,180],{"class":179},"sZZnC","local\n",[146,182,184,187],{"class":148,"line":183},4,[146,185,186],{"class":159},"    hooks",[146,188,164],{"class":163},[146,190,192,195,198,200],{"class":148,"line":191},5,[146,193,194],{"class":163},"      - ",[146,196,197],{"class":159},"id",[146,199,176],{"class":163},[146,201,202],{"class":179},"no-breakpoints\n",[146,204,206,209,211],{"class":148,"line":205},6,[146,207,208],{"class":159},"        name",[146,210,176],{"class":163},[146,212,213],{"class":179},"no breakpoint() or pdb in source\n",[146,215,217,220,222],{"class":148,"line":216},7,[146,218,219],{"class":159},"        language",[146,221,176],{"class":163},[146,223,224],{"class":179},"pygrep\n",[146,226,228,231,233],{"class":148,"line":227},8,[146,229,230],{"class":159},"        entry",[146,232,176],{"class":163},[146,234,235],{"class":179},"'\\bbreakpoint\\(\\)|\\bimport pdb\\b|\\bpdb\\.set_trace\\('\n",[146,237,239,242,245,247],{"class":148,"line":238},9,[146,240,241],{"class":159},"        types",[146,243,244],{"class":163},": [",[146,246,93],{"class":179},[146,248,249],{"class":163},"]\n",[146,251,253,256,258],{"class":148,"line":252},10,[146,254,255],{"class":159},"        exclude",[146,257,176],{"class":163},[146,259,260],{"class":179},"^tests\u002Ffixtures\u002F\n",[146,262,264],{"class":148,"line":263},11,[146,265,267],{"emptyLinePlaceholder":266},true,"\n",[146,269,271,273,275,277],{"class":148,"line":270},12,[146,272,194],{"class":163},[146,274,197],{"class":159},[146,276,176],{"class":163},[146,278,279],{"class":179},"changelog-heading\n",[146,281,283,285,287],{"class":148,"line":282},13,[146,284,208],{"class":159},[146,286,176],{"class":163},[146,288,289],{"class":179},"changelog headings are valid versions\n",[146,291,293,295,297],{"class":148,"line":292},14,[146,294,219],{"class":159},[146,296,176],{"class":163},[146,298,299],{"class":179},"python\n",[146,301,303,305,307],{"class":148,"line":302},15,[146,304,230],{"class":159},[146,306,176],{"class":163},[146,308,309],{"class":179},"python scripts\u002Fhooks\u002Fcheck_changelog.py\n",[146,311,313,316,318],{"class":148,"line":312},16,[146,314,315],{"class":159},"        files",[146,317,176],{"class":163},[146,319,320],{"class":179},"^CHANGELOG\\.md$\n",[146,322,324,327,329,332],{"class":148,"line":323},17,[146,325,326],{"class":159},"        additional_dependencies",[146,328,244],{"class":163},[146,330,331],{"class":179},"\"packaging>=24\"",[146,333,249],{"class":163},[146,335,337],{"class":148,"line":336},18,[146,338,267],{"emptyLinePlaceholder":266},[146,340,342,344,346,348],{"class":148,"line":341},19,[146,343,194],{"class":163},[146,345,197],{"class":159},[146,347,176],{"class":163},[146,349,350],{"class":179},"cli-reference-up-to-date\n",[146,352,354,356,358],{"class":148,"line":353},20,[146,355,208],{"class":159},[146,357,176],{"class":163},[146,359,360],{"class":179},"CLI reference is up to date\n",[146,362,364,366,368],{"class":148,"line":363},21,[146,365,219],{"class":159},[146,367,176],{"class":163},[146,369,370],{"class":179},"system\n",[146,372,374,376,378],{"class":148,"line":373},22,[146,375,230],{"class":159},[146,377,176],{"class":163},[146,379,380],{"class":179},"uv run --frozen python scripts\u002Fhooks\u002Fcheck_cli_docs.py\n",[146,382,384,386,388],{"class":148,"line":383},23,[146,385,315],{"class":159},[146,387,176],{"class":163},[146,389,390],{"class":179},"^(src\u002Fmytool\u002F.*\\.py|docs\u002Freference\\.md)$\n",[146,392,394,397,399],{"class":148,"line":393},24,[146,395,396],{"class":159},"        pass_filenames",[146,398,176],{"class":163},[146,400,402],{"class":401},"sj4cs","false\n",[404,405,407],"h3",{"id":406},"a-hook-that-checks-files-it-is-given","A hook that checks files it is given",[10,409,410,411,414],{},"The changelog hook is a normal Python script that receives filenames, checks each, prints problems as ",[14,412,413],{},"file:line: message",", and returns non-zero if anything is wrong:",[137,416,419],{"className":417,"code":418,"language":93,"meta":142,"style":142},"language-python shiki shiki-themes github-light github-dark","# scripts\u002Fhooks\u002Fcheck_changelog.py\n\"\"\"Every '## [x.y.z]' heading in the changelog must be a valid PEP 440 version.\"\"\"\nfrom __future__ import annotations\n\nimport re\nimport sys\nfrom pathlib import Path\n\nfrom packaging.version import InvalidVersion, Version\n\nHEADING = re.compile(r\"^## \\[(?P\u003Cv>[^\\]]+)\\]\")\n\n\ndef check(path: Path) -> list[str]:\n    problems = []\n    seen: list[Version] = []\n    for lineno, line in enumerate(path.read_text(encoding=\"utf-8\").splitlines(), start=1):\n        m = HEADING.match(line)\n        if not m or m[\"v\"].lower() == \"unreleased\":\n            continue\n        try:\n            v = Version(m[\"v\"])\n        except InvalidVersion:\n            problems.append(f\"{path}:{lineno}: '{m['v']}' is not a valid version\")\n            continue\n        if seen and v >= seen[-1]:\n            problems.append(f\"{path}:{lineno}: {v} is not older than the entry above ({seen[-1]})\")\n        seen.append(v)\n    return problems\n\n\ndef main(argv: list[str]) -> int:\n    problems = [p for name in argv for p in check(Path(name))]\n    for problem in problems:\n        print(problem)\n    return 1 if problems else 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main(sys.argv[1:]))\n",[14,420,421,426,431,446,450,458,465,477,481,493,497,555,559,563,581,592,601,641,654,685,690,697,712,720,769,774,801,854,860,869,874,879,900,931,944,953,973,978,983,1000],{"__ignoreMap":142},[146,422,423],{"class":148,"line":149},[146,424,425],{"class":152},"# scripts\u002Fhooks\u002Fcheck_changelog.py\n",[146,427,428],{"class":148,"line":156},[146,429,430],{"class":179},"\"\"\"Every '## [x.y.z]' heading in the changelog must be a valid PEP 440 version.\"\"\"\n",[146,432,433,437,440,443],{"class":148,"line":167},[146,434,436],{"class":435},"szBVR","from",[146,438,439],{"class":401}," __future__",[146,441,442],{"class":435}," import",[146,444,445],{"class":163}," annotations\n",[146,447,448],{"class":148,"line":183},[146,449,267],{"emptyLinePlaceholder":266},[146,451,452,455],{"class":148,"line":191},[146,453,454],{"class":435},"import",[146,456,457],{"class":163}," re\n",[146,459,460,462],{"class":148,"line":205},[146,461,454],{"class":435},[146,463,464],{"class":163}," sys\n",[146,466,467,469,472,474],{"class":148,"line":216},[146,468,436],{"class":435},[146,470,471],{"class":163}," pathlib ",[146,473,454],{"class":435},[146,475,476],{"class":163}," Path\n",[146,478,479],{"class":148,"line":227},[146,480,267],{"emptyLinePlaceholder":266},[146,482,483,485,488,490],{"class":148,"line":238},[146,484,436],{"class":435},[146,486,487],{"class":163}," packaging.version ",[146,489,454],{"class":435},[146,491,492],{"class":163}," InvalidVersion, Version\n",[146,494,495],{"class":148,"line":252},[146,496,267],{"emptyLinePlaceholder":266},[146,498,499,502,505,508,511,514,517,521,525,528,531,534,536,539,542,545,548,550,552],{"class":148,"line":263},[146,500,501],{"class":401},"HEADING",[146,503,504],{"class":435}," =",[146,506,507],{"class":163}," re.compile(",[146,509,510],{"class":435},"r",[146,512,513],{"class":179},"\"",[146,515,516],{"class":401},"^",[146,518,520],{"class":519},"sA_wV","## ",[146,522,524],{"class":523},"snhLl","\\[",[146,526,527],{"class":401},"(",[146,529,530],{"class":159},"?P\u003Cv>",[146,532,533],{"class":401},"[",[146,535,516],{"class":435},[146,537,538],{"class":523},"\\]",[146,540,541],{"class":401},"]",[146,543,544],{"class":435},"+",[146,546,547],{"class":401},")",[146,549,538],{"class":523},[146,551,513],{"class":179},[146,553,554],{"class":163},")\n",[146,556,557],{"class":148,"line":270},[146,558,267],{"emptyLinePlaceholder":266},[146,560,561],{"class":148,"line":282},[146,562,267],{"emptyLinePlaceholder":266},[146,564,565,568,572,575,578],{"class":148,"line":292},[146,566,567],{"class":435},"def",[146,569,571],{"class":570},"sScJk"," check",[146,573,574],{"class":163},"(path: Path) -> list[",[146,576,577],{"class":401},"str",[146,579,580],{"class":163},"]:\n",[146,582,583,586,589],{"class":148,"line":302},[146,584,585],{"class":163},"    problems ",[146,587,588],{"class":435},"=",[146,590,591],{"class":163}," []\n",[146,593,594,597,599],{"class":148,"line":312},[146,595,596],{"class":163},"    seen: list[Version] ",[146,598,588],{"class":435},[146,600,591],{"class":163},[146,602,603,606,609,612,615,618,622,624,627,630,633,635,638],{"class":148,"line":323},[146,604,605],{"class":435},"    for",[146,607,608],{"class":163}," lineno, line ",[146,610,611],{"class":435},"in",[146,613,614],{"class":401}," enumerate",[146,616,617],{"class":163},"(path.read_text(",[146,619,621],{"class":620},"s4XuR","encoding",[146,623,588],{"class":435},[146,625,626],{"class":179},"\"utf-8\"",[146,628,629],{"class":163},").splitlines(), ",[146,631,632],{"class":620},"start",[146,634,588],{"class":435},[146,636,637],{"class":401},"1",[146,639,640],{"class":163},"):\n",[146,642,643,646,648,651],{"class":148,"line":336},[146,644,645],{"class":163},"        m ",[146,647,588],{"class":435},[146,649,650],{"class":401}," HEADING",[146,652,653],{"class":163},".match(line)\n",[146,655,656,659,662,665,668,671,674,677,680,683],{"class":148,"line":341},[146,657,658],{"class":435},"        if",[146,660,661],{"class":435}," not",[146,663,664],{"class":163}," m ",[146,666,667],{"class":435},"or",[146,669,670],{"class":163}," m[",[146,672,673],{"class":179},"\"v\"",[146,675,676],{"class":163},"].lower() ",[146,678,679],{"class":435},"==",[146,681,682],{"class":179}," \"unreleased\"",[146,684,164],{"class":163},[146,686,687],{"class":148,"line":353},[146,688,689],{"class":435},"            continue\n",[146,691,692,695],{"class":148,"line":363},[146,693,694],{"class":435},"        try",[146,696,164],{"class":163},[146,698,699,702,704,707,709],{"class":148,"line":373},[146,700,701],{"class":163},"            v ",[146,703,588],{"class":435},[146,705,706],{"class":163}," Version(m[",[146,708,673],{"class":179},[146,710,711],{"class":163},"])\n",[146,713,714,717],{"class":148,"line":383},[146,715,716],{"class":435},"        except",[146,718,719],{"class":163}," InvalidVersion:\n",[146,721,722,725,728,730,733,736,739,742,744,747,749,752,754,757,760,762,764,767],{"class":148,"line":393},[146,723,724],{"class":163},"            problems.append(",[146,726,727],{"class":435},"f",[146,729,513],{"class":179},[146,731,732],{"class":401},"{",[146,734,735],{"class":163},"path",[146,737,738],{"class":401},"}",[146,740,741],{"class":179},":",[146,743,732],{"class":401},[146,745,746],{"class":163},"lineno",[146,748,738],{"class":401},[146,750,751],{"class":179},": '",[146,753,732],{"class":401},[146,755,756],{"class":163},"m[",[146,758,759],{"class":179},"'v'",[146,761,541],{"class":163},[146,763,738],{"class":401},[146,765,766],{"class":179},"' is not a valid version\"",[146,768,554],{"class":163},[146,770,772],{"class":148,"line":771},25,[146,773,689],{"class":435},[146,775,777,779,782,785,788,791,794,797,799],{"class":148,"line":776},26,[146,778,658],{"class":435},[146,780,781],{"class":163}," seen ",[146,783,784],{"class":435},"and",[146,786,787],{"class":163}," v ",[146,789,790],{"class":435},">=",[146,792,793],{"class":163}," seen[",[146,795,796],{"class":435},"-",[146,798,637],{"class":401},[146,800,580],{"class":163},[146,802,804,806,808,810,812,814,816,818,820,822,824,826,828,831,833,836,838,841,843,845,847,849,852],{"class":148,"line":803},27,[146,805,724],{"class":163},[146,807,727],{"class":435},[146,809,513],{"class":179},[146,811,732],{"class":401},[146,813,735],{"class":163},[146,815,738],{"class":401},[146,817,741],{"class":179},[146,819,732],{"class":401},[146,821,746],{"class":163},[146,823,738],{"class":401},[146,825,176],{"class":179},[146,827,732],{"class":401},[146,829,830],{"class":163},"v",[146,832,738],{"class":401},[146,834,835],{"class":179}," is not older than the entry above (",[146,837,732],{"class":401},[146,839,840],{"class":163},"seen[",[146,842,796],{"class":435},[146,844,637],{"class":401},[146,846,541],{"class":163},[146,848,738],{"class":401},[146,850,851],{"class":179},")\"",[146,853,554],{"class":163},[146,855,857],{"class":148,"line":856},28,[146,858,859],{"class":163},"        seen.append(v)\n",[146,861,863,866],{"class":148,"line":862},29,[146,864,865],{"class":435},"    return",[146,867,868],{"class":163}," problems\n",[146,870,872],{"class":148,"line":871},30,[146,873,267],{"emptyLinePlaceholder":266},[146,875,877],{"class":148,"line":876},31,[146,878,267],{"emptyLinePlaceholder":266},[146,880,882,884,887,890,892,895,898],{"class":148,"line":881},32,[146,883,567],{"class":435},[146,885,886],{"class":570}," main",[146,888,889],{"class":163},"(argv: list[",[146,891,577],{"class":401},[146,893,894],{"class":163},"]) -> ",[146,896,897],{"class":401},"int",[146,899,164],{"class":163},[146,901,903,905,907,910,913,916,918,921,923,926,928],{"class":148,"line":902},33,[146,904,585],{"class":163},[146,906,588],{"class":435},[146,908,909],{"class":163}," [p ",[146,911,912],{"class":435},"for",[146,914,915],{"class":163}," name ",[146,917,611],{"class":435},[146,919,920],{"class":163}," argv ",[146,922,912],{"class":435},[146,924,925],{"class":163}," p ",[146,927,611],{"class":435},[146,929,930],{"class":163}," check(Path(name))]\n",[146,932,934,936,939,941],{"class":148,"line":933},34,[146,935,605],{"class":435},[146,937,938],{"class":163}," problem ",[146,940,611],{"class":435},[146,942,943],{"class":163}," problems:\n",[146,945,947,950],{"class":148,"line":946},35,[146,948,949],{"class":401},"        print",[146,951,952],{"class":163},"(problem)\n",[146,954,956,958,961,964,967,970],{"class":148,"line":955},36,[146,957,865],{"class":435},[146,959,960],{"class":401}," 1",[146,962,963],{"class":435}," if",[146,965,966],{"class":163}," problems ",[146,968,969],{"class":435},"else",[146,971,972],{"class":401}," 0\n",[146,974,976],{"class":148,"line":975},37,[146,977,267],{"emptyLinePlaceholder":266},[146,979,981],{"class":148,"line":980},38,[146,982,267],{"emptyLinePlaceholder":266},[146,984,986,989,992,995,998],{"class":148,"line":985},39,[146,987,988],{"class":435},"if",[146,990,991],{"class":401}," __name__",[146,993,994],{"class":435}," ==",[146,996,997],{"class":179}," \"__main__\"",[146,999,164],{"class":163},[146,1001,1003,1006,1009,1012,1014],{"class":148,"line":1002},40,[146,1004,1005],{"class":435},"    raise",[146,1007,1008],{"class":401}," SystemExit",[146,1010,1011],{"class":163},"(main(sys.argv[",[146,1013,637],{"class":401},[146,1015,1016],{"class":163},":]))\n",[404,1018,1020],{"id":1019},"a-hook-that-keeps-generated-docs-in-sync","A hook that keeps generated docs in sync",[10,1022,1023,1024,1027,1028,1031],{},"The documentation hook uses ",[14,1025,1026],{},"pass_filenames: false"," because it does not check individual files: it regenerates the CLI reference from the real command tree and compares it with the committed file. Running through ",[14,1029,1030],{},"uv run --frozen"," means it uses the project's own environment and locked dependencies:",[137,1033,1035],{"className":417,"code":1034,"language":93,"meta":142,"style":142},"# scripts\u002Fhooks\u002Fcheck_cli_docs.py\n\"\"\"Fail if docs\u002Freference.md does not match what the CLI would generate now.\"\"\"\nfrom __future__ import annotations\n\nimport difflib\nimport sys\nfrom pathlib import Path\n\nfrom mytool.docs import render_reference   # renders help for every command as Markdown\n\nDOC = Path(\"docs\u002Freference.md\")\n\n\ndef main() -> int:\n    expected = render_reference()\n    actual = DOC.read_text(encoding=\"utf-8\") if DOC.exists() else \"\"\n    if expected == actual:\n        return 0\n    diff = difflib.unified_diff(actual.splitlines(), expected.splitlines(),\n                                \"docs\u002Freference.md (committed)\", \"generated\", lineterm=\"\", n=1)\n    print(\"\\n\".join(list(diff)[:40]))\n    print('\\ndocs\u002Freference.md is stale: run \"uv run mytool docs > docs\u002Freference.md\"')\n    return 1\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n",[14,1036,1037,1042,1047,1057,1061,1068,1074,1084,1088,1103,1107,1122,1126,1130,1143,1153,1187,1200,1207,1217,1249,1278,1294,1301,1305,1309,1321],{"__ignoreMap":142},[146,1038,1039],{"class":148,"line":149},[146,1040,1041],{"class":152},"# scripts\u002Fhooks\u002Fcheck_cli_docs.py\n",[146,1043,1044],{"class":148,"line":156},[146,1045,1046],{"class":179},"\"\"\"Fail if docs\u002Freference.md does not match what the CLI would generate now.\"\"\"\n",[146,1048,1049,1051,1053,1055],{"class":148,"line":167},[146,1050,436],{"class":435},[146,1052,439],{"class":401},[146,1054,442],{"class":435},[146,1056,445],{"class":163},[146,1058,1059],{"class":148,"line":183},[146,1060,267],{"emptyLinePlaceholder":266},[146,1062,1063,1065],{"class":148,"line":191},[146,1064,454],{"class":435},[146,1066,1067],{"class":163}," difflib\n",[146,1069,1070,1072],{"class":148,"line":205},[146,1071,454],{"class":435},[146,1073,464],{"class":163},[146,1075,1076,1078,1080,1082],{"class":148,"line":216},[146,1077,436],{"class":435},[146,1079,471],{"class":163},[146,1081,454],{"class":435},[146,1083,476],{"class":163},[146,1085,1086],{"class":148,"line":227},[146,1087,267],{"emptyLinePlaceholder":266},[146,1089,1090,1092,1095,1097,1100],{"class":148,"line":238},[146,1091,436],{"class":435},[146,1093,1094],{"class":163}," mytool.docs ",[146,1096,454],{"class":435},[146,1098,1099],{"class":163}," render_reference   ",[146,1101,1102],{"class":152},"# renders help for every command as Markdown\n",[146,1104,1105],{"class":148,"line":252},[146,1106,267],{"emptyLinePlaceholder":266},[146,1108,1109,1112,1114,1117,1120],{"class":148,"line":263},[146,1110,1111],{"class":401},"DOC",[146,1113,504],{"class":435},[146,1115,1116],{"class":163}," Path(",[146,1118,1119],{"class":179},"\"docs\u002Freference.md\"",[146,1121,554],{"class":163},[146,1123,1124],{"class":148,"line":270},[146,1125,267],{"emptyLinePlaceholder":266},[146,1127,1128],{"class":148,"line":282},[146,1129,267],{"emptyLinePlaceholder":266},[146,1131,1132,1134,1136,1139,1141],{"class":148,"line":292},[146,1133,567],{"class":435},[146,1135,886],{"class":570},[146,1137,1138],{"class":163},"() -> ",[146,1140,897],{"class":401},[146,1142,164],{"class":163},[146,1144,1145,1148,1150],{"class":148,"line":302},[146,1146,1147],{"class":163},"    expected ",[146,1149,588],{"class":435},[146,1151,1152],{"class":163}," render_reference()\n",[146,1154,1155,1158,1160,1163,1166,1168,1170,1172,1175,1177,1179,1182,1184],{"class":148,"line":312},[146,1156,1157],{"class":163},"    actual ",[146,1159,588],{"class":435},[146,1161,1162],{"class":401}," DOC",[146,1164,1165],{"class":163},".read_text(",[146,1167,621],{"class":620},[146,1169,588],{"class":435},[146,1171,626],{"class":179},[146,1173,1174],{"class":163},") ",[146,1176,988],{"class":435},[146,1178,1162],{"class":401},[146,1180,1181],{"class":163},".exists() ",[146,1183,969],{"class":435},[146,1185,1186],{"class":179}," \"\"\n",[146,1188,1189,1192,1195,1197],{"class":148,"line":323},[146,1190,1191],{"class":435},"    if",[146,1193,1194],{"class":163}," expected ",[146,1196,679],{"class":435},[146,1198,1199],{"class":163}," actual:\n",[146,1201,1202,1205],{"class":148,"line":336},[146,1203,1204],{"class":435},"        return",[146,1206,972],{"class":401},[146,1208,1209,1212,1214],{"class":148,"line":341},[146,1210,1211],{"class":163},"    diff ",[146,1213,588],{"class":435},[146,1215,1216],{"class":163}," difflib.unified_diff(actual.splitlines(), expected.splitlines(),\n",[146,1218,1219,1222,1225,1228,1230,1233,1235,1238,1240,1243,1245,1247],{"class":148,"line":353},[146,1220,1221],{"class":179},"                                \"docs\u002Freference.md (committed)\"",[146,1223,1224],{"class":163},", ",[146,1226,1227],{"class":179},"\"generated\"",[146,1229,1224],{"class":163},[146,1231,1232],{"class":620},"lineterm",[146,1234,588],{"class":435},[146,1236,1237],{"class":179},"\"\"",[146,1239,1224],{"class":163},[146,1241,1242],{"class":620},"n",[146,1244,588],{"class":435},[146,1246,637],{"class":401},[146,1248,554],{"class":163},[146,1250,1251,1254,1256,1258,1261,1263,1266,1269,1272,1275],{"class":148,"line":363},[146,1252,1253],{"class":401},"    print",[146,1255,527],{"class":163},[146,1257,513],{"class":179},[146,1259,1260],{"class":401},"\\n",[146,1262,513],{"class":179},[146,1264,1265],{"class":163},".join(",[146,1267,1268],{"class":401},"list",[146,1270,1271],{"class":163},"(diff)[:",[146,1273,1274],{"class":401},"40",[146,1276,1277],{"class":163},"]))\n",[146,1279,1280,1282,1284,1287,1289,1292],{"class":148,"line":373},[146,1281,1253],{"class":401},[146,1283,527],{"class":163},[146,1285,1286],{"class":179},"'",[146,1288,1260],{"class":401},[146,1290,1291],{"class":179},"docs\u002Freference.md is stale: run \"uv run mytool docs > docs\u002Freference.md\"'",[146,1293,554],{"class":163},[146,1295,1296,1298],{"class":148,"line":383},[146,1297,865],{"class":435},[146,1299,1300],{"class":401}," 1\n",[146,1302,1303],{"class":148,"line":393},[146,1304,267],{"emptyLinePlaceholder":266},[146,1306,1307],{"class":148,"line":771},[146,1308,267],{"emptyLinePlaceholder":266},[146,1310,1311,1313,1315,1317,1319],{"class":148,"line":776},[146,1312,988],{"class":435},[146,1314,991],{"class":401},[146,1316,994],{"class":435},[146,1318,997],{"class":179},[146,1320,164],{"class":163},[146,1322,1323,1325,1327],{"class":148,"line":803},[146,1324,1005],{"class":435},[146,1326,1008],{"class":401},[146,1328,1329],{"class":163},"(main())\n",[10,1331,1332,1333,1337],{},"Generating reference documentation from the command tree is covered in ",[44,1334,1336],{"href":1335},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fgenerating-man-pages-and-docs-from-a-cli\u002F","generating man pages and docs from a CLI",". The hook is what keeps it honest: changing a help string without regenerating the docs now fails on commit, with the exact command to fix it.",[50,1339,1341],{"id":1340},"the-hook-contract","The hook contract",[76,1343],{"name":1344},"lh-contract",[10,1346,1347],{},"Every hook, whatever its language, follows the same contract with pre-commit:",[55,1349,1350,1359,1365,1371,1381],{},[58,1351,1352,1355,1356,1358],{},[27,1353,1354],{},"Input"," is the list of staged files matching the hook's filters, as command-line arguments — unless ",[14,1357,1026],{},". The list may be split across several invocations if it is long.",[58,1360,1361,1364],{},[27,1362,1363],{},"Exit code 0"," means pass; anything else means fail.",[58,1366,1367,1370],{},[27,1368,1369],{},"Modifying files also means fail",", even with exit code 0. pre-commit detects the change and stops the commit so the developer can review and stage the modification. Hooks that fix things should still exit non-zero to be explicit.",[58,1372,1373,1376,1377,1380],{},[27,1374,1375],{},"Output"," is shown only when the hook fails (or with ",[14,1378,1379],{},"verbose: true","). Keep it short and actionable.",[58,1382,1383,1386],{},[27,1384,1385],{},"The working directory"," is the repository root.",[50,1388,1390],{"id":1389},"ux-considerations","UX considerations",[55,1392,1393,1406,1412,1434],{},[58,1394,1395,1398,1399,1401,1402,1405],{},[27,1396,1397],{},"Fast or not at all."," Local hooks run on every commit. Anything over a second or two will be skipped with ",[14,1400,41],{}," within a week. Filter with ",[14,1403,1404],{},"files:"," so slow hooks only run when relevant files change, and move genuinely slow checks to CI.",[58,1407,1408,1411],{},[27,1409,1410],{},"Tell people the fix."," The last line of a failure should be the exact command that fixes it, as the docs hook does.",[58,1413,1414,1423,1424,1426,1427,1430,1431,1433],{},[27,1415,1416,1417,1419,1420,1422],{},"Prefer ",[14,1418,93],{}," over ",[14,1421,105],{}," when you can."," ",[14,1425,105],{}," hooks fail confusingly for a contributor who has not run ",[14,1428,1429],{},"uv sync",". When you need the project environment, ",[14,1432,1030],{}," makes the failure mode obvious and cheap to fix.",[58,1435,1436,1439],{},[27,1437,1438],{},"Name hooks for the problem."," \"CLI reference is up to date\" in pre-commit's output tells a contributor what failed without opening the config.",[76,1441],{"name":1442},"lh-terminal",[50,1444,1446],{"id":1445},"testing-the-behaviour","Testing the behaviour",[10,1448,1449,1450,1453],{},"Hook scripts are ordinary Python and deserve ordinary unit tests — call ",[14,1451,1452],{},"main()"," with filenames and assert on the return value and output:",[137,1455,1457],{"className":417,"code":1456,"language":93,"meta":142,"style":142},"# tests\u002Ftest_hooks.py\nfrom scripts.hooks.check_changelog import main\n\n\ndef test_valid_changelog(tmp_path, capsys):\n    f = tmp_path \u002F \"CHANGELOG.md\"\n    f.write_text(\"# Changelog\\n\\n## [Unreleased]\\n\\n## [2.4.0]\\n\\n## [2.3.1]\\n\")\n    assert main([str(f)]) == 0\n    assert capsys.readouterr().out == \"\"\n\n\ndef test_invalid_and_misordered_versions(tmp_path, capsys):\n    f = tmp_path \u002F \"CHANGELOG.md\"\n    f.write_text(\"## [2.3.1]\\n\\n## [2.4.0]\\n\\n## [two]\\n\")\n    assert main([str(f)]) == 1\n    out = capsys.readouterr().out\n    assert \":3: 2.4.0 is not older\" in out\n    assert \":5: 'two' is not a valid version\" in out\n",[14,1458,1459,1464,1476,1480,1484,1494,1510,1540,1557,1568,1572,1576,1585,1597,1619,1633,1643,1656],{"__ignoreMap":142},[146,1460,1461],{"class":148,"line":149},[146,1462,1463],{"class":152},"# tests\u002Ftest_hooks.py\n",[146,1465,1466,1468,1471,1473],{"class":148,"line":156},[146,1467,436],{"class":435},[146,1469,1470],{"class":163}," scripts.hooks.check_changelog ",[146,1472,454],{"class":435},[146,1474,1475],{"class":163}," main\n",[146,1477,1478],{"class":148,"line":167},[146,1479,267],{"emptyLinePlaceholder":266},[146,1481,1482],{"class":148,"line":183},[146,1483,267],{"emptyLinePlaceholder":266},[146,1485,1486,1488,1491],{"class":148,"line":191},[146,1487,567],{"class":435},[146,1489,1490],{"class":570}," test_valid_changelog",[146,1492,1493],{"class":163},"(tmp_path, capsys):\n",[146,1495,1496,1499,1501,1504,1507],{"class":148,"line":205},[146,1497,1498],{"class":163},"    f ",[146,1500,588],{"class":435},[146,1502,1503],{"class":163}," tmp_path ",[146,1505,1506],{"class":435},"\u002F",[146,1508,1509],{"class":179}," \"CHANGELOG.md\"\n",[146,1511,1512,1515,1518,1521,1524,1526,1529,1531,1534,1536,1538],{"class":148,"line":216},[146,1513,1514],{"class":163},"    f.write_text(",[146,1516,1517],{"class":179},"\"# Changelog",[146,1519,1520],{"class":401},"\\n\\n",[146,1522,1523],{"class":179},"## [Unreleased]",[146,1525,1520],{"class":401},[146,1527,1528],{"class":179},"## [2.4.0]",[146,1530,1520],{"class":401},[146,1532,1533],{"class":179},"## [2.3.1]",[146,1535,1260],{"class":401},[146,1537,513],{"class":179},[146,1539,554],{"class":163},[146,1541,1542,1545,1548,1550,1553,1555],{"class":148,"line":227},[146,1543,1544],{"class":435},"    assert",[146,1546,1547],{"class":163}," main([",[146,1549,577],{"class":401},[146,1551,1552],{"class":163},"(f)]) ",[146,1554,679],{"class":435},[146,1556,972],{"class":401},[146,1558,1559,1561,1564,1566],{"class":148,"line":238},[146,1560,1544],{"class":435},[146,1562,1563],{"class":163}," capsys.readouterr().out ",[146,1565,679],{"class":435},[146,1567,1186],{"class":179},[146,1569,1570],{"class":148,"line":252},[146,1571,267],{"emptyLinePlaceholder":266},[146,1573,1574],{"class":148,"line":263},[146,1575,267],{"emptyLinePlaceholder":266},[146,1577,1578,1580,1583],{"class":148,"line":270},[146,1579,567],{"class":435},[146,1581,1582],{"class":570}," test_invalid_and_misordered_versions",[146,1584,1493],{"class":163},[146,1586,1587,1589,1591,1593,1595],{"class":148,"line":282},[146,1588,1498],{"class":163},[146,1590,588],{"class":435},[146,1592,1503],{"class":163},[146,1594,1506],{"class":435},[146,1596,1509],{"class":179},[146,1598,1599,1601,1604,1606,1608,1610,1613,1615,1617],{"class":148,"line":292},[146,1600,1514],{"class":163},[146,1602,1603],{"class":179},"\"## [2.3.1]",[146,1605,1520],{"class":401},[146,1607,1528],{"class":179},[146,1609,1520],{"class":401},[146,1611,1612],{"class":179},"## [two]",[146,1614,1260],{"class":401},[146,1616,513],{"class":179},[146,1618,554],{"class":163},[146,1620,1621,1623,1625,1627,1629,1631],{"class":148,"line":302},[146,1622,1544],{"class":435},[146,1624,1547],{"class":163},[146,1626,577],{"class":401},[146,1628,1552],{"class":163},[146,1630,679],{"class":435},[146,1632,1300],{"class":401},[146,1634,1635,1638,1640],{"class":148,"line":312},[146,1636,1637],{"class":163},"    out ",[146,1639,588],{"class":435},[146,1641,1642],{"class":163}," capsys.readouterr().out\n",[146,1644,1645,1647,1650,1653],{"class":148,"line":323},[146,1646,1544],{"class":435},[146,1648,1649],{"class":179}," \":3: 2.4.0 is not older\"",[146,1651,1652],{"class":435}," in",[146,1654,1655],{"class":163}," out\n",[146,1657,1658,1660,1663,1665],{"class":148,"line":336},[146,1659,1544],{"class":435},[146,1661,1662],{"class":179}," \":5: 'two' is not a valid version\"",[146,1664,1652],{"class":435},[146,1666,1655],{"class":163},[10,1668,1669,1670,1673,1674,1677,1678,1681],{},"For the hook definitions themselves, ",[14,1671,1672],{},"pre-commit run \u003Chook-id> --all-files"," runs one hook against the whole repository — the quickest way to see its real behaviour — and ",[14,1675,1676],{},"pre-commit try-repo . \u003Chook-id>"," exercises hook definitions from the working tree before they are committed. Add ",[14,1679,1680],{},"pre-commit run --all-files"," to CI as well, so a hook that fails for everyone is caught even if a contributor skipped it locally.",[50,1683,1685],{"id":1684},"conclusion","Conclusion",[10,1687,1688,1689,1691,1692,1694,1695,1697,1698,1694,1700,1702],{},"Local hooks let a repository enforce its own rules at the cheapest possible moment — before the commit exists. Use ",[14,1690,85],{}," for forbidden patterns, ",[14,1693,93],{}," with ",[14,1696,97],{}," for self-contained checks, and ",[14,1699,105],{},[14,1701,1030],{}," when the check needs your project's own code. Follow the contract (filenames in, exit code out, modifications count as failure), keep hooks fast and their messages actionable, and unit-test the scripts behind them like any other code.",[50,1704,1706],{"id":1705},"frequently-asked-questions","Frequently asked questions",[404,1708,1710],{"id":1709},"where-should-hook-scripts-live","Where should hook scripts live?",[10,1712,1713,1714,1717,1718,1721],{},"In a ",[14,1715,1716],{},"scripts\u002Fhooks\u002F"," directory that is not part of the installed package, so they never ship to users. Give the directory an ",[14,1719,1720],{},"__init__.py"," if you want to import the scripts in tests, and exclude it from the wheel in your build configuration.",[404,1723,1725],{"id":1724},"can-a-local-hook-use-my-clis-own-commands","Can a local hook use my CLI's own commands?",[10,1727,1728,1729,1732,1733,1736,1737,1740,1741,1744],{},"Yes, through ",[14,1730,1731],{},"language: system"," and ",[14,1734,1735],{},"uv run --frozen mytool ...",". That is often the best design: add a ",[14,1738,1739],{},"mytool check-docs"," or ",[14,1742,1743],{},"mytool lint-config"," command, and have the hook call it, so developers can run the same check by hand.",[404,1746,1748],{"id":1747},"how-do-i-skip-a-slow-hook-locally-but-keep-it-in-ci","How do I skip a slow hook locally but keep it in CI?",[10,1750,1751,1752,1755,1756,1759,1760,1763,1764,1767],{},"Set the ",[14,1753,1754],{},"SKIP"," environment variable (",[14,1757,1758],{},"SKIP=cli-reference-up-to-date git commit ...",") for occasional skips, or give the hook ",[14,1761,1762],{},"stages: [manual]"," and run ",[14,1765,1766],{},"pre-commit run --hook-stage manual"," in CI. The second keeps commits fast by default while CI still enforces it.",[404,1769,1771],{"id":1770},"how-do-i-stop-a-local-hook-from-diverging-from-ci","How do I stop a local hook from diverging from CI?",[10,1773,1774,1775,1777,1778,1780],{},"Run the same thing in both places. The simplest arrangement is a CI step that runs ",[14,1776,1680],{},", so every hook in the configuration — local ones included — gates merges exactly as it gates commits. For hooks that need the project environment, make sure the CI job runs ",[14,1779,1429],{}," first, as a developer would.",[404,1782,1784],{"id":1783},"should-hooks-auto-fix-or-only-check","Should hooks auto-fix or only check?",[10,1786,1787],{},"Auto-fix when the fix is mechanical and unambiguous (regenerating docs, sorting a list); only check when a human must decide. Either way, the hook fails the commit so the change is reviewed.",[50,1789,1791],{"id":1790},"related","Related",[55,1793,1794,1800,1806,1811,1817],{},[58,1795,1796,1797],{},"Up: ",[44,1798,1799],{"href":46},"Pre-commit hooks for CLI projects",[58,1801,1802],{},[44,1803,1805],{"href":1804},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fshipping-your-cli-as-a-pre-commit-hook\u002F","Shipping your CLI as a pre-commit hook",[58,1807,1808],{},[44,1809,1810],{"href":63},"Setting up pre-commit for Python CLI repos",[58,1812,1813],{},[44,1814,1816],{"href":1815},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project\u002F","Configuring Ruff for a CLI project",[58,1818,1819],{},[44,1820,1821],{"href":1335},"Generating man pages and docs from a CLI",[1823,1824,1825],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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}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}",{"title":142,"searchDepth":156,"depth":156,"links":1827},[1828,1829,1830,1834,1835,1836,1837,1838,1845],{"id":52,"depth":156,"text":53},{"id":73,"depth":156,"text":74},{"id":128,"depth":156,"text":129,"children":1831},[1832,1833],{"id":406,"depth":167,"text":407},{"id":1019,"depth":167,"text":1020},{"id":1340,"depth":156,"text":1341},{"id":1389,"depth":156,"text":1390},{"id":1445,"depth":156,"text":1446},{"id":1684,"depth":156,"text":1685},{"id":1705,"depth":156,"text":1706,"children":1839},[1840,1841,1842,1843,1844],{"id":1709,"depth":167,"text":1710},{"id":1724,"depth":167,"text":1725},{"id":1747,"depth":167,"text":1748},{"id":1770,"depth":167,"text":1771},{"id":1783,"depth":167,"text":1784},{"id":1790,"depth":156,"text":1791},"2026-09-18","Write project-specific pre-commit hooks for a Python CLI repo: pygrep, python and system hooks, keeping generated docs in sync, the hook contract and testing.","intermediate",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python",{"title":5,"description":1847},"project-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python\u002Findex",[1856,1857,1858,1859],"pre-commit","git-hooks","automation","quality","-600-dl6vKpN1ug7JdnrJBoCbLwJkodXrfrYsS0YAUw",[1862,1865,1868,1871,1874,1877,1880,1883,1886,1889,1892,1895,1898,1901,1904,1907,1910,1913,1916,1919,1922,1925,1928,1931,1934,1937,1940,1943,1946,1949,1952,1955,1958,1961,1964,1967,1970,1973,1976,1979,1982,1985,1988,1991,1994,1997,2000,2003,2006,2009,2012,2015,2018,2021,2024,2027,2030,2033,2036,2039,2042,2045,2048,2051,2054,2057,2060,2063,2066,2069,2072,2075,2078,2081,2084,2087,2090,2093,2096,2099,2102,2105,2108,2111,2114,2117,2120,2123,2126,2129,2132,2135,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,2296,2299,2302,2305,2308,2311,2314,2317,2320,2323,2326,2329,2332,2335,2338,2341,2344,2347,2350,2353,2356,2359,2362,2365,2368,2371,2374,2376,2379,2380,2383,2386,2389,2392,2395,2398,2401,2404],{"path":1863,"title":1864},"\u002Fabout","About Python CLI Toolcraft",{"path":1866,"title":1867},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1869,"title":1870},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1872,"title":1873},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1875,"title":1876},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1878,"title":1879},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":1881,"title":1882},"\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":1884,"title":1885},"\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":1887,"title":1888},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":1890,"title":1891},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":1893,"title":1894},"\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":1896,"title":1897},"\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":1899,"title":1900},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1902,"title":1903},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1905,"title":1906},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1908,"title":1909},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":1911,"title":1912},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":1914,"title":1915},"\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":1917,"title":1918},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":1920,"title":1921},"\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":1923,"title":1924},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1926,"title":1927},"\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":1929,"title":1930},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1932,"title":1933},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1935,"title":1936},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1938,"title":1939},"\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":1941,"title":1942},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1944,"title":1945},"\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":1947,"title":1948},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1950,"title":1951},"\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":1953,"title":1954},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":1956,"title":1957},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":1959,"title":1960},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1962,"title":1963},"\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":1965,"title":1966},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":1968,"title":1969},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1971,"title":1972},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":1974,"title":1975},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1977,"title":1978},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":1980,"title":1981},"\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":1983,"title":1984},"\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":1986,"title":1987},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1989,"title":1990},"\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":1992,"title":1993},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":1995,"title":1996},"\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":1998,"title":1999},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2001,"title":2002},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2004,"title":2005},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2007,"title":2008},"\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":2010,"title":2011},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2013,"title":2014},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2016,"title":2017},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2019,"title":2020},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2022,"title":2023},"\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":2025,"title":2026},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2028,"title":2029},"\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":2031,"title":2032},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2034,"title":2035},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2037,"title":2038},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2040,"title":2041},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2043,"title":2044},"\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":2046,"title":2047},"\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":2049,"title":2050},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2052,"title":2053},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2055,"title":2056},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2058,"title":2059},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2061,"title":2062},"\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":2064,"title":2065},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2067,"title":2068},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2070,"title":2071},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2073,"title":2074},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2076,"title":2077},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2079,"title":2080},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2082,"title":2083},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2085,"title":2086},"\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":2088,"title":2089},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2091,"title":2092},"\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":2094,"title":2095},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2097,"title":2098},"\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":2100,"title":2101},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2103,"title":2104},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2106,"title":2107},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2109,"title":2110},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2112,"title":2113},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2115,"title":2116},"\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":2118,"title":2119},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2121,"title":2122},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2124,"title":2125},"\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":2127,"title":2128},"\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":2130,"title":2131},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2133,"title":2134},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":1506,"title":2136},"Python CLI Toolcraft",{"path":2138,"title":2139},"\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":2141,"title":2142},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2144,"title":2145},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2147,"title":2148},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2150,"title":2151},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2153,"title":2154},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2156,"title":2157},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2159,"title":2160},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2162,"title":2163},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2165,"title":2166},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2168,"title":2169},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2171,"title":2172},"\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":2174,"title":2175},"\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":2177,"title":2178},"\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":2180,"title":2181},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2183,"title":2184},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2186,"title":2187},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2189,"title":2190},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2192,"title":2193},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2195,"title":2196},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2198,"title":2199},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2201,"title":2202},"\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":2204,"title":2205},"\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":2207,"title":2208},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2210,"title":2211},"\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":2213,"title":2214},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2216,"title":2217},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2219,"title":2220},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2222,"title":2223},"\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":2225,"title":2226},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2228,"title":2229},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2231,"title":2232},"\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":2234,"title":2235},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2237,"title":2238},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2240,"title":2241},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2243,"title":2244},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2246,"title":2247},"\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":2249,"title":2250},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2252,"title":2253},"\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":2255,"title":2256},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2258,"title":2259},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2261,"title":2262},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2264,"title":2265},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2267,"title":2268},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2270,"title":2271},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2273,"title":2274},"\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":2276,"title":2277},"\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":2279,"title":2280},"\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":2282,"title":2283},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2285,"title":2286},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2288,"title":2289},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2291,"title":2292},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2294,"title":2295},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2297,"title":2298},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2300,"title":2301},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2303,"title":2304},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2306,"title":2307},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2309,"title":2310},"\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":2312,"title":2313},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2315,"title":2316},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2318,"title":2319},"\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":2321,"title":2322},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2324,"title":2325},"\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":2327,"title":2328},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2330,"title":2331},"\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":2333,"title":2334},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2336,"title":2337},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2339,"title":2340},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2342,"title":2343},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2345,"title":2346},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2348,"title":2349},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2351,"title":2352},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2354,"title":2355},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2357,"title":2358},"\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":2360,"title":2361},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2363,"title":2364},"\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":2366,"title":2367},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2369,"title":2370},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2372,"title":2373},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2375,"title":1810},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fsetting-up-pre-commit-for-python-cli-repos",{"path":2377,"title":2378},"\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":1852,"title":5},{"path":2381,"title":2382},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2384,"title":2385},"\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":2387,"title":2388},"\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":2390,"title":2391},"\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":2393,"title":2394},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2396,"title":2397},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2399,"title":2400},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2402,"title":2403},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2405,"title":2406},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736907971]