simbiot-harness/venv/lib/python3.12/site-packages/wrapt/patches.py
Simbiot Harness b60439152b Simbiot Harness v0.1: тактовая лестница на Strands + CQRS/ES-спина
- harness/es.py: append-only event store + проекции (образец — кооп-портал)
- harness/levels.py: уровни 1с/1м/1ч/1д, контекст ∝ масштабу
- harness/scheduler.py: уровневые потоки, идемпотентные тики, absorbed-стабы
- harness/isolated_runner.py + _llm_worker.py: LLM-тик в сабпроцессе
  (глобальный asyncio-рантайм Strands не переживает 2+ create_harness)
- cli.py: init/task/tasks/status/events + requeue (написан самим минутным агентом)
- 9router: отдельный ключ sk-simbiot, модель auto (glm-5.3 первым)
2026-09-29 15:28:50 +00:00

851 lines
36 KiB
Python

"""Utilities for monkey patching and wrapping object attributes."""
import contextlib
import importlib
import inspect
import sys
import warnings
from .__wrapt__ import BaseObjectProxy, FunctionWrapper
from .exceptions import (
PathResolutionError,
TargetModuleNotFoundError,
WrapperChainTooDeepError,
WrapperNotFoundError,
WrapperNotOutermostError,
)
from .importer import register_post_import_hook
# Sentinel used where the absence of a value must be distinguishable from
# None being supplied, or where an attribute having had no prior definition
# must be represented as a value. Exposed as public API since code walking
# wrapper chains needs to be able to test for it by identity, but only
# meaningful where wrapt itself checks for it.
class _MissingType:
"""The type of the MISSING sentinel, which marks the absence of a value
or attribute definition where None is itself meaningful."""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __repr__(self):
return "<wrapt.MISSING>"
def __reduce__(self):
# Pickle and copy resolve back to the singleton so identity
# comparison against MISSING survives a round trip.
return (_MissingType, ())
MISSING = _MissingType()
# Helper functions for applying wrappers to existing functions.
def resolve_path(target, name):
"""
Resolves the dotted path supplied as `name` to an attribute on a target
object. The `target` can be a module, class, or instance of a class. If the
`target` argument is a string, it is assumed to be the name of a module,
which will be imported if necessary and then used as the target object.
Returns a tuple containing the parent object holding the attribute lookup
resolved to, the attribute name (path prefix removed if present), and the
original attribute value. If the module cannot be imported, raises
`TargetModuleNotFoundError`, and if the attribute path cannot be resolved,
raises `PathResolutionError`, in both cases with the original exception
preserved as the `__cause__` attribute.
"""
if isinstance(target, str):
# Use importlib.import_module() rather than __import__() as the
# latter, even though it imports a dotted module name successfully
# from the sys.modules cache, computes its return value by importing
# the top level package, which fails for a module registered in
# sys.modules whose parent package is not importable.
try:
target = importlib.import_module(target)
except ModuleNotFoundError as exc:
raise TargetModuleNotFoundError(
f"unable to import module {target!r} while resolving "
f"the target for {name!r}"
) from exc
parent = target
path = name.split(".")
attribute = path[0]
# We can't just always use getattr() because in doing
# that on a class it will cause binding to occur which
# will complicate things later and cause some things not
# to work. For the case of a class we therefore access
# the __dict__ directly. To cope though with the wrong
# class being given to us, or a method being moved into
# a base class, we need to walk the class hierarchy to
# work out exactly which __dict__ the method was defined
# in, as accessing it from __dict__ will fail if it was
# not actually on the class given. Fallback to using
# getattr() if we can't find it. If it truly doesn't
# exist, then that will fail.
def lookup_attribute(parent, attribute):
try:
if inspect.isclass(parent):
for cls in inspect.getmro(parent):
if attribute in vars(cls):
return vars(cls)[attribute]
else:
return getattr(parent, attribute)
else:
return getattr(parent, attribute)
except AttributeError as exc:
raise PathResolutionError(
f"unable to resolve attribute {attribute!r} in path "
f"{name!r} on {target!r}"
) from exc
original = lookup_attribute(parent, attribute)
for attribute in path[1:]:
parent = original
original = lookup_attribute(parent, attribute)
return (parent, attribute, original)
def resolve_owner(target, name):
"""
Sibling of the `resolve_path()` function, resolving the dotted path
supplied as `name` on a target object in the same way and returning a
tuple of the same shape, with one difference: the first element is the
object whose `__dict__` actually defines the attribute, rather than
the object the path resolved to. The two answer different questions.
`resolve_path()` answers where to write so that lookups through the
named object are affected, which is the correct location for
installing a wrapper, and shadowing an inherited definition is
legitimate there. `resolve_owner()` answers where the attribute
physically lives, which is the correct location for removing a
wrapper, since restoring anywhere other than the defining location
would leave a shadowing copy behind. For a class target the owner is
the defining class found by walking the MRO, for an instance target
it is the instance itself if the attribute is in its `__dict__` and
otherwise the defining class, and for a module target it is the
module. For dotted paths the owner logic applies to the final segment
only. An attribute served dynamically, such as by a module level or
metaclass `__getattr__`, exists in no `__dict__`, and rather than
guess, `PathResolutionError` is raised, where `resolve_path()` would
return the value happily. Failures resolving the path itself raise
exactly as for `resolve_path()`.
"""
parent, attribute, original = resolve_path(target, name)
if inspect.isclass(parent):
candidates = inspect.getmro(parent)
elif inspect.ismodule(parent):
candidates = (parent,)
else:
candidates = (parent,) + tuple(inspect.getmro(type(parent)))
for candidate in candidates:
try:
if attribute in vars(candidate):
return (candidate, attribute, original)
except TypeError:
continue
raise PathResolutionError(
f"attribute {attribute!r} of {parent!r} is not defined in any "
f"__dict__; it is served dynamically, so there is no owning "
f"location to patch"
)
def apply_patch(parent, attribute, replacement):
"""
Convenience function for applying a patch to an attribute. This maps to
the standard setattr() function.
"""
setattr(parent, attribute, replacement)
def wrap_object(target, name, factory, args=(), kwargs=None):
"""
Wraps an object which is the attribute of a target object with a wrapper
object created by the `factory` function. The `target` can be a module,
class, or instance of a class. In the special case of `target` being a
string, it is assumed to be the name of a module, with the module being
imported if necessary and then used as the target object. The `name` is a
string representing the dotted path to the attribute. The `factory` function
should accept the original object and may accept additional positional and
keyword arguments which will be set by unpacking input arguments using
`*args` and `**kwargs` calling conventions. The factory function should
return a new object that will replace the original object, and should
ordinarily be a `BaseObjectProxy` subclass or return an instance of
one, so that the replacement can carry the installation details which
`unwrap_object()` later relies upon to restore things faithfully.
"""
if kwargs is None:
kwargs = {}
parent, attribute, original = resolve_path(target, name)
# Record whether applying the patch creates the attribute slot on the
# parent, or overwrites one which already existed. When the value was
# being served from somewhere else, such as a base class via the MRO,
# the class of an instance, or a dynamic __getattr__, the patch
# creates a shadowing slot, and faithful removal is deleting that
# slot again rather than assigning the original into it. This fact
# only exists at installation time, so it is stashed on the wrapper
# itself, in the local state of the proxy where lookups cannot be
# confused with anything of the wrapped object's, for
# unwrap_object() to consult. A factory result which is not a wrapt
# proxy cannot carry it and unwrap_object() falls back to what can
# be determined at removal time.
try:
created = attribute not in vars(parent)
except TypeError:
created = False
wrapper = factory(original, *args, **kwargs)
try:
wrapper.__self_setattr__("__wrapt_wrap_object_created_slot__", created)
except AttributeError:
pass
apply_patch(parent, attribute, wrapper)
return wrapper
# Function for applying a proxy object to an attribute of a class
# instance. The wrapper works by defining an attribute of the same name
# on the class which is a descriptor and which intercepts access to the
# instance attribute. The descriptor is itself an object proxy wrapping
# whatever previously occupied the class attribute, be that another
# AttributeWrapper, some other descriptor such as a property, a plain
# class default, or the MISSING sentinel when nothing was defined, so
# stacked applications compose rather than replace one another and the
# prior definition keeps working beneath the interception.
class AttributeWrapper(BaseObjectProxy):
"""A descriptor that intercepts access to an instance attribute to apply
a wrapper factory. The descriptor is an object proxy whose wrapped object
is whatever previously occupied the class attribute, or the MISSING
sentinel when nothing did, so stacked applications compose and a prior
descriptor keeps executing its own logic beneath the interception."""
def __init__(self, wrapped, attribute, factory, args=(), kwargs=None):
super(AttributeWrapper, self).__init__(wrapped)
self._self_attribute = attribute
self._self_factory = factory
self._self_args = args
self._self_kwargs = kwargs if kwargs is not None else {}
def __get__(self, instance, owner=None):
# Class level access returns the descriptor itself. Being a
# transparent proxy, introspection of the prior definition then
# works through delegation.
if instance is None:
return self
# Reads follow the standard attribute lookup precedence: a data
# descriptor prior takes precedence over the instance
# dictionary, a non-data descriptor prior yields to it, and a
# plain class default is the fallback when no instance value
# exists. Only when the prior is the MISSING sentinel, meaning
# no definition of any sort existed, is AttributeError raised.
prior = self.__wrapped__
prior_type = type(prior)
if hasattr(prior_type, "__get__") and (
hasattr(prior_type, "__set__") or hasattr(prior_type, "__delete__")
):
value = prior.__get__(instance, owner)
elif self._self_attribute in instance.__dict__:
value = instance.__dict__[self._self_attribute]
elif hasattr(prior_type, "__get__"):
value = prior.__get__(instance, owner)
elif prior is not MISSING:
value = prior
else:
raise AttributeError(
f"{type(instance).__name__!r} object has no attribute "
f"{self._self_attribute!r}"
)
return self._self_factory(value, *self._self_args, **self._self_kwargs)
def __set__(self, instance, value):
# Writes delegate to a prior descriptor which implements
# __set__, so its validation and storage are honoured, and
# otherwise store into the instance dictionary.
prior = self.__wrapped__
if hasattr(type(prior), "__set__"):
prior.__set__(instance, value)
else:
instance.__dict__[self._self_attribute] = value
def __delete__(self, instance):
prior = self.__wrapped__
if hasattr(type(prior), "__delete__"):
prior.__delete__(instance)
else:
# Match the exception deleting the attribute would raise if
# the wrapper had not been applied, which is AttributeError
# rather than the KeyError of the raw dictionary lookup.
try:
del instance.__dict__[self._self_attribute]
except KeyError:
raise AttributeError(
f"{type(instance).__name__!r} object has no attribute "
f"{self._self_attribute!r}"
) from None
def wrap_object_attribute(module, name, factory, args=(), kwargs=None):
"""
Wraps an object which is the attribute of a class instance with a wrapper
object created by the `factory` function. It does this by patching the
class, not the instance, with a descriptor that intercepts access to the
instance attribute. The `module` can be a module, class, or instance of a
class. In the special case of `module` being a string, it is assumed to be
the name of a module, with the module being imported if necessary and then
used as the target object. The `name` is a string representing the dotted
path to the attribute. The `factory` function should accept the original
object and may accept additional positional and keyword arguments which will
be set by unpacking input arguments using `*args` and `**kwargs` calling
conventions. The factory function should return a new object that will
replace the original object. Returns the `AttributeWrapper` descriptor
installed on the class, which wraps whatever previously occupied the
class attribute, or the `MISSING` sentinel when nothing did, so repeated
applications compose rather than replace one another.
"""
if kwargs is None:
kwargs = {}
path, attribute = name.rsplit(".", 1)
parent = resolve_path(module, path)[2]
prior = vars(parent).get(attribute, MISSING)
wrapper = AttributeWrapper(prior, attribute, factory, args, kwargs)
apply_patch(parent, attribute, wrapper)
return wrapper
# Functions for creating a simple decorator using a FunctionWrapper,
# plus short cut functions for applying wrappers to functions. These are
# for use when doing monkey patching. For a more featured way of
# creating decorators see the decorator decorator instead.
def function_wrapper(wrapper):
"""
Creates a decorator for wrapping a function with a `wrapper` function.
The decorator which is returned may also be applied to any other callable
objects such as lambda functions, methods, classmethods, and staticmethods,
or objects which implement the `__call__()` method. The `wrapper` function
should accept the `wrapped` function, `instance`, `args`, and `kwargs`,
arguments and return the result of calling the wrapped function or some
other appropriate value.
"""
def _wrapper(wrapped, instance, args, kwargs):
target_wrapped = args[0]
if instance is None:
target_wrapper = wrapper
elif inspect.isclass(instance):
target_wrapper = wrapper.__get__(None, instance)
else:
target_wrapper = wrapper.__get__(instance, type(instance))
return FunctionWrapper(target_wrapped, target_wrapper)
return FunctionWrapper(wrapper, _wrapper)
def wrap_function_wrapper(target, name, wrapper):
"""
Wraps a function which is the attribute of a target object with a `wrapper`
function. The `target` can be a module, class, or instance of a class. In
the special case of `target` being a string, it is assumed to be the name
of a module, with the module being imported if necessary. If the `target`
is a string with a trailing ``?``, the wrapping will be deferred until the
module is imported. If the module is already imported, the wrapping will be
applied immediately. The `name` is a string representing the dotted path to
the attribute. The `wrapper` function should accept the `wrapped` function,
`instance`, `args`, and `kwargs` arguments, and would return the result of
calling the wrapped attribute or some other appropriate value. Returns the
wrapped target function if the wrapping was applied immediately, or ``None``
if the wrapping was deferred.
"""
if isinstance(target, str) and target.endswith("?"):
target = target[:-1]
if target in sys.modules:
return wrap_object(sys.modules[target], name, FunctionWrapper, (wrapper,))
def callback(module):
wrap_object(module, name, FunctionWrapper, (wrapper,))
register_post_import_hook(callback, target)
return None
return wrap_object(target, name, FunctionWrapper, (wrapper,))
def patch_function_wrapper(target, name, _enabled=MISSING, *, enabled=MISSING):
"""
Creates a decorator which can be applied to a wrapper function, where the
wrapper function will be used to wrap a function which is the attribute of
a target object. The decorator returns the original wrapper function. The
`target` can be a module, class, or instance of a class. In the special case
of `target` being a string, it is assumed to be the name of a module, with
the module being imported if necessary. If the `target` is a string with a
trailing ``?``, the wrapping will be deferred until the module is imported.
If the module is already imported, the wrapping will be applied immediately.
The `name` is a string representing the dotted path to the attribute. The
`enabled` argument is keyword only and can be a boolean or a callable that
returns a boolean. When a callable is provided, it will be called each time
the wrapper is invoked to determine if the wrapper function should be
executed or whether the wrapped function should be called directly. If
`enabled` is not provided, or an explicit value of `None` is supplied,
the wrapper is enabled by default.
"""
if _enabled is not MISSING:
if enabled is not MISSING:
raise TypeError(
"patch_function_wrapper() got multiple values for " "argument 'enabled'"
)
warnings.warn(
"Passing 'enabled' positionally to patch_function_wrapper() is "
"deprecated and will be an error in a future version of wrapt; "
"pass it as a keyword argument.",
DeprecationWarning,
stacklevel=2,
)
enabled = _enabled
if enabled is MISSING:
enabled = None
def _wrapper(wrapper):
if isinstance(target, str) and target.endswith("?"):
_target = target[:-1]
if _target in sys.modules:
wrap_object(
sys.modules[_target], name, FunctionWrapper, (wrapper, enabled)
)
return wrapper
def callback(module):
wrap_object(module, name, FunctionWrapper, (wrapper, enabled))
register_post_import_hook(callback, _target)
return wrapper
wrap_object(target, name, FunctionWrapper, (wrapper, enabled))
return wrapper
return _wrapper
def transient_function_wrapper(target, name):
"""Creates a decorator that patches a target function with a wrapper
function, but only for the duration of the call that the decorator was
applied to. The `target` can be a module, class, or instance of a class.
In the special case of `target` being a string, it is assumed to be the name
of a module, with the module being imported if necessary. The `name` is a
string representing the dotted path to the attribute. The patch is applied
to the object the path resolves to, so patching an attribute reached
through inheritance, or through an instance, only affects lookups made
through that object and not the class where the attribute is defined. In
that case the temporary attribute which shadowed the inherited definition
is removed again when the call exits, restoring the original lookup.
The temporary wrapper is removed using `unwrap_object()`, so if code
called within the scope of the patch wrapped over the top of the
temporary wrapper with a wrapt wrapper and left it there, the temporary
wrapper is spliced out beneath it, with the other wrapper left in
place. If something removed or replaced the temporary wrapper during
the call, `WrapperNotFoundError` is raised, and if what was applied on
top of it is not a wrapt wrapper and so cannot be spliced past,
`WrapperNotOutermostError` is raised. Both are loud deliberately, as
they indicate the surrounding code, typically a test harness, is not
managing its own patches properly, and the leaked patch state would
otherwise surface as hard to diagnose failures later.
"""
def _decorator(wrapper):
def _wrapper(wrapped, instance, args, kwargs):
target_wrapped = args[0]
if instance is None:
target_wrapper = wrapper
elif inspect.isclass(instance):
target_wrapper = wrapper.__get__(None, instance)
else:
target_wrapper = wrapper.__get__(instance, type(instance))
def _execute(wrapped, instance, args, kwargs):
# The wrap and unwrap functions handle all the details:
# wrap_object() records whether applying the patch created
# a shadowing attribute slot, and unwrap_object() consults
# that record to restore or remove the attribute
# faithfully, splices the temporary wrapper out from
# beneath any wrapt wrapper left applied on top of it, and
# raises if the temporary wrapper was removed, replaced,
# or pinned beneath a non wrapt wrapper. An exception
# raised on exit supersedes any in-flight exception from
# the wrapped call, which remains visible as the chained
# __context__.
replacement = wrap_object(
target, name, FunctionWrapper, (target_wrapper,)
)
try:
return wrapped(*args, **kwargs)
finally:
unwrap_object(target, name, replacement)
return FunctionWrapper(target_wrapped, _execute)
return FunctionWrapper(wrapper, _wrapper)
return _decorator
@contextlib.contextmanager
def scoped_function_wrapper(target, name, wrapper):
"""Returns a context manager which patches a target function with a
wrapper function for the duration of a with statement, the block scoped
counterpart of the `transient_function_wrapper()` decorator. The
arguments take the same form as for `wrap_function_wrapper()`: the
`target` can be a module, class, or instance of a class, or the name of
a module as a string, the `name` is a string representing the dotted
path to the attribute, and the `wrapper` function should accept the
`wrapped`, `instance`, `args`, and `kwargs` arguments. The deferred
form of the `target` with a trailing ``?`` is not supported, since the
patch must be applied at the point the with statement is entered. The
context manager yields nothing, is single use, and removes the wrapper
using `unwrap_object()` when the block exits, with the same behaviour
as `transient_function_wrapper()` when something interfered with the
patch during the block: a wrapt wrapper applied on top of the temporary
wrapper and left there is tolerated, with the temporary wrapper spliced
out beneath it, while the temporary wrapper having been removed or
replaced raises `WrapperNotFoundError`, and a non wrapt wrapper left
applied on top raises `WrapperNotOutermostError`.
"""
if isinstance(target, str) and target.endswith("?"):
raise ValueError(
"deferred wrapping using a target with a trailing '?' cannot "
"be used with scoped_function_wrapper()"
)
handle = wrap_function_wrapper(target, name, wrapper)
try:
yield
finally:
unwrap_object(target, name, handle)
# Functions for introspecting chains of wrappers, linked by each wrapper
# holding the object it wraps as the __wrapped__ attribute. The chain
# protocol is shared by wrapt proxies and wrappers, functions decorated
# using functools.wraps(), and anything else honouring the convention.
def wrapper_chain(obj, *, limit=64):
"""
Returns an iterator yielding `obj`, then each successive object found
by following the `__wrapped__` attribute, outermost wrapper first. The
final item yielded is the innermost object of the chain, which is not
itself a wrapper. Traversal ends cleanly at an object with no
`__wrapped__` attribute, or upon returning to an object already seen.
If the `limit` on the number of items yielded is reached with a further
chain link still pending, `WrapperChainTooDeepError` is raised, as a
truncated scan would otherwise be indistinguishable from a complete
one. A chain of exactly `limit` items which ends naturally is not an
error. Note that reading `__wrapped__` from a lazy object proxy will
cause it to materialize, and an exception raised by a broken proxy or
a lazy object factory will propagate to the caller.
"""
# Cycle detection must use identity, not equality or a set of the
# objects themselves, since proxies delegate __eq__ and __hash__ to
# the wrapped object. Objects seen are therefore tracked by id(),
# with strong references also held so no visited object can be
# garbage collected and have its id reused while the scan runs, ids
# being unique among simultaneously live objects.
seen = []
seen_ids = set()
current = obj
while True:
if id(current) in seen_ids:
return
if len(seen) >= limit:
raise WrapperChainTooDeepError(
f"wrapper chain of {obj!r} exceeded {limit} levels"
)
seen.append(current)
seen_ids.add(id(current))
yield current
try:
current = current.__wrapped__
except AttributeError:
return
def unwrapped(obj, *, limit=64):
"""
Returns the innermost object of the chain of wrappers followed from
`obj` by the `wrapper_chain()` function, or `obj` itself when it is not
wrapped. Shares the full contract of `wrapper_chain()`, including
raising `WrapperChainTooDeepError` when the scan is indeterminate,
rather than returning a mid chain wrapper as if it were the innermost
object.
"""
result = obj
for result in wrapper_chain(obj, limit=limit):
pass
return result
def find_wrapper(obj, handle=None, *, predicate=None, limit=64):
"""
Scans the chain of wrappers followed from `obj` by the
`wrapper_chain()` function for a specific wrapper and returns it, or
`None` when it is not present. The `handle` argument is the wrapper
object to look for, as returned by the wrap functions when the wrapper
was installed, and is matched by object identity only, never equality,
which proxies delegate to the wrapped object. Alternatively a
`predicate` function may be supplied, in which case the first chain
entry for which it returns true is returned, and is itself usable as a
handle thereafter. When both are supplied, both must match. At least
one of the two must be supplied, since testing for the mere presence
of any wrapper is fragile: were the target library to adopt wrapt for
its own purposes, such a test would wrongly conclude a wrapper of
yours was installed. Shares the full contract of `wrapper_chain()`,
including raising `WrapperChainTooDeepError` when the scan is
indeterminate, rather than returning `None` as a false negative.
"""
if handle is None and predicate is None:
raise TypeError(
"find_wrapper() requires a wrapper handle or a predicate"
)
for entry in wrapper_chain(obj, limit=limit):
if handle is not None and entry is not handle:
continue
if predicate is not None and not predicate(entry):
continue
return entry
return None
def is_wrapped_by(obj, handle=None, *, predicate=None, limit=64):
"""
Boolean convenience form of the `find_wrapper()` function, returning
whether the wrapper is present in the chain of wrappers followed from
`obj`. With a `handle`, this answers whether the wrapper it was
returned for when installed is still in place, which is the check to
run when a third party may have replaced the attribute wholesale.
Shares the full contract of `find_wrapper()`, including raising
`WrapperChainTooDeepError` when the scan is indeterminate, rather
than returning `False` as a false negative.
"""
return find_wrapper(obj, handle, predicate=predicate, limit=limit) is not None
def unwrap_object(target, name, handle, *, missing_ok=False):
"""
Removes a wrapper which was installed on an attribute of a target
object, the inverse of the `wrap_object()` function and the removal
call for every wrap form, `wrap_function_wrapper()` and
`wrap_object_attribute()` included. The `target` and `name` arguments
take the same form as for `resolve_path()`. The `handle` argument is
the wrapper object which was returned when the wrapper was installed,
and is matched by object identity only. Note that it is not the
wrapper function: passing the wrapper function surfaces immediately
as `WrapperNotFoundError`, since a function is never a chain entry.
When the wrapper is found and is outermost, the attribute is restored
to the object the wrapper wrapped, at the location where the attribute
is actually defined per `resolve_owner()`, so removal through a
subclass restores the defining base class rather than leaving a
shadowing copy. If restoring would merely shadow the identical object
already reachable through the MRO, or the wrapper was installed where
no prior definition existed (a `MISSING` terminal), the attribute is
instead removed so no residue is left behind. When the wrapper is
found buried beneath other wrapt wrappers, it is spliced out of the
chain in place, without touching the attribute or disturbing the
wrappers above it. When what sits directly above it is not a wrapt
wrapper, such as a plain `functools.wraps()` closure whose
`__wrapped__` is only metadata, `WrapperNotOutermostError` is raised
naming what is above, since splicing there would silently not take
effect.
When the wrapper is not found, because the attribute was never
wrapped, the wrapper was already removed, or a third party replaced
the attribute wholesale, `WrapperNotFoundError` is raised by default.
Passing `missing_ok=True` returns `None` instead, for shutdown paths
which must tolerate third party interference. In either case nothing
is mutated. The wrapper which was removed is returned. Note that
`missing_ok` does not suppress `WrapperChainTooDeepError` from the
underlying scan, since an indeterminate scan is not the same thing as
the wrapper being gone.
"""
try:
owner, attribute, current = resolve_owner(target, name)
except PathResolutionError as exc:
# The attribute is either wholly absent or served dynamically
# with no owning location. In neither case is anything of the
# caller's statically installed, so both are the not-found
# case, except when the wrapper is present in the dynamically
# served value, where removal is simply not possible.
try:
current = resolve_path(target, name)[2]
except PathResolutionError:
current = None
if current is not None and find_wrapper(current, handle) is not None:
raise WrapperNotOutermostError(
f"cannot remove {type(handle).__name__} from "
f"{target!r}.{name}: the attribute is served dynamically "
f"and has no owning location to restore"
) from exc
if missing_ok:
return None
raise WrapperNotFoundError(
f"handle {handle!r} not found on {target!r}.{name}"
) from exc
found = find_wrapper(current, handle)
if found is not None:
try:
restored = found.__wrapped__
except AttributeError:
# The terminal original object was matched, which the chain
# also yields; not being a wrapper, it is not a legitimate
# handle.
found = None
if found is None:
if missing_ok:
return None
raise WrapperNotFoundError(
f"handle {handle!r} not found on {target!r}.{name}"
)
if found is not current:
chain = list(wrapper_chain(current))
# The scan must use identity, since list.index() would use
# __eq__, which proxies delegate to the wrapped object.
position = next(
index for index, entry in enumerate(chain) if entry is found
)
neighbour = chain[position - 1]
if not issubclass(type(neighbour), BaseObjectProxy):
above = [type(entry).__name__ for entry in chain[:position]]
raise WrapperNotOutermostError(
f"cannot remove {type(found).__name__} from "
f"{target!r}.{name}: wrapped by {above}"
)
neighbour.__wrapped__ = restored
return found
if restored is MISSING:
# The wrapper was installed where no prior definition existed.
delattr(owner, attribute)
return found
# When wrap_object() installed the wrapper, it recorded on the
# wrapper itself whether applying the patch created the attribute
# slot. That fact is authoritative: since the wrapper is still
# installed and outermost here, the slot cannot have been touched
# since it was recorded. It must be read from the proxy's own local
# state only, since ordinary attribute access would delegate a
# missing entry to the wrapped object and could answer with an
# inner wrapper's record instead.
try:
created = found.__self_dict__.get("__wrapt_wrap_object_created_slot__")
except AttributeError:
created = None
if created is not None:
if created:
# Installing the wrapper created a shadowing slot, so
# removing the attribute reinstates the original lookup,
# be that through the MRO, the class of an instance, or a
# dynamic __getattr__, rather than leaving a permanent
# copy of the original behind.
delattr(owner, attribute)
else:
apply_patch(owner, attribute, restored)
return found
# The wrapper does not carry the installation record, so fall back
# to what can be determined at removal time: if removing the
# attribute from a class would re-expose the identical restored
# object through the MRO, the wrap must have created a shadow.
if inspect.isclass(owner):
inherited = next(
(
vars(cls)[attribute]
for cls in inspect.getmro(owner)[1:]
if attribute in vars(cls)
),
None,
)
if inherited is restored:
delattr(owner, attribute)
return found
apply_patch(owner, attribute, restored)
return found