Aspect.__call__()   F
last analyzed

Complexity

Conditions 44

Size

Total Lines 121

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 44
dl 0
loc 121
rs 2
c 1
b 0
f 0

2 Methods

Rating   Name   Duplication   Size   Complexity  
F Aspect.advising_generator_wrapper() 0 80 28
F Aspect.advising_function_wrapper() 0 32 13

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like Aspect.__call__() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
from __future__ import print_function
2
3
import re
4
import sys
5
import warnings
6
from collections import deque
7
from functools import partial
8
from inspect import isclass
9
from inspect import isfunction
10
from inspect import isgenerator
11
from inspect import isgeneratorfunction
12
from inspect import ismethod
13
from inspect import ismethoddescriptor
14
from inspect import ismodule
15
from inspect import isroutine
16
from logging import getLogger
17
18
from .utils import PY2
19
from .utils import PY3
20
from .utils import Sentinel
21
from .utils import basestring
22
from .utils import force_bind
23
from .utils import logf
24
from .utils import make_method_matcher
25
from .utils import mimic
26
27
try:
28
    from types import InstanceType
29
except ImportError:
30
    InstanceType = None
31
32
try:
33
    import __builtin__
34
except ImportError:
35
    import builtins as __builtin__  # pylint: disable=F0401
36
37
try:
38
    from types import ClassType
39
except ImportError:
40
    ClassType = type
41
42
43
__all__ = 'weave', 'Aspect', 'Proceed', 'Return', 'ALL_METHODS', 'NORMAL_METHODS', 'ABSOLUTELY_ALL_METHODS'
44
__version__ = '1.4.2'
45
46
logger = getLogger(__name__)
47
logdebug = logf(logger.debug)
48
logexception = logf(logger.exception)
49
50
UNSPECIFIED = Sentinel('UNSPECIFIED')
51
ABSOLUTELLY_ALL_METHODS = re.compile('.*')
52
ABSOLUTELY_ALL_METHODS = ABSOLUTELLY_ALL_METHODS
53
ALL_METHODS = re.compile('(?!__getattribute__$)')
54
NORMAL_METHODS = re.compile('(?!__.*__$)')
55
VALID_IDENTIFIER = re.compile(r'^[^\W\d]\w*$', re.UNICODE if PY3 else 0)
56
57
58
class UnacceptableAdvice(RuntimeError):
59
    pass
60
61
62
class ExpectedGenerator(TypeError):
63
    pass
64
65
66
class ExpectedGeneratorFunction(ExpectedGenerator):
67
    pass
68
69
70
class ExpectedAdvice(TypeError):
71
    pass
72
73
74
class UnsupportedType(TypeError):
75
    pass
76
77
78
class Proceed(object):
79
    """
80
    Instruction for calling the decorated function. Can be used multiple times.
81
82
    If not used as an instance then the default args and kwargs are used.
83
    """
84
    __slots__ = 'args', 'kwargs'
85
86
    def __init__(self, *args, **kwargs):
87
        self.args = args
88
        self.kwargs = kwargs
89
90
91
class Return(object):
92
    """
93
    Instruction for returning a *optional* value.
94
95
    If not used as an instance then ``None`` is returned.
96
    """
97
    __slots__ = 'value',
98
99
    def __init__(self, value):
100
        self.value = value
101
102
103
class Aspect(object):
104
    """
105
    Container for the advice yielding generator. Can be used as a decorator on other function to change behavior
106
    according to the advices yielded from the generator.
107
108
    Args:
109
        advising_function (generator function): A generator function that yields :ref:`advices`.
110
        bind (bool): A convenience flag so you can access the cutpoint function (you'll get it as an argument).
111
112
    Usage::
113
114
        >>> @Aspect
115
        ... def my_decorator(*args, **kwargs):
116
        ...     print("Got called with args: %s kwargs: %s" % (args, kwargs))
117
        ...     result = yield
118
        ...     print(" ... and the result is: %s" % (result,))
119
        >>> @my_decorator
120
        ... def foo(a, b, c=1):
121
        ...     print((a, b, c))
122
        >>> foo(1, 2, c=3)
123
        Got called with args: (1, 2) kwargs: {'c': 3}
124
        (1, 2, 3)
125
         ... and the result is: None
126
127
    Normally you don't have access to the cutpoints (the functions you're going to use the aspect/decorator on) because
128
    you don't and should not call them directly. There are situations where you'd want to get the name or other data
129
    from the function. This is where you use the ``bind=True`` option::
130
131
        >>> @Aspect(bind=True)
132
        ... def my_decorator(cutpoint, *args, **kwargs):
133
        ...     print("`%s` got called with args: %s kwargs: %s" % (cutpoint.__name__, args, kwargs))
134
        ...     result = yield
135
        ...     print(" ... and the result is: %s" % (result,))
136
        >>> @my_decorator
137
        ... def foo(a, b, c=1):
138
        ...     print((a, b, c))
139
        >>> foo(1, 2, c=3)
140
        `foo` got called with args: (1, 2) kwargs: {'c': 3}
141
        (1, 2, 3)
142
         ... and the result is: None
143
144
    """
145
    __slots__ = 'advising_function', 'bind'
146
147
    def __new__(cls, advising_function=UNSPECIFIED, bind=False):
148
        if advising_function is UNSPECIFIED:
149
            return partial(cls, bind=bind)
150
        else:
151
            self = super(Aspect, cls).__new__(cls)
152
            self.__init__(advising_function, bind)
153
            return self
154
155
    def __init__(self, advising_function, bind=False):
156
        if not isgeneratorfunction(advising_function):
157
            raise ExpectedGeneratorFunction("advising_function %s must be a generator function." % advising_function)
158
        self.advising_function = advising_function
159
        self.bind = bind
160
161
    def __call__(self, cutpoint_function):
162
        if isgeneratorfunction(cutpoint_function):
163
            if PY3:
164
                from aspectlib.py3support import decorate_advising_generator_py3
165
                return decorate_advising_generator_py3(self.advising_function, cutpoint_function, self.bind)
166
            else:
167
                def advising_generator_wrapper(*args, **kwargs):
168
                    if self.bind:
169
                        advisor = self.advising_function(cutpoint_function, *args, **kwargs)
170
                    else:
171
                        advisor = self.advising_function(*args, **kwargs)
172
                    if not isgenerator(advisor):
173
                        raise ExpectedGenerator("advising_function %s did not return a generator." % self.advising_function)
174
                    try:
175
                        advice = next(advisor)
176
                        while True:
177
                            logdebug('Got advice %r from %s', advice, self.advising_function)
178
                            if advice is Proceed or advice is None or isinstance(advice, Proceed):
179
                                if isinstance(advice, Proceed):
180
                                    args = advice.args
181
                                    kwargs = advice.kwargs
182
                                gen = cutpoint_function(*args, **kwargs)
183
                                try:
184
                                    try:
185
                                        generated = next(gen)
186
                                    except StopIteration as exc:
187
                                        logexception("The cutpoint has been exhausted (early).")
188
                                        result = exc.args
189
                                        if result:
190
                                            if len(result) == 1:
191
                                                result = exc.args[0]
192
                                        else:
193
                                            result = None
194
                                    else:
195
                                        while True:
196
                                            try:
197
                                                sent = yield generated
198
                                            except GeneratorExit as exc:
199
                                                logexception("Got GeneratorExit while consuming the cutpoint")
200
                                                gen.close()
201
                                                raise exc
202
                                            except BaseException as exc:
203
                                                logexception("Got exception %r. Throwing it the cutpoint", exc)
204
                                                try:
205
                                                    generated = gen.throw(*sys.exc_info())
206
                                                except StopIteration as exc:
207
                                                    logexception("The cutpoint has been exhausted.")
208
                                                    result = exc.args
209
                                                    if result:
210
                                                        if len(result) == 1:
211
                                                            result = exc.args[0]
212
                                                    else:
213
                                                        result = None
214
                                                    break
215
                                            else:
216
                                                try:
217
                                                    if sent is None:
218
                                                        generated = next(gen)
219
                                                    else:
220
                                                        generated = gen.send(sent)
221
                                                except StopIteration as exc:
222
                                                    logexception("The cutpoint has been exhausted.")
223
                                                    result = exc.args
224
                                                    if result:
225
                                                        if len(result) == 1:
226
                                                            result = exc.args[0]
227
                                                    else:
228
                                                        result = None
229
                                                    break
230
                                except BaseException as exc:
231
                                    advice = advisor.throw(*sys.exc_info())
232
                                else:
233
                                    try:
234
                                        advice = advisor.send(result)
235
                                    except StopIteration:
236
                                        raise StopIteration(result)
237
                                finally:
238
                                    gen.close()
239
                            elif advice is Return:
240
                                return
241
                            elif isinstance(advice, Return):
242
                                raise StopIteration(advice.value)
243
                            else:
244
                                raise UnacceptableAdvice("Unknown advice %s" % advice)
245
                    finally:
246
                        advisor.close()
247
                return mimic(advising_generator_wrapper, cutpoint_function)
248
        else:
249
            def advising_function_wrapper(*args, **kwargs):
250
                if self.bind:
251
                    advisor = self.advising_function(cutpoint_function, *args, **kwargs)
252
                else:
253
                    advisor = self.advising_function(*args, **kwargs)
254
                if not isgenerator(advisor):
255
                    raise ExpectedGenerator("advising_function %s did not return a generator." % self.advising_function)
256
                try:
257
                    advice = next(advisor)
258
                    while True:
259
                        logdebug('Got advice %r from %s', advice, self.advising_function)
260
                        if advice is Proceed or advice is None or isinstance(advice, Proceed):
261
                            if isinstance(advice, Proceed):
262
                                args = advice.args
263
                                kwargs = advice.kwargs
264
                            try:
265
                                result = cutpoint_function(*args, **kwargs)
266
                            except Exception:
267
                                advice = advisor.throw(*sys.exc_info())
268
                            else:
269
                                try:
270
                                    advice = advisor.send(result)
271
                                except StopIteration:
272
                                    return result
273
                        elif advice is Return:
274
                            return
275
                        elif isinstance(advice, Return):
276
                            return advice.value
277
                        else:
278
                            raise UnacceptableAdvice("Unknown advice %s" % advice)
279
                finally:
280
                    advisor.close()
281
            return mimic(advising_function_wrapper, cutpoint_function)
282
283
284
class Fabric(object):
285
    pass
286
287
288
class Rollback(object):
289
    """
290
    When called, rollbacks all the patches and changes the :func:`weave` has done.
291
    """
292
    __slots__ = '_rollbacks'
293
294
    def __init__(self, rollback=None):
295
        if rollback is None:
296
            self._rollbacks = []
297
        elif isinstance(rollback, (list, tuple)):
298
            self._rollbacks = rollback
299
        else:
300
            self._rollbacks = [rollback]
301
302
    def merge(self, *others):
303
        self._rollbacks.extend(others)
304
305
    def __enter__(self):
306
        return self
307
308
    def __exit__(self, *_):
309
        for rollback in self._rollbacks:
310
            rollback()
311
        del self._rollbacks[:]
312
313
    rollback = __call__ = __exit__
314
315
316
class ObjectBag(object):
317
    def __init__(self):
318
        self._objects = {}
319
320
    def has(self, obj):
321
        if id(obj) in self._objects:
322
            logdebug('  --- ObjectBag ALREADY HAS %r', obj)
323
            return True
324
        else:
325
            self._objects[id(obj)] = obj
326
            return False
327
328
BrokenBag = type('BrokenBag', (), dict(has=lambda self, obj: False))()
329
330
331
class EmptyRollback(object):
332
    def __enter__(self):
333
        return self
334
335
    def __exit__(self, *_):
336
        pass
337
338
    rollback = __call__ = __exit__
339
340
Nothing = EmptyRollback()
341
342
343
def _checked_apply(aspects, function, module=None):
344
    logdebug('  applying aspects %s to function %s.', aspects, function)
345
    if callable(aspects):
346
        wrapper = aspects(function)
347
        assert callable(wrapper), 'Aspect %s did not return a callable (it return %s).' % (aspects, wrapper)
348
    else:
349
        wrapper = function
350
        for aspect in aspects:
351
            wrapper = aspect(wrapper)
352
            assert callable(wrapper), 'Aspect %s did not return a callable (it return %s).' % (aspect, wrapper)
353
    return mimic(wrapper, function, module=module)
354
355
356
def _check_name(name):
357
    if not VALID_IDENTIFIER.match(name):
358
        raise SyntaxError(
359
            "Could not match %r to %r. It should be a string of "
360
            "letters, numbers and underscore that starts with a letter or underscore." % (
361
                name, VALID_IDENTIFIER.pattern
362
            )
363
        )
364
365
366
def weave(target, aspects, **options):
367
    """
368
    Send a message to a recipient
369
370
    Args:
371
        target (string, class, instance, function or builtin):
372
            The object to weave.
373
        aspects (:py:obj:`aspectlib.Aspect`, function decorator or list of):
374
            The aspects to apply to the object.
375
        subclasses (bool):
376
            If ``True``, subclasses of target are weaved. *Only available for classes*
377
        aliases (bool):
378
            If ``True``, aliases of target are replaced.
379
        lazy (bool):
380
            If ``True`` only target's ``__init__`` method is patched, the rest of the methods are patched after
381
            ``__init__`` is called. *Only available for classes*.
382
        methods (list or regex or string):
383
            Methods from target to patch. *Only available for classes*
384
385
    Returns:
386
        aspectlib.Rollback: An object that can rollback the patches.
387
388
    Raises:
389
        TypeError: If target is a unacceptable object, or the specified options are not available for that type of
390
            object.
391
392
    .. versionchanged:: 0.4.0
393
394
        Replaced `only_methods`, `skip_methods`, `skip_magicmethods` options with `methods`.
395
        Renamed `on_init` option to `lazy`.
396
        Added `aliases` option.
397
        Replaced `skip_subclasses` option with `subclasses`.
398
    """
399
    if not callable(aspects):
400
        if not hasattr(aspects, '__iter__'):
401
            raise ExpectedAdvice('%s must be an `Aspect` instance, a callable or an iterable of.' % aspects)
402
        for obj in aspects:
403
            if not callable(obj):
404
                raise ExpectedAdvice('%s must be an `Aspect` instance or a callable.' % obj)
405
    assert target, "Can't weave falsy value %r." % target
406
    logdebug("weave (target=%s, aspects=%s, **options=%s)", target, aspects, options)
407
408
    bag = options.setdefault('bag', ObjectBag())
409
410
    if isinstance(target, (list, tuple)):
411
        return Rollback([
412
            weave(item, aspects, **options) for item in target
413
        ])
414
    elif isinstance(target, basestring):
415
        parts = target.split('.')
416
        for part in parts:
417
            _check_name(part)
418
419
        if len(parts) == 1:
420
            return weave_module(_import_module(part), aspects, **options)
421
422
        for pos in reversed(range(1, len(parts))):
423
            owner, name = '.'.join(parts[:pos]), '.'.join(parts[pos:])
424
            try:
425
                owner = _import_module(owner)
426
            except ImportError:
427
                continue
428
            else:
429
                break
430
        else:
431
            raise ImportError("Could not import %r. Last try was for %s" % (target, owner))
432
433
        if '.' in name:
434
            path, name = name.rsplit('.', 1)
435
            path = deque(path.split('.'))
436
            while path:
437
                owner = getattr(owner, path.popleft())
438
439
        logdebug("@ patching %s from %s ...", name, owner)
440
        obj = getattr(owner, name)
441
442
        if isinstance(obj, (type, ClassType)):
443
            logdebug("   .. as a class %r.", obj)
444
            return weave_class(
445
                obj, aspects,
446
                owner=owner, name=name, **options
447
            )
448
        elif callable(obj):  # or isinstance(obj, FunctionType) ??
449
            logdebug("   .. as a callable %r.", obj)
450
            if bag.has(obj):
451
                return Nothing
452
            return patch_module_function(owner, obj, aspects, force_name=name, **options)
453
        else:
454
            return weave(obj, aspects, **options)
455
456
    name = getattr(target, '__name__', None)
457
    if name and getattr(__builtin__, name, None) is target:
458
        if bag.has(target):
459
            return Nothing
460
        return patch_module_function(__builtin__, target, aspects, **options)
461
    elif PY3 and ismethod(target):
462
        if bag.has(target):
463
            return Nothing
464
        inst = target.__self__
465
        name = target.__name__
466
        logdebug("@ patching %r (%s) as instance method.", target, name)
467
        func = target.__func__
468
        setattr(inst, name, _checked_apply(aspects, func).__get__(inst, type(inst)))
469
        return Rollback(lambda: delattr(inst, name))
470
    elif PY3 and isfunction(target):
471
        if bag.has(target):
472
            return Nothing
473
        owner = _import_module(target.__module__)
474
        path = deque(target.__qualname__.split('.')[:-1])
475
        while path:
476
            owner = getattr(owner, path.popleft())
477
        name = target.__name__
478
        logdebug("@ patching %r (%s) as a property.", target, name)
479
        func = owner.__dict__[name]
480
        return patch_module(owner, name, _checked_apply(aspects, func), func, **options)
481
    elif PY2 and isfunction(target):
482
        if bag.has(target):
483
            return Nothing
484
        return patch_module_function(_import_module(target.__module__), target, aspects, **options)
485
    elif PY2 and ismethod(target):
486
        if target.im_self:
487
            if bag.has(target):
488
                return Nothing
489
            inst = target.im_self
490
            name = target.__name__
491
            logdebug("@ patching %r (%s) as instance method.", target, name)
492
            func = target.im_func
493
            setattr(inst, name, _checked_apply(aspects, func).__get__(inst, type(inst)))
494
            return Rollback(lambda: delattr(inst, name))
495
        else:
496
            klass = target.im_class
497
            name = target.__name__
498
            return weave(klass, aspects, methods='%s$' % name, **options)
499
    elif isclass(target):
500
        return weave_class(target, aspects, **options)
501
    elif ismodule(target):
502
        return weave_module(target, aspects, **options)
503
    elif type(target).__module__ not in ('builtins', '__builtin__') or InstanceType and isinstance(target, InstanceType):
504
        return weave_instance(target, aspects, **options)
505
    else:
506
        raise UnsupportedType("Can't weave object %s of type %s" % (target, type(target)))
507
508
509
def _rewrap_method(func, klass, aspect):
510
    if isinstance(func, staticmethod):
511
        if hasattr(func, '__func__'):
512
            return staticmethod(_checked_apply(aspect, func.__func__))
513
        else:
514
            return staticmethod(_checked_apply(aspect, func.__get__(None, klass)))
515
    elif isinstance(func, classmethod):
516
        if hasattr(func, '__func__'):
517
            return classmethod(_checked_apply(aspect, func.__func__))
518
        else:
519
            return classmethod(_checked_apply(aspect, func.__get__(None, klass).im_func))
520
    else:
521
        return _checked_apply(aspect, func)
522
523
524
def weave_instance(instance, aspect, methods=NORMAL_METHODS, lazy=False, bag=BrokenBag, **options):
525
    """
526
    Low-level weaver for instances.
527
528
    .. warning:: You should not use this directly.
529
530
    :returns: An :obj:`aspectlib.Rollback` object.
531
    """
532
    if bag.has(instance):
533
        return Nothing
534
535
    entanglement = Rollback()
536
    method_matches = make_method_matcher(methods)
537
    logdebug("weave_instance (module=%r, aspect=%s, methods=%s, lazy=%s, **options=%s)",
538
             instance, aspect, methods, lazy, options)
539
540
    def fixup(func):
541
        return func.__get__(instance, type(instance))
542
    fixed_aspect = aspect + [fixup] if isinstance(aspect, (list, tuple)) else [aspect, fixup]
543
544
    for attr in dir(instance):
545
        func = getattr(instance, attr)
546
        if method_matches(attr):
547
            if ismethod(func):
548
                if hasattr(func, '__func__'):
549
                    realfunc = func.__func__
550
                else:
551
                    realfunc = func.im_func
552
                entanglement.merge(
553
                    patch_module(instance, attr, _checked_apply(fixed_aspect, realfunc, module=None), **options)
554
                )
555
    return entanglement
556
557
558
def weave_module(module, aspect, methods=NORMAL_METHODS, lazy=False, bag=BrokenBag, **options):
559
    """
560
    Low-level weaver for "whole module weaving".
561
562
    .. warning:: You should not use this directly.
563
564
    :returns: An :obj:`aspectlib.Rollback` object.
565
    """
566
    if bag.has(module):
567
        return Nothing
568
569
    entanglement = Rollback()
570
    method_matches = make_method_matcher(methods)
571
    logdebug("weave_module (module=%r, aspect=%s, methods=%s, lazy=%s, **options=%s)",
572
             module, aspect, methods, lazy, options)
573
574
    for attr in dir(module):
575
        func = getattr(module, attr)
576
        if method_matches(attr):
577
            if isroutine(func):
578
                entanglement.merge(patch_module_function(module, func, aspect, force_name=attr, **options))
579
            elif isclass(func):
580
                entanglement.merge(
581
                    weave_class(func, aspect, owner=module, name=attr, methods=methods, lazy=lazy, bag=bag, **options),
582
                    #  it's not consistent with the other ways of weaving a class (it's never weaved as a routine).
583
                    #  therefore it's disabled until it's considered useful.
584
                    #  #patch_module_function(module, getattr(module, attr), aspect, force_name=attr, **options),
585
                )
586
    return entanglement
587
588
589
def weave_class(klass, aspect, methods=NORMAL_METHODS, subclasses=True, lazy=False,
590
                owner=None, name=None, aliases=True, bases=True, bag=BrokenBag):
591
    """
592
    Low-level weaver for classes.
593
594
    .. warning:: You should not use this directly.
595
    """
596
    assert isclass(klass), "Can't weave %r. Must be a class." % klass
597
598
    if bag.has(klass):
599
        return Nothing
600
601
    entanglement = Rollback()
602
    method_matches = make_method_matcher(methods)
603
    logdebug("weave_class (klass=%r, methods=%s, subclasses=%s, lazy=%s, owner=%s, name=%s, aliases=%s, bases=%s)",
604
             klass, methods, subclasses, lazy, owner, name, aliases, bases)
605
606
    if subclasses and hasattr(klass, '__subclasses__'):
607
        sub_targets = klass.__subclasses__()
608
        if sub_targets:
609
            logdebug("~ weaving subclasses: %s", sub_targets)
610
        for sub_class in sub_targets:
611
            if not issubclass(sub_class, Fabric):
612
                entanglement.merge(weave_class(sub_class, aspect,
613
                                               methods=methods, subclasses=subclasses, lazy=lazy, bag=bag))
614
    if lazy:
615
        def __init__(self, *args, **kwargs):
616
            super(SubClass, self).__init__(*args, **kwargs)
617
            for attr in dir(self):
618
                func = getattr(self, attr, None)
619
                if method_matches(attr) and attr not in wrappers and isroutine(func):
620
                    setattr(self, attr, _checked_apply(aspect, force_bind(func)).__get__(self, SubClass))
621
622
        wrappers = {
623
            '__init__': _checked_apply(aspect, __init__) if method_matches('__init__') else __init__
624
        }
625
        for attr, func in klass.__dict__.items():
626
            if method_matches(attr):
627
                if ismethoddescriptor(func):
628
                    wrappers[attr] = _rewrap_method(func, klass, aspect)
629
630
        logdebug(" * creating subclass with attributes %r", wrappers)
631
        name = name or klass.__name__
632
        SubClass = type(name, (klass, Fabric), wrappers)
633
        SubClass.__module__ = klass.__module__
634
        module = owner or _import_module(klass.__module__)
635
        entanglement.merge(patch_module(module, name, SubClass, original=klass, aliases=aliases))
636
    else:
637
        original = {}
638
        for attr, func in klass.__dict__.items():
639
            if method_matches(attr):
640
                if isroutine(func):
641
                    logdebug("@ patching attribute %r (original: %r).", attr, func)
642
                    setattr(klass, attr, _rewrap_method(func, klass, aspect))
643
                else:
644
                    continue
645
                original[attr] = func
646
        entanglement.merge(lambda: deque((
647
            setattr(klass, attr, func) for attr, func in original.items()
648
        ), maxlen=0))
649
        if bases:
650
            super_original = set()
651
            for sklass in _find_super_classes(klass):
652
                if sklass is not object:
653
                    for attr, func in sklass.__dict__.items():
654
                        if method_matches(attr) and attr not in original and attr not in super_original:
655
                            if isroutine(func):
656
                                logdebug("@ patching attribute %r (from superclass: %s, original: %r).",
657
                                         attr, sklass.__name__, func)
658
                                setattr(klass, attr, _rewrap_method(func, sklass, aspect))
659
                            else:
660
                                continue
661
                            super_original.add(attr)
662
            entanglement.merge(lambda: deque((
663
                delattr(klass, attr) for attr in super_original
664
            ), maxlen=0))
665
666
    return entanglement
667
668
669
def _find_super_classes(klass):
670
    if hasattr(klass, '__mro__'):
671
        for k in klass.__mro__:
672
            yield k
673
    else:
674
        for base in klass.__bases__:
675
            yield base
676
            for k in _find_super_classes(base):
677
                yield k
678
679
680
def _import_module(module):
681
    __import__(module)
682
    return sys.modules[module]
683
684
685
def patch_module(module, name, replacement, original=UNSPECIFIED, aliases=True, location=None, **_bogus_options):
686
    """
687
    Low-level attribute patcher.
688
689
    :param module module: Object to patch.
690
    :param str name: Attribute to patch
691
    :param replacement: The replacement value.
692
    :param original: The original value (in case the object beeing patched uses descriptors or is plain weird).
693
    :param bool aliases: If ``True`` patch all the attributes that have the same original value.
694
695
    :returns: An :obj:`aspectlib.Rollback` object.
696
    """
697
    rollback = Rollback()
698
    seen = False
699
    original = getattr(module, name) if original is UNSPECIFIED else original
700
    location = module.__name__ if hasattr(module, '__name__') else type(module).__module__
701
    target = module.__name__ if hasattr(module, '__name__') else type(module).__name__
702
    try:
703
        replacement.__module__ = location
704
    except (TypeError, AttributeError):
705
        pass
706
    for alias in dir(module):
707
        logdebug("alias:%s (%s)", alias, name)
708
        if hasattr(module, alias):
709
            obj = getattr(module, alias)
710
            logdebug("- %s:%s (%s)", obj, original, obj is original)
711
            if obj is original:
712
                if aliases or alias == name:
713
                    logdebug("= saving %s on %s.%s ...", replacement, target, alias)
714
                    setattr(module, alias, replacement)
715
                    rollback.merge(lambda alias=alias: setattr(module, alias, original))
716
                if alias == name:
717
                    seen = True
718
            elif alias == name:
719
                if ismethod(obj):
720
                    logdebug("= saving %s on %s.%s ...", replacement, target, alias)
721
                    setattr(module, alias, replacement)
722
                    rollback.merge(lambda alias=alias: setattr(module, alias, original))
723
                    seen = True
724
                else:
725
                    raise AssertionError("%s.%s = %s is not %s." % (module, alias, obj, original))
726
727
    if not seen:
728
        warnings.warn('Setting %s.%s to %s. There was no previous definition, probably patching the wrong module.' % (
729
            target, name, replacement
730
        ))
731
        logdebug("= saving %s on %s.%s ...", replacement, target, name)
732
        setattr(module, name, replacement)
733
        rollback.merge(lambda: setattr(module, name, original))
734
    return rollback
735
736
737
def patch_module_function(module, target, aspect, force_name=None, bag=BrokenBag, **options):
738
    """
739
    Low-level patcher for one function from a specified module.
740
741
    .. warning:: You should not use this directly.
742
743
    :returns: An :obj:`aspectlib.Rollback` object.
744
    """
745
    logdebug("patch_module_function (module=%s, target=%s, aspect=%s, force_name=%s, **options=%s",
746
             module, target, aspect, force_name, options)
747
    name = force_name or target.__name__
748
    return patch_module(module, name, _checked_apply(aspect, target, module=module), original=target, **options)
749