Skip to main content

Utilities to handle OS environment variables

Project description

os-env-injection

Utilities to handle OS environment variables

Why this library?

You have a function which requires several arguments that typically depend on the system on when the function is running

def f(url: str, user: str, password: str, subdomain: str):
    ...

You would like to have the possibility to have its values to be read from the OS env, so that, at will, you could invoke it as

f()

If you simply write

import os

def f(
    url: str = os.environ["URL"],
    user: str = os.environ["USER"],
    password: str = os.environ["PASSWORD"],
    subdomain: str = os.environ["SUBDOMAIN"],
):
    ...

then you will encounter a problem whenever the OS env variables are not set, because the defaults are evaluated at import time. This means in practice, that you will be forced to use them, instead of passing the values directly.

A workaround is

import os

def f(
    url: str = os.environ.get("URL", None),
    user: str = os.environ.get("USER", None),
    password: str = os.environ.get("PASSWORD", None),
    subdomain: str = os.environ.get("SUBDOMAIN", None),
):
    ...

In this way, there are still a couple of drawbacks:

  • it is necessary to write code to check the values of each variable to raise an exception if values are missing (None)
  • the default values will be evaluated when the function is first imported. In case you are setting the OS env dynamically (e.g. by executing a shell script with exports) you could end up in troubles.

The nice solution - Quickstart

First, install the library

pip install os-env-injection

From the previous example, say that all variables are required except for subdomain which can stay None. You can leverage on this library in two ways, depending on your favorite style.

Imperative style

from os_env_injection import inject_var


def f(
	url: str = "",
	user: str = "",
	password: str = "",
	subdomain: str | None = None,
) -> None:
	url = inject_var(var_value=url, os_env_key="OS_ENV_URL")
	user = inject_var(var_value=user, os_env_key="OS_ENV_USER")
	password = inject_var(var_value=password, os_env_key="OS_ENV_PASSWORD")
	subdomain = inject_var(
		var_value=subdomain, os_env_key="OS_ENV_SUBDOMAIN", is_required=False
	)
	...

Note: inject_var(var_value=url, os_env_key="OS_ENV_URL") is the same as inject_var(var_value=url, os_env_key="OS_ENV_URL", is_required=True).

Functional style

from os_env_injection import inject_os_env, Injection


@inject_os_env(
	injections=[
		Injection(var_name="url", os_env_key="OS_ENV_URL"),
		Injection(var_name="user", os_env_key="OS_ENV_USER"),
		Injection(var_name="password", os_env_key="OS_ENV_PASSWORD"),
		Injection(
			var_name="subdomain",
			os_env_key="OS_ENV_SUBDOMAIN",
			is_required=False,
		),
	]
)
def f(
	url: str = "",
	user: str = "",
	password: str = "",
	subdomain: str | None = None,
) -> None:
    ...

Note: Injection(var_name="url") is the same as Injection(var_name="url", os_env_key="url", is_required=True).

What will happen?

  • If you explicitly pass a valid value when you call f, it will be used.
  • If no value is passed (or the value matches the invalid sentinel, which defaults to "" or None), it will try to read it from the OS environment variable specified in os_env_key.
  • If the value is still unset (or the environment variable itself is missing or set to the invalid sentinel), it will raise an exception if is_required is True.
  • If is_required is False, it will not raise an exception and simply keep the invalid sentinel (e.g., None).

Why set a default value at all?

When you decorate a function with @inject_os_env and expect the OS environment to supply the values, you usually want to call the function without checking or passing those arguments yourself (e.g., just f()).

If your function signature doesn't provide default values (i.e., def f(url: str):), calling f() will cause Python to raise a TypeError at runtime for missing arguments, and type checkers (mypy, pyright, etc.) will also complain. Assigning a default value (like url: str = "") satisfies both Python and the type checker, allowing you to invoke f() cleanly while trusting the decorator to inject the real values.

Understanding Sentinel Values (value_to_consider_invalid)

By default, both None and the empty string "" are treated as "missing" values that trigger the fallback to the environment variable.

Why is this useful? Eliminating None-checks

When working with type checkers like mypy or pyright, using None as a default value forces you to make the type optional, which can clutter downstream code:

# Before: using `None` as the default value
@inject_os_env([Injection("url")])
def f(url: str | None = None):
    # Type checkers will complain here because `url` might be None!
    # Even though `inject_os_env` guarantees `url` will be populated from the OS var,
    # the type signature `str | None` says otherwise.
    processed_url = url.strip()  # Error: Item "None" of "str | None" has no attribute "strip"

    # You are forced to add redundant checks just to satisfy the type checker:
    assert url is not None
    processed_url = url.strip()

By using "" as the invalid sentinel, you can keep strict typing without the boilerplate:

# After: using `""` as the default invalid sentinel
@inject_os_env([Injection("url")])
def f(url: str = ""):
    # No type checker complaints!
    # `url` is guaranteed to be a string.
    # If the env var is missing, the decorator raises an error before we even reach this point.
    processed_url = url.strip()

Custom Sentinels

If your environment variable might legitimately be an empty string, or you want to use a different sentinel, you can explicitly set value_to_consider_invalid:

url = inject_var(
    var_value=url,
    os_env_key="OS_ENV_URL",
    value_to_consider_invalid="__MISSING__"
)
# or functionally:
Injection(
    var_name="url",
    value_to_consider_invalid="__MISSING__"
)

Setup development environment (for contributors only)

  • Create a virtual environment and activate it

    python -m venv venv
    source venv/bin/activate
    
  • Install the developer dependencies you will need

    pip install -U pip wheel setuptools
    pip install -e .[dev]
    
  • Set black as pre-commit package (will automatically apply black before committing)

    pre-commit install
    
  • To run the tests

    pytest
    

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

os_env_injection-2.0.1.tar.gz (12.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

os_env_injection-2.0.1-py3-none-any.whl (5.9 kB view details)

Uploaded Python 3

File details

Details for the file os_env_injection-2.0.1.tar.gz.

File metadata

  • Download URL: os_env_injection-2.0.1.tar.gz
  • Upload date:
  • Size: 12.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.20

File hashes

Hashes for os_env_injection-2.0.1.tar.gz
Algorithm Hash digest
SHA256 549924e53ca8b3ea018994c83e82f2e4494444a72f160deecc290b69056c82a1
MD5 158245cf238b00f3ad8bbfe66ecaa96c
BLAKE2b-256 02bdbed69c700ab327fcfaa0a96639cc2251725c84d77dcbbe5226546494d8bd

See more details on using hashes here.

File details

Details for the file os_env_injection-2.0.1-py3-none-any.whl.

File metadata

File hashes

Hashes for os_env_injection-2.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e3d21f0edc5bf29f1318c2d2abea022c4a5c49af3c0996851ca2f18b3504ea48
MD5 5ac4ac89cc0550a5b8df7feb37af7546
BLAKE2b-256 f73c637054deed19166e923fab1b02b3769fb48e512cf85533da22167bfda4de

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page