Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Single type factory args #12

Merged
merged 7 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12"] # format: 3.7, 3.8, 3.9
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] # format: 3.7, 3.8, 3.9
platform: [ubuntu-latest, macos-latest, windows-latest]
fail-fast: false
steps:
- uses: actions/checkout@v2
- name: Set up Python
Expand All @@ -26,6 +27,7 @@ jobs:
run: |
sh scripts/install.sh
- name: Lint
if: matrix.python-version != '3.7'
run: |
poetry run sh scripts/lint.sh
- name: Tests
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
# envolved Changelog
## 1.3.0
### Added
* single-environment variable can now be given additional arguments, that are passed to the parser.
* env-var defaults can now be wrapped in a `Factory` to allow for a default Factory.
### Changed
* type annotation correctness is no longer supported for python 3.7
### Documentation
* Fixed some typos in the documentation
## 1.2.1
### Fixed
* The children of envvars that are excluded from the description are now also excluded.
Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
# -- Project information -----------------------------------------------------

project = "envolved"
copyright = "2020, ben avrahami <[email protected]>"
author = "ben avrahami <[email protected]>"
copyright = "2020, ben avrahami"
author = "ben avrahami"

# -- General configuration ---------------------------------------------------

Expand Down
11 changes: 8 additions & 3 deletions docs/cookbook.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,22 @@ We can actually use :func:`~envvar.inferred_env_var` to infer the name of :class
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
# 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.
Note a sticking point here, we 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={
Expand All @@ -102,11 +105,13 @@ Note a sticking point here, he have to specify not only the type of the 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
# 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
78 changes: 57 additions & 21 deletions docs/envvar.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ EnvVars
.. module:: envvar

.. function:: env_var(key: str, *, type: collections.abc.Callable[[str], T],\
default: T | missing | discard = missing,\
default: T | missing | discard | Factory[T] = missing,\
description: str | collections.abc.Sequence[str] | None = None, \
validators: collections.abc.Iterable[collections.abc.Callable[[T], T]] = (), \
case_sensitive: bool = False, strip_whitespaces: bool = True) -> envvar.SingleEnvVar[T]
Expand All @@ -14,16 +14,17 @@ EnvVars
:param key: The key of the environment variable.
:param type: A callable to use to parse the string value of the environment variable.
:param default: The default value of the EnvVar if the environment variable is missing. If unset, an exception will
be raised if the environment variable is missing. The default can also be set to :attr:`~envvar.discard` to
indicate to parent :class:`SchemaEnvVars <envvar.SchemaEnvVar>` that this env var should be discarded from the
be raised if the environment variable is missing. The default can also be a :class:`~envvar.Factory` to specify a default factory,
or :attr:`~envvar.discard` to indicate to parent :class:`SchemaEnvVars <envvar.SchemaEnvVar>` that this env var should be discarded from the
arguments if it is missing.
:param description: A description of the EnvVar. See :ref:`describing:Describing Environment Variables`.
:param validators: A list of callables to validate the value of the EnvVar. Validators can be added to the EnvVar
after it is created with :func:`~envvar.EnvVar.validator`.
:param case_sensitive: Whether the key of the EnvVar is case sensitive.
:param strip_whitespaces: Whether to strip whitespaces from the value of the environment variable before parsing it.

.. function:: env_var(key: str, *, type: collections.abc.Callable[..., T], default: T | missing = missing, \
.. function:: env_var(key: str, *, type: collections.abc.Callable[..., T], \
default: T | missing | discard | Factory[T] = missing, \
args: dict[str, envvar.EnvVar | InferEnvVar] = ..., \
pos_args: collections.base.Sequence[envvar.EnvVar | InferEnvVar] = ..., \
description: str | collections.abc.Sequence[str] | None = None,\
Expand All @@ -36,8 +37,8 @@ EnvVars
:param key: The key of the environment variable. This will be a common prefix applied to all environment variables.
:param type: A callable to call with ``pos_args`` and ``args`` to create the EnvVar value.
:param default: The default value of the EnvVar if the environment variable is missing. If unset, an exception will
be raised if the environment variable is missing. The default can also be set to :attr:`~envvar.discard` to
indicate to parent :class:`SchemaEnvVars <envvar.SchemaEnvVar>` that this env var should be discarded from the
be raised if the environment variable is missing. The default can also be a :class:`~envvar.Factory` to specify a default factory,
or :attr:`~envvar.discard` to indicate to parent :class:`SchemaEnvVars <envvar.SchemaEnvVar>` that this env var should be discarded from the
arguments if it is missing.
:param pos_args: A sequence of EnvVars to to retrieve and use as positional arguments to ``type``. Arguments can be
:ref:`inferred <infer:Inferred Env Vars>` in some cases.
Expand All @@ -46,15 +47,14 @@ EnvVars
:param description: A description of the EnvVar. See :ref:`describing:Describing Environment Variables`.
:param validators: A list of callables to validate the value of the EnvVar. Validators can be added to the EnvVar
after it is created with :func:`~envvar.EnvVar.validator`.
:param on_partial: The value to use if the EnvVar is partially missing. See
:attr:`~envvar.SchemaEnvVar.on_partial`.
:param on_partial: The value to use if the EnvVar is partially missing. See :attr:`~envvar.SchemaEnvVar.on_partial`.

.. class:: EnvVar

This is the base class for all environment variables.

.. attribute:: default
:type: T | missing | discard
:type: T | missing | discard | envvar.Factory[T]

The default value of the EnvVar. If this attribute is set to anything other than :attr:`missing`, then it will
be used as the default value if the environment variable is not set. If set to :attr:`discard`, then the
Expand Down Expand Up @@ -139,16 +139,6 @@ EnvVars

An :class:`EnvVar` subclass that interfaces with a single environment variable.

When the value is retrieved, it will be searched for in the following order:

#. The environment variable with the name as the :attr:`key` of the EnvVar is considered. If it exists, it will be
used.
#. If :attr:`case_sensitive` is ``False``. Environment variables with case-insensitive names equivalent to
:attr:`key` of the EnvVar is considered. If any exist, they will be used. If multiple exist, a
:exc:`RuntimeError` will be raised.
#. The :attr:`default` value of the EnvVar is used, if it exists.
#. A :exc:`~exceptions.MissingEnvError` is raised.

.. property:: key
:type: str

Expand Down Expand Up @@ -179,6 +169,41 @@ EnvVars
If set to ``True`` (as is the default), whitespaces will be stripped from the environment variable value before
it is processed.

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

Return the value of the environment variable. The value will be searched for in the following order:

#. The environment variable with the name as the :attr:`key` of the EnvVar is considered. If it exists, it will be
used.

#. If :attr:`case_sensitive` is ``False``. Environment variables with case-insensitive names equivalent to
:attr:`key` of the EnvVar is considered. If any exist, they will be used. If multiple exist, a
:exc:`RuntimeError` will be raised.

#. The :attr:`~EnvVar.default` value of the EnvVar is used, if it exists. If the :attr:`~EnvVar.default` is an instance of
:class:`~envvar.Factory`, the factory will be called (without arguments) to create the value of the EnvVar.

#. A :exc:`~exceptions.MissingEnvError` is raised.

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

.. code-block::
:caption: Using SingleEnvVar to fetch a value from an environment variable, with additional keyword arguments.

from dataclasses import dataclass

def parse_users(value: str, *, reverse: bool=False) -> list[str]:
return sorted(value.split(','), reverse=reverse)

users_ev = env_var("USERNAMES", type=parse_users)

if desc:
users = users_ev.get(reverse=True) # will return a list of usernames sorted in reverse order
else:
users = users_ev.get() # will return a list of usernames sorted in ascending order


.. class:: SchemaEnvVar

An :class:`EnvVar` subclass that interfaces with a multiple environment variables, combining them into a single
Expand Down Expand Up @@ -206,7 +231,7 @@ EnvVars
The sequence of positional arguments to the :attr:`type` callable. (read only)

.. attribute:: on_partial
:type: T | as_default | missing | discard
:type: T | as_default | missing | discard | envvar.Factory[T]

This attribute dictates how the EnvVar should behave when only some of the keys are explicitly present (i.e.
When only some of the expected environment variables exist in the environment).
Expand All @@ -215,10 +240,11 @@ EnvVars

.. note::

The EnvVar's :attr:`default` must not be :data:`missing` if this option is used.
The EnvVar's :attr:`~EnvVar.default` must not be :data:`missing` if this option is used.

* If set to :data:`missing`, a :exc:`~exceptions.MissingEnvError` will be raised, even if the EnvVar's
:attr:`~EnvVar.default` is set.
* If set to :class:`~envvar.Factory`, the factory will be called to create the value of the EnvVar.
* If set to a value, that value will be returned.

.. method:: get(**kwargs)->T
Expand Down Expand Up @@ -247,3 +273,13 @@ EnvVars

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.

.. class:: Factory(callback: collections.abc.Callable[[], T])

A wrapped around a callable, indicating that the callable should be used as a factory for creating objects, rather than
as a normal object.

.. attribute:: callback
:type: collections.abc.Callable[[], T]

The callable that will be used to create the object.
4 changes: 2 additions & 2 deletions docs/string_parsing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ All the special parsers are:
* ``complex``: parses the string as a complex number, treating "i" as an imaginary unit in addition to "j".
* 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
* enum type ``E``: translates each enum name to the corresponding enum member, ignoring cases (equivalent to
``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
* pydantic ``BaseModel``: parses the string as JSON and validates it against the model (both pydantic v1 and v2
models are supported).
* pydantic ``TypeAdapter``: parses the string as JSON and validates it against the adapted type.

Expand Down
3 changes: 2 additions & 1 deletion envolved/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from envolved._version import __version__
from envolved.describe import describe_env_vars
from envolved.envvar import EnvVar, as_default, discard, env_var, inferred_env_var, missing, no_patch
from envolved.envvar import EnvVar, Factory, as_default, discard, env_var, inferred_env_var, missing, no_patch
from envolved.exceptions import MissingEnvError

__all__ = [
Expand All @@ -14,4 +14,5 @@
"inferred_env_var",
"missing",
"no_patch",
"Factory",
]
2 changes: 1 addition & 1 deletion envolved/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.2.1"
__version__ = "1.3.0"
20 changes: 1 addition & 19 deletions envolved/envparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def reload(self):
if self.lock.locked():
# if the lock is already held by someone, we don't need to do any work, just wait until they're done
with self.lock:
pass
return
with self.lock:
self.environ_case_insensitive = {}
for k in environ.keys():
Expand Down Expand Up @@ -91,15 +91,6 @@ def audit_hook(self, event: str, args: Tuple[Any, ...]): # pragma: no cover
self.environ_case_insensitive[lower].discard(key)

def get(self, case_sensitive: bool, key: str) -> str:
"""
look up the value of an environment variable.
:param case_sensitive: Whether to make the lookup case-sensitive.
:param key: The environment variable name.
:return: the string value of the environment variable
:raises KeyError: if the variable is missing
:raises CaseInsensitiveAmbiguity: if there is ambiguity over multiple case-insensitive matches.
"""

if case_sensitive:
return getenv_unsafe(key)

Expand All @@ -123,15 +114,6 @@ def get(self, case_sensitive: bool, key: str) -> str:

class NonAuditingEnvParser(ReloadingEnvParser):
def get(self, case_sensitive: bool, key: str) -> str:
"""
look up the value of an environment variable.
:param case_sensitive: Whether to make the lookup case-sensitive.
:param key: The environment variable name.
:return: the string value of the environment variable
:raises KeyError: if the variable is missing
:raises CaseInsensitiveAmbiguity: if there is ambiguity over multiple case-insensitive matches.
"""

if case_sensitive:
return getenv_unsafe(key)

Expand Down
Loading
Loading