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
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
1"""Utilities for monkey patching and wrapping object attributes."""
3import contextlib
4import importlib
5import inspect
6import sys
7import warnings
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
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.
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."""
30 _instance = None
32 def __new__(cls):
33 if cls._instance is None:
34 cls._instance = super().__new__(cls)
35 return cls._instance
37 def __repr__(self):
38 return "<wrapt.MISSING>"
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, ())
46MISSING = _MissingType()
48# Helper functions for applying wrappers to existing functions.
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 """
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.
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
80 parent = target
82 path = name.split(".")
83 attribute = path[0]
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.
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
114 original = lookup_attribute(parent, attribute)
116 for attribute in path[1:]:
117 parent = original
118 original = lookup_attribute(parent, attribute)
120 return (parent, attribute, original)
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 """
148 parent, attribute, original = resolve_path(target, name)
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)))
157 for candidate in candidates:
158 try:
159 if attribute in vars(candidate):
160 return (candidate, attribute, original)
161 except TypeError:
162 continue
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 )
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 """
177 setattr(parent, attribute, replacement)
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 """
197 if kwargs is None:
198 kwargs = {}
200 parent, attribute, original = resolve_path(target, name)
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.
215 try:
216 created = attribute not in vars(parent)
217 except TypeError:
218 created = False
220 wrapper = factory(original, *args, **kwargs)
222 try:
223 wrapper.__self_setattr__("__wrapt_wrap_object_created_slot__", created)
224 except AttributeError:
225 pass
227 apply_patch(parent, attribute, wrapper)
229 return wrapper
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.
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."""
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 {}
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.
262 if instance is None:
263 return self
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.
272 prior = self.__wrapped__
273 prior_type = type(prior)
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 )
291 return self._self_factory(value, *self._self_args, **self._self_kwargs)
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.
298 prior = self.__wrapped__
300 if hasattr(type(prior), "__set__"):
301 prior.__set__(instance, value)
302 else:
303 instance.__dict__[self._self_attribute] = value
305 def __delete__(self, instance):
306 prior = self.__wrapped__
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.
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
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 """
343 if kwargs is None:
344 kwargs = {}
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
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.
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 """
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)
381 return FunctionWrapper(wrapper, _wrapper)
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 """
400 if isinstance(target, str) and target.endswith("?"):
401 target = target[:-1]
403 if target in sys.modules:
404 return wrap_object(sys.modules[target], name, FunctionWrapper, (wrapper,))
406 def callback(module):
407 wrap_object(module, name, FunctionWrapper, (wrapper,))
409 register_post_import_hook(callback, target)
410 return None
412 return wrap_object(target, name, FunctionWrapper, (wrapper,))
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 """
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
448 if enabled is MISSING:
449 enabled = None
451 def _wrapper(wrapper):
452 if isinstance(target, str) and target.endswith("?"):
453 _target = target[:-1]
455 if _target in sys.modules:
456 wrap_object(
457 sys.modules[_target], name, FunctionWrapper, (wrapper, enabled)
458 )
459 return wrapper
461 def callback(module):
462 wrap_object(module, name, FunctionWrapper, (wrapper, enabled))
464 register_post_import_hook(callback, _target)
465 return wrapper
467 wrap_object(target, name, FunctionWrapper, (wrapper, enabled))
468 return wrapper
470 return _wrapper
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.
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 """
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))
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__.
522 replacement = wrap_object(
523 target, name, FunctionWrapper, (target_wrapper,)
524 )
526 try:
527 return wrapped(*args, **kwargs)
528 finally:
529 unwrap_object(target, name, replacement)
531 return FunctionWrapper(target_wrapped, _execute)
533 return FunctionWrapper(wrapper, _wrapper)
535 return _decorator
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 """
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 )
566 handle = wrap_function_wrapper(target, name, wrapper)
568 try:
569 yield
570 finally:
571 unwrap_object(target, name, handle)
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.
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 """
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.
603 seen = []
604 seen_ids = set()
606 current = obj
608 while True:
609 if id(current) in seen_ids:
610 return
612 if len(seen) >= limit:
613 raise WrapperChainTooDeepError(
614 f"wrapper chain of {obj!r} exceeded {limit} levels"
615 )
617 seen.append(current)
618 seen_ids.add(id(current))
620 yield current
622 try:
623 current = current.__wrapped__
624 except AttributeError:
625 return
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 """
638 result = obj
640 for result in wrapper_chain(obj, limit=limit):
641 pass
643 return result
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 """
665 if handle is None and predicate is None:
666 raise TypeError(
667 "find_wrapper() requires a wrapper handle or a predicate"
668 )
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
677 return None
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 """
692 return find_wrapper(obj, handle, predicate=predicate, limit=limit) is not None
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.
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.
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 """
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.
743 try:
744 current = resolve_path(target, name)[2]
745 except PathResolutionError:
746 current = None
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
755 if missing_ok:
756 return None
758 raise WrapperNotFoundError(
759 f"handle {handle!r} not found on {target!r}.{name}"
760 ) from exc
762 found = find_wrapper(current, handle)
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
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 )
780 if found is not current:
781 chain = list(wrapper_chain(current))
783 # The scan must use identity, since list.index() would use
784 # __eq__, which proxies delegate to the wrapped object.
786 position = next(
787 index for index, entry in enumerate(chain) if entry is found
788 )
789 neighbour = chain[position - 1]
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 )
798 neighbour.__wrapped__ = restored
799 return found
801 if restored is MISSING:
802 # The wrapper was installed where no prior definition existed.
803 delattr(owner, attribute)
804 return found
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.
815 try:
816 created = found.__self_dict__.get("__wrapt_wrap_object_created_slot__")
817 except AttributeError:
818 created = None
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
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.
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
850 apply_patch(owner, attribute, restored)
851 return found