From f938fe6608fc5e0c22078a978796ea3396236a0c Mon Sep 17 00:00:00 2001 From: Ben Avrahami Date: Tue, 2 Jan 2024 02:29:01 +0200 Subject: [PATCH] doc done --- docs/cookbook.rst | 47 ++++++++++++++++++++++++++++++- docs/describing.rst | 52 +++++++++++++++++++++++++++++++++-- docs/envvar.rst | 2 +- docs/string_parsing.rst | 2 +- envolved/basevar.py | 11 ++++++++ envolved/describe/__init__.py | 10 +++---- envolved/describe/flat.py | 21 +++++++------- envolved/describe/nested.py | 20 +++++++------- envolved/infer_env_var.py | 4 ++- pyproject.toml | 2 +- 10 files changed, 138 insertions(+), 33 deletions(-) diff --git a/docs/cookbook.rst b/docs/cookbook.rst index 8577f3a..cdd4f13 100644 --- a/docs/cookbook.rst +++ b/docs/cookbook.rst @@ -64,4 +64,49 @@ Here are some common types and factories to use when creating a :class:`~envvar. 'y': inferred_env_var(), }) - # this will result in a dict that has ints for keys "x" and "y" \ No newline at end of file + # this will result in a dict that has ints for keys "x" and "y" + +Inferring Schema Parameter Names Without a Schema +-------------------------------------------------- + +We can actually use :func:`~envvar.inferred_env_var` to infer the name of :class:`~envvar.EnvVar` parameters without a schema. This is useful when +we want to prototype a schema without having to create a schema class. + +.. code-block:: + from envolved import ... + + my_schema_ev = env_var('FOO_', type=SimpleNamespace, args={ + 'x': inferred_env_var(type=int, default=0), + 'y': inferred_env_var(type=string, default='hello'), + }) + + # this will result in a namespace that fills `x` and `y` with the values of `FOO_X` and `FOO_Y` respectively + + +Note a sticking point here, he have to specify not only the type of the inferred env var, but also the default value. + +.. code-block:: + from envolved import ... + + my_schema_ev = env_var('FOO_', type=SimpleNamespace, args={ + 'x': inferred_env_var(type=int), # <-- this code will raise an exception + }) + +.. note:: Why is this the behaviour? + + In normal :func:`~envvar.env_var`, not passing a `default` implies that the EnvVar is required, why can't we do the same for :func:`~envvar.inferred_env_var`? We do this to reduce side + effects when an actual schema is passed in. If we were to assume that the inferred env var is required, then plugging in a schema that has a default value for that parameter would be + a hard-to-detect breaking change that can have catostraphic consequences. By requiring the default value to be passed in, we force the user to be explicit about the default values, + ehan it might be inferred. + +We can specify that an inferred env var is required by explicitly stating `default=missing` + +.. code-block:: + from envolved import ..., missing + + my_schema_ev = env_var('FOO_', type=SimpleNamespace, args={ + 'x': inferred_env_var(type=int, default=missing), + 'y': inferred_env_var(type=string, default='hello'), + }) + + # this will result in a namespace that fills `x` with the value of `FOO_X` and will raise an exception if `FOO_X` is not set diff --git a/docs/describing.rst b/docs/describing.rst index 0d88b57..8131877 100644 --- a/docs/describing.rst +++ b/docs/describing.rst @@ -14,7 +14,7 @@ Another feature of envolved is the ability to describe all EnvVars. 'level': env_var('_LEVEL', type=int, default=20), }) - print('\n'join(describe_env_vars())) + print('\n'.join(describe_env_vars())) # OUTPUT: # BACKLOG_SIZE: Backlog size @@ -33,13 +33,15 @@ Another feature of envolved is the ability to describe all EnvVars. .. function:: describe_env_vars(**kwargs)->List[str] Returns a list of string lines that describe all the EnvVars. All keyword arguments are passed to - :class:`textwrap.wrap` to wrap the lines. + :func:`textwrap.wrap` to wrap the lines. .. note:: This function will include a description of every alive EnvVar. EnvVars defined in functions, for instance, will not be included. +Descriptions + Excluding EnvVars from the description ------------------------------------------ @@ -70,3 +72,49 @@ In some cases it is useful to exclude some EnvVars from the description. This ca of EnvVar names to EnvVars. :return: `env_vars`, to allow for piping. +.. class:: EnvVarsDescription(env_vars: collections.abc.Iterable[EnvVar] | None) + + A class that allows for more fine-grained control over the description of EnvVars. + + :param env_vars: A collection of EnvVars to describe. If None, all alive EnvVars will be described. If the collection + includes two EnvVars, one which is a parent of the other, only the parent will be described. + + .. method:: flat()->FlatEnvVarsDescription + + Returns a flat description of the EnvVars. + + .. method:: nested()->NestedEnvVarsDescription + + Returns a nested description of the EnvVars. + +.. class:: FlatEnvVarsDescription + + A flat representation of the EnvVars description. Only single-environment variable EnvVars (or single-environment variable children of envars) will be described. + + .. method:: wrap_sorted(*, unique_keys: bool = True, **kwargs)->List[str] + + Returns a list of string lines that describe the EnvVars, sorted by their environment variable key. + + :param unique_keys: If True, and if any EnvVars share an environment variable key, they will be combined into one description. + :param kwargs: Keyword arguments to pass to :func:`textwrap.wrap`. + :return: A list of string lines that describe the EnvVars. + + .. method:: wrap_grouped(**kwargs)->List[str] + + Returns a list of string lines that describe the EnvVars, sorted by their environment variable key, but env-vars that are used by the same schema will appear together. + + :param kwargs: Keyword arguments to pass to :func:`textwrap.wrap`. + :return: A list of string lines that describe the EnvVars. + +.. class:: NestedEnvVarsDescription + + A nested representation of the EnvVars description. All EnvVars will be described. + + .. method:: wrap(indent_increment: str = ..., **kwargs)->List[str] + + Returns a list of string lines that describe the EnvVars in a tree structure. + + :param indent_increment: The string to use to increment the indentation of the description with each level. If not provided, + will use the keyword argument "subsequent_indent" from :func:`textwrap.wrap`, if provided. Otherwise, will use a single space. + :param kwargs: Keyword arguments to pass to :func:`textwrap.wrap`. + :return: A list of string lines that describe the EnvVars. \ No newline at end of file diff --git a/docs/envvar.rst b/docs/envvar.rst index b3fe6cb..1fac482 100644 --- a/docs/envvar.rst +++ b/docs/envvar.rst @@ -1,4 +1,4 @@ -Creating EnvVars +EnvVars ========================================================= .. module:: envvar diff --git a/docs/string_parsing.rst b/docs/string_parsing.rst index 72cfff6..cd2babd 100644 --- a/docs/string_parsing.rst +++ b/docs/string_parsing.rst @@ -94,7 +94,7 @@ Utility Parsers value_type: ParserInput[V] | collections.abc.Mapping[K, ParserInput[V]], \ output_type: collections.abc.Callable[[collections.abc.Iterable[tuple[K,V]]], G] = ..., *, \ key_first: bool = True, opener: str | typing.Pattern = '', \ - closer: str | typing.Pattern = '', strip: bool = True, strip_keys: bool = True, strip_values: bool = True, strip_items) -> CollectionParser[G] + closer: str | typing.Pattern = '', strip: bool = True, strip_keys: bool = True, strip_values: bool = True) -> CollectionParser[G] A factory method to create a :class:`CollectionParser` where each item is a delimited key-value pair. diff --git a/envolved/basevar.py b/envolved/basevar.py index c400efe..a0a30cd 100644 --- a/envolved/basevar.py +++ b/envolved/basevar.py @@ -1 +1,12 @@ # this module is to preserved backwards compatibility +from envolved.envvar import EnvVar, SchemaEnvVar, SingleEnvVar, as_default, discard, missing, no_patch + +__all__ = [ + "EnvVar", + "as_default", + "discard", + "missing", + "no_patch", + "SchemaEnvVar", + "SingleEnvVar", +] diff --git a/envolved/describe/__init__.py b/envolved/describe/__init__.py index 1845d6e..8ae3bf8 100644 --- a/envolved/describe/__init__.py +++ b/envolved/describe/__init__.py @@ -2,8 +2,8 @@ from typing import Any, Iterable, List, Mapping, Set, TypeVar, Union -from envolved.describe.flat import SingleEnvVarDescriptionSet -from envolved.describe.nested import NestedDescription, RootNestedDescription +from envolved.describe.flat import FlatEnvVarsDescription +from envolved.describe.nested import NestedEnvVarsDescription, RootNestedDescription from envolved.envvar import EnvVar, InferEnvVar, top_level_env_vars @@ -26,10 +26,10 @@ def __init__(self, env_vars: Iterable[EnvVar] | None = None) -> None: # remove any children we found along the way self.env_var_roots -= children - def flat(self) -> SingleEnvVarDescriptionSet: - return SingleEnvVarDescriptionSet.from_envvars(self.env_var_roots) + def flat(self) -> FlatEnvVarsDescription: + return FlatEnvVarsDescription.from_envvars(self.env_var_roots) - def nested(self) -> NestedDescription: + def nested(self) -> NestedEnvVarsDescription: return RootNestedDescription.from_envvars(self.env_var_roots) diff --git a/envolved/describe/flat.py b/envolved/describe/flat.py index d6d8f3a..95c3308 100644 --- a/envolved/describe/flat.py +++ b/envolved/describe/flat.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from itertools import chain, groupby -from typing import Any, Iterable, List, Optional, Tuple +from typing import Any, Iterable, List, Tuple from warnings import warn from envolved.describe.util import prefix_description, wrap_description as wrap @@ -77,7 +77,7 @@ def collate(cls, instances: Iterable[SingleEnvVarDescription]) -> SingleEnvVarDe return without_description[0] -class SingleEnvVarDescriptionSet: +class FlatEnvVarsDescription: def __init__(self, env_var_descriptions: Iterable[SingleEnvVarDescription]) -> None: self.env_var_descriptions = env_var_descriptions @@ -98,19 +98,18 @@ def key(i: SingleEnvVarDescription) -> str: return ret - def wrap_grouped(self, group_sep_line: Optional[str] = None, **kwargs: Any) -> Iterable[str]: + def wrap_grouped(self, **kwargs: Any) -> Iterable[str]: env_var_descriptions = sorted(self.env_var_descriptions, key=lambda i: (i.path, i.env_var.key)) - first_group = True - ret = [] - for _, group in groupby(env_var_descriptions, key=lambda i: i.path): - if not first_group and group_sep_line is not None: - ret.append(group_sep_line) - first_group = False - ret.extend(chain.from_iterable(d.wrap(**kwargs) for d in group)) + ret = list( + chain.from_iterable( + chain.from_iterable(d.wrap(**kwargs) for d in group) + for _, group in groupby(env_var_descriptions, key=lambda i: i.path) + ) + ) return ret @classmethod - def from_envvars(cls, env_vars: Iterable[EnvVar]) -> SingleEnvVarDescriptionSet: + def from_envvars(cls, env_vars: Iterable[EnvVar]) -> FlatEnvVarsDescription: env_var_descriptions = list( chain.from_iterable(SingleEnvVarDescription.from_envvar((), env_var) for env_var in env_vars) ) diff --git a/envolved/describe/nested.py b/envolved/describe/nested.py index ab5102a..a2d9064 100644 --- a/envolved/describe/nested.py +++ b/envolved/describe/nested.py @@ -2,13 +2,13 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Any, Iterable, Tuple +from typing import Any, Iterable, Optional, Tuple from envolved.describe.util import prefix_description, suffix_description, wrap_description as wrap from envolved.envvar import Description, EnvVar, SchemaEnvVar, SingleEnvVar -class NestedDescription(ABC): +class NestedEnvVarsDescription(ABC): @abstractmethod def get_path(self) -> Tuple[str, ...]: ... @@ -18,7 +18,7 @@ def wrap(self, *, indent_increment: str, **kwargs: Any) -> Iterable[str]: ... @classmethod - def from_env_var(cls, path: Tuple[str, ...], env_var: EnvVar) -> NestedDescription: + def from_env_var(cls, path: Tuple[str, ...], env_var: EnvVar) -> NestedEnvVarsDescription: if isinstance(env_var, SingleEnvVar): path = (*path, env_var.key.upper()) return SingleNestedDescription(path, env_var) @@ -35,7 +35,7 @@ def from_env_var(cls, path: Tuple[str, ...], env_var: EnvVar) -> NestedDescripti @dataclass -class SingleNestedDescription(NestedDescription): +class SingleNestedDescription(NestedEnvVarsDescription): path: Tuple[str, ...] env_var: SingleEnvVar @@ -62,8 +62,8 @@ def wrap(self, *, indent_increment: str, **kwargs: Any) -> Iterable[str]: return wrap(text, **kwargs) -class NestedDescriptionWithChildren(NestedDescription): - children: Iterable[NestedDescription] +class NestedDescriptionWithChildren(NestedEnvVarsDescription): + children: Iterable[NestedEnvVarsDescription] @abstractmethod def title(self) -> Description | None: @@ -83,7 +83,7 @@ def wrap(self, *, indent_increment: str, **kwargs: Any) -> Iterable[str]: class SchemaNestedDescription(NestedDescriptionWithChildren): path: Tuple[str, ...] env_var: SchemaEnvVar - children: Iterable[NestedDescription] + children: Iterable[NestedEnvVarsDescription] def get_path(self) -> Tuple[str, ...]: return self.path @@ -97,7 +97,7 @@ def title(self) -> Description | None: @dataclass class RootNestedDescription(NestedDescriptionWithChildren): - children: Iterable[NestedDescription] + children: Iterable[NestedEnvVarsDescription] def get_path(self) -> Tuple[str, ...]: return () @@ -107,9 +107,9 @@ def title(self) -> Description | None: @classmethod def from_envvars(cls, env_vars: Iterable[EnvVar]) -> RootNestedDescription: - return cls([NestedDescription.from_env_var((), env_var) for env_var in env_vars]) + return cls([NestedEnvVarsDescription.from_env_var((), env_var) for env_var in env_vars]) - def wrap(self, *, indent_increment: str | None = None, **kwargs: Any) -> Iterable[str]: + def wrap(self, *, indent_increment: Optional[str] = None, **kwargs: Any) -> Iterable[str]: if indent_increment is None: indent_increment = kwargs.get("subsequent_indent", " ") assert isinstance(indent_increment, str) diff --git a/envolved/infer_env_var.py b/envolved/infer_env_var.py index 2da9ac3..063aad8 100644 --- a/envolved/infer_env_var.py +++ b/envolved/infer_env_var.py @@ -1,4 +1,6 @@ -from envolved.envvar import InferEnvVar +from envolved.envvar import InferEnvVar, inferred_env_var + +__all__ = ["InferEnvVar", "inferred_env_var"] # this module is to preserved backwards compatibility diff --git a/pyproject.toml b/pyproject.toml index e5d66c6..954de1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "envolved" -version = "1.1.2" +version = "1.2.0" description = "" authors = ["ben avrahami "] license = "MIT"