Skip to content

Commit

Permalink
Feature/proxy attribute forwarding (#215)
Browse files Browse the repository at this point in the history
* feat: implement cchannel attribute forwarding for proxy setups

Co-authored-by: Sébastian Clerson <s.clerson@technologyandstrategy.com>
  • Loading branch information
sebclrsn and Sébastian Clerson authored Jan 12, 2023
1 parent a4a691c commit 15bf26d
Show file tree
Hide file tree
Showing 15 changed files with 335 additions and 89 deletions.
6 changes: 6 additions & 0 deletions docs/advanced_usage/advanced_config_file.rst
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ that will have exclusive access to the communication channel. A
the created :py:class:`~pykiso.lib.auxiliaries.proxy_auxiliary.ProxyAuxiliary`. This allows to
keep a minimalistic :py:class:`~pykiso.connector.CChannel` implementation.

Access to attributes and methods of the defined communication channel that has been
attached to the created :py:class:`~pykiso.lib.auxiliaries.proxy_auxiliary.ProxyAuxiliary`
is still possible, but keep in mind that if one auxiliary modifies one the communication
channel's attributes, every other auxiliary sharing this communication channel will be
affected by this change.

An illustration of the resulting internal setup can be found at :ref:`proxy_aux`.

In other words, if you define the following YAML configuration file:
Expand Down
10 changes: 10 additions & 0 deletions docs/whats_new/version_ongoing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,13 @@ specify their names for each defined auxiliary.

This is no longer the case and specifying them in only one auxiliary
will be enough for the loggers to stay enabled.


Internal creation of proxy auxiliaries
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

It is no longer necessary to manually defined a ``ProxyAuxiliary`` with
``CCProxy``s yourself. If you simply pass the communication channel to
each auxiliary that has to share it, ``pykiso`` will do the rest for you.


17 changes: 4 additions & 13 deletions src/pykiso/auxiliary.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
import time
from typing import Any

from .logging_initializer import add_logging_level
from .test_setup.config_registry import ConfigRegistry
from .types import MsgType
from pykiso.logging_initializer import add_internal_log_levels
from pykiso.test_setup.config_registry import ConfigRegistry
from pykiso.types import MsgType

log = logging.getLogger(__name__)

Expand All @@ -38,18 +38,9 @@ class AuxiliaryCommon(metaclass=abc.ABCMeta):
multiprocessing and thread auxiliary interface.
"""

def __new__(cls: AuxiliaryCommon, *args, **kwargs) -> AuxiliaryCommon:
"""Create instance and add internal kiso log levels in
case the auxiliary is used outside the pykiso context
"""
if not hasattr(logging, "INTERNAL_WARNING"):
add_logging_level("INTERNAL_WARNING", logging.WARNING + 1)
add_logging_level("INTERNAL_INFO", logging.INFO + 1)
add_logging_level("INTERNAL_DEBUG", logging.DEBUG + 1)
return super(AuxiliaryCommon, cls).__new__(cls)

def __init__(self) -> None:
"""Auxiliary common attributes initialization."""
add_internal_log_levels()
self.name = None
self.queue_in = None
self.lock = None
Expand Down
17 changes: 4 additions & 13 deletions src/pykiso/interfaces/dt_auxiliary.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from typing import Any, Callable, List, Optional

from ..exceptions import AuxiliaryCreationError
from ..logging_initializer import add_logging_level, initialize_loggers
from ..logging_initializer import add_internal_log_levels, initialize_loggers

log = logging.getLogger(__name__)

Expand All @@ -49,16 +49,6 @@ class DTAuxiliaryInterface(abc.ABC):
for the reception and one for the transmmission.
"""

def __new__(cls, *args, **kwargs):
"""Create instance and add internal kiso log levels in
case the auxiliary is used outside the pykiso context
"""
if not hasattr(logging, "INTERNAL_WARNING"):
add_logging_level("INTERNAL_WARNING", logging.WARNING + 1)
add_logging_level("INTERNAL_INFO", logging.INFO + 1)
add_logging_level("INTERNAL_DEBUG", logging.DEBUG + 1)
return super(DTAuxiliaryInterface, cls).__new__(cls)

def __init__(
self,
name: str = None,
Expand All @@ -82,10 +72,11 @@ def __init__(
:param auto_start: determine if the auxiliayry is automatically
started (magic import) or manually (by user)
"""
initialize_loggers(activate_log)
add_internal_log_levels()
self.name = name
self.is_proxy_capable = is_proxy_capable
self.auto_start = auto_start
initialize_loggers(activate_log)
self.lock = threading.RLock()
self.stop_tx = threading.Event()
self.stop_rx = threading.Event()
Expand Down Expand Up @@ -209,7 +200,7 @@ def _start_rx_task(self) -> None:
log.internal_debug("reception task is not needed, don't start it")
return

log.internal_debug(f"start reception task {self.name}_tx")
log.internal_debug(f"start reception task {self.name}_rx")
self.rx_thread = threading.Thread(
name=f"{self.name}_rx", target=self._reception_task
)
Expand Down
33 changes: 29 additions & 4 deletions src/pykiso/lib/auxiliaries/mp_proxy_auxiliary.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from typing import List, Optional, Tuple

from pykiso import AuxiliaryInterface, CChannel, MpAuxiliaryInterface
from pykiso.lib.connectors.cc_mp_proxy import CCMpProxy
from pykiso.test_setup.config_registry import ConfigRegistry
from pykiso.test_setup.dynamic_loader import PACKAGE

Expand Down Expand Up @@ -95,13 +96,13 @@ def __init__(
activate=activate_trace, dir=trace_dir, name=trace_name
)
self.proxy_channels = self.get_proxy_con(aux_list)
self.logger = None
self.logger: logging.Logger = None
self.aux_list = aux_list
super().__init__(**kwargs)

def _init_trace(
self,
logger,
logger: logging.Logger,
activate: bool,
t_dir: Optional[str] = None,
t_name: Optional[str] = None,
Expand Down Expand Up @@ -144,7 +145,7 @@ def _init_trace(
handler.setLevel(logging.DEBUG)
logger.addHandler(handler)

def get_proxy_con(self, aux_list: List[str]) -> Tuple[AuxiliaryInterface]:
def get_proxy_con(self, aux_list: List[str]) -> Tuple[CCMpProxy]:
"""Retrieve all connector associated to all given existing Auxiliaries.
If auxiliary alias exists but auxiliary instance was not created
Expand All @@ -155,7 +156,7 @@ def get_proxy_con(self, aux_list: List[str]) -> Tuple[AuxiliaryInterface]:
:return: tuple containing all connectors associated to
all given auxiliaries
"""
channel_inst = []
channel_inst: List[CCMpProxy] = []

for aux_name in aux_list:
aux_inst = sys.modules.get(f"{PACKAGE}.auxiliaries.{aux_name}")
Expand All @@ -177,6 +178,14 @@ def get_proxy_con(self, aux_list: List[str]) -> Tuple[AuxiliaryInterface]:
else:
log.error(f"Auxiliary : {aux_name} doesn't exist")

# Check if auxes/connectors are compatible with the proxy aux
self._check_channels_compatibility(channel_inst)

# Finally bind the physical channel to the proxy channels to
# share its API to the user's auxiliaries
for channel in channel_inst:
channel._bind_channel_info(self)

return tuple(channel_inst)

@staticmethod
Expand All @@ -192,6 +201,22 @@ def _check_compatibility(aux: AuxiliaryInterface) -> None:
f"Auxiliary {aux} is not compatible with a proxy auxiliary"
)

@staticmethod
def _check_channels_compatibility(channels: List[CChannel]) -> None:
"""Check if all associated channels are compatible.
:param channels: all channels collected by the proxy aux
:raises TypeError: if the connector is not an instance of
CCProxy
"""
for channel in channels:
if not isinstance(channel, CCMpProxy):
raise TypeError(
f"Channel {channel} is not compatible! "
f"Expected a CCMpProxy instance, got {channel.__class__.__name__}"
)

def _create_auxiliary_instance(self) -> bool:
"""Open current associated channel.
Expand Down
13 changes: 9 additions & 4 deletions src/pykiso/lib/auxiliaries/proxy_auxiliary.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def _init_trace(

return logger

def get_proxy_con(self, aux_list: List[str]) -> Tuple:
def get_proxy_con(self, aux_list: List[str]) -> Tuple[CCProxy, ...]:
"""Retrieve all connector associated to all given existing Auxiliaries.
If auxiliary alias exists but auxiliary instance was not created
Expand All @@ -155,7 +155,7 @@ def get_proxy_con(self, aux_list: List[str]) -> Tuple:
:return: tuple containing all connectors associated to
all given auxiliaries
"""
channel_inst = []
channel_inst: List[CCProxy] = []

for aux in aux_list:
# aux_list can contain a auxiliary instance just grab the
Expand Down Expand Up @@ -184,10 +184,15 @@ def get_proxy_con(self, aux_list: List[str]) -> Tuple:
# invalid one
else:
log.error(f"Auxiliary '{aux}' doesn't exist")
# Finally just check if auxes/connectors are compatible with
# the proxy aux

# Check if auxes/connectors are compatible with the proxy aux
self._check_channels_compatibility(channel_inst)

# Finally bind the physical channel to the proxy channels to
# share its API to the user's auxiliaries
for channel in channel_inst:
channel._bind_channel_info(self)

return tuple(channel_inst)

@staticmethod
Expand Down
46 changes: 44 additions & 2 deletions src/pykiso/lib/connectors/cc_mp_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,17 @@
.. currentmodule:: cc_mp_proxy
"""

from __future__ import annotations

import logging
import multiprocessing
import queue
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

from pykiso.connector import CChannel

if TYPE_CHECKING:
from pykiso.lib.auxiliaries.mp_proxy_auxiliary import MpProxyAuxiliary
from pykiso.types import ProxyReturn

log = logging.getLogger(__name__)
Expand All @@ -43,11 +43,53 @@ class CCMpProxy(CChannel):

def __init__(self, **kwargs):
"""Initialize attributes."""
kwargs.update(processing=True)
super().__init__(**kwargs)
# instantiate directly both queue_in and queue_out
self.queue_in = multiprocessing.Queue()
self.queue_out = multiprocessing.Queue()
self.timeout = 1
# used for physical channel attributes access from main auxiliary
self._proxy = None
self._physical_channel = None

def _bind_channel_info(self, proxy_aux: MpProxyAuxiliary):
"""Bind a :py:class:`~pykiso.lib.auxiliaries.mp_proxy_auxiliary.MpProxyAuxiliary`
instance that is instanciated in order to handle the connection of
multiple auxiliaries to a single communication channel in order to
hide the underlying proxy setup.
:param proxy_aux: the proxy auxiliary instance that is holding the
real communication channel.
"""
self._proxy = proxy_aux
self._physical_channel = proxy_aux.channel

def __getattr__(self, name: str) -> Any:
"""Implement getattr to retrieve attributes from the real channel attached
to the underlying :py:class:`~pykiso.lib.auxiliaries.mp_proxy_auxiliary.MpProxyAuxiliary`.
:param name: name of the attribute to get.
:raises AttributeError: if the attribute is not part of the real
channel instance or if the real channel hasn't been bound to
this proxy channel yet.
:return: the found attribute value.
"""
if self._physical_channel is not None:
with self._proxy.lock:
return getattr(self._physical_channel, name)
raise AttributeError(
f"{self.__class__.__name__} object has no attribute '{name}'"
)

def __getstate__(self):
"""Avoid getattr to be called on pickling before instanciation,
which would cause infinite recursion.
"""
return self.__dict__

def __setstate__(self, state):
self.__dict__ = state

def _cc_open(self) -> None:
"""Open proxy channel.
Expand Down
34 changes: 34 additions & 0 deletions src/pykiso/lib/connectors/cc_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from pykiso.connector import CChannel

if TYPE_CHECKING:
from pykiso.lib.auxiliaries.proxy_auxiliary import ProxyAuxiliary
from pykiso.types import ProxyReturn


Expand All @@ -48,6 +49,39 @@ def __init__(self, **kwargs):
self.timeout = 1
self._lock = threading.Lock()
self._tx_callback = None
self._proxy = None
self._physical_channel = None

def _bind_channel_info(self, proxy_aux: ProxyAuxiliary):
"""Bind a :py:class:`~pykiso.lib.auxiliaries.proxy_auxiliary.ProxyAuxiliary`
instance that is instanciated in order to handle the connection of
multiple auxiliaries to a single communication channel.
This allows to access the real communication channel's attributes
and hides the underlying proxy setup.
:param proxy_aux: the proxy auxiliary instance that is holding the
real communication channel.
"""
self._proxy = proxy_aux
self._physical_channel = proxy_aux.channel

def __getattr__(self, name: str) -> Any:
"""Implement getattr to retrieve attributes from the real channel attached
to the underlying :py:class:`~pykiso.lib.auxiliaries.proxy_auxiliary.ProxyAuxiliary`.
:param name: name of the attribute to get.
:raises AttributeError: if the attribute is not part of the real
channel instance or if the real channel hasn't been bound to
this proxy channel yet.
:return: the found attribute value.
"""
if self._physical_channel is not None:
with self._proxy.lock:
return getattr(self._physical_channel, name)
raise AttributeError(
f"{self.__class__.__name__} object has no attribute '{name}'"
)

def detach_tx_callback(self) -> None:
"""Detach the current callback."""
Expand Down
10 changes: 1 addition & 9 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,11 @@
from pykiso.lib.connectors import cc_example
from pykiso.lib.connectors.cc_pcan_can import CCPCanCan
from pykiso.lib.connectors.cc_vector_can import CCVectorCan
from pykiso.logging_initializer import (
LogOptions,
add_logging_level,
get_logging_options,
)
from pykiso.logging_initializer import LogOptions, get_logging_options
from pykiso.test_coordinator import test_case
from pykiso.test_coordinator.test_case import define_test_parameters
from pykiso.test_setup.dynamic_loader import DynamicImportLinker

# add the internal log levels to avoid errors during unit tests execution
add_logging_level("INTERNAL_WARNING", logging.WARNING + 1)
add_logging_level("INTERNAL_INFO", logging.INFO + 1)
add_logging_level("INTERNAL_DEBUG", logging.DEBUG + 1)

## skip slow test by default
def pytest_addoption(parser):
Expand Down
Loading

0 comments on commit 15bf26d

Please sign in to comment.