Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6094d72
refactor(connection_metadata.py): develop new connection metadata obj…
matosys Dec 4, 2024
b30df0e
feat(_balder/cnnrelations/base_connection_relation.py): implement met…
matosys Nov 26, 2024
ca39795
feat(_balder/cnnrelations/): implement methods `is_resolved()`, `get_…
matosys Nov 26, 2024
c532bd7
feat(_balder/cnnrelations/): implement method `cut_into_all_possible_…
matosys Nov 26, 2024
f2cce97
feat(base_connection_relation.py): implement new method `equal_with()`
matosys Dec 3, 2024
d59be55
feat(cnnrelations): implement new method `contained_in()`
matosys Dec 3, 2024
f30bf26
refactor(connection.py): move methods `clone_without_based_on_element…
matosys Dec 3, 2024
4bbee54
refactor(connection.py): move static methods into correct file section
matosys Dec 3, 2024
8f7b758
refactor(connection.py): move method `get_intersection_with_other_sin…
matosys Dec 3, 2024
4629b97
refactor(connection.py): delete unused method `cleanup_connection_lis…
matosys Dec 7, 2024
b32b0b9
refactor(connection.py): move implementation of method `has_connectio…
matosys Dec 7, 2024
dae6f28
feat(base_connection_relation.py): implement new method `get_all_used…
matosys Dec 7, 2024
cf1c1d3
refactor(connection.py): move implementation of method `get_conn_part…
matosys Dec 7, 2024
12dd83a
refactor(connection.py): rename methods `check_equal_connections_are_…
matosys Dec 7, 2024
f5b7b7c
feat(connection.py): add new class methods `filter_connections_that_a…
matosys Dec 7, 2024
03129b5
feat(connection): completely replace tuple approach and use new AND/O…
matosys Dec 8, 2024
a0c84c8
refactor(routing_path.py): fix wrong typing for property `elements`
matosys Dec 8, 2024
3a56fa9
refactor(solver.py): fix wrong typing for method `get_initial_mapping()`
matosys Dec 8, 2024
da50f93
refactor(testcase_executor.py): remove unused import for `UnresolvedP…
matosys Dec 8, 2024
3b1bd27
feat(*): provide new __hash__ method for relation, connection and met…
matosys Dec 9, 2024
a210c61
feat(_balder/cnnrelations/): add new property `metadata`
matosys Dec 9, 2024
6df4ea0
feat(decorator_for_vdevice.py): allow new AND/OR relations and simpli…
matosys Jan 8, 2025
21a78bc
docs(*): update documentation to new definition of AND/OR by using `&…
matosys Jan 7, 2025
d93fef1
refactor(.pylintrc): increase max allowed arguments from 5 to 6
matosys Jan 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ exclude-too-few-public-methods=
ignored-parents=

# Maximum number of arguments for function / method.
max-args=5
max-args=6

# Maximum number of attributes for a class (see R0902).
max-attributes=15
Expand Down
29 changes: 3 additions & 26 deletions doc/source/basics/connections.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,34 +83,11 @@ It is also possible that a connection requires multiple other connections (**AND
:class:`.DnsConnection` requires a :class:`UdpConnection` **AND** a :class:`TcpConnection`, because DNS uses UDP per
default, but it uses TCP for requests that sends data that is to much for UDP.

So we can define an AND connection simply by using tuples:
So we can define an AND connection simply with:

.. code-block::

conn = DnsConnection.based_on((UdpConnection, TcpConnection))

Limits of connection-relations
------------------------------

You can define **AND**/**OR** definitions in almost any possible variation, but there is one limit.
It is not allowed to define **AND** connections inside other **AND** connections, so for example:

.. code-block:: python

# ALLOWED
conn = SpecialConnection.based_on((AConnection, BConnection), CConnection)
# NOT ALLOWED
conn = SpecialConnection.based_on((AConnection, BConnection, (CConnection, DConnection)))

The last definition is not allowed because we use an inner **AND** connection there. We can write the same logic more
easier by refactoring the both **AND** relations:

.. code-block:: python

# same like the NOT ALLOWED tree from above - BUT NOW IT IS ALLOWED
conn = SpecialConnection.based_on((AConnection, BConnection, CConnection, DConnection)))

This limitation makes it easier to read the logic.
conn = DnsConnection.based_on(UdpConnection & TcpConnection)

Using the base connection object
================================
Expand Down Expand Up @@ -139,7 +116,7 @@ connection:

.. code-block:: python

conn = Connection.based_on(AConnection, BConnection)
conn = Connection.based_on(AConnection | BConnection)

**A container connection always has based-on elements**.

Expand Down
10 changes: 5 additions & 5 deletions doc/source/basics/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ Basically our scenario level implementation looks like:
from balder.connections import TcpConnection
from .connections import SerialConnection

@balder.for_vdevice(with_vdevice='OtherVDevice', [TcpConnection, SerialConnection])
@balder.for_vdevice('OtherVDevice', with_connections=TcpConnection | SerialConnection)
class SendMessengerFeature(balder.Feature):

class OtherVDevice(balder.VDevice):
Expand Down Expand Up @@ -394,12 +394,12 @@ use the **Method-Based-Binding** decorator:

class SetupSendMessengerFeature(MessengerFeature):

@balder.for_vdevice(MessengerFeature.OtherVDevice, with_connection=SerialConnection)
@balder.for_vdevice(MessengerFeature.OtherVDevice, with_connections=SerialConnection)
def send(msg) -> None:
serial = MySerial(com=..)
...

@balder.for_vdevice(MessengerFeature.OtherVDevice, with_connection=TcpConnection)
@balder.for_vdevice(MessengerFeature.OtherVDevice, with_connections=TcpConnection)
def send(msg) -> None:
sock = socket.socket(...)
...
Expand All @@ -419,7 +419,7 @@ So take a look at the following :class:`Setup`, that matches our :class:`Scenari

class MySetup(balder.Setup):

@balder.connect(SlaveDevice, over_connection=balder.Connection.based_on(SerialConnection, TcpConnection))
@balder.connect(SlaveDevice, over_connection=SerialConnection | TcpConnection)
class MainDevice(balder.Device):
msg = SetupSendMessengerFeature()

Expand All @@ -429,7 +429,7 @@ So take a look at the following :class:`Setup`, that matches our :class:`Scenari
This example connects the two relevant devices over a :class:`TcpConnection` with each other, because the scenario
defines, that the devices should be connected over an TcpConnection. If the test
now uses on of our methods ``MyMessengerFeature.send(..)``, the variation with the decorator
``@balder.for_vdevice(..., over_connection=[TcpConnection])`` will be used.
``@balder.for_vdevice(..., over_connection=TcpConnection)`` will be used.

If one would exchange the connection with the ``SerialConnection``, Balder would select the method variation with the
decorator ``@balder.for_vdevice(MessengerFeature.OtherVDevice, with_connection=SerialConnection)``.
Expand Down
6 changes: 3 additions & 3 deletions doc/source/basics/scenarios.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ You can also define some by your own.
In addition to define single connections, you can also select a part of the global connection tree or combine some
connections with an OR or an AND relationship. So for example you could connect our devices and allow an Ethernet as
well as a Serial connection, by defining
``@balder.connect(SendDevice, over_connection=Connection.based_on(MyEthernet, MySerial))``. Of course you could also
define, that you need both, the Serial and the Ethernet connection. This can be done by using tuples:
``@balder.connect(SendDevice, over_connection=Connection.based_on((MyEthernet, MySerial)))``
``@balder.connect(SendDevice, over_connection=MyEthernet | MySerial)``. Of course you could also
define, that you need both, the Serial and the Ethernet connection. This can be done with:
``@balder.connect(SendDevice, over_connection=MyEthernet & MySerial``

In our example we only define that we want a universal :class:`Connection` between our devices ``SendDevice`` and
``RecvDevice``. With this the connection type doesn't matter and every connection works here.
Expand Down
6 changes: 3 additions & 3 deletions doc/source/basics/vdevices.rst
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ Let's go back to an easy scenario which only has one single vDevice:
class Receiver(balder.Device):
recv = RecvFeature()

@balder.connect(with_device=Receiver, over_connection=balder.Connection.based_on(SmsConnection, EMailConnection))
@balder.connect(with_device=Receiver, over_connection=SmsConnection | EMailConnection)
class Sender(balder.Device):
send = SendFeature(receiver=ScenarioSendMessage.Receiver)

Expand Down Expand Up @@ -405,11 +405,11 @@ all of that with our two features ``SendFeature`` and ``RecvFeature``:
send_to_recv1 = SendFeature(receiver='Receiver1')
send_to_recv2 = SendFeature(receiver='Receiver2')

@balder.connect(with_device=Sender, over_connection=balder.Connection.based_on(SmsConnection, EMailConnection))
@balder.connect(with_device=Sender, over_connection=SmsConnection | EMailConnection)
class Receiver1(balder.Device):
recv = RecvFeature()

@balder.connect(with_device=Sender, over_connection=balder.Connection.based_on(SmsConnection, EMailConnection))
@balder.connect(with_device=Sender, over_connection=SmsConnection | EMailConnection)
class Receiver2(balder.Device):
recv = RecvFeature()

Expand Down
89 changes: 87 additions & 2 deletions src/_balder/cnnrelations/and_connection_relation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

import itertools

from .base_connection_relation import BaseConnectionRelation
from .base_connection_relation import BaseConnectionRelation, BaseConnectionRelationT

if TYPE_CHECKING:
from ..connection import Connection
from ..connection import List, Union, Connection
from .or_connection_relation import OrConnectionRelation


Expand Down Expand Up @@ -62,3 +62,88 @@ def get_simplified_relation(self) -> OrConnectionRelation:
if not self_simplified_or_relations:
all_new_ands.append(and_template)
return OrConnectionRelation(*all_new_ands)

def is_single(self) -> bool:

if len(self._connections) == 0:
return True

return min(cnn.is_single() for cnn in self._connections)

def get_singles(self) -> List[Connection]:
from ..connection import Connection # pylint: disable=import-outside-toplevel

singles_and_relations = ()
for cur_elem in self._connections:
# get all singles of this AND relation element
singles_and_relations += (cur_elem.get_singles(),)
# now get the variations and add them to our results
return [
Connection.based_on(AndConnectionRelation(*cur_tuple))
for cur_tuple in itertools.product(*singles_and_relations)
]

def cut_into_all_possible_subtree_branches(self) -> List[AndConnectionRelation]:
if not self.is_single():
raise ValueError('can not execute method, because relation is not single')

tuple_with_all_possibilities = (
tuple(cur_tuple_item.cut_into_all_possible_subtree_branches() for cur_tuple_item in self._connections))

cloned_tuple_list = []
for cur_tuple in list(itertools.product(*tuple_with_all_possibilities)):
cloned_tuple = AndConnectionRelation(*[cur_tuple_item.clone() for cur_tuple_item in cur_tuple])
cloned_tuple_list.append(cloned_tuple)
return cloned_tuple_list

def contained_in(self, other_conn: Union[Connection, BaseConnectionRelationT], ignore_metadata=False) -> bool:
# This method checks if the AND relation is contained in the `other_conn`. To ensure that an AND relation is
# contained in a connection tree, there has to be another AND relation into the `other_conn`, that has the same
# length or is bigger. In addition, there has to exist an order combination where every element of the this AND
# relation is contained in the found AND relation of the `other_cnn`. In this case it doesn't matter where the
# AND relation is in `other_elem` (will be converted to single, and AND relation will be searched in all
# BASED_ON elements). If the AND relation of `other_conn` has fewer items than this AND relation, it will be
# ignored. The method only search for a valid existing item in the `other_conn` AND relation for every item of
# this AND relation.
from ..connection import Connection # pylint: disable=import-outside-toplevel

if not self.is_resolved():
raise ValueError('can not execute method, because connection relation is not resolved')
if not other_conn.is_resolved():
raise ValueError('can not execute method, because other connection relation is not resolved')

if isinstance(other_conn, BaseConnectionRelation):
other_conn = Connection.based_on(other_conn)

self_singles = self.get_singles()
other_singles = other_conn.get_singles()

for cur_self_single, cur_other_single in itertools.product(self_singles, other_singles):
# check if we can find an AND relation in the other object -> go the single connection upwards and
# search for a `AndConnectionRelation`

# self is a container connection -> use raw inner AND list
cur_self_single_and_relation = cur_self_single.based_on_elements.connections[0]

cur_sub_other_single = cur_other_single
while cur_sub_other_single is not None:
if isinstance(cur_sub_other_single, AndConnectionRelation):
# found an AND relation -> check if length does match
if len(cur_sub_other_single) < len(cur_self_single_and_relation):
# this complete element is not possible - skip this single!
break
# length is okay, no check if every single element is contained in one of this tuple
for cur_inner_self_elem in cur_self_single_and_relation.connections:

if not cur_inner_self_elem.contained_in(cur_sub_other_single,
ignore_metadata=ignore_metadata):
# at least one element is not contained in other AND relation - this complete element
# is not possible - skip this single!
break
# all items are contained in the current other AND relation -> match
return True
# go further up, if this element is no AND relation
cur_sub_other_single = cur_sub_other_single.based_on_elements[0] \
if cur_sub_other_single.based_on_elements else None

return False
Loading