from enum import auto
from types import MappingProxyType, ModuleType
from typing import Union, TypedDict
from .line_profiler_utils import StringEnum
#: Default scoping policies:
#:
#: * Profile sibling and descendant functions
#: (:py:attr:`ScopingPolicy.SIBLINGS`)
#: * Descend ingo sibling and descendant classes
#: (:py:attr:`ScopingPolicy.SIBLINGS`)
#: * Don't descend into modules (:py:attr:`ScopingPolicy.EXACT`)
DEFAULT_SCOPING_POLICIES = MappingProxyType(
{'func': 'siblings', 'class': 'siblings', 'module': 'exact'})
[docs]
class ScopingPolicy(StringEnum):
"""
:py:class:`StrEnum` for scoping policies, that is, how it is
decided whether to:
* Profile a function found in a namespace (a class or a module), and
* Descend into nested namespaces so that their methods and functions
are profiled,
when using :py:meth:`LineProfiler.add_class`,
:py:meth:`LineProfiler.add_module`, and
:py:func:`~.add_imported_function_or_module()`.
Available policies are:
:py:attr:`ScopingPolicy.EXACT`
Only profile *functions* found in the namespace fulfilling
:py:attr:`ScopingPolicy.CHILDREN` as defined below, without
descending into nested namespaces
:py:attr:`ScopingPolicy.CHILDREN`
Only profile/descend into *child* objects, which are:
* Classes and functions defined *locally* in the very
module, or in the very class as its "inner classes" and
methods
* Direct submodules, in case when the namespace is a module
object representing a package
:py:attr:`ScopingPolicy.DESCENDANTS`
Only profile/descend into *descendant* objects, which are:
* Child classes, functions, and modules, as defined above in
:py:attr:`ScopingPolicy.CHILDREN`
* Their child classes, functions, and modules, ...
* ... and so on
Note:
Since imported submodule module objects are by default
placed into the namespace of their parent-package module
objects, this functions largely identical to
:py:attr:`ScopingPolicy.CHILDREN` for descent from module
objects into other modules objects.
:py:attr:`ScopingPolicy.SIBLINGS`
Only profile/descend into *sibling* and descendant objects,
which are:
* Descendant classes, functions, and modules, as defined above
in :py:attr:`ScopingPolicy.DESCENDANTS`
* Classes and functions (and descendants thereof) defined in the
same parent namespace to this very class, or in modules (and
subpackages and their descendants) sharing a parent package
to this very module
* Modules (and subpackages and their descendants) sharing a
parent package, when the namespace is a module
:py:attr:`ScopingPolicy.NONE`
Don't check scopes; profile all functions found in the local
namespace of the class/module, and descend into all nested
namespaces recursively
Note:
This is probably a *very* bad idea for module scoping,
potentially resulting in accidentally recursing through a
significant portion of loaded modules;
proceed with care.
Note:
Other than :py:class:`enum.Enum` methods starting and ending
with single underscores (e.g. :py:meth:`!_missing_`), all
methods prefixed with a single underscore are to be considered
implementation details.
"""
EXACT = auto()
CHILDREN = auto()
DESCENDANTS = auto()
SIBLINGS = auto()
NONE = auto()
# Verification
def __init_subclass__(cls, *args, **kwargs):
"""
Call :py:meth:`_check_class`.
"""
super().__init_subclass__(*args, **kwargs)
cls._check_class()
@classmethod
def _check_class(cls):
"""
Verify that :py:meth:`.get_filter` return a callable for all
policy values and object types.
"""
mock_module = ModuleType('mock_module')
class MockClass:
pass
for member in cls.__members__.values():
for obj_type in 'func', 'class', 'module':
for namespace in mock_module, MockClass:
assert callable(member.get_filter(namespace, obj_type))
# Filtering
[docs]
def get_filter(self, namespace, obj_type):
"""
Args:
namespace (Union[type, types.ModuleType]):
Class or module to be profiled.
obj_type (Literal['func', 'class', 'module']):
Type of object encountered in ``namespace``:
``'func'``
Either a function, or a component function of a
callable-like object (e.g. :py:class:`property`)
``'class'`` (resp. ``'module'``)
A class (resp. a module)
Returns:
func (Callable[..., bool]):
Filter callable returning whether the argument (as
specified by ``obj_type``) should be added
via :py:meth:`LineProfiler.add_class`,
:py:meth:`LineProfiler.add_module`, or
:py:meth:`LineProfiler.add_callable`
"""
is_class = isinstance(namespace, type)
if obj_type == 'module':
if is_class:
return self._return_const(False)
return self._get_module_filter_in_module(namespace)
if is_class:
method = self._get_callable_filter_in_class
else:
method = self._get_callable_filter_in_module
return method(namespace, is_class=(obj_type == 'class'))
[docs]
@classmethod
def to_policies(cls, policies=None):
"""
Normalize ``policies`` into a dictionary of policies for various
object types.
Args:
policies (Union[str, ScopingPolicy, \
ScopingPolicyDict, None]):
:py:class:`ScopingPolicy`, string convertible thereto
(case-insensitive), or a mapping containing such values
and the keys as outlined in the return value;
the default :py:const:`None` is equivalent to
:py:data:`DEFAULT_SCOPING_POLICIES`.
Returns:
normalized_policies (dict[Literal['func', 'class', \
'module'], ScopingPolicy]):
Dictionary with the following key-value pairs:
``'func'``
:py:class:`ScopingPolicy` for profiling functions
and other callable-like objects composed thereof
(e.g. :py:class:`property`).
``'class'``
:py:class:`ScopingPolicy` for descending into
classes.
``'module'``
:py:class:`ScopingPolicy` for descending into
modules (if the namespace is itself a module).
Note:
If ``policies`` is a mapping, it is required to contain all
three of the aforementioned keys.
Example:
>>> assert (ScopingPolicy.to_policies('children')
... == dict.fromkeys(['func', 'class', 'module'],
... ScopingPolicy.CHILDREN))
>>> assert (ScopingPolicy.to_policies({
... 'func': 'NONE',
... 'class': 'descendants',
... 'module': 'exact',
... 'unused key': 'unused value'})
... == {'func': ScopingPolicy.NONE,
... 'class': ScopingPolicy.DESCENDANTS,
... 'module': ScopingPolicy.EXACT})
>>> ScopingPolicy.to_policies({})
Traceback (most recent call last):
...
KeyError: 'func'
"""
if policies is None:
policies = DEFAULT_SCOPING_POLICIES
if isinstance(policies, str):
policy = cls(policies)
return _ScopingPolicyDict(
dict.fromkeys(['func', 'class', 'module'], policy))
return _ScopingPolicyDict({'func': cls(policies['func']),
'class': cls(policies['class']),
'module': cls(policies['module'])})
@staticmethod
def _return_const(value):
def return_const(*_, **__):
return value
return return_const
@staticmethod
def _match_prefix(s, prefix, sep='.'):
return s == prefix or s.startswith(prefix + sep)
def _get_callable_filter_in_class(self, cls, is_class):
def func_is_child(other):
if not modules_are_equal(other):
return False
return other.__qualname__ == f'{cls.__qualname__}.{other.__name__}'
def modules_are_equal(other): # = sibling check
return cls.__module__ == other.__module__
def func_is_descdendant(other):
if not modules_are_equal(other):
return False
return other.__qualname__.startswith(cls.__qualname__ + '.')
return {'exact': (self._return_const(False)
if is_class else
func_is_child),
'children': func_is_child,
'descendants': func_is_descdendant,
'siblings': modules_are_equal,
'none': self._return_const(True)}[self.value]
def _get_callable_filter_in_module(self, mod, is_class):
def func_is_child(other):
return other.__module__ == mod.__name__
def func_is_descdendant(other):
return self._match_prefix(other.__module__, mod.__name__)
def func_is_cousin(other):
if func_is_descdendant(other):
return True
return self._match_prefix(other.__module__, parent)
parent, _, basename = mod.__name__.rpartition('.')
return {'exact': (self._return_const(False)
if is_class else
func_is_child),
'children': func_is_child,
'descendants': func_is_descdendant,
'siblings': (func_is_cousin # Only if a pkg
if basename else
func_is_descdendant),
'none': self._return_const(True)}[self.value]
def _get_module_filter_in_module(self, mod):
def module_is_descendant(other):
return other.__name__.startswith(mod.__name__ + '.')
def module_is_child(other):
return other.__name__.rpartition('.')[0] == mod.__name__
def module_is_sibling(other):
return other.__name__.startswith(parent + '.')
parent, _, basename = mod.__name__.rpartition('.')
return {'exact': self._return_const(False),
'children': module_is_child,
'descendants': module_is_descendant,
'siblings': (module_is_sibling # Only if a pkg
if basename else
self._return_const(False)),
'none': self._return_const(True)}[self.value]
# Sanity check in case we extended `ScopingPolicy` and forgot to update
# the corresponding methods
ScopingPolicy._check_class()
ScopingPolicyDict = TypedDict('ScopingPolicyDict',
{'func': Union[str, ScopingPolicy],
'class': Union[str, ScopingPolicy],
'module': Union[str, ScopingPolicy]})
_ScopingPolicyDict = TypedDict('_ScopingPolicyDict',
{'func': ScopingPolicy,
'class': ScopingPolicy,
'module': ScopingPolicy})