22import inspect
33import contextlib
44import dataclasses
5+ import warnings
56
67from collections .abc import Mapping as C_Mapping
78from typing import (
1920 ClassVar ,
2021 TypeVar ,
2122)
22- from typing_extensions import get_args , get_origin
23+ from typing_extensions import get_args , get_origin , dataclass_transform
2324from functools import wraps
2425
2526if TYPE_CHECKING :
4445_F_BaseConfiguration : Any = type (object )
4546_F_ContainerInjectableContext : Any = type (object )
4647_T = TypeVar ("_T" , bound = "BaseConfiguration" )
48+ _C = TypeVar ("_C" , bound = "CredentialsConfiguration" )
4749
4850
4951def is_base_configuration_inner_hint (inner_hint : Type [Any ]) -> bool :
@@ -106,18 +108,26 @@ def is_secret_hint(hint: Type[Any]) -> bool:
106108
107109
108110@overload
109- def configspec (cls : Type [TAnyClass ]) -> Type [TAnyClass ]: ...
111+ def configspec (cls : Type [TAnyClass ], init : bool = True ) -> Type [TAnyClass ]: ...
110112
111113
112114@overload
113- def configspec (cls : None = ...) -> Callable [[Type [TAnyClass ]], Type [TAnyClass ]]: ...
115+ def configspec (
116+ cls : None = ..., init : bool = True
117+ ) -> Callable [[Type [TAnyClass ]], Type [TAnyClass ]]: ...
114118
115119
120+ @dataclass_transform (eq_default = False , field_specifiers = (dataclasses .Field , dataclasses .field ))
116121def configspec (
117- cls : Optional [Type [Any ]] = None ,
122+ cls : Optional [Type [Any ]] = None , init : bool = True
118123) -> Union [Type [TAnyClass ], Callable [[Type [TAnyClass ]], Type [TAnyClass ]]]:
119124 """Converts (via derivation) any decorated class to a Python dataclass that may be used as a spec to resolve configurations
120125
126+ __init__ method is synthesized by default. `init` flag is ignored if the decorated class implements custom __init__ as well as
127+ when any of base classes has no synthesized __init__
128+
129+ All fields must have default values. This decorator will add `None` default values that miss one.
130+
121131 In comparison the Python dataclass, a spec implements full dictionary interface for its attributes, allows instance creation from ie. strings
122132 or other types (parsing, deserialization) and control over configuration resolution process. See `BaseConfiguration` and CredentialsConfiguration` for
123133 more information.
@@ -142,6 +152,10 @@ def wrap(cls: Type[TAnyClass]) -> Type[TAnyClass]:
142152 # get all annotations without corresponding attributes and set them to None
143153 for ann in cls .__annotations__ :
144154 if not hasattr (cls , ann ) and not ann .startswith (("__" , "_abc_" )):
155+ warnings .warn (
156+ f"Missing default value for field { ann } on { cls .__name__ } . None assumed. All"
157+ " fields in configspec must have default."
158+ )
145159 setattr (cls , ann , None )
146160 # get all attributes without corresponding annotations
147161 for att_name , att_value in list (cls .__dict__ .items ()):
@@ -177,17 +191,18 @@ def default_factory(att_value=att_value): # type: ignore[no-untyped-def]
177191
178192 # We don't want to overwrite user's __init__ method
179193 # Create dataclass init only when not defined in the class
180- # (never put init on BaseConfiguration itself)
181- try :
182- is_base = cls is BaseConfiguration
183- except NameError :
184- is_base = True
185- init = False
186- base_params = getattr (cls , "__dataclass_params__" , None )
187- if not is_base and (base_params and base_params .init or cls .__init__ is object .__init__ ):
188- init = True
194+ # NOTE: any class without synthesized __init__ breaks the creation chain
195+ has_default_init = super (cls , cls ).__init__ == cls .__init__ # type: ignore[misc]
196+ base_params = getattr (cls , "__dataclass_params__" , None ) # cls.__init__ is object.__init__
197+ synth_init = init and ((not base_params or base_params .init ) and has_default_init )
198+ if synth_init != init and has_default_init :
199+ warnings .warn (
200+ f"__init__ method will not be generated on { cls .__name__ } because bas class didn't"
201+ " synthesize __init__. Please correct `init` flag in confispec decorator. You are"
202+ " probably receiving incorrect __init__ signature for type checking"
203+ )
189204 # do not generate repr as it may contain secret values
190- return dataclasses .dataclass (cls , init = init , eq = False , repr = False ) # type: ignore
205+ return dataclasses .dataclass (cls , init = synth_init , eq = False , repr = False ) # type: ignore
191206
192207 # called with parenthesis
193208 if cls is None :
@@ -198,12 +213,14 @@ def default_factory(att_value=att_value): # type: ignore[no-untyped-def]
198213
199214@configspec
200215class BaseConfiguration (MutableMapping [str , Any ]):
201- __is_resolved__ : bool = dataclasses .field (default = False , init = False , repr = False )
216+ __is_resolved__ : bool = dataclasses .field (default = False , init = False , repr = False , compare = False )
202217 """True when all config fields were resolved and have a specified value type"""
203- __section__ : str = dataclasses .field (default = None , init = False , repr = False )
204- """Obligatory section used by config providers when searching for keys, always present in the search path"""
205- __exception__ : Exception = dataclasses . field ( default = None , init = False , repr = False )
218+ __exception__ : Exception = dataclasses .field (
219+ default = None , init = False , repr = False , compare = False
220+ )
206221 """Holds the exception that prevented the full resolution"""
222+ __section__ : ClassVar [str ] = None
223+ """Obligatory section used by config providers when searching for keys, always present in the search path"""
207224 __config_gen_annotations__ : ClassVar [List [str ]] = []
208225 """Additional annotations for config generator, currently holds a list of fields of interest that have defaults"""
209226 __dataclass_fields__ : ClassVar [Dict [str , TDtcField ]]
@@ -342,9 +359,10 @@ def call_method_in_mro(config, method_name: str) -> None:
342359class CredentialsConfiguration (BaseConfiguration ):
343360 """Base class for all credentials. Credentials are configurations that may be stored only by providers supporting secrets."""
344361
345- __section__ : str = "credentials"
362+ __section__ : ClassVar [ str ] = "credentials"
346363
347- def __init__ (self , init_value : Any = None ) -> None :
364+ @classmethod
365+ def from_init_value (cls : Type [_C ], init_value : Any = None ) -> _C :
348366 """Initializes credentials from `init_value`
349367
350368 Init value may be a native representation of the credentials or a dict. In case of native representation (for example a connection string or JSON with service account credentials)
@@ -353,14 +371,10 @@ def __init__(self, init_value: Any = None) -> None:
353371
354372 Credentials will be marked as resolved if all required fields are set.
355373 """
356- if init_value is None :
357- return
358- elif isinstance (init_value , C_Mapping ):
359- self .update (init_value )
360- else :
361- self .parse_native_representation (init_value )
362- if not self .is_partial ():
363- self .resolve ()
374+ # create an instance
375+ self = cls ()
376+ self ._apply_init_value (init_value )
377+ return self
364378
365379 def to_native_credentials (self ) -> Any :
366380 """Returns native credentials object.
@@ -369,6 +383,16 @@ def to_native_credentials(self) -> Any:
369383 """
370384 return self .to_native_representation ()
371385
386+ def _apply_init_value (self , init_value : Any = None ) -> None :
387+ if isinstance (init_value , C_Mapping ):
388+ self .update (init_value )
389+ elif init_value is not None :
390+ self .parse_native_representation (init_value )
391+ else :
392+ return
393+ if not self .is_partial ():
394+ self .resolve ()
395+
372396 def __str__ (self ) -> str :
373397 """Get string representation of credentials to be displayed, with all secret parts removed"""
374398 return super ().__str__ ()
0 commit comments