Skip to content

Commit

Permalink
Type Complete config and settings (Azure#31422)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

---------

Co-authored-by: Krista Pratico <[email protected]>
  • Loading branch information
lmazuel and kristapratico authored Aug 7, 2023
1 parent 07d1063 commit c4c18a7
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 50 deletions.
40 changes: 24 additions & 16 deletions sdk/core/azure-core/azure/core/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
83 changes: 51 additions & 32 deletions sdk/core/azure-core/azure/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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


Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -438,33 +453,37 @@ 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,
default=None,
)


settings = Settings()
settings: Settings = Settings()
"""The settings unique instance.
:type settings: Settings
Expand Down
4 changes: 2 additions & 2 deletions sdk/core/azure-core/azure/core/tracing/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down

0 comments on commit c4c18a7

Please sign in to comment.