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