Skip to main content

Utilities to handle OS environment variables

Reason this release was yanked:

mypy types correctly configured

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.0.tar.gz (12.1 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.0-py3-none-any.whl (5.9 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: os_env_injection-2.0.0.tar.gz
  • Upload date:
  • Size: 12.1 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.0.tar.gz
Algorithm Hash digest
SHA256 c7ae371c6981f3eae9d5d5e88eff9ce03be71fedb8d95b7de24f9b70adc4343a
MD5 08221f06eff086b8f73237112de0a822
BLAKE2b-256 a34414a44467415533d6639f9d00823a22725d19119eb7ee595fd3c4db33e491

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for os_env_injection-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c9f0b5fd3c9bb8926776a892ac3d9d09dc730365aea429635d12bc7ea4a69858
MD5 48cfbda35972bd40c4d1697c8b451b77
BLAKE2b-256 022cb0620213a45414415abb693ad93a4a57715b29a4825846b8667b0b6d74e6

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