You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
184 lines
5.9 KiB
184 lines
5.9 KiB
# Copyright 2020 The Pigweed Authors
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
# use this file except in compliance with the License. You may obtain a copy of
|
|
# the License at
|
|
#
|
|
# https://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations under
|
|
# the License.
|
|
"""The envparse module defines an environment variable parser."""
|
|
|
|
import argparse
|
|
from dataclasses import dataclass
|
|
import os
|
|
from typing import Callable, Dict, Generic, IO, List, Mapping, Optional, TypeVar
|
|
|
|
|
|
class EnvNamespace(argparse.Namespace): # pylint: disable=too-few-public-methods
|
|
"""Base class for parsed environment variable namespaces."""
|
|
|
|
|
|
T = TypeVar('T')
|
|
TypeConversion = Callable[[str], T]
|
|
|
|
|
|
@dataclass
|
|
class VariableDescriptor(Generic[T]):
|
|
name: str
|
|
type: TypeConversion[T]
|
|
default: Optional[T]
|
|
|
|
|
|
class EnvironmentValueError(Exception):
|
|
"""Exception indicating a bad type conversion on an environment variable.
|
|
|
|
Stores a reference to the lower-level exception from the type conversion
|
|
function through the __cause__ attribute for more detailed information on
|
|
the error.
|
|
"""
|
|
def __init__(self, variable: str, value: str):
|
|
self.variable: str = variable
|
|
self.value: str = value
|
|
super().__init__(
|
|
f'Bad value for environment variable {variable}: {value}')
|
|
|
|
|
|
class EnvironmentParser:
|
|
"""Parser for environment variables.
|
|
|
|
Args:
|
|
prefix: If provided, checks that all registered environment variables
|
|
start with the specified string.
|
|
error_on_unrecognized: If True and prefix is provided, will raise an
|
|
exception if the environment contains a variable with the specified
|
|
prefix that is not registered on the EnvironmentParser.
|
|
|
|
Example:
|
|
|
|
parser = envparse.EnvironmentParser(prefix='PW_')
|
|
parser.add_var('PW_LOG_LEVEL')
|
|
parser.add_var('PW_LOG_FILE', type=envparse.FileType('w'))
|
|
parser.add_var('PW_USE_COLOR', type=envparse.strict_bool, default=False)
|
|
env = parser.parse_env()
|
|
|
|
configure_logging(env.PW_LOG_LEVEL, env.PW_LOG_FILE)
|
|
"""
|
|
def __init__(self,
|
|
prefix: Optional[str] = None,
|
|
error_on_unrecognized: bool = True) -> None:
|
|
self._prefix: Optional[str] = prefix
|
|
self._error_on_unrecognized: bool = error_on_unrecognized
|
|
self._variables: Dict[str, VariableDescriptor] = {}
|
|
self._allowed_suffixes: List[str] = []
|
|
|
|
def add_var(
|
|
self,
|
|
name: str,
|
|
# pylint: disable=redefined-builtin
|
|
type: TypeConversion[T] = str, # type: ignore[assignment]
|
|
# pylint: enable=redefined-builtin
|
|
default: Optional[T] = None,
|
|
) -> None:
|
|
"""Registers an environment variable.
|
|
|
|
Args:
|
|
name: The environment variable's name.
|
|
type: Type conversion for the variable's value.
|
|
default: Default value for the variable.
|
|
|
|
Raises:
|
|
ValueError: If prefix was provided to the constructor and name does
|
|
not start with the prefix.
|
|
"""
|
|
if self._prefix is not None and not name.startswith(self._prefix):
|
|
raise ValueError(
|
|
f'Variable {name} does not have prefix {self._prefix}')
|
|
|
|
self._variables[name] = VariableDescriptor(name, type, default)
|
|
|
|
def add_allowed_suffix(self, suffix: str) -> None:
|
|
"""Registers an environmant variable name suffix to be allowed."""
|
|
|
|
self._allowed_suffixes.append(suffix)
|
|
|
|
def parse_env(self,
|
|
env: Optional[Mapping[str, str]] = None) -> EnvNamespace:
|
|
"""Parses known environment variables into a namespace.
|
|
|
|
Args:
|
|
env: Dictionary of environment variables. Defaults to os.environ.
|
|
|
|
Raises:
|
|
EnvironmentValueError: If the type conversion fails.
|
|
"""
|
|
if env is None:
|
|
env = os.environ
|
|
|
|
namespace = EnvNamespace()
|
|
|
|
for var, desc in self._variables.items():
|
|
if var not in env:
|
|
val = desc.default
|
|
else:
|
|
try:
|
|
val = desc.type(env[var]) # type: ignore
|
|
except Exception as err:
|
|
raise EnvironmentValueError(var, env[var]) from err
|
|
|
|
setattr(namespace, var, val)
|
|
|
|
allowed_suffixes = tuple(self._allowed_suffixes)
|
|
for var in env:
|
|
if (not hasattr(namespace, var)
|
|
and (self._prefix is None or var.startswith(self._prefix))
|
|
and var.endswith(allowed_suffixes)):
|
|
setattr(namespace, var, env[var])
|
|
|
|
if self._prefix is not None and self._error_on_unrecognized:
|
|
for var in env:
|
|
if (var.startswith(self._prefix) and var not in self._variables
|
|
and not var.endswith(allowed_suffixes)):
|
|
raise ValueError(
|
|
f'Unrecognized environment variable {var}')
|
|
|
|
return namespace
|
|
|
|
def __repr__(self) -> str:
|
|
return f'{type(self).__name__}(prefix={self._prefix})'
|
|
|
|
|
|
# List of emoji which are considered to represent "True".
|
|
_BOOLEAN_TRUE_EMOJI = set([
|
|
'✔️',
|
|
'👍',
|
|
'👍🏻',
|
|
'👍🏼',
|
|
'👍🏽',
|
|
'👍🏾',
|
|
'👍🏿',
|
|
'💯',
|
|
])
|
|
|
|
|
|
def strict_bool(value: str) -> bool:
|
|
return (value == '1' or value.lower() == 'true'
|
|
or value in _BOOLEAN_TRUE_EMOJI)
|
|
|
|
|
|
# TODO(mohrr) Switch to Literal when no longer supporting Python 3.7.
|
|
# OpenMode = Literal['r', 'rb', 'w', 'wb']
|
|
OpenMode = str
|
|
|
|
|
|
class FileType:
|
|
def __init__(self, mode: OpenMode) -> None:
|
|
self._mode: OpenMode = mode
|
|
|
|
def __call__(self, value: str) -> IO:
|
|
return open(value, self._mode)
|