mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-06-06 13:30:41 +02:00
200 lines
6.9 KiB
Python
200 lines
6.9 KiB
Python
import inspect
|
|
import logging
|
|
from collections.abc import Callable
|
|
from itertools import chain
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def get_attribute_doc(attr: Any) -> str | None:
|
|
"""This function takes an input attribute attr and returns its documentation
|
|
string if it's different from the documentation of its type, otherwise,
|
|
it returns None.
|
|
"""
|
|
attr_doc = inspect.getdoc(attr)
|
|
attr_class_doc = inspect.getdoc(type(attr))
|
|
return attr_doc if attr_class_doc != attr_doc else None
|
|
|
|
|
|
def get_class_and_instance_attributes(obj: object) -> dict[str, Any]:
|
|
"""Dictionary containing all attributes (both instance and class level) of a
|
|
given object.
|
|
|
|
If an attribute exists at both the instance and class level,the value from the
|
|
instance attribute takes precedence.
|
|
The __root__ object is removed as this will lead to endless recursion in the for
|
|
loops.
|
|
"""
|
|
|
|
return dict(chain(type(obj).__dict__.items(), obj.__dict__.items()))
|
|
|
|
|
|
def get_object_attr_from_path(target_obj: Any, path: str) -> Any:
|
|
"""
|
|
Traverse the object tree according to the given path.
|
|
|
|
Args:
|
|
target_obj: The root object to start the traversal from.
|
|
path: Access path of the object.
|
|
|
|
Returns:
|
|
The attribute at the end of the path. If the path includes a list index,
|
|
the function returns the specific item at that index. If an attribute in
|
|
the path does not exist, the function logs a debug message and returns None.
|
|
|
|
Raises:
|
|
ValueError: If a list index in the path is not a valid integer.
|
|
"""
|
|
path_list = path.split(".") if path != "" else []
|
|
for part in path_list:
|
|
attr, key = parse_keyed_attribute(part)
|
|
try:
|
|
if key is not None:
|
|
target_obj = getattr(target_obj, attr)[key]
|
|
else:
|
|
target_obj = getattr(target_obj, attr)
|
|
except AttributeError:
|
|
# The attribute doesn't exist
|
|
logger.debug("Attribute % does not exist in the object.", part)
|
|
return None
|
|
return target_obj
|
|
|
|
|
|
def update_value_if_changed(
|
|
target: Any, attr_name_or_index: str | int, new_value: Any
|
|
) -> None:
|
|
"""
|
|
Updates the value of an attribute or a list element on a target object if the new
|
|
value differs from the current one.
|
|
|
|
This function supports updating both attributes of an object and elements of a list.
|
|
|
|
- For objects, the function first checks the current value of the attribute. If the
|
|
current value differs from the new value, the function updates the attribute.
|
|
|
|
- For lists, the function checks the current value at the specified index. If the
|
|
current value differs from the new value, the function updates the list element
|
|
at the given index.
|
|
|
|
Args:
|
|
target (Any):
|
|
The target object that has the attribute or the list.
|
|
attr_name_or_index (str | int):
|
|
The name of the attribute or the index of the list element.
|
|
new_value (Any):
|
|
The new value for the attribute or the list element.
|
|
"""
|
|
|
|
if isinstance(target, list) and isinstance(attr_name_or_index, int):
|
|
if target[attr_name_or_index] != new_value:
|
|
target[attr_name_or_index] = new_value
|
|
elif isinstance(attr_name_or_index, str):
|
|
# If the type matches and the current value is different from the new value,
|
|
# update the attribute.
|
|
if getattr(target, attr_name_or_index) != new_value:
|
|
setattr(target, attr_name_or_index, new_value)
|
|
else:
|
|
logger.error("Incompatible arguments: %s, %s.", target, attr_name_or_index)
|
|
|
|
|
|
def parse_keyed_attribute(attr_string: str) -> tuple[str, str | float | int | None]:
|
|
"""
|
|
Parses an attribute string and extracts a potential attribute name and its key.
|
|
The key can be a string (for dictionary keys) or an integer (for list indices).
|
|
|
|
Args:
|
|
attr_string (str):
|
|
The attribute string to parse. Can be a regular attribute name (e.g.,
|
|
'attr_name'), a list attribute with an index (e.g., 'list_attr[2]'), or
|
|
a dictionary attribute with a key (e.g., 'dict_attr["key"]' or
|
|
'dict_attr[0]').
|
|
|
|
Returns:
|
|
tuple[str, str | float | int | None]:
|
|
A tuple containing the attribute name and the key as either a string,
|
|
an integer if it's a digit, or None if no key is present.
|
|
|
|
Examples:
|
|
```python
|
|
>>> parse_keyed_attribute('list_attr[2]')
|
|
("list_attr", 2)
|
|
>>> parse_keyed_attribute('attr_name')
|
|
("attr_name", None)
|
|
>>> parse_keyed_attribute('dict_attr["key"]')
|
|
("dict_attr", "key")
|
|
>>> parse_keyed_attribute("dict_attr['key']")
|
|
("dict_attr", "key")
|
|
>>> parse_keyed_attribute("dict_attr["0"]")
|
|
("dict_attr", "0")
|
|
>>> parse_keyed_attribute("dict_attr[0]")
|
|
("dict_attr", 0)
|
|
```
|
|
"""
|
|
|
|
key = None
|
|
attr_name = attr_string
|
|
if "[" in attr_string and attr_string.endswith("]"):
|
|
attr_name, key_part = attr_string.split("[", 1)
|
|
key_part = key_part.rstrip("]")
|
|
# Remove quotes if present (supports both single and double quotes)
|
|
if key_part.startswith(('"', "'")) and key_part.endswith(('"', "'")):
|
|
key = key_part[1:-1]
|
|
elif "." in key_part:
|
|
key = float(key_part)
|
|
else:
|
|
key = int(key_part)
|
|
return attr_name, key
|
|
|
|
|
|
def get_component_classes() -> list[type]:
|
|
"""
|
|
Returns references to the component classes in a list.
|
|
"""
|
|
import pydase.components
|
|
|
|
return [
|
|
getattr(pydase.components, cls_name) for cls_name in pydase.components.__all__
|
|
]
|
|
|
|
|
|
def get_data_service_class_reference() -> Any:
|
|
import pydase.data_service.data_service
|
|
|
|
return getattr(pydase.data_service.data_service, "DataService")
|
|
|
|
|
|
def is_property_attribute(target_obj: Any, access_path: str) -> bool:
|
|
parent_path, attr_name = (
|
|
".".join(access_path.split(".")[:-1]),
|
|
access_path.split(".")[-1],
|
|
)
|
|
target_obj = get_object_attr_from_path(target_obj, parent_path)
|
|
return isinstance(getattr(type(target_obj), attr_name, None), property)
|
|
|
|
|
|
def function_has_arguments(func: Callable[..., Any]) -> bool:
|
|
sig = inspect.signature(func)
|
|
parameters = dict(sig.parameters)
|
|
# Remove 'self' parameter for instance methods.
|
|
parameters.pop("self", None)
|
|
|
|
# Check if there are any parameters left which would indicate additional arguments.
|
|
if len(parameters) > 0:
|
|
return True
|
|
return False
|
|
|
|
|
|
def render_in_frontend(func: Callable[..., Any]) -> bool:
|
|
"""Determines if the method should be rendered in the frontend.
|
|
|
|
It checks if the "@frontend" decorator was used or the method is a coroutine."""
|
|
|
|
if inspect.iscoroutinefunction(func):
|
|
return True
|
|
|
|
try:
|
|
return func._display_in_frontend # type: ignore
|
|
except AttributeError:
|
|
return False
|