from __future__ import annotations
from enum import auto
from types import FunctionType, MappingProxyType, ModuleType
from typing import Callable, Literal, TypedDict, cast, overload, Union
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: ScopingPolicyDict = {
'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: object, **kwargs: object) -> None:
"""
Call :py:meth:`_check_class`.
"""
super().__init_subclass__(*args, **kwargs)
cls._check_class()
@classmethod
def _check_class(cls) -> None:
"""
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
@overload
def get_filter(
self, namespace: type | ModuleType, obj_type: Literal['func']
) -> Callable[[Callable], bool]: ...
@overload
def get_filter(
self, namespace: type | ModuleType, obj_type: Literal['class']
) -> Callable[[type], bool]: ...
@overload
def get_filter(
self, namespace: type | ModuleType, obj_type: Literal['module']
) -> Callable[[ModuleType], bool]: ...
[docs]
def get_filter(self, namespace: type | ModuleType, obj_type: str):
"""
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(
cast(ModuleType, namespace)
)
if is_class:
return self._get_callable_filter_in_class(
cast(type, namespace), is_class=(obj_type == 'class')
)
return self._get_callable_filter_in_module(
cast(ModuleType, namespace), is_class=(obj_type == 'class')
)
[docs]
@classmethod
def to_policies(
cls, policies: str | ScopingPolicy | ScopingPolicyDict | None = None
) -> _ScopingPolicyDict:
"""
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(
{
'func': policy,
'class': policy,
'module': policy,
}
)
return _ScopingPolicyDict(
{
'func': cls(policies['func']),
'class': cls(policies['class']),
'module': cls(policies['module']),
}
)
@staticmethod
def _return_const(value: bool) -> Callable[[object], bool]:
def return_const(*_, **__):
return value
return return_const
@staticmethod
def _match_prefix(s: str, prefix: str, sep: str = '.') -> bool:
return s == prefix or s.startswith(prefix + sep)
def _get_callable_filter_in_class(
self, cls: type, is_class: bool
) -> Callable[[FunctionType | type], bool]:
def func_is_child(other: FunctionType | type):
if not modules_are_equal(other):
return False
return other.__qualname__ == f'{cls.__qualname__}.{other.__name__}'
def modules_are_equal(other: FunctionType | type): # = sibling check
return cls.__module__ == other.__module__
def func_is_descdendant(other: FunctionType | type):
if not modules_are_equal(other):
return False
return other.__qualname__.startswith(cls.__qualname__ + '.')
policies: dict[str, Callable[[FunctionType | type], bool]] = {
'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),
}
return policies[self.value]
def _get_callable_filter_in_module(
self, mod: ModuleType, is_class: bool
) -> Callable[[FunctionType | type], bool]:
def func_is_child(other: FunctionType | type):
return other.__module__ == mod.__name__
def func_is_descdendant(other: FunctionType | type):
return self._match_prefix(other.__module__, mod.__name__)
def func_is_cousin(other: FunctionType | type):
if func_is_descdendant(other):
return True
return self._match_prefix(other.__module__, parent)
parent, _, basename = mod.__name__.rpartition('.')
policies: dict[str, Callable[[FunctionType | type], bool]] = {
'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),
}
return policies[self.value]
def _get_module_filter_in_module(
self, mod: ModuleType
) -> Callable[[ModuleType], bool]:
def module_is_descendant(other: ModuleType):
return other.__name__.startswith(mod.__name__ + '.')
def module_is_child(other: ModuleType):
return other.__name__.rpartition('.')[0] == mod.__name__
def module_is_sibling(other: ModuleType):
return other.__name__.startswith(parent + '.')
parent, _, basename = mod.__name__.rpartition('.')
policies: dict[str, Callable[[ModuleType], bool]] = {
'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),
}
return policies[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},
)