Completed
Push — master ( e2f766...6a3298 )
by Ionel Cristian
01:41
created

Aspect.advising_generator_wrapper()   F

Complexity

Conditions 28

Size

Total Lines 80

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 28
dl 0
loc 80
rs 2.3269
c 1
b 0
f 0

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.advising_generator_wrapper() 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.0'
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
        print(owner, target.__module__)
476
        while path:
477
            owner = getattr(owner, path.popleft())
478
            print(owner, )
479
        name = target.__name__
480
        logdebug("@ patching %r (%s) as a property.", target, name)
481
        func = owner.__dict__[name]
482
        return patch_module(owner, name, _checked_apply(aspects, func), func, **options)
483
    elif PY2 and isfunction(target):
484
        if bag.has(target):
485
            return Nothing
486
        return patch_module_function(_import_module(target.__module__), target, aspects, **options)
487
    elif PY2 and ismethod(target):
488
        if target.im_self:
489
            if bag.has(target):
490
                return Nothing
491
            inst = target.im_self
492
            name = target.__name__
493
            logdebug("@ patching %r (%s) as instance method.", target, name)
494
            func = target.im_func
495
            setattr(inst, name, _checked_apply(aspects, func).__get__(inst, type(inst)))
496
            return Rollback(lambda: delattr(inst, name))
497
        else:
498
            klass = target.im_class
499
            name = target.__name__
500
            return weave(klass, aspects, methods='%s$' % name, **options)
501
    elif isclass(target):
502
        return weave_class(target, aspects, **options)
503
    elif ismodule(target):
504
        return weave_module(target, aspects, **options)
505
    elif type(target).__module__ not in ('builtins', '__builtin__') or InstanceType and isinstance(target, InstanceType):
506
        return weave_instance(target, aspects, **options)
507
    else:
508
        raise UnsupportedType("Can't weave object %s of type %s" % (target, type(target)))
509
510
511
def _rewrap_method(func, klass, aspect):
512
    if isinstance(func, staticmethod):
513
        if hasattr(func, '__func__'):
514
            return staticmethod(_checked_apply(aspect, func.__func__))
515
        else:
516
            return staticmethod(_checked_apply(aspect, func.__get__(None, klass)))
517
    elif isinstance(func, classmethod):
518
        if hasattr(func, '__func__'):
519
            return classmethod(_checked_apply(aspect, func.__func__))
520
        else:
521
            return classmethod(_checked_apply(aspect, func.__get__(None, klass).im_func))
522
    else:
523
        return _checked_apply(aspect, func)
524
525
526
def weave_instance(instance, aspect, methods=NORMAL_METHODS, lazy=False, bag=BrokenBag, **options):
527
    """
528
    Low-level weaver for instances.
529
530
    .. warning:: You should not use this directly.
531
532
    :returns: An :obj:`aspectlib.Rollback` object.
533
    """
534
    if bag.has(instance):
535
        return Nothing
536
537
    entanglement = Rollback()
538
    method_matches = make_method_matcher(methods)
539
    logdebug("weave_instance (module=%r, aspect=%s, methods=%s, lazy=%s, **options=%s)",
540
             instance, aspect, methods, lazy, options)
541
542
    def fixup(func):
543
        return func.__get__(instance, type(instance))
544
    fixed_aspect = aspect + [fixup] if isinstance(aspect, (list, tuple)) else [aspect, fixup]
545
546
    for attr in dir(instance):
547
        func = getattr(instance, attr)
548
        if method_matches(attr):
549
            if ismethod(func):
550
                if hasattr(func, '__func__'):
551
                    realfunc = func.__func__
552
                else:
553
                    realfunc = func.im_func
554
                entanglement.merge(
555
                    patch_module(instance, attr, _checked_apply(fixed_aspect, realfunc, module=None), **options)
556
                )
557
    return entanglement
558
559
560
def weave_module(module, aspect, methods=NORMAL_METHODS, lazy=False, bag=BrokenBag, **options):
561
    """
562
    Low-level weaver for "whole module weaving".
563
564
    .. warning:: You should not use this directly.
565
566
    :returns: An :obj:`aspectlib.Rollback` object.
567
    """
568
    if bag.has(module):
569
        return Nothing
570
571
    entanglement = Rollback()
572
    method_matches = make_method_matcher(methods)
573
    logdebug("weave_module (module=%r, aspect=%s, methods=%s, lazy=%s, **options=%s)",
574
             module, aspect, methods, lazy, options)
575
576
    for attr in dir(module):
577
        func = getattr(module, attr)
578
        if method_matches(attr):
579
            if isroutine(func):
580
                entanglement.merge(patch_module_function(module, func, aspect, force_name=attr, **options))
581
            elif isclass(func):
582
                entanglement.merge(
583
                    weave_class(func, aspect, owner=module, name=attr, methods=methods, lazy=lazy, bag=bag, **options),
584
                    #  it's not consistent with the other ways of weaving a class (it's never weaved as a routine).
585
                    #  therefore it's disabled until it's considered useful.
586
                    #  #patch_module_function(module, getattr(module, attr), aspect, force_name=attr, **options),
587
                )
588
    return entanglement
589
590
591
def weave_class(klass, aspect, methods=NORMAL_METHODS, subclasses=True, lazy=False,
592
                owner=None, name=None, aliases=True, bases=True, bag=BrokenBag):
593
    """
594
    Low-level weaver for classes.
595
596
    .. warning:: You should not use this directly.
597
    """
598
    assert isclass(klass), "Can't weave %r. Must be a class." % klass
599
600
    if bag.has(klass):
601
        return Nothing
602
603
    entanglement = Rollback()
604
    method_matches = make_method_matcher(methods)
605
    logdebug("weave_class (klass=%r, methods=%s, subclasses=%s, lazy=%s, owner=%s, name=%s, aliases=%s, bases=%s)",
606
             klass, methods, subclasses, lazy, owner, name, aliases, bases)
607
608
    if subclasses and hasattr(klass, '__subclasses__'):
609
        sub_targets = klass.__subclasses__()
610
        if sub_targets:
611
            logdebug("~ weaving subclasses: %s", sub_targets)
612
        for sub_class in sub_targets:
613
            if not issubclass(sub_class, Fabric):
614
                entanglement.merge(weave_class(sub_class, aspect,
615
                                               methods=methods, subclasses=subclasses, lazy=lazy, bag=bag))
616
    if lazy:
617
        def __init__(self, *args, **kwargs):
618
            super(SubClass, self).__init__(*args, **kwargs)
619
            for attr in dir(self):
620
                func = getattr(self, attr, None)
621
                if method_matches(attr) and attr not in wrappers and isroutine(func):
622
                    setattr(self, attr, _checked_apply(aspect, force_bind(func)).__get__(self, SubClass))
623
624
        wrappers = {
625
            '__init__': _checked_apply(aspect, __init__) if method_matches('__init__') else __init__
626
        }
627
        for attr, func in klass.__dict__.items():
628
            if method_matches(attr):
629
                if ismethoddescriptor(func):
630
                    wrappers[attr] = _rewrap_method(func, klass, aspect)
631
632
        logdebug(" * creating subclass with attributes %r", wrappers)
633
        name = name or klass.__name__
634
        SubClass = type(name, (klass, Fabric), wrappers)
635
        SubClass.__module__ = klass.__module__
636
        module = owner or _import_module(klass.__module__)
637
        entanglement.merge(patch_module(module, name, SubClass, original=klass, aliases=aliases))
638
    else:
639
        original = {}
640
        for attr, func in klass.__dict__.items():
641
            if method_matches(attr):
642
                if isroutine(func):
643
                    logdebug("@ patching attribute %r (original: %r).", attr, func)
644
                    setattr(klass, attr, _rewrap_method(func, klass, aspect))
645
                else:
646
                    continue
647
                original[attr] = func
648
        entanglement.merge(lambda: deque((
649
            setattr(klass, attr, func) for attr, func in original.items()
650
        ), maxlen=0))
651
        if bases:
652
            super_original = set()
653
            for sklass in _find_super_classes(klass):
654
                if sklass is not object:
655
                    for attr, func in sklass.__dict__.items():
656
                        if method_matches(attr) and attr not in original and attr not in super_original:
657
                            if isroutine(func):
658
                                logdebug("@ patching attribute %r (from superclass: %s, original: %r).",
659
                                         attr, sklass.__name__, func)
660
                                setattr(klass, attr, _rewrap_method(func, sklass, aspect))
661
                            else:
662
                                continue
663
                            super_original.add(attr)
664
            entanglement.merge(lambda: deque((
665
                delattr(klass, attr) for attr in super_original
666
            ), maxlen=0))
667
668
    return entanglement
669
670
671
def _find_super_classes(klass):
672
    if hasattr(klass, '__mro__'):
673
        for k in klass.__mro__:
674
            yield k
675
    else:
676
        for base in klass.__bases__:
677
            yield base
678
            for k in _find_super_classes(base):
679
                yield k
680
681
682
def _import_module(module):
683
    __import__(module)
684
    return sys.modules[module]
685
686
687
def patch_module(module, name, replacement, original=UNSPECIFIED, aliases=True, location=None, **_bogus_options):
688
    """
689
    Low-level attribute patcher.
690
691
    :param module module: Object to patch.
692
    :param str name: Attribute to patch
693
    :param replacement: The replacement value.
694
    :param original: The original value (in case the object beeing patched uses descriptors or is plain weird).
695
    :param bool aliases: If ``True`` patch all the attributes that have the same original value.
696
697
    :returns: An :obj:`aspectlib.Rollback` object.
698
    """
699
    rollback = Rollback()
700
    seen = False
701
    original = getattr(module, name) if original is UNSPECIFIED else original
702
    location = module.__name__ if hasattr(module, '__name__') else type(module).__module__
703
    target = module.__name__ if hasattr(module, '__name__') else type(module).__name__
704
    try:
705
        replacement.__module__ = location
706
    except (TypeError, AttributeError):
707
        pass
708
    for alias in dir(module):
709
        logdebug("alias:%s (%s)", alias, name)
710
        if hasattr(module, alias):
711
            obj = getattr(module, alias)
712
            logdebug("- %s:%s (%s)", obj, original, obj is original)
713
            if obj is original:
714
                if aliases or alias == name:
715
                    logdebug("= saving %s on %s.%s ...", replacement, target, alias)
716
                    setattr(module, alias, replacement)
717
                    rollback.merge(lambda alias=alias: setattr(module, alias, original))
718
                if alias == name:
719
                    seen = True
720
            elif alias == name:
721
                if ismethod(obj):
722
                    logdebug("= saving %s on %s.%s ...", replacement, target, alias)
723
                    setattr(module, alias, replacement)
724
                    rollback.merge(lambda alias=alias: setattr(module, alias, original))
725
                    seen = True
726
                else:
727
                    raise AssertionError("%s.%s = %s is not %s." % (module, alias, obj, original))
728
729
    if not seen:
730
        warnings.warn('Setting %s.%s to %s. There was no previous definition, probably patching the wrong module.' % (
731
            target, name, replacement
732
        ))
733
        logdebug("= saving %s on %s.%s ...", replacement, target, name)
734
        setattr(module, name, replacement)
735
        rollback.merge(lambda: setattr(module, name, original))
736
    return rollback
737
738
739
def patch_module_function(module, target, aspect, force_name=None, bag=BrokenBag, **options):
740
    """
741
    Low-level patcher for one function from a specified module.
742
743
    .. warning:: You should not use this directly.
744
745
    :returns: An :obj:`aspectlib.Rollback` object.
746
    """
747
    logdebug("patch_module_function (module=%s, target=%s, aspect=%s, force_name=%s, **options=%s",
748
             module, target, aspect, force_name, options)
749
    name = force_name or target.__name__
750
    return patch_module(module, name, _checked_apply(aspect, target, module=module), original=target, **options)
751