mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-06-08 14:30:41 +02:00
Merge pull request #74 from tiqi-group/cleanup/ruff_linting
Cleanup: switching to ruff linter and formatter
This commit is contained in:
commit
abafd1a2b2
8
.flake8
8
.flake8
@ -1,8 +0,0 @@
|
|||||||
[flake8]
|
|
||||||
ignore = E501,W503,FS003,F403,F405,E203
|
|
||||||
include = src
|
|
||||||
max-line-length = 88
|
|
||||||
max-doc-length = 88
|
|
||||||
max-complexity = 7
|
|
||||||
max-expression-complexity = 7
|
|
||||||
use_class_attributes_order_strict_mode=True
|
|
5
.github/workflows/python-package.yml
vendored
5
.github/workflows/python-package.yml
vendored
@ -20,6 +20,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
- uses: chartboost/ruff-action@v1
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
@ -29,10 +30,6 @@ jobs:
|
|||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
python -m pip install poetry
|
python -m pip install poetry
|
||||||
poetry install
|
poetry install
|
||||||
- name: Lint with flake8
|
|
||||||
run: |
|
|
||||||
# stop the build if there are Python syntax errors or undefined names
|
|
||||||
poetry run flake8 src/pydase --count --show-source --statistics
|
|
||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
run: |
|
run: |
|
||||||
poetry run pytest
|
poetry run pytest
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -128,6 +128,9 @@ venv.bak/
|
|||||||
.dmypy.json
|
.dmypy.json
|
||||||
dmypy.json
|
dmypy.json
|
||||||
|
|
||||||
|
# ruff
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
|
988
poetry.lock
generated
988
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -25,17 +25,10 @@ pytest = "^7.4.0"
|
|||||||
pytest-cov = "^4.1.0"
|
pytest-cov = "^4.1.0"
|
||||||
mypy = "^1.4.1"
|
mypy = "^1.4.1"
|
||||||
black = "^23.1.0"
|
black = "^23.1.0"
|
||||||
isort = "^5.12.0"
|
|
||||||
flake8 = "^5.0.4"
|
|
||||||
flake8-use-fstring = "^1.4"
|
|
||||||
flake8-functions = "^0.0.7"
|
|
||||||
flake8-comprehensions = "^3.11.1"
|
|
||||||
flake8-pep585 = "^0.1.7"
|
|
||||||
flake8-pep604 = "^0.1.0"
|
|
||||||
flake8-eradicate = "^1.4.0"
|
|
||||||
matplotlib = "^3.7.2"
|
matplotlib = "^3.7.2"
|
||||||
pyright = "^1.1.323"
|
pyright = "^1.1.323"
|
||||||
pytest-mock = "^3.11.1"
|
pytest-mock = "^3.11.1"
|
||||||
|
ruff = "^0.1.5"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.docs.dependencies]
|
[tool.poetry.group.docs.dependencies]
|
||||||
@ -48,6 +41,37 @@ pymdown-extensions = "^10.1"
|
|||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py310" # Always generate Python 3.10-compatible code
|
||||||
|
line-length = 88
|
||||||
|
select = ["ALL"]
|
||||||
|
ignore = [
|
||||||
|
"ANN101", # typing self
|
||||||
|
"ANN401", # disallow Any typing
|
||||||
|
"B904", # Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
|
||||||
|
"COM812", # Missing trailing comma rule; defer to Black for formatting
|
||||||
|
"E203", # whitespace-before-punctuation
|
||||||
|
"UP007", # Use `X | Y` for type annotations
|
||||||
|
"S310", # suspicious-url-open-usage
|
||||||
|
"A", # flake8-builtins
|
||||||
|
"ARG", # flake8-unused-arguments
|
||||||
|
"BLE", # flake8-blind-except
|
||||||
|
"D", # pydocstyle
|
||||||
|
"EM", # flake8 error messages
|
||||||
|
"FBT", # Boolean trap detection
|
||||||
|
"PTH", # flake8-use-pathlib
|
||||||
|
"SLF", # flake8-self
|
||||||
|
"TD", # flake8-todos
|
||||||
|
"TRY", # Exception Handling AntiPatterns in Python
|
||||||
|
]
|
||||||
|
extend-exclude = [
|
||||||
|
"docs", "frontend", "tests"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint.mccabe]
|
||||||
|
max-complexity = 7
|
||||||
|
|
||||||
|
|
||||||
[tool.pyright]
|
[tool.pyright]
|
||||||
include = ["src/pydase"]
|
include = ["src/pydase"]
|
||||||
exclude = ["**/node_modules", "**/__pycache__", "docs", "frontend", "tests"]
|
exclude = ["**/node_modules", "**/__pycache__", "docs", "frontend", "tests"]
|
||||||
|
@ -57,5 +57,3 @@ class ColouredEnum(Enum):
|
|||||||
my_service.status = MyStatus.FAILED
|
my_service.status = MyStatus.FAILED
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
|
||||||
|
@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
import PIL.Image # type: ignore
|
import PIL.Image # type: ignore[import-untyped]
|
||||||
|
|
||||||
from pydase.data_service.data_service import DataService
|
from pydase.data_service.data_service import DataService
|
||||||
|
|
||||||
@ -33,17 +33,17 @@ class Image(DataService):
|
|||||||
|
|
||||||
def load_from_path(self, path: Path | str) -> None:
|
def load_from_path(self, path: Path | str) -> None:
|
||||||
with PIL.Image.open(path) as image:
|
with PIL.Image.open(path) as image:
|
||||||
self._load_from_PIL(image)
|
self._load_from_pil(image)
|
||||||
|
|
||||||
def load_from_matplotlib_figure(self, fig: "Figure", format_: str = "png") -> None:
|
def load_from_matplotlib_figure(self, fig: "Figure", format_: str = "png") -> None:
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
fig.savefig(buffer, format=format_) # type: ignore
|
fig.savefig(buffer, format=format_) # type: ignore[reportUnknownMemberType]
|
||||||
value_ = base64.b64encode(buffer.getvalue())
|
value_ = base64.b64encode(buffer.getvalue())
|
||||||
self._load_from_base64(value_, format_)
|
self._load_from_base64(value_, format_)
|
||||||
|
|
||||||
def load_from_url(self, url: str) -> None:
|
def load_from_url(self, url: str) -> None:
|
||||||
image = PIL.Image.open(urlopen(url))
|
image = PIL.Image.open(urlopen(url))
|
||||||
self._load_from_PIL(image)
|
self._load_from_pil(image)
|
||||||
|
|
||||||
def load_from_base64(self, value_: bytes, format_: Optional[str] = None) -> None:
|
def load_from_base64(self, value_: bytes, format_: Optional[str] = None) -> None:
|
||||||
if format_ is None:
|
if format_ is None:
|
||||||
@ -60,7 +60,7 @@ class Image(DataService):
|
|||||||
self._value = value
|
self._value = value
|
||||||
self._format = format_
|
self._format = format_
|
||||||
|
|
||||||
def _load_from_PIL(self, image: PIL.Image.Image) -> None:
|
def _load_from_pil(self, image: PIL.Image.Image) -> None:
|
||||||
if image.format is not None:
|
if image.format is not None:
|
||||||
format_ = image.format
|
format_ = image.format
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
|
@ -13,15 +13,15 @@ class NumberSlider(DataService):
|
|||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
-----------
|
-----------
|
||||||
value (float | int, optional):
|
value (float, optional):
|
||||||
The initial value of the slider. Defaults to 0.
|
The initial value of the slider. Defaults to 0.
|
||||||
min (float, optional):
|
min (float, optional):
|
||||||
The minimum value of the slider. Defaults to 0.
|
The minimum value of the slider. Defaults to 0.
|
||||||
max (float, optional):
|
max (float, optional):
|
||||||
The maximum value of the slider. Defaults to 100.
|
The maximum value of the slider. Defaults to 100.
|
||||||
step_size (float | int, optional):
|
step_size (float, optional):
|
||||||
The increment/decrement step size of the slider. Defaults to 1.0.
|
The increment/decrement step size of the slider. Defaults to 1.0.
|
||||||
type (Literal["int"] | Literal["float"], optional):
|
type (Literal["int", "float"], optional):
|
||||||
The type of the slider value. Determines if the value is an integer or float.
|
The type of the slider value. Determines if the value is an integer or float.
|
||||||
Defaults to "float".
|
Defaults to "float".
|
||||||
|
|
||||||
@ -38,23 +38,23 @@ class NumberSlider(DataService):
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__( # noqa: PLR0913
|
||||||
self,
|
self,
|
||||||
value: float | int = 0,
|
value: float = 0,
|
||||||
min: float = 0.0,
|
min_: float = 0.0,
|
||||||
max: float = 100.0,
|
max_: float = 100.0,
|
||||||
step_size: float | int = 1.0,
|
step_size: float = 1.0,
|
||||||
type: Literal["int"] | Literal["float"] = "float",
|
type_: Literal["int", "float"] = "float",
|
||||||
) -> None:
|
) -> None:
|
||||||
if type not in {"float", "int"}:
|
if type_ not in {"float", "int"}:
|
||||||
logger.error(f"Unknown type '{type}'. Using 'float'.")
|
logger.error("Unknown type '%s'. Using 'float'.", type_)
|
||||||
type = "float"
|
type_ = "float"
|
||||||
|
|
||||||
self._type = type
|
self._type = type_
|
||||||
self.step_size = step_size
|
self.step_size = step_size
|
||||||
self.value = value
|
self.value = value
|
||||||
self.min = min
|
self.min = min_
|
||||||
self.max = max
|
self.max = max_
|
||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ from typing import Literal
|
|||||||
from confz import BaseConfig, EnvSource
|
from confz import BaseConfig, EnvSource
|
||||||
|
|
||||||
|
|
||||||
class OperationMode(BaseConfig): # type: ignore
|
class OperationMode(BaseConfig): # type: ignore[misc]
|
||||||
environment: Literal["development"] | Literal["production"] = "development"
|
environment: Literal["development", "production"] = "development"
|
||||||
|
|
||||||
CONFIG_SOURCES = EnvSource(allow=["ENVIRONMENT"])
|
CONFIG_SOURCES = EnvSource(allow=["ENVIRONMENT"])
|
||||||
|
@ -2,8 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
from collections.abc import Callable
|
from typing import TYPE_CHECKING, Any, ClassVar
|
||||||
from typing import TYPE_CHECKING, Any
|
|
||||||
|
|
||||||
from pydase.data_service.abstract_data_service import AbstractDataService
|
from pydase.data_service.abstract_data_service import AbstractDataService
|
||||||
from pydase.utils.helpers import get_class_and_instance_attributes
|
from pydase.utils.helpers import get_class_and_instance_attributes
|
||||||
@ -11,13 +10,15 @@ from pydase.utils.helpers import get_class_and_instance_attributes
|
|||||||
from .data_service_list import DataServiceList
|
from .data_service_list import DataServiceList
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
from .data_service import DataService
|
from .data_service import DataService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CallbackManager:
|
class CallbackManager:
|
||||||
_notification_callbacks: list[Callable[[str, str, Any], Any]] = []
|
_notification_callbacks: ClassVar[list[Callable[[str, str, Any], Any]]] = []
|
||||||
"""
|
"""
|
||||||
A list of callback functions that are executed when a change occurs in the
|
A list of callback functions that are executed when a change occurs in the
|
||||||
DataService instance. These functions are intended to handle or respond to these
|
DataService instance. These functions are intended to handle or respond to these
|
||||||
@ -38,7 +39,7 @@ class CallbackManager:
|
|||||||
This implementation follows the observer pattern, with the DataService instance as
|
This implementation follows the observer pattern, with the DataService instance as
|
||||||
the "subject" and the callback functions as the "observers".
|
the "subject" and the callback functions as the "observers".
|
||||||
"""
|
"""
|
||||||
_list_mapping: dict[int, DataServiceList] = {}
|
_list_mapping: ClassVar[dict[int, DataServiceList]] = {}
|
||||||
"""
|
"""
|
||||||
A dictionary mapping the id of the original lists to the corresponding
|
A dictionary mapping the id of the original lists to the corresponding
|
||||||
DataServiceList instances.
|
DataServiceList instances.
|
||||||
@ -53,7 +54,7 @@ class CallbackManager:
|
|||||||
self.service = service
|
self.service = service
|
||||||
|
|
||||||
def _register_list_change_callbacks( # noqa: C901
|
def _register_list_change_callbacks( # noqa: C901
|
||||||
self, obj: "AbstractDataService", parent_path: str
|
self, obj: AbstractDataService, parent_path: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
This method ensures that notifications are emitted whenever a public list
|
This method ensures that notifications are emitted whenever a public list
|
||||||
@ -135,8 +136,8 @@ class CallbackManager:
|
|||||||
new_path = f"{parent_path}.{attr_name}[{i}]"
|
new_path = f"{parent_path}.{attr_name}[{i}]"
|
||||||
self._register_list_change_callbacks(item, new_path)
|
self._register_list_change_callbacks(item, new_path)
|
||||||
|
|
||||||
def _register_DataService_instance_callbacks(
|
def _register_data_service_instance_callbacks(
|
||||||
self, obj: "AbstractDataService", parent_path: str
|
self, obj: AbstractDataService, parent_path: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
This function is a key part of the observer pattern implemented by the
|
This function is a key part of the observer pattern implemented by the
|
||||||
@ -208,7 +209,7 @@ class CallbackManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _register_service_callbacks(
|
def _register_service_callbacks(
|
||||||
self, nested_attr: "AbstractDataService", parent_path: str, attr_name: str
|
self, nested_attr: AbstractDataService, parent_path: str, attr_name: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handles registration of callbacks for DataService attributes"""
|
"""Handles registration of callbacks for DataService attributes"""
|
||||||
|
|
||||||
@ -217,11 +218,11 @@ class CallbackManager:
|
|||||||
nested_attr.__dict__["__root__"] = self.service.__root__
|
nested_attr.__dict__["__root__"] = self.service.__root__
|
||||||
|
|
||||||
new_path = f"{parent_path}.{attr_name}"
|
new_path = f"{parent_path}.{attr_name}"
|
||||||
self._register_DataService_instance_callbacks(nested_attr, new_path)
|
self._register_data_service_instance_callbacks(nested_attr, new_path)
|
||||||
|
|
||||||
def __register_recursive_parameter_callback(
|
def __register_recursive_parameter_callback(
|
||||||
self,
|
self,
|
||||||
obj: "AbstractDataService | DataServiceList",
|
obj: AbstractDataService | DataServiceList,
|
||||||
callback: Callable[[str | int, Any], None],
|
callback: Callable[[str | int, Any], None],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
@ -248,14 +249,14 @@ class CallbackManager:
|
|||||||
item._callback_manager.callbacks.add(callback)
|
item._callback_manager.callbacks.add(callback)
|
||||||
for attr_name in set(dir(item)) - set(dir(object)) - {"__root__"}:
|
for attr_name in set(dir(item)) - set(dir(object)) - {"__root__"}:
|
||||||
attr_value = getattr(item, attr_name)
|
attr_value = getattr(item, attr_name)
|
||||||
if isinstance(attr_value, (AbstractDataService, DataServiceList)):
|
if isinstance(attr_value, AbstractDataService | DataServiceList):
|
||||||
self.__register_recursive_parameter_callback(
|
self.__register_recursive_parameter_callback(
|
||||||
attr_value, callback
|
attr_value, callback
|
||||||
)
|
)
|
||||||
|
|
||||||
def _register_property_callbacks( # noqa: C901
|
def _register_property_callbacks( # noqa: C901
|
||||||
self,
|
self,
|
||||||
obj: "AbstractDataService",
|
obj: AbstractDataService,
|
||||||
parent_path: str,
|
parent_path: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
@ -284,8 +285,8 @@ class CallbackManager:
|
|||||||
item, parent_path=f"{parent_path}.{attr_name}[{i}]"
|
item, parent_path=f"{parent_path}.{attr_name}[{i}]"
|
||||||
)
|
)
|
||||||
if isinstance(attr_value, property):
|
if isinstance(attr_value, property):
|
||||||
dependencies = attr_value.fget.__code__.co_names # type: ignore
|
dependencies = attr_value.fget.__code__.co_names # type: ignore[union-attr]
|
||||||
source_code_string = inspect.getsource(attr_value.fget) # type: ignore
|
source_code_string = inspect.getsource(attr_value.fget) # type: ignore[arg-type]
|
||||||
|
|
||||||
for dependency in dependencies:
|
for dependency in dependencies:
|
||||||
# check if the dependencies are attributes of obj
|
# check if the dependencies are attributes of obj
|
||||||
@ -304,11 +305,13 @@ class CallbackManager:
|
|||||||
dependency_value = getattr(obj, dependency)
|
dependency_value = getattr(obj, dependency)
|
||||||
|
|
||||||
if isinstance(
|
if isinstance(
|
||||||
dependency_value, (DataServiceList, AbstractDataService)
|
dependency_value, DataServiceList | AbstractDataService
|
||||||
):
|
):
|
||||||
|
|
||||||
def list_or_data_service_callback(
|
def list_or_data_service_callback(
|
||||||
name: Any, value: Any, dependent_attr: str = attr_name
|
name: Any,
|
||||||
|
value: Any,
|
||||||
|
dependent_attr: str = attr_name,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Emits a notification through the service's callback
|
"""Emits a notification through the service's callback
|
||||||
manager.
|
manager.
|
||||||
@ -345,8 +348,8 @@ class CallbackManager:
|
|||||||
# Add to callbacks
|
# Add to callbacks
|
||||||
obj._callback_manager.callbacks.add(callback)
|
obj._callback_manager.callbacks.add(callback)
|
||||||
|
|
||||||
def _register_start_stop_task_callbacks( # noqa
|
def _register_start_stop_task_callbacks( # noqa: C901
|
||||||
self, obj: "AbstractDataService", parent_path: str
|
self, obj: AbstractDataService, parent_path: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
This function registers callbacks for start and stop methods of async functions.
|
This function registers callbacks for start and stop methods of async functions.
|
||||||
@ -400,7 +403,7 @@ class CallbackManager:
|
|||||||
self._register_list_change_callbacks(
|
self._register_list_change_callbacks(
|
||||||
self.service, f"{self.service.__class__.__name__}"
|
self.service, f"{self.service.__class__.__name__}"
|
||||||
)
|
)
|
||||||
self._register_DataService_instance_callbacks(
|
self._register_data_service_instance_callbacks(
|
||||||
self.service, f"{self.service.__class__.__name__}"
|
self.service, f"{self.service.__class__.__name__}"
|
||||||
)
|
)
|
||||||
self._register_property_callbacks(
|
self._register_property_callbacks(
|
||||||
@ -411,12 +414,12 @@ class CallbackManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def emit_notification(self, parent_path: str, name: str, value: Any) -> None:
|
def emit_notification(self, parent_path: str, name: str, value: Any) -> None:
|
||||||
logger.debug(f"{parent_path}.{name} changed to {value}!")
|
logger.debug("%s.%s changed to %s!", parent_path, name, value)
|
||||||
|
|
||||||
for callback in self._notification_callbacks:
|
for callback in self._notification_callbacks:
|
||||||
try:
|
try:
|
||||||
callback(parent_path, name, value)
|
callback(parent_path, name, value)
|
||||||
except Exception as e:
|
except Exception as e: # noqa: PERF203
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
|
|
||||||
def add_notification_callback(
|
def add_notification_callback(
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
import warnings
|
import warnings
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from typing import TYPE_CHECKING, Any, Optional, get_type_hints
|
||||||
from typing import Any, Optional, get_type_hints
|
|
||||||
|
|
||||||
import rpyc # type: ignore
|
import rpyc # type: ignore[import-untyped]
|
||||||
|
|
||||||
import pydase.units as u
|
import pydase.units as u
|
||||||
from pydase.data_service.abstract_data_service import AbstractDataService
|
from pydase.data_service.abstract_data_service import AbstractDataService
|
||||||
@ -24,9 +23,12 @@ from pydase.utils.serializer import (
|
|||||||
get_nested_dict_by_path,
|
get_nested_dict_by_path,
|
||||||
)
|
)
|
||||||
from pydase.utils.warnings import (
|
from pydase.utils.warnings import (
|
||||||
warn_if_instance_class_does_not_inherit_from_DataService,
|
warn_if_instance_class_does_not_inherit_from_data_service,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -56,8 +58,8 @@ class DataService(rpyc.Service, AbstractDataService):
|
|||||||
filename = kwargs.pop("filename", None)
|
filename = kwargs.pop("filename", None)
|
||||||
if filename is not None:
|
if filename is not None:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"The 'filename' argument is deprecated and will be removed in a future version. "
|
"The 'filename' argument is deprecated and will be removed in a future "
|
||||||
"Please pass the 'filename' argument to `pydase.Server`.",
|
"version. Please pass the 'filename' argument to `pydase.Server`.",
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
stacklevel=2,
|
stacklevel=2,
|
||||||
)
|
)
|
||||||
@ -80,14 +82,15 @@ class DataService(rpyc.Service, AbstractDataService):
|
|||||||
|
|
||||||
super().__setattr__(__name, __value)
|
super().__setattr__(__name, __value)
|
||||||
|
|
||||||
if self.__dict__.get("_initialised") and not __name == "_initialised":
|
if self.__dict__.get("_initialised") and __name != "_initialised":
|
||||||
for callback in self._callback_manager.callbacks:
|
for callback in self._callback_manager.callbacks:
|
||||||
callback(__name, __value)
|
callback(__name, __value)
|
||||||
elif __name.startswith(f"_{self.__class__.__name__}__"):
|
elif __name.startswith(f"_{self.__class__.__name__}__"):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Warning: You should not set private but rather protected attributes! "
|
"Warning: You should not set private but rather protected attributes! "
|
||||||
f"Use {__name.replace(f'_{self.__class__.__name__}__', '_')} instead "
|
"Use %s instead of %s.",
|
||||||
f"of {__name.replace(f'_{self.__class__.__name__}__', '__')}."
|
__name.replace(f"_{self.__class__.__name__}__", "_"),
|
||||||
|
__name.replace(f"_{self.__class__.__name__}__", "__"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __check_instance_classes(self) -> None:
|
def __check_instance_classes(self) -> None:
|
||||||
@ -95,9 +98,9 @@ class DataService(rpyc.Service, AbstractDataService):
|
|||||||
# every class defined by the user should inherit from DataService if it is
|
# every class defined by the user should inherit from DataService if it is
|
||||||
# assigned to a public attribute
|
# assigned to a public attribute
|
||||||
if not attr_name.startswith("_"):
|
if not attr_name.startswith("_"):
|
||||||
warn_if_instance_class_does_not_inherit_from_DataService(attr_value)
|
warn_if_instance_class_does_not_inherit_from_data_service(attr_value)
|
||||||
|
|
||||||
def __set_attribute_based_on_type( # noqa:CFQ002
|
def __set_attribute_based_on_type( # noqa: PLR0913
|
||||||
self,
|
self,
|
||||||
target_obj: Any,
|
target_obj: Any,
|
||||||
attr_name: str,
|
attr_name: str,
|
||||||
@ -154,9 +157,11 @@ class DataService(rpyc.Service, AbstractDataService):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if hasattr(self, "_state_manager"):
|
if hasattr(self, "_state_manager"):
|
||||||
getattr(self, "_state_manager").save_state()
|
self._state_manager.save_state() # type: ignore[reportGeneralTypeIssue]
|
||||||
|
|
||||||
def load_DataService_from_JSON(self, json_dict: dict[str, Any]) -> None:
|
def load_DataService_from_JSON( # noqa: N802
|
||||||
|
self, json_dict: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"'load_DataService_from_JSON' is deprecated and will be removed in a "
|
"'load_DataService_from_JSON' is deprecated and will be removed in a "
|
||||||
"future version. "
|
"future version. "
|
||||||
@ -178,8 +183,9 @@ class DataService(rpyc.Service, AbstractDataService):
|
|||||||
class_attr_is_read_only = nested_class_dict["readonly"]
|
class_attr_is_read_only = nested_class_dict["readonly"]
|
||||||
if class_attr_is_read_only:
|
if class_attr_is_read_only:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'Attribute "{path}" is read-only. Ignoring value from JSON '
|
"Attribute '%s' is read-only. Ignoring value from JSON "
|
||||||
"file..."
|
"file...",
|
||||||
|
path,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
# Split the path into parts
|
# Split the path into parts
|
||||||
@ -193,11 +199,14 @@ class DataService(rpyc.Service, AbstractDataService):
|
|||||||
self.update_DataService_attribute(parts[:-1], attr_name, value)
|
self.update_DataService_attribute(parts[:-1], attr_name, value)
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
f'Attribute type of "{path}" changed from "{value_type}" to '
|
"Attribute type of '%s' changed from '%s' to "
|
||||||
f'"{class_value_type}". Ignoring value from JSON file...'
|
"'%s'. Ignoring value from JSON file...",
|
||||||
|
path,
|
||||||
|
value_type,
|
||||||
|
class_value_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
def serialize(self) -> dict[str, dict[str, Any]]: # noqa
|
def serialize(self) -> dict[str, dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Serializes the instance into a dictionary, preserving the structure of the
|
Serializes the instance into a dictionary, preserving the structure of the
|
||||||
instance.
|
instance.
|
||||||
@ -216,7 +225,7 @@ class DataService(rpyc.Service, AbstractDataService):
|
|||||||
"""
|
"""
|
||||||
return Serializer.serialize_object(self)["value"]
|
return Serializer.serialize_object(self)["value"]
|
||||||
|
|
||||||
def update_DataService_attribute(
|
def update_DataService_attribute( # noqa: N802
|
||||||
self,
|
self,
|
||||||
path_list: list[str],
|
path_list: list[str],
|
||||||
attr_name: str,
|
attr_name: str,
|
||||||
|
@ -3,7 +3,7 @@ from typing import Any
|
|||||||
|
|
||||||
import pydase.units as u
|
import pydase.units as u
|
||||||
from pydase.utils.warnings import (
|
from pydase.utils.warnings import (
|
||||||
warn_if_instance_class_does_not_inherit_from_DataService,
|
warn_if_instance_class_does_not_inherit_from_data_service,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -36,14 +36,14 @@ class DataServiceList(list):
|
|||||||
self._callbacks = callback_list
|
self._callbacks = callback_list
|
||||||
|
|
||||||
for item in args[0]:
|
for item in args[0]:
|
||||||
warn_if_instance_class_does_not_inherit_from_DataService(item)
|
warn_if_instance_class_does_not_inherit_from_data_service(item)
|
||||||
|
|
||||||
# prevent gc to delete the passed list by keeping a reference
|
# prevent gc to delete the passed list by keeping a reference
|
||||||
self._original_list = args[0]
|
self._original_list = args[0]
|
||||||
|
|
||||||
super().__init__(*args, **kwargs) # type: ignore
|
super().__init__(*args, **kwargs) # type: ignore[reportUnknownMemberType]
|
||||||
|
|
||||||
def __setitem__(self, key: int, value: Any) -> None: # type: ignore
|
def __setitem__(self, key: int, value: Any) -> None: # type: ignore[override]
|
||||||
current_value = self.__getitem__(key)
|
current_value = self.__getitem__(key)
|
||||||
|
|
||||||
# parse ints into floats if current value is a float
|
# parse ints into floats if current value is a float
|
||||||
@ -52,7 +52,7 @@ class DataServiceList(list):
|
|||||||
|
|
||||||
if isinstance(current_value, u.Quantity):
|
if isinstance(current_value, u.Quantity):
|
||||||
value = u.convert_to_quantity(value, str(current_value.u))
|
value = u.convert_to_quantity(value, str(current_value.u))
|
||||||
super().__setitem__(key, value) # type: ignore
|
super().__setitem__(key, value) # type: ignore[reportUnknownMemberType]
|
||||||
|
|
||||||
for callback in self._callbacks:
|
for callback in self._callbacks:
|
||||||
callback(key, value)
|
callback(key, value)
|
||||||
|
@ -41,7 +41,7 @@ def load_state(func: Callable[..., Any]) -> Callable[..., Any]:
|
|||||||
... self._name = value
|
... self._name = value
|
||||||
"""
|
"""
|
||||||
|
|
||||||
func._load_state = True # type: ignore
|
func._load_state = True # type: ignore[attr-defined]
|
||||||
return func
|
return func
|
||||||
|
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ def has_load_state_decorator(prop: property) -> bool:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return getattr(prop.fset, "_load_state")
|
return prop.fset._load_state # type: ignore[union-attr]
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -96,13 +96,15 @@ class StateManager:
|
|||||||
update.
|
update.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, service: "DataService", filename: Optional[str | Path] = None):
|
def __init__(
|
||||||
|
self, service: "DataService", filename: Optional[str | Path] = None
|
||||||
|
) -> None:
|
||||||
self.filename = getattr(service, "_filename", None)
|
self.filename = getattr(service, "_filename", None)
|
||||||
|
|
||||||
if filename is not None:
|
if filename is not None:
|
||||||
if self.filename is not None:
|
if self.filename is not None:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Overwriting filename {self.filename!r} with {filename!r}."
|
"Overwriting filename '%s' with '%s'.", self.filename, filename
|
||||||
)
|
)
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
|
|
||||||
@ -136,7 +138,7 @@ class StateManager:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Traverse the serialized representation and set the attributes of the class
|
# Traverse the serialized representation and set the attributes of the class
|
||||||
json_dict = self._get_state_dict_from_JSON_file()
|
json_dict = self._get_state_dict_from_json_file()
|
||||||
if json_dict == {}:
|
if json_dict == {}:
|
||||||
logger.debug("Could not load the service state.")
|
logger.debug("Could not load the service state.")
|
||||||
return
|
return
|
||||||
@ -155,15 +157,16 @@ class StateManager:
|
|||||||
self.set_service_attribute_value_by_path(path, value)
|
self.set_service_attribute_value_by_path(path, value)
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Attribute type of {path!r} changed from {value_type!r} to "
|
"Attribute type of '%s' changed from '%s' to "
|
||||||
f"{class_attr_value_type!r}. Ignoring value from JSON file..."
|
"'%s'. Ignoring value from JSON file...",
|
||||||
|
path,
|
||||||
|
value_type,
|
||||||
|
class_attr_value_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_state_dict_from_JSON_file(self) -> dict[str, Any]:
|
def _get_state_dict_from_json_file(self) -> dict[str, Any]:
|
||||||
if self.filename is not None:
|
if self.filename is not None and os.path.exists(self.filename):
|
||||||
# Check if the file specified by the filename exists
|
with open(self.filename) as f:
|
||||||
if os.path.exists(self.filename):
|
|
||||||
with open(self.filename, "r") as f:
|
|
||||||
# Load JSON data from file and update class attributes with these
|
# Load JSON data from file and update class attributes with these
|
||||||
# values
|
# values
|
||||||
return cast(dict[str, Any], json.load(f))
|
return cast(dict[str, Any], json.load(f))
|
||||||
@ -192,7 +195,7 @@ class StateManager:
|
|||||||
|
|
||||||
# This will also filter out methods as they are 'read-only'
|
# This will also filter out methods as they are 'read-only'
|
||||||
if current_value_dict["readonly"]:
|
if current_value_dict["readonly"]:
|
||||||
logger.debug(f"Attribute {path!r} is read-only. Ignoring new value...")
|
logger.debug("Attribute '%s' is read-only. Ignoring new value...", path)
|
||||||
return
|
return
|
||||||
|
|
||||||
converted_value = self.__convert_value_if_needed(value, current_value_dict)
|
converted_value = self.__convert_value_if_needed(value, current_value_dict)
|
||||||
@ -201,7 +204,7 @@ class StateManager:
|
|||||||
if self.__attr_value_has_changed(converted_value, current_value_dict["value"]):
|
if self.__attr_value_has_changed(converted_value, current_value_dict["value"]):
|
||||||
self.__update_attribute_by_path(path, converted_value)
|
self.__update_attribute_by_path(path, converted_value)
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Value of attribute {path!r} has not changed...")
|
logger.debug("Value of attribute '%s' has not changed...", path)
|
||||||
|
|
||||||
def __attr_value_has_changed(self, value_object: Any, current_value: Any) -> bool:
|
def __attr_value_has_changed(self, value_object: Any, current_value: Any) -> bool:
|
||||||
"""Check if the serialized value of `value_object` differs from `current_value`.
|
"""Check if the serialized value of `value_object` differs from `current_value`.
|
||||||
@ -262,8 +265,9 @@ class StateManager:
|
|||||||
has_decorator = has_load_state_decorator(prop)
|
has_decorator = has_load_state_decorator(prop)
|
||||||
if not has_decorator:
|
if not has_decorator:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Property {attr_name!r} has no '@load_state' decorator. "
|
"Property '%s' has no '@load_state' decorator. "
|
||||||
"Ignoring value from JSON file..."
|
"Ignoring value from JSON file...",
|
||||||
|
attr_name,
|
||||||
)
|
)
|
||||||
return has_decorator
|
return has_decorator
|
||||||
return True
|
return True
|
||||||
|
@ -3,7 +3,6 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
from collections.abc import Callable
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import TYPE_CHECKING, Any, TypedDict
|
from typing import TYPE_CHECKING, Any, TypedDict
|
||||||
|
|
||||||
@ -12,6 +11,8 @@ from pydase.data_service.data_service_list import DataServiceList
|
|||||||
from pydase.utils.helpers import get_class_and_instance_attributes
|
from pydase.utils.helpers import get_class_and_instance_attributes
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
from .data_service import DataService
|
from .data_service import DataService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -94,7 +95,7 @@ class TaskManager:
|
|||||||
|
|
||||||
self._set_start_and_stop_for_async_methods()
|
self._set_start_and_stop_for_async_methods()
|
||||||
|
|
||||||
def _set_start_and_stop_for_async_methods(self) -> None: # noqa: C901
|
def _set_start_and_stop_for_async_methods(self) -> None:
|
||||||
# inspect the methods of the class
|
# inspect the methods of the class
|
||||||
for name, method in inspect.getmembers(
|
for name, method in inspect.getmembers(
|
||||||
self.service, predicate=inspect.iscoroutinefunction
|
self.service, predicate=inspect.iscoroutinefunction
|
||||||
@ -111,18 +112,18 @@ class TaskManager:
|
|||||||
start_method(*args)
|
start_method(*args)
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"No start method found for service '{service_name}'"
|
"No start method found for service '%s'", service_name
|
||||||
)
|
)
|
||||||
|
|
||||||
def start_autostart_tasks(self) -> None:
|
def start_autostart_tasks(self) -> None:
|
||||||
self._initiate_task_startup()
|
self._initiate_task_startup()
|
||||||
attrs = get_class_and_instance_attributes(self.service)
|
attrs = get_class_and_instance_attributes(self.service)
|
||||||
|
|
||||||
for _, attr_value in attrs.items():
|
for attr_value in attrs.values():
|
||||||
if isinstance(attr_value, AbstractDataService):
|
if isinstance(attr_value, AbstractDataService):
|
||||||
attr_value._task_manager.start_autostart_tasks()
|
attr_value._task_manager.start_autostart_tasks()
|
||||||
elif isinstance(attr_value, DataServiceList):
|
elif isinstance(attr_value, DataServiceList):
|
||||||
for i, item in enumerate(attr_value):
|
for item in attr_value:
|
||||||
if isinstance(item, AbstractDataService):
|
if isinstance(item, AbstractDataService):
|
||||||
item._task_manager.start_autostart_tasks()
|
item._task_manager.start_autostart_tasks()
|
||||||
|
|
||||||
@ -145,7 +146,7 @@ class TaskManager:
|
|||||||
|
|
||||||
return stop_task
|
return stop_task
|
||||||
|
|
||||||
def _make_start_task( # noqa
|
def _make_start_task( # noqa: C901
|
||||||
self, name: str, method: Callable[..., Any]
|
self, name: str, method: Callable[..., Any]
|
||||||
) -> Callable[..., Any]:
|
) -> Callable[..., Any]:
|
||||||
"""
|
"""
|
||||||
@ -161,7 +162,7 @@ class TaskManager:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@wraps(method)
|
@wraps(method)
|
||||||
def start_task(*args: Any, **kwargs: Any) -> None:
|
def start_task(*args: Any, **kwargs: Any) -> None: # noqa: C901
|
||||||
def task_done_callback(task: asyncio.Task[None], name: str) -> None:
|
def task_done_callback(task: asyncio.Task[None], name: str) -> None:
|
||||||
"""Handles tasks that have finished.
|
"""Handles tasks that have finished.
|
||||||
|
|
||||||
@ -179,8 +180,10 @@ class TaskManager:
|
|||||||
if exception is not None:
|
if exception is not None:
|
||||||
# Handle the exception, or you can re-raise it.
|
# Handle the exception, or you can re-raise it.
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Task '{name}' encountered an exception: "
|
"Task '%s' encountered an exception: %s: %s",
|
||||||
f"{type(exception).__name__}: {exception}"
|
name,
|
||||||
|
type(exception).__name__,
|
||||||
|
exception,
|
||||||
)
|
)
|
||||||
raise exception
|
raise exception
|
||||||
|
|
||||||
@ -188,7 +191,7 @@ class TaskManager:
|
|||||||
try:
|
try:
|
||||||
await method(*args, **kwargs)
|
await method(*args, **kwargs)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
logger.info(f"Task {name} was cancelled")
|
logger.info("Task '%s' was cancelled", name)
|
||||||
|
|
||||||
if not self.tasks.get(name):
|
if not self.tasks.get(name):
|
||||||
# Get the signature of the coroutine method to start
|
# Get the signature of the coroutine method to start
|
||||||
@ -207,7 +210,7 @@ class TaskManager:
|
|||||||
# with the 'kwargs' dictionary. If a parameter is specified in both
|
# with the 'kwargs' dictionary. If a parameter is specified in both
|
||||||
# 'args_padded' and 'kwargs', the value from 'kwargs' is used.
|
# 'args_padded' and 'kwargs', the value from 'kwargs' is used.
|
||||||
kwargs_updated = {
|
kwargs_updated = {
|
||||||
**dict(zip(parameter_names, args_padded)),
|
**dict(zip(parameter_names, args_padded, strict=True)),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,6 +233,6 @@ class TaskManager:
|
|||||||
for callback in self.task_status_change_callbacks:
|
for callback in self.task_status_change_callbacks:
|
||||||
callback(name, kwargs_updated)
|
callback(name, kwargs_updated)
|
||||||
else:
|
else:
|
||||||
logger.error(f"Task `{name}` is already running!")
|
logger.error("Task '%s' is already running!", name)
|
||||||
|
|
||||||
return start_task
|
return start_task
|
||||||
|
@ -10,13 +10,12 @@ from types import FrameType
|
|||||||
from typing import Any, Optional, Protocol, TypedDict
|
from typing import Any, Optional, Protocol, TypedDict
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from rpyc import ForkingServer, ThreadedServer # type: ignore
|
from rpyc import ForkingServer, ThreadedServer # type: ignore[import-untyped]
|
||||||
from uvicorn.server import HANDLED_SIGNALS
|
from uvicorn.server import HANDLED_SIGNALS
|
||||||
|
|
||||||
from pydase import DataService
|
from pydase import DataService
|
||||||
from pydase.data_service.state_manager import StateManager
|
from pydase.data_service.state_manager import StateManager
|
||||||
from pydase.utils.serializer import dump, get_nested_dict_by_path
|
from pydase.utils.serializer import dump, get_nested_dict_by_path
|
||||||
from pydase.version import __version__
|
|
||||||
|
|
||||||
from .web_server import WebAPI
|
from .web_server import WebAPI
|
||||||
|
|
||||||
@ -110,8 +109,6 @@ class Server:
|
|||||||
Filename of the file managing the service state persistence. Defaults to None.
|
Filename of the file managing the service state persistence. Defaults to None.
|
||||||
use_forking_server: bool
|
use_forking_server: bool
|
||||||
Whether to use ForkingServer for multiprocessing. Default is False.
|
Whether to use ForkingServer for multiprocessing. Default is False.
|
||||||
web_settings: dict[str, Any]
|
|
||||||
Additional settings for the web server. Default is {} (an empty dictionary).
|
|
||||||
additional_servers : list[AdditionalServer]
|
additional_servers : list[AdditionalServer]
|
||||||
A list of additional servers to run alongside the main server. Each entry in the
|
A list of additional servers to run alongside the main server. Each entry in the
|
||||||
list should be a dictionary with the following structure:
|
list should be a dictionary with the following structure:
|
||||||
@ -164,27 +161,27 @@ class Server:
|
|||||||
Additional keyword arguments.
|
Additional keyword arguments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__( # noqa: CFQ002
|
def __init__( # noqa: PLR0913
|
||||||
self,
|
self,
|
||||||
service: DataService,
|
service: DataService,
|
||||||
host: str = "0.0.0.0",
|
host: str = "127.0.0.1",
|
||||||
rpc_port: int = 18871,
|
rpc_port: int = 18871,
|
||||||
web_port: int = 8001,
|
web_port: int = 8001,
|
||||||
enable_rpc: bool = True,
|
enable_rpc: bool = True,
|
||||||
enable_web: bool = True,
|
enable_web: bool = True,
|
||||||
filename: Optional[str | Path] = None,
|
filename: Optional[str | Path] = None,
|
||||||
use_forking_server: bool = False,
|
use_forking_server: bool = False,
|
||||||
web_settings: dict[str, Any] = {},
|
additional_servers: list[AdditionalServer] | None = None,
|
||||||
additional_servers: list[AdditionalServer] = [],
|
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if additional_servers is None:
|
||||||
|
additional_servers = []
|
||||||
self._service = service
|
self._service = service
|
||||||
self._host = host
|
self._host = host
|
||||||
self._rpc_port = rpc_port
|
self._rpc_port = rpc_port
|
||||||
self._web_port = web_port
|
self._web_port = web_port
|
||||||
self._enable_rpc = enable_rpc
|
self._enable_rpc = enable_rpc
|
||||||
self._enable_web = enable_web
|
self._enable_web = enable_web
|
||||||
self._web_settings = web_settings
|
|
||||||
self._kwargs = kwargs
|
self._kwargs = kwargs
|
||||||
self._loop: asyncio.AbstractEventLoop
|
self._loop: asyncio.AbstractEventLoop
|
||||||
self._rpc_server_type = ForkingServer if use_forking_server else ThreadedServer
|
self._rpc_server_type = ForkingServer if use_forking_server else ThreadedServer
|
||||||
@ -192,17 +189,6 @@ class Server:
|
|||||||
self.should_exit = False
|
self.should_exit = False
|
||||||
self.servers: dict[str, asyncio.Future[Any]] = {}
|
self.servers: dict[str, asyncio.Future[Any]] = {}
|
||||||
self.executor: ThreadPoolExecutor | None = None
|
self.executor: ThreadPoolExecutor | None = None
|
||||||
self._info: dict[str, Any] = {
|
|
||||||
"name": self._service.get_service_name(),
|
|
||||||
"version": __version__,
|
|
||||||
"rpc_port": self._rpc_port,
|
|
||||||
"web_port": self._web_port,
|
|
||||||
"enable_rpc": self._enable_rpc,
|
|
||||||
"enable_web": self._enable_web,
|
|
||||||
"web_settings": self._web_settings,
|
|
||||||
"additional_servers": [],
|
|
||||||
**kwargs,
|
|
||||||
}
|
|
||||||
self._state_manager = StateManager(self._service, filename)
|
self._state_manager = StateManager(self._service, filename)
|
||||||
if getattr(self._service, "_filename", None) is not None:
|
if getattr(self._service, "_filename", None) is not None:
|
||||||
self._service._state_manager = self._state_manager
|
self._service._state_manager = self._state_manager
|
||||||
@ -234,7 +220,7 @@ class Server:
|
|||||||
async def serve(self) -> None:
|
async def serve(self) -> None:
|
||||||
process_id = os.getpid()
|
process_id = os.getpid()
|
||||||
|
|
||||||
logger.info(f"Started server process [{process_id}]")
|
logger.info("Started server process [%s]", process_id)
|
||||||
|
|
||||||
await self.startup()
|
await self.startup()
|
||||||
if self.should_exit:
|
if self.should_exit:
|
||||||
@ -242,7 +228,7 @@ class Server:
|
|||||||
await self.main_loop()
|
await self.main_loop()
|
||||||
await self.shutdown()
|
await self.shutdown()
|
||||||
|
|
||||||
logger.info(f"Finished server process [{process_id}]")
|
logger.info("Finished server process [%s]", process_id)
|
||||||
|
|
||||||
async def startup(self) -> None: # noqa: C901
|
async def startup(self) -> None: # noqa: C901
|
||||||
self._loop = asyncio.get_running_loop()
|
self._loop = asyncio.get_running_loop()
|
||||||
@ -270,28 +256,18 @@ class Server:
|
|||||||
port=server["port"],
|
port=server["port"],
|
||||||
host=self._host,
|
host=self._host,
|
||||||
state_manager=self._state_manager,
|
state_manager=self._state_manager,
|
||||||
info=self._info,
|
|
||||||
**server["kwargs"],
|
**server["kwargs"],
|
||||||
)
|
)
|
||||||
|
|
||||||
server_name = (
|
server_name = (
|
||||||
addin_server.__module__ + "." + addin_server.__class__.__name__
|
addin_server.__module__ + "." + addin_server.__class__.__name__
|
||||||
)
|
)
|
||||||
self._info["additional_servers"].append(
|
|
||||||
{
|
|
||||||
"name": server_name,
|
|
||||||
"port": server["port"],
|
|
||||||
"host": self._host,
|
|
||||||
**server["kwargs"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
future_or_task = self._loop.create_task(addin_server.serve())
|
future_or_task = self._loop.create_task(addin_server.serve())
|
||||||
self.servers[server_name] = future_or_task
|
self.servers[server_name] = future_or_task
|
||||||
if self._enable_web:
|
if self._enable_web:
|
||||||
self._wapi: WebAPI = WebAPI(
|
self._wapi: WebAPI = WebAPI(
|
||||||
service=self._service,
|
service=self._service,
|
||||||
info=self._info,
|
|
||||||
state_manager=self._state_manager,
|
state_manager=self._state_manager,
|
||||||
**self._kwargs,
|
**self._kwargs,
|
||||||
)
|
)
|
||||||
@ -302,10 +278,6 @@ class Server:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def sio_callback(parent_path: str, name: str, value: Any) -> None:
|
def sio_callback(parent_path: str, name: str, value: Any) -> None:
|
||||||
# TODO: an error happens when an attribute is set to a list
|
|
||||||
# > File "/usr/lib64/python3.11/json/encoder.py", line 180, in default
|
|
||||||
# > raise TypeError(f'Object of type {o.__class__.__name__} '
|
|
||||||
# > TypeError: Object of type list is not JSON serializable
|
|
||||||
full_access_path = ".".join([*parent_path.split(".")[1:], name])
|
full_access_path = ".".join([*parent_path.split(".")[1:], name])
|
||||||
cached_value_dict = deepcopy(
|
cached_value_dict = deepcopy(
|
||||||
get_nested_dict_by_path(self._state_manager.cache, full_access_path)
|
get_nested_dict_by_path(self._state_manager.cache, full_access_path)
|
||||||
@ -319,7 +291,7 @@ class Server:
|
|||||||
|
|
||||||
async def notify() -> None:
|
async def notify() -> None:
|
||||||
try:
|
try:
|
||||||
await self._wapi.sio.emit( # type: ignore
|
await self._wapi.sio.emit( # type: ignore[reportUnknownMemberType]
|
||||||
"notify",
|
"notify",
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
@ -330,7 +302,7 @@ class Server:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to send notification: {e}")
|
logger.warning("Failed to send notification: %s", e)
|
||||||
|
|
||||||
self._loop.create_task(notify())
|
self._loop.create_task(notify())
|
||||||
|
|
||||||
@ -338,7 +310,7 @@ class Server:
|
|||||||
|
|
||||||
# overwrite uvicorn's signal handlers, otherwise it will bogart SIGINT and
|
# overwrite uvicorn's signal handlers, otherwise it will bogart SIGINT and
|
||||||
# SIGTERM, which makes it impossible to escape out of
|
# SIGTERM, which makes it impossible to escape out of
|
||||||
web_server.install_signal_handlers = lambda: None # type: ignore
|
web_server.install_signal_handlers = lambda: None # type: ignore[method-assign]
|
||||||
future_or_task = self._loop.create_task(web_server.serve())
|
future_or_task = self._loop.create_task(web_server.serve())
|
||||||
self.servers["web"] = future_or_task
|
self.servers["web"] = future_or_task
|
||||||
|
|
||||||
@ -349,7 +321,7 @@ class Server:
|
|||||||
async def shutdown(self) -> None:
|
async def shutdown(self) -> None:
|
||||||
logger.info("Shutting down")
|
logger.info("Shutting down")
|
||||||
|
|
||||||
logger.info(f"Saving data to {self._state_manager.filename}.")
|
logger.info("Saving data to %s.", self._state_manager.filename)
|
||||||
if self._state_manager is not None:
|
if self._state_manager is not None:
|
||||||
self._state_manager.save_state()
|
self._state_manager.save_state()
|
||||||
|
|
||||||
@ -366,9 +338,9 @@ class Server:
|
|||||||
try:
|
try:
|
||||||
await task
|
await task
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
logger.debug(f"Cancelled {server_name} server.")
|
logger.debug("Cancelled '%s' server.", server_name)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Unexpected exception: {e}.")
|
logger.warning("Unexpected exception: %s", e)
|
||||||
|
|
||||||
async def __cancel_tasks(self) -> None:
|
async def __cancel_tasks(self) -> None:
|
||||||
for task in asyncio.all_tasks(self._loop):
|
for task in asyncio.all_tasks(self._loop):
|
||||||
@ -376,9 +348,9 @@ class Server:
|
|||||||
try:
|
try:
|
||||||
await task
|
await task
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
logger.debug(f"Cancelled task {task.get_coro()}.")
|
logger.debug("Cancelled task '%s'.", task.get_coro())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Unexpected exception: {e}.")
|
logger.exception("Unexpected exception: %s", e)
|
||||||
|
|
||||||
def install_signal_handlers(self) -> None:
|
def install_signal_handlers(self) -> None:
|
||||||
if threading.current_thread() is not threading.main_thread():
|
if threading.current_thread() is not threading.main_thread():
|
||||||
@ -390,11 +362,13 @@ class Server:
|
|||||||
|
|
||||||
def handle_exit(self, sig: int = 0, frame: Optional[FrameType] = None) -> None:
|
def handle_exit(self, sig: int = 0, frame: Optional[FrameType] = None) -> None:
|
||||||
if self.should_exit and sig == signal.SIGINT:
|
if self.should_exit and sig == signal.SIGINT:
|
||||||
logger.warning(f"Received signal {sig}, forcing exit...")
|
logger.warning("Received signal '%s', forcing exit...", sig)
|
||||||
os._exit(1)
|
os._exit(1)
|
||||||
else:
|
else:
|
||||||
self.should_exit = True
|
self.should_exit = True
|
||||||
logger.warning(f"Received signal {sig}, exiting... (CTRL+C to force quit)")
|
logger.warning(
|
||||||
|
"Received signal '%s', exiting... (CTRL+C to force quit)", sig
|
||||||
|
)
|
||||||
|
|
||||||
def custom_exception_handler(
|
def custom_exception_handler(
|
||||||
self, loop: asyncio.AbstractEventLoop, context: dict[str, Any]
|
self, loop: asyncio.AbstractEventLoop, context: dict[str, Any]
|
||||||
@ -411,7 +385,7 @@ class Server:
|
|||||||
|
|
||||||
async def emit_exception() -> None:
|
async def emit_exception() -> None:
|
||||||
try:
|
try:
|
||||||
await self._wapi.sio.emit( # type: ignore
|
await self._wapi.sio.emit( # type: ignore[reportUnknownMemberType]
|
||||||
"exception",
|
"exception",
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
@ -421,7 +395,7 @@ class Server:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to send notification: {e}")
|
logger.exception("Failed to send notification: %s", e)
|
||||||
|
|
||||||
loop.create_task(emit_exception())
|
loop.create_task(emit_exception())
|
||||||
else:
|
else:
|
||||||
|
@ -2,7 +2,7 @@ import logging
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, TypedDict
|
from typing import Any, TypedDict
|
||||||
|
|
||||||
import socketio # type: ignore
|
import socketio # type: ignore[import-untyped]
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
@ -70,23 +70,21 @@ class WebAPI:
|
|||||||
__sio_app: socketio.ASGIApp
|
__sio_app: socketio.ASGIApp
|
||||||
__fastapi_app: FastAPI
|
__fastapi_app: FastAPI
|
||||||
|
|
||||||
def __init__( # noqa: CFQ002
|
def __init__( # noqa: PLR0913
|
||||||
self,
|
self,
|
||||||
service: DataService,
|
service: DataService,
|
||||||
state_manager: StateManager,
|
state_manager: StateManager,
|
||||||
frontend: str | Path | None = None,
|
frontend: str | Path | None = None,
|
||||||
css: str | Path | None = None,
|
css: str | Path | None = None,
|
||||||
enable_CORS: bool = True,
|
enable_cors: bool = True,
|
||||||
info: dict[str, Any] = {},
|
|
||||||
*args: Any,
|
*args: Any,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
):
|
) -> None:
|
||||||
self.service = service
|
self.service = service
|
||||||
self.state_manager = state_manager
|
self.state_manager = state_manager
|
||||||
self.frontend = frontend
|
self.frontend = frontend
|
||||||
self.css = css
|
self.css = css
|
||||||
self.enable_CORS = enable_CORS
|
self.enable_cors = enable_cors
|
||||||
self.info = info
|
|
||||||
self.args = args
|
self.args = args
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
|
|
||||||
@ -100,14 +98,14 @@ class WebAPI:
|
|||||||
|
|
||||||
def setup_socketio(self) -> None:
|
def setup_socketio(self) -> None:
|
||||||
# the socketio ASGI app, to notify clients when params update
|
# the socketio ASGI app, to notify clients when params update
|
||||||
if self.enable_CORS:
|
if self.enable_cors:
|
||||||
sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*")
|
sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*")
|
||||||
else:
|
else:
|
||||||
sio = socketio.AsyncServer(async_mode="asgi")
|
sio = socketio.AsyncServer(async_mode="asgi")
|
||||||
|
|
||||||
@sio.event # type: ignore
|
@sio.event # type: ignore[reportUnknownMemberType]
|
||||||
def set_attribute(sid: str, data: UpdateDict) -> Any:
|
def set_attribute(sid: str, data: UpdateDict) -> Any:
|
||||||
logger.debug(f"Received frontend update: {data}")
|
logger.debug("Received frontend update: %s", data)
|
||||||
path_list = [*data["parent_path"].split("."), data["name"]]
|
path_list = [*data["parent_path"].split("."), data["name"]]
|
||||||
path_list.remove("DataService") # always at the start, does not do anything
|
path_list.remove("DataService") # always at the start, does not do anything
|
||||||
path = ".".join(path_list)
|
path = ".".join(path_list)
|
||||||
@ -115,9 +113,9 @@ class WebAPI:
|
|||||||
path=path, value=data["value"]
|
path=path, value=data["value"]
|
||||||
)
|
)
|
||||||
|
|
||||||
@sio.event # type: ignore
|
@sio.event # type: ignore[reportUnknownMemberType]
|
||||||
def run_method(sid: str, data: RunMethodDict) -> Any:
|
def run_method(sid: str, data: RunMethodDict) -> Any:
|
||||||
logger.debug(f"Running method: {data}")
|
logger.debug("Running method: %s", data)
|
||||||
path_list = [*data["parent_path"].split("."), data["name"]]
|
path_list = [*data["parent_path"].split("."), data["name"]]
|
||||||
path_list.remove("DataService") # always at the start, does not do anything
|
path_list.remove("DataService") # always at the start, does not do anything
|
||||||
method = get_object_attr_from_path_list(self.service, path_list)
|
method = get_object_attr_from_path_list(self.service, path_list)
|
||||||
@ -126,10 +124,10 @@ class WebAPI:
|
|||||||
self.__sio = sio
|
self.__sio = sio
|
||||||
self.__sio_app = socketio.ASGIApp(self.__sio)
|
self.__sio_app = socketio.ASGIApp(self.__sio)
|
||||||
|
|
||||||
def setup_fastapi_app(self) -> None: # noqa
|
def setup_fastapi_app(self) -> None:
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
if self.enable_CORS:
|
if self.enable_cors:
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
@ -147,10 +145,6 @@ class WebAPI:
|
|||||||
def name() -> str:
|
def name() -> str:
|
||||||
return self.service.get_service_name()
|
return self.service.get_service_name()
|
||||||
|
|
||||||
@app.get("/info")
|
|
||||||
def info() -> dict[str, Any]:
|
|
||||||
return self.info
|
|
||||||
|
|
||||||
@app.get("/service-properties")
|
@app.get("/service-properties")
|
||||||
def service_properties() -> dict[str, Any]:
|
def service_properties() -> dict[str, Any]:
|
||||||
return self.state_manager.cache
|
return self.state_manager.cache
|
||||||
|
@ -15,7 +15,7 @@ class QuantityDict(TypedDict):
|
|||||||
|
|
||||||
|
|
||||||
def convert_to_quantity(
|
def convert_to_quantity(
|
||||||
value: QuantityDict | float | int | Quantity, unit: str = ""
|
value: QuantityDict | float | Quantity, unit: str = ""
|
||||||
) -> Quantity:
|
) -> Quantity:
|
||||||
"""
|
"""
|
||||||
Convert a given value into a pint.Quantity object with the specified unit.
|
Convert a given value into a pint.Quantity object with the specified unit.
|
||||||
@ -53,4 +53,4 @@ def convert_to_quantity(
|
|||||||
quantity = float(value["magnitude"]) * Unit(value["unit"])
|
quantity = float(value["magnitude"]) * Unit(value["unit"])
|
||||||
else:
|
else:
|
||||||
quantity = value
|
quantity = value
|
||||||
return quantity # type: ignore
|
return quantity # type: ignore[reportUnknownMemberType]
|
||||||
|
@ -54,12 +54,12 @@ def get_object_attr_from_path_list(target_obj: Any, path: list[str]) -> Any:
|
|||||||
index_str = index_str.replace("]", "")
|
index_str = index_str.replace("]", "")
|
||||||
index = int(index_str)
|
index = int(index_str)
|
||||||
target_obj = getattr(target_obj, attr)[index]
|
target_obj = getattr(target_obj, attr)[index]
|
||||||
except ValueError:
|
except ValueError: # noqa: PERF203
|
||||||
# No index, so just get the attribute
|
# No index, so just get the attribute
|
||||||
target_obj = getattr(target_obj, part)
|
target_obj = getattr(target_obj, part)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# The attribute doesn't exist
|
# The attribute doesn't exist
|
||||||
logger.debug(f"Attribute {part} does not exist in the object.")
|
logger.debug("Attribute % does not exist in the object.", part)
|
||||||
return None
|
return None
|
||||||
return target_obj
|
return target_obj
|
||||||
|
|
||||||
@ -141,7 +141,7 @@ def update_value_if_changed(
|
|||||||
if getattr(target, attr_name_or_index) != new_value:
|
if getattr(target, attr_name_or_index) != new_value:
|
||||||
setattr(target, attr_name_or_index, new_value)
|
setattr(target, attr_name_or_index, new_value)
|
||||||
else:
|
else:
|
||||||
logger.error(f"Incompatible arguments: {target}, {attr_name_or_index}.")
|
logger.error("Incompatible arguments: %s, %s.", target, attr_name_or_index)
|
||||||
|
|
||||||
|
|
||||||
def parse_list_attr_and_index(attr_string: str) -> tuple[str, Optional[int]]:
|
def parse_list_attr_and_index(attr_string: str) -> tuple[str, Optional[int]]:
|
||||||
@ -175,7 +175,7 @@ def parse_list_attr_and_index(attr_string: str) -> tuple[str, Optional[int]]:
|
|||||||
if index_part.isdigit():
|
if index_part.isdigit():
|
||||||
index = int(index_part)
|
index = int(index_part)
|
||||||
else:
|
else:
|
||||||
logger.error(f"Invalid index format in key: {attr_name}")
|
logger.error("Invalid index format in key: %s", attr_name)
|
||||||
return attr_name, index
|
return attr_name, index
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import sys
|
|||||||
from copy import copy
|
from copy import copy
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import socketio
|
import socketio # type: ignore[import-untyped]
|
||||||
import uvicorn.logging
|
import uvicorn.logging
|
||||||
from uvicorn.config import LOGGING_CONFIG
|
from uvicorn.config import LOGGING_CONFIG
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ class DefaultFormatter(uvicorn.logging.ColourizedFormatter):
|
|||||||
for formatting the output, instead of the plain text message.
|
for formatting the output, instead of the plain text message.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def formatMessage(self, record: logging.LogRecord) -> str:
|
def formatMessage(self, record: logging.LogRecord) -> str: # noqa: N802
|
||||||
recordcopy = copy(record)
|
recordcopy = copy(record)
|
||||||
levelname = recordcopy.levelname
|
levelname = recordcopy.levelname
|
||||||
seperator = " " * (8 - len(recordcopy.levelname))
|
seperator = " " * (8 - len(recordcopy.levelname))
|
||||||
@ -33,7 +33,7 @@ class DefaultFormatter(uvicorn.logging.ColourizedFormatter):
|
|||||||
return logging.Formatter.formatMessage(self, recordcopy)
|
return logging.Formatter.formatMessage(self, recordcopy)
|
||||||
|
|
||||||
def should_use_colors(self) -> bool:
|
def should_use_colors(self) -> bool:
|
||||||
return sys.stderr.isatty() # pragma: no cover
|
return sys.stderr.isatty()
|
||||||
|
|
||||||
|
|
||||||
class SocketIOHandler(logging.Handler):
|
class SocketIOHandler(logging.Handler):
|
||||||
@ -74,7 +74,7 @@ def setup_logging(level: Optional[str | int] = None) -> None:
|
|||||||
with an option to override the level. By default, in a development environment, the
|
with an option to override the level. By default, in a development environment, the
|
||||||
log level is set to DEBUG, whereas in other environments, it is set to INFO.
|
log level is set to DEBUG, whereas in other environments, it is set to INFO.
|
||||||
|
|
||||||
Parameters:
|
Args:
|
||||||
level (Optional[str | int]):
|
level (Optional[str | int]):
|
||||||
A specific log level to set for the application. If None, the log level is
|
A specific log level to set for the application. If None, the log level is
|
||||||
determined based on the application's operation mode. Accepts standard log
|
determined based on the application's operation mode. Accepts standard log
|
||||||
@ -123,7 +123,10 @@ def setup_logging(level: Optional[str | int] = None) -> None:
|
|||||||
# add formatter to ch
|
# add formatter to ch
|
||||||
ch.setFormatter(
|
ch.setFormatter(
|
||||||
DefaultFormatter(
|
DefaultFormatter(
|
||||||
fmt="%(asctime)s.%(msecs)03d | %(levelprefix)s | %(name)s:%(funcName)s:%(lineno)d - %(message)s",
|
fmt=(
|
||||||
|
"%(asctime)s.%(msecs)03d | %(levelprefix)s | "
|
||||||
|
"%(name)s:%(funcName)s:%(lineno)d - %(message)s"
|
||||||
|
),
|
||||||
datefmt="%Y-%m-%d %H:%M:%S",
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -140,7 +143,8 @@ def setup_logging(level: Optional[str | int] = None) -> None:
|
|||||||
"fmt"
|
"fmt"
|
||||||
] = "%(asctime)s.%(msecs)03d | %(levelprefix)s %(message)s"
|
] = "%(asctime)s.%(msecs)03d | %(levelprefix)s %(message)s"
|
||||||
LOGGING_CONFIG["formatters"]["default"]["datefmt"] = "%Y-%m-%d %H:%M:%S"
|
LOGGING_CONFIG["formatters"]["default"]["datefmt"] = "%Y-%m-%d %H:%M:%S"
|
||||||
LOGGING_CONFIG["formatters"]["access"][
|
LOGGING_CONFIG["formatters"]["access"]["fmt"] = (
|
||||||
"fmt"
|
"%(asctime)s.%(msecs)03d | %(levelprefix)s %(client_addr)s "
|
||||||
] = '%(asctime)s.%(msecs)03d | %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s'
|
'- "%(request_line)s" %(status_code)s'
|
||||||
|
)
|
||||||
LOGGING_CONFIG["formatters"]["access"]["datefmt"] = "%Y-%m-%d %H:%M:%S"
|
LOGGING_CONFIG["formatters"]["access"]["datefmt"] = "%Y-%m-%d %H:%M:%S"
|
||||||
|
@ -28,7 +28,7 @@ class Serializer:
|
|||||||
def serialize_object(obj: Any) -> dict[str, Any]:
|
def serialize_object(obj: Any) -> dict[str, Any]:
|
||||||
result: dict[str, Any] = {}
|
result: dict[str, Any] = {}
|
||||||
if isinstance(obj, AbstractDataService):
|
if isinstance(obj, AbstractDataService):
|
||||||
result = Serializer._serialize_DataService(obj)
|
result = Serializer._serialize_data_service(obj)
|
||||||
|
|
||||||
elif isinstance(obj, list):
|
elif isinstance(obj, list):
|
||||||
result = Serializer._serialize_list(obj)
|
result = Serializer._serialize_list(obj)
|
||||||
@ -38,7 +38,7 @@ class Serializer:
|
|||||||
|
|
||||||
# Special handling for u.Quantity
|
# Special handling for u.Quantity
|
||||||
elif isinstance(obj, u.Quantity):
|
elif isinstance(obj, u.Quantity):
|
||||||
result = Serializer._serialize_Quantity(obj)
|
result = Serializer._serialize_quantity(obj)
|
||||||
|
|
||||||
# Handling for Enums
|
# Handling for Enums
|
||||||
elif isinstance(obj, Enum):
|
elif isinstance(obj, Enum):
|
||||||
@ -83,7 +83,7 @@ class Serializer:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _serialize_Quantity(obj: u.Quantity) -> dict[str, Any]:
|
def _serialize_quantity(obj: u.Quantity) -> dict[str, Any]:
|
||||||
obj_type = "Quantity"
|
obj_type = "Quantity"
|
||||||
readonly = False
|
readonly = False
|
||||||
doc = get_attribute_doc(obj)
|
doc = get_attribute_doc(obj)
|
||||||
@ -154,7 +154,7 @@ class Serializer:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _serialize_DataService(obj: AbstractDataService) -> dict[str, Any]:
|
def _serialize_data_service(obj: AbstractDataService) -> dict[str, Any]:
|
||||||
readonly = False
|
readonly = False
|
||||||
doc = get_attribute_doc(obj)
|
doc = get_attribute_doc(obj)
|
||||||
obj_type = type(obj).__name__
|
obj_type = type(obj).__name__
|
||||||
@ -180,9 +180,7 @@ class Serializer:
|
|||||||
|
|
||||||
# Skip keys that start with "start_" or "stop_" and end with an async
|
# Skip keys that start with "start_" or "stop_" and end with an async
|
||||||
# method name
|
# method name
|
||||||
if (key.startswith("start_") or key.startswith("stop_")) and key.split(
|
if key.startswith(("start_", "stop_")) and key.split("_", 1)[1] in {
|
||||||
"_", 1
|
|
||||||
)[1] in {
|
|
||||||
name
|
name
|
||||||
for name, _ in inspect.getmembers(
|
for name, _ in inspect.getmembers(
|
||||||
obj, predicate=inspect.iscoroutinefunction
|
obj, predicate=inspect.iscoroutinefunction
|
||||||
@ -293,6 +291,7 @@ def get_nested_dict_by_path(
|
|||||||
def get_next_level_dict_by_key(
|
def get_next_level_dict_by_key(
|
||||||
serialization_dict: dict[str, Any],
|
serialization_dict: dict[str, Any],
|
||||||
attr_name: str,
|
attr_name: str,
|
||||||
|
*,
|
||||||
allow_append: bool = False,
|
allow_append: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
@ -366,23 +365,23 @@ def generate_serialized_data_paths(
|
|||||||
attribute in the serialized data.
|
attribute in the serialized data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
paths = []
|
paths: list[str] = []
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
if value["type"] == "method":
|
if value["type"] == "method":
|
||||||
# ignoring methods
|
# ignoring methods
|
||||||
continue
|
continue
|
||||||
new_path = f"{parent_path}.{key}" if parent_path else key
|
new_path = f"{parent_path}.{key}" if parent_path else key
|
||||||
if isinstance(value["value"], dict) and value["type"] != "Quantity":
|
if isinstance(value["value"], dict) and value["type"] != "Quantity":
|
||||||
paths.extend(generate_serialized_data_paths(value["value"], new_path)) # type: ignore
|
paths.extend(generate_serialized_data_paths(value["value"], new_path))
|
||||||
elif isinstance(value["value"], list):
|
elif isinstance(value["value"], list):
|
||||||
for index, item in enumerate(value["value"]):
|
for index, item in enumerate(value["value"]):
|
||||||
indexed_key_path = f"{new_path}[{index}]"
|
indexed_key_path = f"{new_path}[{index}]"
|
||||||
if isinstance(item["value"], dict):
|
if isinstance(item["value"], dict):
|
||||||
paths.extend( # type: ignore
|
paths.extend(
|
||||||
generate_serialized_data_paths(item["value"], indexed_key_path)
|
generate_serialized_data_paths(item["value"], indexed_key_path)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
paths.append(indexed_key_path) # type: ignore
|
paths.append(indexed_key_path)
|
||||||
else:
|
else:
|
||||||
paths.append(new_path) # type: ignore
|
paths.append(new_path)
|
||||||
return paths
|
return paths
|
||||||
|
@ -5,7 +5,7 @@ from pydase.utils.helpers import get_component_class_names
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def warn_if_instance_class_does_not_inherit_from_DataService(__value: object) -> None:
|
def warn_if_instance_class_does_not_inherit_from_data_service(__value: object) -> None:
|
||||||
base_class_name = __value.__class__.__base__.__name__
|
base_class_name = __value.__class__.__base__.__name__
|
||||||
module_name = __value.__class__.__module__
|
module_name = __value.__class__.__module__
|
||||||
|
|
||||||
@ -18,9 +18,10 @@ def warn_if_instance_class_does_not_inherit_from_DataService(__value: object) ->
|
|||||||
"_abc",
|
"_abc",
|
||||||
]
|
]
|
||||||
and base_class_name
|
and base_class_name
|
||||||
not in ["DataService", "list", "Enum"] + get_component_class_names()
|
not in ["DataService", "list", "Enum", *get_component_class_names()]
|
||||||
and type(__value).__name__ not in ["CallbackManager", "TaskManager", "Quantity"]
|
and type(__value).__name__ not in ["CallbackManager", "TaskManager", "Quantity"]
|
||||||
):
|
):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Warning: Class {type(__value).__name__} does not inherit from DataService."
|
"Warning: Class '%s' does not inherit from DataService.",
|
||||||
|
type(__value).__name__,
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from importlib.metadata import distribution
|
from importlib.metadata import distribution
|
||||||
|
|
||||||
__version__ = distribution("pydase").version
|
__version__ = distribution("pydase").version
|
||||||
__major__, __minor__, __patch__ = [int(v) for v in __version__.split(".")]
|
__major__, __minor__, __patch__ = (int(v) for v in __version__.split("."))
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from pytest import CaptureFixture, LogCaptureFixture
|
from pytest import LogCaptureFixture
|
||||||
|
|
||||||
from pydase.components.number_slider import NumberSlider
|
from pydase.components.number_slider import NumberSlider
|
||||||
from pydase.data_service.data_service import DataService
|
from pydase.data_service.data_service import DataService
|
||||||
@ -38,6 +38,6 @@ def test_NumberSlider(caplog: LogCaptureFixture) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_init_error(caplog: LogCaptureFixture) -> None: # noqa
|
def test_init_error(caplog: LogCaptureFixture) -> None: # noqa
|
||||||
number_slider = NumberSlider(type="str") # type: ignore # noqa
|
number_slider = NumberSlider(type_="str") # type: ignore # noqa
|
||||||
|
|
||||||
assert "Unknown type 'str'. Using 'float'" in caplog.text
|
assert "Unknown type 'str'. Using 'float'" in caplog.text
|
||||||
|
@ -162,7 +162,7 @@ def test_load_state(tmp_path: Path, caplog: LogCaptureFixture):
|
|||||||
"Ignoring value from JSON file..."
|
"Ignoring value from JSON file..."
|
||||||
) in caplog.text
|
) in caplog.text
|
||||||
assert (
|
assert (
|
||||||
"Attribute type of 'removed_attr' changed from 'str' to None. "
|
"Attribute type of 'removed_attr' changed from 'str' to 'None'. "
|
||||||
"Ignoring value from JSON file..." in caplog.text
|
"Ignoring value from JSON file..." in caplog.text
|
||||||
)
|
)
|
||||||
assert "Value of attribute 'subservice.name' has not changed..." in caplog.text
|
assert "Value of attribute 'subservice.name' has not changed..." in caplog.text
|
||||||
|
@ -15,7 +15,7 @@ def test_setattr_warnings(caplog: LogCaptureFixture) -> None: # noqa
|
|||||||
|
|
||||||
ServiceClass()
|
ServiceClass()
|
||||||
|
|
||||||
assert "Warning: Class SubClass does not inherit from DataService." in caplog.text
|
assert "Warning: Class 'SubClass' does not inherit from DataService." in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_private_attribute_warning(caplog: LogCaptureFixture) -> None: # noqa
|
def test_private_attribute_warning(caplog: LogCaptureFixture) -> None: # noqa
|
||||||
|
Loading…
x
Reference in New Issue
Block a user