[{"data":1,"prerenderedAt":1639},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands\u002F":3,"content-directory":1392},{"id":4,"title":5,"body":6,"date":1376,"description":1377,"difficulty":1378,"draft":1379,"extension":1380,"meta":1381,"navigation":121,"path":1382,"seo":1383,"stem":1384,"tags":1385,"updated":1376,"__hash__":1391},"content\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands\u002Findex.md","Dependency Injection Patterns for CLI Commands",{"type":7,"value":8,"toc":1356},"minimark",[9,13,18,57,61,70,74,78,81,275,347,450,462,466,469,475,481,563,566,572,578,582,585,590,676,821,832,836,839,842,849,852,856,859,873,883,887,890,1008,1128,1142,1146,1149,1155,1218,1224,1233,1236,1239,1243,1246,1250,1255,1262,1266,1275,1279,1288,1292,1295,1299,1314,1318,1352],[10,11,12],"p",{},"Commands need things: an HTTP client, a database session, a clock, a settings object. How those\narrive decides whether your logic can be tested without patching, and whether two commands can\ndisagree about the state of the world. The answer is not a framework — it is two small habits.",[14,15,17],"h2",{"id":16},"tldr","TL;DR",[19,20,21,30,42,51,54],"ul",{},[22,23,24,25,29],"li",{},"Build collaborators ",[26,27,28],"strong",{},"once at the boundary",", put them on the context, pass them down explicitly.",[22,31,32,33,36,37,41],{},"A ",[26,34,35],{},"default parameter"," (",[38,39,40],"code",{},"client: Client | None = None",") covers most cases with no ceremony.",[22,43,44,45,50],{},"Describe the seam with a ",[26,46,47],{},[38,48,49],{},"Protocol"," so a fake cannot silently drift from the real thing.",[22,52,53],{},"Never reach for a module-level global — it is the thing tests have to remember to reset.",[22,55,56],{},"If you need a container, keep it dumb: a frozen dataclass of already-built objects.",[14,58,60],{"id":59},"prerequisites","Prerequisites",[10,62,63,64,69],{},"A CLI with the layering from\n",[65,66,68],"a",{"href":67},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002F","structuring multi-command Python CLIs",":\ncommand functions that call plain functions in a core package.",[14,71,73],{"id":72},"where-things-get-constructed","Where things get constructed",[75,76],"inline-diagram",{"name":77},"di-wiring-flow",[10,79,80],{},"The rule: construct at the boundary, once per invocation, from settings. Commands receive what they\nneed; core functions take it as arguments.",[82,83,88],"pre",{"className":84,"code":85,"language":86,"meta":87,"style":87},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fcontext.py\nfrom dataclasses import dataclass\n\n@dataclass(frozen=True, slots=True)\nclass Services:\n    settings: Settings\n    bucket: BucketClient\n    clock: Callable[[], datetime]\n\ndef build(settings: Settings) -> Services:\n    return Services(\n        settings=settings,\n        bucket=S3Bucket(region=settings.region, retries=settings.retries),\n        clock=lambda: datetime.now(UTC),\n    )\n","python","",[38,89,90,99,116,123,157,169,175,181,187,192,204,213,224,251,269],{"__ignoreMap":87},[91,92,95],"span",{"class":93,"line":94},"line",1,[91,96,98],{"class":97},"sJ8bj","# src\u002Fmytool\u002Fcontext.py\n",[91,100,102,106,110,113],{"class":93,"line":101},2,[91,103,105],{"class":104},"szBVR","from",[91,107,109],{"class":108},"sVt8B"," dataclasses ",[91,111,112],{"class":104},"import",[91,114,115],{"class":108}," dataclass\n",[91,117,119],{"class":93,"line":118},3,[91,120,122],{"emptyLinePlaceholder":121},true,"\n",[91,124,126,130,133,137,140,144,147,150,152,154],{"class":93,"line":125},4,[91,127,129],{"class":128},"sScJk","@dataclass",[91,131,132],{"class":108},"(",[91,134,136],{"class":135},"s4XuR","frozen",[91,138,139],{"class":104},"=",[91,141,143],{"class":142},"sj4cs","True",[91,145,146],{"class":108},", ",[91,148,149],{"class":135},"slots",[91,151,139],{"class":104},[91,153,143],{"class":142},[91,155,156],{"class":108},")\n",[91,158,160,163,166],{"class":93,"line":159},5,[91,161,162],{"class":104},"class",[91,164,165],{"class":128}," Services",[91,167,168],{"class":108},":\n",[91,170,172],{"class":93,"line":171},6,[91,173,174],{"class":108},"    settings: Settings\n",[91,176,178],{"class":93,"line":177},7,[91,179,180],{"class":108},"    bucket: BucketClient\n",[91,182,184],{"class":93,"line":183},8,[91,185,186],{"class":108},"    clock: Callable[[], datetime]\n",[91,188,190],{"class":93,"line":189},9,[91,191,122],{"emptyLinePlaceholder":121},[91,193,195,198,201],{"class":93,"line":194},10,[91,196,197],{"class":104},"def",[91,199,200],{"class":128}," build",[91,202,203],{"class":108},"(settings: Settings) -> Services:\n",[91,205,207,210],{"class":93,"line":206},11,[91,208,209],{"class":104},"    return",[91,211,212],{"class":108}," Services(\n",[91,214,216,219,221],{"class":93,"line":215},12,[91,217,218],{"class":135},"        settings",[91,220,139],{"class":104},[91,222,223],{"class":108},"settings,\n",[91,225,227,230,232,235,238,240,243,246,248],{"class":93,"line":226},13,[91,228,229],{"class":135},"        bucket",[91,231,139],{"class":104},[91,233,234],{"class":108},"S3Bucket(",[91,236,237],{"class":135},"region",[91,239,139],{"class":104},[91,241,242],{"class":108},"settings.region, ",[91,244,245],{"class":135},"retries",[91,247,139],{"class":104},[91,249,250],{"class":108},"settings.retries),\n",[91,252,254,257,260,263,266],{"class":93,"line":253},14,[91,255,256],{"class":135},"        clock",[91,258,259],{"class":104},"=lambda",[91,261,262],{"class":108},": datetime.now(",[91,264,265],{"class":142},"UTC",[91,267,268],{"class":108},"),\n",[91,270,272],{"class":93,"line":271},15,[91,273,274],{"class":108},"    )\n",[82,276,278],{"className":84,"code":277,"language":86,"meta":87,"style":87},"# src\u002Fmytool\u002Fcli.py\n@app.callback()\ndef main(ctx: typer.Context, config: Path | None = None) -> None:\n    settings = load_settings(config)\n    configure_logging(settings.verbosity)\n    ctx.obj = build(settings)\n",[38,279,280,285,293,322,332,337],{"__ignoreMap":87},[91,281,282],{"class":93,"line":94},[91,283,284],{"class":97},"# src\u002Fmytool\u002Fcli.py\n",[91,286,287,290],{"class":93,"line":101},[91,288,289],{"class":128},"@app.callback",[91,291,292],{"class":108},"()\n",[91,294,295,297,300,303,306,309,312,314,317,320],{"class":93,"line":118},[91,296,197],{"class":104},[91,298,299],{"class":128}," main",[91,301,302],{"class":108},"(ctx: typer.Context, config: Path ",[91,304,305],{"class":104},"|",[91,307,308],{"class":142}," None",[91,310,311],{"class":104}," =",[91,313,308],{"class":142},[91,315,316],{"class":108},") -> ",[91,318,319],{"class":142},"None",[91,321,168],{"class":108},[91,323,324,327,329],{"class":93,"line":125},[91,325,326],{"class":108},"    settings ",[91,328,139],{"class":104},[91,330,331],{"class":108}," load_settings(config)\n",[91,333,334],{"class":93,"line":159},[91,335,336],{"class":108},"    configure_logging(settings.verbosity)\n",[91,338,339,342,344],{"class":93,"line":171},[91,340,341],{"class":108},"    ctx.obj ",[91,343,139],{"class":104},[91,345,346],{"class":108}," build(settings)\n",[82,348,350],{"className":84,"code":349,"language":86,"meta":87,"style":87},"# src\u002Fmytool\u002Fcommands\u002Fsync.py\n@app.command()\ndef sync(ctx: typer.Context, source: Path, dry_run: bool = False) -> None:\n    services = ctx.obj\n    result = core.sync_directory(source, bucket=services.bucket, dry_run=dry_run)\n    typer.echo(f\"{result.uploaded} uploaded\")\n",[38,351,352,357,364,388,398,424],{"__ignoreMap":87},[91,353,354],{"class":93,"line":94},[91,355,356],{"class":97},"# src\u002Fmytool\u002Fcommands\u002Fsync.py\n",[91,358,359,362],{"class":93,"line":101},[91,360,361],{"class":128},"@app.command",[91,363,292],{"class":108},[91,365,366,368,371,374,377,379,382,384,386],{"class":93,"line":118},[91,367,197],{"class":104},[91,369,370],{"class":128}," sync",[91,372,373],{"class":108},"(ctx: typer.Context, source: Path, dry_run: ",[91,375,376],{"class":142},"bool",[91,378,311],{"class":104},[91,380,381],{"class":142}," False",[91,383,316],{"class":108},[91,385,319],{"class":142},[91,387,168],{"class":108},[91,389,390,393,395],{"class":93,"line":125},[91,391,392],{"class":108},"    services ",[91,394,139],{"class":104},[91,396,397],{"class":108}," ctx.obj\n",[91,399,400,403,405,408,411,413,416,419,421],{"class":93,"line":159},[91,401,402],{"class":108},"    result ",[91,404,139],{"class":104},[91,406,407],{"class":108}," core.sync_directory(source, ",[91,409,410],{"class":135},"bucket",[91,412,139],{"class":104},[91,414,415],{"class":108},"services.bucket, ",[91,417,418],{"class":135},"dry_run",[91,420,139],{"class":104},[91,422,423],{"class":108},"dry_run)\n",[91,425,426,429,432,436,439,442,445,448],{"class":93,"line":171},[91,427,428],{"class":108},"    typer.echo(",[91,430,431],{"class":104},"f",[91,433,435],{"class":434},"sZZnC","\"",[91,437,438],{"class":142},"{",[91,440,441],{"class":108},"result.uploaded",[91,443,444],{"class":142},"}",[91,446,447],{"class":434}," uploaded\"",[91,449,156],{"class":108},[10,451,452,453,457,458,461],{},"Three properties fall out. Every command sees the ",[454,455,456],"em",{},"same"," client, built once — no connection pool\nper command, no duplicated configuration parsing. The construction is in one reviewable function.\nAnd a test can build a ",[38,459,460],{},"Services"," with fakes and hand it straight to the command.",[14,463,465],{"id":464},"four-ways-to-supply-a-dependency","Four ways to supply a dependency",[75,467],{"name":468},"di-approach-matrix",[10,470,471,474],{},[26,472,473],{},"A module-level global"," is what people reach for first and the only one that is simply wrong. It\nsurvives between tests, so a suite passes or fails depending on order, and every test that touches\nit needs to remember a reset.",[10,476,477,480],{},[26,478,479],{},"A default parameter"," is the lightest thing that works, and it covers most core functions:",[82,482,484],{"className":84,"code":483,"language":86,"meta":87,"style":87},"def sync_directory(\n    source: Path,\n    *,\n    bucket: BucketClient | None = None,\n    dry_run: bool = False,\n) -> SyncResult:\n    bucket = bucket or S3Bucket()\n    ...\n",[38,485,486,496,501,509,524,537,542,558],{"__ignoreMap":87},[91,487,488,490,493],{"class":93,"line":94},[91,489,197],{"class":104},[91,491,492],{"class":128}," sync_directory",[91,494,495],{"class":108},"(\n",[91,497,498],{"class":93,"line":101},[91,499,500],{"class":108},"    source: Path,\n",[91,502,503,506],{"class":93,"line":118},[91,504,505],{"class":104},"    *",[91,507,508],{"class":108},",\n",[91,510,511,514,516,518,520,522],{"class":93,"line":125},[91,512,513],{"class":108},"    bucket: BucketClient ",[91,515,305],{"class":104},[91,517,308],{"class":142},[91,519,311],{"class":104},[91,521,308],{"class":142},[91,523,508],{"class":108},[91,525,526,529,531,533,535],{"class":93,"line":159},[91,527,528],{"class":108},"    dry_run: ",[91,530,376],{"class":142},[91,532,311],{"class":104},[91,534,381],{"class":142},[91,536,508],{"class":108},[91,538,539],{"class":93,"line":171},[91,540,541],{"class":108},") -> SyncResult:\n",[91,543,544,547,549,552,555],{"class":93,"line":177},[91,545,546],{"class":108},"    bucket ",[91,548,139],{"class":104},[91,550,551],{"class":108}," bucket ",[91,553,554],{"class":104},"or",[91,556,557],{"class":108}," S3Bucket()\n",[91,559,560],{"class":93,"line":183},[91,561,562],{"class":142},"    ...\n",[10,564,565],{},"Production code calls it with no extra argument; a test passes a fake. No patching, no container,\nand the signature documents the dependency.",[10,567,568,571],{},[26,569,570],{},"A context object"," is the right answer when several commands share the same collaborators, which\nis the usual case for a CLI. It is the pattern shown above.",[10,573,574,577],{},[26,575,576],{},"A container"," — a registry that constructs objects on demand — earns its place only when there\nare many interdependent parts with real lifecycles. For a CLI, a frozen dataclass of already-built\nobjects gives you the same benefit without the indirection.",[14,579,581],{"id":580},"making-the-seam-explicit","Making the seam explicit",[75,583],{"name":584},"di-protocol-boundary",[10,586,32,587,589],{},[38,588,49],{}," documents what a collaborator must provide, and lets a type checker verify that both\nthe real implementation and the test fake satisfy it:",[82,591,593],{"className":84,"code":592,"language":86,"meta":87,"style":87},"from typing import Protocol\n\nclass BucketClient(Protocol):\n    def is_current(self, path: Path) -> bool: ...\n    def upload(self, path: Path, *, retries: int = 3) -> None: ...\n",[38,594,595,607,611,625,644],{"__ignoreMap":87},[91,596,597,599,602,604],{"class":93,"line":94},[91,598,105],{"class":104},[91,600,601],{"class":108}," typing ",[91,603,112],{"class":104},[91,605,606],{"class":108}," Protocol\n",[91,608,609],{"class":93,"line":101},[91,610,122],{"emptyLinePlaceholder":121},[91,612,613,615,618,620,622],{"class":93,"line":118},[91,614,162],{"class":104},[91,616,617],{"class":128}," BucketClient",[91,619,132],{"class":108},[91,621,49],{"class":128},[91,623,624],{"class":108},"):\n",[91,626,627,630,633,636,638,641],{"class":93,"line":125},[91,628,629],{"class":104},"    def",[91,631,632],{"class":128}," is_current",[91,634,635],{"class":108},"(self, path: Path) -> ",[91,637,376],{"class":142},[91,639,640],{"class":108},": ",[91,642,643],{"class":142},"...\n",[91,645,646,648,651,654,657,660,663,665,668,670,672,674],{"class":93,"line":159},[91,647,629],{"class":104},[91,649,650],{"class":128}," upload",[91,652,653],{"class":108},"(self, path: Path, ",[91,655,656],{"class":104},"*",[91,658,659],{"class":108},", retries: ",[91,661,662],{"class":142},"int",[91,664,311],{"class":104},[91,666,667],{"class":142}," 3",[91,669,316],{"class":108},[91,671,319],{"class":142},[91,673,640],{"class":108},[91,675,643],{"class":142},[82,677,679],{"className":84,"code":678,"language":86,"meta":87,"style":87},"class FakeBucket:                      # no inheritance needed\n    def __init__(self) -> None:\n        self.uploaded: list[str] = []\n        self.current: set[str] = set()\n\n    def is_current(self, path: Path) -> bool:\n        return str(path) in self.current\n\n    def upload(self, path: Path, *, retries: int = 3) -> None:\n        self.uploaded.append(str(path))\n",[38,680,681,694,708,727,745,749,761,781,785,809],{"__ignoreMap":87},[91,682,683,685,688,691],{"class":93,"line":94},[91,684,162],{"class":104},[91,686,687],{"class":128}," FakeBucket",[91,689,690],{"class":108},":                      ",[91,692,693],{"class":97},"# no inheritance needed\n",[91,695,696,698,701,704,706],{"class":93,"line":101},[91,697,629],{"class":104},[91,699,700],{"class":142}," __init__",[91,702,703],{"class":108},"(self) -> ",[91,705,319],{"class":142},[91,707,168],{"class":108},[91,709,710,713,716,719,722,724],{"class":93,"line":118},[91,711,712],{"class":142},"        self",[91,714,715],{"class":108},".uploaded: list[",[91,717,718],{"class":142},"str",[91,720,721],{"class":108},"] ",[91,723,139],{"class":104},[91,725,726],{"class":108}," []\n",[91,728,729,731,734,736,738,740,743],{"class":93,"line":125},[91,730,712],{"class":142},[91,732,733],{"class":108},".current: set[",[91,735,718],{"class":142},[91,737,721],{"class":108},[91,739,139],{"class":104},[91,741,742],{"class":142}," set",[91,744,292],{"class":108},[91,746,747],{"class":93,"line":159},[91,748,122],{"emptyLinePlaceholder":121},[91,750,751,753,755,757,759],{"class":93,"line":171},[91,752,629],{"class":104},[91,754,632],{"class":128},[91,756,635],{"class":108},[91,758,376],{"class":142},[91,760,168],{"class":108},[91,762,763,766,769,772,775,778],{"class":93,"line":177},[91,764,765],{"class":104},"        return",[91,767,768],{"class":142}," str",[91,770,771],{"class":108},"(path) ",[91,773,774],{"class":104},"in",[91,776,777],{"class":142}," self",[91,779,780],{"class":108},".current\n",[91,782,783],{"class":93,"line":183},[91,784,122],{"emptyLinePlaceholder":121},[91,786,787,789,791,793,795,797,799,801,803,805,807],{"class":93,"line":189},[91,788,629],{"class":104},[91,790,650],{"class":128},[91,792,653],{"class":108},[91,794,656],{"class":104},[91,796,659],{"class":108},[91,798,662],{"class":142},[91,800,311],{"class":104},[91,802,667],{"class":142},[91,804,316],{"class":108},[91,806,319],{"class":142},[91,808,168],{"class":108},[91,810,811,813,816,818],{"class":93,"line":194},[91,812,712],{"class":142},[91,814,815],{"class":108},".uploaded.append(",[91,817,718],{"class":142},[91,819,820],{"class":108},"(path))\n",[10,822,823,824,827,828,831],{},"The value arrives when the real interface changes. Adding a method to ",[38,825,826],{},"S3Bucket"," without adding it\nto the protocol is fine; adding it to the protocol without updating the fake is a ",[38,829,830],{},"mypy"," error in\nthe pull request that made the change, rather than a runtime surprise months later.",[14,833,835],{"id":834},"what-not-to-inject","What not to inject",[10,837,838],{},"Injection is a tool for things that are slow, stateful or non-deterministic. Injecting everything\nproduces signatures nobody can read.",[10,840,841],{},"Worth injecting: HTTP clients, database sessions, filesystem roots, the clock, random sources,\nanything that talks to a service.",[10,843,844,845,848],{},"Not worth injecting: pure functions, formatting helpers, constants, standard library modules that\ndo not touch the world. ",[38,846,847],{},"json.dumps"," does not need to arrive as a parameter.",[10,850,851],{},"The test is whether a test would want to substitute it. If no test ever would, a plain import is\nclearer.",[14,853,855],{"id":854},"ux-considerations","UX considerations",[10,857,858],{},"This is an internal pattern, but it shows up in two user-visible ways.",[10,860,861,864,865,868,869,872],{},[26,862,863],{},"Start-up cost."," Building collaborators in the callback means constructing them on ",[454,866,867],{},"every","\ninvocation, including ",[38,870,871],{},"--help",". If a client opens a connection or reads credentials eagerly, that\ncost lands on the cheapest commands. Construct lazily inside the object, or build only what the\nchosen command needs.",[10,874,875,878,879,882],{},[26,876,877],{},"Error timing."," Failing to construct a client in the callback means ",[38,880,881],{},"mytool --help"," fails when\nthe configuration is wrong. Validate configuration there, but defer anything that can fail for\nenvironmental reasons — a missing credential should surface when a command needs it, with a message\nabout that command.",[14,884,886],{"id":885},"testing-the-behaviour","Testing the behaviour",[10,888,889],{},"The payoff is that tests need no patching at all:",[82,891,893],{"className":84,"code":892,"language":86,"meta":87,"style":87},"def test_sync_skips_current_files(tmp_path):\n    (tmp_path \u002F \"a.txt\").write_text(\"x\")\n    bucket = FakeBucket()\n    bucket.current.add(str(tmp_path \u002F \"a.txt\"))\n\n    result = core.sync_directory(tmp_path, bucket=bucket)\n\n    assert result == SyncResult(uploaded=0, skipped=1)\n",[38,894,895,905,924,933,950,954,970,974],{"__ignoreMap":87},[91,896,897,899,902],{"class":93,"line":94},[91,898,197],{"class":104},[91,900,901],{"class":128}," test_sync_skips_current_files",[91,903,904],{"class":108},"(tmp_path):\n",[91,906,907,910,913,916,919,922],{"class":93,"line":101},[91,908,909],{"class":108},"    (tmp_path ",[91,911,912],{"class":104},"\u002F",[91,914,915],{"class":434}," \"a.txt\"",[91,917,918],{"class":108},").write_text(",[91,920,921],{"class":434},"\"x\"",[91,923,156],{"class":108},[91,925,926,928,930],{"class":93,"line":118},[91,927,546],{"class":108},[91,929,139],{"class":104},[91,931,932],{"class":108}," FakeBucket()\n",[91,934,935,938,940,943,945,947],{"class":93,"line":125},[91,936,937],{"class":108},"    bucket.current.add(",[91,939,718],{"class":142},[91,941,942],{"class":108},"(tmp_path ",[91,944,912],{"class":104},[91,946,915],{"class":434},[91,948,949],{"class":108},"))\n",[91,951,952],{"class":93,"line":159},[91,953,122],{"emptyLinePlaceholder":121},[91,955,956,958,960,963,965,967],{"class":93,"line":171},[91,957,402],{"class":108},[91,959,139],{"class":104},[91,961,962],{"class":108}," core.sync_directory(tmp_path, ",[91,964,410],{"class":135},[91,966,139],{"class":104},[91,968,969],{"class":108},"bucket)\n",[91,971,972],{"class":93,"line":177},[91,973,122],{"emptyLinePlaceholder":121},[91,975,976,979,982,985,988,991,993,996,998,1001,1003,1006],{"class":93,"line":183},[91,977,978],{"class":104},"    assert",[91,980,981],{"class":108}," result ",[91,983,984],{"class":104},"==",[91,986,987],{"class":108}," SyncResult(",[91,989,990],{"class":135},"uploaded",[91,992,139],{"class":104},[91,994,995],{"class":142},"0",[91,997,146],{"class":108},[91,999,1000],{"class":135},"skipped",[91,1002,139],{"class":104},[91,1004,1005],{"class":142},"1",[91,1007,156],{"class":108},[82,1009,1011],{"className":84,"code":1010,"language":86,"meta":87,"style":87},"def test_command_uses_the_injected_services(cli):\n    services = Services(settings=Settings(), bucket=FakeBucket(), clock=lambda: FIXED_TIME)\n\n    result = cli.invoke(app, [\"sync\", \"data\"], obj=services)     # runner passes ctx.obj\n\n    assert result.exit_code == 0\n    assert services.bucket.uploaded == [\"data\u002Fa.txt\"]\n",[38,1012,1013,1023,1059,1063,1094,1098,1110],{"__ignoreMap":87},[91,1014,1015,1017,1020],{"class":93,"line":94},[91,1016,197],{"class":104},[91,1018,1019],{"class":128}," test_command_uses_the_injected_services",[91,1021,1022],{"class":108},"(cli):\n",[91,1024,1025,1027,1029,1032,1035,1037,1040,1042,1044,1047,1050,1052,1054,1057],{"class":93,"line":101},[91,1026,392],{"class":108},[91,1028,139],{"class":104},[91,1030,1031],{"class":108}," Services(",[91,1033,1034],{"class":135},"settings",[91,1036,139],{"class":104},[91,1038,1039],{"class":108},"Settings(), ",[91,1041,410],{"class":135},[91,1043,139],{"class":104},[91,1045,1046],{"class":108},"FakeBucket(), ",[91,1048,1049],{"class":135},"clock",[91,1051,259],{"class":104},[91,1053,640],{"class":108},[91,1055,1056],{"class":142},"FIXED_TIME",[91,1058,156],{"class":108},[91,1060,1061],{"class":93,"line":118},[91,1062,122],{"emptyLinePlaceholder":121},[91,1064,1065,1067,1069,1072,1075,1077,1080,1083,1086,1088,1091],{"class":93,"line":125},[91,1066,402],{"class":108},[91,1068,139],{"class":104},[91,1070,1071],{"class":108}," cli.invoke(app, [",[91,1073,1074],{"class":434},"\"sync\"",[91,1076,146],{"class":108},[91,1078,1079],{"class":434},"\"data\"",[91,1081,1082],{"class":108},"], ",[91,1084,1085],{"class":135},"obj",[91,1087,139],{"class":104},[91,1089,1090],{"class":108},"services)     ",[91,1092,1093],{"class":97},"# runner passes ctx.obj\n",[91,1095,1096],{"class":93,"line":159},[91,1097,122],{"emptyLinePlaceholder":121},[91,1099,1100,1102,1105,1107],{"class":93,"line":171},[91,1101,978],{"class":104},[91,1103,1104],{"class":108}," result.exit_code ",[91,1106,984],{"class":104},[91,1108,1109],{"class":142}," 0\n",[91,1111,1112,1114,1117,1119,1122,1125],{"class":93,"line":177},[91,1113,978],{"class":104},[91,1115,1116],{"class":108}," services.bucket.uploaded ",[91,1118,984],{"class":104},[91,1120,1121],{"class":108}," [",[91,1123,1124],{"class":434},"\"data\u002Fa.txt\"",[91,1126,1127],{"class":108},"]\n",[10,1129,1130,1133,1134,1137,1138,1141],{},[38,1131,1132],{},"CliRunner.invoke"," accepts an ",[38,1135,1136],{},"obj="," argument that becomes ",[38,1139,1140],{},"ctx.obj",", which means a command test can\nsupply the whole service graph without touching the callback. That single capability is what makes\nthe context-object pattern comfortable to test.",[14,1143,1145],{"id":1144},"retrofitting-this-into-existing-code","Retrofitting this into existing code",[10,1147,1148],{},"Most codebases arrive at this pattern from the opposite direction: a module-level client that grew\nbecause it was the obvious place to put it. The refactor is safe if you do it in three steps.",[10,1150,1151,1154],{},[26,1152,1153],{},"Step one: add the parameter, keep the global."," Give the function an optional parameter that\nfalls back to the existing global. Nothing calling it needs to change, and tests can start passing\na fake immediately.",[82,1156,1158],{"className":84,"code":1157,"language":86,"meta":87,"style":87},"_client = S3Bucket()          # the existing global, untouched for now\n\ndef sync_directory(source: Path, *, bucket: BucketClient | None = None) -> SyncResult:\n    bucket = bucket or _client\n    ...\n",[38,1159,1160,1173,1177,1201,1214],{"__ignoreMap":87},[91,1161,1162,1165,1167,1170],{"class":93,"line":94},[91,1163,1164],{"class":108},"_client ",[91,1166,139],{"class":104},[91,1168,1169],{"class":108}," S3Bucket()          ",[91,1171,1172],{"class":97},"# the existing global, untouched for now\n",[91,1174,1175],{"class":93,"line":101},[91,1176,122],{"emptyLinePlaceholder":121},[91,1178,1179,1181,1183,1186,1188,1191,1193,1195,1197,1199],{"class":93,"line":118},[91,1180,197],{"class":104},[91,1182,492],{"class":128},[91,1184,1185],{"class":108},"(source: Path, ",[91,1187,656],{"class":104},[91,1189,1190],{"class":108},", bucket: BucketClient ",[91,1192,305],{"class":104},[91,1194,308],{"class":142},[91,1196,311],{"class":104},[91,1198,308],{"class":142},[91,1200,541],{"class":108},[91,1202,1203,1205,1207,1209,1211],{"class":93,"line":125},[91,1204,546],{"class":108},[91,1206,139],{"class":104},[91,1208,551],{"class":108},[91,1210,554],{"class":104},[91,1212,1213],{"class":108}," _client\n",[91,1215,1216],{"class":93,"line":159},[91,1217,562],{"class":142},[10,1219,1220,1223],{},[26,1221,1222],{},"Step two: push construction upwards."," Build the client in the callback, put it on the context,\nand pass it from each command. The global is now unused by anything except the fallback.",[10,1225,1226,1229,1230,1232],{},[26,1227,1228],{},"Step three: delete the global."," Make the parameter required, or keep the ",[38,1231,319],{}," default and\nconstruct inside — but with nothing at module level. The import-time side effect disappears, which\nalso removes it from your start-up cost.",[10,1234,1235],{},"Each step is a separate commit, the tests pass throughout, and at no point does the codebase have\ntwo competing sources for the same object.",[10,1237,1238],{},"The one thing to resist is doing all three at once across a dozen call sites. Half-finished\ninjection — some callers passing, some relying on a global — is harder to reason about than either\nend state.",[14,1240,1242],{"id":1241},"conclusion","Conclusion",[10,1244,1245],{},"Build collaborators once at the boundary, hand them down explicitly, and describe the seam with a\nprotocol. Two habits, no framework, and the result is a codebase where tests construct what they\nneed instead of patching what they cannot reach.",[14,1247,1249],{"id":1248},"frequently-asked-questions","Frequently asked questions",[1251,1252,1254],"h3",{"id":1253},"is-a-dependency-injection-library-worth-it","Is a dependency injection library worth it?",[10,1256,1257,1258,1261],{},"Rarely for a CLI. Libraries like ",[38,1259,1260],{},"dependency-injector"," solve wiring problems that appear in\nlong-lived applications with many interdependent services and lifecycles. A command-line tool\nconstructs a handful of objects once and exits; a frozen dataclass does the same job with no\nindirection to learn.",[1251,1263,1265],{"id":1264},"how-do-i-inject-into-a-command-that-click-constructs","How do I inject into a command that Click constructs?",[10,1267,1268,1269,1271,1272,1274],{},"Through the context, which is exactly what it exists for. The callback assigns ",[38,1270,1140],{},", commands\nread it, and the test runner can supply it directly with ",[38,1273,1136],{},". There is no need for the framework\nto know about your objects.",[1251,1276,1278],{"id":1277},"what-about-expensive-collaborators-only-one-command-needs","What about expensive collaborators only one command needs?",[10,1280,1281,1282,1284,1285,1287],{},"Build them lazily. Either give the ",[38,1283,460],{}," object a cached property, or have the command\nconstruct that one collaborator itself from settings. Constructing a data-warehouse client in the\ncallback so that ",[38,1286,871],{}," pays for it is a common and avoidable mistake.",[1251,1289,1291],{"id":1290},"does-this-conflict-with-lazy-command-loading","Does this conflict with lazy command loading?",[10,1293,1294],{},"No — they compose. The registry imports a command module on demand, and that module's function\nstill receives services from the context. What you should avoid is constructing collaborators at\nmodule import time, which would defeat both patterns at once.",[1251,1296,1298],{"id":1297},"how-do-i-handle-a-dependency-needed-by-only-some-commands-in-a-group","How do I handle a dependency needed by only some commands in a group?",[10,1300,1301,1302,1305,1306,1309,1310,1313],{},"Put it on the group's own callback rather than the root. A ",[38,1303,1304],{},"db"," sub-app can build a connection in\nits callback, so ",[38,1307,1308],{},"mytool sync"," never opens one, and ",[38,1311,1312],{},"mytool db migrate"," still gets it through the\ncontext chain.",[14,1315,1317],{"id":1316},"related","Related",[19,1319,1320,1326,1333,1339,1346],{},[22,1321,1322,1323],{},"Up: ",[65,1324,1325],{"href":67},"Structuring multi-command Python CLIs",[22,1327,1328,1329],{},"Sideways: ",[65,1330,1332],{"href":1331},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects\u002F","Sharing state with Click context objects",[22,1334,1328,1335],{},[65,1336,1338],{"href":1337},"\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",[22,1340,1341,1342],{},"Related: ",[65,1343,1345],{"href":1344},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmocking-filesystem-and-network-in-cli-tests\u002F","Mocking the filesystem and network in CLI tests",[22,1347,1341,1348],{},[65,1349,1351],{"href":1350},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002F","Testing Python CLI applications",[1353,1354,1355],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .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 pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":87,"searchDepth":101,"depth":101,"links":1357},[1358,1359,1360,1361,1362,1363,1364,1365,1366,1367,1368,1375],{"id":16,"depth":101,"text":17},{"id":59,"depth":101,"text":60},{"id":72,"depth":101,"text":73},{"id":464,"depth":101,"text":465},{"id":580,"depth":101,"text":581},{"id":834,"depth":101,"text":835},{"id":854,"depth":101,"text":855},{"id":885,"depth":101,"text":886},{"id":1144,"depth":101,"text":1145},{"id":1241,"depth":101,"text":1242},{"id":1248,"depth":101,"text":1249,"children":1369},[1370,1371,1372,1373,1374],{"id":1253,"depth":118,"text":1254},{"id":1264,"depth":118,"text":1265},{"id":1277,"depth":118,"text":1278},{"id":1290,"depth":118,"text":1291},{"id":1297,"depth":118,"text":1298},{"id":1316,"depth":101,"text":1317},"2026-08-01","Wire collaborators into Python CLI commands without globals - default parameters, context objects, protocols, and tests that need no monkeypatching.","advanced",false,"md",{},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands",{"title":5,"description":1377},"modern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands\u002Findex",[1386,1387,1388,1389,1390],"architecture","dependency-injection","testing","click","typer","YeCvRgBOov3SttXpNRolMU2WHz9VEBdw-TSXNBwiY2c",[1393,1396,1399,1402,1405,1408,1411,1414,1417,1420,1423,1426,1429,1432,1435,1438,1441,1444,1447,1450,1453,1456,1459,1462,1465,1468,1471,1474,1477,1480,1483,1486,1488,1491,1494,1497,1500,1503,1506,1509,1512,1515,1518,1521,1524,1525,1528,1531,1534,1537,1540,1543,1546,1549,1552,1555,1558,1561,1564,1567,1570,1573,1576,1579,1582,1585,1588,1591,1594,1597,1600,1603,1606,1609,1612,1615,1618,1621,1624,1627,1630,1633,1636],{"path":1394,"title":1395},"\u002Fabout","About Python CLI Toolcraft",{"path":1397,"title":1398},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1400,"title":1401},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1403,"title":1404},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1406,"title":1407},"\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":1409,"title":1410},"\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":1412,"title":1413},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1415,"title":1416},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1418,"title":1419},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1421,"title":1422},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1424,"title":1425},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1427,"title":1428},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1430,"title":1431},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1433,"title":1434},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1436,"title":1437},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1439,"title":1440},"\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":1442,"title":1443},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1445,"title":1446},"\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":1448,"title":1449},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1451,"title":1452},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1454,"title":1455},"\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":1457,"title":1458},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1460,"title":1461},"\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":1463,"title":1464},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1466,"title":1467},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1469,"title":1470},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1472,"title":1473},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1475,"title":1476},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1478,"title":1479},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1481,"title":1482},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1484,"title":1485},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":912,"title":1487},"Python CLI Toolcraft",{"path":1489,"title":1490},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1492,"title":1493},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1495,"title":1496},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1498,"title":1499},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1501,"title":1502},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1504,"title":1505},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1507,"title":1508},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1510,"title":1511},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1513,"title":1514},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1516,"title":1517},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1519,"title":1520},"\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":1522,"title":1523},"\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":1382,"title":5},{"path":1526,"title":1527},"\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":1529,"title":1530},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1532,"title":1533},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1535,"title":1536},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1538,"title":1539},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1541,"title":1542},"\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":1544,"title":1545},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1547,"title":1548},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1550,"title":1551},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1553,"title":1554},"\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":1556,"title":1557},"\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":1559,"title":1560},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1562,"title":1563},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1565,"title":1566},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1568,"title":1569},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1571,"title":1572},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1574,"title":1575},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1577,"title":1578},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1580,"title":1581},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1583,"title":1584},"\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":1586,"title":1587},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1589,"title":1590},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1592,"title":1593},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1595,"title":1596},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1598,"title":1599},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1601,"title":1602},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1604,"title":1605},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1607,"title":1608},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1610,"title":1611},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1613,"title":1614},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1616,"title":1617},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1619,"title":1620},"\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":1622,"title":1623},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1625,"title":1626},"\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":1628,"title":1629},"\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":1631,"title":1632},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1634,"title":1635},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1637,"title":1638},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690032]