"""
Contexts
========
This module provides context management classes for maintaining state during
design processing and instantiation. Contexts can be set to provide a value for
a particular subtree of the design. If a context object is created in the class
body, it will be set first, before any of the other fields are initialized,
which means that subcircuits or components will see the new context value, even
if they are declared before the context is set.
If a context is needed as part of the function, it does not need to be assigned
to a member field, and indeed doing so does not set the context value, instead
it is activated using a ``with`` block. Note that this means that setting the
context object on ``self`` as part of the ``__init__`` method will have no
effect, if you need to construct a context inside the initializer, it must be
done using a ``with`` block.
Some context objects are predefined and set by the infrastructure, such as the
current design, but user code can create their own contexts as needed, which
can be particularly useful when building reusable librareis where there are
optional settings that should be apply to entire designs (or a significant
subset). For an example of this, see the jitx standard library's use of
contexts to control things like preferred silkscreen line widths in the
landpattern generators.
.. note::
Context objects should be immutable and hashable, because if it's used in a
memoized object, the context object will be part of the memoization key,
which means a new object will be generated if the identity of the context
object changes, but should it be mutated instead, the memoization may not
recognize that the value has changed.
Below is an example of both mechanisms for registering a context value. Note
that when using a ``with`` block, the context is only active within that block
(and within any calls made inside that block, such as creating components)
which can be useful if only a part of your circuit needs a different context
value.
>>> @dataclass(frozen=True)
... class MyCustomContext:
... some_relevant_value: int
>>> class MyCircuit(Circuit):
... circuit_context = MyCustomContext(42)
...
... def __init__(self):
... assert MyCustomContext.require().some_relevant_value == 42
... with MyCustomContext(7):
... self.needs_context_7()
...
... def needs_context_7(self):
... assert MyCustomContext.require().some_relevant_value == 7
"""
from collections.abc import Callable
from typing import cast
from jitx._structural import Instantiable
from jitx.error import UserCodeException
from ._instantiation import instantiation
[docs]
class Context:
"""Base class for context objects that maintain state during processing.
Context objects can be used as context managers to automatically push and
pop context state, ensuring proper cleanup.
"""
[docs]
def set(self):
"""Set this context as the current active context."""
instantiation.set(self.__class__, self)
[docs]
@classmethod
def get[T: Context](cls: type[T]) -> T | None:
"""Get the current active context of this type.
Returns:
The current context instance, or None if not set.
"""
from ._structural import instantiation
return cast(T | None, instantiation.get(cls))
[docs]
@classmethod
def require[T: Context](cls: type[T]) -> T:
"""Get the current active context of this type, raising an error if not set.
This method can be used in a declarative / class context, and will be
populated at instantiation.
Returns:
The current context instance.
Raises:
ValueError: If no context of this type is currently active.
"""
if not instantiation.active():
inst = Instantiable(cls.require, (), {})
inst.__name__ = f"{cls.__qualname__}.require()"
# trick static type checker.
return cast(T, inst)
val = cls.get()
if val is None:
raise ContextMissingException(cls.__qualname__)
return cast(T, val)
def __enter__(self):
"""Enter the context manager, pushing a new context frame."""
instantiation.push()
self.set()
def __exit__(self, typ, value, tb):
"""Exit the context manager, popping the context frame."""
instantiation.pop()
[docs]
class ContextMissingException(UserCodeException):
"""Raised if :py:meth:`Context.require` is called when the context has not been activated."""
def __init__(self, name):
super().__init__(
f"{name} is not active",
hint=f"This code requires a {name} context to be activated somewhere in the design hierarchy",
)
# Helper class to build a directory of context accessors, e.g.
# class DesignContext:
# def __init__(self):
# self.substrate = ContextProperty(SubstrateContext)
# design = DesignContext()
# ...
# now accessing design.substrate will give the current substrate.
[docs]
class ContextProperty[T: Context, S]:
"""Property descriptor for accessing context objects.
Creates a property that automatically retrieves the current context
and optionally applies a field accessor function.
>>> class DesignContext:
... def __init__(self):
... self.substrate = ContextProperty(SubstrateContext)
>>> design = DesignContext()
>>> # Accessing design.substrate will get the current substrate context
"""
def __init__(self, context: type[T], field: Callable[[T], S] = lambda x: x):
"""Initialize a context property.
Args:
context: The context class to retrieve.
field: Optional function to extract a field from the context.
"""
self.context = context
self.field = field
def __get__(self, instance, owner):
"""Get the context value, applying the field accessor if provided."""
return self.field(self.context.require())
def __set__(self, instance, value):
"""Implemented to allow += to work, will only accept the same object as
already present in the context."""
if value is not self.field(self.context.require()):
raise AttributeError("Context properties cannot be replaced")
[docs]
def magic_context[T: Context, S](
context: Callable[[], type[T]], field: Callable[[T], S]
):
return context, field