- 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 первым)
851 lines
36 KiB
Python
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
|