From e8f0cbb2acb2f3c0185e4b1f4257c1a4d9ba0d91 Mon Sep 17 00:00:00 2001 From: "gabriele.tornetta" Date: Fri, 8 Dec 2023 15:23:08 +0000 Subject: [PATCH] feat: private variables Add support for declaring private environment variables. These accept the new `private` argument, and are prefixed with an underscore when they are fetched from the source (e.g. the environment). When generating help info, private variables are excluded by default. They can be included by setting the new `include_private` argument to `help_info` to `True`. --- envier/env.py | 26 +++++++++++++++++++++++--- tests/test_env.py | 20 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/envier/env.py b/envier/env.py index fea4a9f..e9ee301 100644 --- a/envier/env.py +++ b/envier/env.py @@ -41,6 +41,7 @@ def __init__( map: t.Optional[MapType] = None, default: t.Union[T, NoDefaultType] = NoDefault, deprecations: t.Optional[t.List[DeprecationInfo]] = None, + private: bool = False, help: t.Optional[str] = None, help_type: t.Optional[str] = None, help_default: t.Optional[str] = None, @@ -60,6 +61,7 @@ def __init__( self.map = map self.default = default self.deprecations = deprecations + self.private = private self.help = help self.help_type = help_type @@ -69,10 +71,14 @@ def _retrieve(self, env: "Env", prefix: str) -> T: source = env.source full_name = prefix + _normalized(self.name) + if self.private: + full_name = f"_{full_name}" raw = source.get(full_name.format(**env.dynamic)) if raw is None and self.deprecations: for name, deprecated_when, removed_when in self.deprecations: full_deprecated_name = prefix + _normalized(name) + if self.private: + full_deprecated_name = f"_{full_deprecated_name}" raw = source.get(full_deprecated_name.format(**env.dynamic)) if raw is not None: deprecated_when_message = ( @@ -261,6 +267,7 @@ def var( map: t.Optional[MapType] = None, default: t.Union[T, NoDefaultType] = NoDefault, deprecations: t.Optional[t.List[DeprecationInfo]] = None, + private: bool = False, help: t.Optional[str] = None, help_type: t.Optional[str] = None, help_default: t.Optional[str] = None, @@ -273,6 +280,7 @@ def var( map, default, deprecations, + private, help, help_type, help_default, @@ -288,6 +296,7 @@ def v( map: t.Optional[MapType] = None, default: t.Union[T, NoDefaultType] = NoDefault, deprecations: t.Optional[t.List[DeprecationInfo]] = None, + private: bool = False, help: t.Optional[str] = None, help_type: t.Optional[str] = None, help_default: t.Optional[str] = None, @@ -300,6 +309,7 @@ def v( map, default, deprecations, + private, help, help_type, help_default, @@ -379,7 +389,9 @@ def include( setattr(cls, k, v) @classmethod - def help_info(cls, recursive: bool = False) -> t.List[HelpInfo]: + def help_info( + cls, recursive: bool = False, include_private: bool = False + ) -> t.List[HelpInfo]: """Extract the help information from the class. Returns a list of all the environment variables declared by the class. @@ -388,6 +400,9 @@ def help_info(cls, recursive: bool = False) -> t.List[HelpInfo]: Set ``recursive`` to ``True`` to include variables from nested Env classes. + + Set ``include_private`` to ``True`` to include variables that are + marked as private (i.e. their name starts with an underscore). """ entries = [] @@ -398,6 +413,9 @@ def add_entries(full_prefix: str, config: t.Type[Env]) -> None: ) for v in vars: + if not include_private and v.private: + continue + # Add a period at the end if necessary. help_message = v.help.strip() if v.help is not None else "" if help_message and not help_message.endswith("."): @@ -412,9 +430,11 @@ def add_entries(full_prefix: str, config: t.Type[Env]) -> None: # typing.t.Union[, NoneType] help_type = v.type.__args__[0].__name__ # type: ignore[attr-defined] + private_prefix = "_" if v.private else "" + entries.append( ( - "``" + full_prefix + _normalized(v.name) + "``", + f"``{private_prefix}{full_prefix}{_normalized(v.name)}``", help_type, # type: ignore[attr-defined] v.help_default if v.help_default is not None @@ -428,7 +448,7 @@ def add_entries(full_prefix: str, config: t.Type[Env]) -> None: while configs: full_prefix, config = configs.pop() new_prefix = full_prefix + _normalized(config.__prefix__) - if not new_prefix.endswith("_"): + if new_prefix and not new_prefix.endswith("_"): new_prefix += "_" add_entries(new_prefix, config) diff --git a/tests/test_env.py b/tests/test_env.py index fa29b20..4374a00 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -357,3 +357,23 @@ class Config(Env): config = Config(dynamic={"prefix": "pfx", "dyn": "bar"}) assert config.foobar == 24 + + +def test_env_private(monkeypatch): + monkeypatch.setenv("_PRIVATE_FOO", "24") + monkeypatch.setenv("PUBLIC_FOO", "25") + + class Config(Env): + private = Env.var(int, "private.foo", default=42, private=True) + public = Env.var(int, "public.foo", default=42) + + config = Config() + + assert config.private == 24 + assert config.public == 25 + + assert Config.help_info() == [("``PUBLIC_FOO``", "``int``", "42", "")] + assert set(Config.help_info(include_private=True)) == { + ("``_PRIVATE_FOO``", "``int``", "42", ""), + ("``PUBLIC_FOO``", "``int``", "42", ""), + }