798 lines
24 KiB
Python
798 lines
24 KiB
Python
# __ __ __
|
|
# .-----.-----.----|__| | .---.-.
|
|
# | _ | _ | _| | | | _ |
|
|
# |___ |_____|__| |__|__|__|___._|
|
|
# |_____|
|
|
#
|
|
|
|
"""
|
|
NOTE: The contents of this file have been inlined from the gorilla package's source code
|
|
https://github.com/christophercrouzet/gorilla/blob/v0.3.0/gorilla.py
|
|
|
|
This module has fixes / adaptations for MLflow use cases that make it different from the original
|
|
gorilla library
|
|
|
|
The following modifications have been made:
|
|
- Modify `get_original_attribute` logic, search from children classes to parent classes,
|
|
and for each class check "_gorilla_original_{attr_name}" attribute first.
|
|
first. This will ensure get the correct original attribute in any cases, e.g.,
|
|
the case some classes in the hierarchy haven't been patched, but some others are
|
|
patched, this case the previous code is risky to get wrong original attribute.
|
|
- Make `get_original_attribute` support bypassing descriptor protocol.
|
|
- remove `get_attribute` method, use `get_original_attribute` with
|
|
`bypass_descriptor_protocol=True` instead of calling it.
|
|
- After reverting patch, there will be no side-effect, restore object to be exactly the
|
|
original status.
|
|
- Remove `create_patches` and `patches` methods.
|
|
|
|
gorilla
|
|
~~~~~~~
|
|
|
|
Convenient approach to monkey patching.
|
|
|
|
:copyright: Copyright 2014-2017 by Christopher Crouzet.
|
|
:license: MIT, see LICENSE for details.
|
|
"""
|
|
|
|
import collections
|
|
import copy
|
|
import inspect
|
|
import logging
|
|
import pkgutil
|
|
import sys
|
|
import types
|
|
|
|
__version__ = "0.3.0"
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _iteritems(d, **kwargs):
|
|
return iter(d.items(**kwargs))
|
|
|
|
|
|
def _load_module(finder, name):
|
|
loader, _ = finder.find_loader(name)
|
|
return loader.load_module()
|
|
|
|
|
|
# Pattern for each internal attribute name.
|
|
_PATTERN = "_gorilla_%s"
|
|
|
|
# Pattern for the name of the overidden attributes to be stored.
|
|
_ORIGINAL_NAME = _PATTERN % ("original_%s",)
|
|
|
|
# Pattern for the name of the patch attributes to be stored.
|
|
_ACTIVE_PATCH = "_gorilla_active_patch_%s"
|
|
|
|
# Attribute for the decorator data.
|
|
_DECORATOR_DATA = _PATTERN % ("decorator_data",)
|
|
|
|
|
|
def default_filter(name, obj):
|
|
"""Attribute filter.
|
|
|
|
It filters out module attributes, and also methods starting with an
|
|
underscore ``_``.
|
|
|
|
This is used as the default filter for the :func:`create_patches` function
|
|
and the :func:`patches` decorator.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
Attribute name.
|
|
obj : object
|
|
Attribute value.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
Whether the attribute should be returned.
|
|
"""
|
|
return not (isinstance(obj, types.ModuleType) or name.startswith("_"))
|
|
|
|
|
|
class DecoratorData:
|
|
"""Decorator data.
|
|
|
|
Attributes
|
|
----------
|
|
patches : list of gorilla.Patch
|
|
Patches created through the decorators.
|
|
override : dict
|
|
Any overriding value defined by the :func:`destination`, :func:`name`,
|
|
and :func:`settings` decorators.
|
|
filter : bool or None
|
|
Value defined by the :func:`filter` decorator, if any, or ``None``
|
|
otherwise.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Constructor."""
|
|
self.patches = []
|
|
self.override = {}
|
|
self.filter = None
|
|
|
|
|
|
class Settings:
|
|
"""Define the patching behaviour.
|
|
|
|
Attributes
|
|
----------
|
|
allow_hit : bool
|
|
A hit occurs when an attribute at the destination already exists with
|
|
the name given by the patch. If ``False``, the patch process won't
|
|
allow setting a new value for the attribute by raising an exception.
|
|
Defaults to ``False``.
|
|
store_hit : bool
|
|
If ``True`` and :attr:`allow_hit` is also set to ``True``, then any
|
|
attribute at the destination that is hit is stored under a different
|
|
name before being overwritten by the patch. Defaults to ``True``.
|
|
"""
|
|
|
|
def __init__(self, **kwargs):
|
|
"""Constructor.
|
|
|
|
Parameters
|
|
----------
|
|
kwargs
|
|
Keyword arguments, see the attributes.
|
|
"""
|
|
self.allow_hit = False
|
|
self.store_hit = True
|
|
self._update(**kwargs)
|
|
|
|
def __repr__(self):
|
|
values = ", ".join([f"{key}={value!r}" for key, value in sorted(_iteritems(self.__dict__))])
|
|
return f"{type(self).__name__}({values})"
|
|
|
|
def __eq__(self, other):
|
|
if isinstance(other, type(self)):
|
|
return self.__dict__ == other.__dict__
|
|
|
|
return NotImplemented
|
|
|
|
def __ne__(self, other):
|
|
is_equal = self.__eq__(other)
|
|
return is_equal if is_equal is NotImplemented else not is_equal
|
|
|
|
def _update(self, **kwargs):
|
|
"""Update some settings.
|
|
|
|
Parameters
|
|
----------
|
|
kwargs
|
|
Settings to update.
|
|
"""
|
|
self.__dict__.update(**kwargs)
|
|
|
|
|
|
class Patch:
|
|
"""Describe all the information required to apply a patch.
|
|
|
|
Attributes
|
|
----------
|
|
destination : obj
|
|
Patch destination.
|
|
name : str
|
|
Name of the attribute at the destination.
|
|
obj : obj
|
|
Attribute value.
|
|
settings : gorilla.Settings or None
|
|
Settings. If ``None``, the default settings are used.
|
|
|
|
Warning
|
|
-------
|
|
It is highly recommended to use the output of the function
|
|
:func:`get_attribute` for setting the attribute :attr:`obj`. This will
|
|
ensure that the descriptor protocol is bypassed instead of possibly
|
|
retrieving attributes invalid for patching, such as bound methods.
|
|
"""
|
|
|
|
def __init__(self, destination, name, obj, settings=None):
|
|
"""Constructor.
|
|
|
|
Parameters
|
|
----------
|
|
destination : object
|
|
See the :attr:`~Patch.destination` attribute.
|
|
name : str
|
|
See the :attr:`~Patch.name` attribute.
|
|
obj : object
|
|
See the :attr:`~Patch.obj` attribute.
|
|
settings : gorilla.Settings
|
|
See the :attr:`~Patch.settings` attribute.
|
|
"""
|
|
self.destination = destination
|
|
self.name = name
|
|
self.obj = obj
|
|
self.settings = settings
|
|
self.is_inplace_patch = None
|
|
|
|
def __repr__(self):
|
|
return "{}(destination={!r}, name={!r}, obj={!r}, settings={!r})".format(
|
|
type(self).__name__,
|
|
self.destination,
|
|
self.name,
|
|
self.obj,
|
|
self.settings,
|
|
)
|
|
|
|
def __eq__(self, other):
|
|
if isinstance(other, type(self)):
|
|
return self.__dict__ == other.__dict__
|
|
|
|
return NotImplemented
|
|
|
|
def __ne__(self, other):
|
|
is_equal = self.__eq__(other)
|
|
return is_equal if is_equal is NotImplemented else not is_equal
|
|
|
|
def __hash__(self):
|
|
return super().__hash__()
|
|
|
|
def _update(self, **kwargs):
|
|
"""Update some attributes.
|
|
|
|
If a 'settings' attribute is passed as a dict, then it will update the
|
|
content of the settings, if any, instead of completely overwriting it.
|
|
|
|
Parameters
|
|
----------
|
|
kwargs
|
|
Attributes to update.
|
|
|
|
Raises
|
|
------
|
|
ValueError
|
|
The setting doesn't exist.
|
|
"""
|
|
for key, value in _iteritems(kwargs):
|
|
if key == "settings":
|
|
if isinstance(value, dict):
|
|
if self.settings is None:
|
|
self.settings = Settings(**value)
|
|
else:
|
|
self.settings._update(**value)
|
|
else:
|
|
self.settings = copy.deepcopy(value)
|
|
else:
|
|
setattr(self, key, value)
|
|
|
|
|
|
def apply(patch):
|
|
"""Apply a patch.
|
|
|
|
The patch's :attr:`~Patch.obj` attribute is injected into the patch's
|
|
:attr:`~Patch.destination` under the patch's :attr:`~Patch.name`.
|
|
|
|
This is a wrapper around calling
|
|
``setattr(patch.destination, patch.name, patch.obj)``.
|
|
|
|
Parameters
|
|
----------
|
|
patch : gorilla.Patch
|
|
Patch.
|
|
|
|
Raises
|
|
------
|
|
RuntimeError
|
|
Overwriting an existing attribute is not allowed when the setting
|
|
:attr:`Settings.allow_hit` is set to ``True``.
|
|
|
|
Note
|
|
----
|
|
If both the attributes :attr:`Settings.allow_hit` and
|
|
:attr:`Settings.store_hit` are ``True`` but that the target attribute seems
|
|
to have already been stored, then it won't be stored again to avoid losing
|
|
the original attribute that was stored the first time around.
|
|
"""
|
|
# is_inplace_patch = True represents the patch object will overwrite the original
|
|
# attribute
|
|
patch.is_inplace_patch = patch.name in patch.destination.__dict__
|
|
settings = Settings() if patch.settings is None else patch.settings
|
|
|
|
curr_active_patch = _ACTIVE_PATCH % (patch.name,)
|
|
if curr_active_patch in patch.destination.__dict__:
|
|
_logger.debug(
|
|
f"Patch {patch.name} on {destination.__name__} already existed. Overwrite old patch."
|
|
)
|
|
|
|
# When a hit occurs due to an attribute at the destination already existing
|
|
# with the patch's name, the existing attribute is referred to as 'target'.
|
|
try:
|
|
target = get_original_attribute(
|
|
patch.destination, patch.name, bypass_descriptor_protocol=True
|
|
)
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
if not settings.allow_hit:
|
|
raise RuntimeError(
|
|
"An attribute named '%s' already exists at the destination " # noqa: UP031
|
|
"'%s'. Set a different name through the patch object to avoid "
|
|
"a name clash or set the setting 'allow_hit' to True to "
|
|
"overwrite the attribute. In the latter case, it is "
|
|
"recommended to also set the 'store_hit' setting to True in "
|
|
"order to store the original attribute under a different "
|
|
"name so it can still be accessed." % (patch.name, patch.destination.__name__)
|
|
)
|
|
|
|
if settings.store_hit:
|
|
original_name = _ORIGINAL_NAME % (patch.name,)
|
|
setattr(patch.destination, original_name, target)
|
|
|
|
setattr(patch.destination, patch.name, patch.obj)
|
|
setattr(patch.destination, curr_active_patch, patch)
|
|
|
|
|
|
def revert(patch):
|
|
"""Revert a patch.
|
|
Parameters
|
|
----------
|
|
patch : gorilla.Patch
|
|
Patch.
|
|
|
|
Note
|
|
----
|
|
This is only possible if the attribute :attr:`Settings.store_hit` was set
|
|
to ``True`` when applying the patch and overriding an existing attribute.
|
|
|
|
Notice:
|
|
This method is taken from
|
|
https://github.com/christophercrouzet/gorilla/blob/v0.4.0/gorilla.py#L318-L351
|
|
with modifictions for autologging disablement purposes.
|
|
"""
|
|
# If an curr_active_patch has not been set on destination class for the current patch,
|
|
# then the patch has not been applied and we do not need to revert anything.
|
|
curr_active_patch = _ACTIVE_PATCH % (patch.name,)
|
|
if curr_active_patch not in patch.destination.__dict__:
|
|
# already reverted.
|
|
return
|
|
|
|
original_name = _ORIGINAL_NAME % (patch.name,)
|
|
|
|
if patch.is_inplace_patch:
|
|
# check whether original_name is in destination. We cannot use hasattr because it will
|
|
# try to get attribute from parent classes if attribute not found in destination class.
|
|
if original_name not in patch.destination.__dict__:
|
|
raise RuntimeError(
|
|
"Cannot revert the attribute named '%s' since the setting " # noqa: UP031
|
|
"'store_hit' was not set to True when applying the patch."
|
|
% (patch.destination.__name__,)
|
|
)
|
|
# restore original method
|
|
# during reverting patch, we need restore the raw attribute to the patch point
|
|
# so get original attribute bypassing descriptor protocal
|
|
original = object.__getattribute__(patch.destination, original_name)
|
|
setattr(patch.destination, patch.name, original)
|
|
else:
|
|
# delete patched method
|
|
delattr(patch.destination, patch.name)
|
|
|
|
if original_name in patch.destination.__dict__:
|
|
delattr(patch.destination, original_name)
|
|
delattr(patch.destination, curr_active_patch)
|
|
|
|
|
|
def patch(destination, name=None, settings=None):
|
|
"""Decorator to create a patch.
|
|
|
|
The object being decorated becomes the :attr:`~Patch.obj` attribute of the
|
|
patch.
|
|
|
|
Parameters
|
|
----------
|
|
destination : object
|
|
Patch destination.
|
|
name : str
|
|
Name of the attribute at the destination.
|
|
settings : gorilla.Settings
|
|
Settings.
|
|
|
|
Returns
|
|
-------
|
|
object
|
|
The decorated object.
|
|
|
|
See Also
|
|
--------
|
|
:class:`Patch`.
|
|
"""
|
|
|
|
def decorator(wrapped):
|
|
base = _get_base(wrapped)
|
|
name_ = base.__name__ if name is None else name
|
|
settings_ = copy.deepcopy(settings)
|
|
patch = Patch(destination, name_, wrapped, settings=settings_)
|
|
data = get_decorator_data(base, set_default=True)
|
|
data.patches.append(patch)
|
|
return wrapped
|
|
|
|
return decorator
|
|
|
|
|
|
def destination(value):
|
|
"""Modifier decorator to update a patch's destination.
|
|
|
|
This only modifies the behaviour of the :func:`create_patches` function
|
|
and the :func:`patches` decorator, given that their parameter
|
|
``use_decorators`` is set to ``True``.
|
|
|
|
Parameters
|
|
----------
|
|
value : object
|
|
Patch destination.
|
|
|
|
Returns
|
|
-------
|
|
object
|
|
The decorated object.
|
|
"""
|
|
|
|
def decorator(wrapped):
|
|
data = get_decorator_data(_get_base(wrapped), set_default=True)
|
|
data.override["destination"] = value
|
|
return wrapped
|
|
|
|
return decorator
|
|
|
|
|
|
def name(value):
|
|
"""Modifier decorator to update a patch's name.
|
|
|
|
This only modifies the behaviour of the :func:`create_patches` function
|
|
and the :func:`patches` decorator, given that their parameter
|
|
``use_decorators`` is set to ``True``.
|
|
|
|
Parameters
|
|
----------
|
|
value : object
|
|
Patch name.
|
|
|
|
Returns
|
|
-------
|
|
object
|
|
The decorated object.
|
|
"""
|
|
|
|
def decorator(wrapped):
|
|
data = get_decorator_data(_get_base(wrapped), set_default=True)
|
|
data.override["name"] = value
|
|
return wrapped
|
|
|
|
return decorator
|
|
|
|
|
|
def settings(**kwargs):
|
|
"""Modifier decorator to update a patch's settings.
|
|
|
|
This only modifies the behaviour of the :func:`create_patches` function
|
|
and the :func:`patches` decorator, given that their parameter
|
|
``use_decorators`` is set to ``True``.
|
|
|
|
Parameters
|
|
----------
|
|
kwargs
|
|
Settings to update. See :class:`Settings` for the list.
|
|
|
|
Returns
|
|
-------
|
|
object
|
|
The decorated object.
|
|
"""
|
|
|
|
def decorator(wrapped):
|
|
data = get_decorator_data(_get_base(wrapped), set_default=True)
|
|
data.override.setdefault("settings", {}).update(kwargs)
|
|
return wrapped
|
|
|
|
return decorator
|
|
|
|
|
|
def filter(value):
|
|
"""Modifier decorator to force the inclusion or exclusion of an attribute.
|
|
|
|
This only modifies the behaviour of the :func:`create_patches` function
|
|
and the :func:`patches` decorator, given that their parameter
|
|
``use_decorators`` is set to ``True``.
|
|
|
|
Parameters
|
|
----------
|
|
value : bool
|
|
``True`` to force inclusion, ``False`` to force exclusion, and ``None``
|
|
to inherit from the behaviour defined by :func:`create_patches` or
|
|
:func:`patches`.
|
|
|
|
Returns
|
|
-------
|
|
object
|
|
The decorated object.
|
|
"""
|
|
|
|
def decorator(wrapped):
|
|
data = get_decorator_data(_get_base(wrapped), set_default=True)
|
|
data.filter = value
|
|
return wrapped
|
|
|
|
return decorator
|
|
|
|
|
|
def find_patches(modules, recursive=True):
|
|
"""Find all the patches created through decorators.
|
|
|
|
Parameters
|
|
----------
|
|
modules : list of module
|
|
Modules and/or packages to search the patches in.
|
|
recursive : bool
|
|
``True`` to search recursively in subpackages.
|
|
|
|
Returns
|
|
-------
|
|
list of gorilla.Patch
|
|
Patches found.
|
|
|
|
Raises
|
|
------
|
|
TypeError
|
|
The input is not a valid package or module.
|
|
|
|
See Also
|
|
--------
|
|
:func:`patch`, :func:`patches`.
|
|
"""
|
|
out = []
|
|
modules = (
|
|
module for package in modules for module in _module_iterator(package, recursive=recursive)
|
|
)
|
|
for module in modules:
|
|
members = _get_members(module, filter=None)
|
|
for _, value in members:
|
|
base = _get_base(value)
|
|
decorator_data = get_decorator_data(base)
|
|
if decorator_data is None:
|
|
continue
|
|
|
|
out.extend(decorator_data.patches)
|
|
|
|
return out
|
|
|
|
|
|
def get_original_attribute(obj, name, bypass_descriptor_protocol=False):
|
|
"""Retrieve an overridden attribute that has been stored.
|
|
|
|
Parameters
|
|
----------
|
|
obj : object
|
|
Object to search the attribute in.
|
|
name : str
|
|
Name of the attribute.
|
|
bypass_descriptor_protocol: boolean
|
|
bypassing descriptor protocol if true. When storing original method during patching or
|
|
restoring original method during reverting patch, we need set bypass_descriptor_protocol
|
|
to be True to ensure get the raw attribute object.
|
|
|
|
Returns
|
|
-------
|
|
object
|
|
The attribute found.
|
|
|
|
Raises
|
|
------
|
|
AttributeError
|
|
The attribute couldn't be found.
|
|
|
|
Note
|
|
----
|
|
if setting store_hit=False, then after patch applied, this methods may return patched
|
|
attribute instead of original attribute in specific cases.
|
|
|
|
See Also
|
|
--------
|
|
:attr:`Settings.allow_hit`.
|
|
"""
|
|
|
|
original_name = _ORIGINAL_NAME % (name,)
|
|
curr_active_patch = _ACTIVE_PATCH % (name,)
|
|
|
|
def _get_attr(obj_, name_):
|
|
if bypass_descriptor_protocol:
|
|
return object.__getattribute__(obj_, name_)
|
|
else:
|
|
return getattr(obj_, name_)
|
|
|
|
no_original_stored_err = (
|
|
"Original attribute %s was not stored when patching, set store_hit=True will address this."
|
|
)
|
|
|
|
if inspect.isclass(obj):
|
|
# Search from children classes to parent classes, and check "original_name" attribute
|
|
# first. This will ensure get the correct original attribute in any cases, e.g.,
|
|
# the case some classes in the hierarchy haven't been patched, but some others are
|
|
# patched, this case the previous code is risky to get wrong original attribute.
|
|
for obj_ in inspect.getmro(obj):
|
|
if original_name in obj_.__dict__:
|
|
return _get_attr(obj_, original_name)
|
|
elif name in obj_.__dict__:
|
|
if curr_active_patch in obj_.__dict__:
|
|
patch = getattr(obj, curr_active_patch)
|
|
if patch.is_inplace_patch:
|
|
raise RuntimeError(no_original_stored_err % (f"{obj_.__name__}.{name}",))
|
|
else:
|
|
# non inplace patch, we can get original methods in parent classes.
|
|
# so go on checking parent classes
|
|
continue
|
|
return _get_attr(obj_, name)
|
|
else:
|
|
# go on checking parent classes
|
|
continue
|
|
raise AttributeError(f"'{type(obj)}' object has no attribute '{name}'")
|
|
else:
|
|
try:
|
|
return _get_attr(obj, original_name)
|
|
except AttributeError:
|
|
if curr_active_patch in obj.__dict__:
|
|
raise RuntimeError(no_original_stored_err % (f"{type(obj).__name__}.{name}",))
|
|
return _get_attr(obj, name)
|
|
|
|
|
|
def get_decorator_data(obj, set_default=False):
|
|
"""Retrieve any decorator data from an object.
|
|
|
|
Parameters
|
|
----------
|
|
obj : object
|
|
Object.
|
|
set_default : bool
|
|
If no data is found, a default one is set on the object and returned,
|
|
otherwise ``None`` is returned.
|
|
|
|
Returns
|
|
-------
|
|
gorilla.DecoratorData
|
|
The decorator data or ``None``.
|
|
"""
|
|
if inspect.isclass(obj):
|
|
datas = getattr(obj, _DECORATOR_DATA, {})
|
|
data = datas.setdefault(obj, None)
|
|
if data is None and set_default:
|
|
data = DecoratorData()
|
|
datas[obj] = data
|
|
setattr(obj, _DECORATOR_DATA, datas)
|
|
else:
|
|
data = getattr(obj, _DECORATOR_DATA, None)
|
|
if data is None and set_default:
|
|
data = DecoratorData()
|
|
setattr(obj, _DECORATOR_DATA, data)
|
|
|
|
return data
|
|
|
|
|
|
def _get_base(obj):
|
|
"""Unwrap decorators to retrieve the base object.
|
|
|
|
Parameters
|
|
----------
|
|
obj : object
|
|
Object.
|
|
|
|
Returns
|
|
-------
|
|
object
|
|
The base object found or the input object otherwise.
|
|
"""
|
|
if hasattr(obj, "__func__"):
|
|
obj = obj.__func__
|
|
elif isinstance(obj, property):
|
|
obj = obj.fget
|
|
elif isinstance(obj, (classmethod, staticmethod)):
|
|
# Fallback for Python < 2.7 back when no `__func__` attribute
|
|
# was defined for those descriptors.
|
|
obj = obj.__get__(None, object)
|
|
else:
|
|
return obj
|
|
|
|
return _get_base(obj)
|
|
|
|
|
|
def _get_members(obj, traverse_bases=True, filter=default_filter, recursive=True):
|
|
"""Retrieve the member attributes of a module or a class.
|
|
|
|
The descriptor protocol is bypassed.
|
|
|
|
Parameters
|
|
----------
|
|
obj : module or class
|
|
Object.
|
|
traverse_bases : bool
|
|
If the object is a class, the base classes are also traversed.
|
|
filter : function
|
|
Attributes for which the function returns ``False`` are skipped. The
|
|
function needs to define two parameters: ``name``, the attribute name,
|
|
and ``obj``, the attribute value. If ``None``, no attribute is skipped.
|
|
recursive : bool
|
|
``True`` to search recursively through subclasses.
|
|
|
|
Returns
|
|
------
|
|
list of (name, value)
|
|
A list of tuples each containing the name and the value of the
|
|
attribute.
|
|
"""
|
|
if filter is None:
|
|
filter = _true
|
|
|
|
out = []
|
|
stack = collections.deque((obj,))
|
|
while stack:
|
|
obj = stack.popleft()
|
|
if traverse_bases and inspect.isclass(obj):
|
|
roots = [base for base in inspect.getmro(obj) if base not in (type, object)]
|
|
else:
|
|
roots = [obj]
|
|
|
|
members = []
|
|
seen = set()
|
|
for root in roots:
|
|
for name, value in _iteritems(getattr(root, "__dict__", {})):
|
|
if name not in seen and filter(name, value):
|
|
members.append((name, value))
|
|
|
|
seen.add(name)
|
|
|
|
members = sorted(members)
|
|
for _, value in members:
|
|
if recursive and inspect.isclass(value):
|
|
stack.append(value)
|
|
|
|
out.extend(members)
|
|
|
|
return out
|
|
|
|
|
|
def _module_iterator(root, recursive=True):
|
|
"""Iterate over modules.
|
|
|
|
Parameters
|
|
----------
|
|
root : module
|
|
Root module or package to iterate from.
|
|
recursive : bool
|
|
``True`` to iterate within subpackages.
|
|
|
|
Yields
|
|
------
|
|
module
|
|
The modules found.
|
|
"""
|
|
yield root
|
|
|
|
stack = collections.deque((root,))
|
|
while stack:
|
|
package = stack.popleft()
|
|
# The '__path__' attribute of a package might return a list of paths if
|
|
# the package is referenced as a namespace.
|
|
paths = getattr(package, "__path__", [])
|
|
for path in paths:
|
|
modules = pkgutil.iter_modules([path])
|
|
for finder, name, is_package in modules:
|
|
module_name = f"{package.__name__}.{name}"
|
|
module = sys.modules.get(module_name, None)
|
|
if module is None:
|
|
# Import the module through the finder to support package
|
|
# namespaces.
|
|
module = _load_module(finder, module_name)
|
|
|
|
if is_package:
|
|
if recursive:
|
|
stack.append(module)
|
|
yield module
|
|
else:
|
|
yield module
|
|
|
|
|
|
def _true(*args, **kwargs):
|
|
"""Return ``True``."""
|
|
return True
|