1# Copyright 2019 The Cirq Developers
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""A more flexible abstract base class metaclass ABCMetaImplementAnyOneOf."""
15
16import abc
17import functools
18from typing import cast, Callable, Set, TypeVar
19
20# Required due to PEP 560
21try:
22    # python 3.6 class for generic metaclasses
23    from typing import GenericMeta  # type: ignore
24except ImportError:
25    # In python 3.7, GenericMeta doesn't exist but we don't need it
26    class GenericMeta(type):  # type: ignore
27        pass
28
29
30T = TypeVar('T')
31
32
33def alternative(*, requires: str, implementation: T) -> Callable[[T], T]:
34    """A decorator indicating an abstract method with an alternative default
35    implementation.
36
37    This decorator may be used multiple times on the same function to specify
38    multiple alternatives.  If multiple alternatives are available, the
39    outermost (lowest line number) alternative is used.
40
41    Usage:
42        class Parent(metaclass=ABCMetaImplementAnyOneOf):
43            def _default_do_a_using_b(self, ...):
44                ...
45            def _default_do_a_using_c(self, ...):
46                ...
47
48            # Abstract method with alternatives
49            @alternative(requires='do_b', implementation=_default_do_a_using_b)
50            @alternative(requires='do_c', implementation=_default_do_a_using_c)
51            def do_a(self, ...):
52                '''Method docstring.'''
53
54            # Abstract or concrete methods `do_b` and `do_c`:
55            ...
56
57        class Child(Parent):
58            def do_b(self):
59                ...
60
61        child = Child()
62        child.do_a(...)
63
64    Arguments:
65        requires: The name of another abstract method in the same class that
66            `implementation` needs to be implemented.
67        implementation: A function that uses the method named by requires to
68            implement the default behavior of the wrapped abstract method.  This
69            function must have the same signature as the decorated function.
70    """
71
72    def decorator(func: T) -> T:
73        alternatives = getattr(func, '_abstract_alternatives_', [])
74        alternatives.insert(0, (requires, implementation))
75        setattr(func, '_abstract_alternatives_', alternatives)
76        return func
77
78    return decorator
79
80
81class ABCMetaImplementAnyOneOf(abc.ABCMeta):
82    """A metaclass extending `abc.ABCMeta` for defining abstract base classes
83    (ABCs) with more flexibility in which methods must be overridden.
84
85    Use this metaclass in the same way as `abc.ABCMeta` to create an ABC.
86
87    In addition to the decorators in the` abc` module, the decorator
88    `@alternative(...)` may be used.
89    """
90
91    def __new__(mcls, name, bases, namespace, **kwargs):
92        cls = super().__new__(mcls, name, bases, namespace, **kwargs)
93        implemented_by = {}
94
95        def has_some_implementation(name: str) -> bool:
96            if name in implemented_by:
97                return True
98            try:
99                value = getattr(cls, name)
100            except AttributeError:
101                raise TypeError(
102                    'A method named \'{}\' was listed as a possible '
103                    'implementation alternative but it does not exist in the '
104                    'definition of {!r}.'.format(name, cls)
105                )
106            if getattr(value, '__isabstractmethod__', False):
107                return False
108            if hasattr(value, '_abstract_alternatives_'):
109                return False
110            return True
111
112        def find_next_implementations(all_names: Set[str]) -> bool:
113            next_implemented_by = {}
114            for name in all_names:
115                if has_some_implementation(name):
116                    continue
117                value = getattr(cls, name, None)
118                if not hasattr(value, '_abstract_alternatives_'):
119                    continue
120                for alt_name, impl in getattr(value, '_abstract_alternatives_'):
121                    if has_some_implementation(alt_name):
122                        next_implemented_by[name] = impl
123                        break
124            implemented_by.update(next_implemented_by)
125            return bool(next_implemented_by)
126
127        # Find all abstract methods (methods that haven't been implemented or
128        # don't have an implemented alternative).
129        all_names = set(alt_name for alt_name in namespace.keys() if hasattr(cls, alt_name))
130        for base in bases:
131            all_names.update(getattr(base, '__abstractmethods__', set()))
132            all_names.update(
133                alt_name for alt_name, _ in getattr(base, '_implemented_by_', {}).items()
134            )
135        while find_next_implementations(all_names):
136            pass
137        abstracts = frozenset(name for name in all_names if not has_some_implementation(name))
138        # Replace the abstract methods with their default implementations.
139        for name, default_impl in implemented_by.items():
140            abstract_method = getattr(cls, name)
141
142            def wrap_scope(impl: T) -> T:
143                def impl_of_abstract(*args, **kwargs):
144                    return impl(*args, **kwargs)
145
146                functools.update_wrapper(impl_of_abstract, abstract_method)
147                return cast(T, impl_of_abstract)
148
149            impl_of_abstract = wrap_scope(default_impl)
150            impl_of_abstract._abstract_alternatives_ = abstract_method._abstract_alternatives_
151            setattr(cls, name, impl_of_abstract)
152        # If __abstractmethods__ is non-empty, this is an abstract class and
153        # can't be instantiated.
154        cls.__abstractmethods__ |= abstracts  # Add to the set made by ABCMeta
155        cls._implemented_by_ = implemented_by
156        return cls
157
158
159class GenericMetaImplementAnyOneOf(GenericMeta, ABCMetaImplementAnyOneOf):
160    """Generic version of ABCMetaImplementAnyOneOf.
161
162    Classes which inherit from Generic[T] must use this type instead of
163    ABCMetaImplementAnyOneOf due to https://github.com/python/typing/issues/449.
164
165    This issue is specific to python3.6; this class can be removed when Cirq
166    python3.6 support is turned down.
167    """
168