[{"data":1,"prerenderedAt":1455},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis\u002F":3,"content-directory":1208},{"id":4,"title":5,"body":6,"date":1193,"description":1194,"difficulty":1195,"draft":1196,"extension":1197,"meta":1198,"navigation":182,"path":1199,"seo":1200,"stem":1201,"tags":1202,"updated":1193,"__hash__":1207},"content\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis\u002Findex.md","Homebrew and Scoop Packaging for Python CLIs",{"type":7,"value":8,"toc":1169},"minimark",[9,22,27,70,74,83,87,91,98,123,134,138,141,321,336,340,351,460,466,470,473,665,674,678,681,684,687,853,863,867,881,891,894,900,904,907,995,998,1002,1011,1015,1020,1023,1027,1040,1044,1051,1055,1072,1076,1079,1083,1086,1090,1097,1101,1112,1116,1127,1131,1165],[10,11,12,13,17,18,21],"p",{},"Homebrew and Scoop reach developers where they already are: ",[14,15,16],"code",{},"brew install"," and ",[14,19,20],{},"scoop install"," are\nmuscle memory, the manager handles upgrades and uninstalls, and neither asks the user to know\nanything about Python. The cost is that each channel becomes a permanent step in your release\nprocess — which is fine, provided the bump is automated.",[23,24,26],"h2",{"id":25},"tldr","TL;DR",[28,29,30,43,50,56,67],"ul",{},[31,32,33,34,38,39,42],"li",{},"A ",[35,36,37],"strong",{},"tap"," (Homebrew) or ",[35,40,41],{},"bucket"," (Scoop) is a small repository holding one file per tool.",[31,44,45,46,49],{},"The formula points at a ",[35,47,48],{},"release artifact and its sha256","; both change every release.",[31,51,52,55],{},[35,53,54],{},"Automate the bump"," from your release workflow, or the channel will lag and mislead people.",[31,57,58,59,62,63,66],{},"Homebrew can install from a ",[35,60,61],{},"binary"," or from a ",[35,64,65],{},"Python virtual environment"," it builds itself.",[31,68,69],{},"Add a channel because an audience asked for it, not for completeness.",[23,71,73],{"id":72},"prerequisites","Prerequisites",[10,75,76,77,82],{},"A tagged release that publishes artifacts with stable names and checksums — see\n",[78,79,81],"a",{"href":80},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci\u002F","building cross-platform release binaries in CI"," —\nplus a GitHub account able to host a second, small repository.",[23,84,86],{"id":85},"what-a-tap-actually-is","What a tap actually is",[88,89],"inline-diagram",{"name":90},"tap-repo-layout",[10,92,93,94,97],{},"A tap is a git repository named ",[14,95,96],{},"homebrew-\u003Csomething>",", containing Ruby formula files. Users add\nit implicitly by installing a fully qualified name:",[99,100,105],"pre",{"className":101,"code":102,"language":103,"meta":104,"style":104},"language-bash shiki shiki-themes github-light github-dark","brew install yourorg\u002Ftap\u002Fmytool\n","bash","",[14,106,107],{"__ignoreMap":104},[108,109,112,116,120],"span",{"class":110,"line":111},"line",1,[108,113,115],{"class":114},"sScJk","brew",[108,117,119],{"class":118},"sZZnC"," install",[108,121,122],{"class":118}," yourorg\u002Ftap\u002Fmytool\n",[10,124,125,126,129,130,133],{},"That resolves to the repository ",[14,127,128],{},"github.com\u002Fyourorg\u002Fhomebrew-tap"," and the formula ",[14,131,132],{},"mytool.rb","\ninside it. Nothing needs to be submitted anywhere or approved by anyone — which is why a tap is a\nreasonable channel for a tool with a few hundred users, long before it would qualify for\nhomebrew-core.",[23,135,137],{"id":136},"a-formula-for-a-binary-release","A formula for a binary release",[10,139,140],{},"The simplest formula downloads your release archive and installs the executable:",[99,142,146],{"className":143,"code":144,"language":145,"meta":104,"style":104},"language-ruby shiki shiki-themes github-light github-dark","class Mytool \u003C Formula\n  desc \"Sync directories to object storage, quickly\"\n  homepage \"https:\u002F\u002Fexample.com\u002Fmytool\"\n  version \"1.4.0\"\n  license \"MIT\"\n\n  on_macos do\n    on_arm do\n      url \"https:\u002F\u002Fgithub.com\u002Fyourorg\u002Fmytool\u002Freleases\u002Fdownload\u002Fv1.4.0\u002Fmytool-macos-arm64.tar.gz\"\n      sha256 \"3f8a...c1\"\n    end\n    on_intel do\n      url \"https:\u002F\u002Fgithub.com\u002Fyourorg\u002Fmytool\u002Freleases\u002Fdownload\u002Fv1.4.0\u002Fmytool-macos-x86_64.tar.gz\"\n      sha256 \"9b21...4e\"\n    end\n  end\n\n  on_linux do\n    url \"https:\u002F\u002Fgithub.com\u002Fyourorg\u002Fmytool\u002Freleases\u002Fdownload\u002Fv1.4.0\u002Fmytool-linux-x86_64.tar.gz\"\n    sha256 \"77de...02\"\n  end\n\n  def install\n    bin.install \"mytool\"\n  end\n\n  test do\n    assert_match version.to_s, shell_output(\"#{bin}\u002Fmytool --version\")\n  end\nend\n","ruby",[14,147,148,153,159,165,171,177,184,190,196,202,208,214,220,226,232,237,243,248,254,260,266,271,276,282,288,293,298,304,310,315],{"__ignoreMap":104},[108,149,150],{"class":110,"line":111},[108,151,152],{},"class Mytool \u003C Formula\n",[108,154,156],{"class":110,"line":155},2,[108,157,158],{},"  desc \"Sync directories to object storage, quickly\"\n",[108,160,162],{"class":110,"line":161},3,[108,163,164],{},"  homepage \"https:\u002F\u002Fexample.com\u002Fmytool\"\n",[108,166,168],{"class":110,"line":167},4,[108,169,170],{},"  version \"1.4.0\"\n",[108,172,174],{"class":110,"line":173},5,[108,175,176],{},"  license \"MIT\"\n",[108,178,180],{"class":110,"line":179},6,[108,181,183],{"emptyLinePlaceholder":182},true,"\n",[108,185,187],{"class":110,"line":186},7,[108,188,189],{},"  on_macos do\n",[108,191,193],{"class":110,"line":192},8,[108,194,195],{},"    on_arm do\n",[108,197,199],{"class":110,"line":198},9,[108,200,201],{},"      url \"https:\u002F\u002Fgithub.com\u002Fyourorg\u002Fmytool\u002Freleases\u002Fdownload\u002Fv1.4.0\u002Fmytool-macos-arm64.tar.gz\"\n",[108,203,205],{"class":110,"line":204},10,[108,206,207],{},"      sha256 \"3f8a...c1\"\n",[108,209,211],{"class":110,"line":210},11,[108,212,213],{},"    end\n",[108,215,217],{"class":110,"line":216},12,[108,218,219],{},"    on_intel do\n",[108,221,223],{"class":110,"line":222},13,[108,224,225],{},"      url \"https:\u002F\u002Fgithub.com\u002Fyourorg\u002Fmytool\u002Freleases\u002Fdownload\u002Fv1.4.0\u002Fmytool-macos-x86_64.tar.gz\"\n",[108,227,229],{"class":110,"line":228},14,[108,230,231],{},"      sha256 \"9b21...4e\"\n",[108,233,235],{"class":110,"line":234},15,[108,236,213],{},[108,238,240],{"class":110,"line":239},16,[108,241,242],{},"  end\n",[108,244,246],{"class":110,"line":245},17,[108,247,183],{"emptyLinePlaceholder":182},[108,249,251],{"class":110,"line":250},18,[108,252,253],{},"  on_linux do\n",[108,255,257],{"class":110,"line":256},19,[108,258,259],{},"    url \"https:\u002F\u002Fgithub.com\u002Fyourorg\u002Fmytool\u002Freleases\u002Fdownload\u002Fv1.4.0\u002Fmytool-linux-x86_64.tar.gz\"\n",[108,261,263],{"class":110,"line":262},20,[108,264,265],{},"    sha256 \"77de...02\"\n",[108,267,269],{"class":110,"line":268},21,[108,270,242],{},[108,272,274],{"class":110,"line":273},22,[108,275,183],{"emptyLinePlaceholder":182},[108,277,279],{"class":110,"line":278},23,[108,280,281],{},"  def install\n",[108,283,285],{"class":110,"line":284},24,[108,286,287],{},"    bin.install \"mytool\"\n",[108,289,291],{"class":110,"line":290},25,[108,292,242],{},[108,294,296],{"class":110,"line":295},26,[108,297,183],{"emptyLinePlaceholder":182},[108,299,301],{"class":110,"line":300},27,[108,302,303],{},"  test do\n",[108,305,307],{"class":110,"line":306},28,[108,308,309],{},"    assert_match version.to_s, shell_output(\"#{bin}\u002Fmytool --version\")\n",[108,311,313],{"class":110,"line":312},29,[108,314,242],{},[108,316,318],{"class":110,"line":317},30,[108,319,320],{},"end\n",[10,322,323,324,327,328,331,332,335],{},"The ",[14,325,326],{},"test do"," block is not optional in spirit: ",[14,329,330],{},"brew test mytool"," runs it, and it is what catches a\nformula that installs something which cannot start. Keep it to ",[14,333,334],{},"--version",", which exercises the\nentry point without needing configuration.",[23,337,339],{"id":338},"a-formula-that-builds-from-pypi","A formula that builds from PyPI",[10,341,342,343,346,347,350],{},"If you would rather not maintain frozen binaries, Homebrew can create a virtual environment from\nyour wheel and its dependencies. ",[14,344,345],{},"brew update-python-resources"," generates the ",[14,348,349],{},"resource"," blocks:",[99,352,354],{"className":143,"code":353,"language":145,"meta":104,"style":104},"class Mytool \u003C Formula\n  include Language::Python::Virtualenv\n\n  desc \"Sync directories to object storage, quickly\"\n  homepage \"https:\u002F\u002Fexample.com\u002Fmytool\"\n  url \"https:\u002F\u002Ffiles.pythonhosted.org\u002Fpackages\u002F...\u002Fmytool-1.4.0.tar.gz\"\n  sha256 \"aa12...9f\"\n  license \"MIT\"\n\n  depends_on \"python@3.12\"\n\n  resource \"typer\" do\n    url \"https:\u002F\u002Ffiles.pythonhosted.org\u002Fpackages\u002F...\u002Ftyper-0.12.5.tar.gz\"\n    sha256 \"...\"\n  end\n\n  def install\n    virtualenv_install_with_resources\n  end\n\n  test do\n    assert_match version.to_s, shell_output(\"#{bin}\u002Fmytool --version\")\n  end\nend\n",[14,355,356,360,365,369,373,377,382,387,391,395,400,404,409,414,419,423,427,431,436,440,444,448,452,456],{"__ignoreMap":104},[108,357,358],{"class":110,"line":111},[108,359,152],{},[108,361,362],{"class":110,"line":155},[108,363,364],{},"  include Language::Python::Virtualenv\n",[108,366,367],{"class":110,"line":161},[108,368,183],{"emptyLinePlaceholder":182},[108,370,371],{"class":110,"line":167},[108,372,158],{},[108,374,375],{"class":110,"line":173},[108,376,164],{},[108,378,379],{"class":110,"line":179},[108,380,381],{},"  url \"https:\u002F\u002Ffiles.pythonhosted.org\u002Fpackages\u002F...\u002Fmytool-1.4.0.tar.gz\"\n",[108,383,384],{"class":110,"line":186},[108,385,386],{},"  sha256 \"aa12...9f\"\n",[108,388,389],{"class":110,"line":192},[108,390,176],{},[108,392,393],{"class":110,"line":198},[108,394,183],{"emptyLinePlaceholder":182},[108,396,397],{"class":110,"line":204},[108,398,399],{},"  depends_on \"python@3.12\"\n",[108,401,402],{"class":110,"line":210},[108,403,183],{"emptyLinePlaceholder":182},[108,405,406],{"class":110,"line":216},[108,407,408],{},"  resource \"typer\" do\n",[108,410,411],{"class":110,"line":222},[108,412,413],{},"    url \"https:\u002F\u002Ffiles.pythonhosted.org\u002Fpackages\u002F...\u002Ftyper-0.12.5.tar.gz\"\n",[108,415,416],{"class":110,"line":228},[108,417,418],{},"    sha256 \"...\"\n",[108,420,421],{"class":110,"line":234},[108,422,242],{},[108,424,425],{"class":110,"line":239},[108,426,183],{"emptyLinePlaceholder":182},[108,428,429],{"class":110,"line":245},[108,430,281],{},[108,432,433],{"class":110,"line":250},[108,434,435],{},"    virtualenv_install_with_resources\n",[108,437,438],{"class":110,"line":256},[108,439,242],{},[108,441,442],{"class":110,"line":262},[108,443,183],{"emptyLinePlaceholder":182},[108,445,446],{"class":110,"line":268},[108,447,303],{},[108,449,450],{"class":110,"line":273},[108,451,309],{},[108,453,454],{"class":110,"line":278},[108,455,242],{},[108,457,458],{"class":110,"line":284},[108,459,320],{},[10,461,462,463,465],{},"This trades one maintenance burden for another: no binaries to build, but a ",[14,464,349],{}," block per\ndependency that must be regenerated whenever your dependency tree moves. For a tool with three\ndependencies it is pleasant; for one with thirty it is not.",[23,467,469],{"id":468},"scoop-on-windows","Scoop, on Windows",[10,471,472],{},"A Scoop bucket is the same idea with a JSON manifest:",[99,474,478],{"className":475,"code":476,"language":477,"meta":104,"style":104},"language-json shiki shiki-themes github-light github-dark","{\n  \"version\": \"1.4.0\",\n  \"description\": \"Sync directories to object storage, quickly\",\n  \"homepage\": \"https:\u002F\u002Fexample.com\u002Fmytool\",\n  \"license\": \"MIT\",\n  \"architecture\": {\n    \"64bit\": {\n      \"url\": \"https:\u002F\u002Fgithub.com\u002Fyourorg\u002Fmytool\u002Freleases\u002Fdownload\u002Fv1.4.0\u002Fmytool-windows-x86_64.zip\",\n      \"hash\": \"5c90...ab\"\n    }\n  },\n  \"bin\": \"mytool.exe\",\n  \"checkver\": { \"github\": \"https:\u002F\u002Fgithub.com\u002Fyourorg\u002Fmytool\" },\n  \"autoupdate\": {\n    \"architecture\": {\n      \"64bit\": {\n        \"url\": \"https:\u002F\u002Fgithub.com\u002Fyourorg\u002Fmytool\u002Freleases\u002Fdownload\u002Fv$version\u002Fmytool-windows-x86_64.zip\"\n      }\n    }\n  }\n}\n","json",[14,479,480,486,501,513,525,537,545,552,564,574,579,584,596,615,622,629,636,646,651,655,660],{"__ignoreMap":104},[108,481,482],{"class":110,"line":111},[108,483,485],{"class":484},"sVt8B","{\n",[108,487,488,492,495,498],{"class":110,"line":155},[108,489,491],{"class":490},"sj4cs","  \"version\"",[108,493,494],{"class":484},": ",[108,496,497],{"class":118},"\"1.4.0\"",[108,499,500],{"class":484},",\n",[108,502,503,506,508,511],{"class":110,"line":161},[108,504,505],{"class":490},"  \"description\"",[108,507,494],{"class":484},[108,509,510],{"class":118},"\"Sync directories to object storage, quickly\"",[108,512,500],{"class":484},[108,514,515,518,520,523],{"class":110,"line":167},[108,516,517],{"class":490},"  \"homepage\"",[108,519,494],{"class":484},[108,521,522],{"class":118},"\"https:\u002F\u002Fexample.com\u002Fmytool\"",[108,524,500],{"class":484},[108,526,527,530,532,535],{"class":110,"line":173},[108,528,529],{"class":490},"  \"license\"",[108,531,494],{"class":484},[108,533,534],{"class":118},"\"MIT\"",[108,536,500],{"class":484},[108,538,539,542],{"class":110,"line":179},[108,540,541],{"class":490},"  \"architecture\"",[108,543,544],{"class":484},": {\n",[108,546,547,550],{"class":110,"line":186},[108,548,549],{"class":490},"    \"64bit\"",[108,551,544],{"class":484},[108,553,554,557,559,562],{"class":110,"line":192},[108,555,556],{"class":490},"      \"url\"",[108,558,494],{"class":484},[108,560,561],{"class":118},"\"https:\u002F\u002Fgithub.com\u002Fyourorg\u002Fmytool\u002Freleases\u002Fdownload\u002Fv1.4.0\u002Fmytool-windows-x86_64.zip\"",[108,563,500],{"class":484},[108,565,566,569,571],{"class":110,"line":198},[108,567,568],{"class":490},"      \"hash\"",[108,570,494],{"class":484},[108,572,573],{"class":118},"\"5c90...ab\"\n",[108,575,576],{"class":110,"line":204},[108,577,578],{"class":484},"    }\n",[108,580,581],{"class":110,"line":210},[108,582,583],{"class":484},"  },\n",[108,585,586,589,591,594],{"class":110,"line":216},[108,587,588],{"class":490},"  \"bin\"",[108,590,494],{"class":484},[108,592,593],{"class":118},"\"mytool.exe\"",[108,595,500],{"class":484},[108,597,598,601,604,607,609,612],{"class":110,"line":222},[108,599,600],{"class":490},"  \"checkver\"",[108,602,603],{"class":484},": { ",[108,605,606],{"class":490},"\"github\"",[108,608,494],{"class":484},[108,610,611],{"class":118},"\"https:\u002F\u002Fgithub.com\u002Fyourorg\u002Fmytool\"",[108,613,614],{"class":484}," },\n",[108,616,617,620],{"class":110,"line":228},[108,618,619],{"class":490},"  \"autoupdate\"",[108,621,544],{"class":484},[108,623,624,627],{"class":110,"line":234},[108,625,626],{"class":490},"    \"architecture\"",[108,628,544],{"class":484},[108,630,631,634],{"class":110,"line":239},[108,632,633],{"class":490},"      \"64bit\"",[108,635,544],{"class":484},[108,637,638,641,643],{"class":110,"line":245},[108,639,640],{"class":490},"        \"url\"",[108,642,494],{"class":484},[108,644,645],{"class":118},"\"https:\u002F\u002Fgithub.com\u002Fyourorg\u002Fmytool\u002Freleases\u002Fdownload\u002Fv$version\u002Fmytool-windows-x86_64.zip\"\n",[108,647,648],{"class":110,"line":250},[108,649,650],{"class":484},"      }\n",[108,652,653],{"class":110,"line":256},[108,654,578],{"class":484},[108,656,657],{"class":110,"line":262},[108,658,659],{"class":484},"  }\n",[108,661,662],{"class":110,"line":268},[108,663,664],{"class":484},"}\n",[10,666,323,667,17,670,673],{},[14,668,669],{},"checkver",[14,671,672],{},"autoupdate"," blocks are worth filling in: Scoop can then discover new releases\nand rewrite the manifest itself, which removes most of the maintenance from that channel.",[23,675,677],{"id":676},"automating-the-bump","Automating the bump",[88,679],{"name":680},"formula-update-sequence",[10,682,683],{},"The failure mode of every tap is lag. A release goes out, the formula does not move, and users\ninstalling through Homebrew quietly get an old version — which is worse than not offering the\nchannel, because it looks current.",[10,685,686],{},"Automate it from the release workflow:",[99,688,692],{"className":689,"code":690,"language":691,"meta":104,"style":104},"language-yaml shiki shiki-themes github-light github-dark","  tap:\n    needs: publish\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n        with:\n          repository: yourorg\u002Fhomebrew-tap\n          token: ${{ secrets.TAP_TOKEN }}\n      - name: bump formula\n        run: |\n          VERSION=\"${GITHUB_REF_NAME#v}\"\n          SHA=$(curl -sSL \"https:\u002F\u002Fgithub.com\u002Fyourorg\u002Fmytool\u002Freleases\u002Fdownload\u002F${GITHUB_REF_NAME}\u002Fchecksums.txt\" \\\n                | grep macos-arm64 | cut -d' ' -f1)\n          sed -i \"s\u002Fversion \\\".*\\\"\u002Fversion \\\"${VERSION}\\\"\u002F\" Formula\u002Fmytool.rb\n          sed -i \"0,\u002Fsha256 \\\".*\\\"\u002Fs\u002F\u002Fsha256 \\\"${SHA}\\\"\u002F\" Formula\u002Fmytool.rb\n      - uses: peter-evans\u002Fcreate-pull-request@v6\n        with:\n          token: ${{ secrets.TAP_TOKEN }}\n          title: \"mytool ${{ github.ref_name }}\"\n","yaml",[14,693,694,703,713,723,730,743,750,760,770,782,793,798,803,808,813,818,829,835,843],{"__ignoreMap":104},[108,695,696,700],{"class":110,"line":111},[108,697,699],{"class":698},"s9eBZ","  tap",[108,701,702],{"class":484},":\n",[108,704,705,708,710],{"class":110,"line":155},[108,706,707],{"class":698},"    needs",[108,709,494],{"class":484},[108,711,712],{"class":118},"publish\n",[108,714,715,718,720],{"class":110,"line":161},[108,716,717],{"class":698},"    runs-on",[108,719,494],{"class":484},[108,721,722],{"class":118},"ubuntu-latest\n",[108,724,725,728],{"class":110,"line":167},[108,726,727],{"class":698},"    steps",[108,729,702],{"class":484},[108,731,732,735,738,740],{"class":110,"line":173},[108,733,734],{"class":484},"      - ",[108,736,737],{"class":698},"uses",[108,739,494],{"class":484},[108,741,742],{"class":118},"actions\u002Fcheckout@v4\n",[108,744,745,748],{"class":110,"line":179},[108,746,747],{"class":698},"        with",[108,749,702],{"class":484},[108,751,752,755,757],{"class":110,"line":186},[108,753,754],{"class":698},"          repository",[108,756,494],{"class":484},[108,758,759],{"class":118},"yourorg\u002Fhomebrew-tap\n",[108,761,762,765,767],{"class":110,"line":192},[108,763,764],{"class":698},"          token",[108,766,494],{"class":484},[108,768,769],{"class":118},"${{ secrets.TAP_TOKEN }}\n",[108,771,772,774,777,779],{"class":110,"line":198},[108,773,734],{"class":484},[108,775,776],{"class":698},"name",[108,778,494],{"class":484},[108,780,781],{"class":118},"bump formula\n",[108,783,784,787,789],{"class":110,"line":204},[108,785,786],{"class":698},"        run",[108,788,494],{"class":484},[108,790,792],{"class":791},"szBVR","|\n",[108,794,795],{"class":110,"line":210},[108,796,797],{"class":118},"          VERSION=\"${GITHUB_REF_NAME#v}\"\n",[108,799,800],{"class":110,"line":216},[108,801,802],{"class":118},"          SHA=$(curl -sSL \"https:\u002F\u002Fgithub.com\u002Fyourorg\u002Fmytool\u002Freleases\u002Fdownload\u002F${GITHUB_REF_NAME}\u002Fchecksums.txt\" \\\n",[108,804,805],{"class":110,"line":222},[108,806,807],{"class":118},"                | grep macos-arm64 | cut -d' ' -f1)\n",[108,809,810],{"class":110,"line":228},[108,811,812],{"class":118},"          sed -i \"s\u002Fversion \\\".*\\\"\u002Fversion \\\"${VERSION}\\\"\u002F\" Formula\u002Fmytool.rb\n",[108,814,815],{"class":110,"line":234},[108,816,817],{"class":118},"          sed -i \"0,\u002Fsha256 \\\".*\\\"\u002Fs\u002F\u002Fsha256 \\\"${SHA}\\\"\u002F\" Formula\u002Fmytool.rb\n",[108,819,820,822,824,826],{"class":110,"line":239},[108,821,734],{"class":484},[108,823,737],{"class":698},[108,825,494],{"class":484},[108,827,828],{"class":118},"peter-evans\u002Fcreate-pull-request@v6\n",[108,830,831,833],{"class":110,"line":245},[108,832,747],{"class":698},[108,834,702],{"class":484},[108,836,837,839,841],{"class":110,"line":250},[108,838,764],{"class":698},[108,840,494],{"class":484},[108,842,769],{"class":118},[108,844,845,848,850],{"class":110,"line":256},[108,846,847],{"class":698},"          title",[108,849,494],{"class":484},[108,851,852],{"class":118},"\"mytool ${{ github.ref_name }}\"\n",[10,854,855,856,17,859,862],{},"Opening a pull request rather than pushing directly is deliberate: the tap's own CI can run\n",[14,857,858],{},"brew install --build-from-source",[14,860,861],{},"brew test"," against the change before it reaches users, and\na human sees the diff. For a tool with a fast release cadence, auto-merging once those checks pass\nis a reasonable next step.",[23,864,866],{"id":865},"ux-considerations","UX considerations",[10,868,869,872,873,876,877,880],{},[35,870,871],{},"Install instructions should name the tap explicitly."," ",[14,874,875],{},"brew install yourorg\u002Ftap\u002Fmytool"," works\nwithout a separate ",[14,878,879],{},"brew tap"," step and is one line for the reader to copy.",[10,882,883,886,887,890],{},[35,884,885],{},"Keep the description short and factual."," It appears in ",[14,888,889],{},"brew search"," results and in Scoop\nlistings, where a sentence beginning \"A blazing-fast…\" is noise. Say what the tool does.",[88,892],{"name":893},"channel-maintenance-cost",[10,895,896,899],{},[35,897,898],{},"Do not offer a channel you will not maintain."," A tap two versions behind generates bug reports\nabout problems you already fixed. If a channel cannot be automated, it is usually better not to\npublish it.",[23,901,903],{"id":902},"testing-the-behaviour","Testing the behaviour",[10,905,906],{},"Both managers have a local test loop, and using it before the first release avoids a public\nmistake:",[99,908,910],{"className":101,"code":909,"language":103,"meta":104,"style":104},"# Homebrew\nbrew install --build-from-source .\u002FFormula\u002Fmytool.rb\nbrew test mytool\nbrew audit --strict --new mytool        # catches most formula style problems\n\n# Scoop\nscoop install .\u002Fmytool.json\nmytool --version\nscoop uninstall mytool\n",[14,911,912,918,930,940,959,963,968,978,986],{"__ignoreMap":104},[108,913,914],{"class":110,"line":111},[108,915,917],{"class":916},"sJ8bj","# Homebrew\n",[108,919,920,922,924,927],{"class":110,"line":155},[108,921,115],{"class":114},[108,923,119],{"class":118},[108,925,926],{"class":490}," --build-from-source",[108,928,929],{"class":118}," .\u002FFormula\u002Fmytool.rb\n",[108,931,932,934,937],{"class":110,"line":161},[108,933,115],{"class":114},[108,935,936],{"class":118}," test",[108,938,939],{"class":118}," mytool\n",[108,941,942,944,947,950,953,956],{"class":110,"line":167},[108,943,115],{"class":114},[108,945,946],{"class":118}," audit",[108,948,949],{"class":490}," --strict",[108,951,952],{"class":490}," --new",[108,954,955],{"class":118}," mytool",[108,957,958],{"class":916},"        # catches most formula style problems\n",[108,960,961],{"class":110,"line":173},[108,962,183],{"emptyLinePlaceholder":182},[108,964,965],{"class":110,"line":179},[108,966,967],{"class":916},"# Scoop\n",[108,969,970,973,975],{"class":110,"line":186},[108,971,972],{"class":114},"scoop",[108,974,119],{"class":118},[108,976,977],{"class":118}," .\u002Fmytool.json\n",[108,979,980,983],{"class":110,"line":192},[108,981,982],{"class":114},"mytool",[108,984,985],{"class":490}," --version\n",[108,987,988,990,993],{"class":110,"line":198},[108,989,972],{"class":114},[108,991,992],{"class":118}," uninstall",[108,994,939],{"class":118},[10,996,997],{},"In the tap repository itself, a small CI job that installs and tests the formula on every pull\nrequest turns the automated bump into something you can merge without thinking about it.",[23,999,1001],{"id":1000},"conclusion","Conclusion",[10,1003,1004,1005,1007,1008,1010],{},"A tap or a bucket is a small repository, one file per tool, and a workflow that rewrites two\nstrings on every release. That last part is what decides whether the channel helps: automated, it\nis a genuine convenience for users who already live in ",[14,1006,115],{}," or ",[14,1009,972],{},"; manual, it becomes a\nstale copy of your tool that people install by mistake.",[23,1012,1014],{"id":1013},"frequently-asked-questions","Frequently asked questions",[1016,1017,1019],"h3",{"id":1018},"should-i-aim-for-homebrew-core-instead-of-my-own-tap","Should I aim for homebrew-core instead of my own tap?",[10,1021,1022],{},"Only once the tool has a substantial user base. homebrew-core has notability requirements, a\nreview process and stricter formula rules, and it removes your control over release timing. A\npersonal tap is instant, needs no approval, and users install it with a single fully qualified\ncommand.",[1016,1024,1026],{"id":1025},"can-the-formula-install-from-pypi-without-a-binary","Can the formula install from PyPI without a binary?",[10,1028,1029,1030,1033,1034,1036,1037,1039],{},"Yes — the ",[14,1031,1032],{},"virtualenv_install_with_resources"," route builds an isolated environment from your sdist\nand pinned resources. It avoids maintaining frozen binaries at the cost of a ",[14,1035,349],{}," block per\ndependency, which ",[14,1038,345],{}," regenerates for you.",[1016,1041,1043],{"id":1042},"what-happens-if-the-checksum-is-wrong","What happens if the checksum is wrong?",[10,1045,1046,1047,1050],{},"The install fails loudly with a mismatch, which is exactly right — that check is the point. It is\nalso the most common bump mistake, usually from hashing the wrong architecture's archive, so pull\nthe values from your published ",[14,1048,1049],{},"checksums.txt"," rather than computing them locally.",[1016,1052,1054],{"id":1053},"do-homebrew-and-scoop-handle-upgrades-automatically","Do Homebrew and Scoop handle upgrades automatically?",[10,1056,1057,1058,1061,1062,1065,1066,1068,1069,1071],{},"They handle the mechanism (",[14,1059,1060],{},"brew upgrade",", ",[14,1063,1064],{},"scoop update","), not the discovery. Homebrew sees a new\nversion when your formula changes; Scoop can discover one itself if you filled in ",[14,1067,669],{}," and\n",[14,1070,672],{},". Either way the user runs the upgrade command — neither manager updates silently.",[1016,1073,1075],{"id":1074},"is-it-worth-publishing-to-apt-or-rpm-as-well","Is it worth publishing to apt or rpm as well?",[10,1077,1078],{},"Only for a tool aimed at servers or base images, and it is substantially more work: distribution\npackaging has its own conventions, review processes and release cadences. For developer tooling,\nHomebrew, Scoop and pipx cover almost everyone.",[1016,1080,1082],{"id":1081},"how-do-users-discover-that-a-tap-exists","How do users discover that a tap exists?",[10,1084,1085],{},"Only from your documentation — taps are not indexed anywhere central. That makes the install\nsection of the README the entire discovery mechanism, which is an argument for keeping it short\nand putting the recommended route first rather than listing every channel with equal weight.",[1016,1087,1089],{"id":1088},"can-one-tap-hold-several-tools","Can one tap hold several tools?",[10,1091,1092,1093,1096],{},"Yes, and it is the normal arrangement for an organisation: one ",[14,1094,1095],{},"homebrew-tap"," repository with a\nformula per tool. Users install each by name, and the bump automation in each tool's release\nworkflow targets the same repository. The same applies to a Scoop bucket with one manifest per\ntool.",[1016,1098,1100],{"id":1099},"does-a-formula-need-to-pin-the-python-version","Does a formula need to pin the Python version?",[10,1102,1103,1104,1107,1108,1111],{},"For a binary formula, no — the interpreter is inside the artifact. For the virtual-environment\nroute, yes: ",[14,1105,1106],{},"depends_on \"python@3.12\""," is what stops a Homebrew Python upgrade breaking the\ninstalled tool, and it is the line to revisit whenever you raise your own ",[14,1109,1110],{},"requires-python",".",[1016,1113,1115],{"id":1114},"how-do-i-handle-a-tool-whose-name-is-already-taken-in-a-bucket","How do I handle a tool whose name is already taken in a bucket?",[10,1117,1118,1119,1122,1123,1126],{},"Namespace the install rather than the binary: users type ",[14,1120,1121],{},"scoop install yourorg\u002Fmytool"," for a\nbucket you control, so the collision only matters if you also want to be in the main bucket. In\nthat case the usual answer is to rename the command itself before it has users, because two tools\nclaiming the same name on ",[14,1124,1125],{},"PATH"," is a problem no packaging can solve.",[23,1128,1130],{"id":1129},"related","Related",[28,1132,1133,1140,1146,1152,1159],{},[31,1134,1135,1136],{},"Up: ",[78,1137,1139],{"href":1138},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002F","Distributing CLIs as standalone binaries",[31,1141,1142,1143],{},"Sideways: ",[78,1144,1145],{"href":80},"Building cross-platform release binaries in CI",[31,1147,1142,1148],{},[78,1149,1151],{"href":1150},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fshipping-a-cli-as-a-zipapp-with-shiv\u002F","Shipping a CLI as a zipapp with shiv",[31,1153,1154,1155],{},"Related: ",[78,1156,1158],{"href":1157},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx\u002F","Installing and distributing CLIs with pipx",[31,1160,1154,1161],{},[78,1162,1164],{"href":1163},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi\u002F","Publishing a Python CLI to PyPI",[1166,1167,1168],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":104,"searchDepth":155,"depth":155,"links":1170},[1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1192],{"id":25,"depth":155,"text":26},{"id":72,"depth":155,"text":73},{"id":85,"depth":155,"text":86},{"id":136,"depth":155,"text":137},{"id":338,"depth":155,"text":339},{"id":468,"depth":155,"text":469},{"id":676,"depth":155,"text":677},{"id":865,"depth":155,"text":866},{"id":902,"depth":155,"text":903},{"id":1000,"depth":155,"text":1001},{"id":1013,"depth":155,"text":1014,"children":1182},[1183,1184,1185,1186,1187,1188,1189,1190,1191],{"id":1018,"depth":161,"text":1019},{"id":1025,"depth":161,"text":1026},{"id":1042,"depth":161,"text":1043},{"id":1053,"depth":161,"text":1054},{"id":1074,"depth":161,"text":1075},{"id":1081,"depth":161,"text":1082},{"id":1088,"depth":161,"text":1089},{"id":1099,"depth":161,"text":1100},{"id":1114,"depth":161,"text":1115},{"id":1129,"depth":155,"text":1130},"2026-08-01","Publish a Python CLI through a Homebrew tap or Scoop bucket - formula and manifest anatomy, automated version bumps, and whether the channel is worth it.","advanced",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis",{"title":5,"description":1194},"project-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis\u002Findex",[1203,972,1204,1205,1206],"homebrew","packaging","distribution","release","MHFT5frEcZDE_1afYZgxP0dQaK0lKKg34oPPqHTWPZ4",[1209,1212,1215,1218,1221,1224,1227,1230,1233,1236,1239,1242,1245,1248,1251,1254,1257,1260,1263,1266,1269,1272,1275,1278,1281,1284,1287,1290,1293,1296,1299,1302,1305,1308,1311,1314,1317,1320,1323,1326,1329,1332,1335,1338,1341,1344,1347,1350,1353,1356,1359,1362,1365,1368,1371,1374,1377,1380,1383,1386,1389,1392,1395,1396,1399,1402,1405,1408,1411,1414,1417,1420,1423,1425,1428,1431,1434,1437,1440,1443,1446,1449,1452],{"path":1210,"title":1211},"\u002Fabout","About Python CLI Toolcraft",{"path":1213,"title":1214},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1216,"title":1217},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1219,"title":1220},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1222,"title":1223},"\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":1225,"title":1226},"\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":1228,"title":1229},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1231,"title":1232},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1234,"title":1235},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1237,"title":1238},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1240,"title":1241},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1243,"title":1244},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1246,"title":1247},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1249,"title":1250},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1252,"title":1253},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1255,"title":1256},"\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":1258,"title":1259},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1261,"title":1262},"\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":1264,"title":1265},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1267,"title":1268},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1270,"title":1271},"\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":1273,"title":1274},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1276,"title":1277},"\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":1279,"title":1280},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1282,"title":1283},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1285,"title":1286},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1288,"title":1289},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1291,"title":1292},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1294,"title":1295},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1297,"title":1298},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1300,"title":1301},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1303,"title":1304},"\u002F","Python CLI Toolcraft",{"path":1306,"title":1307},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1309,"title":1310},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1312,"title":1313},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1315,"title":1316},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1318,"title":1319},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1321,"title":1322},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1324,"title":1325},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1327,"title":1328},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1330,"title":1331},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1333,"title":1334},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1336,"title":1337},"\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":1339,"title":1340},"\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":1342,"title":1343},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1345,"title":1346},"\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":1348,"title":1349},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1351,"title":1352},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1354,"title":1355},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1357,"title":1358},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1360,"title":1361},"\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":1363,"title":1364},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1366,"title":1367},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1369,"title":1370},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1372,"title":1373},"\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":1375,"title":1376},"\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":1378,"title":1379},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1381,"title":1382},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1384,"title":1385},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1387,"title":1388},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1390,"title":1391},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1393,"title":1394},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1199,"title":5},{"path":1397,"title":1398},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1400,"title":1401},"\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":1403,"title":1404},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1406,"title":1407},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1409,"title":1410},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1412,"title":1413},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1415,"title":1416},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1418,"title":1419},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1421,"title":1422},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1424,"title":1164},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi",{"path":1426,"title":1427},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1429,"title":1430},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1432,"title":1433},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1435,"title":1436},"\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":1438,"title":1439},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1441,"title":1442},"\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":1444,"title":1445},"\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":1447,"title":1448},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1450,"title":1451},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1453,"title":1454},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690034]