diff --git a/dataclass_wizard/environ/dumpers.py b/dataclass_wizard/environ/dumpers.py index 689b87b6..d85e5dbb 100644 --- a/dataclass_wizard/environ/dumpers.py +++ b/dataclass_wizard/environ/dumpers.py @@ -110,7 +110,7 @@ def cls_asdict(obj: T, dict_factory=dict, """ # Call the optional hook that runs before we process the subclass - cls_dumper.__pre_as_dict__(obj) + # cls_dumper.__pre_as_dict__(obj) # This a list that contains a mapping of each `EnvWizard` field to its # serialized value. diff --git a/dataclass_wizard/environ/lookups.py b/dataclass_wizard/environ/lookups.py index 8a116b1a..44d1ffa4 100644 --- a/dataclass_wizard/environ/lookups.py +++ b/dataclass_wizard/environ/lookups.py @@ -1,19 +1,14 @@ import os from dataclasses import MISSING from os import environ, name -from typing import ClassVar, Dict, Optional, Set from ..decorators import cached_class_property from ..lazy_imports import dotenv -from ..type_def import StrCollection, EnvFileType from ..utils.string_conv import to_snake_case # Type of `os.environ` or `DotEnv` dict -Environ = Dict[str, Optional[str]] - -# Type of (unique) environment variable names -EnvVars = Set[str] +Environ = dict[str, 'str | None'] # noinspection PyMethodParameters @@ -21,10 +16,10 @@ class Env: __slots__ = () - _accessed_cleaned_to_env: ClassVar[bool] = False + _accessed_cleaned_to_env = False @cached_class_property - def var_names(cls) -> EnvVars: + def var_names(cls): """ Cached mapping of `os.environ` key names. This can be refreshed with :meth:`reload` as needed. @@ -32,9 +27,9 @@ def var_names(cls) -> EnvVars: return set(environ) @classmethod - def reload(cls, env: dict = environ): - """Refresh cached environment variable names.""" - env_vars: EnvVars = cls.var_names + def reload(cls, env=environ): + + env_vars = cls.var_names new_vars = set(env) - env_vars # update names of environment variables @@ -47,11 +42,8 @@ def reload(cls, env: dict = environ): ) @classmethod - def dotenv_values(cls, files: EnvFileType) -> Environ: - """ - Retrieve the values (environment variables) from a dotenv file, - or a list/tuple of dotenv files. - """ + def dotenv_values(cls, files): + if isinstance(files, (str, os.PathLike)): files = [files] elif files is True: @@ -70,7 +62,8 @@ def dotenv_values(cls, files: EnvFileType) -> Environ: return env @classmethod - def update_with_dotenv(cls, files: EnvFileType = '.env', dotenv_values=None): + def update_with_dotenv(cls, files='.env', dotenv_values=None): + if dotenv_values is None: dotenv_values = cls.dotenv_values(files) @@ -79,27 +72,20 @@ def update_with_dotenv(cls, files: EnvFileType = '.env', dotenv_values=None): # update `os.environ` with new environment variables environ.update(dotenv_values) - # noinspection PyDunderSlots,PyUnresolvedReferences + # noinspection PyDunderSlots,PyUnresolvedReferences,PyClassVar @cached_class_property - def cleaned_to_env(cls) -> Environ: + def cleaned_to_env(cls): cls._accessed_cleaned_to_env = True return {clean(var): var for var in cls.var_names} -def clean(s: str) -> str: - """ - TODO: - see https://stackoverflow.com/questions/1276764/stripping-everything-but-alphanumeric-chars-from-a-string-in-python - also, see if we can refactor to use something like Rust and `pyo3` for a slight performance improvement. - """ +def clean(s): + return s.replace('-', '').replace('_', '').lower() -def try_cleaned(key: str): - """ - Return the value of the env variable as a *string* if present in - the Environment, or `MISSING` otherwise. - """ +def try_cleaned(key): + key = Env.cleaned_to_env.get(clean(key)) if key is not None: @@ -110,11 +96,8 @@ def try_cleaned(key: str): if name == 'nt': # Where Env Var Names Must Be UPPERCASE - def lookup_exact(var: StrCollection): - """ - Lookup by variable name(s) with *exact* letter casing, and return - `None` if not found in the environment. - """ + def lookup_exact(var): + if isinstance(var, str): var = var.upper() @@ -132,11 +115,8 @@ def lookup_exact(var: StrCollection): else: # Where Env Var Names Can Be Mixed Case - def lookup_exact(var: StrCollection): - """ - Lookup by variable name(s) with *exact* letter casing, and return - `None` if not found in the environment. - """ + def lookup_exact(var): + if isinstance(var, str): if var in Env.var_names: return environ[var] @@ -149,22 +129,8 @@ def lookup_exact(var: StrCollection): return MISSING -def with_screaming_snake_case(field_name: str) -> Optional[str]: - """ - Lookup with `SCREAMING_SNAKE_CASE` letter casing first - this is the - default lookup. - - This function assumes the dataclass field name is lower-cased. +def with_screaming_snake_case(field_name): - For a field named 'my_env_var', this tries the following lookups in order: - - MY_ENV_VAR (screaming snake-case) - - my_env_var (snake-case) - - Any other variations - i.e. MyEnvVar, myEnvVar, myenvvar, my-env-var - - :param field_name: The dataclass field name to lookup in the environment. - :return: The value of the matched environment variable, if one is found in - the environment. - """ upper_key = field_name.upper() if upper_key in Env.var_names: @@ -176,20 +142,8 @@ def with_screaming_snake_case(field_name: str) -> Optional[str]: return try_cleaned(field_name) -def with_snake_case(field_name: str) -> Optional[str]: - """Lookup with `snake_case` letter casing first. - - This function assumes the dataclass field name is lower-cased. - - For a field named 'my_env_var', this tries the following lookups in order: - - my_env_var (snake-case) - - MY_ENV_VAR (screaming snake-case) - - Any other variations - i.e. MyEnvVar, myEnvVar, myenvvar, my-env-var +def with_snake_case(field_name): - :param field_name: The dataclass field name to lookup in the environment. - :return: The value of the matched environment variable, if one is found in - the environment. - """ if field_name in Env.var_names: return environ[field_name] @@ -201,22 +155,8 @@ def with_snake_case(field_name: str) -> Optional[str]: return try_cleaned(field_name) -def with_pascal_or_camel_case(field_name: str) -> Optional[str]: - """Lookup with `PascalCase` or `camelCase` letter casing first. - - This function assumes the dataclass field name is either pascal- or camel- - cased. - - For a field named 'myEnvVar', this tries the following lookups in order: - - myEnvVar, MyEnvVar (camel-case, or pascal-case) - - MY_ENV_VAR (screaming snake-case) - - my_env_var (snake-case) - - Any other variations - i.e. my-env-var, myenvvar +def with_pascal_or_camel_case(field_name): - :param field_name: The dataclass field name to lookup in the environment. - :return: The value of the matched environment variable, if one is found in - the environment. - """ if field_name in Env.var_names: return environ[field_name] diff --git a/dataclass_wizard/environ/lookups.pyi b/dataclass_wizard/environ/lookups.pyi new file mode 100644 index 00000000..a8f55e96 --- /dev/null +++ b/dataclass_wizard/environ/lookups.pyi @@ -0,0 +1,125 @@ +from dataclasses import MISSING +from os import environ +from typing import ClassVar + +from ..decorators import cached_class_property +from ..type_def import StrCollection, EnvFileType + + +type _MISSING_TYPE = type(MISSING) +type STR_OR_MISSING = str | _MISSING_TYPE +type STR_OR_NONE = str | None + +# Type of `os.environ` or `DotEnv` dict +Environ = dict[str, STR_OR_NONE] + +# Type of (unique) environment variable names +EnvVars = set[str] + + +# noinspection PyMethodParameters +class Env: + + __slots__ = () + + _accessed_cleaned_to_env: ClassVar[bool] = False + + """ + Cached mapping of `os.environ` key names. This can be refreshed with + :meth:`reload` as needed. + """ + var_names: EnvVars + + @classmethod + def reload(cls, env: dict = environ): + """Refresh cached environment variable names.""" + + @classmethod + def dotenv_values(cls, files: EnvFileType) -> Environ: + """ + Retrieve the values (environment variables) from a dotenv file, + or a list/tuple of dotenv files. + """ + + @classmethod + def update_with_dotenv(cls, files: EnvFileType = '.env', dotenv_values=None): + ... + + # noinspection PyDunderSlots,PyUnresolvedReferences + @cached_class_property + def cleaned_to_env(cls) -> Environ: + ... + + +def clean(s: str) -> str: + """ + TODO: + see https://stackoverflow.com/questions/1276764/stripping-everything-but-alphanumeric-chars-from-a-string-in-python + also, see if we can refactor to use something like Rust and `pyo3` for a slight performance improvement. + """ + + +def try_cleaned(key: str) -> STR_OR_MISSING: + """ + Return the value of the env variable as a *string* if present in + the Environment, or `MISSING` otherwise. + """ + + +def lookup_exact(var: StrCollection) -> STR_OR_MISSING: + """ + Lookup by variable name(s) with *exact* letter casing, and return + `None` if not found in the environment. + """ + + +def with_screaming_snake_case(field_name: str) -> STR_OR_MISSING: + """ + Lookup with `SCREAMING_SNAKE_CASE` letter casing first - this is the + default lookup. + + This function assumes the dataclass field name is lower-cased. + + For a field named 'my_env_var', this tries the following lookups in order: + - MY_ENV_VAR (screaming snake-case) + - my_env_var (snake-case) + - Any other variations - i.e. MyEnvVar, myEnvVar, myenvvar, my-env-var + + :param field_name: The dataclass field name to lookup in the environment. + :return: The value of the matched environment variable, if one is found in + the environment. + """ + + +def with_snake_case(field_name: str) -> STR_OR_MISSING: + """Lookup with `snake_case` letter casing first. + + This function assumes the dataclass field name is lower-cased. + + For a field named 'my_env_var', this tries the following lookups in order: + - my_env_var (snake-case) + - MY_ENV_VAR (screaming snake-case) + - Any other variations - i.e. MyEnvVar, myEnvVar, myenvvar, my-env-var + + :param field_name: The dataclass field name to lookup in the environment. + :return: The value of the matched environment variable, if one is found in + the environment. + """ + + +def with_pascal_or_camel_case(field_name: str) -> STR_OR_MISSING: + """Lookup with `PascalCase` or `camelCase` letter casing first. + + This function assumes the dataclass field name is either pascal- or camel- + cased. + + For a field named 'myEnvVar', this tries the following lookups in order: + - myEnvVar, MyEnvVar (camel-case, or pascal-case) + - MY_ENV_VAR (screaming snake-case) + - my_env_var (snake-case) + - Any other variations - i.e. my-env-var, myenvvar + + :param field_name: The dataclass field name to lookup in the environment. + :return: The value of the matched environment variable, if one is found in + the environment. + """