Coverage for / home / runner / work / bijux-cli / bijux-cli / src / bijux_cli / core / context.py: 97%
56 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-01-26 17:59 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-01-26 17:59 +0000
1# SPDX-License-Identifier: Apache-2.0
2# Copyright © 2025 Bijan Mousavi
4"""Provides a concrete implementation for request-scoped context management.
6This module defines the `Context` class, which implements the `ExecutionContext`.
7It uses Python's `contextvars` to provide a thread-safe and async-safe
8mechanism for storing and retrieving key-value data associated with a specific
9command execution or request. This allows state to be passed through the
10application's call stack without explicit argument passing.
11"""
13from __future__ import annotations
15from collections.abc import Iterator
16from contextlib import contextmanager
17from contextvars import ContextVar, Token
18from typing import Any, TypeVar
20from injector import inject
22from bijux_cli.core.contracts import ExecutionContext
23from bijux_cli.core.di import DIContainer
25T = TypeVar("T")
26_current_context: ContextVar[dict[str, Any] | None] = ContextVar(
27 "current_context", default=None
28)
31class Context(ExecutionContext):
32 """Provides thread-safe, request-scoped storage for CLI commands.
34 This class uses `contextvars` to manage a dictionary of data that is
35 isolated to the current task or thread. It is intended to be used as
36 both a synchronous and asynchronous context manager.
38 Attributes:
39 _di (DIContainer): The dependency injection container.
40 _data (dict[str, Any]): The dictionary storing the context's data.
41 _token (Token | None): The token for resetting the `ContextVar`.
42 """
44 @inject
45 def __init__(self, di: DIContainer) -> None:
46 """Initializes a new Context instance.
48 Args:
49 di (DIContainer): The dependency injection container used to
50 resolve the logging service.
51 """
52 self._di = di
53 self._data: dict[str, Any] = {}
54 self._token: Token[dict[str, Any] | None] | None = None
56 def set(self, key: str, value: Any) -> None:
57 """Sets a value in the current context's data.
59 Args:
60 key (str): The key for the value.
61 value (Any): The value to store.
63 Returns:
64 None:
65 """
66 self._data[key] = value
68 def get(self, key: str) -> Any:
69 """Gets a value from the current context's data.
71 Args:
72 key (str): The key of the value to retrieve.
74 Returns:
75 Any: The value associated with the key.
77 Raises:
78 KeyError: If the key is not found in the context.
79 """
80 if key not in self._data:
81 raise KeyError(f"Key '{key}' not found in context")
82 return self._data[key]
84 def clear(self) -> None:
85 """Removes all values from the context's data."""
86 self._data.clear()
88 def __enter__(self) -> Context:
89 """Enters the context as a synchronous manager.
91 This sets the current `contextvar` to this instance's data dictionary.
93 Returns:
94 Context: The context instance itself.
95 """
96 self._token = _current_context.set(self._data)
97 return self
99 def __exit__(self, _exc_type: Any, _exc_value: Any, traceback: Any) -> None:
100 """Exits the synchronous context manager.
102 This resets the `contextvar` to its previous state.
104 Args:
105 _exc_type (Any): Exception type, if any (unused).
106 _exc_value (Any): Exception value, if any (unused).
107 traceback (Any): Traceback, if any (unused).
109 Returns:
110 None:
111 """
112 if self._token: 112 ↛ exitline 112 didn't return from function '__exit__' because the condition on line 112 was always true
113 _current_context.reset(self._token)
114 self._token = None
116 async def __aenter__(self) -> Context:
117 """Enters the context as an asynchronous manager.
119 This sets the current `contextvar` to this instance's data dictionary.
121 Returns:
122 Context: The context instance itself.
123 """
124 self._token = _current_context.set(self._data)
125 return self
127 async def __aexit__(self, _exc_type: Any, _exc_value: Any, traceback: Any) -> None:
128 """Exits the asynchronous context manager.
130 This resets the `contextvar` to its previous state.
132 Args:
133 _exc_type (Any): Exception type, if any (unused).
134 _exc_value (Any): Exception value, if any (unused).
135 traceback (Any): Traceback, if any (unused).
137 Returns:
138 None:
139 """
140 if self._token: 140 ↛ exitline 140 didn't return from function '__aexit__' because the condition on line 140 was always true
141 _current_context.reset(self._token)
142 self._token = None
144 @classmethod
145 def current_data(cls) -> dict[str, Any]:
146 """Returns the dictionary for the currently active CLI context.
148 This provides direct access to the data stored in the underlying
149 `contextvar` for the current execution scope.
151 Returns:
152 dict[str, Any]: The active context data dictionary.
153 """
154 data = _current_context.get()
155 if data is None:
156 data = {}
157 _current_context.set(data)
158 return data
160 @classmethod
161 def set_current_data(cls, data: dict[str, Any]) -> None:
162 """Sets the dictionary for the currently active CLI context.
164 Args:
165 data (dict[str, Any]): The data to use for the active context.
167 Returns:
168 None:
169 """
170 _current_context.set(data)
172 @classmethod
173 @contextmanager
174 def use_context(cls, data: dict[str, Any]) -> Iterator[None]:
175 """Temporarily replaces the current context data within a `with` block.
177 Args:
178 data (dict[str, Any]): The dictionary to use as the context for
179 the duration of the `with` block.
181 Yields:
182 None:
183 """
184 token = _current_context.set(data)
185 try:
186 yield
187 finally:
188 _current_context.reset(token)
191__all__ = ["Context"]