Skip to content

Instantly share code, notes, and snippets.

@Tinche
Created September 16, 2021 23:23
Show Gist options
  • Save Tinche/30a0c7a98f01c96c6bc63c7a34768d7b to your computer and use it in GitHub Desktop.
Save Tinche/30a0c7a98f01c96c6bc63c7a34768d7b to your computer and use it in GitHub Desktop.
Environment variable loading using cattrs
from os import environ
from typing import TypeVar
from attr import NOTHING, define, fields
from cattr import GenConverter
from cattr._compat import is_sequence
@define
class MyEnvVars:
"""Model your environment variables as members of this class."""
PWD: str
PATH: list[str]
USER: str
INT_LIST: list[int]
NONEXISTENT: str = "MyDefault"
c = GenConverter()
def gen_structure_envvar_seq(type):
def structure_envvar_seq(encoded_seq: str, _) -> list:
return [
c.structure(e, type.__args__[0]) for e in encoded_seq.split(":")
]
return structure_envvar_seq
c.register_structure_hook_factory(is_sequence, gen_structure_envvar_seq)
T = TypeVar("T")
def load_env_vars(cls: type[T], vars: dict[str, str] = environ) -> T:
obj = {
f.name: vars[f.name]
for f in fields(cls)
if f.default is NOTHING or f.name in vars
}
return c.structure(obj, cls)
print(load_env_vars(MyEnvVars))
@sscherfke
Copy link

Thanks a lot. c.register_structure_hook_factory(is_sequence, ...) did the trick.

I yesterday also found c._structure_list() – would this be a better alternative to the list comp. in your example?

    def test_load_list(self, monkeypatch: pytest.MonkeyPatch):
        """
        Lists can be loaded from env vars
        """
        converter = cattr.GenConverter()

        def gen_str2list(type):
            # def structure_envvar_seq(encoded_seq: str, _) -> list:
            #     if isinstance(encoded_seq, str):
            #         val = val.split(",")
            #     return [
            #         c.structure(e, type.__args__[0]) for e in encoded_seq.split(":")
            #     ]
            #
            # return structure_envvar_seq
            def str2list(val, _):
                if isinstance(val, str):
                    val = val.split(",")
                return converter._structure_list(val, type)

            return str2list

        converter.register_structure_hook_factory(is_sequence, gen_str2list)

        @settings
        class Settings:
            x: List[int]

        monkeypatch.setenv("X", "3,4,42")

        result = _core.load_settings(Settings, [EnvLoader("")], converter)
        assert result == Settings([3, 4, 42])

@Tinche
Copy link
Author

Tinche commented Sep 18, 2021

I yesterday also found c._structure_list() – would this be a better alternative to the list comp. in your example?

It's kind of an internal API, but sure, it's not going anywhere.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment