baldertest 0.1.0b11__py3-none-any.whl → 0.1.0b13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. _balder/_version.py +8 -3
  2. _balder/cnnrelations/and_connection_relation.py +28 -1
  3. _balder/cnnrelations/base_connection_relation.py +1 -1
  4. _balder/collector.py +101 -113
  5. _balder/connection.py +76 -80
  6. _balder/controllers/device_controller.py +17 -29
  7. _balder/controllers/feature_controller.py +2 -2
  8. _balder/controllers/normal_scenario_setup_controller.py +1 -2
  9. _balder/controllers/scenario_controller.py +121 -0
  10. _balder/controllers/vdevice_controller.py +2 -23
  11. _balder/decorator_connect.py +3 -3
  12. _balder/decorator_covered_by.py +28 -36
  13. _balder/executor/basic_executable_executor.py +13 -6
  14. _balder/executor/basic_executor.py +31 -4
  15. _balder/executor/executor_tree.py +5 -4
  16. _balder/executor/parametrized_testcase_executor.py +2 -2
  17. _balder/executor/scenario_executor.py +12 -71
  18. _balder/executor/setup_executor.py +4 -3
  19. _balder/executor/testcase_executor.py +35 -19
  20. _balder/executor/unresolved_parametrized_testcase_executor.py +32 -57
  21. _balder/executor/variation_executor.py +127 -148
  22. _balder/feature.py +2 -1
  23. _balder/feature_replacement_mapping.py +107 -0
  24. _balder/feature_vdevice_mapping.py +88 -0
  25. _balder/fixture_manager.py +1 -1
  26. _balder/fixture_metadata.py +1 -1
  27. _balder/routing_path.py +27 -16
  28. _balder/scenario.py +2 -2
  29. _balder/setup.py +2 -1
  30. _balder/testresult.py +4 -3
  31. _balder/utils/__init__.py +0 -0
  32. _balder/{utils.py → utils/functions.py} +29 -31
  33. _balder/utils/inner_device_managing_metaclass.py +14 -0
  34. _balder/utils/mixin_can_be_covered_by_executor.py +24 -0
  35. _balder/utils/typings.py +4 -0
  36. {baldertest-0.1.0b11.dist-info → baldertest-0.1.0b13.dist-info}/METADATA +3 -2
  37. {baldertest-0.1.0b11.dist-info → baldertest-0.1.0b13.dist-info}/RECORD +41 -35
  38. {baldertest-0.1.0b11.dist-info → baldertest-0.1.0b13.dist-info}/WHEEL +1 -1
  39. {baldertest-0.1.0b11.dist-info → baldertest-0.1.0b13.dist-info}/entry_points.txt +0 -0
  40. {baldertest-0.1.0b11.dist-info → baldertest-0.1.0b13.dist-info/licenses}/LICENSE +0 -0
  41. {baldertest-0.1.0b11.dist-info → baldertest-0.1.0b13.dist-info}/top_level.txt +0 -0
_balder/_version.py CHANGED
@@ -1,8 +1,13 @@
1
- # file generated by setuptools_scm
1
+ # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
3
6
  TYPE_CHECKING = False
4
7
  if TYPE_CHECKING:
5
- from typing import Tuple, Union
8
+ from typing import Tuple
9
+ from typing import Union
10
+
6
11
  VERSION_TUPLE = Tuple[Union[int, str], ...]
7
12
  else:
8
13
  VERSION_TUPLE = object
@@ -12,5 +17,5 @@ __version__: str
12
17
  __version_tuple__: VERSION_TUPLE
13
18
  version_tuple: VERSION_TUPLE
14
19
 
15
- __version__ = version = '0.1.0b11'
20
+ __version__ = version = '0.1.0b13'
16
21
  __version_tuple__ = version_tuple = (0, 1, 0)
@@ -1,5 +1,5 @@
1
1
  from __future__ import annotations
2
- from typing import TYPE_CHECKING
2
+ from typing import TYPE_CHECKING, Type
3
3
 
4
4
  import itertools
5
5
 
@@ -19,6 +19,33 @@ class AndConnectionRelation(BaseConnectionRelation):
19
19
  based_on_strings = [cur_elem.get_tree_str() for cur_elem in self._connections]
20
20
  return f"({' | '.join(based_on_strings)})"
21
21
 
22
+ def get_possibilities_for_direct_parent_cnn(self, for_cnn_class: Type[Connection]) -> list[AndConnectionRelation]:
23
+ """
24
+ Helper method that returns a list of possible :class:`AndConnectionRelation` elements that hold the next parent
25
+ in the connection-tree that can be there instead of the origin connection.
26
+ The method returns a list, because there can exist different possibilities for this AND connection.
27
+ """
28
+ direct_ancestors_relations = ()
29
+ for cur_and_elem in self.connections:
30
+ # `cur_and_elem` needs to be a connection, because we are using simplified which has only
31
+ # `OR[AND[Cnn, ...], Cnn, ..]`
32
+ if cur_and_elem.__class__ in for_cnn_class.get_parents():
33
+ # element already is a direct ancestor
34
+ direct_ancestors_relations += (cur_and_elem,)
35
+ else:
36
+ all_pos_possibilities = []
37
+ # add all possible direct parents to the possibilities list
38
+ for cur_direct_parent in for_cnn_class.get_parents():
39
+ if cur_direct_parent.is_parent_of(cur_and_elem.__class__):
40
+ all_pos_possibilities.append(cur_direct_parent.based_on(cur_and_elem))
41
+ direct_ancestors_relations += (all_pos_possibilities,)
42
+ # resolve the opportunities and create multiple possible AND relations where all elements are
43
+ # direct parents
44
+ return [
45
+ AndConnectionRelation(*cur_possibility).get_resolved()
46
+ for cur_possibility in itertools.product(*direct_ancestors_relations)
47
+ ]
48
+
22
49
  def get_simplified_relation(self) -> OrConnectionRelation:
23
50
  from ..connection import Connection # pylint: disable=import-outside-toplevel
24
51
  from .or_connection_relation import OrConnectionRelation # pylint: disable=import-outside-toplevel
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
  from typing import List, Union, Type, Dict, TypeVar, TYPE_CHECKING
3
3
  from abc import ABC, abstractmethod
4
- from ..utils import cnn_type_check_and_convert
4
+ from _balder.utils.functions import cnn_type_check_and_convert
5
5
 
6
6
  if TYPE_CHECKING:
7
7
  from ..connection import Connection
_balder/collector.py CHANGED
@@ -10,7 +10,7 @@ import inspect
10
10
  import pathlib
11
11
  import functools
12
12
  import importlib.util
13
- from _balder.utils import get_class_that_defines_method, inspect_method
13
+ from _balder.utils.functions import get_class_that_defines_method, get_method_type
14
14
  from _balder.setup import Setup
15
15
  from _balder.device import Device
16
16
  from _balder.feature import Feature
@@ -23,7 +23,8 @@ from _balder.fixture_execution_level import FixtureExecutionLevel
23
23
  from _balder.controllers import ScenarioController, SetupController, DeviceController, VDeviceController, \
24
24
  FeatureController, NormalScenarioSetupController
25
25
  from _balder.exceptions import DuplicateForVDeviceError, UnknownVDeviceException
26
- from _balder.utils import get_scenario_inheritance_list_of
26
+ from _balder.utils.functions import get_scenario_inheritance_list_of
27
+ from _balder.utils.typings import MethodLiteralType
27
28
 
28
29
  if TYPE_CHECKING:
29
30
  from _balder.plugin_manager import PluginManager
@@ -62,8 +63,13 @@ class Collector:
62
63
  sys.path.insert(0, str(self.working_dir.parent.absolute()))
63
64
 
64
65
  self._all_py_files: Union[List[pathlib.Path], None] = None
66
+
67
+ self._all_collected_scenarios: Union[List[Type[Scenario]], None] = None
68
+ self._all_collected_setups: Union[List[Type[Setup]], None] = None
69
+
65
70
  self._all_scenarios: Union[List[Type[Scenario]], None] = None
66
71
  self._all_setups: Union[List[Type[Setup]], None] = None
72
+
67
73
  self._all_connections: Union[List[Type[Connection]], None] = None
68
74
 
69
75
  self.balderglob_was_loaded = False
@@ -123,6 +129,20 @@ class Collector:
123
129
  raise AttributeError("please call the `collect()` method before omitting this value")
124
130
  return self._all_py_files
125
131
 
132
+ @property
133
+ def all_collected_scenarios(self) -> List[Type[Scenario]]:
134
+ """returns a list of all collected scenarios that were found by the collector"""
135
+ if self._all_collected_scenarios is None:
136
+ raise AttributeError("please call the `collect()` method before omitting this value")
137
+ return self._all_collected_scenarios
138
+
139
+ @property
140
+ def all_collected_setups(self) -> List[Type[Setup]]:
141
+ """returns a list of all collected setups that were found by the collector"""
142
+ if self._all_collected_setups is None:
143
+ raise AttributeError("please call the `collect()` method before omitting this value")
144
+ return self._all_collected_setups
145
+
126
146
  @property
127
147
  def all_scenarios(self) -> List[Type[Scenario]]:
128
148
  """returns a list of all scenarios that were found by the collector"""
@@ -154,6 +174,29 @@ class Collector:
154
174
  raise AttributeError("please call the `collect()` method before omitting this value")
155
175
  return self._all_connections
156
176
 
177
+ def get_class_and_method_type_for(self, func) -> Tuple[Union[type, None], MethodLiteralType]:
178
+ """
179
+ This helper function returns the related class and the type of the method (`staticmethod`, `classmethod`,
180
+ `instancemethod` or `function`) as tuple.
181
+ """
182
+
183
+ available_classes = self.all_collected_scenarios + self.all_collected_setups
184
+ available_classes_with_mro = []
185
+ for cur_class in available_classes:
186
+ available_classes_with_mro.extend([*inspect.getmro(cur_class)])
187
+ available_classes_with_mro = set(available_classes_with_mro)
188
+
189
+ qualname = func.__qualname__
190
+
191
+ if '.' not in qualname:
192
+ return None, 'function'
193
+
194
+ expected_class_name = qualname.rpartition('.')[0]
195
+ for cur_class in available_classes_with_mro:
196
+ if cur_class.__qualname__ == expected_class_name:
197
+ return cur_class, get_method_type(cur_class, func)
198
+ raise ValueError(f'function {func.__qualname__} is not part of any scenario or setup')
199
+
157
200
  def get_fixture_manager(self) -> FixtureManager:
158
201
  """
159
202
  Resolves all fixtures and returns the fixture manager for this session
@@ -164,7 +207,7 @@ class Collector:
164
207
  cur_level = FixtureExecutionLevel(cur_level_as_str)
165
208
  resolved_dict[cur_level] = {}
166
209
  for cur_fn in cur_module_fixture_dict:
167
- cls, func_type = inspect_method(cur_fn)
210
+ cls, func_type = self.get_class_and_method_type_for(cur_fn)
168
211
  # mechanism also works for balderglob fixtures (`func_type` is 'function' and `cls` is None)
169
212
  if cls not in resolved_dict[cur_level].keys():
170
213
  resolved_dict[cur_level][cls] = []
@@ -352,120 +395,62 @@ class Collector:
352
395
  result.append(cur_class)
353
396
  return result
354
397
 
355
- def set_run_skip_ignore_of_test_methods_in_scenarios(self):
356
- """
357
- This method secures that the scenario classes have a valid RUN, SKIP and IGNORE attribute.
358
- """
398
+ def _validate_skip_ignore(self):
399
+ """
400
+ This method secures that the scenario classes have a valid SKIP and IGNORE attribute.
401
+ """
402
+ ignore_str = "IGNORE"
403
+ skip_str = "SKIP"
404
+
405
+ def validate(scenario_class, list_attr_name: str):
406
+ # IGNORE/SKIP is mentioned in this specific class
407
+ if not isinstance(getattr(scenario_class, list_attr_name), list):
408
+ raise TypeError(f"the class attribute `{cur_class.__name__}.{list_attr_name}` has to be from type list")
409
+ # check that all elements that are mentioned here exists as a valid test method
410
+ for method in getattr(scenario_class, list_attr_name):
411
+ if isinstance(method, str):
412
+ if not hasattr(scenario_class, method):
413
+ raise ValueError(f'can not find the mentioned attribute `{method}` from '
414
+ f'{list_attr_name} list for scenario `{scenario_class}`')
415
+ if not method.startswith('test_'):
416
+ raise ValueError(f'the attribute `{method}` mentioned in {list_attr_name} list of scenario '
417
+ f'`{scenario_class}` is no valid test method and can not be used here')
418
+ elif not inspect.ismethod(method) or not method.__name__.startswith('test_'):
419
+ raise TypeError(f"the given element {method.__name__} for class attribute "
420
+ f"`{scenario_class.__name__}.{list_attr_name}` is no valid test method")
421
+ return True
422
+
359
423
  for cur_scenario in self._all_scenarios:
360
424
  # determines hierarchy of inherited classes
361
425
  base_classes = get_scenario_inheritance_list_of(cur_scenario)
362
426
  # removes the last scenario class
363
427
  base_classes = base_classes[:-1]
364
- # now determine all RUN, SKIP and IGNORE values if they aren't already mentioned - if there exists a value
428
+ # now determine all SKIP and IGNORE values if they aren't already mentioned - if there exists a value
365
429
  # for them, check if the value is valid
366
430
  base_classes.reverse()
431
+ # now go through all RUN/SKIP/IGNORE values and check if the values are correct here
367
432
  for cur_idx, cur_class in enumerate(base_classes):
368
433
  next_parent = None if cur_idx == 0 else base_classes[cur_idx - 1]
369
- if "IGNORE" in cur_class.__dict__.keys():
370
- # IGNORE is mentioned in this specific class
371
- if not isinstance(cur_class.IGNORE, list):
372
- raise TypeError(f"the class attribute `{cur_class.__name__}.IGNORE` has to be from type list")
373
- # check that all elements that are mentioned here exists in the parent's RUN, SKIP or IGNORE class
374
- # variable or are defined in this class
375
- for cur_ignore_method in cur_class.IGNORE:
376
- if not inspect.ismethod(cur_ignore_method) or \
377
- not cur_ignore_method.__name__.startswith('test_'):
378
- raise TypeError(f"the given element {cur_ignore_method.__name__} for class attribute "
379
- f"`{cur_class.__name__}.IGNORE` is no valid test method")
380
- if cur_ignore_method not in cur_class.__dict__.values() and next_parent is None or \
381
- cur_ignore_method not in cur_class.__dict__.values() and next_parent is not None \
382
- and cur_ignore_method not in next_parent.RUN + next_parent.SKIP + next_parent.IGNORE:
383
- raise ValueError(f"the element `{cur_ignore_method.__name__}` given at class attribute "
384
- f"`{cur_class.__name__}.IGNORE` is not a test method of this scenario")
385
- else:
386
- # IGNORE is not mentioned in this specific class -> so add an empty list for further access
387
- cur_class.IGNORE = []
388
- if "SKIP" in cur_class.__dict__.keys():
389
- # SKIP is mentioned in this specific class
390
- if not isinstance(cur_class.SKIP, list):
391
- raise TypeError(f"the class attribute `{cur_class.__name__}.SKIP` has to be from type list")
392
- # check that all elements that are mentioned here exist in the parent's RUN or SKIP class variable
393
- # or are defined in this class
394
- for cur_skip_method in cur_class.SKIP:
395
- if not inspect.ismethod(cur_skip_method) or \
396
- not cur_skip_method.__name__.startswith('test_'):
397
- raise TypeError(f"the given element {cur_skip_method.__name__} for class attribute "
398
- f"`{cur_class.__name__}.IGNORE` is no valid test method")
399
- if cur_skip_method not in cur_class.__dict__.values():
400
- if next_parent is None:
401
- raise ValueError(f"the element `{cur_skip_method.__name__}` given at class attribute "
402
- f"`{cur_class.__name__}.SKIP` is not a test method of this scenario")
403
-
404
- if cur_skip_method in next_parent.IGNORE:
405
- raise ValueError(f"the element `{cur_skip_method.__name__}` given at class "
406
- f"attribute `{cur_class.__name__}.SKIP` was already added to "
407
- f"IGNORE in a higher parent class - not possible to add it now to "
408
- f"SKIP")
409
-
410
- if cur_skip_method not in next_parent.SKIP and cur_skip_method not in next_parent.IGNORE:
411
- raise ValueError(f"the element `{cur_skip_method.__name__}` given at class "
412
- f"attribute `{cur_class.__name__}.SKIP` is not a test method of "
413
- f"this scenario")
414
- else:
415
- # SKIP is not mentioned in this specific class -> so add an empty list for further access
416
- cur_class.SKIP = []
417
- if "RUN" in cur_class.__dict__.keys():
418
- # RUN is mentioned in this specific class
419
- if not isinstance(cur_class.RUN, list):
420
- raise TypeError(f"the class attribute `{cur_class.__name__}.RUN` has to be from type list")
421
- # check that all elements that are mentioned here exists in the parent's RUN class variable or are
422
- # defined in this class
423
- for cur_run_method in cur_class.RUN:
424
- if not inspect.ismethod(cur_run_method) or \
425
- not cur_run_method.__name__.startswith('test_'):
426
- raise TypeError(f"the given element {cur_run_method.__name__} for class attribute "
427
- f"`{cur_class.__name__}.RUN` is no valid test method")
428
- if cur_run_method not in cur_class.__dict__.values():
429
- if next_parent is None:
430
- raise ValueError(
431
- f"the element `{cur_run_method.__name__}` given at class attribute "
432
- f"`{cur_class.__name__}.RUN` is not a test method of this scenario")
433
-
434
- if cur_run_method in next_parent.IGNORE:
435
- raise ValueError(
436
- f"the element `{cur_run_method.__name__}` given at class attribute "
437
- f"`{cur_class.__name__}.RUN` was already added to IGNORE in a higher parent "
438
- f"class - not possible to add it now to RUN")
439
- if cur_run_method in next_parent.SKIP:
440
- raise ValueError(
441
- f"the element `{cur_run_method.__name__}` given at class attribute "
442
- f"`{cur_class.__name__}.RUN` was already added to SKIP in a higher parent "
443
- f"class - not possible to add it now to RUN")
444
- if cur_run_method not in next_parent.RUN:
445
- raise ValueError(
446
- f"the element `{cur_run_method.__name__}` given at class attribute "
447
- f"`{cur_class.__name__}.RUN` is not a test method of this scenario")
448
- else:
449
- # RUN is not mentioned in this specific class -> so add an empty list for further access
450
- cur_class.RUN = []
451
-
452
- # also add all in this class defined test methods to this RUN list
453
- for cur_item_name, cur_item in cur_class.__dict__.items():
454
- if cur_item_name.startswith("test_") and inspect.ismethod(cur_item):
455
- cur_class.RUN.append(cur_item)
456
- # now add all items from parent classes to the lists that are not mentioned yet
457
- if next_parent is not None:
458
- for cur_parent_run_method in next_parent.RUN:
459
- if cur_parent_run_method not in cur_class.RUN and cur_parent_run_method not in cur_class.SKIP \
460
- and cur_parent_run_method not in cur_class.IGNORE:
461
- cur_class.RUN.append(cur_parent_run_method)
462
- for cur_parent_skip_method in next_parent.SKIP:
463
- if cur_parent_skip_method not in cur_class.SKIP \
464
- and cur_parent_skip_method not in cur_class.IGNORE:
465
- cur_class.SKIP.append(cur_parent_skip_method)
466
- for cur_parent_ignore_method in next_parent.IGNORE:
467
- if cur_parent_ignore_method not in cur_class.IGNORE:
468
- cur_class.IGNORE.append(cur_parent_ignore_method)
434
+
435
+ validate(cur_class, 'IGNORE')
436
+ validate(cur_class, 'SKIP')
437
+
438
+ # make sure that the method is not mentioned in IGNORE and in SKIP
439
+ ignore_list = cur_class.__dict__.get(ignore_str, [])
440
+ skip_list = cur_class.__dict__.get(skip_str, [])
441
+ for cur_ignore_method_as_str in ignore_list:
442
+ if cur_ignore_method_as_str in skip_list:
443
+ raise ValueError(f'mentioned test method `{cur_ignore_method_as_str}` is in '
444
+ f'`{cur_class.__name__}.IGNORE` and `{cur_class.__name__}.SKIP`')
445
+ # make sure that the skip method was not defined in IGNORE in parent classes
446
+ if next_parent:
447
+ for cur_skip_method_as_str in skip_list:
448
+ cur_skip_method = getattr(cur_class, cur_skip_method_as_str)
449
+ if cur_skip_method in ScenarioController.get_for(next_parent).get_ignore_test_methods():
450
+ raise ValueError(f"the element `{cur_skip_method.__name__}` given at class "
451
+ f"attribute `{cur_class.__name__}.SKIP` was already added to "
452
+ f"IGNORE in a higher parent class - not possible to add it now to "
453
+ f"SKIP")
469
454
 
470
455
  @staticmethod
471
456
  def rework_method_variation_decorators():
@@ -829,12 +814,16 @@ class Collector:
829
814
  self._all_connections = self.get_all_connection_classes()
830
815
 
831
816
  # collect all `Scenario` classes
832
- self._all_scenarios = self.get_all_scenario_classes(py_file_paths=all_scenario_filepaths, filter_abstracts=True)
817
+ self._all_collected_scenarios = self.get_all_scenario_classes(
818
+ py_file_paths=all_scenario_filepaths, filter_abstracts=True
819
+ )
833
820
  # collect all `Setup` classes
834
- self._all_setups = self.get_all_setup_classes(py_file_paths=all_setup_filepaths, filter_abstracts=True)
821
+ self._all_collected_setups = self.get_all_setup_classes(
822
+ py_file_paths=all_setup_filepaths, filter_abstracts=True
823
+ )
835
824
 
836
825
  self._all_scenarios, self._all_setups = plugin_manager.execute_collected_classes(
837
- scenarios=self._all_scenarios, setups=self._all_setups)
826
+ scenarios=self._all_collected_scenarios, setups=self._all_collected_setups)
838
827
 
839
828
  self._all_scenarios = Collector.filter_parent_classes_of(items=self._all_scenarios)
840
829
  self._all_setups = Collector.filter_parent_classes_of(items=self._all_setups)
@@ -847,8 +836,7 @@ class Collector:
847
836
  self._set_original_device_features()
848
837
  self._exchange_strings_with_objects()
849
838
 
850
- # todo move this implementation in related controller
851
- self.set_run_skip_ignore_of_test_methods_in_scenarios()
839
+ self._validate_skip_ignore()
852
840
 
853
841
  self._validate_scenario_and_setups()
854
842
 
_balder/connection.py CHANGED
@@ -8,7 +8,7 @@ from _balder.connection_metadata import ConnectionMetadata
8
8
  from _balder.device import Device
9
9
  from _balder.exceptions import IllegalConnectionTypeError
10
10
  from _balder.cnnrelations import AndConnectionRelation, OrConnectionRelation
11
- from _balder.utils import cnn_type_check_and_convert
11
+ from _balder.utils.functions import cnn_type_check_and_convert
12
12
 
13
13
 
14
14
  class ConnectionType(type):
@@ -100,7 +100,7 @@ class Connection(metaclass=ConnectionType):
100
100
 
101
101
  :param tree_name: the tree name the parents should be returned (default: use tree defined in `GlobalSetting`)
102
102
  """
103
- from _balder.balder_session import BalderSession
103
+ from _balder.balder_session import BalderSession # pylint: disable=import-outside-toplevel
104
104
  if tree_name is None:
105
105
  tree_name = BalderSession.get_current_active_global_conntree_name()
106
106
  conn_dict = Connection.__parents.get(cls, {})
@@ -241,6 +241,62 @@ class Connection(metaclass=ConnectionType):
241
241
 
242
242
  # ---------------------------------- PROTECTED METHODS -------------------------------------------------------------
243
243
 
244
+ def _is_directly_contained_in(self, other_conn: Connection, ignore_metadata: bool) -> bool:
245
+ """
246
+ Helper method that returns true if this connection is directly contained in the `other_conn`.
247
+
248
+ .. note::
249
+ 'Directly contained-in' means that the resolved versions of both connections are of the same connection
250
+ type, and the elements of that connection are directly embedded within one another from that point onward.
251
+ It does not check deeper into the other_conn.
252
+
253
+ :param other_conn: the other connection
254
+ :param ignore_metadata: True, if the metadata should be ignored
255
+ """
256
+
257
+ resolved_self = self.get_resolved()
258
+ resolved_other = other_conn.get_resolved()
259
+
260
+ if resolved_self.__class__ == Connection and len(resolved_self.based_on_elements) == 0 or \
261
+ resolved_other.__class__ == Connection and len(resolved_other.based_on_elements) == 0:
262
+ # one of the resolved object is a raw `Connection` object without based-on-elements -> always true
263
+ return True
264
+
265
+ if resolved_self.__class__ == Connection:
266
+ return resolved_self.based_on_elements.contained_in(resolved_other, ignore_metadata=ignore_metadata)
267
+
268
+ if resolved_self.__class__ == resolved_other.__class__:
269
+ # The element itself has already matched, now we still have to check whether at least one inner element
270
+ # of this type is contained in minimum one element of the other
271
+ singles_self = resolved_self.get_singles()
272
+ singles_other = other_conn.get_singles()
273
+
274
+ # check that for one single_self element all hierarchical based_on elements are in one of the single
275
+ # other element
276
+ for cur_single_self, cur_single_other in itertools.product(singles_self, singles_other):
277
+ # check if both consists of only one element
278
+ if len(cur_single_self.based_on_elements) == 0:
279
+ # the cur self single is only one element -> this is contained in the other
280
+ return True
281
+
282
+ if len(cur_single_other.based_on_elements) == 0:
283
+ # the other element is only one element, but the self element not -> contained_in
284
+ # for this single definitely false
285
+ continue
286
+
287
+ # note: for both only one `based_on_elements` is possible, because they are singles
288
+ self_first_basedon = cur_single_self.based_on_elements[0]
289
+ other_first_basedon = cur_single_other.based_on_elements[0]
290
+
291
+ if isinstance(self_first_basedon, Connection) and \
292
+ isinstance(other_first_basedon, AndConnectionRelation):
293
+ continue
294
+
295
+ # find a complete valid match
296
+ if self_first_basedon.contained_in(other_first_basedon, ignore_metadata=ignore_metadata):
297
+ return True
298
+ return False
299
+
244
300
  # ---------------------------------- METHODS -----------------------------------------------------------------------
245
301
 
246
302
  def get_intersection_with_other_single(
@@ -516,25 +572,8 @@ class Connection(metaclass=ConnectionType):
516
572
  for next_higher_parent in simplified_based_on:
517
573
  if isinstance(next_higher_parent, AndConnectionRelation):
518
574
  # determine all possibilities
519
- direct_ancestors_relations = ()
520
- for cur_and_elem in next_higher_parent:
521
- # `cur_and_elem` needs to be a connection, because we are using simplified which has only
522
- # `OR[AND[Cnn, ...], Cnn, ..]`
523
- if cur_and_elem.__class__ in self.__class__.get_parents():
524
- # element already is a direct ancestor
525
- direct_ancestors_relations += (cur_and_elem, )
526
- else:
527
- all_pos_possibilities = []
528
- # add all possible direct parents to the possibilities list
529
- for cur_direct_parent in self.__class__.get_parents():
530
- if cur_direct_parent.is_parent_of(cur_and_elem.__class__):
531
- all_pos_possibilities.append(cur_direct_parent.based_on(cur_and_elem))
532
- direct_ancestors_relations += (all_pos_possibilities, )
533
- # resolve the opportunities and create multiple possible AND relations where all elements are
534
- # direct parents
535
- for cur_possibility in itertools.product(*direct_ancestors_relations):
536
- new_child_relation = AndConnectionRelation(*cur_possibility).get_resolved()
537
- copied_base.append_to_based_on(new_child_relation)
575
+ for new_and_relation in next_higher_parent.get_possibilities_for_direct_parent_cnn(self.__class__):
576
+ copied_base.append_to_based_on(new_and_relation)
538
577
  else:
539
578
  # `next_higher_parent` needs to be a connection, because we are using simplified which has only
540
579
  # `OR[AND[Cnn, ...], Cnn, ..]`
@@ -673,9 +712,10 @@ class Connection(metaclass=ConnectionType):
673
712
  ignore_metadata=False
674
713
  ) -> bool:
675
714
  """
676
- This method helps to find out whether this connection-tree fits within another connection tree. A connection
677
- object is a certain part of the large connection tree that Balder has at its disposal. This method checks
678
- whether a possibility of this connection tree fits in one possibility of the given connection tree.
715
+ This method determines if one connection tree can be embedded within another connection tree. A connection
716
+ object represents a specific segment of the extensive connection tree managed by Balder. The method evaluates
717
+ whether at least one SINGLE connection object of this connection tree fits within a possible SINGLE connection
718
+ of the provided connection `other_conn`.
679
719
 
680
720
  .. note::
681
721
  The method returns true if one single connection of this object fits in another single connection that is
@@ -698,70 +738,26 @@ class Connection(metaclass=ConnectionType):
698
738
  resolved_self = self.get_resolved()
699
739
  resolved_other = other_conn.get_resolved()
700
740
 
701
- if resolved_self.__class__ == Connection and len(resolved_self.based_on_elements) == 0 or \
702
- resolved_other.__class__ == Connection and len(resolved_other.based_on_elements) == 0:
703
- # one of the resolved object is a raw `Connection` object without based-on-elements -> always true
741
+ if self._is_directly_contained_in(resolved_other, ignore_metadata=ignore_metadata):
704
742
  return True
705
743
 
706
- if resolved_self.__class__ == Connection:
707
- return resolved_self.based_on_elements.contained_in(resolved_other, ignore_metadata=ignore_metadata)
744
+ # the elements itself do not match -> go deeper within the other connection
708
745
 
709
- if resolved_self.__class__ == resolved_other.__class__:
710
- # The element itself has already matched, now we still have to check whether at least one inner element
711
- # of this type is contained in the minimum one element of the other
712
- singles_self = resolved_self.get_singles()
713
- singles_other = resolved_other.get_singles()
746
+ resolved_other_relation = resolved_other.based_on_elements \
747
+ if isinstance(resolved_other, Connection) else resolved_other
714
748
 
715
- # check that for one single_self element all hierarchical based_on elements are in one of the single
716
- # other element
717
- for cur_single_self, cur_single_other in itertools.product(singles_self, singles_other):
718
- # check if both consists of only one element
719
- if len(cur_single_self.based_on_elements) == 0:
720
- # the cur self single is only one element -> this is contained in the other
721
- return True
722
-
723
- if len(cur_single_other.based_on_elements) == 0:
724
- # the other element is only one element, but the self element not -> contained_in
725
- # for this single definitely false
726
- continue
727
-
728
- # note: for both only one `based_on_elements` is possible, because they are singles
729
- self_first_based_on = cur_single_self.based_on_elements[0]
730
- other_first_based_on = cur_single_other.based_on_elements[0]
731
-
732
- self_is_and = isinstance(self_first_based_on, AndConnectionRelation)
733
- self_is_cnn = isinstance(self_first_based_on, Connection)
734
- other_is_and = isinstance(other_first_based_on, AndConnectionRelation)
735
- other_is_cnn = isinstance(other_first_based_on, Connection)
736
-
737
- if self_is_and and (other_is_and or other_is_cnn) or self_is_cnn and other_is_cnn:
738
- # find a complete valid match
739
- if self_first_based_on.contained_in(other_first_based_on, ignore_metadata=ignore_metadata):
740
- return True
741
- # skip all others possibilities
742
- else:
743
- # the elements itself do not match -> go deeper within the other connection
744
- if isinstance(resolved_other, AndConnectionRelation):
749
+ for cur_other_based_on in resolved_other_relation.connections:
750
+ # `cur_other_based_on` can only be a Connection or an AND (resolved can not ba a inner OR)
751
+ if isinstance(cur_other_based_on, AndConnectionRelation):
745
752
  # check if the current connection fits in one of the AND relation items -> allowed too (f.e. a
746
753
  # smaller AND contained in a bigger AND)
747
- for cur_other_and_element in resolved_other.connections:
754
+ for cur_other_and_element in cur_other_based_on.connections:
748
755
  if resolved_self.contained_in(cur_other_and_element, ignore_metadata=ignore_metadata):
749
756
  return True
750
-
751
- resolved_other_relation = resolved_other.based_on_elements \
752
- if isinstance(resolved_other, Connection) else resolved_other
753
-
754
- for cur_other_based_on in resolved_other_relation.connections:
755
- if isinstance(cur_other_based_on, AndConnectionRelation):
756
- # check if the current connection fits in one of the AND relation items -> allowed too (f.e. a
757
- # smaller AND contained in a bigger AND)
758
- for cur_other_and_element in cur_other_based_on.connections:
759
- if resolved_self.contained_in(cur_other_and_element, ignore_metadata=ignore_metadata):
760
- return True
761
- else:
762
- if resolved_self.contained_in(cur_other_based_on, ignore_metadata=ignore_metadata):
763
- # element was found in this branch
764
- return True
757
+ else:
758
+ if resolved_self.contained_in(cur_other_based_on, ignore_metadata=ignore_metadata):
759
+ # element was found in this branch
760
+ return True
765
761
  return False
766
762
 
767
763
  def intersection_with(