[{"data":1,"prerenderedAt":1168},["ShallowReactive",2],{"page-\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fpatching-decorators-applied-at-import-time\u002F":3},{"id":4,"title":5,"body":6,"description":1132,"extension":1133,"meta":1134,"navigation":104,"path":1164,"seo":1165,"stem":1166,"__hash__":1167},"content\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fpatching-decorators-applied-at-import-time\u002Findex.md","Patching Decorators Applied at Import Time",{"type":7,"value":8,"toc":1122},"minimark",[9,30,35,63,67,70,76,160,185,196,225,228,239,288,295,301,351,504,508,528,670,674,677,813,816,840,844,910,914,917,927,943,953,962,1034,1038,1044,1053,1065,1079,1083,1112,1118],[10,11,12,13,17,18,21,22,25,26,29],"p",{},"The test patches ",[14,15,16],"code",{},"myapp.cache.cached"," and the function still caches. Nothing is wrong with the patch — it is simply too late. ",[14,19,20],{},"@cached"," ran when ",[14,23,24],{},"myapp.services"," was imported, which happened when the test module imported the function under test, which happened before any ",[14,27,28],{},"patch"," call in the test body. The name now points at a wrapper that closed over the original implementation, and rebinding the decorator changes nothing about the wrapper that already exists.",[31,32,34],"h2",{"id":33},"prerequisites","Prerequisites",[36,37,38,54],"ul",{},[39,40,41,45,46,49,50,53],"li",{},[42,43,44],"strong",{},"Python 3.8+","; ",[14,47,48],{},"functools.wraps"," sets ",[14,51,52],{},"__wrapped__",", which several of the strategies below rely on.",[39,55,56,57,62],{},"The binding model from ",[58,59,61],"a",{"href":60},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fwhere-to-patch-understanding-mock-patch-targets\u002F","where to patch",".",[31,64,66],{"id":65},"solution","Solution",[10,68,69],{},"Four strategies, ordered by how much they change. The first two are test-only; the last two change the code, and are the ones worth arguing for.",[10,71,72,75],{},[42,73,74],{},"1. Patch what the wrapper delegates to."," Most decorators call something at runtime — a cache backend, a metrics client, a retry policy. That call happens per invocation, so it is patchable long after decoration:",[77,78,83],"pre",{"className":79,"code":80,"language":81,"meta":82,"style":82},"language-python shiki shiki-themes github-light github-dark","# myapp\u002Fcache.py\nimport functools\n\ndef cached(fn):\n    @functools.wraps(fn)\n    def wrapper(*args, **kwargs):\n        key = (fn.__name__, args, tuple(sorted(kwargs.items())))\n        if key in BACKEND:                    # read at CALL time, not at decoration\n            return BACKEND[key]\n        BACKEND[key] = result = fn(*args, **kwargs)\n        return result\n    return wrapper\n","python","",[14,84,85,93,99,106,112,118,124,130,136,142,148,154],{"__ignoreMap":82},[86,87,90],"span",{"class":88,"line":89},"line",1,[86,91,92],{},"# myapp\u002Fcache.py\n",[86,94,96],{"class":88,"line":95},2,[86,97,98],{},"import functools\n",[86,100,102],{"class":88,"line":101},3,[86,103,105],{"emptyLinePlaceholder":104},true,"\n",[86,107,109],{"class":88,"line":108},4,[86,110,111],{},"def cached(fn):\n",[86,113,115],{"class":88,"line":114},5,[86,116,117],{},"    @functools.wraps(fn)\n",[86,119,121],{"class":88,"line":120},6,[86,122,123],{},"    def wrapper(*args, **kwargs):\n",[86,125,127],{"class":88,"line":126},7,[86,128,129],{},"        key = (fn.__name__, args, tuple(sorted(kwargs.items())))\n",[86,131,133],{"class":88,"line":132},8,[86,134,135],{},"        if key in BACKEND:                    # read at CALL time, not at decoration\n",[86,137,139],{"class":88,"line":138},9,[86,140,141],{},"            return BACKEND[key]\n",[86,143,145],{"class":88,"line":144},10,[86,146,147],{},"        BACKEND[key] = result = fn(*args, **kwargs)\n",[86,149,151],{"class":88,"line":150},11,[86,152,153],{},"        return result\n",[86,155,157],{"class":88,"line":156},12,[86,158,159],{},"    return wrapper\n",[77,161,163],{"className":79,"code":162,"language":81,"meta":82,"style":82},"# The test never touches the decorator; it swaps the backend the wrapper reads.\ndef test_cache_miss_calls_through(monkeypatch):\n    monkeypatch.setattr(\"myapp.cache.BACKEND\", {})   # empty backend, per test\n    assert services.expensive(2) == 4\n",[14,164,165,170,175,180],{"__ignoreMap":82},[86,166,167],{"class":88,"line":89},[86,168,169],{},"# The test never touches the decorator; it swaps the backend the wrapper reads.\n",[86,171,172],{"class":88,"line":95},[86,173,174],{},"def test_cache_miss_calls_through(monkeypatch):\n",[86,176,177],{"class":88,"line":101},[86,178,179],{},"    monkeypatch.setattr(\"myapp.cache.BACKEND\", {})   # empty backend, per test\n",[86,181,182],{"class":88,"line":108},[86,183,184],{},"    assert services.expensive(2) == 4\n",[10,186,187,192,193,195],{},[42,188,189,190,62],{},"2. Reach through ",[14,191,52],{}," ",[14,194,48],{}," copies the original onto the wrapper, so the undecorated function is still available for a direct test:",[77,197,199],{"className":79,"code":198,"language":81,"meta":82,"style":82},"from myapp.services import expensive\n\ndef test_pure_logic_without_the_decorator():\n    raw = expensive.__wrapped__          # the undecorated function\n    assert raw(2) == 4                   # no caching, no retries, no metrics\n",[14,200,201,206,210,215,220],{"__ignoreMap":82},[86,202,203],{"class":88,"line":89},[86,204,205],{},"from myapp.services import expensive\n",[86,207,208],{"class":88,"line":95},[86,209,105],{"emptyLinePlaceholder":104},[86,211,212],{"class":88,"line":101},[86,213,214],{},"def test_pure_logic_without_the_decorator():\n",[86,216,217],{"class":88,"line":108},[86,218,219],{},"    raw = expensive.__wrapped__          # the undecorated function\n",[86,221,222],{"class":88,"line":114},[86,223,224],{},"    assert raw(2) == 4                   # no caching, no retries, no metrics\n",[10,226,227],{},"This tests the logic honestly and skips the decorator entirely, which is exactly right when the decorator is infrastructure and the logic is the subject.",[10,229,230,233,234,238],{},[42,231,232],{},"3. Patch, then reload."," When the decorator's ",[235,236,237],"em",{},"behaviour at decoration time"," is the thing under test — a registry decorator, a route decorator — the module has to be re-imported with the patch in place:",[77,240,242],{"className":79,"code":241,"language":81,"meta":82,"style":82},"import importlib\nfrom unittest.mock import patch\nimport myapp.services\n\ndef test_registration_uses_the_patched_registry():\n    with patch(\"myapp.registry.REGISTRY\", new={}) as registry:\n        importlib.reload(myapp.services)          # decoration runs again, patched\n        assert \"expensive\" in registry\n    importlib.reload(myapp.services)              # restore the real registration\n",[14,243,244,249,254,259,263,268,273,278,283],{"__ignoreMap":82},[86,245,246],{"class":88,"line":89},[86,247,248],{},"import importlib\n",[86,250,251],{"class":88,"line":95},[86,252,253],{},"from unittest.mock import patch\n",[86,255,256],{"class":88,"line":101},[86,257,258],{},"import myapp.services\n",[86,260,261],{"class":88,"line":108},[86,262,105],{"emptyLinePlaceholder":104},[86,264,265],{"class":88,"line":114},[86,266,267],{},"def test_registration_uses_the_patched_registry():\n",[86,269,270],{"class":88,"line":120},[86,271,272],{},"    with patch(\"myapp.registry.REGISTRY\", new={}) as registry:\n",[86,274,275],{"class":88,"line":126},[86,276,277],{},"        importlib.reload(myapp.services)          # decoration runs again, patched\n",[86,279,280],{"class":88,"line":132},[86,281,282],{},"        assert \"expensive\" in registry\n",[86,284,285],{"class":88,"line":138},[86,286,287],{},"    importlib.reload(myapp.services)              # restore the real registration\n",[10,289,290,291,294],{},"Treat this as a last resort. Reloading rebinds every object in the module while other modules keep references to the old ones, so ",[14,292,293],{},"isinstance"," checks across the boundary start failing and any module-level state is rebuilt.",[10,296,297,300],{},[42,298,299],{},"4. Move the decision inside the wrapper."," The durable fix: a decorator that reads its configuration at call time needs no patching at all.",[77,302,304],{"className":79,"code":303,"language":81,"meta":82,"style":82},"def retry(attempts=None):\n    def decorate(fn):\n        @functools.wraps(fn)\n        def wrapper(*args, **kwargs):\n            # Resolved per call, so a test can change settings without re-decorating.\n            n = attempts if attempts is not None else settings.get(\"retry_attempts\", 3)\n            ...\n        return wrapper\n    return decorate\n",[14,305,306,311,316,321,326,331,336,341,346],{"__ignoreMap":82},[86,307,308],{"class":88,"line":89},[86,309,310],{},"def retry(attempts=None):\n",[86,312,313],{"class":88,"line":95},[86,314,315],{},"    def decorate(fn):\n",[86,317,318],{"class":88,"line":101},[86,319,320],{},"        @functools.wraps(fn)\n",[86,322,323],{"class":88,"line":108},[86,324,325],{},"        def wrapper(*args, **kwargs):\n",[86,327,328],{"class":88,"line":114},[86,329,330],{},"            # Resolved per call, so a test can change settings without re-decorating.\n",[86,332,333],{"class":88,"line":120},[86,334,335],{},"            n = attempts if attempts is not None else settings.get(\"retry_attempts\", 3)\n",[86,337,338],{"class":88,"line":126},[86,339,340],{},"            ...\n",[86,342,343],{"class":88,"line":132},[86,344,345],{},"        return wrapper\n",[86,347,348],{"class":88,"line":138},[86,349,350],{},"    return decorate\n",[352,353,356,500],"figure",{"className":354},[355],"diagram",[357,358,365,366,365,370,365,374,365,382,365,391,365,397,365,404,365,411,365,417,365,422,365,427,365,434,365,438,365,442,365,447,365,451,365,455,365,460,365,464,365,467,365,471,365,474,365,477,365,480,365,484,365,487,365,491,365,494,365,497],"svg",{"viewBox":359,"role":360,"ariaLabelledBy":361,"xmlns":364},"0 0 760 268","img",[362,363],"decortime-t","decortime-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[367,368,369],"title",{"id":362},"Why the patch arrives too late",[371,372,373],"desc",{"id":363},"A timeline of decoration and patching: the module is imported, the decorator runs and builds a wrapper holding a reference to the original function, the test then patches the decorator name, and the already-built wrapper continues to be what every caller invokes.",[375,376],"rect",{"x":377,"y":377,"width":378,"height":379,"rx":380,"fill":381},"0","760","268","14","#fffdf8",[383,384,369],"text",{"x":385,"y":386,"textAnchor":387,"fontSize":388,"fontWeight":389,"fill":390},"380","30","middle","15","700","#3d405b",[88,392],{"x1":393,"y1":394,"x2":395,"y2":394,"stroke":390,"strokeWidth":396},"40","150","720","2",[375,398],{"x":399,"y":400,"width":401,"height":402,"rx":403,"fill":390},"35","136","10","28","3",[375,405],{"x":406,"y":407,"width":408,"height":409,"rx":401,"fill":381,"stroke":390,"strokeWidth":410},"12","50","209","72","1.8",[383,412,416],{"x":413,"y":414,"textAnchor":387,"fontSize":415,"fontWeight":389,"fill":390},"116","74","11","import",[383,418,421],{"x":413,"y":419,"textAnchor":387,"fontSize":420,"fontWeight":389,"fill":390},"89","12.5","module loads",[383,423,426],{"x":413,"y":424,"textAnchor":387,"fontSize":425,"fill":390},"106","10.5","decorator is looked up",[88,428],{"x1":393,"y1":429,"x2":393,"y2":430,"stroke":390,"strokeWidth":431,"strokeDashArray":432},"124","134","1.4",[433,403],"4",[375,435],{"x":436,"y":400,"width":401,"height":402,"rx":403,"fill":437},"262","#81b29a",[375,439],{"x":440,"y":441,"width":408,"height":409,"rx":401,"fill":381,"stroke":437,"strokeWidth":410},"162","176",[383,443,446],{"x":444,"y":445,"textAnchor":387,"fontSize":415,"fontWeight":389,"fill":390},"267","200","decoration",[383,448,450],{"x":444,"y":449,"textAnchor":387,"fontSize":420,"fontWeight":389,"fill":390},"215","wrapper built",[383,452,454],{"x":444,"y":453,"textAnchor":387,"fontSize":425,"fill":390},"232","closes over the original",[88,456],{"x1":444,"y1":457,"x2":444,"y2":458,"stroke":437,"strokeWidth":431,"strokeDashArray":459},"166","174",[433,403],[375,461],{"x":462,"y":400,"width":401,"height":402,"rx":403,"fill":463},"488","#e07a5f",[375,465],{"x":466,"y":407,"width":408,"height":409,"rx":401,"fill":381,"stroke":463,"strokeWidth":410},"389",[383,468,470],{"x":469,"y":414,"textAnchor":387,"fontSize":415,"fontWeight":389,"fill":390},"493","test patches",[383,472,473],{"x":469,"y":419,"textAnchor":387,"fontSize":420,"fontWeight":389,"fill":390},"the name only",[383,475,476],{"x":469,"y":424,"textAnchor":387,"fontSize":425,"fill":390},"wrapper is unaffected",[88,478],{"x1":469,"y1":429,"x2":469,"y2":430,"stroke":463,"strokeWidth":431,"strokeDashArray":479},[433,403],[375,481],{"x":482,"y":400,"width":401,"height":402,"rx":403,"fill":483},"715","#f2cc8f",[375,485],{"x":486,"y":441,"width":408,"height":409,"rx":401,"fill":381,"stroke":483,"strokeWidth":410},"539",[383,488,490],{"x":489,"y":445,"textAnchor":387,"fontSize":415,"fontWeight":389,"fill":390},"644","call",[383,492,493],{"x":489,"y":449,"textAnchor":387,"fontSize":420,"fontWeight":389,"fill":390},"wrapper runs",[383,495,496],{"x":489,"y":453,"textAnchor":387,"fontSize":425,"fill":390},"exactly as before",[88,498],{"x1":395,"y1":457,"x2":395,"y2":458,"stroke":483,"strokeWidth":431,"strokeDashArray":499},[433,403],[501,502,503],"figcaption",{},"Decoration is a one-time event at import; patching the decorator afterwards rebinds a name nobody will look up again.",[31,505,507],{"id":506},"why-this-works","Why this works",[10,509,510,513,514,517,518,521,522,524,525,527],{},[14,511,512],{},"@decorator"," is syntax for ",[14,515,516],{},"fn = decorator(fn)"," executed at module import. The result is a new function object bound to the module namespace, holding the original in a closure cell. Nothing re-evaluates that expression later, so the decorator name is irrelevant after import — which is why strategies one and four work: both target something the ",[235,519,520],{},"wrapper"," consults while running, rather than something the decorator consulted while building it. ",[14,523,52],{}," works for the opposite reason: ",[14,526,48],{}," deliberately keeps a reference to the pre-decoration function so tooling can reach it.",[352,529,531,667],{"className":530},[355],[357,532,365,537,365,540,365,543,365,575,365,578,365,580,365,585,365,589,365,593,365,598,365,604,365,610,365,614,365,617,365,621,365,625,365,629,365,632,365,635,365,638,365,641,365,645,365,650,365,653,365,656,365,659,365,662],{"viewBox":533,"role":360,"ariaLabelledBy":534,"xmlns":364},"0 0 760 332",[535,536],"decorstrat-t","decorstrat-d",[367,538,539],{"id":535},"Choosing a strategy for an import-time decorator",[371,541,542],{"id":536},"A decision diagram: if the wrapper reads something at call time, patch that; if the logic alone is under test, use the wrapped attribute; if decoration-time behaviour is the subject, reload the module with the patch in place.",[544,545,546,547,546,560,546,565,546,570,365],"defs",{},"\n    ",[548,549,556],"marker",{"id":550,"viewBox":551,"refX":552,"refY":553,"markerWidth":554,"markerHeight":554,"orient":555},"decorstrat-a","0 0 10 10","9","5","7","auto-start-reverse",[557,558],"path",{"d":559,"fill":390},"M0 0 L10 5 L0 10 z",[548,561,563],{"id":562,"viewBox":551,"refX":552,"refY":553,"markerWidth":554,"markerHeight":554,"orient":555},"decorstrat-c",[557,564],{"d":559,"fill":463},[548,566,568],{"id":567,"viewBox":551,"refX":552,"refY":553,"markerWidth":554,"markerHeight":554,"orient":555},"decorstrat-s",[557,569],{"d":559,"fill":437},[548,571,573],{"id":572,"viewBox":551,"refX":552,"refY":553,"markerWidth":554,"markerHeight":554,"orient":555},"decorstrat-g",[557,574],{"d":559,"fill":483},[375,576],{"x":377,"y":377,"width":378,"height":577,"rx":380,"fill":381},"332",[383,579,539],{"x":385,"y":386,"textAnchor":387,"fontSize":388,"fontWeight":389,"fill":390},[581,582],"polygon",{"points":583,"fill":584,"stroke":390,"strokeWidth":410},"380,50 590,108 380,166 170,108","#f4f1de",[383,586,588],{"x":385,"y":587,"textAnchor":387,"fontSize":406,"fontWeight":389,"fill":390},"104","What is actually",[383,590,592],{"x":385,"y":591,"textAnchor":387,"fontSize":406,"fontWeight":389,"fill":390},"121","under test?",[557,594],{"d":595,"fill":596,"stroke":437,"strokeWidth":410,"markerEnd":597},"M380 166 L380 192 L133 192 L133 206","none","url(#decorstrat-s)",[383,599,603],{"x":600,"y":601,"textAnchor":387,"fontSize":415,"fontWeight":389,"fill":602},"133","196","#2a5f49","the wrapped logic",[375,605],{"x":606,"y":607,"width":608,"height":609,"rx":406,"fill":381,"stroke":437,"strokeWidth":396},"24","214","217","84",[383,611,613],{"x":600,"y":612,"textAnchor":387,"fontSize":420,"fontWeight":389,"fill":390},"245","use __wrapped__",[383,615,616],{"x":600,"y":436,"textAnchor":387,"fontSize":415,"fill":390},"skip the decorator",[383,618,620],{"x":600,"y":619,"textAnchor":387,"fontSize":415,"fill":390},"277","test pure behaviour",[557,622],{"d":623,"fill":596,"stroke":483,"strokeWidth":410,"markerEnd":624},"M380 166 L380 192 L380 192 L380 206","url(#decorstrat-g)",[383,626,628],{"x":385,"y":601,"textAnchor":387,"fontSize":415,"fontWeight":389,"fill":627},"#8a5a00","runtime behaviour",[375,630],{"x":631,"y":607,"width":608,"height":609,"rx":406,"fill":381,"stroke":483,"strokeWidth":396},"271",[383,633,634],{"x":385,"y":612,"textAnchor":387,"fontSize":420,"fontWeight":389,"fill":390},"patch what it reads",[383,636,637],{"x":385,"y":436,"textAnchor":387,"fontSize":415,"fill":390},"backend, clock, settings",[383,639,640],{"x":385,"y":619,"textAnchor":387,"fontSize":415,"fill":390},"no reload needed",[557,642],{"d":643,"fill":596,"stroke":463,"strokeWidth":410,"markerEnd":644},"M380 166 L380 192 L627 192 L627 206","url(#decorstrat-c)",[383,646,649],{"x":647,"y":601,"textAnchor":387,"fontSize":415,"fontWeight":389,"fill":648},"627","#8f3d22","decoration itself",[375,651],{"x":652,"y":607,"width":608,"height":609,"rx":406,"fill":381,"stroke":463,"strokeWidth":396},"519",[383,654,655],{"x":647,"y":612,"textAnchor":387,"fontSize":420,"fontWeight":389,"fill":390},"patch then reload",[383,657,658],{"x":647,"y":436,"textAnchor":387,"fontSize":415,"fill":390},"registry, routes",[383,660,661],{"x":647,"y":619,"textAnchor":387,"fontSize":415,"fill":390},"last resort",[383,663,666],{"x":385,"y":664,"textAnchor":387,"fontSize":415,"fontStyle":665,"fill":390},"320","italic","A decorator that reads configuration at call time never needs any of this.",[501,668,669],{},"Only the third branch needs a reload, and only when the act of decorating is the behaviour being verified.",[31,671,673],{"id":672},"testing-the-decorator-itself","Testing the decorator itself",[10,675,676],{},"A decorator is a function that returns a function, and it can be tested directly without importing anything it decorates. That test is usually clearer than any test of decorated code, and it belongs next to the decorator rather than next to its users.",[77,678,680],{"className":79,"code":679,"language":81,"meta":82,"style":82},"import functools\nimport pytest\nfrom myapp.cache import cached\n\ndef test_cached_calls_through_once():\n    calls = []\n\n    @cached                            # applied here, inside the test, on our terms\n    def add(a, b):\n        calls.append((a, b))\n        return a + b\n\n    assert add(1, 2) == 3\n    assert add(1, 2) == 3\n    assert calls == [(1, 2)]           # the second call was served from cache\n\ndef test_cached_preserves_metadata():\n    @cached\n    def documented(x):\n        \"\"\"Adds nothing.\"\"\"\n        return x\n\n    assert documented.__name__ == \"documented\"      # functools.wraps applied\n    assert documented.__doc__ == \"Adds nothing.\"\n    assert documented.__wrapped__(5) == 5           # original reachable\n",[14,681,682,686,691,696,700,705,710,714,719,724,729,734,738,744,749,755,760,766,772,778,784,790,795,801,807],{"__ignoreMap":82},[86,683,684],{"class":88,"line":89},[86,685,98],{},[86,687,688],{"class":88,"line":95},[86,689,690],{},"import pytest\n",[86,692,693],{"class":88,"line":101},[86,694,695],{},"from myapp.cache import cached\n",[86,697,698],{"class":88,"line":108},[86,699,105],{"emptyLinePlaceholder":104},[86,701,702],{"class":88,"line":114},[86,703,704],{},"def test_cached_calls_through_once():\n",[86,706,707],{"class":88,"line":120},[86,708,709],{},"    calls = []\n",[86,711,712],{"class":88,"line":126},[86,713,105],{"emptyLinePlaceholder":104},[86,715,716],{"class":88,"line":132},[86,717,718],{},"    @cached                            # applied here, inside the test, on our terms\n",[86,720,721],{"class":88,"line":138},[86,722,723],{},"    def add(a, b):\n",[86,725,726],{"class":88,"line":144},[86,727,728],{},"        calls.append((a, b))\n",[86,730,731],{"class":88,"line":150},[86,732,733],{},"        return a + b\n",[86,735,736],{"class":88,"line":156},[86,737,105],{"emptyLinePlaceholder":104},[86,739,741],{"class":88,"line":740},13,[86,742,743],{},"    assert add(1, 2) == 3\n",[86,745,747],{"class":88,"line":746},14,[86,748,743],{},[86,750,752],{"class":88,"line":751},15,[86,753,754],{},"    assert calls == [(1, 2)]           # the second call was served from cache\n",[86,756,758],{"class":88,"line":757},16,[86,759,105],{"emptyLinePlaceholder":104},[86,761,763],{"class":88,"line":762},17,[86,764,765],{},"def test_cached_preserves_metadata():\n",[86,767,769],{"class":88,"line":768},18,[86,770,771],{},"    @cached\n",[86,773,775],{"class":88,"line":774},19,[86,776,777],{},"    def documented(x):\n",[86,779,781],{"class":88,"line":780},20,[86,782,783],{},"        \"\"\"Adds nothing.\"\"\"\n",[86,785,787],{"class":88,"line":786},21,[86,788,789],{},"        return x\n",[86,791,793],{"class":88,"line":792},22,[86,794,105],{"emptyLinePlaceholder":104},[86,796,798],{"class":88,"line":797},23,[86,799,800],{},"    assert documented.__name__ == \"documented\"      # functools.wraps applied\n",[86,802,804],{"class":88,"line":803},24,[86,805,806],{},"    assert documented.__doc__ == \"Adds nothing.\"\n",[86,808,810],{"class":88,"line":809},25,[86,811,812],{},"    assert documented.__wrapped__(5) == 5           # original reachable\n",[10,814,815],{},"Applying the decorator inside the test sidesteps the entire import-time problem, because decoration happens when the test runs. Combined with strategy one for the integration path, this covers both halves: the decorator's own semantics here, and the decorated function's behaviour where it is used.",[10,817,818,819,821,822,825,826,825,829,832,833,835,836,839],{},"The metadata assertions deserve a place in that suite. A decorator that forgets ",[14,820,48],{}," breaks ",[14,823,824],{},"__name__",", ",[14,827,828],{},"__doc__",[14,830,831],{},"__module__"," and ",[14,834,52],{},", which in turn breaks pytest's own introspection, ",[14,837,838],{},"inspect.signature",", and every other test strategy on this page. One assertion catches it permanently.",[31,841,843],{"id":842},"edge-cases-and-failure-modes","Edge cases and failure modes",[36,845,846,855,864,877,887,900],{},[39,847,848,851,852,854],{},[42,849,850],{},"Class decorators and metaclasses have the same timing"," but no ",[14,853,52],{}," convention, so strategy two is unavailable; prefer strategies one and four.",[39,856,857,863],{},[42,858,859,862],{},[14,860,861],{},"@pytest.fixture"," is itself a decorator applied at import."," Patching something a fixture's decorator consulted at definition time has the same problem, and the answer is the same: read it inside the fixture body.",[39,865,866,869,870,872,873,876],{},[42,867,868],{},"Stacked decorators build nested wrappers",", so ",[14,871,52],{}," reaches one level down, not to the original. Walk the chain with ",[14,874,875],{},"inspect.unwrap"," when several are applied.",[39,878,879,882,883,886],{},[42,880,881],{},"Reloading a module that defines exception classes"," creates new class objects, so ",[14,884,885],{},"except OldError"," in an unreloaded module no longer catches the new one — a particularly confusing symptom of strategy three.",[39,888,889,895,896,62],{},[42,890,891,894],{},[14,892,893],{},"lru_cache"," on a method holds instances alive"," for the process lifetime; clearing it between tests is required for isolation and is a leak source in production, as covered in ",[58,897,899],{"href":898},"\u002Fsystematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc\u002Ffinding-memory-leaks-with-tracemalloc-snapshots\u002F","finding memory leaks with tracemalloc snapshots",[39,901,902,905,906,909],{},[42,903,904],{},"Import order decides whether a patch is early enough."," A ",[14,907,908],{},"conftest.py"," that imports the application at module scope makes every later patch too late; import inside fixtures instead.",[31,911,913],{"id":912},"a-checklist-for-decorator-heavy-codebases","A checklist for decorator-heavy codebases",[10,915,916],{},"When several decorators stack on every handler — auth, caching, metrics, retries — testing gets harder in proportion. Four conventions keep it tractable.",[10,918,919,922,923,926],{},[42,920,921],{},"Keep decorators thin and their dependencies late."," A decorator that reads ",[14,924,925],{},"settings"," or a client at call time is testable without ceremony; one that captures them at decoration time is not. This single rule removes most of the difficulty on this page.",[10,928,929,934,935,825,937,939,940,942],{},[42,930,931,932,62],{},"Always apply ",[14,933,48],{}," It costs one line and preserves ",[14,936,824],{},[14,938,828],{},", the signature and ",[14,941,52],{},", which every debugging and introspection tool depends on — including pytest's own fixture resolution when the decorator is applied to a fixture.",[10,944,945,948,949,952],{},[42,946,947],{},"Expose a bypass for tests."," A module-level ",[14,950,951],{},"DISABLED = False"," that each wrapper consults is crude but effective, and far safer than reloading modules. It also documents that the decorator has an off switch, which is useful in production incidents as well as in tests.",[10,954,955,958,959,961],{},[42,956,957],{},"Test the decorator and the decorated function separately."," The decorator's semantics get a focused unit test; the business logic gets tested through ",[14,960,52],{}," or through an undecorated inner function. Very little needs to test both at once, and the tests that do belong at the integration level where the whole stack is exercised deliberately.",[352,963,965,1031],{"className":964},[355],[357,966,365,971,365,974,365,977,365,979,365,981,365,985,365,988,365,992,365,997,365,1000,365,1002,365,1005,365,1008,365,1011,365,1013,365,1016,365,1019,365,1022,365,1024,365,1028],{"viewBox":967,"role":360,"ariaLabelledBy":968,"xmlns":364},"0 0 760 320",[969,970],"decorhabits-t","decorhabits-d",[367,972,973],{"id":969},"Four habits that keep decorated code testable",[371,975,976],{"id":970},"A stack of four conventions for decorator-heavy code: read dependencies at call time, always apply functools.wraps, expose a bypass switch, and test the decorator separately from the logic it wraps.",[375,978],{"x":377,"y":377,"width":378,"height":664,"rx":380,"fill":381},[383,980,973],{"x":385,"y":386,"textAnchor":387,"fontSize":388,"fontWeight":389,"fill":390},[375,982],{"x":386,"y":983,"width":389,"height":984,"rx":401,"fill":584,"stroke":437,"strokeWidth":410},"56","52",[375,986],{"x":386,"y":983,"width":987,"height":984,"rx":433,"fill":437},"8",[383,989,991],{"x":984,"y":990,"fontSize":420,"fontWeight":389,"fill":390},"86","read deps at call time",[383,993,996],{"x":994,"y":990,"textAnchor":995,"fontSize":415,"fill":390},"714","end","the one rule that removes most difficulty",[375,998],{"x":386,"y":999,"width":389,"height":984,"rx":401,"fill":381,"stroke":483,"strokeWidth":410},"120",[375,1001],{"x":386,"y":999,"width":987,"height":984,"rx":433,"fill":483},[383,1003,1004],{"x":984,"y":394,"fontSize":420,"fontWeight":389,"fill":390},"always functools.wraps",[383,1006,1007],{"x":994,"y":394,"textAnchor":995,"fontSize":415,"fill":390},"keeps __wrapped__, signature and introspection",[375,1009],{"x":386,"y":1010,"width":389,"height":984,"rx":401,"fill":584,"stroke":463,"strokeWidth":410},"184",[375,1012],{"x":386,"y":1010,"width":987,"height":984,"rx":433,"fill":463},[383,1014,1015],{"x":984,"y":607,"fontSize":420,"fontWeight":389,"fill":390},"expose a bypass",[383,1017,1018],{"x":994,"y":607,"textAnchor":995,"fontSize":415,"fill":390},"safer than reloading modules mid-session",[375,1020],{"x":386,"y":1021,"width":389,"height":984,"rx":401,"fill":381,"stroke":390,"strokeWidth":410},"248",[375,1023],{"x":386,"y":1021,"width":987,"height":984,"rx":433,"fill":390},[383,1025,1027],{"x":984,"y":1026,"fontSize":420,"fontWeight":389,"fill":390},"278","test the two separately",[383,1029,1030],{"x":994,"y":1026,"textAnchor":995,"fontSize":415,"fill":390},"decorator semantics, then business logic",[501,1032,1033],{},"Applied together these make import-time decoration a non-issue rather than a recurring workaround.",[31,1035,1037],{"id":1036},"frequently-asked-questions","Frequently Asked Questions",[10,1039,1040,1043],{},[42,1041,1042],{},"Why does patching a decorator have no effect?","\nBecause decoration happens once, at import. By the time the test patches the decorator name, the decorated function already exists as a wrapper built from the original, and the wrapper holds a direct reference to it.",[10,1045,1046,1049,1050,1052],{},[42,1047,1048],{},"Is importlib.reload a safe way to re-apply a patched decorator?","\nIt works but it is invasive: reloading rebinds the module's objects while other modules keep references to the old ones, so classes fail ",[14,1051,293],{}," checks across the boundary. Use it only for leaf modules with no importers.",[10,1054,1055,1058,1059,1061,1062,1064],{},[42,1056,1057],{},"Can I patch the function the decorator wraps instead?","\nOften yes. ",[14,1060,48],{}," preserves the original as ",[14,1063,52],{},", so patching what the wrapper delegates to changes behaviour without re-running the decorator.",[10,1066,1067,1070,1071,1074,1075,1078],{},[42,1068,1069],{},"What is the most maintainable fix?","\nMake the decorator's behaviour configurable at call time rather than at decoration time — read the flag, the clock or the client inside the wrapper, so a test can change it without re-decorating anything.\n",[42,1072,1073],{},"Can I stop a decorator from being applied during tests entirely?","\nYes, by making the decorator a no-op under a flag it reads at import: ",[14,1076,1077],{},"if os.environ.get(\"DISABLE_CACHE\"): return fn",". It works, and it changes what the tests exercise — the decorated path in production is then never covered. Prefer it only for decorators whose behaviour is genuinely orthogonal to the logic, such as metrics, and cover the decorator itself with its own unit test.",[31,1080,1082],{"id":1081},"related","Related",[36,1084,1085,1091,1098,1105],{},[39,1086,1087,1090],{},[58,1088,1089],{"href":60},"Where to patch: understanding mock.patch targets"," — the binding rules that make timing matter.",[39,1092,1093,1097],{},[58,1094,1096],{"href":1095},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002F","Patching strategies for complex codebases"," — the wider set of patching failure modes.",[39,1099,1100,1104],{},[58,1101,1103],{"href":1102},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdependency-injection-for-testability\u002Freplacing-singletons-and-module-globals-in-tests\u002F","Replacing singletons and module globals in tests"," — the same import-time problem, in data rather than in behaviour.",[39,1106,1107,1111],{},[58,1108,1110],{"href":1109},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fpatching-builtins-and-sys-modules-safely\u002F","Patching builtins and sys.modules safely"," — when the reload strategy needs its state restored.",[10,1113,1114,1115],{},"← Back to ",[58,1116,1117],{"href":1095},"Patching Strategies for Complex Codebases",[1119,1120,1121],"style",{},"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":82,"searchDepth":95,"depth":95,"links":1123},[1124,1125,1126,1127,1128,1129,1130,1131],{"id":33,"depth":95,"text":34},{"id":65,"depth":95,"text":66},{"id":506,"depth":95,"text":507},{"id":672,"depth":95,"text":673},{"id":842,"depth":95,"text":843},{"id":912,"depth":95,"text":913},{"id":1036,"depth":95,"text":1037},{"id":1081,"depth":95,"text":1082},"Patch a decorator that already wrapped its target at import: why the patch is too late, and the four workable strategies including reload, inner-target patching and injection.","md",{"slug":1135,"type":1136,"breadcrumb":1137,"datePublished":1138,"dateModified":1138,"faq":1139,"howto":1148},"patching-decorators-applied-at-import-time","article","Import-Time Decorators","2026-08-01",[1140,1142,1144,1146],{"q":1042,"a":1141},"Because decoration happens once, at import. By the time the test patches the decorator name, the decorated function already exists as a wrapper built from the original, and the wrapper holds a direct reference to it.",{"q":1048,"a":1143},"It works but it is invasive: reloading rebinds the module's objects while other modules keep references to the old ones, so classes fail isinstance checks across the boundary. Use it only for leaf modules with no importers.",{"q":1057,"a":1145},"Often yes. functools.wraps preserves the original as __wrapped__, so patching what the wrapper delegates to changes behaviour without re-running the decorator.",{"q":1069,"a":1147},"Make the decorator's behaviour configurable at call time rather than at decoration time — read the flag, the clock or the client inside the wrapper, so a test can change it without re-decorating anything.",{"name":1149,"description":1150,"steps":1151},"How to test code behind an import-time decorator","Work around decoration that has already happened, or remove the need to.",[1152,1155,1158,1161],{"name":1153,"text":1154},"Confirm the decoration already ran","Check that the module was imported before the patch was applied; a wrapper exists from that moment.",{"name":1156,"text":1157},"Patch inside the wrapper instead","Target the object the wrapper delegates to, or the configuration it reads at call time.",{"name":1159,"text":1160},"Reload the module only as a last resort","Reload after patching when the module has no importers holding references.",{"name":1162,"text":1163},"Make the decorator read its inputs at call time","Move the decision inside the wrapper so tests need no patching at all.","\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fpatching-decorators-applied-at-import-time",{"title":5,"description":1132},"advanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fpatching-decorators-applied-at-import-time\u002Findex","iXPbCj5STZwSblqYISSdjtIprwtlJGUzgULkw-Cyyg4",1785613403107]