Module attrbox.env

Load configuration from environment files.

The file format is similar to a Bash file, but it is not as complete as python-dotenv.

Supported:

  • unquoted and single-quoted key
  • unquoted, single- and double-quoted value
  • spaces before and after key, equal sign, and value are ignored
  • export at the start of the line is ignored
  • # comment at the start of the line
  • value expansion for values in the environment
  • value expansion for values only in the file (e.g., when update_env=False)

Unsupported:

  • key without a value
  • multiline value
  • # comment after value
  • escape sequences in value

Examples

>>> loads('''
... # a comment
... normal=value
... ' quoted '=" space "
... export expanded="expanded-${normal}"
... ''')
{'normal': 'value', ' quoted ': ' space ', 'expanded': 'expanded-value'}

Global variables

var PathStr

Type representing a Path or a string to a path.

Functions

def expand(value: str, store: Optional[Mapping[str, str]] = None, *, dotted_keys: bool = False) ‑> str

Expand variables of the form $var and ${var}.

A simplified form of os.path.expandvars.

Args

value : str
value to expand
store : Mapping[str, str], optional
valid substitutions. If None, os.environ is used. Defaults to None.
dotted_keys : bool
if True allow ${dotted.name} to map to nested values {"dotted": {"name": "value"}}. Defaults to False.

Returns

str
expanded value. Unknown variables are left unchanged.

Examples

Regular expansion works as expected:

>>> expand("$a ${b}", {'a': 'hello', 'b': 'world'})
'hello world'

Unknown variables are left unchanged:

>>> expand("$a is $b", {'a': 'this'})
'this is $b'
>>> expand("no vars", {})
'no vars'

Values are passed to str:

>>> expand("$a", {'a': 5})
'5'

Dotted names are optionally possible:

>>> expand("${a.b}", {"a": {"b": "works"}}, dotted_keys=True)
'works'
def load(file: SupportsRead, /, *, update_env: bool = True, dotted_keys: bool = True) ‑> Dict[str, str]

Load an environment file.

Args

file : SupportsRead
file-like (has .read()).
update_env : bool, optional
If True, update the os.path.environ as values are read in. Defaults to True.
dotted_keys : bool, optional
If True, split the key by . and use that to create a nested dict. Defaults to True.

Returns

Dict[str, str]
configuration values

Examples

>>> root = Path(__file__).parent.parent.parent
>>> load((root / "test/config_3.env").open())
{'section': {'key': 'value3', 'env': 'loaded'}}
def loads(text: str, /, *, update_env: bool = True, dotted_keys: bool = True) ‑> Dict[str, str]

Parse an environment file from a string.

Args

text : str
text to parse.
update_env : bool, optional
If True, update the os.path.environ as values are read in. Defaults to True.
dotted_keys : bool, optional
If True, split the key by . and use that to create a nested dict. Defaults to True.

Returns

Dict[str, str]
configuration values

Examples

If you don't want to update the environment:

>>> 'fake' in ENV
False
>>> loads('''export 'fake'=ignored
... works=not $fake''', update_env=False)
{'fake': 'ignored', 'works': 'not ignored'}
>>> 'fake' in ENV
False

Keys with dots in them create nested dicts, but are optional:

>>> loads('section.key=value', update_env=False, dotted_keys=True)
{'section': {'key': 'value'}}
>>> loads('section.key=value', update_env=False, dotted_keys=False)
{'section.key': 'value'}
def find_env(path: Union[pathlib._local.Path, str, ForwardRef(None)] = None, name: str = '.env') ‑> Optional[pathlib._local.Path]

Find the .env file in the ancestors of the current path.

Args

path : PathLike, optional
A starting path to check. If None, starts with the current working directory. Defaults to None.
name : str, optional
file name to search for. Defaults to ".env".

Returns

Optional[Path]
path to environment file or None if it is not found.

Examples

Search from the current working directory:

>>> str(find_env())
'.../.env'

Search from a specific directory:

>>> str(find_env("."))
'.../.env'

Pass a Path object:

>>> str(find_env(Path(__file__)))
'.../.env'

Point directly to the .env file:

>>> str(find_env(Path(__file__).parent.parent.parent / ".env"))
'.../.env'
def load_env(path: Union[pathlib._local.Path, str, ForwardRef(None)] = None) ‑> Dict[str, str]

Load an environment file.

We recursively search for a .env file from the path given or the current working directory, if omitted.

Args

path : PathStr, optional
starting path. If None, start from the current working directory. Defaults to None.

Raises

FileNotFoundError
If not .env file is found.

Returns

Dict[str, str]
configuration values

Examples

>>> load_env() # our .env doesn't have any values
{}

If no .env can be found, a FileNotFoundError is raised:

>>> load_env("/")
Traceback (most recent call last):
    ...
FileNotFoundError: Cannot find .env file to load.

Classes

class SupportsRead (*args, **kwargs)

Protocol for a class that implements a .read() method.

Expand source code
class SupportsRead(Protocol):  # pylint: disable=too-few-public-methods
    """Protocol for a class that implements a `.read()` method."""

    def read(self) -> str:
        """Read the contents of the file-like object."""
        return ""  # pragma: no cover

Ancestors

  • typing.Protocol
  • typing.Generic

Methods

def read(self) ‑> str

Read the contents of the file-like object.