[{"data":1,"prerenderedAt":3853},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fshowing-non-blocking-update-notices\u002F":3,"content-directory":3009},{"id":4,"title":5,"body":6,"date":2993,"description":2994,"difficulty":2995,"draft":2996,"extension":2997,"meta":2998,"navigation":123,"path":2999,"seo":3000,"stem":3001,"tags":3002,"updated":2993,"__hash__":3008},"content\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fshowing-non-blocking-update-notices\u002Findex.md","Showing Non-Blocking Update Notices in a Python CLI",{"type":7,"value":8,"toc":2975},"minimark",[9,29,34,59,63,71,75,78,82,1598,1614,1632,1637,1640,1974,1991,1994,1998,2053,2056,2060,2067,2874,2877,2881,2888,2892,2896,2899,2903,2909,2916,2923,2927,2937,2941,2971],[10,11,12,13,17,18,23,24,28],"p",{},"Knowing that a newer version exists is half the job. The other half is telling the user without making the tool slower, noisier or less scriptable — and that half is where most update notifiers go wrong. A synchronous check adds a network round trip to every command; a notice printed before the output scrolls away; a notice on stdout corrupts JSON; a notice on every run gets the tool aliased with the check disabled. This guide builds the display layer properly: a daily cache, a background thread with a hard time budget, a single notice on stderr after the command finishes, and automatic silence wherever a notice does not belong. It uses the ",[14,15,16],"code",{},"check_pypi()"," function from ",[19,20,22],"a",{"href":21},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fchecking-pypi-for-a-newer-version\u002F","checking PyPI for a newer version"," and is part of the ",[19,25,27],{"href":26},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002F","update checks topic",".",[30,31,33],"h2",{"id":32},"prerequisites","Prerequisites",[35,36,37,45,52],"ul",{},[38,39,40,41,44],"li",{},"Python 3.10+, Typer or Click, and ",[14,42,43],{},"platformdirs"," for the state directory.",[38,46,47,48,51],{},"A function that returns update information or ",[14,49,50],{},"None"," and never raises.",[38,53,54,55,28],{},"Familiarity with ",[19,56,58],{"href":57},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells\u002F","detecting CI environments and non-interactive shells",[30,60,62],{"id":61},"the-timing-model","The timing model",[10,64,65,66,70],{},"The whole design follows from one constraint: the user's command must take the same time with the update check enabled as without it. That rules out checking before the command runs, and it means the result of a check may not be available until a ",[67,68,69],"em",{},"later"," run.",[72,73],"inline-diagram",{"name":74},"upd-notice-timeline",[10,76,77],{},"On most runs the cache says a check is not due, and the notifier does nothing but read a small file. When a check is due, a daemon thread starts as the command starts, fetches and writes the cache in parallel with the real work, and the notifier waits for it at the end for at most a fraction of a second. If the thread has finished, its result can be shown immediately; if not, the process exits anyway — the thread is a daemon — and the next run shows the cached result.",[30,79,81],{"id":80},"the-recipe","The recipe",[83,84,89],"pre",{"className":85,"code":86,"language":87,"meta":88,"style":88},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fnotifier.py\nfrom __future__ import annotations\n\nimport json\nimport os\nimport sys\nimport tempfile\nimport threading\nfrom collections.abc import Callable\nfrom datetime import datetime, timedelta, timezone\nfrom pathlib import Path\n\nDISABLE_VARS = (\"MYTOOL_NO_UPDATE_CHECK\", \"NO_UPDATE_NOTIFIER\")\nCI_VARS = (\"CI\", \"GITHUB_ACTIONS\", \"GITLAB_CI\", \"BUILDKITE\", \"TF_BUILD\")\n\n\ndef _now() -> datetime:\n    return datetime.now(timezone.utc)\n\n\nclass UpdateNotifier:\n    def __init__(self, current: str, cache_file: Path,\n                 fetch_latest: Callable[[str], str | None], *,\n                 interval: timedelta = timedelta(hours=24),\n                 upgrade_hint: str = \"pipx upgrade mytool\",\n                 clock: Callable[[], datetime] = _now) -> None:\n        self.current = current\n        self.cache_file = cache_file\n        self.fetch_latest = fetch_latest\n        self.interval = interval\n        self.upgrade_hint = upgrade_hint\n        self.clock = clock\n        self._thread: threading.Thread | None = None\n\n    # -- policy ---------------------------------------------------------\n    @staticmethod\n    def enabled(*, machine_output: bool = False) -> bool:\n        if machine_output or any(os.environ.get(v) for v in DISABLE_VARS):\n            return False\n        if any(os.environ.get(v) for v in CI_VARS):\n            return False\n        return sys.stderr.isatty()\n\n    # -- cache ----------------------------------------------------------\n    def _load(self) -> dict:\n        try:\n            data = json.loads(self.cache_file.read_text(encoding=\"utf-8\"))\n        except (OSError, ValueError):\n            return {}\n        return data if isinstance(data, dict) and data.get(\"schema\") == 1 else {}\n\n    def _save(self, data: dict) -> None:\n        try:\n            self.cache_file.parent.mkdir(parents=True, exist_ok=True)\n            fd, tmp = tempfile.mkstemp(dir=self.cache_file.parent, suffix=\".tmp\")\n            with os.fdopen(fd, \"w\", encoding=\"utf-8\") as fh:\n                json.dump({\"schema\": 1, **data}, fh)\n            os.replace(tmp, self.cache_file)\n        except OSError:\n            pass                                   # a read-only home must not break the tool\n\n    def _stamp(self, data: dict, key: str) -> datetime | None:\n        try:\n            return datetime.fromisoformat(data[key])\n        except (KeyError, TypeError, ValueError):\n            return None\n\n    # -- lifecycle ------------------------------------------------------\n    def start(self) -> None:\n        data = self._load()\n        last = self._stamp(data, \"checked_at\")\n        due = data.get(\"current\") != self.current or last is None \\\n            or self.clock() - last >= self.interval\n        if due:\n            self._thread = threading.Thread(target=self._check, name=\"update-check\", daemon=True)\n            self._thread.start()\n\n    def _check(self) -> None:\n        latest = self.fetch_latest(self.current)\n        data = self._load()\n        data.update(current=self.current, latest=latest,\n                    checked_at=self.clock().isoformat())\n        self._save(data)\n\n    def finish(self, wait: float = 0.25) -> str | None:\n        \"\"\"Wait briefly for a running check, then print a notice if one is due.\"\"\"\n        if self._thread is not None:\n            self._thread.join(wait)\n        data = self._load()\n        latest = data.get(\"latest\")\n        if not latest or data.get(\"current\") != self.current:\n            return None\n        notified = self._stamp(data, \"notified_at\")\n        if notified is not None and self.clock() - notified \u003C self.interval:\n            return None\n        message = (f\"A new version of mytool is available: {self.current} → {latest}\\n\"\n                   f\"Upgrade with: {self.upgrade_hint}\")\n        print(f\"\\n{message}\", file=sys.stderr)\n        data[\"notified_at\"] = self.clock().isoformat()\n        self._save(data)\n        return message\n","python","",[14,90,91,100,118,125,134,142,150,158,166,179,192,205,210,235,270,275,280,293,302,307,312,324,342,369,393,408,423,437,450,463,476,489,502,520,525,531,540,571,604,613,633,640,649,654,660,676,684,712,730,738,782,787,806,813,841,872,900,922,933,943,952,957,983,990,998,1019,1026,1031,1037,1051,1065,1083,1118,1142,1150,1192,1200,1205,1219,1237,1248,1272,1285,1293,1298,1327,1333,1351,1359,1370,1384,1409,1416,1433,1466,1473,1512,1533,1565,1583,1590],{"__ignoreMap":88},[92,93,96],"span",{"class":94,"line":95},"line",1,[92,97,99],{"class":98},"sJ8bj","# src\u002Fmytool\u002Fnotifier.py\n",[92,101,103,107,111,114],{"class":94,"line":102},2,[92,104,106],{"class":105},"szBVR","from",[92,108,110],{"class":109},"sj4cs"," __future__",[92,112,113],{"class":105}," import",[92,115,117],{"class":116},"sVt8B"," annotations\n",[92,119,121],{"class":94,"line":120},3,[92,122,124],{"emptyLinePlaceholder":123},true,"\n",[92,126,128,131],{"class":94,"line":127},4,[92,129,130],{"class":105},"import",[92,132,133],{"class":116}," json\n",[92,135,137,139],{"class":94,"line":136},5,[92,138,130],{"class":105},[92,140,141],{"class":116}," os\n",[92,143,145,147],{"class":94,"line":144},6,[92,146,130],{"class":105},[92,148,149],{"class":116}," sys\n",[92,151,153,155],{"class":94,"line":152},7,[92,154,130],{"class":105},[92,156,157],{"class":116}," tempfile\n",[92,159,161,163],{"class":94,"line":160},8,[92,162,130],{"class":105},[92,164,165],{"class":116}," threading\n",[92,167,169,171,174,176],{"class":94,"line":168},9,[92,170,106],{"class":105},[92,172,173],{"class":116}," collections.abc ",[92,175,130],{"class":105},[92,177,178],{"class":116}," Callable\n",[92,180,182,184,187,189],{"class":94,"line":181},10,[92,183,106],{"class":105},[92,185,186],{"class":116}," datetime ",[92,188,130],{"class":105},[92,190,191],{"class":116}," datetime, timedelta, timezone\n",[92,193,195,197,200,202],{"class":94,"line":194},11,[92,196,106],{"class":105},[92,198,199],{"class":116}," pathlib ",[92,201,130],{"class":105},[92,203,204],{"class":116}," Path\n",[92,206,208],{"class":94,"line":207},12,[92,209,124],{"emptyLinePlaceholder":123},[92,211,213,216,219,222,226,229,232],{"class":94,"line":212},13,[92,214,215],{"class":109},"DISABLE_VARS",[92,217,218],{"class":105}," =",[92,220,221],{"class":116}," (",[92,223,225],{"class":224},"sZZnC","\"MYTOOL_NO_UPDATE_CHECK\"",[92,227,228],{"class":116},", ",[92,230,231],{"class":224},"\"NO_UPDATE_NOTIFIER\"",[92,233,234],{"class":116},")\n",[92,236,238,241,243,245,248,250,253,255,258,260,263,265,268],{"class":94,"line":237},14,[92,239,240],{"class":109},"CI_VARS",[92,242,218],{"class":105},[92,244,221],{"class":116},[92,246,247],{"class":224},"\"CI\"",[92,249,228],{"class":116},[92,251,252],{"class":224},"\"GITHUB_ACTIONS\"",[92,254,228],{"class":116},[92,256,257],{"class":224},"\"GITLAB_CI\"",[92,259,228],{"class":116},[92,261,262],{"class":224},"\"BUILDKITE\"",[92,264,228],{"class":116},[92,266,267],{"class":224},"\"TF_BUILD\"",[92,269,234],{"class":116},[92,271,273],{"class":94,"line":272},15,[92,274,124],{"emptyLinePlaceholder":123},[92,276,278],{"class":94,"line":277},16,[92,279,124],{"emptyLinePlaceholder":123},[92,281,283,286,290],{"class":94,"line":282},17,[92,284,285],{"class":105},"def",[92,287,289],{"class":288},"sScJk"," _now",[92,291,292],{"class":116},"() -> datetime:\n",[92,294,296,299],{"class":94,"line":295},18,[92,297,298],{"class":105},"    return",[92,300,301],{"class":116}," datetime.now(timezone.utc)\n",[92,303,305],{"class":94,"line":304},19,[92,306,124],{"emptyLinePlaceholder":123},[92,308,310],{"class":94,"line":309},20,[92,311,124],{"emptyLinePlaceholder":123},[92,313,315,318,321],{"class":94,"line":314},21,[92,316,317],{"class":105},"class",[92,319,320],{"class":288}," UpdateNotifier",[92,322,323],{"class":116},":\n",[92,325,327,330,333,336,339],{"class":94,"line":326},22,[92,328,329],{"class":105},"    def",[92,331,332],{"class":109}," __init__",[92,334,335],{"class":116},"(self, current: ",[92,337,338],{"class":109},"str",[92,340,341],{"class":116},", cache_file: Path,\n",[92,343,345,348,350,353,355,358,361,363,366],{"class":94,"line":344},23,[92,346,347],{"class":116},"                 fetch_latest: Callable[[",[92,349,338],{"class":109},[92,351,352],{"class":116},"], ",[92,354,338],{"class":109},[92,356,357],{"class":105}," |",[92,359,360],{"class":109}," None",[92,362,352],{"class":116},[92,364,365],{"class":105},"*",[92,367,368],{"class":116},",\n",[92,370,372,375,378,381,385,387,390],{"class":94,"line":371},24,[92,373,374],{"class":116},"                 interval: timedelta ",[92,376,377],{"class":105},"=",[92,379,380],{"class":116}," timedelta(",[92,382,384],{"class":383},"s4XuR","hours",[92,386,377],{"class":105},[92,388,389],{"class":109},"24",[92,391,392],{"class":116},"),\n",[92,394,396,399,401,403,406],{"class":94,"line":395},25,[92,397,398],{"class":116},"                 upgrade_hint: ",[92,400,338],{"class":109},[92,402,218],{"class":105},[92,404,405],{"class":224}," \"pipx upgrade mytool\"",[92,407,368],{"class":116},[92,409,411,414,416,419,421],{"class":94,"line":410},26,[92,412,413],{"class":116},"                 clock: Callable[[], datetime] ",[92,415,377],{"class":105},[92,417,418],{"class":116}," _now) -> ",[92,420,50],{"class":109},[92,422,323],{"class":116},[92,424,426,429,432,434],{"class":94,"line":425},27,[92,427,428],{"class":109},"        self",[92,430,431],{"class":116},".current ",[92,433,377],{"class":105},[92,435,436],{"class":116}," current\n",[92,438,440,442,445,447],{"class":94,"line":439},28,[92,441,428],{"class":109},[92,443,444],{"class":116},".cache_file ",[92,446,377],{"class":105},[92,448,449],{"class":116}," cache_file\n",[92,451,453,455,458,460],{"class":94,"line":452},29,[92,454,428],{"class":109},[92,456,457],{"class":116},".fetch_latest ",[92,459,377],{"class":105},[92,461,462],{"class":116}," fetch_latest\n",[92,464,466,468,471,473],{"class":94,"line":465},30,[92,467,428],{"class":109},[92,469,470],{"class":116},".interval ",[92,472,377],{"class":105},[92,474,475],{"class":116}," interval\n",[92,477,479,481,484,486],{"class":94,"line":478},31,[92,480,428],{"class":109},[92,482,483],{"class":116},".upgrade_hint ",[92,485,377],{"class":105},[92,487,488],{"class":116}," upgrade_hint\n",[92,490,492,494,497,499],{"class":94,"line":491},32,[92,493,428],{"class":109},[92,495,496],{"class":116},".clock ",[92,498,377],{"class":105},[92,500,501],{"class":116}," clock\n",[92,503,505,507,510,513,515,517],{"class":94,"line":504},33,[92,506,428],{"class":109},[92,508,509],{"class":116},"._thread: threading.Thread ",[92,511,512],{"class":105},"|",[92,514,360],{"class":109},[92,516,218],{"class":105},[92,518,519],{"class":109}," None\n",[92,521,523],{"class":94,"line":522},34,[92,524,124],{"emptyLinePlaceholder":123},[92,526,528],{"class":94,"line":527},35,[92,529,530],{"class":98},"    # -- policy ---------------------------------------------------------\n",[92,532,534,537],{"class":94,"line":533},36,[92,535,536],{"class":288},"    @",[92,538,539],{"class":109},"staticmethod\n",[92,541,543,545,548,551,553,556,559,561,564,567,569],{"class":94,"line":542},37,[92,544,329],{"class":105},[92,546,547],{"class":288}," enabled",[92,549,550],{"class":116},"(",[92,552,365],{"class":105},[92,554,555],{"class":116},", machine_output: ",[92,557,558],{"class":109},"bool",[92,560,218],{"class":105},[92,562,563],{"class":109}," False",[92,565,566],{"class":116},") -> ",[92,568,558],{"class":109},[92,570,323],{"class":116},[92,572,574,577,580,583,586,589,592,595,598,601],{"class":94,"line":573},38,[92,575,576],{"class":105},"        if",[92,578,579],{"class":116}," machine_output ",[92,581,582],{"class":105},"or",[92,584,585],{"class":109}," any",[92,587,588],{"class":116},"(os.environ.get(v) ",[92,590,591],{"class":105},"for",[92,593,594],{"class":116}," v ",[92,596,597],{"class":105},"in",[92,599,600],{"class":109}," DISABLE_VARS",[92,602,603],{"class":116},"):\n",[92,605,607,610],{"class":94,"line":606},39,[92,608,609],{"class":105},"            return",[92,611,612],{"class":109}," False\n",[92,614,616,618,620,622,624,626,628,631],{"class":94,"line":615},40,[92,617,576],{"class":105},[92,619,585],{"class":109},[92,621,588],{"class":116},[92,623,591],{"class":105},[92,625,594],{"class":116},[92,627,597],{"class":105},[92,629,630],{"class":109}," CI_VARS",[92,632,603],{"class":116},[92,634,636,638],{"class":94,"line":635},41,[92,637,609],{"class":105},[92,639,612],{"class":109},[92,641,643,646],{"class":94,"line":642},42,[92,644,645],{"class":105},"        return",[92,647,648],{"class":116}," sys.stderr.isatty()\n",[92,650,652],{"class":94,"line":651},43,[92,653,124],{"emptyLinePlaceholder":123},[92,655,657],{"class":94,"line":656},44,[92,658,659],{"class":98},"    # -- cache ----------------------------------------------------------\n",[92,661,663,665,668,671,674],{"class":94,"line":662},45,[92,664,329],{"class":105},[92,666,667],{"class":288}," _load",[92,669,670],{"class":116},"(self) -> ",[92,672,673],{"class":109},"dict",[92,675,323],{"class":116},[92,677,679,682],{"class":94,"line":678},46,[92,680,681],{"class":105},"        try",[92,683,323],{"class":116},[92,685,687,690,692,695,698,701,704,706,709],{"class":94,"line":686},47,[92,688,689],{"class":116},"            data ",[92,691,377],{"class":105},[92,693,694],{"class":116}," json.loads(",[92,696,697],{"class":109},"self",[92,699,700],{"class":116},".cache_file.read_text(",[92,702,703],{"class":383},"encoding",[92,705,377],{"class":105},[92,707,708],{"class":224},"\"utf-8\"",[92,710,711],{"class":116},"))\n",[92,713,715,718,720,723,725,728],{"class":94,"line":714},48,[92,716,717],{"class":105},"        except",[92,719,221],{"class":116},[92,721,722],{"class":109},"OSError",[92,724,228],{"class":116},[92,726,727],{"class":109},"ValueError",[92,729,603],{"class":116},[92,731,733,735],{"class":94,"line":732},49,[92,734,609],{"class":105},[92,736,737],{"class":116}," {}\n",[92,739,741,743,746,749,752,755,757,760,763,766,769,771,774,777,780],{"class":94,"line":740},50,[92,742,645],{"class":105},[92,744,745],{"class":116}," data ",[92,747,748],{"class":105},"if",[92,750,751],{"class":109}," isinstance",[92,753,754],{"class":116},"(data, ",[92,756,673],{"class":109},[92,758,759],{"class":116},") ",[92,761,762],{"class":105},"and",[92,764,765],{"class":116}," data.get(",[92,767,768],{"class":224},"\"schema\"",[92,770,759],{"class":116},[92,772,773],{"class":105},"==",[92,775,776],{"class":109}," 1",[92,778,779],{"class":105}," else",[92,781,737],{"class":116},[92,783,785],{"class":94,"line":784},51,[92,786,124],{"emptyLinePlaceholder":123},[92,788,790,792,795,798,800,802,804],{"class":94,"line":789},52,[92,791,329],{"class":105},[92,793,794],{"class":288}," _save",[92,796,797],{"class":116},"(self, data: ",[92,799,673],{"class":109},[92,801,566],{"class":116},[92,803,50],{"class":109},[92,805,323],{"class":116},[92,807,809,811],{"class":94,"line":808},53,[92,810,681],{"class":105},[92,812,323],{"class":116},[92,814,816,819,822,825,827,830,832,835,837,839],{"class":94,"line":815},54,[92,817,818],{"class":109},"            self",[92,820,821],{"class":116},".cache_file.parent.mkdir(",[92,823,824],{"class":383},"parents",[92,826,377],{"class":105},[92,828,829],{"class":109},"True",[92,831,228],{"class":116},[92,833,834],{"class":383},"exist_ok",[92,836,377],{"class":105},[92,838,829],{"class":109},[92,840,234],{"class":116},[92,842,844,847,849,852,855,857,859,862,865,867,870],{"class":94,"line":843},55,[92,845,846],{"class":116},"            fd, tmp ",[92,848,377],{"class":105},[92,850,851],{"class":116}," tempfile.mkstemp(",[92,853,854],{"class":383},"dir",[92,856,377],{"class":105},[92,858,697],{"class":109},[92,860,861],{"class":116},".cache_file.parent, ",[92,863,864],{"class":383},"suffix",[92,866,377],{"class":105},[92,868,869],{"class":224},"\".tmp\"",[92,871,234],{"class":116},[92,873,875,878,881,884,886,888,890,892,894,897],{"class":94,"line":874},56,[92,876,877],{"class":105},"            with",[92,879,880],{"class":116}," os.fdopen(fd, ",[92,882,883],{"class":224},"\"w\"",[92,885,228],{"class":116},[92,887,703],{"class":383},[92,889,377],{"class":105},[92,891,708],{"class":224},[92,893,759],{"class":116},[92,895,896],{"class":105},"as",[92,898,899],{"class":116}," fh:\n",[92,901,903,906,908,911,914,916,919],{"class":94,"line":902},57,[92,904,905],{"class":116},"                json.dump({",[92,907,768],{"class":224},[92,909,910],{"class":116},": ",[92,912,913],{"class":109},"1",[92,915,228],{"class":116},[92,917,918],{"class":105},"**",[92,920,921],{"class":116},"data}, fh)\n",[92,923,925,928,930],{"class":94,"line":924},58,[92,926,927],{"class":116},"            os.replace(tmp, ",[92,929,697],{"class":109},[92,931,932],{"class":116},".cache_file)\n",[92,934,936,938,941],{"class":94,"line":935},59,[92,937,717],{"class":105},[92,939,940],{"class":109}," OSError",[92,942,323],{"class":116},[92,944,946,949],{"class":94,"line":945},60,[92,947,948],{"class":105},"            pass",[92,950,951],{"class":98},"                                   # a read-only home must not break the tool\n",[92,953,955],{"class":94,"line":954},61,[92,956,124],{"emptyLinePlaceholder":123},[92,958,960,962,965,967,969,972,974,977,979,981],{"class":94,"line":959},62,[92,961,329],{"class":105},[92,963,964],{"class":288}," _stamp",[92,966,797],{"class":116},[92,968,673],{"class":109},[92,970,971],{"class":116},", key: ",[92,973,338],{"class":109},[92,975,976],{"class":116},") -> datetime ",[92,978,512],{"class":105},[92,980,360],{"class":109},[92,982,323],{"class":116},[92,984,986,988],{"class":94,"line":985},63,[92,987,681],{"class":105},[92,989,323],{"class":116},[92,991,993,995],{"class":94,"line":992},64,[92,994,609],{"class":105},[92,996,997],{"class":116}," datetime.fromisoformat(data[key])\n",[92,999,1001,1003,1005,1008,1010,1013,1015,1017],{"class":94,"line":1000},65,[92,1002,717],{"class":105},[92,1004,221],{"class":116},[92,1006,1007],{"class":109},"KeyError",[92,1009,228],{"class":116},[92,1011,1012],{"class":109},"TypeError",[92,1014,228],{"class":116},[92,1016,727],{"class":109},[92,1018,603],{"class":116},[92,1020,1022,1024],{"class":94,"line":1021},66,[92,1023,609],{"class":105},[92,1025,519],{"class":109},[92,1027,1029],{"class":94,"line":1028},67,[92,1030,124],{"emptyLinePlaceholder":123},[92,1032,1034],{"class":94,"line":1033},68,[92,1035,1036],{"class":98},"    # -- lifecycle ------------------------------------------------------\n",[92,1038,1040,1042,1045,1047,1049],{"class":94,"line":1039},69,[92,1041,329],{"class":105},[92,1043,1044],{"class":288}," start",[92,1046,670],{"class":116},[92,1048,50],{"class":109},[92,1050,323],{"class":116},[92,1052,1054,1057,1059,1062],{"class":94,"line":1053},70,[92,1055,1056],{"class":116},"        data ",[92,1058,377],{"class":105},[92,1060,1061],{"class":109}," self",[92,1063,1064],{"class":116},"._load()\n",[92,1066,1068,1071,1073,1075,1078,1081],{"class":94,"line":1067},71,[92,1069,1070],{"class":116},"        last ",[92,1072,377],{"class":105},[92,1074,1061],{"class":109},[92,1076,1077],{"class":116},"._stamp(data, ",[92,1079,1080],{"class":224},"\"checked_at\"",[92,1082,234],{"class":116},[92,1084,1086,1089,1091,1093,1096,1098,1101,1103,1105,1107,1110,1113,1115],{"class":94,"line":1085},72,[92,1087,1088],{"class":116},"        due ",[92,1090,377],{"class":105},[92,1092,765],{"class":116},[92,1094,1095],{"class":224},"\"current\"",[92,1097,759],{"class":116},[92,1099,1100],{"class":105},"!=",[92,1102,1061],{"class":109},[92,1104,431],{"class":116},[92,1106,582],{"class":105},[92,1108,1109],{"class":116}," last ",[92,1111,1112],{"class":105},"is",[92,1114,360],{"class":109},[92,1116,1117],{"class":116}," \\\n",[92,1119,1121,1124,1126,1129,1132,1134,1137,1139],{"class":94,"line":1120},73,[92,1122,1123],{"class":105},"            or",[92,1125,1061],{"class":109},[92,1127,1128],{"class":116},".clock() ",[92,1130,1131],{"class":105},"-",[92,1133,1109],{"class":116},[92,1135,1136],{"class":105},">=",[92,1138,1061],{"class":109},[92,1140,1141],{"class":116},".interval\n",[92,1143,1145,1147],{"class":94,"line":1144},74,[92,1146,576],{"class":105},[92,1148,1149],{"class":116}," due:\n",[92,1151,1153,1155,1158,1160,1163,1166,1168,1170,1173,1176,1178,1181,1183,1186,1188,1190],{"class":94,"line":1152},75,[92,1154,818],{"class":109},[92,1156,1157],{"class":116},"._thread ",[92,1159,377],{"class":105},[92,1161,1162],{"class":116}," threading.Thread(",[92,1164,1165],{"class":383},"target",[92,1167,377],{"class":105},[92,1169,697],{"class":109},[92,1171,1172],{"class":116},"._check, ",[92,1174,1175],{"class":383},"name",[92,1177,377],{"class":105},[92,1179,1180],{"class":224},"\"update-check\"",[92,1182,228],{"class":116},[92,1184,1185],{"class":383},"daemon",[92,1187,377],{"class":105},[92,1189,829],{"class":109},[92,1191,234],{"class":116},[92,1193,1195,1197],{"class":94,"line":1194},76,[92,1196,818],{"class":109},[92,1198,1199],{"class":116},"._thread.start()\n",[92,1201,1203],{"class":94,"line":1202},77,[92,1204,124],{"emptyLinePlaceholder":123},[92,1206,1208,1210,1213,1215,1217],{"class":94,"line":1207},78,[92,1209,329],{"class":105},[92,1211,1212],{"class":288}," _check",[92,1214,670],{"class":116},[92,1216,50],{"class":109},[92,1218,323],{"class":116},[92,1220,1222,1225,1227,1229,1232,1234],{"class":94,"line":1221},79,[92,1223,1224],{"class":116},"        latest ",[92,1226,377],{"class":105},[92,1228,1061],{"class":109},[92,1230,1231],{"class":116},".fetch_latest(",[92,1233,697],{"class":109},[92,1235,1236],{"class":116},".current)\n",[92,1238,1240,1242,1244,1246],{"class":94,"line":1239},80,[92,1241,1056],{"class":116},[92,1243,377],{"class":105},[92,1245,1061],{"class":109},[92,1247,1064],{"class":116},[92,1249,1251,1254,1257,1259,1261,1264,1267,1269],{"class":94,"line":1250},81,[92,1252,1253],{"class":116},"        data.update(",[92,1255,1256],{"class":383},"current",[92,1258,377],{"class":105},[92,1260,697],{"class":109},[92,1262,1263],{"class":116},".current, ",[92,1265,1266],{"class":383},"latest",[92,1268,377],{"class":105},[92,1270,1271],{"class":116},"latest,\n",[92,1273,1275,1278,1280,1282],{"class":94,"line":1274},82,[92,1276,1277],{"class":383},"                    checked_at",[92,1279,377],{"class":105},[92,1281,697],{"class":109},[92,1283,1284],{"class":116},".clock().isoformat())\n",[92,1286,1288,1290],{"class":94,"line":1287},83,[92,1289,428],{"class":109},[92,1291,1292],{"class":116},"._save(data)\n",[92,1294,1296],{"class":94,"line":1295},84,[92,1297,124],{"emptyLinePlaceholder":123},[92,1299,1301,1303,1306,1309,1312,1314,1317,1319,1321,1323,1325],{"class":94,"line":1300},85,[92,1302,329],{"class":105},[92,1304,1305],{"class":288}," finish",[92,1307,1308],{"class":116},"(self, wait: ",[92,1310,1311],{"class":109},"float",[92,1313,218],{"class":105},[92,1315,1316],{"class":109}," 0.25",[92,1318,566],{"class":116},[92,1320,338],{"class":109},[92,1322,357],{"class":105},[92,1324,360],{"class":109},[92,1326,323],{"class":116},[92,1328,1330],{"class":94,"line":1329},86,[92,1331,1332],{"class":224},"        \"\"\"Wait briefly for a running check, then print a notice if one is due.\"\"\"\n",[92,1334,1336,1338,1340,1342,1344,1347,1349],{"class":94,"line":1335},87,[92,1337,576],{"class":105},[92,1339,1061],{"class":109},[92,1341,1157],{"class":116},[92,1343,1112],{"class":105},[92,1345,1346],{"class":105}," not",[92,1348,360],{"class":109},[92,1350,323],{"class":116},[92,1352,1354,1356],{"class":94,"line":1353},88,[92,1355,818],{"class":109},[92,1357,1358],{"class":116},"._thread.join(wait)\n",[92,1360,1362,1364,1366,1368],{"class":94,"line":1361},89,[92,1363,1056],{"class":116},[92,1365,377],{"class":105},[92,1367,1061],{"class":109},[92,1369,1064],{"class":116},[92,1371,1373,1375,1377,1379,1382],{"class":94,"line":1372},90,[92,1374,1224],{"class":116},[92,1376,377],{"class":105},[92,1378,765],{"class":116},[92,1380,1381],{"class":224},"\"latest\"",[92,1383,234],{"class":116},[92,1385,1387,1389,1391,1394,1396,1398,1400,1402,1404,1406],{"class":94,"line":1386},91,[92,1388,576],{"class":105},[92,1390,1346],{"class":105},[92,1392,1393],{"class":116}," latest ",[92,1395,582],{"class":105},[92,1397,765],{"class":116},[92,1399,1095],{"class":224},[92,1401,759],{"class":116},[92,1403,1100],{"class":105},[92,1405,1061],{"class":109},[92,1407,1408],{"class":116},".current:\n",[92,1410,1412,1414],{"class":94,"line":1411},92,[92,1413,609],{"class":105},[92,1415,519],{"class":109},[92,1417,1419,1422,1424,1426,1428,1431],{"class":94,"line":1418},93,[92,1420,1421],{"class":116},"        notified ",[92,1423,377],{"class":105},[92,1425,1061],{"class":109},[92,1427,1077],{"class":116},[92,1429,1430],{"class":224},"\"notified_at\"",[92,1432,234],{"class":116},[92,1434,1436,1438,1441,1443,1445,1447,1450,1452,1454,1456,1458,1461,1463],{"class":94,"line":1435},94,[92,1437,576],{"class":105},[92,1439,1440],{"class":116}," notified ",[92,1442,1112],{"class":105},[92,1444,1346],{"class":105},[92,1446,360],{"class":109},[92,1448,1449],{"class":105}," and",[92,1451,1061],{"class":109},[92,1453,1128],{"class":116},[92,1455,1131],{"class":105},[92,1457,1440],{"class":116},[92,1459,1460],{"class":105},"\u003C",[92,1462,1061],{"class":109},[92,1464,1465],{"class":116},".interval:\n",[92,1467,1469,1471],{"class":94,"line":1468},95,[92,1470,609],{"class":105},[92,1472,519],{"class":109},[92,1474,1476,1479,1481,1483,1486,1489,1492,1495,1498,1501,1504,1506,1509],{"class":94,"line":1475},96,[92,1477,1478],{"class":116},"        message ",[92,1480,377],{"class":105},[92,1482,221],{"class":116},[92,1484,1485],{"class":105},"f",[92,1487,1488],{"class":224},"\"A new version of mytool is available: ",[92,1490,1491],{"class":109},"{self",[92,1493,1494],{"class":116},".current",[92,1496,1497],{"class":109},"}",[92,1499,1500],{"class":224}," → ",[92,1502,1503],{"class":109},"{",[92,1505,1266],{"class":116},[92,1507,1508],{"class":109},"}\\n",[92,1510,1511],{"class":224},"\"\n",[92,1513,1515,1518,1521,1523,1526,1528,1531],{"class":94,"line":1514},97,[92,1516,1517],{"class":105},"                   f",[92,1519,1520],{"class":224},"\"Upgrade with: ",[92,1522,1491],{"class":109},[92,1524,1525],{"class":116},".upgrade_hint",[92,1527,1497],{"class":109},[92,1529,1530],{"class":224},"\"",[92,1532,234],{"class":116},[92,1534,1536,1539,1541,1543,1545,1548,1551,1553,1555,1557,1560,1562],{"class":94,"line":1535},98,[92,1537,1538],{"class":109},"        print",[92,1540,550],{"class":116},[92,1542,1485],{"class":105},[92,1544,1530],{"class":224},[92,1546,1547],{"class":109},"\\n{",[92,1549,1550],{"class":116},"message",[92,1552,1497],{"class":109},[92,1554,1530],{"class":224},[92,1556,228],{"class":116},[92,1558,1559],{"class":383},"file",[92,1561,377],{"class":105},[92,1563,1564],{"class":116},"sys.stderr)\n",[92,1566,1568,1571,1573,1576,1578,1580],{"class":94,"line":1567},99,[92,1569,1570],{"class":116},"        data[",[92,1572,1430],{"class":224},[92,1574,1575],{"class":116},"] ",[92,1577,377],{"class":105},[92,1579,1061],{"class":109},[92,1581,1582],{"class":116},".clock().isoformat()\n",[92,1584,1586,1588],{"class":94,"line":1585},100,[92,1587,428],{"class":109},[92,1589,1292],{"class":116},[92,1591,1593,1595],{"class":94,"line":1592},101,[92,1594,645],{"class":105},[92,1596,1597],{"class":116}," message\n",[10,1599,1600,1601,1604,1605,1607,1608,1610,1611,28],{},"The ",[14,1602,1603],{},"fetch_latest"," callable receives the current version and returns the newer version string or ",[14,1606,50],{}," — a thin adapter around ",[14,1609,16],{},". Injecting it, along with the clock, is what makes the class testable without a network or ",[14,1612,1613],{},"time.sleep()",[10,1615,1616,1617,1620,1621,1623,1624,1627,1628,1631],{},"Several decisions are encoded here. The notice is rate-limited separately from the check (",[14,1618,1619],{},"notified_at","), so heavy users see it once a day rather than on every run. A cache written for a different installed version is ignored, so the notice disappears the moment the user upgrades. Every file operation swallows ",[14,1622,722],{},", because a full disk or read-only home directory is not a reason for a command to fail. And ",[14,1625,1626],{},"enabled()"," is a static policy function you call ",[67,1629,1630],{},"before"," constructing anything, so disabled runs pay nothing at all.",[1633,1634,1636],"h3",{"id":1635},"wiring-it-into-a-typer-app","Wiring it into a Typer app",[10,1638,1639],{},"Start the notifier in the root callback and finish it when the context closes, which happens after the command has produced its output:",[83,1641,1643],{"className":85,"code":1642,"language":87,"meta":88,"style":88},"# src\u002Fmytool\u002Fcli.py\nfrom pathlib import Path\n\nimport typer\nfrom platformdirs import user_state_path\n\nfrom mytool.notifier import UpdateNotifier\nfrom mytool.updates import check_pypi, installed_version\n\napp = typer.Typer()\n\n\ndef _latest(current: str) -> str | None:\n    info = check_pypi(\"mytool\", current=current)\n    return info.latest if info else None\n\n\n@app.callback()\ndef main(ctx: typer.Context,\n         json_output: bool = typer.Option(False, \"--json\", help=\"Machine-readable output.\")) -> None:\n    \"\"\"My tool.\"\"\"\n    current = installed_version(\"mytool\")\n    if current and UpdateNotifier.enabled(machine_output=json_output):\n        notifier = UpdateNotifier(current, user_state_path(\"mytool\") \u002F \"update-check.json\", _latest)\n        notifier.start()\n        ctx.call_on_close(notifier.finish)\n\n\n@app.command()\ndef status() -> None:\n    \"\"\"Show status.\"\"\"\n    typer.echo(\"all systems nominal\")\n",[14,1644,1645,1650,1660,1664,1671,1683,1687,1699,1711,1715,1725,1729,1733,1755,1777,1794,1798,1802,1810,1820,1857,1862,1876,1897,1920,1925,1930,1934,1938,1945,1959,1964],{"__ignoreMap":88},[92,1646,1647],{"class":94,"line":95},[92,1648,1649],{"class":98},"# src\u002Fmytool\u002Fcli.py\n",[92,1651,1652,1654,1656,1658],{"class":94,"line":102},[92,1653,106],{"class":105},[92,1655,199],{"class":116},[92,1657,130],{"class":105},[92,1659,204],{"class":116},[92,1661,1662],{"class":94,"line":120},[92,1663,124],{"emptyLinePlaceholder":123},[92,1665,1666,1668],{"class":94,"line":127},[92,1667,130],{"class":105},[92,1669,1670],{"class":116}," typer\n",[92,1672,1673,1675,1678,1680],{"class":94,"line":136},[92,1674,106],{"class":105},[92,1676,1677],{"class":116}," platformdirs ",[92,1679,130],{"class":105},[92,1681,1682],{"class":116}," user_state_path\n",[92,1684,1685],{"class":94,"line":144},[92,1686,124],{"emptyLinePlaceholder":123},[92,1688,1689,1691,1694,1696],{"class":94,"line":152},[92,1690,106],{"class":105},[92,1692,1693],{"class":116}," mytool.notifier ",[92,1695,130],{"class":105},[92,1697,1698],{"class":116}," UpdateNotifier\n",[92,1700,1701,1703,1706,1708],{"class":94,"line":160},[92,1702,106],{"class":105},[92,1704,1705],{"class":116}," mytool.updates ",[92,1707,130],{"class":105},[92,1709,1710],{"class":116}," check_pypi, installed_version\n",[92,1712,1713],{"class":94,"line":168},[92,1714,124],{"emptyLinePlaceholder":123},[92,1716,1717,1720,1722],{"class":94,"line":181},[92,1718,1719],{"class":116},"app ",[92,1721,377],{"class":105},[92,1723,1724],{"class":116}," typer.Typer()\n",[92,1726,1727],{"class":94,"line":194},[92,1728,124],{"emptyLinePlaceholder":123},[92,1730,1731],{"class":94,"line":207},[92,1732,124],{"emptyLinePlaceholder":123},[92,1734,1735,1737,1740,1743,1745,1747,1749,1751,1753],{"class":94,"line":212},[92,1736,285],{"class":105},[92,1738,1739],{"class":288}," _latest",[92,1741,1742],{"class":116},"(current: ",[92,1744,338],{"class":109},[92,1746,566],{"class":116},[92,1748,338],{"class":109},[92,1750,357],{"class":105},[92,1752,360],{"class":109},[92,1754,323],{"class":116},[92,1756,1757,1760,1762,1765,1768,1770,1772,1774],{"class":94,"line":237},[92,1758,1759],{"class":116},"    info ",[92,1761,377],{"class":105},[92,1763,1764],{"class":116}," check_pypi(",[92,1766,1767],{"class":224},"\"mytool\"",[92,1769,228],{"class":116},[92,1771,1256],{"class":383},[92,1773,377],{"class":105},[92,1775,1776],{"class":116},"current)\n",[92,1778,1779,1781,1784,1786,1789,1792],{"class":94,"line":272},[92,1780,298],{"class":105},[92,1782,1783],{"class":116}," info.latest ",[92,1785,748],{"class":105},[92,1787,1788],{"class":116}," info ",[92,1790,1791],{"class":105},"else",[92,1793,519],{"class":109},[92,1795,1796],{"class":94,"line":277},[92,1797,124],{"emptyLinePlaceholder":123},[92,1799,1800],{"class":94,"line":282},[92,1801,124],{"emptyLinePlaceholder":123},[92,1803,1804,1807],{"class":94,"line":295},[92,1805,1806],{"class":288},"@app.callback",[92,1808,1809],{"class":116},"()\n",[92,1811,1812,1814,1817],{"class":94,"line":304},[92,1813,285],{"class":105},[92,1815,1816],{"class":288}," main",[92,1818,1819],{"class":116},"(ctx: typer.Context,\n",[92,1821,1822,1825,1827,1829,1832,1835,1837,1840,1842,1845,1847,1850,1853,1855],{"class":94,"line":309},[92,1823,1824],{"class":116},"         json_output: ",[92,1826,558],{"class":109},[92,1828,218],{"class":105},[92,1830,1831],{"class":116}," typer.Option(",[92,1833,1834],{"class":109},"False",[92,1836,228],{"class":116},[92,1838,1839],{"class":224},"\"--json\"",[92,1841,228],{"class":116},[92,1843,1844],{"class":383},"help",[92,1846,377],{"class":105},[92,1848,1849],{"class":224},"\"Machine-readable output.\"",[92,1851,1852],{"class":116},")) -> ",[92,1854,50],{"class":109},[92,1856,323],{"class":116},[92,1858,1859],{"class":94,"line":314},[92,1860,1861],{"class":224},"    \"\"\"My tool.\"\"\"\n",[92,1863,1864,1867,1869,1872,1874],{"class":94,"line":326},[92,1865,1866],{"class":116},"    current ",[92,1868,377],{"class":105},[92,1870,1871],{"class":116}," installed_version(",[92,1873,1767],{"class":224},[92,1875,234],{"class":116},[92,1877,1878,1881,1884,1886,1889,1892,1894],{"class":94,"line":344},[92,1879,1880],{"class":105},"    if",[92,1882,1883],{"class":116}," current ",[92,1885,762],{"class":105},[92,1887,1888],{"class":116}," UpdateNotifier.enabled(",[92,1890,1891],{"class":383},"machine_output",[92,1893,377],{"class":105},[92,1895,1896],{"class":116},"json_output):\n",[92,1898,1899,1902,1904,1907,1909,1911,1914,1917],{"class":94,"line":371},[92,1900,1901],{"class":116},"        notifier ",[92,1903,377],{"class":105},[92,1905,1906],{"class":116}," UpdateNotifier(current, user_state_path(",[92,1908,1767],{"class":224},[92,1910,759],{"class":116},[92,1912,1913],{"class":105},"\u002F",[92,1915,1916],{"class":224}," \"update-check.json\"",[92,1918,1919],{"class":116},", _latest)\n",[92,1921,1922],{"class":94,"line":395},[92,1923,1924],{"class":116},"        notifier.start()\n",[92,1926,1927],{"class":94,"line":410},[92,1928,1929],{"class":116},"        ctx.call_on_close(notifier.finish)\n",[92,1931,1932],{"class":94,"line":425},[92,1933,124],{"emptyLinePlaceholder":123},[92,1935,1936],{"class":94,"line":439},[92,1937,124],{"emptyLinePlaceholder":123},[92,1939,1940,1943],{"class":94,"line":452},[92,1941,1942],{"class":288},"@app.command",[92,1944,1809],{"class":116},[92,1946,1947,1949,1952,1955,1957],{"class":94,"line":465},[92,1948,285],{"class":105},[92,1950,1951],{"class":288}," status",[92,1953,1954],{"class":116},"() -> ",[92,1956,50],{"class":109},[92,1958,323],{"class":116},[92,1960,1961],{"class":94,"line":478},[92,1962,1963],{"class":224},"    \"\"\"Show status.\"\"\"\n",[92,1965,1966,1969,1972],{"class":94,"line":491},[92,1967,1968],{"class":116},"    typer.echo(",[92,1970,1971],{"class":224},"\"all systems nominal\"",[92,1973,234],{"class":116},[10,1975,1976,1979,1980,1983,1984,1987,1988,1990],{},[14,1977,1978],{},"ctx.call_on_close"," runs when the root context is torn down — after the command returns, and also when it raises ",[14,1981,1982],{},"typer.Exit",". For Click the same two lines go in the group function. If your tool has a global ",[14,1985,1986],{},"--json"," flag, as here, passing it to ",[14,1989,1626],{}," keeps notices out of machine-readable runs; per-command format options need the same treatment.",[72,1992],{"name":1993},"upd-notice-terminal",[30,1995,1997],{"id":1996},"ux-considerations","UX considerations",[35,1999,2000,2007,2013,2019,2029,2035],{},[38,2001,2002,2006],{},[2003,2004,2005],"strong",{},"After the output, on stderr, separated by a blank line."," The user sees the result they asked for first, and pipes are untouched.",[38,2008,2009,2012],{},[2003,2010,2011],{},"Two lines at most."," What changed and the exact command to upgrade. Link to release notes only if the upgrade is a major version.",[38,2014,2015,2018],{},[2003,2016,2017],{},"Once per day."," Rate-limiting the notice is what makes users tolerate it.",[38,2020,2021,2024,2025,2028],{},[2003,2022,2023],{},"Honour every off switch."," A tool-specific variable, a generic one, a config key, CI detection and non-TTY stderr. Document all of them in ",[14,2026,2027],{},"--help"," or the README.",[38,2030,2031,2034],{},[2003,2032,2033],{},"Never block on exit."," The 0.25-second join is a ceiling, not a target; a daemon thread that is still waiting on the network is simply abandoned.",[38,2036,2037,2040,2041,2044,2045,2049,2050,28],{},[2003,2038,2039],{},"Match the hint to the install."," Hard-coding ",[14,2042,2043],{},"pipx upgrade"," is wrong for uv or Homebrew users; use the detection from ",[19,2046,2048],{"href":2047},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fself-upgrading-a-cli-installed-with-pipx-or-uv\u002F","self-upgrading a CLI installed with pipx or uv"," to build ",[14,2051,2052],{},"upgrade_hint",[72,2054],{"name":2055},"upd-notice-switches",[30,2057,2059],{"id":2058},"testing-the-behaviour","Testing the behaviour",[10,2061,2062,2063,2066],{},"Inject a fake fetcher and clock, point the cache at ",[14,2064,2065],{},"tmp_path",", and the whole lifecycle becomes deterministic:",[83,2068,2070],{"className":85,"code":2069,"language":87,"meta":88,"style":88},"# tests\u002Ftest_notifier.py\nfrom datetime import datetime, timedelta, timezone\n\nimport pytest\n\nfrom mytool.notifier import UpdateNotifier\n\nT0 = datetime(2026, 10, 2, 9, 0, tzinfo=timezone.utc)\n\n\nclass Clock:\n    def __init__(self):\n        self.now = T0\n\n    def __call__(self):\n        return self.now\n\n\n@pytest.fixture\ndef make(tmp_path):\n    calls = []\n\n    def factory(latest=\"1.6.0\", clock=None):\n        def fetch(current):\n            calls.append(current)\n            return latest\n        return UpdateNotifier(\"1.4.2\", tmp_path \u002F \"state.json\", fetch, clock=clock or Clock())\n\n    factory.calls = calls\n    return factory\n\n\ndef test_check_runs_once_per_interval(make):\n    clock = Clock()\n    n = make(clock=clock)\n    n.start(); n.finish()\n    n.start(); n.finish()\n    assert len(make.calls) == 1\n    clock.now += timedelta(hours=25)\n    n.start(); n.finish()\n    assert len(make.calls) == 2\n\n\ndef test_notice_on_stderr_once_per_day(make, capsys):\n    clock = Clock()\n    n = make(clock=clock)\n    n.start()\n    assert \"1.4.2 → 1.6.0\" in n.finish()\n    assert capsys.readouterr().out == \"\"\n    n.start()\n    assert n.finish() is None                    # already notified today\n\n\ndef test_no_notice_when_up_to_date(make):\n    n = make(latest=None)\n    n.start()\n    assert n.finish() is None\n\n\ndef test_corrupt_cache_is_ignored(make, tmp_path):\n    (tmp_path \u002F \"state.json\").write_text(\"{not json\")\n    n = make()\n    n.start()\n    assert n.finish() is not None\n\n\n@pytest.mark.parametrize(\"env\", [\"CI\", \"MYTOOL_NO_UPDATE_CHECK\", \"NO_UPDATE_NOTIFIER\"])\ndef test_disabled_by_environment(monkeypatch, env):\n    monkeypatch.setattr(\"sys.stderr.isatty\", lambda: True)\n    monkeypatch.setenv(env, \"1\")\n    assert UpdateNotifier.enabled() is False\n\n\ndef test_disabled_for_machine_output_and_pipes(monkeypatch):\n    for var in (\"CI\", \"GITHUB_ACTIONS\", \"MYTOOL_NO_UPDATE_CHECK\", \"NO_UPDATE_NOTIFIER\"):\n        monkeypatch.delenv(var, raising=False)\n    monkeypatch.setattr(\"sys.stderr.isatty\", lambda: True)\n    assert UpdateNotifier.enabled() is True\n    assert UpdateNotifier.enabled(machine_output=True) is False\n    monkeypatch.setattr(\"sys.stderr.isatty\", lambda: False)\n    assert UpdateNotifier.enabled() is False\n",[14,2071,2072,2077,2087,2091,2098,2102,2112,2116,2159,2163,2167,2176,2185,2197,2201,2210,2219,2223,2227,2232,2242,2252,2256,2280,2291,2296,2303,2337,2341,2351,2358,2362,2366,2376,2386,2403,2408,2412,2428,2447,2451,2464,2468,2472,2482,2490,2504,2509,2522,2534,2538,2552,2556,2560,2569,2585,2589,2599,2603,2607,2617,2634,2643,2647,2659,2663,2667,2693,2703,2722,2732,2743,2747,2751,2761,2789,2803,2819,2830,2848,2864],{"__ignoreMap":88},[92,2073,2074],{"class":94,"line":95},[92,2075,2076],{"class":98},"# tests\u002Ftest_notifier.py\n",[92,2078,2079,2081,2083,2085],{"class":94,"line":102},[92,2080,106],{"class":105},[92,2082,186],{"class":116},[92,2084,130],{"class":105},[92,2086,191],{"class":116},[92,2088,2089],{"class":94,"line":120},[92,2090,124],{"emptyLinePlaceholder":123},[92,2092,2093,2095],{"class":94,"line":127},[92,2094,130],{"class":105},[92,2096,2097],{"class":116}," pytest\n",[92,2099,2100],{"class":94,"line":136},[92,2101,124],{"emptyLinePlaceholder":123},[92,2103,2104,2106,2108,2110],{"class":94,"line":144},[92,2105,106],{"class":105},[92,2107,1693],{"class":116},[92,2109,130],{"class":105},[92,2111,1698],{"class":116},[92,2113,2114],{"class":94,"line":152},[92,2115,124],{"emptyLinePlaceholder":123},[92,2117,2118,2121,2123,2126,2129,2131,2134,2136,2139,2141,2144,2146,2149,2151,2154,2156],{"class":94,"line":160},[92,2119,2120],{"class":116},"T0 ",[92,2122,377],{"class":105},[92,2124,2125],{"class":116}," datetime(",[92,2127,2128],{"class":109},"2026",[92,2130,228],{"class":116},[92,2132,2133],{"class":109},"10",[92,2135,228],{"class":116},[92,2137,2138],{"class":109},"2",[92,2140,228],{"class":116},[92,2142,2143],{"class":109},"9",[92,2145,228],{"class":116},[92,2147,2148],{"class":109},"0",[92,2150,228],{"class":116},[92,2152,2153],{"class":383},"tzinfo",[92,2155,377],{"class":105},[92,2157,2158],{"class":116},"timezone.utc)\n",[92,2160,2161],{"class":94,"line":168},[92,2162,124],{"emptyLinePlaceholder":123},[92,2164,2165],{"class":94,"line":181},[92,2166,124],{"emptyLinePlaceholder":123},[92,2168,2169,2171,2174],{"class":94,"line":194},[92,2170,317],{"class":105},[92,2172,2173],{"class":288}," Clock",[92,2175,323],{"class":116},[92,2177,2178,2180,2182],{"class":94,"line":207},[92,2179,329],{"class":105},[92,2181,332],{"class":109},[92,2183,2184],{"class":116},"(self):\n",[92,2186,2187,2189,2192,2194],{"class":94,"line":212},[92,2188,428],{"class":109},[92,2190,2191],{"class":116},".now ",[92,2193,377],{"class":105},[92,2195,2196],{"class":116}," T0\n",[92,2198,2199],{"class":94,"line":237},[92,2200,124],{"emptyLinePlaceholder":123},[92,2202,2203,2205,2208],{"class":94,"line":272},[92,2204,329],{"class":105},[92,2206,2207],{"class":109}," __call__",[92,2209,2184],{"class":116},[92,2211,2212,2214,2216],{"class":94,"line":277},[92,2213,645],{"class":105},[92,2215,1061],{"class":109},[92,2217,2218],{"class":116},".now\n",[92,2220,2221],{"class":94,"line":282},[92,2222,124],{"emptyLinePlaceholder":123},[92,2224,2225],{"class":94,"line":295},[92,2226,124],{"emptyLinePlaceholder":123},[92,2228,2229],{"class":94,"line":304},[92,2230,2231],{"class":288},"@pytest.fixture\n",[92,2233,2234,2236,2239],{"class":94,"line":309},[92,2235,285],{"class":105},[92,2237,2238],{"class":288}," make",[92,2240,2241],{"class":116},"(tmp_path):\n",[92,2243,2244,2247,2249],{"class":94,"line":314},[92,2245,2246],{"class":116},"    calls ",[92,2248,377],{"class":105},[92,2250,2251],{"class":116}," []\n",[92,2253,2254],{"class":94,"line":326},[92,2255,124],{"emptyLinePlaceholder":123},[92,2257,2258,2260,2263,2266,2268,2271,2274,2276,2278],{"class":94,"line":344},[92,2259,329],{"class":105},[92,2261,2262],{"class":288}," factory",[92,2264,2265],{"class":116},"(latest",[92,2267,377],{"class":105},[92,2269,2270],{"class":224},"\"1.6.0\"",[92,2272,2273],{"class":116},", clock",[92,2275,377],{"class":105},[92,2277,50],{"class":109},[92,2279,603],{"class":116},[92,2281,2282,2285,2288],{"class":94,"line":371},[92,2283,2284],{"class":105},"        def",[92,2286,2287],{"class":288}," fetch",[92,2289,2290],{"class":116},"(current):\n",[92,2292,2293],{"class":94,"line":395},[92,2294,2295],{"class":116},"            calls.append(current)\n",[92,2297,2298,2300],{"class":94,"line":410},[92,2299,609],{"class":105},[92,2301,2302],{"class":116}," latest\n",[92,2304,2305,2307,2310,2313,2316,2318,2321,2324,2327,2329,2332,2334],{"class":94,"line":425},[92,2306,645],{"class":105},[92,2308,2309],{"class":116}," UpdateNotifier(",[92,2311,2312],{"class":224},"\"1.4.2\"",[92,2314,2315],{"class":116},", tmp_path ",[92,2317,1913],{"class":105},[92,2319,2320],{"class":224}," \"state.json\"",[92,2322,2323],{"class":116},", fetch, ",[92,2325,2326],{"class":383},"clock",[92,2328,377],{"class":105},[92,2330,2331],{"class":116},"clock ",[92,2333,582],{"class":105},[92,2335,2336],{"class":116}," Clock())\n",[92,2338,2339],{"class":94,"line":439},[92,2340,124],{"emptyLinePlaceholder":123},[92,2342,2343,2346,2348],{"class":94,"line":452},[92,2344,2345],{"class":116},"    factory.calls ",[92,2347,377],{"class":105},[92,2349,2350],{"class":116}," calls\n",[92,2352,2353,2355],{"class":94,"line":465},[92,2354,298],{"class":105},[92,2356,2357],{"class":116}," factory\n",[92,2359,2360],{"class":94,"line":478},[92,2361,124],{"emptyLinePlaceholder":123},[92,2363,2364],{"class":94,"line":491},[92,2365,124],{"emptyLinePlaceholder":123},[92,2367,2368,2370,2373],{"class":94,"line":504},[92,2369,285],{"class":105},[92,2371,2372],{"class":288}," test_check_runs_once_per_interval",[92,2374,2375],{"class":116},"(make):\n",[92,2377,2378,2381,2383],{"class":94,"line":522},[92,2379,2380],{"class":116},"    clock ",[92,2382,377],{"class":105},[92,2384,2385],{"class":116}," Clock()\n",[92,2387,2388,2391,2393,2396,2398,2400],{"class":94,"line":527},[92,2389,2390],{"class":116},"    n ",[92,2392,377],{"class":105},[92,2394,2395],{"class":116}," make(",[92,2397,2326],{"class":383},[92,2399,377],{"class":105},[92,2401,2402],{"class":116},"clock)\n",[92,2404,2405],{"class":94,"line":533},[92,2406,2407],{"class":116},"    n.start(); n.finish()\n",[92,2409,2410],{"class":94,"line":542},[92,2411,2407],{"class":116},[92,2413,2414,2417,2420,2423,2425],{"class":94,"line":573},[92,2415,2416],{"class":105},"    assert",[92,2418,2419],{"class":109}," len",[92,2421,2422],{"class":116},"(make.calls) ",[92,2424,773],{"class":105},[92,2426,2427],{"class":109}," 1\n",[92,2429,2430,2433,2436,2438,2440,2442,2445],{"class":94,"line":606},[92,2431,2432],{"class":116},"    clock.now ",[92,2434,2435],{"class":105},"+=",[92,2437,380],{"class":116},[92,2439,384],{"class":383},[92,2441,377],{"class":105},[92,2443,2444],{"class":109},"25",[92,2446,234],{"class":116},[92,2448,2449],{"class":94,"line":615},[92,2450,2407],{"class":116},[92,2452,2453,2455,2457,2459,2461],{"class":94,"line":635},[92,2454,2416],{"class":105},[92,2456,2419],{"class":109},[92,2458,2422],{"class":116},[92,2460,773],{"class":105},[92,2462,2463],{"class":109}," 2\n",[92,2465,2466],{"class":94,"line":642},[92,2467,124],{"emptyLinePlaceholder":123},[92,2469,2470],{"class":94,"line":651},[92,2471,124],{"emptyLinePlaceholder":123},[92,2473,2474,2476,2479],{"class":94,"line":656},[92,2475,285],{"class":105},[92,2477,2478],{"class":288}," test_notice_on_stderr_once_per_day",[92,2480,2481],{"class":116},"(make, capsys):\n",[92,2483,2484,2486,2488],{"class":94,"line":662},[92,2485,2380],{"class":116},[92,2487,377],{"class":105},[92,2489,2385],{"class":116},[92,2491,2492,2494,2496,2498,2500,2502],{"class":94,"line":678},[92,2493,2390],{"class":116},[92,2495,377],{"class":105},[92,2497,2395],{"class":116},[92,2499,2326],{"class":383},[92,2501,377],{"class":105},[92,2503,2402],{"class":116},[92,2505,2506],{"class":94,"line":686},[92,2507,2508],{"class":116},"    n.start()\n",[92,2510,2511,2513,2516,2519],{"class":94,"line":714},[92,2512,2416],{"class":105},[92,2514,2515],{"class":224}," \"1.4.2 → 1.6.0\"",[92,2517,2518],{"class":105}," in",[92,2520,2521],{"class":116}," n.finish()\n",[92,2523,2524,2526,2529,2531],{"class":94,"line":732},[92,2525,2416],{"class":105},[92,2527,2528],{"class":116}," capsys.readouterr().out ",[92,2530,773],{"class":105},[92,2532,2533],{"class":224}," \"\"\n",[92,2535,2536],{"class":94,"line":740},[92,2537,2508],{"class":116},[92,2539,2540,2542,2545,2547,2549],{"class":94,"line":784},[92,2541,2416],{"class":105},[92,2543,2544],{"class":116}," n.finish() ",[92,2546,1112],{"class":105},[92,2548,360],{"class":109},[92,2550,2551],{"class":98},"                    # already notified today\n",[92,2553,2554],{"class":94,"line":789},[92,2555,124],{"emptyLinePlaceholder":123},[92,2557,2558],{"class":94,"line":808},[92,2559,124],{"emptyLinePlaceholder":123},[92,2561,2562,2564,2567],{"class":94,"line":815},[92,2563,285],{"class":105},[92,2565,2566],{"class":288}," test_no_notice_when_up_to_date",[92,2568,2375],{"class":116},[92,2570,2571,2573,2575,2577,2579,2581,2583],{"class":94,"line":843},[92,2572,2390],{"class":116},[92,2574,377],{"class":105},[92,2576,2395],{"class":116},[92,2578,1266],{"class":383},[92,2580,377],{"class":105},[92,2582,50],{"class":109},[92,2584,234],{"class":116},[92,2586,2587],{"class":94,"line":874},[92,2588,2508],{"class":116},[92,2590,2591,2593,2595,2597],{"class":94,"line":902},[92,2592,2416],{"class":105},[92,2594,2544],{"class":116},[92,2596,1112],{"class":105},[92,2598,519],{"class":109},[92,2600,2601],{"class":94,"line":924},[92,2602,124],{"emptyLinePlaceholder":123},[92,2604,2605],{"class":94,"line":935},[92,2606,124],{"emptyLinePlaceholder":123},[92,2608,2609,2611,2614],{"class":94,"line":945},[92,2610,285],{"class":105},[92,2612,2613],{"class":288}," test_corrupt_cache_is_ignored",[92,2615,2616],{"class":116},"(make, tmp_path):\n",[92,2618,2619,2622,2624,2626,2629,2632],{"class":94,"line":954},[92,2620,2621],{"class":116},"    (tmp_path ",[92,2623,1913],{"class":105},[92,2625,2320],{"class":224},[92,2627,2628],{"class":116},").write_text(",[92,2630,2631],{"class":224},"\"{not json\"",[92,2633,234],{"class":116},[92,2635,2636,2638,2640],{"class":94,"line":959},[92,2637,2390],{"class":116},[92,2639,377],{"class":105},[92,2641,2642],{"class":116}," make()\n",[92,2644,2645],{"class":94,"line":985},[92,2646,2508],{"class":116},[92,2648,2649,2651,2653,2655,2657],{"class":94,"line":992},[92,2650,2416],{"class":105},[92,2652,2544],{"class":116},[92,2654,1112],{"class":105},[92,2656,1346],{"class":105},[92,2658,519],{"class":109},[92,2660,2661],{"class":94,"line":1000},[92,2662,124],{"emptyLinePlaceholder":123},[92,2664,2665],{"class":94,"line":1021},[92,2666,124],{"emptyLinePlaceholder":123},[92,2668,2669,2672,2674,2677,2680,2682,2684,2686,2688,2690],{"class":94,"line":1028},[92,2670,2671],{"class":288},"@pytest.mark.parametrize",[92,2673,550],{"class":116},[92,2675,2676],{"class":224},"\"env\"",[92,2678,2679],{"class":116},", [",[92,2681,247],{"class":224},[92,2683,228],{"class":116},[92,2685,225],{"class":224},[92,2687,228],{"class":116},[92,2689,231],{"class":224},[92,2691,2692],{"class":116},"])\n",[92,2694,2695,2697,2700],{"class":94,"line":1033},[92,2696,285],{"class":105},[92,2698,2699],{"class":288}," test_disabled_by_environment",[92,2701,2702],{"class":116},"(monkeypatch, env):\n",[92,2704,2705,2708,2711,2713,2716,2718,2720],{"class":94,"line":1039},[92,2706,2707],{"class":116},"    monkeypatch.setattr(",[92,2709,2710],{"class":224},"\"sys.stderr.isatty\"",[92,2712,228],{"class":116},[92,2714,2715],{"class":105},"lambda",[92,2717,910],{"class":116},[92,2719,829],{"class":109},[92,2721,234],{"class":116},[92,2723,2724,2727,2730],{"class":94,"line":1053},[92,2725,2726],{"class":116},"    monkeypatch.setenv(env, ",[92,2728,2729],{"class":224},"\"1\"",[92,2731,234],{"class":116},[92,2733,2734,2736,2739,2741],{"class":94,"line":1067},[92,2735,2416],{"class":105},[92,2737,2738],{"class":116}," UpdateNotifier.enabled() ",[92,2740,1112],{"class":105},[92,2742,612],{"class":109},[92,2744,2745],{"class":94,"line":1085},[92,2746,124],{"emptyLinePlaceholder":123},[92,2748,2749],{"class":94,"line":1120},[92,2750,124],{"emptyLinePlaceholder":123},[92,2752,2753,2755,2758],{"class":94,"line":1144},[92,2754,285],{"class":105},[92,2756,2757],{"class":288}," test_disabled_for_machine_output_and_pipes",[92,2759,2760],{"class":116},"(monkeypatch):\n",[92,2762,2763,2766,2769,2771,2773,2775,2777,2779,2781,2783,2785,2787],{"class":94,"line":1152},[92,2764,2765],{"class":105},"    for",[92,2767,2768],{"class":116}," var ",[92,2770,597],{"class":105},[92,2772,221],{"class":116},[92,2774,247],{"class":224},[92,2776,228],{"class":116},[92,2778,252],{"class":224},[92,2780,228],{"class":116},[92,2782,225],{"class":224},[92,2784,228],{"class":116},[92,2786,231],{"class":224},[92,2788,603],{"class":116},[92,2790,2791,2794,2797,2799,2801],{"class":94,"line":1194},[92,2792,2793],{"class":116},"        monkeypatch.delenv(var, ",[92,2795,2796],{"class":383},"raising",[92,2798,377],{"class":105},[92,2800,1834],{"class":109},[92,2802,234],{"class":116},[92,2804,2805,2807,2809,2811,2813,2815,2817],{"class":94,"line":1202},[92,2806,2707],{"class":116},[92,2808,2710],{"class":224},[92,2810,228],{"class":116},[92,2812,2715],{"class":105},[92,2814,910],{"class":116},[92,2816,829],{"class":109},[92,2818,234],{"class":116},[92,2820,2821,2823,2825,2827],{"class":94,"line":1207},[92,2822,2416],{"class":105},[92,2824,2738],{"class":116},[92,2826,1112],{"class":105},[92,2828,2829],{"class":109}," True\n",[92,2831,2832,2834,2836,2838,2840,2842,2844,2846],{"class":94,"line":1221},[92,2833,2416],{"class":105},[92,2835,1888],{"class":116},[92,2837,1891],{"class":383},[92,2839,377],{"class":105},[92,2841,829],{"class":109},[92,2843,759],{"class":116},[92,2845,1112],{"class":105},[92,2847,612],{"class":109},[92,2849,2850,2852,2854,2856,2858,2860,2862],{"class":94,"line":1239},[92,2851,2707],{"class":116},[92,2853,2710],{"class":224},[92,2855,228],{"class":116},[92,2857,2715],{"class":105},[92,2859,910],{"class":116},[92,2861,1834],{"class":109},[92,2863,234],{"class":116},[92,2865,2866,2868,2870,2872],{"class":94,"line":1250},[92,2867,2416],{"class":105},[92,2869,2738],{"class":116},[92,2871,1112],{"class":105},[92,2873,612],{"class":109},[10,2875,2876],{},"Add one timing test to the end-to-end suite: run the installed command with the check enabled and a fetcher that sleeps for five seconds, and assert it still finishes in well under a second. That is the regression test for the property users care about most.",[30,2878,2880],{"id":2879},"conclusion","Conclusion",[10,2882,2883,2884,2887],{},"A good update notice is invisible until it is useful: a cached check that runs at most daily, in a background thread that can never delay the command, and one short message on stderr after the output — silent in CI, pipes and machine-readable modes. Build it as a small class with an injected fetcher and clock, wire it to the root callback with ",[14,2885,2886],{},"call_on_close",", and test the lifecycle deterministically.",[30,2889,2891],{"id":2890},"frequently-asked-questions","Frequently asked questions",[1633,2893,2895],{"id":2894},"why-a-thread-rather-than-a-separate-background-process","Why a thread rather than a separate background process?",[10,2897,2898],{},"A thread is simpler, portable and needs no cleanup. Its limitation is that a slow check is abandoned when the command exits, so the result arrives a run later. A detached subprocess survives the exit and can always complete, which matters for tools whose commands are typically very short; the cost is spawning a Python process, which is slow on Windows and harder to test.",[1633,2900,2902],{"id":2901},"does-a-daemon-thread-delay-interpreter-shutdown","Does a daemon thread delay interpreter shutdown?",[10,2904,2905,2906,28],{},"No — daemon threads are abandoned at exit. The only care needed is that the thread does not hold a half-written file when that happens, which is why the cache is written atomically with a temporary file and ",[14,2907,2908],{},"os.replace",[1633,2910,2912,2913,2915],{"id":2911},"should-the-notice-appear-for-every-command-including-help","Should the notice appear for every command, including ",[14,2914,2027],{},"?",[10,2917,2918,2919,2922],{},"Help and version commands usually exit before the callback runs, so the notice naturally skips them, which is fine. Some tools deliberately show the notice in ",[14,2920,2921],{},"mytool --version"," output, since a user checking the version is the one most likely to want to know.",[1633,2924,2926],{"id":2925},"how-do-i-let-users-turn-it-off-permanently","How do I let users turn it off permanently?",[10,2928,2929,2930,2933,2934,2936],{},"Read a config key such as ",[14,2931,2932],{},"update_check = false"," in the same place as other settings and pass it into ",[14,2935,1626],{},". Mention the setting in the notice's documentation, not in the notice itself — two lines is the budget.",[30,2938,2940],{"id":2939},"related","Related",[35,2942,2943,2949,2954,2959,2965],{},[38,2944,2945,2946],{},"Up: ",[19,2947,2948],{"href":26},"Update checks, upgrade commands and telemetry",[38,2950,2951],{},[19,2952,2953],{"href":21},"Checking PyPI for a newer version",[38,2955,2956],{},[19,2957,2958],{"href":2047},"Self-upgrading a CLI installed with pipx or uv",[38,2960,2961],{},[19,2962,2964],{"href":2963},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs\u002F","Storing app data with platformdirs",[38,2966,2967],{},[19,2968,2970],{"href":2969},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools\u002F","Parallelising CLI work with thread pools",[2972,2973,2974],"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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":88,"searchDepth":102,"depth":102,"links":2976},[2977,2978,2979,2982,2983,2984,2985,2992],{"id":32,"depth":102,"text":33},{"id":61,"depth":102,"text":62},{"id":80,"depth":102,"text":81,"children":2980},[2981],{"id":1635,"depth":120,"text":1636},{"id":1996,"depth":102,"text":1997},{"id":2058,"depth":102,"text":2059},{"id":2879,"depth":102,"text":2880},{"id":2890,"depth":102,"text":2891,"children":2986},[2987,2988,2989,2991],{"id":2894,"depth":120,"text":2895},{"id":2901,"depth":120,"text":2902},{"id":2911,"depth":120,"text":2990},"Should the notice appear for every command, including --help?",{"id":2925,"depth":120,"text":2926},{"id":2939,"depth":102,"text":2940},"2026-10-02","Run update checks in a background thread with a daily cache, print one short notice on stderr after the output, and switch it off for CI, pipes and JSON mode.","intermediate",false,"md",{},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fshowing-non-blocking-update-notices",{"title":5,"description":2994},"cli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fshowing-non-blocking-update-notices\u002Findex",[3003,3004,3005,3006,3007],"updates","threading","ux","caching","typer","cBLrbkWOAihUziEEzYT__fQN2lRY1Xxh8ZLYmyKPnEo",[3010,3013,3016,3019,3022,3025,3028,3031,3034,3037,3040,3043,3046,3049,3052,3055,3058,3061,3064,3067,3070,3073,3076,3079,3082,3085,3088,3091,3094,3097,3100,3103,3106,3109,3112,3115,3118,3121,3124,3127,3130,3133,3136,3139,3142,3145,3148,3151,3154,3157,3160,3163,3166,3169,3172,3175,3178,3181,3184,3187,3190,3193,3196,3199,3202,3205,3208,3211,3214,3217,3220,3223,3226,3229,3232,3235,3238,3241,3244,3247,3250,3253,3256,3259,3262,3265,3268,3271,3274,3277,3280,3283,3286,3289,3292,3295,3298,3301,3304,3307,3310,3313,3316,3319,3322,3325,3328,3331,3334,3337,3340,3343,3346,3349,3352,3355,3358,3361,3364,3367,3370,3373,3376,3379,3382,3385,3388,3391,3394,3397,3400,3403,3406,3409,3412,3415,3418,3421,3424,3425,3427,3430,3433,3436,3439,3442,3445,3448,3451,3454,3457,3460,3463,3466,3469,3472,3475,3478,3481,3484,3487,3490,3493,3496,3499,3502,3505,3508,3511,3514,3517,3520,3523,3526,3529,3532,3535,3538,3541,3544,3547,3550,3553,3556,3559,3562,3565,3568,3571,3574,3577,3580,3583,3586,3589,3592,3595,3598,3601,3604,3607,3610,3613,3616,3619,3622,3625,3628,3631,3634,3637,3640,3643,3646,3649,3652,3655,3658,3661,3664,3667,3670,3673,3676,3679,3682,3685,3688,3691,3694,3697,3700,3703,3706,3709,3712,3715,3718,3721,3724,3727,3730,3733,3736,3739,3742,3745,3748,3751,3754,3757,3760,3763,3766,3769,3772,3775,3778,3781,3784,3787,3790,3793,3796,3799,3802,3805,3808,3811,3814,3817,3820,3823,3826,3829,3832,3835,3838,3841,3844,3847,3850],{"path":3011,"title":3012},"\u002Fabout","About Python CLI Toolcraft",{"path":3014,"title":3015},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":3017,"title":3018},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":3020,"title":3021},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dates-and-durations-in-cli-arguments","Validating Dates and Durations in Python CLI Arguments",{"path":3023,"title":3024},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":3026,"title":3027},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":3029,"title":3030},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-urls-hosts-and-ports","Validating URLs, Hosts and Ports in Python CLI Arguments",{"path":3032,"title":3033},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":3035,"title":3036},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbrowsing-records-with-a-textual-datatable","Browsing Records with a Textual DataTable Picker",{"path":3038,"title":3039},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbuilding-your-first-textual-app","Building Your First Textual App for a Python CLI",{"path":3041,"title":3042},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fchoosing-between-a-cli-a-prompt-flow-and-a-tui","Choosing Between a CLI, a Prompt Flow and a TUI",{"path":3044,"title":3045},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":3047,"title":3048},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Frunning-background-work-in-textual-with-workers","Running Background Work in Textual with Workers",{"path":3050,"title":3051},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fstyling-textual-apps-with-tcss","Styling Textual Apps with TCSS",{"path":3053,"title":3054},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":3056,"title":3057},"\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":3059,"title":3060},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fdocumenting-environment-variables-in-help","Documenting Environment Variables in CLI Help",{"path":3062,"title":3063},"\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":3065,"title":3066},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":3068,"title":3069},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Frich-formatted-help-with-rich-click","Rich-Formatted Help for Click CLIs with rich-click",{"path":3071,"title":3072},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":3074,"title":3075},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":3077,"title":3078},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":3080,"title":3081},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":3083,"title":3084},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Ffixing-unicode-and-encoding-errors-on-windows","Fixing Unicode and Encoding Errors on Windows in Python CLIs",{"path":3086,"title":3087},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fhandling-ansi-escape-codes-on-windows-consoles","Handling ANSI Escape Codes on Windows Consoles",{"path":3089,"title":3090},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":3092,"title":3093},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Frespecting-no-color-and-force-color","Respecting NO_COLOR and FORCE_COLOR in Python CLIs",{"path":3095,"title":3096},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fsupporting-dumb-terminals-and-screen-readers","Supporting Dumb Terminals and Screen Readers in a Python CLI",{"path":3098,"title":3099},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":3101,"title":3102},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdesigning-an-exception-hierarchy-for-a-cli","Designing an Exception Hierarchy for a Python CLI",{"path":3104,"title":3105},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdid-you-mean-suggestions-for-mistyped-input","Did You Mean…? Suggestions for Mistyped CLI Input",{"path":3107,"title":3108},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":3110,"title":3111},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":3113,"title":3114},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":3116,"title":3117},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Freporting-machine-readable-errors-in-json-mode","Reporting Machine-Readable Errors in JSON Mode",{"path":3119,"title":3120},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fwriting-crash-reports-users-can-send","Writing Crash Reports Users Can Send",{"path":3122,"title":3123},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":3125,"title":3126},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fdiscovering-project-config-files-by-walking-up-directories","Discovering Project Config Files by Walking Up Directories",{"path":3128,"title":3129},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":3131,"title":3132},"\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":3134,"title":3135},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":3137,"title":3138},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":3140,"title":3141},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fvalidating-config-files-with-json-schema","Validating Config Files with JSON Schema in a Python CLI",{"path":3143,"title":3144},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fwriting-a-config-init-and-edit-command","Writing a Config Init and Edit Command for a Python CLI",{"path":3146,"title":3147},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":3149,"title":3150},"\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":3152,"title":3153},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":3155,"title":3156},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-tree-views-with-rich","Building Tree Views with Rich in a Python CLI",{"path":3158,"title":3159},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":3161,"title":3162},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":3164,"title":3165},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-markdown-and-syntax-highlighting-with-rich","Rendering Markdown and Syntax Highlighting with Rich",{"path":3167,"title":3168},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":3170,"title":3171},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":3173,"title":3174},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fadding-a-format-flag-for-table-json-and-csv","Adding a Format Flag for Table, JSON and CSV Output",{"path":3176,"title":3177},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fcustom-output-templates-with-a-format-string","Custom Output Templates with a Format String in Python CLIs",{"path":3179,"title":3180},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fexporting-cli-results-to-files","Exporting CLI Results to Files from a Python CLI",{"path":3182,"title":3183},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis","Output Formats for Data-Heavy Python CLIs",{"path":3185,"title":3186},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fselecting-fields-and-columns-from-cli-output","Selecting Fields and Columns from Python CLI Output",{"path":3188,"title":3189},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fwriting-csv-and-tsv-output-correctly","Writing CSV and TSV Output Correctly from a Python CLI",{"path":3191,"title":3192},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fargcomplete-for-argparse-clis","Tab Completion for argparse CLIs with argcomplete",{"path":3194,"title":3195},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fdynamic-completion-values-from-apis-and-files","Dynamic Completion Values from APIs and Files",{"path":3197,"title":3198},"\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":3200,"title":3201},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":3203,"title":3204},"\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":3206,"title":3207},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fshipping-completion-scripts-with-packages","Shipping Shell Completion Scripts with a Python CLI",{"path":3209,"title":3210},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":3212,"title":3213},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-trace-ids-and-context-to-cli-logs","Adding Trace IDs and Context to Python CLI Logs",{"path":3215,"title":3216},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":3218,"title":3219},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":3221,"title":3222},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Flogging-with-structlog-in-a-cli","Logging with structlog in a Python CLI",{"path":3224,"title":3225},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fseparating-logs-from-program-output","Separating Logs from Program Output in a Python CLI",{"path":3227,"title":3228},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":3230,"title":3231},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fwriting-rotating-log-files-from-a-cli","Writing Rotating Log Files from a Python CLI",{"path":3233,"title":3234},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":3236,"title":3237},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":3239,"title":3240},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":3242,"title":3243},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":3245,"title":3246},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fnull-delimited-input-and-xargs-compatibility","Null-Delimited Input and xargs Compatibility in Python CLIs",{"path":3248,"title":3249},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fprocessing-large-files-and-ndjson-streams","Processing Large Files and NDJSON Streams in Python CLIs",{"path":3251,"title":3252},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":3254,"title":3255},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fbuilding-an-api-client-cli-with-httpx","Building an API Client CLI with httpx",{"path":3257,"title":3258},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":3260,"title":3261},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":3263,"title":3264},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fmocking-http-in-cli-tests-with-respx","Mocking HTTP in Python CLI Tests with respx",{"path":3266,"title":3267},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":3269,"title":3270},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":3272,"title":3273},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls","Retries and Backoff for CLI HTTP Calls",{"path":3275,"title":3276},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fuploading-files-with-multipart-and-progress","Uploading Files with Multipart and Progress in a Python CLI",{"path":3278,"title":3279},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fcancelling-async-tasks-on-ctrl-c","Cancelling Async Tasks on Ctrl+C in Python CLIs",{"path":3281,"title":3282},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":3284,"title":3285},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":3287,"title":3288},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":3290,"title":3291},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":3293,"title":3294},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frunning-async-code-in-typer-and-click","Running Async Code in Typer and Click",{"path":3296,"title":3297},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fshowing-progress-for-concurrent-tasks","Showing Progress for Concurrent Tasks in a Python CLI",{"path":3299,"title":3300},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis","Structured Concurrency with TaskGroups in Python CLIs",{"path":3302,"title":3303},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":3305,"title":3306},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":3308,"title":3309},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fhandling-file-permissions-and-umask-in-clis","Handling File Permissions and umask in Python CLIs",{"path":3311,"title":3312},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":3314,"title":3315},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":3317,"title":3318},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":3320,"title":3321},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwalking-directory-trees-with-ignore-rules","Walking Directory Trees with Ignore Rules in a Python CLI",{"path":3323,"title":3324},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":3326,"title":3327},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":3329,"title":3330},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fcaching-http-responses-on-disk-in-a-cli","Caching HTTP Responses on Disk in a Python CLI",{"path":3332,"title":3333},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis","Local State and SQLite in Python CLIs",{"path":3335,"title":3336},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fmigrating-a-cli-sqlite-schema","Migrating a CLI’s SQLite Schema Between Releases",{"path":3338,"title":3339},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Frecording-and-querying-cli-run-history","Recording and Querying CLI Run History in SQLite",{"path":3341,"title":3342},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fstoring-cli-state-in-sqlite","Storing CLI State in SQLite with a Small Repository Class",{"path":3344,"title":3345},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fbuilding-a-watch-mode-with-watchfiles","Building a Watch Mode with watchfiles in Python",{"path":3347,"title":3348},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":3350,"title":3351},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhealth-checks-and-heartbeats-for-long-running-clis","Health Checks and Heartbeats for Long-Running CLIs",{"path":3353,"title":3354},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":3356,"title":3357},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Freloading-config-on-sighup","Reloading Configuration on SIGHUP in a Python CLI",{"path":3359,"title":3360},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-as-a-systemd-service","Running a Python CLI as a systemd Service",{"path":3362,"title":3363},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-on-a-schedule-with-cron-and-systemd","Running a Python CLI on a Schedule with cron and systemd",{"path":3365,"title":3366},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fsending-cli-logs-to-journald-and-syslog","Sending Python CLI Logs to journald and syslog",{"path":3368,"title":3369},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":3371,"title":3372},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":3374,"title":3375},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":3377,"title":3378},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":3380,"title":3381},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Flaunching-the-users-editor-from-a-cli","Launching the User’s Editor from a Python CLI",{"path":3383,"title":3384},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fpiping-between-subprocesses-in-python","Piping Between Subprocesses in Python Without a Shell",{"path":3386,"title":3387},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":3389,"title":3390},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli","Wrapping git and Other Tools from a Python CLI",{"path":3392,"title":3393},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":3395,"title":3396},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":3398,"title":3399},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files","Reading Secrets from Env Vars and Files in CLIs",{"path":3401,"title":3402},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs","Redacting Secrets from CLI Output and Logs",{"path":3404,"title":3405},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Frefreshing-expired-tokens-automatically","Refreshing Expired Tokens Automatically in a Python CLI",{"path":3407,"title":3408},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":3410,"title":3411},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":3413,"title":3414},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fchecking-pypi-for-a-newer-version","Checking PyPI for a Newer Version of Your Python CLI",{"path":3416,"title":3417},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis","Update Checks, Upgrade Commands and Telemetry for Python CLIs",{"path":3419,"title":3420},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fopt-in-usage-telemetry-for-python-clis","Opt-In Usage Telemetry for Python CLIs Done Responsibly",{"path":3422,"title":3423},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fself-upgrading-a-cli-installed-with-pipx-or-uv","Self-Upgrading a Python CLI Installed with pipx or uv",{"path":2999,"title":5},{"path":1913,"title":3426},"Python CLI Toolcraft",{"path":3428,"title":3429},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cleo","Building a Python CLI with Cleo Command Classes",{"path":3431,"title":3432},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts","Building a Type-Hinted Python CLI with Cyclopts",{"path":3434,"title":3435},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks","Beyond Click and Typer: Alternative Python CLI Frameworks",{"path":3437,"title":3438},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fquick-clis-from-functions-with-python-fire","Quick CLIs from Functions with Python Fire",{"path":3440,"title":3441},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fusage-string-driven-clis-with-docopt-ng","Usage-String Driven Python CLIs with docopt-ng",{"path":3443,"title":3444},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Favoiding-import-time-side-effects","Avoiding Import-Time Side Effects in a Python CLI",{"path":3446,"title":3447},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fcaching-expensive-work-between-cli-runs","Caching Expensive Work Between Python CLI Runs",{"path":3449,"title":3450},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests","Guarding CLI Startup with Import Tests",{"path":3452,"title":3453},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":3455,"title":3456},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":3458,"title":3459},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":3461,"title":3462},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":3464,"title":3465},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":3467,"title":3468},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":3470,"title":3471},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargument-groups-and-help-formatting-in-argparse","Argument Groups and Help Formatting in argparse",{"path":3473,"title":3474},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":3476,"title":3477},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":3479,"title":3480},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":3482,"title":3483},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Freading-arguments-from-files-with-fromfile-prefix-chars","Reading Arguments from Files with argparse’s fromfile_prefix_chars",{"path":3485,"title":3486},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":3488,"title":3489},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fadding-dry-run-and-confirmation-to-destructive-commands","Adding Dry-Run and Confirmation to Destructive Commands",{"path":3491,"title":3492},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fdesigning-idempotent-commands","Designing Idempotent Commands in a Python CLI",{"path":3494,"title":3495},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Ffollowing-posix-and-gnu-argument-conventions","Following POSIX and GNU Argument Conventions in Python",{"path":3497,"title":3498},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fglobal-options-vs-per-command-options","Global Options vs Per-Command Options in Python CLIs",{"path":3500,"title":3501},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":3503,"title":3504},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":3506,"title":3507},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fpositional-arguments-vs-options","Positional Arguments vs Options: Designing a CLI Signature",{"path":3509,"title":3510},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":3512,"title":3513},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":3515,"title":3516},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":3518,"title":3519},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":3521,"title":3522},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures","Isolating Plugin Failures in an Extensible Python CLI",{"path":3524,"title":3525},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Ftesting-plugins-against-the-host-cli","Testing Plugins Against the Host CLI",{"path":3527,"title":3528},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":3530,"title":3531},"\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":3533,"title":3534},"\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":3536,"title":3537},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":3539,"title":3540},"\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":3542,"title":3543},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":3545,"title":3546},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Foffering-a-python-api-alongside-your-cli","Offering a Python API Alongside Your CLI",{"path":3548,"title":3549},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fregistering-commands-from-modules-automatically","Registering CLI Commands from Modules Automatically",{"path":3551,"title":3552},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":3554,"title":3555},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":3557,"title":3558},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli","End-to-End Testing an Installed Python CLI",{"path":3560,"title":3561},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":3563,"title":3564},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":3566,"title":3567},"\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":3569,"title":3570},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":3572,"title":3573},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":3575,"title":3576},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":3578,"title":3579},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-commands-that-call-subprocesses","Testing Python CLI Commands That Call Subprocesses",{"path":3581,"title":3582},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":3584,"title":3585},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftranscript-tests-for-cli-commands","Transcript Tests for Python CLI Commands",{"path":3587,"title":3588},"\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":3590,"title":3591},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":3593,"title":3594},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fchoices-and-enums-in-typer-and-click","Choices and Enums in Typer and Click Options",{"path":3596,"title":3597},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fclick-option-callbacks-and-eager-options","Click Option Callbacks and Eager Options Explained",{"path":3599,"title":3600},"\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":3602,"title":3603},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":3605,"title":3606},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Frich-markup-and-help-panels-in-typer","Rich Markup and Help Panels in Typer",{"path":3608,"title":3609},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":3611,"title":3612},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":3614,"title":3615},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":3617,"title":3618},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":3620,"title":3621},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":3623,"title":3624},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-a-cli-docker-image-from-ci","Publishing a Python CLI as a Docker Image from CI",{"path":3626,"title":3627},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-to-pypi-with-trusted-publishing","Publishing a CLI to PyPI with Trusted Publishing",{"path":3629,"title":3630},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Frunning-cli-tests-on-windows-and-macos-runners","Running Python CLI Tests on Windows and macOS Runners",{"path":3632,"title":3633},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci","Smoke-Testing the Built Wheel of a Python CLI in CI",{"path":3635,"title":3636},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions","Testing a CLI Across Python Versions in GitHub Actions",{"path":3638,"title":3639},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":3641,"title":3642},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":3644,"title":3645},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":3647,"title":3648},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":3650,"title":3651},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftemplate-variables-and-conditional-files","Template Variables and Conditional Files in CLI Templates",{"path":3653,"title":3654},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftesting-a-project-template-with-pytest","Testing a CLI Project Template with pytest",{"path":3656,"title":3657},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fupdating-generated-projects-with-copier-update","Updating Generated CLI Projects with copier update",{"path":3659,"title":3660},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":3662,"title":3663},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":3665,"title":3666},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fcode-signing-and-notarizing-cli-binaries","Code Signing and Notarizing Python CLI Binaries",{"path":3668,"title":3669},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":3671,"title":3672},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":3674,"title":3675},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":3677,"title":3678},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Freducing-pyinstaller-binary-size","Reducing the Size of a PyInstaller CLI Binary",{"path":3680,"title":3681},"\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":3683,"title":3684},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":3686,"title":3687},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":3689,"title":3690},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fenforcing-import-boundaries-in-a-cli-codebase","Enforcing Import Boundaries in a Python CLI Codebase",{"path":3692,"title":3693},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ffinding-unused-code-and-dependencies-with-vulture-and-deptry","Finding Unused Code and Dependencies with vulture and deptry",{"path":3695,"title":3696},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":3698,"title":3699},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-pyright-in-strict-mode-on-a-cli","Running Pyright in Strict Mode on a Python CLI",{"path":3701,"title":3702},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-ruff-and-mypy-in-ci-with-annotations","Running Ruff and mypy in CI with Inline Annotations",{"path":3704,"title":3705},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ftype-checking-click-and-typer-code-with-mypy","Type-Checking Click and Typer Code with mypy",{"path":3707,"title":3708},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":3710,"title":3711},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project","Bumping Versions Consistently Across a Python CLI Project",{"path":3713,"title":3714},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs","Deriving CLI Versions from Git Tags with hatch-vcs",{"path":3716,"title":3717},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":3719,"title":3720},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":3722,"title":3723},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":3725,"title":3726},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fshipping-pre-releases-and-release-candidates","Shipping Pre-Releases and Release Candidates of a Python CLI",{"path":3728,"title":3729},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":3731,"title":3732},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":3734,"title":3735},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fchoosing-a-build-backend-for-a-python-cli","Choosing a Build Backend for a Python CLI",{"path":3737,"title":3738},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":3740,"title":3741},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":3743,"title":3744},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Foptional-dependencies-and-extras-for-clis","Optional Dependencies and Extras for Python CLIs",{"path":3746,"title":3747},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":3749,"title":3750},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fwriting-pyproject-toml-metadata-for-a-cli","Writing pyproject.toml Metadata for a Python CLI",{"path":3752,"title":3753},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fdynamic-versioning-with-poetry-plugins","Dynamic Versioning for a Poetry CLI from Git Tags",{"path":3755,"title":3756},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":3758,"title":3759},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmanaging-poetry-lock-files-for-cli-tools","Managing Poetry Lock Files for CLI Tools",{"path":3761,"title":3762},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmigrating-a-cli-from-poetry-to-uv","Migrating a Python CLI from Poetry to uv",{"path":3764,"title":3765},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":3767,"title":3768},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":3770,"title":3771},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpublishing-a-cli-with-poetry","Building and Publishing a Python CLI with Poetry",{"path":3773,"title":3774},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":3776,"title":3777},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fkeeping-hook-versions-current-with-autoupdate","Keeping pre-commit Hook Versions Current with autoupdate",{"path":3779,"title":3780},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Frunning-pre-commit-in-ci","Running pre-commit in CI for a Python CLI Repository",{"path":3782,"title":3783},"\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":3785,"title":3786},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fshipping-your-cli-as-a-pre-commit-hook","Shipping Your Python CLI as a pre-commit Hook",{"path":3788,"title":3789},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fspeeding-up-slow-pre-commit-hooks","Speeding Up Slow pre-commit Hooks in a CLI Repository",{"path":3791,"title":3792},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python","Writing Local pre-commit Hooks in Python",{"path":3794,"title":3795},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fauditing-dependencies-with-pip-audit","Auditing a Python CLI’s Dependencies with pip-audit",{"path":3797,"title":3798},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fgenerating-an-sbom-for-a-python-cli","Generating an SBOM for a Python CLI Release",{"path":3800,"title":3801},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain","Securing the Supply Chain of a Python CLI",{"path":3803,"title":3804},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes","Pinning a Python CLI’s Dependencies with Hashes",{"path":3806,"title":3807},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases","Publishing Attestations and Verifying Python CLI Releases",{"path":3809,"title":3810},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fbuilding-and-publishing-a-cli-with-uv","Building and Publishing a Python CLI with uv",{"path":3812,"title":3813},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":3815,"title":3816},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Flocking-and-syncing-cli-dependencies-with-uv","Locking and Syncing a Python CLI’s Dependencies with uv",{"path":3818,"title":3819},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Frunning-one-off-cli-scripts-with-uv-run","Running One-Off CLI Scripts with uv run and PEP 723",{"path":3821,"title":3822},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fusing-private-package-indexes-with-uv","Using Private Package Indexes with uv for Internal CLIs",{"path":3824,"title":3825},"\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":3827,"title":3828},"\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":3830,"title":3831},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":3833,"title":3834},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fdebugging-wrong-python-and-wrong-venv-problems","Debugging Wrong-Python and Wrong-Venv Problems in CLIs",{"path":3836,"title":3837},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fexternally-managed-environments-and-pep-668","PEP 668 and Python CLIs: the externally-managed-environment Error",{"path":3839,"title":3840},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":3842,"title":3843},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fisolating-cli-tools-from-project-dependencies","Isolating CLI Tools from Your Project’s Dependencies",{"path":3845,"title":3846},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":3848,"title":3849},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":3851,"title":3852},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1790967540148]