Source code for zhinst.toolkit.nodetree.helper
"""Helper functions used in toolkit."""
import typing as t
from contextlib import contextmanager
from collections.abc import Mapping
import re
from _thread import RLock # type: ignore
from functools import wraps
# TypedDict is available in the typing module since 3.8
# Ift we only support 3.8 we should switch to t.TypedDict
from typing_extensions import TypedDict
if t.TYPE_CHECKING: # pragma: no cover
from zhinst.toolkit.nodetree.node import Node
T = t.TypeVar("T")
_NodeInfo = TypedDict(
"_NodeInfo",
{
"Node": str,
"Description": str,
"Properties": str,
"Type": str,
"Unit": str,
"Options": t.Dict[str, str],
},
)
NodeDoc = t.Dict[str, _NodeInfo]
_NOT_FOUND = object()
# Exact implementation of functools.cached_property from Python 3.8
# This is needed for Python 3.7 compatibility
# It should be removed once we drop support for Python 3.7
[docs]class lazy_property:
"""Copied functools.cached_property from Python 3.8.
Decorator that converts a method with a single self argument into a
property cached on the instance.
"""
def __init__(self, func):
self.func = func
self.attrname = None
self.__doc__ = func.__doc__
self.lock = RLock()
def __set_name__(self, owner, name):
if self.attrname is None:
self.attrname = name
elif name != self.attrname:
raise TypeError(
"Cannot assign the same cached_property to two different names "
f"({self.attrname!r} and {name!r})."
)
def __get__(self, instance, owner=None):
if instance is None:
return self
if self.attrname is None:
raise TypeError(
"Cannot use cached_property instance without "
"calling __set_name__ on it."
)
try:
cache = instance.__dict__
except AttributeError: # not all objects have __dict__
msg = (
f"No '__dict__' attribute on {type(instance).__name__!r} "
f"instance to cache {self.attrname!r} property."
)
raise TypeError(msg) from None
val = cache.get(self.attrname, _NOT_FOUND)
if val is _NOT_FOUND:
with self.lock:
# check if another thread filled cache while we awaited lock
val = cache.get(self.attrname, _NOT_FOUND)
if val is _NOT_FOUND:
val = self.func(instance)
try:
cache[self.attrname] = val
except TypeError:
msg = (
f"The '__dict__' attribute on {type(instance).__name__!r} "
"instance does not support item assignment for caching "
f"{self.attrname!r} property."
)
raise TypeError(msg) from None
return val
[docs]@contextmanager
def create_or_append_set_transaction(nodetree) -> t.Generator[None, None, None]:
"""Context manager for a transactional set.
In contrast to the set_transaction from the nodetree this function only
creates a new transaction if no other is in progress.
Should only be called withing the toolkit code.
Warning:
This function will silently fail if the existing transaction is exited
before this function finishes.
Warning:
The set is always performed as deep set if called on device nodes.
Examples:
>>> with nodetree.set_transaction():
nodetree.test[0].a(1)
with create_or_append_set_transaction(nodetree):
nodetree.test[1].a(2)
nodetree.test[2].a(2)
"""
if not nodetree.transaction.in_progress():
with nodetree.set_transaction():
yield
else:
yield
[docs]def resolve_wildcards_labone(path: str, nodes: t.List[str]) -> t.List[str]:
"""Resolves potential wildcards.
Also will resolve partial nodes to its leaf nodes.
Returns:
List of matched nodes in the raw path format
"""
node_raw = re.escape(path)
node_raw = node_raw.replace("/\\*/", "/[^/]*/").replace("/\\*", "/*") + "(/.*)?$"
node_raw_regex = re.compile(node_raw)
return list(filter(node_raw_regex.match, nodes))
[docs]class NodeDict(Mapping):
"""Mapping of dictionary structure results.
The mapping allows to access data with both the string and the toolkit
node objects.
Args:
result: A dictionary of node/value pairs.
Example:
>>> result = device.demods["*"].enable()
>>> print(result)
{
'/dev1234/demods/0/enable': 0,
'/dev1234/demods/1/enable': 1,
}
>>> result[device.demods[0].enable]
0
>>> result["/dev1234/demods/0/enable"]
0
.. versionadded:: 0.3.5 Renamed from WildcardResult
"""
def __init__(self, result: t.Dict[str, t.Any]):
self._result = result
def __repr__(self):
return repr(self._result)
def __getitem__(self, key: t.Union[str, "Node"]):
return self._result[str(key)]
def __iter__(self):
return iter(self._result)
def __len__(self):
return len(self._result)
[docs] def to_dict(self) -> t.Dict[str, t.Any]:
"""Convert the WildcardResult to a dictionary.
After conversion, :class:`Node` objects cannot be used to get items.
"""
return self._result
[docs]def not_callable_in_transactions(
func: t.Callable[["Node", t.Any], t.Any]
) -> t.Callable[["Node", t.Any], t.Any]:
"""Wrapper to prevent certain functions from being used within a transaction.
Certain utils functions which that both get and set values would not work like
expected in a transaction. This wrapper prevents misuse by throwing an error
in such cases.
Args:
func: function to wrap
Returns:
Similar function, but not callable from transactions
"""
@wraps(func)
def wrapper(node: "Node", *args, **kwargs):
if node.root.transaction.in_progress():
raise RuntimeError(
f"'{func.__name__}' cannot be called inside a transaction"
)
return func(node, *args, **kwargs)
return wrapper