Skip to content

Context module

The purpose of the Context module is to allow removing mutable global variables from Python code, and allow replacing it with a single explicit parameter: ctx: Ctx.

Install

Boldi's Context module is distributed as the boldi-ctx Python package, thus to install it, run:

pip install boldi-ctx

...or add "boldi-ctx" as a dependency to your project.

Import

Import the module like so:

import boldi.ctx
# or:
from boldi.ctx import Ctx

Usage

The Ctx class encapsulates mutable globals defined in the Python standard library, and often used in regular Python code. Code that whishes to get rid of mutable global variables can use an instance of the Ctx class as a function argument, and manipulate only that object instead of global variables.

A Ctx instance can replace these otherwise mutable global variables:

Using a Ctx doesn't change any global state (that's the point), thus code unaware of Ctx won't use the values defined and set in a Ctx instance. To make use of values inside a Ctx instance in 3rd party code or in Python standard library functions, the values must be passed explicitly. For example:

import subprocess

from boldi.ctx import Ctx

def printing_example(ctx):
    print(..., file=ctx.stderr)

def subprocess_example(ctx, args):
    # these two are equivalent:
    subprocess.run(
        args,
        check=True,
        text=True,
        stdin=ctx.stdin, stdout=ctx.stdout, stderr=ctx.stderr,
        cwd=ctx.cwd,
        env=ctx.env
    )
    ctx.run(args)

A default Ctx() will hold the original mutable global variables. This is safe to use if constructed in the main() function, or when no explicit ctx parameter has been provided to a function.

from boldi.ctx import Ctx

# for convenience, allows omitting the ctx parameter
def example(ctx: Ctx | None = None):
    ctx = ctx or Ctx()
    ...

Custom ctx values can be set inside unit tests or in any other context where changing the original global variables is not intended.

run() and run_py()

The Ctx.run() and Ctx.run_py() methods are provided for convenience to call the boldi.proc.run and boldi.proc.run_py functions with the keyword arguments using default values from the Ctx object.

API

boldi.ctx

Ctx

Bases: AbstractContextManager

Represents a context where the values in this object should apply.

When a Ctx object is passed as an argument to a function, or defined as a base class or a self attribute of an object, then the values in the Ctx object should be used explicitly instead of the original default values that they replace.

Examples:

If a ctx: Ctx or self.ctx: Ctx is present, always explicitly print(..., file=ctx.stdout) instead of a print(...) (that defaults to sys.stdout).

Source code in pkg/boldi-ctx/boldi/ctx.py
@dataclass
class Ctx(AbstractContextManager):
    """
    Represents a context where the values in this object should apply.

    When a `Ctx` object is passed as an argument to a function,
    or defined as a base class or a `self` attribute of an object,
    then the values in the `Ctx` object should be used explicitly
    instead of the original default values that they replace.

    Examples:
        If a `ctx: Ctx` or `self.ctx: Ctx` is present,
        always explicitly `print(..., file=ctx.stdout)`
        instead of a `print(...)` (that defaults to `sys.stdout`).
    """

    stack: ExitStack = field(default_factory=ExitStack)
    """An [`ExitStack`][contextlib.ExitStack] provided for convenience."""

    stdin: TextIO = field(default_factory=lambda: sys.stdin)
    """Replacement for [`sys.stdin`][]."""

    stdout: TextIO = field(default_factory=lambda: sys.stdout)
    """Replacement for [`sys.stdout`][]."""

    stderr: TextIO = field(default_factory=lambda: sys.stderr)
    """Replacement for [`sys.stderr`][]."""

    argv: list[str] = field(default_factory=lambda: sys.argv)
    """Replacement for [`sys.argv`][]."""

    env: MutableMapping[str, str] = field(default_factory=lambda: os.environ)
    """Replacement for [`os.environ`][]."""

    cwd: Path = field(default_factory=Path.cwd)
    """Replacement for [`pathlib.Path.cwd`][]."""

    def __enter__(self) -> Self:
        """Enter the context of `self.stack`."""
        self.stack.__enter__()
        return self

    def __exit__(self, *exc_info) -> bool | None:
        """Exit the context of `self.stack`."""
        return self.stack.__exit__(*exc_info)

    def chdir(self, path: Path):
        """Change the current working directory to `path` and restore later via `self.stack`."""
        self.stack.callback(setattr, self, "cwd", self.cwd)
        self.stack.enter_context(chdir(path))
        self.cwd = path

    def _set_run_kwargs(self, **kwargs: Unpack[RunArgs]):
        kwargs.setdefault("cwd", self.cwd)
        kwargs.setdefault("env", self.env)
        if not kwargs.get("capture_output"):
            kwargs.setdefault("stdin", self.stdin)
            kwargs.setdefault("stdout", self.stdout)
            kwargs.setdefault("stderr", self.stderr)

    def run(self, *args: Union[str, List[Any]], **kwargs: Unpack[RunArgs]) -> subprocess.CompletedProcess:
        """
        Run a subprocess using the provided command line arguments and updated defaults.

        Args:
            args: Command line arguments, provided as positional arguments.
                As defined in [`boldi.proc.args_iter`][].
            kwargs: Arguments to [`subprocess.run`][].
                Defaults to `check=True`, `text=True`, and values set in `self.{stdin,stdout,stderr,env,cwd}`,
                unless otherwise set by the caller.

        Returns:
            Completed process object.
        """
        self._set_run_kwargs(**kwargs)
        return _run(*args, **kwargs)

    def run_py(self, *args: Union[str, List[Any]], **kwargs: Unpack[RunArgs]) -> subprocess.CompletedProcess:
        """
        Run a subprocess using the current Python interpreter, the provided command line arguments and updated defaults.

        Args:
            args: Command line arguments, provided as positional arguments.
                As defined in [`boldi.proc.args_iter`][].
            kwargs: Arguments to [`subprocess.run`][]. As defined in [`run`][boldi.ctx.Ctx.run].

        Returns:
            Completed process object.
        """
        self._set_run_kwargs(**kwargs)
        return _run_py(*args, **kwargs)
argv: list[str] = field(default_factory=lambda: sys.argv)

Replacement for sys.argv.

cwd: Path = field(default_factory=Path.cwd)

Replacement for pathlib.Path.cwd.

env: MutableMapping[str, str] = field(default_factory=lambda: os.environ)

Replacement for os.environ.

stack: ExitStack = field(default_factory=ExitStack)

An ExitStack provided for convenience.

stderr: TextIO = field(default_factory=lambda: sys.stderr)

Replacement for sys.stderr.

stdin: TextIO = field(default_factory=lambda: sys.stdin)

Replacement for sys.stdin.

stdout: TextIO = field(default_factory=lambda: sys.stdout)

Replacement for sys.stdout.

__enter__() -> Self

Enter the context of self.stack.

Source code in pkg/boldi-ctx/boldi/ctx.py
def __enter__(self) -> Self:
    """Enter the context of `self.stack`."""
    self.stack.__enter__()
    return self
__exit__(*exc_info) -> bool | None

Exit the context of self.stack.

Source code in pkg/boldi-ctx/boldi/ctx.py
def __exit__(self, *exc_info) -> bool | None:
    """Exit the context of `self.stack`."""
    return self.stack.__exit__(*exc_info)
__init__(stack: ExitStack = ExitStack(), stdin: TextIO = lambda: sys.stdin(), stdout: TextIO = lambda: sys.stdout(), stderr: TextIO = lambda: sys.stderr(), argv: list[str] = lambda: sys.argv(), env: MutableMapping[str, str] = lambda: os.environ(), cwd: Path = Path.cwd()) -> None
chdir(path: Path)

Change the current working directory to path and restore later via self.stack.

Source code in pkg/boldi-ctx/boldi/ctx.py
def chdir(self, path: Path):
    """Change the current working directory to `path` and restore later via `self.stack`."""
    self.stack.callback(setattr, self, "cwd", self.cwd)
    self.stack.enter_context(chdir(path))
    self.cwd = path
run(*args: Union[str, List[Any]], **kwargs: Unpack[RunArgs]) -> subprocess.CompletedProcess

Run a subprocess using the provided command line arguments and updated defaults.

Parameters:

  • args (Union[str, List[Any]], default: () ) –

    Command line arguments, provided as positional arguments. As defined in boldi.proc.args_iter.

  • kwargs (Unpack[RunArgs], default: {} ) –

    Arguments to subprocess.run. Defaults to check=True, text=True, and values set in self.{stdin,stdout,stderr,env,cwd}, unless otherwise set by the caller.

Returns:

Source code in pkg/boldi-ctx/boldi/ctx.py
def run(self, *args: Union[str, List[Any]], **kwargs: Unpack[RunArgs]) -> subprocess.CompletedProcess:
    """
    Run a subprocess using the provided command line arguments and updated defaults.

    Args:
        args: Command line arguments, provided as positional arguments.
            As defined in [`boldi.proc.args_iter`][].
        kwargs: Arguments to [`subprocess.run`][].
            Defaults to `check=True`, `text=True`, and values set in `self.{stdin,stdout,stderr,env,cwd}`,
            unless otherwise set by the caller.

    Returns:
        Completed process object.
    """
    self._set_run_kwargs(**kwargs)
    return _run(*args, **kwargs)
run_py(*args: Union[str, List[Any]], **kwargs: Unpack[RunArgs]) -> subprocess.CompletedProcess

Run a subprocess using the current Python interpreter, the provided command line arguments and updated defaults.

Parameters:

Returns:

Source code in pkg/boldi-ctx/boldi/ctx.py
def run_py(self, *args: Union[str, List[Any]], **kwargs: Unpack[RunArgs]) -> subprocess.CompletedProcess:
    """
    Run a subprocess using the current Python interpreter, the provided command line arguments and updated defaults.

    Args:
        args: Command line arguments, provided as positional arguments.
            As defined in [`boldi.proc.args_iter`][].
        kwargs: Arguments to [`subprocess.run`][]. As defined in [`run`][boldi.ctx.Ctx.run].

    Returns:
        Completed process object.
    """
    self._set_run_kwargs(**kwargs)
    return _run_py(*args, **kwargs)