File: scoping_policy.py

package info (click to toggle)
python-line-profiler 5.0.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,256 kB
  • sloc: python: 8,119; sh: 810; ansic: 297; makefile: 14
file content (311 lines) | stat: -rw-r--r-- 11,856 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
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'})


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

    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'))

    @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})