From c4c18a7b427633c0519016d762dc141ae743e41b Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Mon, 7 Aug 2023 09:57:29 -0700 Subject: [PATCH] Type Complete config and settings (#31422) * Type Complete config and settings * Fix typevar creation * Replace a Any * Improve docstring * Update sdk/core/azure-core/azure/core/configuration.py Co-authored-by: Krista Pratico --------- Co-authored-by: Krista Pratico --- .../azure-core/azure/core/configuration.py | 40 +++++---- sdk/core/azure-core/azure/core/settings.py | 83 ++++++++++++------- .../azure-core/azure/core/tracing/common.py | 4 +- 3 files changed, 77 insertions(+), 50 deletions(-) diff --git a/sdk/core/azure-core/azure/core/configuration.py b/sdk/core/azure-core/azure/core/configuration.py index 790747c5f613..bdae07d264a6 100644 --- a/sdk/core/azure-core/azure/core/configuration.py +++ b/sdk/core/azure-core/azure/core/configuration.py @@ -23,15 +23,23 @@ # IN THE SOFTWARE. # # -------------------------------------------------------------------------- -from typing import Union, Optional, TYPE_CHECKING +from __future__ import annotations +from typing import Union, Optional, Any, Generic, TypeVar, TYPE_CHECKING + +HTTPResponseType = TypeVar("HTTPResponseType") +HTTPRequestType = TypeVar("HTTPRequestType") if TYPE_CHECKING: from .pipeline.policies import HTTPPolicy, AsyncHTTPPolicy, SansIOHTTPPolicy - AnyPolicy = Union[HTTPPolicy, AsyncHTTPPolicy, SansIOHTTPPolicy] + AnyPolicy = Union[ + HTTPPolicy[HTTPRequestType, HTTPResponseType], + AsyncHTTPPolicy[HTTPRequestType, HTTPResponseType], + SansIOHTTPPolicy[HTTPRequestType, HTTPResponseType], + ] -class Configuration: # pylint: disable=too-many-instance-attributes +class Configuration(Generic[HTTPRequestType, HTTPResponseType]): # pylint: disable=too-many-instance-attributes """Provides the home for all of the configurable policies in the pipeline. A new Configuration object provides no default policies and does not specify in what @@ -63,39 +71,39 @@ class Configuration: # pylint: disable=too-many-instance-attributes :caption: Creates the service configuration and adds policies. """ - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any) -> None: # Headers (sent with every request) - self.headers_policy: "Optional[AnyPolicy]" = None + self.headers_policy: Optional[AnyPolicy[HTTPRequestType, HTTPResponseType]] = None # Proxy settings (Currently used to configure transport, could be pipeline policy instead) - self.proxy_policy: "Optional[AnyPolicy]" = None + self.proxy_policy: Optional[AnyPolicy[HTTPRequestType, HTTPResponseType]] = None # Redirect configuration - self.redirect_policy: "Optional[AnyPolicy]" = None + self.redirect_policy: Optional[AnyPolicy[HTTPRequestType, HTTPResponseType]] = None # Retry configuration - self.retry_policy: "Optional[AnyPolicy]" = None + self.retry_policy: Optional[AnyPolicy[HTTPRequestType, HTTPResponseType]] = None # Custom hook configuration - self.custom_hook_policy: "Optional[AnyPolicy]" = None + self.custom_hook_policy: Optional[AnyPolicy[HTTPRequestType, HTTPResponseType]] = None # Logger configuration - self.logging_policy: "Optional[AnyPolicy]" = None + self.logging_policy: Optional[AnyPolicy[HTTPRequestType, HTTPResponseType]] = None # Http logger configuration - self.http_logging_policy: "Optional[AnyPolicy]" = None + self.http_logging_policy: Optional[AnyPolicy[HTTPRequestType, HTTPResponseType]] = None # User Agent configuration - self.user_agent_policy: "Optional[AnyPolicy]" = None + self.user_agent_policy: Optional[AnyPolicy[HTTPRequestType, HTTPResponseType]] = None # Authentication configuration - self.authentication_policy: "Optional[AnyPolicy]" = None + self.authentication_policy: Optional[AnyPolicy[HTTPRequestType, HTTPResponseType]] = None # Request ID policy - self.request_id_policy: "Optional[AnyPolicy]" = None + self.request_id_policy: Optional[AnyPolicy[HTTPRequestType, HTTPResponseType]] = None # Polling interval if no retry-after in polling calls results - self.polling_interval = kwargs.get("polling_interval", 30) + self.polling_interval: float = kwargs.get("polling_interval", 30) class ConnectionConfiguration: @@ -131,7 +139,7 @@ def __init__( connection_verify: Union[bool, str] = True, connection_cert: Optional[str] = None, connection_data_block_size: int = 4096, - **kwargs + **kwargs: Any, ) -> None: self.timeout = connection_timeout self.read_timeout = read_timeout diff --git a/sdk/core/azure-core/azure/core/settings.py b/sdk/core/azure-core/azure/core/settings.py index 36b5fec7562a..d41ffbc10964 100644 --- a/sdk/core/azure-core/azure/core/settings.py +++ b/sdk/core/azure-core/azure/core/settings.py @@ -25,15 +25,18 @@ # -------------------------------------------------------------------------- """Provide access to settings for globally used Azure configuration values. """ - +from __future__ import annotations from collections import namedtuple from enum import Enum import logging import os import sys -from typing import Type, Optional, Callable, cast, Union, Dict +from typing import Type, Optional, Callable, cast, Union, Dict, Any, TypeVar, Tuple, Generic, Mapping, List from azure.core.tracing import AbstractSpan +ValidInputType = TypeVar("ValidInputType") +ValueType = TypeVar("ValueType") + __all__ = ("settings", "Settings") @@ -159,7 +162,7 @@ def _get_opentelemetry_span_if_opentelemetry_is_imported() -> Optional[Type[Abst } -def convert_tracing_impl(value: Union[str, Type[AbstractSpan]]) -> Optional[Type[AbstractSpan]]: +def convert_tracing_impl(value: Optional[Union[str, Type[AbstractSpan]]]) -> Optional[Type[AbstractSpan]]: """Convert a string to AbstractSpan If a AbstractSpan is passed in, it is returned as-is. Otherwise the function @@ -195,7 +198,7 @@ def convert_tracing_impl(value: Union[str, Type[AbstractSpan]]) -> Optional[Type return wrapper_class -class PrioritizedSetting: +class PrioritizedSetting(Generic[ValidInputType, ValueType]): """Return a value for a global setting according to configuration precedence. The following methods are searched in order for the setting: @@ -210,38 +213,50 @@ class PrioritizedSetting: The ``env_var`` argument specifies the name of an environment to check for setting values, e.g. ``"AZURE_LOG_LEVEL"``. + If a ``convert`` function is provided, the result will be converted before being used. The optional ``system_hook`` can be used to specify a function that will attempt to look up a value for the setting from system-wide configurations. + If a ``convert`` function is provided, the hook result will be converted before being used. The optional ``default`` argument specified an implicit default value for - the setting that is returned if no other methods provide a value. + the setting that is returned if no other methods provide a value. If a ``convert`` function is provided, + ``default`` will be converted before being used. A ``convert`` argument may be provided to convert values before they are returned. For instance to concert log levels in environment variables - to ``logging`` module values. + to ``logging`` module values. If a ``convert`` function is provided, it must support + str as valid input type. :param str name: the name of the setting :param str env_var: the name of an environment variable to check for the setting :param callable system_hook: a function that will attempt to look up a value for the setting :param default: an implicit default value for the setting - :type default: str or int or float + :type default: any :param callable convert: a function to convert values before they are returned """ - def __init__(self, name: str, env_var: Optional[str] = None, system_hook=None, default=_Unset, convert=None): + def __init__( + self, + name: str, + env_var: Optional[str] = None, + system_hook: Optional[Callable[[], ValidInputType]] = None, + default: Union[ValidInputType, _Unset] = _unset, + convert: Optional[Callable[[Union[ValidInputType, str]], ValueType]] = None, + ): self._name = name self._env_var = env_var self._system_hook = system_hook self._default = default - self._convert = convert if convert else lambda x: x - self._user_value = _Unset + noop_convert: Callable[[Any], Any] = lambda x: x + self._convert: Callable[[Union[ValidInputType, str]], ValueType] = convert if convert else noop_convert + self._user_value: Union[ValidInputType, _Unset] = _unset def __repr__(self) -> str: return "PrioritizedSetting(%r)" % self._name - def __call__(self, value=None): + def __call__(self, value: Optional[ValidInputType] = None) -> ValueType: """Return the setting value according to the standard precedence. :param value: value @@ -256,7 +271,7 @@ def __call__(self, value=None): return self._convert(value) # 3. previously user-set value - if self._user_value is not _Unset: + if self._user_value is not _unset: return self._convert(self._user_value) # 2. environment variable @@ -268,18 +283,18 @@ def __call__(self, value=None): return self._convert(self._system_hook()) # 0. implicit default - if self._default is not _Unset: + if self._default is not _unset: return self._convert(self._default) raise RuntimeError("No configured value found for setting %r" % self._name) - def __get__(self, instance, owner): + def __get__(self, instance: Any, owner: Optional[Any] = None) -> PrioritizedSetting[ValidInputType, ValueType]: return self - def __set__(self, instance, value): + def __set__(self, instance: Any, value: ValidInputType) -> None: self.set_value(value) - def set_value(self, value) -> None: + def set_value(self, value: ValidInputType) -> None: """Specify a value for this setting programmatically. A value set this way takes precedence over all other methods except @@ -292,14 +307,14 @@ def set_value(self, value) -> None: def unset_value(self) -> None: """Unset the previous user value such that the priority is reset.""" - self._user_value = _Unset + self._user_value = _unset @property - def env_var(self): + def env_var(self) -> Optional[str]: return self._env_var @property - def default(self): + def default(self) -> Union[ValidInputType, _Unset]: return self._default @@ -382,11 +397,11 @@ class Settings: """ - def __init__(self): - self._defaults_only = False + def __init__(self) -> None: + self._defaults_only: bool = False @property - def defaults_only(self): + def defaults_only(self) -> bool: """Whether to ignore environment and system settings and return only base default values. :rtype: bool @@ -395,11 +410,11 @@ def defaults_only(self): return self._defaults_only @defaults_only.setter - def defaults_only(self, value): + def defaults_only(self, value: bool) -> None: self._defaults_only = value @property - def defaults(self): + def defaults(self) -> Tuple[Any, ...]: """Return implicit default values for all settings, ignoring environment and system. :rtype: namedtuple @@ -409,7 +424,7 @@ def defaults(self): return self._config(props) @property - def current(self): + def current(self) -> Tuple[Any, ...]: """Return the current values for all settings. :rtype: namedtuple @@ -419,7 +434,7 @@ def current(self): return self.defaults return self.config() - def config(self, **kwargs): + def config(self, **kwargs: Any) -> Tuple[Any, ...]: """Return the currently computed settings, with values overridden by parameter values. :keyword dict kwargs: Settings to override @@ -438,25 +453,29 @@ def config(self, **kwargs): props.update(kwargs) return self._config(props) - def _config(self, props): - Config = namedtuple("Config", list(props.keys())) + def _config(self, props: Mapping[str, Any]) -> Tuple[Any, ...]: + keys: List[str] = list(props.keys()) + # https://github.com/python/mypy/issues/4414 + Config = namedtuple("Config", keys) # type: ignore return Config(**props) - log_level = PrioritizedSetting( + log_level: PrioritizedSetting[Union[str, int], int] = PrioritizedSetting( "log_level", env_var="AZURE_LOG_LEVEL", convert=convert_logging, default=logging.INFO, ) - tracing_enabled = PrioritizedSetting( + tracing_enabled: PrioritizedSetting[Union[str, bool], bool] = PrioritizedSetting( "tracing_enabled", env_var="AZURE_TRACING_ENABLED", convert=convert_bool, default=False, ) - tracing_implementation = PrioritizedSetting( + tracing_implementation: PrioritizedSetting[ + Optional[Union[str, Type[AbstractSpan]]], Optional[Type[AbstractSpan]] + ] = PrioritizedSetting( "tracing_implementation", env_var="AZURE_SDK_TRACING_IMPLEMENTATION", convert=convert_tracing_impl, @@ -464,7 +483,7 @@ def _config(self, props): ) -settings = Settings() +settings: Settings = Settings() """The settings unique instance. :type settings: Settings diff --git a/sdk/core/azure-core/azure/core/tracing/common.py b/sdk/core/azure-core/azure/core/tracing/common.py index 35d3072f141a..937715570197 100644 --- a/sdk/core/azure-core/azure/core/tracing/common.py +++ b/sdk/core/azure-core/azure/core/tracing/common.py @@ -72,7 +72,7 @@ def change_context(span: Optional[AbstractSpan]) -> Generator: :rtype: contextmanager :return: A context manager that will run the given span in the new context """ - span_impl_type: Type[AbstractSpan] = settings.tracing_implementation() + span_impl_type: Optional[Type[AbstractSpan]] = settings.tracing_implementation() if span_impl_type is None or span is None: yield else: @@ -101,7 +101,7 @@ def with_current_context(func: Callable) -> Any: :return: The func wrapped with correct context :rtype: callable """ - span_impl_type: Type[AbstractSpan] = settings.tracing_implementation() + span_impl_type: Optional[Type[AbstractSpan]] = settings.tracing_implementation() if span_impl_type is None: return func