Skip to content

Commit

Permalink
changes for 1.1.0 (#8)
Browse files Browse the repository at this point in the history
* changes for 1.1.0

* added lookupparser

* lint

---------

Co-authored-by: Ben Avrahami <[email protected]>
  • Loading branch information
bentheiii and Ben Avrahami authored Dec 10, 2023
1 parent 0a83f5d commit cbc7e7e
Show file tree
Hide file tree
Showing 13 changed files with 363 additions and 46 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
docs/_build
**/__pycache__
.vscode/settings.json
poetry.lock
coverage.xml
.coverage
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
# envolved Changelog
## 1.1.0
### Added
* Single env vars can now accept pydantic models and type adapters, they will be parsed as jsons.
* added `py.typed` file to the package.
* added `inferred_env_var` to the root `envolved` namespace.
* schema env vars can now have keyword arguments passed to their `get` method, to add values to the schema.
* new parse: `LookupParser`, as a faster alternative to `MatchParser` (that does not support regex matches).
### Changed
* the special parser of `Enum`s is now `LookupParser` instead of `MatchParser`.
### Fixed
* `exclude_from_description` now ignores inferred env vars.
## 1.0.0
### Added
* `inferred_env_var` to explicitly infer the type, name and default value of an env var.
Expand Down
27 changes: 27 additions & 0 deletions docs/basevar.rst
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ EnvVar Classes
When the value is retrieved, all its :attr:`args` and :attr:`pos_args` are retrieved, and are then used as keyword variables on the
EnvVar's :attr:`type`.

Users can also supply keyword arguments to the :meth:`get` method, which will be supplied to the :attr:`type` in addition/instead of
the child EnvVars.

.. property:: type
:type: collections.abc.Callable[..., T]

Expand Down Expand Up @@ -171,5 +174,29 @@ EnvVar Classes
:attr:`~EnvVar.default` is set.
* If set to a value, that value will be returned.

.. method:: get(**kwargs)->T

Return the value of the environment variable. The value will be created by calling the :attr:`type` callable
with the values of all the child EnvVars as keyword arguments, and the values of the ``kwargs`` parameter as
additional keyword arguments.

:param kwargs: Additional keyword arguments to pass to the :attr:`type` callable.
:return: The value of the environment variable.

.. code-block::
:caption: Using SchemaEnvVar to create a class from multiple environment variables, with additional keyword arguments.
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
height: int
user_ev = env_var("USER_", type=User,
args={'name': env_var('NAME', type=str),
'age': env_var('AGE', type=int)})
user_ev.get(age=20, height=168) # will return a User object with the name taken from the environment variables,
# but with the age and height overridden by the keyword arguments.
42 changes: 39 additions & 3 deletions docs/string_parsing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ All the special parsers are:
* union type ``A | None`` or ``typing.Union[A, None]`` or ``typing.Optional[A]``: Will treat the parser as though it
only parses ``A``.
* enum type ``E``: translates each enum name to the corresponding enum member, disregarding cases (equivalent to
``MatchParser.case_insensitive(E)`` see :class:`~parsers.MatchParser`).
``LookupParser.case_insensitive(E)`` see :class:`~parsers.LookupParser`).
* pydantic ``BaseModel``: parses the string as JSON and validates it against the model (both pydnatic v1 and v2
models are supported).
* pydantic ``TypeAdapter``: parses the string as JSON and validates it against the adapted type.


Utility Parsers
---------------
Expand Down Expand Up @@ -138,7 +142,7 @@ Utility Parsers

A parser that checks a string against a se of cases, returning the value of first case that matches.

:param cases: An iterable of match-value pairs. The match an be a string or a regex pattern (which will need to
:param cases: An iterable of match-value pairs. The match can be a string or a regex pattern (which will need to
fully match the string). The case list can also be a mapping of strings to values, or an enum type, in
which case the names of the enum members will be used as the matches.
:param fallback: The value to return if no case matches. If not specified, an exception will be raised.
Expand All @@ -163,4 +167,36 @@ Utility Parsers
case-insensitivity, an error will be raised.

:param cases: Acts the same as in the :class:`constructor <MatchParser>`. Regex patterns are not supported.
:param fallback: Acts the same as in the :class:`constructor <MatchParser>`.
:param fallback: Acts the same as in the :class:`constructor <MatchParser>`.

.. class:: LookupParser(lookup: collection.abc.Iterable[tuple[str, T]] | \
collections.abc.Mapping[str, T] | type[enum.Enum], fallback: T = ...)

A parser that checks a string against a set of cases, returning the value of the matching case. This is a more efficient
version of :class:`MatchParser` that uses a dictionary to store the cases.

:param lookup: An iterable of match-value pairs, a mapping of strings to values, or an enum type,
in which case the names of the enum members will be used as the matches.
:param fallback: The value to return if no case matches. If not specified, an exception will be raised.

.. code-block::
class Color(enum.Enum):
RED = 1
GREEN = 2
BLUE = 3
color_ev = env_var("COLOR", type=LookupParser(Color))
os.environ["COLOR"] = "RED"
assert color_ev.get() == Color.RED
.. classmethod:: case_insensitive(lookup: collection.abc.Iterable[tuple[str, T]] | \
collections.abc.Mapping[str, T] | type[enum.Enum], fallback: T = ...) -> LookupParser[T]

Create a :class:`LookupParser` where the matches are case insensitive. If two cases are equivalent under
case-insensitivity, an error will be raised.

:param lookup: Acts the same as in the :class:`constructor <LookupParser>`.
:param fallback: Acts the same as in the :class:`constructor <LookupParser>`.
3 changes: 2 additions & 1 deletion envolved/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
from envolved.describe import describe_env_vars
from envolved.envvar import env_var
from envolved.exceptions import MissingEnvError
from envolved.infer_env_var import inferred_env_var

__all__ = ["__version__", "env_var", "EnvVar", "MissingEnvError", "describe_env_vars", "as_default"]
__all__ = ["__version__", "env_var", "EnvVar", "MissingEnvError", "describe_env_vars", "as_default", "inferred_env_var"]
2 changes: 1 addition & 1 deletion envolved/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.0.0"
__version__ = "1.1.0"
28 changes: 20 additions & 8 deletions envolved/basevar.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,19 +101,22 @@ def __init__(
self.monkeypatch: Union[T, Missing, Discard, NoPatch] = no_patch

def get(self) -> T:
return self._get_with()

def _get_with(self, **kwargs: Any) -> T:
if self.monkeypatch is not no_patch:
if self.monkeypatch is missing:
raise MissingEnvError(self.describe())
return self.monkeypatch # type: ignore[return-value]
return self._get_validated().value # type: ignore[return-value]
return self._get_validated(**kwargs).value # type: ignore[return-value]

def validator(self, validator: Callable[[T], T]) -> EnvVar[T]:
self._validators.append(validator)
return self

def _get_validated(self) -> _EnvVarResult[T]:
def _get_validated(self, **kwargs: Any) -> _EnvVarResult[T]:
try:
value = self._get()
value = self._get(**kwargs)
except SkipDefault as sd:
raise sd.args[0] from None
except MissingEnvError as mee:
Expand All @@ -125,7 +128,7 @@ def _get_validated(self) -> _EnvVarResult[T]:
return _EnvVarResult(value, exists=True)

@abstractmethod
def _get(self) -> T:
def _get(self, **kwargs: Any) -> T:
pass

@abstractmethod
Expand Down Expand Up @@ -176,7 +179,9 @@ def key(self) -> str:
def type(self) -> Parser[T]:
return self._type

def _get(self) -> T:
def _get(self, **kwargs: Any) -> T:
if kwargs:
raise TypeError(f"unexpected keyword arguments {kwargs!r}")
try:
raw_value = env_parser.get(self.case_sensitive, self._key)
except KeyError as err:
Expand Down Expand Up @@ -255,9 +260,12 @@ def on_partial(self, value: Union[T, Missing, AsDefault, Discard]):
raise TypeError("on_partial cannot be as_default if default is missing")
self._on_partial = value

def _get(self) -> T:
def get(self, **kwargs: Any) -> T:
return super()._get_with(**kwargs)

def _get(self, **kwargs: Any) -> T:
pos_values = []
kw_values = {}
kw_values = kwargs
any_exist = False
errs: List[MissingEnvError] = []
for env_var in self._pos_args:
Expand All @@ -272,9 +280,13 @@ def _get(self) -> T:
if result.exists:
any_exist = True
for key, env_var in self._args.items():
if key in kw_values:
# key could be in kwargs because it was passed in as a positional argument, if so, we don't want to
# overwrite it
continue
try:
result = env_var._get_validated() # noqa: SLF001
except MissingEnvError as e: # noqa: PERF203
except MissingEnvError as e:
errs.append(e)
else:
if result.value is not discard:
Expand Down
8 changes: 7 additions & 1 deletion envolved/describe.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,27 @@

from envolved.basevar import _Description
from envolved.envvar import EnvVar, top_level_env_vars
from envolved.infer_env_var import InferEnvVar


def describe_env_vars(**kwargs: Any) -> List[str]:
descriptions: List[_Description] = sorted(env_var.describe(**kwargs) for env_var in top_level_env_vars)
return _Description.combine(descriptions, [], allow_blanks=True).lines


T = TypeVar("T", bound=Union[EnvVar, Iterable[EnvVar], Mapping[Any, EnvVar]])
T = TypeVar(
"T",
bound=Union[EnvVar, InferEnvVar, Iterable[Union[EnvVar, InferEnvVar]], Mapping[Any, Union[EnvVar, InferEnvVar]]],
)


def exclude_from_description(to_exclude: T) -> T:
global top_level_env_vars # noqa: PLW0603

if isinstance(to_exclude, EnvVar):
evs = frozenset((to_exclude,))
elif isinstance(to_exclude, InferEnvVar):
evs = frozenset()
elif isinstance(to_exclude, Mapping):
evs = frozenset(to_exclude.values())
elif isinstance(to_exclude, Iterable):
Expand Down
Loading

0 comments on commit cbc7e7e

Please sign in to comment.