|
10 | 10 | import tempfile |
11 | 11 | import os |
12 | 12 | import sys |
| 13 | +import types |
| 14 | +import warnings |
13 | 15 | from argparse import ArgumentError, ArgumentParser |
14 | 16 |
|
15 | 17 | try: |
|
28 | 30 | # NOTE: This needs to be in sync with ../kernprof.py and __init__.py |
29 | 31 | __version__ = '4.3.0' |
30 | 32 |
|
| 33 | +# These objects are callables, but are defined in C so we can't handle |
| 34 | +# them anyway |
| 35 | +c_level_callable_types = (types.BuiltinFunctionType, |
| 36 | + types.BuiltinMethodType, |
| 37 | + types.ClassMethodDescriptorType, |
| 38 | + types.MethodDescriptorType, |
| 39 | + types.MethodWrapperType, |
| 40 | + types.WrapperDescriptorType) |
| 41 | + |
31 | 42 | is_function = inspect.isfunction |
32 | 43 |
|
33 | 44 |
|
| 45 | +def is_c_level_callable(func): |
| 46 | + """ |
| 47 | + Returns: |
| 48 | + func_is_c_level (bool): |
| 49 | + Whether a callable is defined at the C level (and is thus |
| 50 | + non-profilable). |
| 51 | + """ |
| 52 | + return isinstance(func, c_level_callable_types) |
| 53 | + |
| 54 | + |
34 | 55 | def load_ipython_extension(ip): |
35 | 56 | """ API for IPython to recognize this module as an IPython extension. |
36 | 57 | """ |
@@ -63,6 +84,8 @@ def _get_underlying_functions(func): |
63 | 84 | f'cannot get functions from {type(func)} objects') |
64 | 85 | if is_function(func): |
65 | 86 | return [func] |
| 87 | + if is_c_level_callable(func): |
| 88 | + return [] |
66 | 89 | return [type(func).__call__] |
67 | 90 |
|
68 | 91 |
|
@@ -90,12 +113,20 @@ def __call__(self, func): |
90 | 113 | start the profiler on function entry and stop it on function |
91 | 114 | exit. |
92 | 115 | """ |
93 | | - # Note: if `func` is a `types.FunctionType` which is already |
94 | | - # decorated by the profiler, the same object is returned; |
| 116 | + # The same object is returned when: |
| 117 | + # - `func` is a `types.FunctionType` which is already |
| 118 | + # decorated by the profiler, or |
| 119 | + # - `func` is any of the C-level callables that can't be |
| 120 | + # profiled |
95 | 121 | # otherwise, wrapper objects are always returned. |
96 | 122 | self.add_callable(func) |
97 | 123 | return self.wrap_callable(func) |
98 | 124 |
|
| 125 | + def wrap_callable(self, func): |
| 126 | + if is_c_level_callable(func): # Non-profilable |
| 127 | + return func |
| 128 | + return super().wrap_callable(func) |
| 129 | + |
99 | 130 | def add_callable(self, func): |
100 | 131 | """ |
101 | 132 | Register a function, method, property, partial object, etc. with |
@@ -132,50 +163,153 @@ def print_stats(self, stream=None, output_unit=None, stripzeros=False, |
132 | 163 | stream=stream, stripzeros=stripzeros, |
133 | 164 | details=details, summarize=summarize, sort=sort, rich=rich) |
134 | 165 |
|
135 | | - def _add_namespace(self, namespace, *, wrap=False): |
136 | | - """ |
137 | | - Add the members (callables (wrappers), methods, classes, ...) in |
138 | | - a namespace and profile them. |
139 | | -
|
140 | | - Args: |
141 | | - namespace (Union[ModuleType, type]): |
142 | | - Module or class to be profiled. |
143 | | - wrap (bool): |
144 | | - Whether to replace the wrapped members with wrappers |
145 | | - which automatically enable/disable the profiler when |
146 | | - called. |
147 | | -
|
148 | | - Returns: |
149 | | - n (int): |
150 | | - Number of members added to the profiler. |
151 | | - """ |
152 | | - return self._add_namespace_inner(set(), namespace, wrap=wrap) |
153 | | - |
154 | | - def _add_namespace_inner(self, duplicate_tracker, namespace, *, |
155 | | - wrap=False): |
| 166 | + def _add_namespace(self, duplicate_tracker, namespace, *, |
| 167 | + filter_scope=None, wrap=False): |
156 | 168 | count = 0 |
157 | | - add_cls = self._add_namespace_inner |
| 169 | + add_cls = self._add_namespace |
158 | 170 | add_func = self.add_callable |
| 171 | + wrap_failures = {} |
| 172 | + if filter_scope is None: |
| 173 | + def filter_scope(*_): |
| 174 | + return True |
| 175 | + |
159 | 176 | for attr, value in vars(namespace).items(): |
160 | 177 | if id(value) in duplicate_tracker: |
161 | 178 | continue |
162 | 179 | duplicate_tracker.add(id(value)) |
163 | 180 | if isinstance(value, type): |
164 | | - if add_cls(duplicate_tracker, value, wrap=wrap): |
165 | | - count += 1 |
| 181 | + if filter_scope(namespace, value): |
| 182 | + if add_cls(duplicate_tracker, value, wrap=wrap): |
| 183 | + count += 1 |
166 | 184 | continue |
167 | 185 | try: |
168 | | - func_needs_adding = add_func(value) |
| 186 | + if not add_func(value): |
| 187 | + continue |
169 | 188 | except TypeError: # Not a callable (wrapper) |
170 | 189 | continue |
171 | | - if not func_needs_adding: |
172 | | - continue |
173 | 190 | if wrap: |
174 | | - setattr(namespace, attr, self.wrap_callable(value)) |
| 191 | + wrapper = self.wrap_callable(value) |
| 192 | + if wrapper is not value: |
| 193 | + try: |
| 194 | + setattr(namespace, attr, wrapper) |
| 195 | + except (TypeError, AttributeError): |
| 196 | + # Corner case in case if a class/module don't |
| 197 | + # allow setting attributes (could e.g. happen |
| 198 | + # with some builtin/extension classes, but their |
| 199 | + # method should be in C anyway, so |
| 200 | + # `.add_callable()` should've returned 0 and we |
| 201 | + # shouldn't be here) |
| 202 | + wrap_failures[attr] = value |
175 | 203 | count += 1 |
| 204 | + if wrap_failures: |
| 205 | + msg = (f'cannot wrap {len(wrap_failures)} attribute(s) of ' |
| 206 | + f'{namespace!r} (`{{attr: value}}`): {wrap_failures!r}') |
| 207 | + warnings.warn(msg, stacklevel=2) |
176 | 208 | return count |
177 | 209 |
|
178 | | - add_class = add_module = _add_namespace |
| 210 | + def add_class(self, cls, *, match_scope='siblings', wrap=False): |
| 211 | + """ |
| 212 | + Add the members (callables (wrappers), methods, classes, ...) in |
| 213 | + a class' local namespace and profile them. |
| 214 | +
|
| 215 | + Args: |
| 216 | + cls (type): |
| 217 | + Class to be profiled. |
| 218 | + match_scope (Literal['exact', 'siblings', 'descendants', |
| 219 | + 'none']): |
| 220 | + Whether (and how) to match the scope of member classes |
| 221 | + and decide on whether to add them: |
| 222 | + - 'exact': only add classes defined locally in this |
| 223 | + namespace, i.e. in the body of `cls`, as "inner |
| 224 | + classes" |
| 225 | + - 'descendants': only add "inner classes", their "inner |
| 226 | + classes", and so on. |
| 227 | + - 'siblings': only add classes fulfilling 'descendants', |
| 228 | + or defined in the same module as `cls` |
| 229 | + - 'none': don't check scopes and add all classes in the |
| 230 | + namespace |
| 231 | + wrap (bool): |
| 232 | + Whether to replace the wrapped members with wrappers |
| 233 | + which automatically enable/disable the profiler when |
| 234 | + called. |
| 235 | +
|
| 236 | + Returns: |
| 237 | + n (int): |
| 238 | + Number of members added to the profiler. |
| 239 | + """ |
| 240 | + def class_is_child(cls, other): |
| 241 | + if not modules_are_equal(cls, other): |
| 242 | + return False |
| 243 | + return other.__qualname__ == f'{cls.__qualname__}.{other.__name__}' |
| 244 | + |
| 245 | + def modules_are_equal(cls, other): # = sibling check |
| 246 | + return cls.__module__ == other.__module__ |
| 247 | + |
| 248 | + def class_is_descendant(cls, other): |
| 249 | + if not modules_are_equal(cls, other): |
| 250 | + return False |
| 251 | + return other.__qualname__.startswith(cls.__qualname__ + '.') |
| 252 | + |
| 253 | + filter_scope = {'exact': class_is_child, |
| 254 | + 'descendants': class_is_descendant, |
| 255 | + 'siblings': modules_are_equal, |
| 256 | + 'none': None}[match_scope] |
| 257 | + return self._add_namespace(set(), cls, |
| 258 | + filter_scope=filter_scope, wrap=wrap) |
| 259 | + |
| 260 | + def add_module(self, mod, *, match_scope='siblings', wrap=False): |
| 261 | + """ |
| 262 | + Add the members (callables (wrappers), methods, classes, ...) in |
| 263 | + a module's local namespace and profile them. |
| 264 | +
|
| 265 | + Args: |
| 266 | + mod (ModuleType): |
| 267 | + Module to be profiled. |
| 268 | + match_scope (Literal['exact', 'siblings', 'descendants', |
| 269 | + 'none']): |
| 270 | + Whether (and how) to match the scope of member classes |
| 271 | + and decide on whether to add them: |
| 272 | + - 'exact': only add classes defined locally in this |
| 273 | + namespace, i.e. in the body of `mod` |
| 274 | + - 'descendants': only add locally-defined classes, |
| 275 | + classes locally defined in their bodies, and so on |
| 276 | + - 'siblings': only add classes fulfilling 'descendants', |
| 277 | + or defined in sibling modules/subpackages to `mod` (if |
| 278 | + `mod` is part of a package) |
| 279 | + - 'none': don't check scopes and add all classes in the |
| 280 | + namespace |
| 281 | + wrap (bool): |
| 282 | + Whether to replace the wrapped members with wrappers |
| 283 | + which automatically enable/disable the profiler when |
| 284 | + called. |
| 285 | +
|
| 286 | + Returns: |
| 287 | + n (int): |
| 288 | + Number of members added to the profiler. |
| 289 | + """ |
| 290 | + def match_prefix(s: str, prefix: str, sep: str = '.') -> bool: |
| 291 | + return s == prefix or s.startswith(prefix + sep) |
| 292 | + |
| 293 | + def class_is_child(mod, other): |
| 294 | + return other.__module__ == mod.__name__ |
| 295 | + |
| 296 | + def class_is_descendant(mod, other): |
| 297 | + return match_prefix(other.__module__, mod.__name__) |
| 298 | + |
| 299 | + def class_is_cousin(mod, other): |
| 300 | + if class_is_descendant(mod, other): |
| 301 | + return True |
| 302 | + return match_prefix(other.__module__, parent) |
| 303 | + |
| 304 | + parent, _, basename = mod.__name__.rpartition('.') |
| 305 | + filter_scope = {'exact': class_is_child, |
| 306 | + 'descendants': class_is_descendant, |
| 307 | + 'siblings': (class_is_cousin # Only if a pkg |
| 308 | + if basename else |
| 309 | + class_is_descendant), |
| 310 | + 'none': None}[match_scope] |
| 311 | + return self._add_namespace(set(), mod, |
| 312 | + filter_scope=filter_scope, wrap=wrap) |
179 | 313 |
|
180 | 314 |
|
181 | 315 | # This could be in the ipython_extension submodule, |
|
0 commit comments