envclasses
envclasses
is a library to map fields on dataclass object to environment variables.
Declare a class with dataclass
and envclass
decorators. If you are new to dataclass,
I recommend to read dataclasses documentation
first.
>>> from dataclasses import dataclass
>>>
>>> @envclass
... @dataclass
... class Foo:
... v: int
Create an instance of Foo
, now foo.v
is 10.
>>> foo = Foo(v=10)
Set environment variable FOO_V
100
.
>>> import os
>>> os.environ['FOO_V'] = '100'
Calling load_env
will change the value from 10
to 100
on foo.v
.
>>> load_env(foo, prefix='foo')
>>> foo.v
100
Supported types
- Primitives (int, float, str, bool)
- Containers (List, Tuple, Dict)
>>> from typing import List, Dict
>>> from dataclasses import dataclass, field
>>>
>>> @envclass
... @dataclass
... class Foo:
... lst: List[int] = field(default_factory=list)
... dct: Dict[str, float] = field(default_factory=dict)
...
>>> foo = Foo()
>>> os.environ['FOO_LST'] = '[1, 2]'
>>> os.environ['FOO_DCT'] = '{key: 100.0}'
>>> load_env(foo, prefix='FOO')
>>> foo
Foo(lst=[1, 2], dct={'key': 100.0})
- Nested envclass
>>> @envclass
... @dataclass
... class Bar:
... v: int
>>>
>>> @envclass
... @dataclass
... class Foo:
... bar: Bar
>>> foo = Foo(Bar(v=10))
>>> os.environ['FOO_BAR_V'] = '100'
>>> load_env(foo, prefix='foo')
>>> foo.bar.v
100
Debugging
If you want to see how envclasses
looks up environment variables, you may want to see the logs
produced in the module. envclasses
uses the logger of standard logging
module. In order to
see DEBUG logs, you can get the logger and configure the log level.
import logging
logging.getLogger("envclasses").setLevel(logging.DEBUG)
1""" 2`envclasses` is a library to map fields on dataclass object to environment variables. 3 4Declare a class with `dataclass` and `envclass` decorators. If you are new to dataclass, 5I recommend to read [dataclasses documentation](https://docs.python.org/3/library/dataclasses.html) 6first. 7 8>>> from dataclasses import dataclass 9>>> 10>>> @envclass 11... @dataclass 12... class Foo: 13... v: int 14 15Create an instance of `Foo`, now `foo.v` is 10. 16 17>>> foo = Foo(v=10) 18 19Set environment variable `FOO_V` `100`. 20 21>>> import os 22>>> os.environ['FOO_V'] = '100' 23 24Calling `load_env` will change the value from `10` to `100` on `foo.v`. 25 26>>> load_env(foo, prefix='foo') 27>>> foo.v 28100 29 30### Supported types 31 32* Primitives (int, float, str, bool) 33* Containers (List, Tuple, Dict) 34 35>>> from typing import List, Dict 36>>> from dataclasses import dataclass, field 37>>> 38>>> @envclass 39... @dataclass 40... class Foo: 41... lst: List[int] = field(default_factory=list) 42... dct: Dict[str, float] = field(default_factory=dict) 43... 44>>> foo = Foo() 45>>> os.environ['FOO_LST'] = '[1, 2]' 46>>> os.environ['FOO_DCT'] = '{key: 100.0}' 47>>> load_env(foo, prefix='FOO') 48>>> foo 49Foo(lst=[1, 2], dct={'key': 100.0}) 50 51* Nested envclass 52 53>>> @envclass 54... @dataclass 55... class Bar: 56... v: int 57>>> 58>>> @envclass 59... @dataclass 60... class Foo: 61... bar: Bar 62 63>>> foo = Foo(Bar(v=10)) 64>>> os.environ['FOO_BAR_V'] = '100' 65>>> load_env(foo, prefix='foo') 66>>> foo.bar.v 67100 68 69### Debugging 70 71If you want to see how `envclasses` looks up environment variables, you may want to see the logs 72produced in the module. `envclasses` uses the logger of standard `logging` module. In order to 73see DEBUG logs, you can get the logger and configure the log level. 74 75```python 76import logging 77 78logging.getLogger("envclasses").setLevel(logging.DEBUG) 79``` 80""" 81import enum 82import functools 83import logging 84import os 85from dataclasses import Field, fields 86from typing import Any, Dict, List, Optional, Type, TypeVar, Union, cast 87 88import yaml 89from typing_inspect import get_args, get_origin, is_optional_type 90 91__all__ = [ 92 'envclass', 93 'is_envclass', 94 'load_env', 95 'EnvclassError', 96 'LoadEnvError', 97] 98 99__version__ = '0.2.7' 100""" Version of envclass. """ 101 102logger = logging.getLogger('envclasses') 103 104ENVCLASS_DUNDER_FUNC_NAME = '__envclasses_load_env__' 105""" Name of the generated dunder function to be called by `load_env`. """ 106 107ENVCLASS_PREFIX = 'env' 108""" Default prefix used for environment variables. """ 109 110T = TypeVar('T') 111 112JsonValue = TypeVar('JsonValue', str, int, float, bool, Dict, List) 113 114 115class EnvclassError(TypeError): 116 """ 117 Exception used in `envclass`. This exception is only raised when there is an 118 error in the class declaration. 119 """ 120 121 122class LoadEnvError(Exception): 123 """ 124 Exception raised in `load_env`. 125 """ 126 127 128class InvalidNumberOfElement(LoadEnvError): 129 """ 130 Raised if the number of element is imcompatible with 131 the number of elemtn of type. 132 """ 133 134 135def _coalesce(typ: Type) -> Type: 136 if not is_optional_type(typ): 137 return typ 138 else: 139 return cast( 140 Type, 141 Union[tuple(tt for tt in get_args(typ, evaluate=True) if not is_optional_type(tt))]) 142 143 144def envclass(_cls: Type[T]) -> Type[T]: 145 """ 146 `envclass` decorator generates methods to loads field values from environment variables. 147 148 """ 149 150 @functools.wraps(_cls) 151 def wrap(cls): 152 153 def load_env(self, _prefix: str = None) -> None: 154 """ 155 Load attributes from environment variables. 156 """ 157 for f in fields(cls): 158 # If no prefix specified, use the default PREFIX. 159 prefix = _prefix if _prefix is not None else ENVCLASS_PREFIX 160 prefix += '_' if prefix and not prefix.endswith('_') else '' 161 logger.debug(f'prefix={prefix}, type={f.type}') 162 163 f_type = _coalesce(f.type) 164 if is_envclass(f_type): 165 _load_dataclass(self, f, prefix, f_type) 166 elif is_list(f_type): 167 _load_list(self, f, prefix, f_type) 168 elif is_tuple(f_type): 169 _load_tuple(self, f, prefix, f_type) 170 elif is_dict(f_type): 171 _load_dict(self, f, prefix, f_type) 172 elif is_enum(f_type): 173 _load_enum(self, f, prefix, f_type) 174 elif is_str(f_type): 175 _load_str(self, f, prefix, f_type) 176 else: 177 _load_other(self, f, prefix, f_type) 178 179 setattr(cls, ENVCLASS_DUNDER_FUNC_NAME, load_env) 180 return cls 181 182 return wrap(_cls) 183 184 185def _load_dataclass(obj, f: Field, prefix: str, f_type: Optional[Type] = None) -> None: 186 """ 187 Override exisiting dataclass object by environment variables. 188 """ 189 inner_prefix = f'{prefix}{f.name}' 190 typ = f_type or f.type 191 if getattr(obj, f.name, None) is None: 192 setattr(obj, f.name, typ()) 193 o = getattr(obj, f.name) 194 try: 195 o.__envclasses_load_env__(inner_prefix) 196 except KeyError: 197 pass 198 199 200def _load_list(obj, f: Field, prefix: str, f_type: Optional[Type] = None) -> None: 201 """ 202 Override list values by environment variables. 203 """ 204 typ = f_type or f.type 205 element_type = typ.__args__[0] 206 name = f'{prefix.upper()}{f.name.upper()}' 207 try: 208 s: str = os.environ[name].strip() 209 except KeyError: 210 return 211 212 yml = yaml.safe_load(s) 213 lst = [element_type(e) for e in yml] 214 setattr(obj, f.name, lst) 215 216 217def _load_tuple(obj, f: Field, prefix: str, f_type: Optional[Type] = None) -> None: 218 """ 219 Override tuple values by environment variables. 220 """ 221 typ = f_type or f.type 222 name = f'{prefix.upper()}{f.name.upper()}' 223 element_types = typ.__args__ 224 try: 225 s: str = os.environ[name].strip() 226 except KeyError: 227 return 228 229 lst = yaml.safe_load(s) 230 if len(lst) != len(element_types): 231 raise InvalidNumberOfElement(f'expected={len(element_types)} ' 232 f'actual={len(lst)}') 233 tpl = tuple(element_type(e) for e, element_type in zip(lst, element_types)) 234 setattr(obj, f.name, tpl) 235 236 237def _load_dict(obj, f: Field, prefix: str, f_type: Optional[Type] = None) -> None: 238 """ 239 Override dict values by environment variables. 240 """ 241 typ = f_type or f.type 242 name = f'{prefix.upper()}{f.name.upper()}' 243 key_type = typ.__args__[0] 244 value_type = typ.__args__[1] 245 try: 246 s = os.environ[name].strip() 247 except KeyError: 248 return 249 250 dct = {_to_value(k, key_type): _to_value(v, value_type) for k, v in yaml.safe_load(s).items()} 251 setattr(obj, f.name, dct) 252 253 254def _to_value(v: JsonValue, typ: Type) -> Any: 255 if isinstance(v, (List, Dict)): 256 return v 257 else: 258 return typ(v) 259 260 261def _load_enum(obj, f: Field, prefix: str, f_type: Optional[Type] = None) -> None: 262 name = f'{prefix.upper()}{f.name.upper()}' 263 typ = f_type or f.type 264 for enum_item in list(typ): 265 try: 266 setattr(obj, f.name, typ(type(enum_item.value)(os.environ[name]))) 267 return 268 except (KeyError, ValueError): 269 continue 270 271 272def _load_str(obj, f: Field, prefix: str, f_type: Optional[Type] = None) -> None: 273 """ 274 Override str values by environment variables. 275 """ 276 name = f'{prefix.upper()}{f.name.upper()}' 277 typ = f_type or f.type 278 try: 279 value = os.environ[name] 280 setattr(obj, f.name, _to_value(value, typ)) 281 except KeyError: 282 pass 283 284 285def _load_other(obj, f: Field, prefix: str, f_type: Optional[Type] = None) -> None: 286 """ 287 Override values by environment variables. 288 """ 289 name = f'{prefix.upper()}{f.name.upper()}' 290 typ = f_type or f.type 291 try: 292 yml = yaml.safe_load(os.environ[name]) 293 setattr(obj, f.name, _to_value(yml, typ)) 294 except KeyError: 295 pass 296 297 298def is_enum(typ: Type) -> bool: 299 """ 300 Test if class is Enum class. 301 """ 302 try: 303 return issubclass(typ, enum.Enum) 304 except TypeError: 305 return isinstance(typ, enum.Enum) 306 307 308def is_list(typ: Type) -> bool: 309 """ 310 Test if the type is `typing.List`. 311 """ 312 try: 313 return issubclass(get_origin(typ), list) 314 except TypeError: 315 return isinstance(typ, list) 316 317 318def is_tuple(typ: Type) -> bool: 319 """ 320 Test if the type is `typing.Tuple`. 321 """ 322 try: 323 return issubclass(get_origin(typ), tuple) 324 except TypeError: 325 return isinstance(typ, tuple) 326 327 328def is_dict(typ: Type) -> bool: 329 """ 330 Test if the type is `typing.Dict`. 331 """ 332 try: 333 return issubclass(get_origin(typ), dict) 334 except TypeError: 335 return isinstance(typ, dict) 336 337 338def is_str(typ: Type) -> bool: 339 """ 340 Test if the type is `str`. 341 """ 342 try: 343 return issubclass(typ, str) 344 except TypeError: 345 return False 346 347 348def is_envclass(instance_or_class: Any) -> bool: 349 """ 350 Test if instance_or_class is envclass. 351 352 >>> from dataclasses import dataclass 353 >>> 354 >>> @envclass 355 ... @dataclass 356 ... class Foo: 357 ... pass 358 >>> is_envclass(Foo) 359 True 360 """ 361 return hasattr(instance_or_class, ENVCLASS_DUNDER_FUNC_NAME) 362 363 364def load_env(inst, prefix: str = None) -> None: 365 """ 366 Load field values from environment variables. 367 368 `inst` is an instance of envclass. If `inst` is not an envclass, 369 `LoadEnvError` exception is raised. 370 371 `prefix` specifies the prefix of environment variables that envclass looks up. 372 for example `prefix="foo"`, envclass will load value from environment variable 373 `FOO_<FIELD_NAME>` and update the field value on the instance. If `prefix` is 374 omitted, the default prefix `env` is used. 375 376 >>> from dataclasses import dataclass 377 >>> 378 >>> @envclass 379 ... @dataclass 380 ... class Foo: 381 ... v: int 382 >>> foo = Foo(v=10) 383 >>> os.environ['ENV_V'] = '100' 384 >>> load_env(foo) 385 >>> foo.v 386 100 387 388 If an empty string is set in `prefix`, no prefix will be expected. 389 390 >>> os.environ['V'] = '100' 391 >>> load_env(foo, '') 392 >>> foo.v 393 100 394 """ 395 inst.__envclasses_load_env__(prefix)
145def envclass(_cls: Type[T]) -> Type[T]: 146 """ 147 `envclass` decorator generates methods to loads field values from environment variables. 148 149 """ 150 151 @functools.wraps(_cls) 152 def wrap(cls): 153 154 def load_env(self, _prefix: str = None) -> None: 155 """ 156 Load attributes from environment variables. 157 """ 158 for f in fields(cls): 159 # If no prefix specified, use the default PREFIX. 160 prefix = _prefix if _prefix is not None else ENVCLASS_PREFIX 161 prefix += '_' if prefix and not prefix.endswith('_') else '' 162 logger.debug(f'prefix={prefix}, type={f.type}') 163 164 f_type = _coalesce(f.type) 165 if is_envclass(f_type): 166 _load_dataclass(self, f, prefix, f_type) 167 elif is_list(f_type): 168 _load_list(self, f, prefix, f_type) 169 elif is_tuple(f_type): 170 _load_tuple(self, f, prefix, f_type) 171 elif is_dict(f_type): 172 _load_dict(self, f, prefix, f_type) 173 elif is_enum(f_type): 174 _load_enum(self, f, prefix, f_type) 175 elif is_str(f_type): 176 _load_str(self, f, prefix, f_type) 177 else: 178 _load_other(self, f, prefix, f_type) 179 180 setattr(cls, ENVCLASS_DUNDER_FUNC_NAME, load_env) 181 return cls 182 183 return wrap(_cls)
envclass
decorator generates methods to loads field values from environment variables.
349def is_envclass(instance_or_class: Any) -> bool: 350 """ 351 Test if instance_or_class is envclass. 352 353 >>> from dataclasses import dataclass 354 >>> 355 >>> @envclass 356 ... @dataclass 357 ... class Foo: 358 ... pass 359 >>> is_envclass(Foo) 360 True 361 """ 362 return hasattr(instance_or_class, ENVCLASS_DUNDER_FUNC_NAME)
Test if instance_or_class is envclass.
>>> from dataclasses import dataclass
>>>
>>> @envclass
... @dataclass
... class Foo:
... pass
>>> is_envclass(Foo)
True
365def load_env(inst, prefix: str = None) -> None: 366 """ 367 Load field values from environment variables. 368 369 `inst` is an instance of envclass. If `inst` is not an envclass, 370 `LoadEnvError` exception is raised. 371 372 `prefix` specifies the prefix of environment variables that envclass looks up. 373 for example `prefix="foo"`, envclass will load value from environment variable 374 `FOO_<FIELD_NAME>` and update the field value on the instance. If `prefix` is 375 omitted, the default prefix `env` is used. 376 377 >>> from dataclasses import dataclass 378 >>> 379 >>> @envclass 380 ... @dataclass 381 ... class Foo: 382 ... v: int 383 >>> foo = Foo(v=10) 384 >>> os.environ['ENV_V'] = '100' 385 >>> load_env(foo) 386 >>> foo.v 387 100 388 389 If an empty string is set in `prefix`, no prefix will be expected. 390 391 >>> os.environ['V'] = '100' 392 >>> load_env(foo, '') 393 >>> foo.v 394 100 395 """ 396 inst.__envclasses_load_env__(prefix)
Load field values from environment variables.
inst
is an instance of envclass. If inst
is not an envclass,
LoadEnvError
exception is raised.
prefix
specifies the prefix of environment variables that envclass looks up.
for example prefix="foo"
, envclass will load value from environment variable
FOO_<FIELD_NAME>
and update the field value on the instance. If prefix
is
omitted, the default prefix env
is used.
>>> from dataclasses import dataclass
>>>
>>> @envclass
... @dataclass
... class Foo:
... v: int
>>> foo = Foo(v=10)
>>> os.environ['ENV_V'] = '100'
>>> load_env(foo)
>>> foo.v
100
If an empty string is set in prefix
, no prefix will be expected.
>>> os.environ['V'] = '100'
>>> load_env(foo, '')
>>> foo.v
100
116class EnvclassError(TypeError): 117 """ 118 Exception used in `envclass`. This exception is only raised when there is an 119 error in the class declaration. 120 """
Exception used in envclass
. This exception is only raised when there is an
error in the class declaration.
Inherited Members
- builtins.TypeError
- TypeError
- builtins.BaseException
- with_traceback
Exception raised in load_env
.
Inherited Members
- builtins.Exception
- Exception
- builtins.BaseException
- with_traceback