[{"data":1,"prerenderedAt":3289},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli\u002F":3,"content-directory":2742},{"id":4,"title":5,"body":6,"date":2729,"description":2730,"difficulty":2731,"draft":2732,"extension":2733,"meta":2734,"navigation":165,"path":2735,"seo":2736,"stem":2737,"tags":2738,"updated":2729,"__hash__":2741},"content\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli\u002Findex.md","Wrapping git and Other Tools from a Python CLI",{"type":7,"value":8,"toc":2709},"minimark",[9,35,40,64,68,81,85,113,117,1375,1380,1392,1404,1416,1436,1439,1459,1463,1877,1884,1888,1907,1910,1914,1964,1968,1979,2566,2584,2588,2597,2601,2608,2615,2619,2636,2640,2646,2650,2669,2673,2705],[10,11,12,13,17,18,21,22,21,25,28,29,34],"p",{},"Release helpers, changelog generators, monorepo \"what changed?\" scripts and pre-deploy checks all end up asking git questions: which branch am I on, is the tree clean, which files changed since the last tag. The first version scatters ",[14,15,16],"code",{},"subprocess.run([\"git\", ...])"," through the commands and parses whatever text comes back. Six months later the parsing breaks on a filename with a space, a user with a German locale gets different output, and every test needs a real repository. This guide shows the alternative — a small, typed wrapper module that is the only code in your CLI allowed to call git. The same pattern works for ",[14,19,20],{},"docker",", ",[14,23,24],{},"kubectl",[14,26,27],{},"terraform"," or any tool you call often. It builds on the ideas in the ",[30,31,33],"a",{"href":32},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002F","subprocess topic",".",[36,37,39],"h2",{"id":38},"prerequisites","Prerequisites",[41,42,43,50,61],"ul",{},[44,45,46,47,34],"li",{},"Python 3.10+ and git 2.20 or newer on your ",[14,48,49],{},"PATH",[44,51,52,53,56,57,34],{},"A Typer or Click CLI with a package layout (",[14,54,55],{},"src\u002Fmytool\u002F...","), as in ",[30,58,60],{"href":59},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fhow-to-structure-a-large-python-cli-project\u002F","how to structure a large Python CLI project",[44,62,63],{},"pytest, for the tests.",[36,65,67],{"id":66},"the-shape-typed-functions-over-one-runner","The shape: typed functions over one runner",[10,69,70,71,21,74,21,77,80],{},"The wrapper has two layers. At the bottom, a single private function runs git with every subprocess decision made once: working directory, environment, encoding, error handling. Above it, small public functions each answer one question and return Python types — ",[14,72,73],{},"str",[14,75,76],{},"bool",[14,78,79],{},"list[Path]"," — never raw output. Commands only ever see the public functions.",[82,83],"inline-diagram",{"name":84},"sp-wrapper-layers",[10,86,87,88,92,93,96,97,100,101,104,105,108,109,112],{},"This buys three things. ",[89,90,91],"strong",{},"Consistency",": every call gets the same environment and error handling, so a fix applies everywhere. ",[89,94,95],{},"Readability",": ",[14,98,99],{},"if not git.is_clean():"," reads like the requirement it implements. ",[89,102,103],{},"Testability",": tests replace one function, ",[14,106,107],{},"_git",", instead of patching ",[14,110,111],{},"subprocess"," in a dozen places.",[36,114,116],{"id":115},"the-recipe","The recipe",[118,119,124],"pre",{"className":120,"code":121,"language":122,"meta":123,"style":123},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fgit.py\n\"\"\"The only module in mytool that runs git.\"\"\"\nfrom __future__ import annotations\n\nimport os\nimport shutil\nimport subprocess\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\n\nclass GitError(Exception):\n    def __init__(self, args: tuple[str, ...], returncode: int, stderr: str) -> None:\n        self.git_args = args\n        self.returncode = returncode\n        self.stderr = stderr.strip()\n        super().__init__(f\"git {' '.join(args)} failed ({returncode}): {self.stderr}\")\n\n\n_ENV_OVERRIDES = {\n    \"LC_ALL\": \"C\",                 # stable, untranslated messages\n    \"GIT_TERMINAL_PROMPT\": \"0\",    # never block on a credential prompt\n    \"GIT_OPTIONAL_LOCKS\": \"0\",     # read-only commands take no index lock\n}\n\n\ndef _git(*args: str, cwd: Path | None = None, ok_codes: tuple[int, ...] = (0,)) -> str:\n    exe = shutil.which(\"git\")\n    if exe is None:\n        raise GitError(args, 127, \"git is not installed or not on PATH\")\n    proc = subprocess.run(\n        [exe, *args],\n        cwd=cwd,\n        env={**os.environ, **_ENV_OVERRIDES},\n        stdin=subprocess.DEVNULL,\n        capture_output=True,\n        text=True,\n        encoding=\"utf-8\",\n        errors=\"surrogateescape\",\n        timeout=60,\n    )\n    if proc.returncode not in ok_codes:\n        raise GitError(args, proc.returncode, proc.stderr)\n    return proc.stdout\n\n\ndef repo_root(cwd: Path | None = None) -> Path:\n    return Path(_git(\"rev-parse\", \"--show-toplevel\", cwd=cwd).strip())\n\n\ndef current_branch(cwd: Path | None = None) -> str | None:\n    \"\"\"Branch name, or None on a detached HEAD.\"\"\"\n    name = _git(\"rev-parse\", \"--abbrev-ref\", \"HEAD\", cwd=cwd).strip()\n    return None if name == \"HEAD\" else name\n\n\ndef is_clean(cwd: Path | None = None) -> bool:\n    return _git(\"status\", \"--porcelain=v1\", \"-z\", cwd=cwd) == \"\"\n\n\n@dataclass(frozen=True)\nclass Change:\n    status: str\n    path: Path\n\n\ndef changed_files(since: str, cwd: Path | None = None) -> list[Change]:\n    \"\"\"Files changed between `since` and HEAD, NUL-separated so any name is safe.\"\"\"\n    out = _git(\"diff\", \"--name-status\", \"-z\", \"--no-renames\", f\"{since}...HEAD\", \"--\", cwd=cwd)\n    fields = out.split(\"\\0\")\n    return [\n        Change(status=fields[i], path=Path(fields[i + 1]))\n        for i in range(0, len(fields) - 1, 2)\n    ]\n\n\ndef latest_tag(cwd: Path | None = None) -> str | None:\n    try:\n        return _git(\"describe\", \"--tags\", \"--abbrev=0\", cwd=cwd).strip()\n    except GitError as exc:\n        if \"No names found\" in exc.stderr or \"No tags can describe\" in exc.stderr:\n            return None\n        raise\n","python","",[14,125,126,135,142,160,167,176,184,192,205,218,223,228,247,286,301,314,327,386,391,396,408,425,442,458,464,469,474,533,549,565,584,595,606,618,641,658,671,683,696,709,722,728,745,753,762,767,772,794,820,825,830,859,865,897,922,927,932,956,990,995,1000,1017,1027,1036,1042,1047,1052,1078,1084,1141,1161,1169,1200,1239,1245,1250,1255,1283,1291,1320,1335,1360,1369],{"__ignoreMap":123},[127,128,131],"span",{"class":129,"line":130},"line",1,[127,132,134],{"class":133},"sJ8bj","# src\u002Fmytool\u002Fgit.py\n",[127,136,138],{"class":129,"line":137},2,[127,139,141],{"class":140},"sZZnC","\"\"\"The only module in mytool that runs git.\"\"\"\n",[127,143,145,149,153,156],{"class":129,"line":144},3,[127,146,148],{"class":147},"szBVR","from",[127,150,152],{"class":151},"sj4cs"," __future__",[127,154,155],{"class":147}," import",[127,157,159],{"class":158},"sVt8B"," annotations\n",[127,161,163],{"class":129,"line":162},4,[127,164,166],{"emptyLinePlaceholder":165},true,"\n",[127,168,170,173],{"class":129,"line":169},5,[127,171,172],{"class":147},"import",[127,174,175],{"class":158}," os\n",[127,177,179,181],{"class":129,"line":178},6,[127,180,172],{"class":147},[127,182,183],{"class":158}," shutil\n",[127,185,187,189],{"class":129,"line":186},7,[127,188,172],{"class":147},[127,190,191],{"class":158}," subprocess\n",[127,193,195,197,200,202],{"class":129,"line":194},8,[127,196,148],{"class":147},[127,198,199],{"class":158}," dataclasses ",[127,201,172],{"class":147},[127,203,204],{"class":158}," dataclass\n",[127,206,208,210,213,215],{"class":129,"line":207},9,[127,209,148],{"class":147},[127,211,212],{"class":158}," pathlib ",[127,214,172],{"class":147},[127,216,217],{"class":158}," Path\n",[127,219,221],{"class":129,"line":220},10,[127,222,166],{"emptyLinePlaceholder":165},[127,224,226],{"class":129,"line":225},11,[127,227,166],{"emptyLinePlaceholder":165},[127,229,231,234,238,241,244],{"class":129,"line":230},12,[127,232,233],{"class":147},"class",[127,235,237],{"class":236},"sScJk"," GitError",[127,239,240],{"class":158},"(",[127,242,243],{"class":151},"Exception",[127,245,246],{"class":158},"):\n",[127,248,250,253,256,259,261,263,266,269,272,275,277,280,283],{"class":129,"line":249},13,[127,251,252],{"class":147},"    def",[127,254,255],{"class":151}," __init__",[127,257,258],{"class":158},"(self, args: tuple[",[127,260,73],{"class":151},[127,262,21],{"class":158},[127,264,265],{"class":151},"...",[127,267,268],{"class":158},"], returncode: ",[127,270,271],{"class":151},"int",[127,273,274],{"class":158},", stderr: ",[127,276,73],{"class":151},[127,278,279],{"class":158},") -> ",[127,281,282],{"class":151},"None",[127,284,285],{"class":158},":\n",[127,287,289,292,295,298],{"class":129,"line":288},14,[127,290,291],{"class":151},"        self",[127,293,294],{"class":158},".git_args ",[127,296,297],{"class":147},"=",[127,299,300],{"class":158}," args\n",[127,302,304,306,309,311],{"class":129,"line":303},15,[127,305,291],{"class":151},[127,307,308],{"class":158},".returncode ",[127,310,297],{"class":147},[127,312,313],{"class":158}," returncode\n",[127,315,317,319,322,324],{"class":129,"line":316},16,[127,318,291],{"class":151},[127,320,321],{"class":158},".stderr ",[127,323,297],{"class":147},[127,325,326],{"class":158}," stderr.strip()\n",[127,328,330,333,336,339,341,344,347,350,353,356,359,362,364,367,369,372,375,378,380,383],{"class":129,"line":329},17,[127,331,332],{"class":151},"        super",[127,334,335],{"class":158},"().",[127,337,338],{"class":151},"__init__",[127,340,240],{"class":158},[127,342,343],{"class":147},"f",[127,345,346],{"class":140},"\"git ",[127,348,349],{"class":151},"{",[127,351,352],{"class":140},"' '",[127,354,355],{"class":158},".join(args)",[127,357,358],{"class":151},"}",[127,360,361],{"class":140}," failed (",[127,363,349],{"class":151},[127,365,366],{"class":158},"returncode",[127,368,358],{"class":151},[127,370,371],{"class":140},"): ",[127,373,374],{"class":151},"{self",[127,376,377],{"class":158},".stderr",[127,379,358],{"class":151},[127,381,382],{"class":140},"\"",[127,384,385],{"class":158},")\n",[127,387,389],{"class":129,"line":388},18,[127,390,166],{"emptyLinePlaceholder":165},[127,392,394],{"class":129,"line":393},19,[127,395,166],{"emptyLinePlaceholder":165},[127,397,399,402,405],{"class":129,"line":398},20,[127,400,401],{"class":151},"_ENV_OVERRIDES",[127,403,404],{"class":147}," =",[127,406,407],{"class":158}," {\n",[127,409,411,414,416,419,422],{"class":129,"line":410},21,[127,412,413],{"class":140},"    \"LC_ALL\"",[127,415,96],{"class":158},[127,417,418],{"class":140},"\"C\"",[127,420,421],{"class":158},",                 ",[127,423,424],{"class":133},"# stable, untranslated messages\n",[127,426,428,431,433,436,439],{"class":129,"line":427},22,[127,429,430],{"class":140},"    \"GIT_TERMINAL_PROMPT\"",[127,432,96],{"class":158},[127,434,435],{"class":140},"\"0\"",[127,437,438],{"class":158},",    ",[127,440,441],{"class":133},"# never block on a credential prompt\n",[127,443,445,448,450,452,455],{"class":129,"line":444},23,[127,446,447],{"class":140},"    \"GIT_OPTIONAL_LOCKS\"",[127,449,96],{"class":158},[127,451,435],{"class":140},[127,453,454],{"class":158},",     ",[127,456,457],{"class":133},"# read-only commands take no index lock\n",[127,459,461],{"class":129,"line":460},24,[127,462,463],{"class":158},"}\n",[127,465,467],{"class":129,"line":466},25,[127,468,166],{"emptyLinePlaceholder":165},[127,470,472],{"class":129,"line":471},26,[127,473,166],{"emptyLinePlaceholder":165},[127,475,477,480,483,485,488,491,493,496,499,502,504,506,509,511,513,515,518,520,523,526,529,531],{"class":129,"line":476},27,[127,478,479],{"class":147},"def",[127,481,482],{"class":236}," _git",[127,484,240],{"class":158},[127,486,487],{"class":147},"*",[127,489,490],{"class":158},"args: ",[127,492,73],{"class":151},[127,494,495],{"class":158},", cwd: Path ",[127,497,498],{"class":147},"|",[127,500,501],{"class":151}," None",[127,503,404],{"class":147},[127,505,501],{"class":151},[127,507,508],{"class":158},", ok_codes: tuple[",[127,510,271],{"class":151},[127,512,21],{"class":158},[127,514,265],{"class":151},[127,516,517],{"class":158},"] ",[127,519,297],{"class":147},[127,521,522],{"class":158}," (",[127,524,525],{"class":151},"0",[127,527,528],{"class":158},",)) -> ",[127,530,73],{"class":151},[127,532,285],{"class":158},[127,534,536,539,541,544,547],{"class":129,"line":535},28,[127,537,538],{"class":158},"    exe ",[127,540,297],{"class":147},[127,542,543],{"class":158}," shutil.which(",[127,545,546],{"class":140},"\"git\"",[127,548,385],{"class":158},[127,550,552,555,558,561,563],{"class":129,"line":551},29,[127,553,554],{"class":147},"    if",[127,556,557],{"class":158}," exe ",[127,559,560],{"class":147},"is",[127,562,501],{"class":151},[127,564,285],{"class":158},[127,566,568,571,574,577,579,582],{"class":129,"line":567},30,[127,569,570],{"class":147},"        raise",[127,572,573],{"class":158}," GitError(args, ",[127,575,576],{"class":151},"127",[127,578,21],{"class":158},[127,580,581],{"class":140},"\"git is not installed or not on PATH\"",[127,583,385],{"class":158},[127,585,587,590,592],{"class":129,"line":586},31,[127,588,589],{"class":158},"    proc ",[127,591,297],{"class":147},[127,593,594],{"class":158}," subprocess.run(\n",[127,596,598,601,603],{"class":129,"line":597},32,[127,599,600],{"class":158},"        [exe, ",[127,602,487],{"class":147},[127,604,605],{"class":158},"args],\n",[127,607,609,613,615],{"class":129,"line":608},33,[127,610,612],{"class":611},"s4XuR","        cwd",[127,614,297],{"class":147},[127,616,617],{"class":158},"cwd,\n",[127,619,621,624,626,628,631,634,636,638],{"class":129,"line":620},34,[127,622,623],{"class":611},"        env",[127,625,297],{"class":147},[127,627,349],{"class":158},[127,629,630],{"class":147},"**",[127,632,633],{"class":158},"os.environ, ",[127,635,630],{"class":147},[127,637,401],{"class":151},[127,639,640],{"class":158},"},\n",[127,642,644,647,649,652,655],{"class":129,"line":643},35,[127,645,646],{"class":611},"        stdin",[127,648,297],{"class":147},[127,650,651],{"class":158},"subprocess.",[127,653,654],{"class":151},"DEVNULL",[127,656,657],{"class":158},",\n",[127,659,661,664,666,669],{"class":129,"line":660},36,[127,662,663],{"class":611},"        capture_output",[127,665,297],{"class":147},[127,667,668],{"class":151},"True",[127,670,657],{"class":158},[127,672,674,677,679,681],{"class":129,"line":673},37,[127,675,676],{"class":611},"        text",[127,678,297],{"class":147},[127,680,668],{"class":151},[127,682,657],{"class":158},[127,684,686,689,691,694],{"class":129,"line":685},38,[127,687,688],{"class":611},"        encoding",[127,690,297],{"class":147},[127,692,693],{"class":140},"\"utf-8\"",[127,695,657],{"class":158},[127,697,699,702,704,707],{"class":129,"line":698},39,[127,700,701],{"class":611},"        errors",[127,703,297],{"class":147},[127,705,706],{"class":140},"\"surrogateescape\"",[127,708,657],{"class":158},[127,710,712,715,717,720],{"class":129,"line":711},40,[127,713,714],{"class":611},"        timeout",[127,716,297],{"class":147},[127,718,719],{"class":151},"60",[127,721,657],{"class":158},[127,723,725],{"class":129,"line":724},41,[127,726,727],{"class":158},"    )\n",[127,729,731,733,736,739,742],{"class":129,"line":730},42,[127,732,554],{"class":147},[127,734,735],{"class":158}," proc.returncode ",[127,737,738],{"class":147},"not",[127,740,741],{"class":147}," in",[127,743,744],{"class":158}," ok_codes:\n",[127,746,748,750],{"class":129,"line":747},43,[127,749,570],{"class":147},[127,751,752],{"class":158}," GitError(args, proc.returncode, proc.stderr)\n",[127,754,756,759],{"class":129,"line":755},44,[127,757,758],{"class":147},"    return",[127,760,761],{"class":158}," proc.stdout\n",[127,763,765],{"class":129,"line":764},45,[127,766,166],{"emptyLinePlaceholder":165},[127,768,770],{"class":129,"line":769},46,[127,771,166],{"emptyLinePlaceholder":165},[127,773,775,777,780,783,785,787,789,791],{"class":129,"line":774},47,[127,776,479],{"class":147},[127,778,779],{"class":236}," repo_root",[127,781,782],{"class":158},"(cwd: Path ",[127,784,498],{"class":147},[127,786,501],{"class":151},[127,788,404],{"class":147},[127,790,501],{"class":151},[127,792,793],{"class":158},") -> Path:\n",[127,795,797,799,802,805,807,810,812,815,817],{"class":129,"line":796},48,[127,798,758],{"class":147},[127,800,801],{"class":158}," Path(_git(",[127,803,804],{"class":140},"\"rev-parse\"",[127,806,21],{"class":158},[127,808,809],{"class":140},"\"--show-toplevel\"",[127,811,21],{"class":158},[127,813,814],{"class":611},"cwd",[127,816,297],{"class":147},[127,818,819],{"class":158},"cwd).strip())\n",[127,821,823],{"class":129,"line":822},49,[127,824,166],{"emptyLinePlaceholder":165},[127,826,828],{"class":129,"line":827},50,[127,829,166],{"emptyLinePlaceholder":165},[127,831,833,835,838,840,842,844,846,848,850,852,855,857],{"class":129,"line":832},51,[127,834,479],{"class":147},[127,836,837],{"class":236}," current_branch",[127,839,782],{"class":158},[127,841,498],{"class":147},[127,843,501],{"class":151},[127,845,404],{"class":147},[127,847,501],{"class":151},[127,849,279],{"class":158},[127,851,73],{"class":151},[127,853,854],{"class":147}," |",[127,856,501],{"class":151},[127,858,285],{"class":158},[127,860,862],{"class":129,"line":861},52,[127,863,864],{"class":140},"    \"\"\"Branch name, or None on a detached HEAD.\"\"\"\n",[127,866,868,871,873,876,878,880,883,885,888,890,892,894],{"class":129,"line":867},53,[127,869,870],{"class":158},"    name ",[127,872,297],{"class":147},[127,874,875],{"class":158}," _git(",[127,877,804],{"class":140},[127,879,21],{"class":158},[127,881,882],{"class":140},"\"--abbrev-ref\"",[127,884,21],{"class":158},[127,886,887],{"class":140},"\"HEAD\"",[127,889,21],{"class":158},[127,891,814],{"class":611},[127,893,297],{"class":147},[127,895,896],{"class":158},"cwd).strip()\n",[127,898,900,902,904,907,910,913,916,919],{"class":129,"line":899},54,[127,901,758],{"class":147},[127,903,501],{"class":151},[127,905,906],{"class":147}," if",[127,908,909],{"class":158}," name ",[127,911,912],{"class":147},"==",[127,914,915],{"class":140}," \"HEAD\"",[127,917,918],{"class":147}," else",[127,920,921],{"class":158}," name\n",[127,923,925],{"class":129,"line":924},55,[127,926,166],{"emptyLinePlaceholder":165},[127,928,930],{"class":129,"line":929},56,[127,931,166],{"emptyLinePlaceholder":165},[127,933,935,937,940,942,944,946,948,950,952,954],{"class":129,"line":934},57,[127,936,479],{"class":147},[127,938,939],{"class":236}," is_clean",[127,941,782],{"class":158},[127,943,498],{"class":147},[127,945,501],{"class":151},[127,947,404],{"class":147},[127,949,501],{"class":151},[127,951,279],{"class":158},[127,953,76],{"class":151},[127,955,285],{"class":158},[127,957,959,961,963,966,968,971,973,976,978,980,982,985,987],{"class":129,"line":958},58,[127,960,758],{"class":147},[127,962,875],{"class":158},[127,964,965],{"class":140},"\"status\"",[127,967,21],{"class":158},[127,969,970],{"class":140},"\"--porcelain=v1\"",[127,972,21],{"class":158},[127,974,975],{"class":140},"\"-z\"",[127,977,21],{"class":158},[127,979,814],{"class":611},[127,981,297],{"class":147},[127,983,984],{"class":158},"cwd) ",[127,986,912],{"class":147},[127,988,989],{"class":140}," \"\"\n",[127,991,993],{"class":129,"line":992},59,[127,994,166],{"emptyLinePlaceholder":165},[127,996,998],{"class":129,"line":997},60,[127,999,166],{"emptyLinePlaceholder":165},[127,1001,1003,1006,1008,1011,1013,1015],{"class":129,"line":1002},61,[127,1004,1005],{"class":236},"@dataclass",[127,1007,240],{"class":158},[127,1009,1010],{"class":611},"frozen",[127,1012,297],{"class":147},[127,1014,668],{"class":151},[127,1016,385],{"class":158},[127,1018,1020,1022,1025],{"class":129,"line":1019},62,[127,1021,233],{"class":147},[127,1023,1024],{"class":236}," Change",[127,1026,285],{"class":158},[127,1028,1030,1033],{"class":129,"line":1029},63,[127,1031,1032],{"class":158},"    status: ",[127,1034,1035],{"class":151},"str\n",[127,1037,1039],{"class":129,"line":1038},64,[127,1040,1041],{"class":158},"    path: Path\n",[127,1043,1045],{"class":129,"line":1044},65,[127,1046,166],{"emptyLinePlaceholder":165},[127,1048,1050],{"class":129,"line":1049},66,[127,1051,166],{"emptyLinePlaceholder":165},[127,1053,1055,1057,1060,1063,1065,1067,1069,1071,1073,1075],{"class":129,"line":1054},67,[127,1056,479],{"class":147},[127,1058,1059],{"class":236}," changed_files",[127,1061,1062],{"class":158},"(since: ",[127,1064,73],{"class":151},[127,1066,495],{"class":158},[127,1068,498],{"class":147},[127,1070,501],{"class":151},[127,1072,404],{"class":147},[127,1074,501],{"class":151},[127,1076,1077],{"class":158},") -> list[Change]:\n",[127,1079,1081],{"class":129,"line":1080},68,[127,1082,1083],{"class":140},"    \"\"\"Files changed between `since` and HEAD, NUL-separated so any name is safe.\"\"\"\n",[127,1085,1087,1090,1092,1094,1097,1099,1102,1104,1106,1108,1111,1113,1115,1117,1119,1122,1124,1127,1129,1132,1134,1136,1138],{"class":129,"line":1086},69,[127,1088,1089],{"class":158},"    out ",[127,1091,297],{"class":147},[127,1093,875],{"class":158},[127,1095,1096],{"class":140},"\"diff\"",[127,1098,21],{"class":158},[127,1100,1101],{"class":140},"\"--name-status\"",[127,1103,21],{"class":158},[127,1105,975],{"class":140},[127,1107,21],{"class":158},[127,1109,1110],{"class":140},"\"--no-renames\"",[127,1112,21],{"class":158},[127,1114,343],{"class":147},[127,1116,382],{"class":140},[127,1118,349],{"class":151},[127,1120,1121],{"class":158},"since",[127,1123,358],{"class":151},[127,1125,1126],{"class":140},"...HEAD\"",[127,1128,21],{"class":158},[127,1130,1131],{"class":140},"\"--\"",[127,1133,21],{"class":158},[127,1135,814],{"class":611},[127,1137,297],{"class":147},[127,1139,1140],{"class":158},"cwd)\n",[127,1142,1144,1147,1149,1152,1154,1157,1159],{"class":129,"line":1143},70,[127,1145,1146],{"class":158},"    fields ",[127,1148,297],{"class":147},[127,1150,1151],{"class":158}," out.split(",[127,1153,382],{"class":140},[127,1155,1156],{"class":151},"\\0",[127,1158,382],{"class":140},[127,1160,385],{"class":158},[127,1162,1164,1166],{"class":129,"line":1163},71,[127,1165,758],{"class":147},[127,1167,1168],{"class":158}," [\n",[127,1170,1172,1175,1178,1180,1183,1186,1188,1191,1194,1197],{"class":129,"line":1171},72,[127,1173,1174],{"class":158},"        Change(",[127,1176,1177],{"class":611},"status",[127,1179,297],{"class":147},[127,1181,1182],{"class":158},"fields[i], ",[127,1184,1185],{"class":611},"path",[127,1187,297],{"class":147},[127,1189,1190],{"class":158},"Path(fields[i ",[127,1192,1193],{"class":147},"+",[127,1195,1196],{"class":151}," 1",[127,1198,1199],{"class":158},"]))\n",[127,1201,1203,1206,1209,1212,1215,1217,1219,1221,1224,1227,1230,1232,1234,1237],{"class":129,"line":1202},73,[127,1204,1205],{"class":147},"        for",[127,1207,1208],{"class":158}," i ",[127,1210,1211],{"class":147},"in",[127,1213,1214],{"class":151}," range",[127,1216,240],{"class":158},[127,1218,525],{"class":151},[127,1220,21],{"class":158},[127,1222,1223],{"class":151},"len",[127,1225,1226],{"class":158},"(fields) ",[127,1228,1229],{"class":147},"-",[127,1231,1196],{"class":151},[127,1233,21],{"class":158},[127,1235,1236],{"class":151},"2",[127,1238,385],{"class":158},[127,1240,1242],{"class":129,"line":1241},74,[127,1243,1244],{"class":158},"    ]\n",[127,1246,1248],{"class":129,"line":1247},75,[127,1249,166],{"emptyLinePlaceholder":165},[127,1251,1253],{"class":129,"line":1252},76,[127,1254,166],{"emptyLinePlaceholder":165},[127,1256,1258,1260,1263,1265,1267,1269,1271,1273,1275,1277,1279,1281],{"class":129,"line":1257},77,[127,1259,479],{"class":147},[127,1261,1262],{"class":236}," latest_tag",[127,1264,782],{"class":158},[127,1266,498],{"class":147},[127,1268,501],{"class":151},[127,1270,404],{"class":147},[127,1272,501],{"class":151},[127,1274,279],{"class":158},[127,1276,73],{"class":151},[127,1278,854],{"class":147},[127,1280,501],{"class":151},[127,1282,285],{"class":158},[127,1284,1286,1289],{"class":129,"line":1285},78,[127,1287,1288],{"class":147},"    try",[127,1290,285],{"class":158},[127,1292,1294,1297,1299,1302,1304,1307,1309,1312,1314,1316,1318],{"class":129,"line":1293},79,[127,1295,1296],{"class":147},"        return",[127,1298,875],{"class":158},[127,1300,1301],{"class":140},"\"describe\"",[127,1303,21],{"class":158},[127,1305,1306],{"class":140},"\"--tags\"",[127,1308,21],{"class":158},[127,1310,1311],{"class":140},"\"--abbrev=0\"",[127,1313,21],{"class":158},[127,1315,814],{"class":611},[127,1317,297],{"class":147},[127,1319,896],{"class":158},[127,1321,1323,1326,1329,1332],{"class":129,"line":1322},80,[127,1324,1325],{"class":147},"    except",[127,1327,1328],{"class":158}," GitError ",[127,1330,1331],{"class":147},"as",[127,1333,1334],{"class":158}," exc:\n",[127,1336,1338,1341,1344,1346,1349,1352,1355,1357],{"class":129,"line":1337},81,[127,1339,1340],{"class":147},"        if",[127,1342,1343],{"class":140}," \"No names found\"",[127,1345,741],{"class":147},[127,1347,1348],{"class":158}," exc.stderr ",[127,1350,1351],{"class":147},"or",[127,1353,1354],{"class":140}," \"No tags can describe\"",[127,1356,741],{"class":147},[127,1358,1359],{"class":158}," exc.stderr:\n",[127,1361,1363,1366],{"class":129,"line":1362},82,[127,1364,1365],{"class":147},"            return",[127,1367,1368],{"class":151}," None\n",[127,1370,1372],{"class":129,"line":1371},83,[127,1373,1374],{"class":147},"        raise\n",[1376,1377,1379],"h3",{"id":1378},"why-these-details","Why these details",[10,1381,1382,1387,1388,1391],{},[89,1383,1384,34],{},[14,1385,1386],{},"LC_ALL=C"," Git translates many messages. If your code ever matches on stderr — as ",[14,1389,1390],{},"latest_tag"," does — it must see English. The C locale guarantees that, and also stabilises date and sort formats.",[10,1393,1394,1399,1400,1403],{},[89,1395,1396,34],{},[14,1397,1398],{},"GIT_TERMINAL_PROMPT=0"," A ",[14,1401,1402],{},"git fetch"," against a private remote with no cached credentials would otherwise prompt for a username and block forever. With this set it fails immediately with an error you can report.",[10,1405,1406,1411,1412,1415],{},[89,1407,1408,34],{},[14,1409,1410],{},"GIT_OPTIONAL_LOCKS=0"," ",[14,1413,1414],{},"git status"," normally refreshes the index and takes a lock to do so. If your tool runs while the user's editor or IDE is also running git, optional locks cause spurious \"index.lock exists\" failures. Read-only wrappers should not take them.",[10,1417,1418,1424,1425,1427,1428,1431,1432,1435],{},[89,1419,1420,1423],{},[14,1421,1422],{},"-z"," and porcelain formats."," Git's human output quotes unusual filenames with C-style escapes and changes between versions. The porcelain formats are documented as stable for scripts, and ",[14,1426,1422],{}," separates records with NUL bytes, which cannot appear in paths. Splitting on ",[14,1429,1430],{},"\"\\0\""," is then correct for every possible filename, including ones with spaces, newlines or non-UTF-8 bytes (which ",[14,1433,1434],{},"surrogateescape"," preserves rather than mangling).",[82,1437],{"name":1438},"sp-porcelain-terminal",[10,1440,1441,1446,1447,1450,1451,1455,1456,34],{},[89,1442,1443,34],{},[14,1444,1445],{},"ok_codes"," Some git commands use non-zero codes as answers. ",[14,1448,1449],{},"git diff --quiet"," exits 1 when there ",[1452,1453,1454],"em",{},"are"," differences. Rather than catching an exception for an expected outcome, allow that code explicitly: ",[14,1457,1458],{},"_git(\"diff\", \"--quiet\", ok_codes=(0, 1))",[36,1460,1462],{"id":1461},"using-it-from-commands","Using it from commands",[118,1464,1466],{"className":120,"code":1465,"language":122,"meta":123,"style":123},"# src\u002Fmytool\u002Fcli.py\nimport typer\n\nfrom mytool import git\n\napp = typer.Typer()\n\n\n@app.callback()\ndef main() -> None:\n    \"\"\"Release helpers.\"\"\"\n\n\n@app.command()\ndef preflight() -> None:\n    \"\"\"Check the repository is ready to release.\"\"\"\n    try:\n        branch = git.current_branch()\n        clean = git.is_clean()\n        tag = git.latest_tag()\n    except git.GitError as exc:\n        typer.secho(f\"error: {exc.stderr or exc}\", fg=\"red\", err=True)\n        raise typer.Exit(1)\n\n    problems = []\n    if branch != \"main\":\n        problems.append(f\"on branch {branch or '(detached HEAD)'}, expected main\")\n    if not clean:\n        problems.append(\"working tree has uncommitted changes\")\n    for p in problems:\n        typer.secho(f\"✗ {p}\", fg=\"red\", err=True)\n    if problems:\n        raise typer.Exit(1)\n    typer.echo(f\"✓ ready to release (previous tag: {tag or 'none'})\")\n\n\nif __name__ == \"__main__\":\n    app()\n",[14,1467,1468,1473,1480,1484,1496,1500,1510,1514,1518,1526,1540,1545,1549,1553,1560,1573,1578,1584,1594,1604,1614,1625,1670,1682,1686,1696,1711,1738,1748,1757,1770,1805,1811,1821,1848,1852,1856,1872],{"__ignoreMap":123},[127,1469,1470],{"class":129,"line":130},[127,1471,1472],{"class":133},"# src\u002Fmytool\u002Fcli.py\n",[127,1474,1475,1477],{"class":129,"line":137},[127,1476,172],{"class":147},[127,1478,1479],{"class":158}," typer\n",[127,1481,1482],{"class":129,"line":144},[127,1483,166],{"emptyLinePlaceholder":165},[127,1485,1486,1488,1491,1493],{"class":129,"line":162},[127,1487,148],{"class":147},[127,1489,1490],{"class":158}," mytool ",[127,1492,172],{"class":147},[127,1494,1495],{"class":158}," git\n",[127,1497,1498],{"class":129,"line":169},[127,1499,166],{"emptyLinePlaceholder":165},[127,1501,1502,1505,1507],{"class":129,"line":178},[127,1503,1504],{"class":158},"app ",[127,1506,297],{"class":147},[127,1508,1509],{"class":158}," typer.Typer()\n",[127,1511,1512],{"class":129,"line":186},[127,1513,166],{"emptyLinePlaceholder":165},[127,1515,1516],{"class":129,"line":194},[127,1517,166],{"emptyLinePlaceholder":165},[127,1519,1520,1523],{"class":129,"line":207},[127,1521,1522],{"class":236},"@app.callback",[127,1524,1525],{"class":158},"()\n",[127,1527,1528,1530,1533,1536,1538],{"class":129,"line":220},[127,1529,479],{"class":147},[127,1531,1532],{"class":236}," main",[127,1534,1535],{"class":158},"() -> ",[127,1537,282],{"class":151},[127,1539,285],{"class":158},[127,1541,1542],{"class":129,"line":225},[127,1543,1544],{"class":140},"    \"\"\"Release helpers.\"\"\"\n",[127,1546,1547],{"class":129,"line":230},[127,1548,166],{"emptyLinePlaceholder":165},[127,1550,1551],{"class":129,"line":249},[127,1552,166],{"emptyLinePlaceholder":165},[127,1554,1555,1558],{"class":129,"line":288},[127,1556,1557],{"class":236},"@app.command",[127,1559,1525],{"class":158},[127,1561,1562,1564,1567,1569,1571],{"class":129,"line":303},[127,1563,479],{"class":147},[127,1565,1566],{"class":236}," preflight",[127,1568,1535],{"class":158},[127,1570,282],{"class":151},[127,1572,285],{"class":158},[127,1574,1575],{"class":129,"line":316},[127,1576,1577],{"class":140},"    \"\"\"Check the repository is ready to release.\"\"\"\n",[127,1579,1580,1582],{"class":129,"line":329},[127,1581,1288],{"class":147},[127,1583,285],{"class":158},[127,1585,1586,1589,1591],{"class":129,"line":388},[127,1587,1588],{"class":158},"        branch ",[127,1590,297],{"class":147},[127,1592,1593],{"class":158}," git.current_branch()\n",[127,1595,1596,1599,1601],{"class":129,"line":393},[127,1597,1598],{"class":158},"        clean ",[127,1600,297],{"class":147},[127,1602,1603],{"class":158}," git.is_clean()\n",[127,1605,1606,1609,1611],{"class":129,"line":398},[127,1607,1608],{"class":158},"        tag ",[127,1610,297],{"class":147},[127,1612,1613],{"class":158}," git.latest_tag()\n",[127,1615,1616,1618,1621,1623],{"class":129,"line":410},[127,1617,1325],{"class":147},[127,1619,1620],{"class":158}," git.GitError ",[127,1622,1331],{"class":147},[127,1624,1334],{"class":158},[127,1626,1627,1630,1632,1635,1637,1640,1642,1645,1647,1649,1651,1654,1656,1659,1661,1664,1666,1668],{"class":129,"line":427},[127,1628,1629],{"class":158},"        typer.secho(",[127,1631,343],{"class":147},[127,1633,1634],{"class":140},"\"error: ",[127,1636,349],{"class":151},[127,1638,1639],{"class":158},"exc.stderr ",[127,1641,1351],{"class":147},[127,1643,1644],{"class":158}," exc",[127,1646,358],{"class":151},[127,1648,382],{"class":140},[127,1650,21],{"class":158},[127,1652,1653],{"class":611},"fg",[127,1655,297],{"class":147},[127,1657,1658],{"class":140},"\"red\"",[127,1660,21],{"class":158},[127,1662,1663],{"class":611},"err",[127,1665,297],{"class":147},[127,1667,668],{"class":151},[127,1669,385],{"class":158},[127,1671,1672,1674,1677,1680],{"class":129,"line":444},[127,1673,570],{"class":147},[127,1675,1676],{"class":158}," typer.Exit(",[127,1678,1679],{"class":151},"1",[127,1681,385],{"class":158},[127,1683,1684],{"class":129,"line":460},[127,1685,166],{"emptyLinePlaceholder":165},[127,1687,1688,1691,1693],{"class":129,"line":466},[127,1689,1690],{"class":158},"    problems ",[127,1692,297],{"class":147},[127,1694,1695],{"class":158}," []\n",[127,1697,1698,1700,1703,1706,1709],{"class":129,"line":471},[127,1699,554],{"class":147},[127,1701,1702],{"class":158}," branch ",[127,1704,1705],{"class":147},"!=",[127,1707,1708],{"class":140}," \"main\"",[127,1710,285],{"class":158},[127,1712,1713,1716,1718,1721,1723,1726,1728,1731,1733,1736],{"class":129,"line":476},[127,1714,1715],{"class":158},"        problems.append(",[127,1717,343],{"class":147},[127,1719,1720],{"class":140},"\"on branch ",[127,1722,349],{"class":151},[127,1724,1725],{"class":158},"branch ",[127,1727,1351],{"class":147},[127,1729,1730],{"class":140}," '(detached HEAD)'",[127,1732,358],{"class":151},[127,1734,1735],{"class":140},", expected main\"",[127,1737,385],{"class":158},[127,1739,1740,1742,1745],{"class":129,"line":535},[127,1741,554],{"class":147},[127,1743,1744],{"class":147}," not",[127,1746,1747],{"class":158}," clean:\n",[127,1749,1750,1752,1755],{"class":129,"line":551},[127,1751,1715],{"class":158},[127,1753,1754],{"class":140},"\"working tree has uncommitted changes\"",[127,1756,385],{"class":158},[127,1758,1759,1762,1765,1767],{"class":129,"line":567},[127,1760,1761],{"class":147},"    for",[127,1763,1764],{"class":158}," p ",[127,1766,1211],{"class":147},[127,1768,1769],{"class":158}," problems:\n",[127,1771,1772,1774,1776,1779,1781,1783,1785,1787,1789,1791,1793,1795,1797,1799,1801,1803],{"class":129,"line":586},[127,1773,1629],{"class":158},[127,1775,343],{"class":147},[127,1777,1778],{"class":140},"\"✗ ",[127,1780,349],{"class":151},[127,1782,10],{"class":158},[127,1784,358],{"class":151},[127,1786,382],{"class":140},[127,1788,21],{"class":158},[127,1790,1653],{"class":611},[127,1792,297],{"class":147},[127,1794,1658],{"class":140},[127,1796,21],{"class":158},[127,1798,1663],{"class":611},[127,1800,297],{"class":147},[127,1802,668],{"class":151},[127,1804,385],{"class":158},[127,1806,1807,1809],{"class":129,"line":597},[127,1808,554],{"class":147},[127,1810,1769],{"class":158},[127,1812,1813,1815,1817,1819],{"class":129,"line":608},[127,1814,570],{"class":147},[127,1816,1676],{"class":158},[127,1818,1679],{"class":151},[127,1820,385],{"class":158},[127,1822,1823,1826,1828,1831,1833,1836,1838,1841,1843,1846],{"class":129,"line":620},[127,1824,1825],{"class":158},"    typer.echo(",[127,1827,343],{"class":147},[127,1829,1830],{"class":140},"\"✓ ready to release (previous tag: ",[127,1832,349],{"class":151},[127,1834,1835],{"class":158},"tag ",[127,1837,1351],{"class":147},[127,1839,1840],{"class":140}," 'none'",[127,1842,358],{"class":151},[127,1844,1845],{"class":140},")\"",[127,1847,385],{"class":158},[127,1849,1850],{"class":129,"line":643},[127,1851,166],{"emptyLinePlaceholder":165},[127,1853,1854],{"class":129,"line":660},[127,1855,166],{"emptyLinePlaceholder":165},[127,1857,1858,1861,1864,1867,1870],{"class":129,"line":673},[127,1859,1860],{"class":147},"if",[127,1862,1863],{"class":151}," __name__",[127,1865,1866],{"class":147}," ==",[127,1868,1869],{"class":140}," \"__main__\"",[127,1871,285],{"class":158},[127,1873,1874],{"class":129,"line":685},[127,1875,1876],{"class":158},"    app()\n",[10,1878,1879,1880,1883],{},"The command reads like a checklist, and it reports ",[89,1881,1882],{},"all"," problems at once instead of stopping at the first — a small courtesy that saves a round trip for the user.",[36,1885,1887],{"id":1886},"shell-out-or-use-a-library","Shell out, or use a library?",[10,1889,1890,1891,1894,1895,1898,1899,1902,1903,1906],{},"Libraries such as ",[14,1892,1893],{},"dulwich"," (pure Python) and ",[14,1896,1897],{},"pygit2"," (libgit2 bindings) avoid the subprocess entirely. They are the right choice when git may not be installed — a slim container image, or end users on locked-down machines. For developer tooling, calling the real ",[14,1900,1901],{},"git"," is usually better: it respects the user's configuration, credential helpers, hooks, ",[14,1904,1905],{},"safe.directory"," settings and any new repository format features, and its behaviour matches what the user sees when they run git themselves.",[82,1908],{"name":1909},"sp-git-vs-library",[36,1911,1913],{"id":1912},"ux-considerations","UX considerations",[41,1915,1916,1922,1932,1942],{},[44,1917,1918,1921],{},[89,1919,1920],{},"Report git's own message."," When a call fails, git's stderr is usually precise (\"fatal: not a git repository\"). Show it rather than a generic \"git failed\".",[44,1923,1924,1927,1928,1931],{},[89,1925,1926],{},"Check you are in a repository first."," Calling ",[14,1929,1930],{},"repo_root()"," at the start of commands that need one produces one clear error instead of a confusing failure halfway through.",[44,1933,1934,1937,1938,1941],{},[89,1935,1936],{},"Operate relative to the repository root."," Users run tools from subdirectories. Pass ",[14,1939,1940],{},"cwd=repo_root()"," to calls whose paths should be root-relative, and print paths relative to where the user is.",[44,1943,1944,1947,1948,1951,1952,1955,1956,1959,1960,34],{},[89,1945,1946],{},"Never modify state silently."," A wrapper that runs ",[14,1949,1950],{},"git stash"," or ",[14,1953,1954],{},"git checkout"," should say so, and should support a ",[14,1957,1958],{},"--dry-run"," as described in ",[30,1961,1963],{"href":1962},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fadding-dry-run-and-confirmation-to-destructive-commands\u002F","adding dry-run and confirmation to destructive commands",[36,1965,1967],{"id":1966},"testing-the-behaviour","Testing the behaviour",[10,1969,1970,1971,1974,1975,1978],{},"Test the parsing against a ",[89,1972,1973],{},"real"," throwaway repository — it is quick to create one in ",[14,1976,1977],{},"tmp_path"," — and test commands by replacing the wrapper functions.",[118,1980,1982],{"className":120,"code":1981,"language":122,"meta":123,"style":123},"# tests\u002Ftest_git.py\nimport subprocess\nfrom pathlib import Path\n\nimport pytest\n\nfrom mytool import git\n\n\n@pytest.fixture\ndef repo(tmp_path: Path) -> Path:\n    def run(*args: str) -> None:\n        subprocess.run([\"git\", *args], cwd=tmp_path, check=True, capture_output=True)\n\n    run(\"init\", \"-q\", \"-b\", \"main\")\n    run(\"config\", \"user.email\", \"t@example.com\")\n    run(\"config\", \"user.name\", \"Test\")\n    (tmp_path \u002F \"a.txt\").write_text(\"a\")\n    run(\"add\", \".\")\n    run(\"commit\", \"-q\", \"-m\", \"first\")\n    run(\"tag\", \"v1.0.0\")\n    return tmp_path\n\n\ndef test_branch_and_clean(repo):\n    assert git.current_branch(repo) == \"main\"\n    assert git.is_clean(repo)\n    (repo \u002F \"b.txt\").write_text(\"b\")\n    assert not git.is_clean(repo)\n\n\ndef test_changed_files_handles_awkward_names(repo):\n    weird = repo \u002F \"odd name\\twith tab.txt\"\n    weird.write_text(\"x\")\n    subprocess.run([\"git\", \"add\", \".\"], cwd=repo, check=True)\n    subprocess.run([\"git\", \"commit\", \"-q\", \"-m\", \"second\"], cwd=repo, check=True)\n    changes = git.changed_files(\"v1.0.0\", cwd=repo)\n    assert changes == [git.Change(\"A\", Path(\"odd name\\twith tab.txt\"))]\n\n\ndef test_latest_tag(repo):\n    assert git.latest_tag(repo) == \"v1.0.0\"\n\n\ndef test_not_a_repo(tmp_path):\n    with pytest.raises(git.GitError) as info:\n        git.repo_root(tmp_path)\n    assert \"not a git repository\" in info.value.stderr\n",[14,1983,1984,1989,1995,2005,2009,2016,2020,2030,2034,2038,2043,2053,2074,2113,2117,2142,2161,2179,2198,2212,2235,2249,2256,2260,2264,2274,2287,2294,2311,2319,2323,2327,2336,2357,2367,2400,2439,2460,2489,2493,2497,2506,2518,2522,2526,2536,2549,2554],{"__ignoreMap":123},[127,1985,1986],{"class":129,"line":130},[127,1987,1988],{"class":133},"# tests\u002Ftest_git.py\n",[127,1990,1991,1993],{"class":129,"line":137},[127,1992,172],{"class":147},[127,1994,191],{"class":158},[127,1996,1997,1999,2001,2003],{"class":129,"line":144},[127,1998,148],{"class":147},[127,2000,212],{"class":158},[127,2002,172],{"class":147},[127,2004,217],{"class":158},[127,2006,2007],{"class":129,"line":162},[127,2008,166],{"emptyLinePlaceholder":165},[127,2010,2011,2013],{"class":129,"line":169},[127,2012,172],{"class":147},[127,2014,2015],{"class":158}," pytest\n",[127,2017,2018],{"class":129,"line":178},[127,2019,166],{"emptyLinePlaceholder":165},[127,2021,2022,2024,2026,2028],{"class":129,"line":186},[127,2023,148],{"class":147},[127,2025,1490],{"class":158},[127,2027,172],{"class":147},[127,2029,1495],{"class":158},[127,2031,2032],{"class":129,"line":194},[127,2033,166],{"emptyLinePlaceholder":165},[127,2035,2036],{"class":129,"line":207},[127,2037,166],{"emptyLinePlaceholder":165},[127,2039,2040],{"class":129,"line":220},[127,2041,2042],{"class":236},"@pytest.fixture\n",[127,2044,2045,2047,2050],{"class":129,"line":225},[127,2046,479],{"class":147},[127,2048,2049],{"class":236}," repo",[127,2051,2052],{"class":158},"(tmp_path: Path) -> Path:\n",[127,2054,2055,2057,2060,2062,2064,2066,2068,2070,2072],{"class":129,"line":230},[127,2056,252],{"class":147},[127,2058,2059],{"class":236}," run",[127,2061,240],{"class":158},[127,2063,487],{"class":147},[127,2065,490],{"class":158},[127,2067,73],{"class":151},[127,2069,279],{"class":158},[127,2071,282],{"class":151},[127,2073,285],{"class":158},[127,2075,2076,2079,2081,2083,2085,2088,2090,2092,2095,2098,2100,2102,2104,2107,2109,2111],{"class":129,"line":249},[127,2077,2078],{"class":158},"        subprocess.run([",[127,2080,546],{"class":140},[127,2082,21],{"class":158},[127,2084,487],{"class":147},[127,2086,2087],{"class":158},"args], ",[127,2089,814],{"class":611},[127,2091,297],{"class":147},[127,2093,2094],{"class":158},"tmp_path, ",[127,2096,2097],{"class":611},"check",[127,2099,297],{"class":147},[127,2101,668],{"class":151},[127,2103,21],{"class":158},[127,2105,2106],{"class":611},"capture_output",[127,2108,297],{"class":147},[127,2110,668],{"class":151},[127,2112,385],{"class":158},[127,2114,2115],{"class":129,"line":288},[127,2116,166],{"emptyLinePlaceholder":165},[127,2118,2119,2122,2125,2127,2130,2132,2135,2137,2140],{"class":129,"line":303},[127,2120,2121],{"class":158},"    run(",[127,2123,2124],{"class":140},"\"init\"",[127,2126,21],{"class":158},[127,2128,2129],{"class":140},"\"-q\"",[127,2131,21],{"class":158},[127,2133,2134],{"class":140},"\"-b\"",[127,2136,21],{"class":158},[127,2138,2139],{"class":140},"\"main\"",[127,2141,385],{"class":158},[127,2143,2144,2146,2149,2151,2154,2156,2159],{"class":129,"line":316},[127,2145,2121],{"class":158},[127,2147,2148],{"class":140},"\"config\"",[127,2150,21],{"class":158},[127,2152,2153],{"class":140},"\"user.email\"",[127,2155,21],{"class":158},[127,2157,2158],{"class":140},"\"t@example.com\"",[127,2160,385],{"class":158},[127,2162,2163,2165,2167,2169,2172,2174,2177],{"class":129,"line":329},[127,2164,2121],{"class":158},[127,2166,2148],{"class":140},[127,2168,21],{"class":158},[127,2170,2171],{"class":140},"\"user.name\"",[127,2173,21],{"class":158},[127,2175,2176],{"class":140},"\"Test\"",[127,2178,385],{"class":158},[127,2180,2181,2184,2187,2190,2193,2196],{"class":129,"line":388},[127,2182,2183],{"class":158},"    (tmp_path ",[127,2185,2186],{"class":147},"\u002F",[127,2188,2189],{"class":140}," \"a.txt\"",[127,2191,2192],{"class":158},").write_text(",[127,2194,2195],{"class":140},"\"a\"",[127,2197,385],{"class":158},[127,2199,2200,2202,2205,2207,2210],{"class":129,"line":393},[127,2201,2121],{"class":158},[127,2203,2204],{"class":140},"\"add\"",[127,2206,21],{"class":158},[127,2208,2209],{"class":140},"\".\"",[127,2211,385],{"class":158},[127,2213,2214,2216,2219,2221,2223,2225,2228,2230,2233],{"class":129,"line":398},[127,2215,2121],{"class":158},[127,2217,2218],{"class":140},"\"commit\"",[127,2220,21],{"class":158},[127,2222,2129],{"class":140},[127,2224,21],{"class":158},[127,2226,2227],{"class":140},"\"-m\"",[127,2229,21],{"class":158},[127,2231,2232],{"class":140},"\"first\"",[127,2234,385],{"class":158},[127,2236,2237,2239,2242,2244,2247],{"class":129,"line":410},[127,2238,2121],{"class":158},[127,2240,2241],{"class":140},"\"tag\"",[127,2243,21],{"class":158},[127,2245,2246],{"class":140},"\"v1.0.0\"",[127,2248,385],{"class":158},[127,2250,2251,2253],{"class":129,"line":427},[127,2252,758],{"class":147},[127,2254,2255],{"class":158}," tmp_path\n",[127,2257,2258],{"class":129,"line":444},[127,2259,166],{"emptyLinePlaceholder":165},[127,2261,2262],{"class":129,"line":460},[127,2263,166],{"emptyLinePlaceholder":165},[127,2265,2266,2268,2271],{"class":129,"line":466},[127,2267,479],{"class":147},[127,2269,2270],{"class":236}," test_branch_and_clean",[127,2272,2273],{"class":158},"(repo):\n",[127,2275,2276,2279,2282,2284],{"class":129,"line":471},[127,2277,2278],{"class":147},"    assert",[127,2280,2281],{"class":158}," git.current_branch(repo) ",[127,2283,912],{"class":147},[127,2285,2286],{"class":140}," \"main\"\n",[127,2288,2289,2291],{"class":129,"line":476},[127,2290,2278],{"class":147},[127,2292,2293],{"class":158}," git.is_clean(repo)\n",[127,2295,2296,2299,2301,2304,2306,2309],{"class":129,"line":535},[127,2297,2298],{"class":158},"    (repo ",[127,2300,2186],{"class":147},[127,2302,2303],{"class":140}," \"b.txt\"",[127,2305,2192],{"class":158},[127,2307,2308],{"class":140},"\"b\"",[127,2310,385],{"class":158},[127,2312,2313,2315,2317],{"class":129,"line":551},[127,2314,2278],{"class":147},[127,2316,1744],{"class":147},[127,2318,2293],{"class":158},[127,2320,2321],{"class":129,"line":567},[127,2322,166],{"emptyLinePlaceholder":165},[127,2324,2325],{"class":129,"line":586},[127,2326,166],{"emptyLinePlaceholder":165},[127,2328,2329,2331,2334],{"class":129,"line":597},[127,2330,479],{"class":147},[127,2332,2333],{"class":236}," test_changed_files_handles_awkward_names",[127,2335,2273],{"class":158},[127,2337,2338,2341,2343,2346,2348,2351,2354],{"class":129,"line":608},[127,2339,2340],{"class":158},"    weird ",[127,2342,297],{"class":147},[127,2344,2345],{"class":158}," repo ",[127,2347,2186],{"class":147},[127,2349,2350],{"class":140}," \"odd name",[127,2352,2353],{"class":151},"\\t",[127,2355,2356],{"class":140},"with tab.txt\"\n",[127,2358,2359,2362,2365],{"class":129,"line":620},[127,2360,2361],{"class":158},"    weird.write_text(",[127,2363,2364],{"class":140},"\"x\"",[127,2366,385],{"class":158},[127,2368,2369,2372,2374,2376,2378,2380,2382,2385,2387,2389,2392,2394,2396,2398],{"class":129,"line":643},[127,2370,2371],{"class":158},"    subprocess.run([",[127,2373,546],{"class":140},[127,2375,21],{"class":158},[127,2377,2204],{"class":140},[127,2379,21],{"class":158},[127,2381,2209],{"class":140},[127,2383,2384],{"class":158},"], ",[127,2386,814],{"class":611},[127,2388,297],{"class":147},[127,2390,2391],{"class":158},"repo, ",[127,2393,2097],{"class":611},[127,2395,297],{"class":147},[127,2397,668],{"class":151},[127,2399,385],{"class":158},[127,2401,2402,2404,2406,2408,2410,2412,2414,2416,2418,2420,2423,2425,2427,2429,2431,2433,2435,2437],{"class":129,"line":660},[127,2403,2371],{"class":158},[127,2405,546],{"class":140},[127,2407,21],{"class":158},[127,2409,2218],{"class":140},[127,2411,21],{"class":158},[127,2413,2129],{"class":140},[127,2415,21],{"class":158},[127,2417,2227],{"class":140},[127,2419,21],{"class":158},[127,2421,2422],{"class":140},"\"second\"",[127,2424,2384],{"class":158},[127,2426,814],{"class":611},[127,2428,297],{"class":147},[127,2430,2391],{"class":158},[127,2432,2097],{"class":611},[127,2434,297],{"class":147},[127,2436,668],{"class":151},[127,2438,385],{"class":158},[127,2440,2441,2444,2446,2449,2451,2453,2455,2457],{"class":129,"line":673},[127,2442,2443],{"class":158},"    changes ",[127,2445,297],{"class":147},[127,2447,2448],{"class":158}," git.changed_files(",[127,2450,2246],{"class":140},[127,2452,21],{"class":158},[127,2454,814],{"class":611},[127,2456,297],{"class":147},[127,2458,2459],{"class":158},"repo)\n",[127,2461,2462,2464,2467,2469,2472,2475,2478,2481,2483,2486],{"class":129,"line":685},[127,2463,2278],{"class":147},[127,2465,2466],{"class":158}," changes ",[127,2468,912],{"class":147},[127,2470,2471],{"class":158}," [git.Change(",[127,2473,2474],{"class":140},"\"A\"",[127,2476,2477],{"class":158},", Path(",[127,2479,2480],{"class":140},"\"odd name",[127,2482,2353],{"class":151},[127,2484,2485],{"class":140},"with tab.txt\"",[127,2487,2488],{"class":158},"))]\n",[127,2490,2491],{"class":129,"line":698},[127,2492,166],{"emptyLinePlaceholder":165},[127,2494,2495],{"class":129,"line":711},[127,2496,166],{"emptyLinePlaceholder":165},[127,2498,2499,2501,2504],{"class":129,"line":724},[127,2500,479],{"class":147},[127,2502,2503],{"class":236}," test_latest_tag",[127,2505,2273],{"class":158},[127,2507,2508,2510,2513,2515],{"class":129,"line":730},[127,2509,2278],{"class":147},[127,2511,2512],{"class":158}," git.latest_tag(repo) ",[127,2514,912],{"class":147},[127,2516,2517],{"class":140}," \"v1.0.0\"\n",[127,2519,2520],{"class":129,"line":747},[127,2521,166],{"emptyLinePlaceholder":165},[127,2523,2524],{"class":129,"line":755},[127,2525,166],{"emptyLinePlaceholder":165},[127,2527,2528,2530,2533],{"class":129,"line":764},[127,2529,479],{"class":147},[127,2531,2532],{"class":236}," test_not_a_repo",[127,2534,2535],{"class":158},"(tmp_path):\n",[127,2537,2538,2541,2544,2546],{"class":129,"line":769},[127,2539,2540],{"class":147},"    with",[127,2542,2543],{"class":158}," pytest.raises(git.GitError) ",[127,2545,1331],{"class":147},[127,2547,2548],{"class":158}," info:\n",[127,2550,2551],{"class":129,"line":774},[127,2552,2553],{"class":158},"        git.repo_root(tmp_path)\n",[127,2555,2556,2558,2561,2563],{"class":129,"line":796},[127,2557,2278],{"class":147},[127,2559,2560],{"class":140}," \"not a git repository\"",[127,2562,741],{"class":147},[127,2564,2565],{"class":158}," info.value.stderr\n",[10,2567,2568,2569,2571,2572,2575,2576,2579,2580,34],{},"The tab-in-filename test is the one that justifies ",[14,2570,1422],{},": without it, git would print the path as ",[14,2573,2574],{},"\"odd name\\twith tab.txt\""," with literal quotes and a backslash escape, and the parser would return the wrong path. Mark these tests to skip when git is unavailable if your CI images vary. For command-level tests, ",[14,2577,2578],{},"monkeypatch.setattr(git, \"current_branch\", lambda: \"feature\")"," and invoke the CLI with a runner, as in ",[30,2581,2583],{"href":2582},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner\u002F","testing Click commands with CliRunner",[36,2585,2587],{"id":2586},"conclusion","Conclusion",[10,2589,2590,2591,2593,2594,2596],{},"Treat every external tool your CLI depends on like a remote API: one client module, one place where calls are made, stable machine-readable formats, typed results and a single error type. For git that means porcelain output, ",[14,2592,1422],{}," separators, a fixed locale and no prompts. The payoff is a command layer that reads like the requirements and a test suite that can exercise it without touching ",[14,2595,111],{}," at all.",[36,2598,2600],{"id":2599},"frequently-asked-questions","Frequently asked questions",[1376,2602,2604,2605,2607],{"id":2603},"should-the-wrapper-cache-results-like-repo_root","Should the wrapper cache results like ",[14,2606,1930],{},"?",[10,2609,2610,2611,2614],{},"Within one command invocation, caching is safe and cheap with ",[14,2612,2613],{},"functools.cache"," on functions that take no arguments — but be careful with functions whose answer changes when your tool itself modifies the repository. Cache the root, not the status.",[1376,2616,2618],{"id":2617},"how-do-i-handle-a-repository-in-a-git-worktree-or-submodule","How do I handle a repository in a git worktree or submodule?",[10,2620,2621,2624,2625,2628,2629,2632,2633,2635],{},[14,2622,2623],{},"rev-parse --show-toplevel"," returns the worktree or submodule root, which is what most tools want. If you need the main repository's ",[14,2626,2627],{},".git"," directory, ask for ",[14,2630,2631],{},"--git-common-dir",". Both are handled by git, which is one more reason to shell out rather than reading ",[14,2634,2627],{}," yourself.",[1376,2637,2639],{"id":2638},"why-does-git-complain-about-dubious-ownership-in-ci","Why does git complain about \"dubious ownership\" in CI?",[10,2641,2642,2643,2645],{},"Git refuses to operate on repositories owned by another user unless they are listed in ",[14,2644,1905],{},". Containers that mount a checkout often trigger it. Report git's message verbatim — it includes the exact config command to run — rather than trying to work around it in your wrapper.",[1376,2647,2649],{"id":2648},"can-i-stream-the-output-of-long-git-operations","Can I stream the output of long git operations?",[10,2651,2652,2653,2657,2658,1951,2661,2664,2665,2668],{},"Yes: use the streaming runner from ",[30,2654,2656],{"href":2655},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time\u002F","streaming subprocess output in real time"," for ",[14,2659,2660],{},"clone",[14,2662,2663],{},"fetch",", and pass ",[14,2666,2667],{},"--progress"," so git reports progress even when stderr is a pipe.",[36,2670,2672],{"id":2671},"related","Related",[41,2674,2675,2681,2687,2693,2699],{},[44,2676,2677,2678],{},"Up: ",[30,2679,2680],{"href":32},"Running subprocesses from Python CLIs",[44,2682,2683],{},[30,2684,2686],{"href":2685},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess\u002F","Calling external commands safely with subprocess",[44,2688,2689],{},[30,2690,2692],{"href":2691},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis\u002F","Avoiding shell injection in Python CLIs",[44,2694,2695],{},[30,2696,2698],{"href":2697},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands\u002F","Dependency injection patterns for CLI commands",[44,2700,2701],{},[30,2702,2704],{"href":2703},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits\u002F","Automating changelogs with conventional commits",[2706,2707,2708],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .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);}",{"title":123,"searchDepth":137,"depth":137,"links":2710},[2711,2712,2713,2716,2717,2718,2719,2720,2721,2728],{"id":38,"depth":137,"text":39},{"id":66,"depth":137,"text":67},{"id":115,"depth":137,"text":116,"children":2714},[2715],{"id":1378,"depth":144,"text":1379},{"id":1461,"depth":137,"text":1462},{"id":1886,"depth":137,"text":1887},{"id":1912,"depth":137,"text":1913},{"id":1966,"depth":137,"text":1967},{"id":2586,"depth":137,"text":2587},{"id":2599,"depth":137,"text":2600,"children":2722},[2723,2725,2726,2727],{"id":2603,"depth":144,"text":2724},"Should the wrapper cache results like repo_root()?",{"id":2617,"depth":144,"text":2618},{"id":2638,"depth":144,"text":2639},{"id":2648,"depth":144,"text":2649},{"id":2671,"depth":137,"text":2672},"2026-09-18","Build a small typed Python module over git: one runner, porcelain and -z output, parsed results, clear errors and tests that never need a real repository.","intermediate",false,"md",{},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli",{"title":5,"description":2730},"cli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli\u002Findex",[1901,111,2739,2740],"architecture","testing","niB1HL2P37tlSOqb_TSfe5CDJOfW6-uyqprr6G80ajY",[2743,2746,2749,2752,2755,2758,2761,2764,2767,2770,2773,2776,2779,2782,2785,2788,2791,2794,2797,2800,2803,2806,2809,2812,2815,2818,2821,2824,2827,2830,2833,2836,2839,2842,2845,2848,2851,2854,2857,2860,2863,2866,2869,2872,2875,2878,2881,2884,2887,2890,2893,2896,2899,2902,2905,2908,2911,2914,2917,2920,2923,2926,2929,2932,2935,2938,2941,2944,2947,2950,2953,2956,2959,2962,2965,2968,2971,2974,2977,2980,2983,2986,2989,2992,2995,2996,2999,3002,3005,3008,3011,3014,3016,3019,3022,3025,3028,3031,3034,3037,3040,3043,3046,3049,3052,3055,3058,3061,3064,3067,3070,3073,3076,3079,3082,3085,3088,3091,3094,3097,3100,3103,3106,3109,3112,3115,3118,3121,3124,3127,3130,3133,3136,3139,3142,3145,3148,3151,3154,3157,3160,3163,3166,3169,3172,3175,3178,3181,3184,3187,3190,3193,3196,3199,3202,3205,3208,3211,3214,3217,3220,3223,3226,3229,3232,3235,3238,3241,3244,3247,3250,3253,3256,3259,3262,3265,3268,3271,3274,3277,3280,3283,3286],{"path":2744,"title":2745},"\u002Fabout","About Python CLI Toolcraft",{"path":2747,"title":2748},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2750,"title":2751},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2753,"title":2754},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2756,"title":2757},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2759,"title":2760},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2762,"title":2763},"\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":2765,"title":2766},"\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":2768,"title":2769},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2771,"title":2772},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2774,"title":2775},"\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":2777,"title":2778},"\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":2780,"title":2781},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2783,"title":2784},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2786,"title":2787},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2789,"title":2790},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2792,"title":2793},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2795,"title":2796},"\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":2798,"title":2799},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2801,"title":2802},"\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":2804,"title":2805},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2807,"title":2808},"\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":2810,"title":2811},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2813,"title":2814},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2816,"title":2817},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2819,"title":2820},"\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":2822,"title":2823},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2825,"title":2826},"\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":2828,"title":2829},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2831,"title":2832},"\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":2834,"title":2835},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2837,"title":2838},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2840,"title":2841},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2843,"title":2844},"\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":2846,"title":2847},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2849,"title":2850},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2852,"title":2853},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2855,"title":2856},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2858,"title":2859},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2861,"title":2862},"\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":2864,"title":2865},"\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":2867,"title":2868},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2870,"title":2871},"\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":2873,"title":2874},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2876,"title":2877},"\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":2879,"title":2880},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2882,"title":2883},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2885,"title":2886},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2888,"title":2889},"\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":2891,"title":2892},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2894,"title":2895},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2897,"title":2898},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2900,"title":2901},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2903,"title":2904},"\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":2906,"title":2907},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2909,"title":2910},"\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":2912,"title":2913},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2915,"title":2916},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2918,"title":2919},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2921,"title":2922},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2924,"title":2925},"\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":2927,"title":2928},"\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":2930,"title":2931},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2933,"title":2934},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2936,"title":2937},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2939,"title":2940},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2942,"title":2943},"\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":2945,"title":2946},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2948,"title":2949},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2951,"title":2952},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2954,"title":2955},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2957,"title":2958},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2960,"title":2961},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2963,"title":2964},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2966,"title":2967},"\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":2969,"title":2970},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2972,"title":2973},"\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":2975,"title":2976},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2978,"title":2979},"\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":2981,"title":2982},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2984,"title":2985},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2987,"title":2988},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2990,"title":2991},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2993,"title":2994},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2735,"title":5},{"path":2997,"title":2998},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":3000,"title":3001},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":3003,"title":3004},"\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":3006,"title":3007},"\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":3009,"title":3010},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":3012,"title":3013},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":2186,"title":3015},"Python CLI Toolcraft",{"path":3017,"title":3018},"\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":3020,"title":3021},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":3023,"title":3024},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":3026,"title":3027},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":3029,"title":3030},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":3032,"title":3033},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":3035,"title":3036},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":3038,"title":3039},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":3041,"title":3042},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":3044,"title":3045},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":3047,"title":3048},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":3050,"title":3051},"\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":3053,"title":3054},"\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":3056,"title":3057},"\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":3059,"title":3060},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":3062,"title":3063},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":3065,"title":3066},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":3068,"title":3069},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":3071,"title":3072},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":3074,"title":3075},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":3077,"title":3078},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":3080,"title":3081},"\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":3083,"title":3084},"\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":3086,"title":3087},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":3089,"title":3090},"\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":3092,"title":3093},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":3095,"title":3096},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":3098,"title":3099},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":3101,"title":3102},"\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":3104,"title":3105},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":3107,"title":3108},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":3110,"title":3111},"\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":3113,"title":3114},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":3116,"title":3117},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":3119,"title":3120},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":3122,"title":3123},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":3125,"title":3126},"\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":3128,"title":3129},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":3131,"title":3132},"\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":3134,"title":3135},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":3137,"title":3138},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":3140,"title":3141},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":3143,"title":3144},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":3146,"title":3147},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":3149,"title":3150},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":3152,"title":3153},"\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":3155,"title":3156},"\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":3158,"title":3159},"\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":3161,"title":3162},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":3164,"title":3165},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":3167,"title":3168},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":3170,"title":3171},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":3173,"title":3174},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":3176,"title":3177},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":3179,"title":3180},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":3182,"title":3183},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":3185,"title":3186},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":3188,"title":3189},"\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":3191,"title":3192},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":3194,"title":3195},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":3197,"title":3198},"\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":3200,"title":3201},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":3203,"title":3204},"\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":3206,"title":3207},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":3209,"title":3210},"\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":3212,"title":3213},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":3215,"title":3216},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":3218,"title":3219},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":3221,"title":3222},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":3224,"title":3225},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":3227,"title":3228},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":3230,"title":3231},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":3233,"title":3234},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":3236,"title":3237},"\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":3239,"title":3240},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":3242,"title":3243},"\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":3245,"title":3246},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":3248,"title":3249},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":3251,"title":3252},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":3254,"title":3255},"\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":3257,"title":3258},"\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":3260,"title":3261},"\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":3263,"title":3264},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":3266,"title":3267},"\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":3269,"title":3270},"\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":3272,"title":3273},"\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":3275,"title":3276},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":3278,"title":3279},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":3281,"title":3282},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":3284,"title":3285},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":3287,"title":3288},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736905050]