1import sys
2from typing import Any, Callable, Dict, Mapping, TypeVar, Union, overload
3
4from ._exceptions import TypedAttributeLookupError
5
6if sys.version_info >= (3, 8):
7    from typing import final
8else:
9    from typing_extensions import final
10
11T_Attr = TypeVar('T_Attr')
12T_Default = TypeVar('T_Default')
13undefined = object()
14
15
16def typed_attribute() -> Any:
17    """Return a unique object, used to mark typed attributes."""
18    return object()
19
20
21class TypedAttributeSet:
22    """
23    Superclass for typed attribute collections.
24
25    Checks that every public attribute of every subclass has a type annotation.
26    """
27
28    def __init_subclass__(cls) -> None:
29        annotations: Dict[str, Any] = getattr(cls, '__annotations__', {})
30        for attrname in dir(cls):
31            if not attrname.startswith('_') and attrname not in annotations:
32                raise TypeError(f'Attribute {attrname!r} is missing its type annotation')
33
34        super().__init_subclass__()
35
36
37class TypedAttributeProvider:
38    """Base class for classes that wish to provide typed extra attributes."""
39
40    @property
41    def extra_attributes(self) -> Mapping[T_Attr, Callable[[], T_Attr]]:
42        """
43        A mapping of the extra attributes to callables that return the corresponding values.
44
45        If the provider wraps another provider, the attributes from that wrapper should also be
46        included in the returned mapping (but the wrapper may override the callables from the
47        wrapped instance).
48
49        """
50        return {}
51
52    @overload
53    def extra(self, attribute: T_Attr) -> T_Attr:
54        ...
55
56    @overload
57    def extra(self, attribute: T_Attr, default: T_Default) -> Union[T_Attr, T_Default]:
58        ...
59
60    @final
61    def extra(self, attribute: Any, default: object = undefined) -> object:
62        """
63        extra(attribute, default=undefined)
64
65        Return the value of the given typed extra attribute.
66
67        :param attribute: the attribute (member of a :class:`~TypedAttributeSet`) to look for
68        :param default: the value that should be returned if no value is found for the attribute
69        :raises ~anyio.TypedAttributeLookupError: if the search failed and no default value was
70            given
71
72        """
73        try:
74            return self.extra_attributes[attribute]()
75        except KeyError:
76            if default is undefined:
77                raise TypedAttributeLookupError('Attribute not found') from None
78            else:
79                return default
80