Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/wrapt/patches.py: 14%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

269 statements  

1"""Utilities for monkey patching and wrapping object attributes.""" 

2 

3import contextlib 

4import importlib 

5import inspect 

6import sys 

7import warnings 

8 

9from .__wrapt__ import BaseObjectProxy, FunctionWrapper 

10from .exceptions import ( 

11 PathResolutionError, 

12 TargetModuleNotFoundError, 

13 WrapperChainTooDeepError, 

14 WrapperNotFoundError, 

15 WrapperNotOutermostError, 

16) 

17from .importer import register_post_import_hook 

18 

19# Sentinel used where the absence of a value must be distinguishable from 

20# None being supplied, or where an attribute having had no prior definition 

21# must be represented as a value. Exposed as public API since code walking 

22# wrapper chains needs to be able to test for it by identity, but only 

23# meaningful where wrapt itself checks for it. 

24 

25 

26class _MissingType: 

27 """The type of the MISSING sentinel, which marks the absence of a value 

28 or attribute definition where None is itself meaningful.""" 

29 

30 _instance = None 

31 

32 def __new__(cls): 

33 if cls._instance is None: 

34 cls._instance = super().__new__(cls) 

35 return cls._instance 

36 

37 def __repr__(self): 

38 return "<wrapt.MISSING>" 

39 

40 def __reduce__(self): 

41 # Pickle and copy resolve back to the singleton so identity 

42 # comparison against MISSING survives a round trip. 

43 return (_MissingType, ()) 

44 

45 

46MISSING = _MissingType() 

47 

48# Helper functions for applying wrappers to existing functions. 

49 

50 

51def resolve_path(target, name): 

52 """ 

53 Resolves the dotted path supplied as `name` to an attribute on a target 

54 object. The `target` can be a module, class, or instance of a class. If the 

55 `target` argument is a string, it is assumed to be the name of a module, 

56 which will be imported if necessary and then used as the target object. 

57 Returns a tuple containing the parent object holding the attribute lookup 

58 resolved to, the attribute name (path prefix removed if present), and the 

59 original attribute value. If the module cannot be imported, raises 

60 `TargetModuleNotFoundError`, and if the attribute path cannot be resolved, 

61 raises `PathResolutionError`, in both cases with the original exception 

62 preserved as the `__cause__` attribute. 

63 """ 

64 

65 if isinstance(target, str): 

66 # Use importlib.import_module() rather than __import__() as the 

67 # latter, even though it imports a dotted module name successfully 

68 # from the sys.modules cache, computes its return value by importing 

69 # the top level package, which fails for a module registered in 

70 # sys.modules whose parent package is not importable. 

71 

72 try: 

73 target = importlib.import_module(target) 

74 except ModuleNotFoundError as exc: 

75 raise TargetModuleNotFoundError( 

76 f"unable to import module {target!r} while resolving " 

77 f"the target for {name!r}" 

78 ) from exc 

79 

80 parent = target 

81 

82 path = name.split(".") 

83 attribute = path[0] 

84 

85 # We can't just always use getattr() because in doing 

86 # that on a class it will cause binding to occur which 

87 # will complicate things later and cause some things not 

88 # to work. For the case of a class we therefore access 

89 # the __dict__ directly. To cope though with the wrong 

90 # class being given to us, or a method being moved into 

91 # a base class, we need to walk the class hierarchy to 

92 # work out exactly which __dict__ the method was defined 

93 # in, as accessing it from __dict__ will fail if it was 

94 # not actually on the class given. Fallback to using 

95 # getattr() if we can't find it. If it truly doesn't 

96 # exist, then that will fail. 

97 

98 def lookup_attribute(parent, attribute): 

99 try: 

100 if inspect.isclass(parent): 

101 for cls in inspect.getmro(parent): 

102 if attribute in vars(cls): 

103 return vars(cls)[attribute] 

104 else: 

105 return getattr(parent, attribute) 

106 else: 

107 return getattr(parent, attribute) 

108 except AttributeError as exc: 

109 raise PathResolutionError( 

110 f"unable to resolve attribute {attribute!r} in path " 

111 f"{name!r} on {target!r}" 

112 ) from exc 

113 

114 original = lookup_attribute(parent, attribute) 

115 

116 for attribute in path[1:]: 

117 parent = original 

118 original = lookup_attribute(parent, attribute) 

119 

120 return (parent, attribute, original) 

121 

122 

123def resolve_owner(target, name): 

124 """ 

125 Sibling of the `resolve_path()` function, resolving the dotted path 

126 supplied as `name` on a target object in the same way and returning a 

127 tuple of the same shape, with one difference: the first element is the 

128 object whose `__dict__` actually defines the attribute, rather than 

129 the object the path resolved to. The two answer different questions. 

130 `resolve_path()` answers where to write so that lookups through the 

131 named object are affected, which is the correct location for 

132 installing a wrapper, and shadowing an inherited definition is 

133 legitimate there. `resolve_owner()` answers where the attribute 

134 physically lives, which is the correct location for removing a 

135 wrapper, since restoring anywhere other than the defining location 

136 would leave a shadowing copy behind. For a class target the owner is 

137 the defining class found by walking the MRO, for an instance target 

138 it is the instance itself if the attribute is in its `__dict__` and 

139 otherwise the defining class, and for a module target it is the 

140 module. For dotted paths the owner logic applies to the final segment 

141 only. An attribute served dynamically, such as by a module level or 

142 metaclass `__getattr__`, exists in no `__dict__`, and rather than 

143 guess, `PathResolutionError` is raised, where `resolve_path()` would 

144 return the value happily. Failures resolving the path itself raise 

145 exactly as for `resolve_path()`. 

146 """ 

147 

148 parent, attribute, original = resolve_path(target, name) 

149 

150 if inspect.isclass(parent): 

151 candidates = inspect.getmro(parent) 

152 elif inspect.ismodule(parent): 

153 candidates = (parent,) 

154 else: 

155 candidates = (parent,) + tuple(inspect.getmro(type(parent))) 

156 

157 for candidate in candidates: 

158 try: 

159 if attribute in vars(candidate): 

160 return (candidate, attribute, original) 

161 except TypeError: 

162 continue 

163 

164 raise PathResolutionError( 

165 f"attribute {attribute!r} of {parent!r} is not defined in any " 

166 f"__dict__; it is served dynamically, so there is no owning " 

167 f"location to patch" 

168 ) 

169 

170 

171def apply_patch(parent, attribute, replacement): 

172 """ 

173 Convenience function for applying a patch to an attribute. This maps to 

174 the standard setattr() function. 

175 """ 

176 

177 setattr(parent, attribute, replacement) 

178 

179 

180def wrap_object(target, name, factory, args=(), kwargs=None): 

181 """ 

182 Wraps an object which is the attribute of a target object with a wrapper 

183 object created by the `factory` function. The `target` can be a module, 

184 class, or instance of a class. In the special case of `target` being a 

185 string, it is assumed to be the name of a module, with the module being 

186 imported if necessary and then used as the target object. The `name` is a 

187 string representing the dotted path to the attribute. The `factory` function 

188 should accept the original object and may accept additional positional and 

189 keyword arguments which will be set by unpacking input arguments using 

190 `*args` and `**kwargs` calling conventions. The factory function should 

191 return a new object that will replace the original object, and should 

192 ordinarily be a `BaseObjectProxy` subclass or return an instance of 

193 one, so that the replacement can carry the installation details which 

194 `unwrap_object()` later relies upon to restore things faithfully. 

195 """ 

196 

197 if kwargs is None: 

198 kwargs = {} 

199 

200 parent, attribute, original = resolve_path(target, name) 

201 

202 # Record whether applying the patch creates the attribute slot on the 

203 # parent, or overwrites one which already existed. When the value was 

204 # being served from somewhere else, such as a base class via the MRO, 

205 # the class of an instance, or a dynamic __getattr__, the patch 

206 # creates a shadowing slot, and faithful removal is deleting that 

207 # slot again rather than assigning the original into it. This fact 

208 # only exists at installation time, so it is stashed on the wrapper 

209 # itself, in the local state of the proxy where lookups cannot be 

210 # confused with anything of the wrapped object's, for 

211 # unwrap_object() to consult. A factory result which is not a wrapt 

212 # proxy cannot carry it and unwrap_object() falls back to what can 

213 # be determined at removal time. 

214 

215 try: 

216 created = attribute not in vars(parent) 

217 except TypeError: 

218 created = False 

219 

220 wrapper = factory(original, *args, **kwargs) 

221 

222 try: 

223 wrapper.__self_setattr__("__wrapt_wrap_object_created_slot__", created) 

224 except AttributeError: 

225 pass 

226 

227 apply_patch(parent, attribute, wrapper) 

228 

229 return wrapper 

230 

231 

232# Function for applying a proxy object to an attribute of a class 

233# instance. The wrapper works by defining an attribute of the same name 

234# on the class which is a descriptor and which intercepts access to the 

235# instance attribute. The descriptor is itself an object proxy wrapping 

236# whatever previously occupied the class attribute, be that another 

237# AttributeWrapper, some other descriptor such as a property, a plain 

238# class default, or the MISSING sentinel when nothing was defined, so 

239# stacked applications compose rather than replace one another and the 

240# prior definition keeps working beneath the interception. 

241 

242 

243class AttributeWrapper(BaseObjectProxy): 

244 """A descriptor that intercepts access to an instance attribute to apply 

245 a wrapper factory. The descriptor is an object proxy whose wrapped object 

246 is whatever previously occupied the class attribute, or the MISSING 

247 sentinel when nothing did, so stacked applications compose and a prior 

248 descriptor keeps executing its own logic beneath the interception.""" 

249 

250 def __init__(self, wrapped, attribute, factory, args=(), kwargs=None): 

251 super(AttributeWrapper, self).__init__(wrapped) 

252 self._self_attribute = attribute 

253 self._self_factory = factory 

254 self._self_args = args 

255 self._self_kwargs = kwargs if kwargs is not None else {} 

256 

257 def __get__(self, instance, owner=None): 

258 # Class level access returns the descriptor itself. Being a 

259 # transparent proxy, introspection of the prior definition then 

260 # works through delegation. 

261 

262 if instance is None: 

263 return self 

264 

265 # Reads follow the standard attribute lookup precedence: a data 

266 # descriptor prior takes precedence over the instance 

267 # dictionary, a non-data descriptor prior yields to it, and a 

268 # plain class default is the fallback when no instance value 

269 # exists. Only when the prior is the MISSING sentinel, meaning 

270 # no definition of any sort existed, is AttributeError raised. 

271 

272 prior = self.__wrapped__ 

273 prior_type = type(prior) 

274 

275 if hasattr(prior_type, "__get__") and ( 

276 hasattr(prior_type, "__set__") or hasattr(prior_type, "__delete__") 

277 ): 

278 value = prior.__get__(instance, owner) 

279 elif self._self_attribute in instance.__dict__: 

280 value = instance.__dict__[self._self_attribute] 

281 elif hasattr(prior_type, "__get__"): 

282 value = prior.__get__(instance, owner) 

283 elif prior is not MISSING: 

284 value = prior 

285 else: 

286 raise AttributeError( 

287 f"{type(instance).__name__!r} object has no attribute " 

288 f"{self._self_attribute!r}" 

289 ) 

290 

291 return self._self_factory(value, *self._self_args, **self._self_kwargs) 

292 

293 def __set__(self, instance, value): 

294 # Writes delegate to a prior descriptor which implements 

295 # __set__, so its validation and storage are honoured, and 

296 # otherwise store into the instance dictionary. 

297 

298 prior = self.__wrapped__ 

299 

300 if hasattr(type(prior), "__set__"): 

301 prior.__set__(instance, value) 

302 else: 

303 instance.__dict__[self._self_attribute] = value 

304 

305 def __delete__(self, instance): 

306 prior = self.__wrapped__ 

307 

308 if hasattr(type(prior), "__delete__"): 

309 prior.__delete__(instance) 

310 else: 

311 # Match the exception deleting the attribute would raise if 

312 # the wrapper had not been applied, which is AttributeError 

313 # rather than the KeyError of the raw dictionary lookup. 

314 

315 try: 

316 del instance.__dict__[self._self_attribute] 

317 except KeyError: 

318 raise AttributeError( 

319 f"{type(instance).__name__!r} object has no attribute " 

320 f"{self._self_attribute!r}" 

321 ) from None 

322 

323 

324def wrap_object_attribute(module, name, factory, args=(), kwargs=None): 

325 """ 

326 Wraps an object which is the attribute of a class instance with a wrapper 

327 object created by the `factory` function. It does this by patching the 

328 class, not the instance, with a descriptor that intercepts access to the 

329 instance attribute. The `module` can be a module, class, or instance of a 

330 class. In the special case of `module` being a string, it is assumed to be 

331 the name of a module, with the module being imported if necessary and then 

332 used as the target object. The `name` is a string representing the dotted 

333 path to the attribute. The `factory` function should accept the original 

334 object and may accept additional positional and keyword arguments which will 

335 be set by unpacking input arguments using `*args` and `**kwargs` calling 

336 conventions. The factory function should return a new object that will 

337 replace the original object. Returns the `AttributeWrapper` descriptor 

338 installed on the class, which wraps whatever previously occupied the 

339 class attribute, or the `MISSING` sentinel when nothing did, so repeated 

340 applications compose rather than replace one another. 

341 """ 

342 

343 if kwargs is None: 

344 kwargs = {} 

345 

346 path, attribute = name.rsplit(".", 1) 

347 parent = resolve_path(module, path)[2] 

348 prior = vars(parent).get(attribute, MISSING) 

349 wrapper = AttributeWrapper(prior, attribute, factory, args, kwargs) 

350 apply_patch(parent, attribute, wrapper) 

351 return wrapper 

352 

353 

354# Functions for creating a simple decorator using a FunctionWrapper, 

355# plus short cut functions for applying wrappers to functions. These are 

356# for use when doing monkey patching. For a more featured way of 

357# creating decorators see the decorator decorator instead. 

358 

359 

360def function_wrapper(wrapper): 

361 """ 

362 Creates a decorator for wrapping a function with a `wrapper` function. 

363 The decorator which is returned may also be applied to any other callable 

364 objects such as lambda functions, methods, classmethods, and staticmethods, 

365 or objects which implement the `__call__()` method. The `wrapper` function 

366 should accept the `wrapped` function, `instance`, `args`, and `kwargs`, 

367 arguments and return the result of calling the wrapped function or some 

368 other appropriate value. 

369 """ 

370 

371 def _wrapper(wrapped, instance, args, kwargs): 

372 target_wrapped = args[0] 

373 if instance is None: 

374 target_wrapper = wrapper 

375 elif inspect.isclass(instance): 

376 target_wrapper = wrapper.__get__(None, instance) 

377 else: 

378 target_wrapper = wrapper.__get__(instance, type(instance)) 

379 return FunctionWrapper(target_wrapped, target_wrapper) 

380 

381 return FunctionWrapper(wrapper, _wrapper) 

382 

383 

384def wrap_function_wrapper(target, name, wrapper): 

385 """ 

386 Wraps a function which is the attribute of a target object with a `wrapper` 

387 function. The `target` can be a module, class, or instance of a class. In 

388 the special case of `target` being a string, it is assumed to be the name 

389 of a module, with the module being imported if necessary. If the `target` 

390 is a string with a trailing ``?``, the wrapping will be deferred until the 

391 module is imported. If the module is already imported, the wrapping will be 

392 applied immediately. The `name` is a string representing the dotted path to 

393 the attribute. The `wrapper` function should accept the `wrapped` function, 

394 `instance`, `args`, and `kwargs` arguments, and would return the result of 

395 calling the wrapped attribute or some other appropriate value. Returns the 

396 wrapped target function if the wrapping was applied immediately, or ``None`` 

397 if the wrapping was deferred. 

398 """ 

399 

400 if isinstance(target, str) and target.endswith("?"): 

401 target = target[:-1] 

402 

403 if target in sys.modules: 

404 return wrap_object(sys.modules[target], name, FunctionWrapper, (wrapper,)) 

405 

406 def callback(module): 

407 wrap_object(module, name, FunctionWrapper, (wrapper,)) 

408 

409 register_post_import_hook(callback, target) 

410 return None 

411 

412 return wrap_object(target, name, FunctionWrapper, (wrapper,)) 

413 

414 

415def patch_function_wrapper(target, name, _enabled=MISSING, *, enabled=MISSING): 

416 """ 

417 Creates a decorator which can be applied to a wrapper function, where the 

418 wrapper function will be used to wrap a function which is the attribute of 

419 a target object. The decorator returns the original wrapper function. The 

420 `target` can be a module, class, or instance of a class. In the special case 

421 of `target` being a string, it is assumed to be the name of a module, with 

422 the module being imported if necessary. If the `target` is a string with a 

423 trailing ``?``, the wrapping will be deferred until the module is imported. 

424 If the module is already imported, the wrapping will be applied immediately. 

425 The `name` is a string representing the dotted path to the attribute. The 

426 `enabled` argument is keyword only and can be a boolean or a callable that 

427 returns a boolean. When a callable is provided, it will be called each time 

428 the wrapper is invoked to determine if the wrapper function should be 

429 executed or whether the wrapped function should be called directly. If 

430 `enabled` is not provided, or an explicit value of `None` is supplied, 

431 the wrapper is enabled by default. 

432 """ 

433 

434 if _enabled is not MISSING: 

435 if enabled is not MISSING: 

436 raise TypeError( 

437 "patch_function_wrapper() got multiple values for " "argument 'enabled'" 

438 ) 

439 warnings.warn( 

440 "Passing 'enabled' positionally to patch_function_wrapper() is " 

441 "deprecated and will be an error in a future version of wrapt; " 

442 "pass it as a keyword argument.", 

443 DeprecationWarning, 

444 stacklevel=2, 

445 ) 

446 enabled = _enabled 

447 

448 if enabled is MISSING: 

449 enabled = None 

450 

451 def _wrapper(wrapper): 

452 if isinstance(target, str) and target.endswith("?"): 

453 _target = target[:-1] 

454 

455 if _target in sys.modules: 

456 wrap_object( 

457 sys.modules[_target], name, FunctionWrapper, (wrapper, enabled) 

458 ) 

459 return wrapper 

460 

461 def callback(module): 

462 wrap_object(module, name, FunctionWrapper, (wrapper, enabled)) 

463 

464 register_post_import_hook(callback, _target) 

465 return wrapper 

466 

467 wrap_object(target, name, FunctionWrapper, (wrapper, enabled)) 

468 return wrapper 

469 

470 return _wrapper 

471 

472 

473def transient_function_wrapper(target, name): 

474 """Creates a decorator that patches a target function with a wrapper 

475 function, but only for the duration of the call that the decorator was 

476 applied to. The `target` can be a module, class, or instance of a class. 

477 In the special case of `target` being a string, it is assumed to be the name 

478 of a module, with the module being imported if necessary. The `name` is a 

479 string representing the dotted path to the attribute. The patch is applied 

480 to the object the path resolves to, so patching an attribute reached 

481 through inheritance, or through an instance, only affects lookups made 

482 through that object and not the class where the attribute is defined. In 

483 that case the temporary attribute which shadowed the inherited definition 

484 is removed again when the call exits, restoring the original lookup. 

485 

486 The temporary wrapper is removed using `unwrap_object()`, so if code 

487 called within the scope of the patch wrapped over the top of the 

488 temporary wrapper with a wrapt wrapper and left it there, the temporary 

489 wrapper is spliced out beneath it, with the other wrapper left in 

490 place. If something removed or replaced the temporary wrapper during 

491 the call, `WrapperNotFoundError` is raised, and if what was applied on 

492 top of it is not a wrapt wrapper and so cannot be spliced past, 

493 `WrapperNotOutermostError` is raised. Both are loud deliberately, as 

494 they indicate the surrounding code, typically a test harness, is not 

495 managing its own patches properly, and the leaked patch state would 

496 otherwise surface as hard to diagnose failures later. 

497 """ 

498 

499 def _decorator(wrapper): 

500 def _wrapper(wrapped, instance, args, kwargs): 

501 target_wrapped = args[0] 

502 if instance is None: 

503 target_wrapper = wrapper 

504 elif inspect.isclass(instance): 

505 target_wrapper = wrapper.__get__(None, instance) 

506 else: 

507 target_wrapper = wrapper.__get__(instance, type(instance)) 

508 

509 def _execute(wrapped, instance, args, kwargs): 

510 # The wrap and unwrap functions handle all the details: 

511 # wrap_object() records whether applying the patch created 

512 # a shadowing attribute slot, and unwrap_object() consults 

513 # that record to restore or remove the attribute 

514 # faithfully, splices the temporary wrapper out from 

515 # beneath any wrapt wrapper left applied on top of it, and 

516 # raises if the temporary wrapper was removed, replaced, 

517 # or pinned beneath a non wrapt wrapper. An exception 

518 # raised on exit supersedes any in-flight exception from 

519 # the wrapped call, which remains visible as the chained 

520 # __context__. 

521 

522 replacement = wrap_object( 

523 target, name, FunctionWrapper, (target_wrapper,) 

524 ) 

525 

526 try: 

527 return wrapped(*args, **kwargs) 

528 finally: 

529 unwrap_object(target, name, replacement) 

530 

531 return FunctionWrapper(target_wrapped, _execute) 

532 

533 return FunctionWrapper(wrapper, _wrapper) 

534 

535 return _decorator 

536 

537 

538@contextlib.contextmanager 

539def scoped_function_wrapper(target, name, wrapper): 

540 """Returns a context manager which patches a target function with a 

541 wrapper function for the duration of a with statement, the block scoped 

542 counterpart of the `transient_function_wrapper()` decorator. The 

543 arguments take the same form as for `wrap_function_wrapper()`: the 

544 `target` can be a module, class, or instance of a class, or the name of 

545 a module as a string, the `name` is a string representing the dotted 

546 path to the attribute, and the `wrapper` function should accept the 

547 `wrapped`, `instance`, `args`, and `kwargs` arguments. The deferred 

548 form of the `target` with a trailing ``?`` is not supported, since the 

549 patch must be applied at the point the with statement is entered. The 

550 context manager yields nothing, is single use, and removes the wrapper 

551 using `unwrap_object()` when the block exits, with the same behaviour 

552 as `transient_function_wrapper()` when something interfered with the 

553 patch during the block: a wrapt wrapper applied on top of the temporary 

554 wrapper and left there is tolerated, with the temporary wrapper spliced 

555 out beneath it, while the temporary wrapper having been removed or 

556 replaced raises `WrapperNotFoundError`, and a non wrapt wrapper left 

557 applied on top raises `WrapperNotOutermostError`. 

558 """ 

559 

560 if isinstance(target, str) and target.endswith("?"): 

561 raise ValueError( 

562 "deferred wrapping using a target with a trailing '?' cannot " 

563 "be used with scoped_function_wrapper()" 

564 ) 

565 

566 handle = wrap_function_wrapper(target, name, wrapper) 

567 

568 try: 

569 yield 

570 finally: 

571 unwrap_object(target, name, handle) 

572 

573 

574# Functions for introspecting chains of wrappers, linked by each wrapper 

575# holding the object it wraps as the __wrapped__ attribute. The chain 

576# protocol is shared by wrapt proxies and wrappers, functions decorated 

577# using functools.wraps(), and anything else honouring the convention. 

578 

579 

580def wrapper_chain(obj, *, limit=64): 

581 """ 

582 Returns an iterator yielding `obj`, then each successive object found 

583 by following the `__wrapped__` attribute, outermost wrapper first. The 

584 final item yielded is the innermost object of the chain, which is not 

585 itself a wrapper. Traversal ends cleanly at an object with no 

586 `__wrapped__` attribute, or upon returning to an object already seen. 

587 If the `limit` on the number of items yielded is reached with a further 

588 chain link still pending, `WrapperChainTooDeepError` is raised, as a 

589 truncated scan would otherwise be indistinguishable from a complete 

590 one. A chain of exactly `limit` items which ends naturally is not an 

591 error. Note that reading `__wrapped__` from a lazy object proxy will 

592 cause it to materialize, and an exception raised by a broken proxy or 

593 a lazy object factory will propagate to the caller. 

594 """ 

595 

596 # Cycle detection must use identity, not equality or a set of the 

597 # objects themselves, since proxies delegate __eq__ and __hash__ to 

598 # the wrapped object. Objects seen are therefore tracked by id(), 

599 # with strong references also held so no visited object can be 

600 # garbage collected and have its id reused while the scan runs, ids 

601 # being unique among simultaneously live objects. 

602 

603 seen = [] 

604 seen_ids = set() 

605 

606 current = obj 

607 

608 while True: 

609 if id(current) in seen_ids: 

610 return 

611 

612 if len(seen) >= limit: 

613 raise WrapperChainTooDeepError( 

614 f"wrapper chain of {obj!r} exceeded {limit} levels" 

615 ) 

616 

617 seen.append(current) 

618 seen_ids.add(id(current)) 

619 

620 yield current 

621 

622 try: 

623 current = current.__wrapped__ 

624 except AttributeError: 

625 return 

626 

627 

628def unwrapped(obj, *, limit=64): 

629 """ 

630 Returns the innermost object of the chain of wrappers followed from 

631 `obj` by the `wrapper_chain()` function, or `obj` itself when it is not 

632 wrapped. Shares the full contract of `wrapper_chain()`, including 

633 raising `WrapperChainTooDeepError` when the scan is indeterminate, 

634 rather than returning a mid chain wrapper as if it were the innermost 

635 object. 

636 """ 

637 

638 result = obj 

639 

640 for result in wrapper_chain(obj, limit=limit): 

641 pass 

642 

643 return result 

644 

645 

646def find_wrapper(obj, handle=None, *, predicate=None, limit=64): 

647 """ 

648 Scans the chain of wrappers followed from `obj` by the 

649 `wrapper_chain()` function for a specific wrapper and returns it, or 

650 `None` when it is not present. The `handle` argument is the wrapper 

651 object to look for, as returned by the wrap functions when the wrapper 

652 was installed, and is matched by object identity only, never equality, 

653 which proxies delegate to the wrapped object. Alternatively a 

654 `predicate` function may be supplied, in which case the first chain 

655 entry for which it returns true is returned, and is itself usable as a 

656 handle thereafter. When both are supplied, both must match. At least 

657 one of the two must be supplied, since testing for the mere presence 

658 of any wrapper is fragile: were the target library to adopt wrapt for 

659 its own purposes, such a test would wrongly conclude a wrapper of 

660 yours was installed. Shares the full contract of `wrapper_chain()`, 

661 including raising `WrapperChainTooDeepError` when the scan is 

662 indeterminate, rather than returning `None` as a false negative. 

663 """ 

664 

665 if handle is None and predicate is None: 

666 raise TypeError( 

667 "find_wrapper() requires a wrapper handle or a predicate" 

668 ) 

669 

670 for entry in wrapper_chain(obj, limit=limit): 

671 if handle is not None and entry is not handle: 

672 continue 

673 if predicate is not None and not predicate(entry): 

674 continue 

675 return entry 

676 

677 return None 

678 

679 

680def is_wrapped_by(obj, handle=None, *, predicate=None, limit=64): 

681 """ 

682 Boolean convenience form of the `find_wrapper()` function, returning 

683 whether the wrapper is present in the chain of wrappers followed from 

684 `obj`. With a `handle`, this answers whether the wrapper it was 

685 returned for when installed is still in place, which is the check to 

686 run when a third party may have replaced the attribute wholesale. 

687 Shares the full contract of `find_wrapper()`, including raising 

688 `WrapperChainTooDeepError` when the scan is indeterminate, rather 

689 than returning `False` as a false negative. 

690 """ 

691 

692 return find_wrapper(obj, handle, predicate=predicate, limit=limit) is not None 

693 

694 

695def unwrap_object(target, name, handle, *, missing_ok=False): 

696 """ 

697 Removes a wrapper which was installed on an attribute of a target 

698 object, the inverse of the `wrap_object()` function and the removal 

699 call for every wrap form, `wrap_function_wrapper()` and 

700 `wrap_object_attribute()` included. The `target` and `name` arguments 

701 take the same form as for `resolve_path()`. The `handle` argument is 

702 the wrapper object which was returned when the wrapper was installed, 

703 and is matched by object identity only. Note that it is not the 

704 wrapper function: passing the wrapper function surfaces immediately 

705 as `WrapperNotFoundError`, since a function is never a chain entry. 

706 

707 When the wrapper is found and is outermost, the attribute is restored 

708 to the object the wrapper wrapped, at the location where the attribute 

709 is actually defined per `resolve_owner()`, so removal through a 

710 subclass restores the defining base class rather than leaving a 

711 shadowing copy. If restoring would merely shadow the identical object 

712 already reachable through the MRO, or the wrapper was installed where 

713 no prior definition existed (a `MISSING` terminal), the attribute is 

714 instead removed so no residue is left behind. When the wrapper is 

715 found buried beneath other wrapt wrappers, it is spliced out of the 

716 chain in place, without touching the attribute or disturbing the 

717 wrappers above it. When what sits directly above it is not a wrapt 

718 wrapper, such as a plain `functools.wraps()` closure whose 

719 `__wrapped__` is only metadata, `WrapperNotOutermostError` is raised 

720 naming what is above, since splicing there would silently not take 

721 effect. 

722 

723 When the wrapper is not found, because the attribute was never 

724 wrapped, the wrapper was already removed, or a third party replaced 

725 the attribute wholesale, `WrapperNotFoundError` is raised by default. 

726 Passing `missing_ok=True` returns `None` instead, for shutdown paths 

727 which must tolerate third party interference. In either case nothing 

728 is mutated. The wrapper which was removed is returned. Note that 

729 `missing_ok` does not suppress `WrapperChainTooDeepError` from the 

730 underlying scan, since an indeterminate scan is not the same thing as 

731 the wrapper being gone. 

732 """ 

733 

734 try: 

735 owner, attribute, current = resolve_owner(target, name) 

736 except PathResolutionError as exc: 

737 # The attribute is either wholly absent or served dynamically 

738 # with no owning location. In neither case is anything of the 

739 # caller's statically installed, so both are the not-found 

740 # case, except when the wrapper is present in the dynamically 

741 # served value, where removal is simply not possible. 

742 

743 try: 

744 current = resolve_path(target, name)[2] 

745 except PathResolutionError: 

746 current = None 

747 

748 if current is not None and find_wrapper(current, handle) is not None: 

749 raise WrapperNotOutermostError( 

750 f"cannot remove {type(handle).__name__} from " 

751 f"{target!r}.{name}: the attribute is served dynamically " 

752 f"and has no owning location to restore" 

753 ) from exc 

754 

755 if missing_ok: 

756 return None 

757 

758 raise WrapperNotFoundError( 

759 f"handle {handle!r} not found on {target!r}.{name}" 

760 ) from exc 

761 

762 found = find_wrapper(current, handle) 

763 

764 if found is not None: 

765 try: 

766 restored = found.__wrapped__ 

767 except AttributeError: 

768 # The terminal original object was matched, which the chain 

769 # also yields; not being a wrapper, it is not a legitimate 

770 # handle. 

771 found = None 

772 

773 if found is None: 

774 if missing_ok: 

775 return None 

776 raise WrapperNotFoundError( 

777 f"handle {handle!r} not found on {target!r}.{name}" 

778 ) 

779 

780 if found is not current: 

781 chain = list(wrapper_chain(current)) 

782 

783 # The scan must use identity, since list.index() would use 

784 # __eq__, which proxies delegate to the wrapped object. 

785 

786 position = next( 

787 index for index, entry in enumerate(chain) if entry is found 

788 ) 

789 neighbour = chain[position - 1] 

790 

791 if not issubclass(type(neighbour), BaseObjectProxy): 

792 above = [type(entry).__name__ for entry in chain[:position]] 

793 raise WrapperNotOutermostError( 

794 f"cannot remove {type(found).__name__} from " 

795 f"{target!r}.{name}: wrapped by {above}" 

796 ) 

797 

798 neighbour.__wrapped__ = restored 

799 return found 

800 

801 if restored is MISSING: 

802 # The wrapper was installed where no prior definition existed. 

803 delattr(owner, attribute) 

804 return found 

805 

806 # When wrap_object() installed the wrapper, it recorded on the 

807 # wrapper itself whether applying the patch created the attribute 

808 # slot. That fact is authoritative: since the wrapper is still 

809 # installed and outermost here, the slot cannot have been touched 

810 # since it was recorded. It must be read from the proxy's own local 

811 # state only, since ordinary attribute access would delegate a 

812 # missing entry to the wrapped object and could answer with an 

813 # inner wrapper's record instead. 

814 

815 try: 

816 created = found.__self_dict__.get("__wrapt_wrap_object_created_slot__") 

817 except AttributeError: 

818 created = None 

819 

820 if created is not None: 

821 if created: 

822 # Installing the wrapper created a shadowing slot, so 

823 # removing the attribute reinstates the original lookup, 

824 # be that through the MRO, the class of an instance, or a 

825 # dynamic __getattr__, rather than leaving a permanent 

826 # copy of the original behind. 

827 delattr(owner, attribute) 

828 else: 

829 apply_patch(owner, attribute, restored) 

830 return found 

831 

832 # The wrapper does not carry the installation record, so fall back 

833 # to what can be determined at removal time: if removing the 

834 # attribute from a class would re-expose the identical restored 

835 # object through the MRO, the wrap must have created a shadow. 

836 

837 if inspect.isclass(owner): 

838 inherited = next( 

839 ( 

840 vars(cls)[attribute] 

841 for cls in inspect.getmro(owner)[1:] 

842 if attribute in vars(cls) 

843 ), 

844 None, 

845 ) 

846 if inherited is restored: 

847 delattr(owner, attribute) 

848 return found 

849 

850 apply_patch(owner, attribute, restored) 

851 return found