"""Helper functions for writing hardware drivers."""
import logging
import re
from collections.abc import Callable
from typing import Any
##############################################################
# extend logging mechanism
SPAM = 5
setattr(logging, "SPAM", 5) # noqa: B010 #TODO: Why is this line necessary at all?
logging.addLevelName(levelName="SPAM", level=5)
[docs]
class Logger(logging.Logger):
"""Extend logger to include a spam level for debugging device communication."""
[docs]
def setLevel(self, level: str | int, globally: bool = False) -> None: # noqa: N802 D102
if isinstance(level, str):
level = level.upper()
try:
level = int(level)
except ValueError:
pass
logging.Logger.setLevel(self, level)
if globally:
for logger in logging.root.manager.loggerDict.values():
if not hasattr(logger, "setLevel"):
continue
logger.setLevel(level)
[docs]
def spam(self, msg: str, *args, **kwargs) -> None:
"""Log a message with severity SPAM, even lower than DEBUG."""
self.log(SPAM, msg, *args, **kwargs)
logging.setLoggerClass(Logger)
format_str = "%(asctime)-15s %(name)s: %(message)s"
logging.basicConfig(format=format_str)
log = logging.getLogger("herosdevices")
SI_PREFIX_EXP = {
"Y": 24,
"Z": 21,
"E": 18,
"P": 15,
"T": 12,
"G": 9,
"M": 6,
"k": 3,
"h": 2,
"base": 0,
"d": -1,
"c": -2,
"m": -3,
"u": -6,
"n": -9,
"p": -12,
"f": -15,
"a": -18,
"z": -21,
"y": -24,
}
[docs]
def limits(lower: float, upper: float) -> Callable[[float], str | bool]:
"""Create a function which checks if a value is within the specified range.
Args:
lower: The lower bound of the valid range.
upper: The upper bound of the valid range.
Returns:
A function that takes a value and returns True if within the range, or a message
indicating it's out of range.
"""
def check(val: float) -> str | bool:
if val < lower or val > upper:
return f"Value {val} is out of range [{lower}, {upper}]"
return True
return check
[docs]
def limits_int(lower: int, upper: int) -> Callable[[int], str | bool]:
"""Create a function to check if a value is within a specified range and is an integer.
Args:
lower: The lower bound of the valid range.
upper: The upper bound of the valid range.
Returns:
A function that takes a value and returns True if within the range and is an integer,
or a message indicating why it's invalid.
"""
def check(val: int) -> str | bool:
if val < lower or val > upper:
return f"Value {val} is out of range [{lower}, {upper}]"
if val % 1 != 0:
return f"Value {val} is not an integer"
return True
return check
[docs]
def explicit(values: list[Any]) -> Callable[[Any], str | bool]:
"""Create a function to check if a value is in a list of allowed values.
Args:
values: A list of allowed values.
Returns:
A function that takes a value and returns True if within the list, or a message
indicating it's not in the list.
"""
def check(val: Any) -> bool | str:
if val not in values:
return f"Value {val} is not in list of allowed values {values}"
return True
return check
[docs]
def merge_dicts(dict1: dict, dict2: dict) -> dict:
"""Recursively merge two dicts of dicts."""
new_dict = dict1.copy()
for k, v in dict2.items():
if k in dict1 and isinstance(dict1[k], dict) and isinstance(v, dict):
new_dict[k] = merge_dicts(new_dict[k], v)
else:
new_dict[k] = v
return new_dict
[docs]
def add_class_descriptor(cls: type, attr_name: str, descriptor) -> None: # noqa: ANN001
"""
Add a descriptor to a class.
This is a simple helper function which uses `setattr` to add an attribute to the class and then also calls
`__set_name__` on the attribute.
Args:
cls: Class to add the descriptor to
attr_name: Name of the attribute the descriptor will be added to
descriptor: The descriptor to be added
"""
setattr(cls, attr_name, descriptor)
getattr(cls, attr_name).__set_name__(cls, attr_name)