Source code for click_extra.envvar
# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
"""Implements environment variable utilities."""
from __future__ import annotations
import os
import re
from typing import Any, Iterable, Mapping
import click
from boltons.iterutils import flatten_iter
TEnvVarID = str | None
TEnvVarIDs = Iterable[TEnvVarID]
TNestedEnvVarIDs = Iterable[TEnvVarID | Iterable["TNestedEnvVarIDs"]]
"""Types environment variable names."""
TEnvVars = Mapping[str, str | None]
"""Type for ``dict``-like environment variables."""
[docs]
def merge_envvar_ids(*envvar_ids: TEnvVarID | TNestedEnvVarIDs) -> tuple[str, ...]:
"""Merge and deduplicate environment variables.
Multiple parameters are accepted and can be single strings or arbitrary-nested
iterables of strings. ``None`` values are ignored.
Variable names are deduplicated while preserving their initial order.
.. caution::
`On Windows, environment variable names are case-insensitive
<https://docs.python.org/3/library/os.html#os.environ>`_, so we `normalize them
to uppercase as the standard library does
<https://github.com/python/cpython/blob/ffef9b0/Lib/os.py#L777-L786>`_.
Returns a tuple of strings. The result is ready to be used as the ``envvar``
parameter for Click's options or arguments.
"""
ids = []
for envvar in flatten_iter(envvar_ids):
if envvar:
if os.name == "nt":
envvar = envvar.upper()
# Deduplicate names.
if envvar not in ids:
ids.append(envvar)
return tuple(ids)
[docs]
def clean_envvar_id(envvar_id: str) -> str:
"""Utility to produce a user-friendly environment variable name from a string.
Separates all contiguous alphanumeric string segments, eliminate empty strings,
join them with an underscore and uppercase the result.
.. attention::
We do not rely too much on this utility to try to reproduce the `current
behavior of Click, which is is not consistent regarding case-handling of
environment variable <https://github.com/pallets/click/issues/2483>`_.
"""
return "_".join(p for p in re.split(r"[^a-zA-Z0-9]+", envvar_id) if p).upper()
[docs]
def param_auto_envvar_id(
param: click.Parameter,
ctx: click.Context | dict[str, Any],
) -> str | None:
"""Compute the auto-generated environment variable of an option or argument.
Returns the auto envvar as it is exactly computed within Click's internals, i.e.
``click.core.Parameter.resolve_envvar_value()`` and
``click.core.Option.resolve_envvar_value()``.
"""
# Skip parameters that have their auto-envvar explicitly disabled.
if not getattr(param, "allow_from_autoenv", None):
return None
if isinstance(ctx, click.Context):
prefix = ctx.auto_envvar_prefix
else:
prefix = ctx.get("auto_envvar_prefix")
if not prefix or not param.name:
return None
# Mimics Click's internals.
return f"{prefix}_{param.name.upper()}"
[docs]
def param_envvar_ids(
param: click.Parameter,
ctx: click.Context | dict[str, Any],
) -> tuple[str, ...]:
"""Returns the deduplicated, ordered list of environment variables for an option or
argument, including the auto-generated one.
The auto-generated environment variable is added at the end of the list, so that
user-defined envvars takes precedence. This respects the current implementation
of ``click.core.Option.resolve_envvar_value()``.
.. caution::
`On Windows, environment variable names are case-insensitive
<https://docs.python.org/3/library/os.html#os.environ>`_, so we `normalize them
to uppercase as the standard library does
<https://github.com/python/cpython/blob/ffef9b0/Lib/os.py#L777-L786>`_.
"""
return merge_envvar_ids(param.envvar, param_auto_envvar_id(param, ctx))
[docs]
def env_copy(extend: TEnvVars | None = None) -> TEnvVars | None:
"""Returns a copy of the current environment variables and eventually ``extend`` it.
Mimics `Python's original implementation
<https://github.com/python/cpython/blob/7b5b429/Lib/subprocess.py#L1648-L1649>`_ by
returning ``None`` if no ``extend`` content are provided.
Environment variables are expected to be a ``dict`` of ``str:str``.
"""
if isinstance(extend, dict):
for k, v in extend.items():
assert isinstance(k, str)
assert isinstance(v, str)
else:
assert not extend
env_copy: TEnvVars | None = None
if extend:
# By casting to dict we make a copy and prevent the modification of the
# global environment.
env_copy = dict(os.environ)
env_copy.update(extend)
return env_copy