Skip to content

Commit 4a4a4fc

Browse files
authored
event handlers: Allow to match the target action with a callable and not only with an object instance (#540)
Signed-off-by: Ivan Santiago Paunovic <[email protected]>
1 parent 2b828e2 commit 4a4a4fc

9 files changed

+263
-260
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Copyright 2021 Open Source Robotics Foundation, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Module for OnActionEventBase class."""
16+
17+
import collections.abc
18+
from typing import Callable
19+
from typing import List # noqa
20+
from typing import Optional
21+
from typing import Text
22+
from typing import Type
23+
from typing import TYPE_CHECKING
24+
from typing import Union
25+
26+
from ..event import Event
27+
from ..event_handler import BaseEventHandler
28+
from ..launch_context import LaunchContext
29+
from ..launch_description_entity import LaunchDescriptionEntity
30+
from ..some_actions_type import SomeActionsType
31+
32+
if TYPE_CHECKING:
33+
from ..action import Action # noqa: F401
34+
35+
36+
class OnActionEventBase(BaseEventHandler):
37+
"""Base event handler for events that have a source action."""
38+
39+
def __init__(
40+
self,
41+
*,
42+
action_matcher: Optional[Union[Callable[['Action'], bool], 'Action']],
43+
on_event: Union[
44+
SomeActionsType,
45+
Callable[[Event, LaunchContext], Optional[SomeActionsType]]
46+
],
47+
target_event_cls: Type[Event],
48+
target_action_cls: Type['Action'],
49+
**kwargs
50+
) -> None:
51+
"""
52+
Construct a `OnActionEventBase` instance.
53+
54+
:param action_matcher: `ExecuteProcess` instance or callable to filter events
55+
from which proces/processes to handle.
56+
:param on_event: Action to be done to handle the event.
57+
:param target_event_cls: A subclass of `Event`, indicating which events
58+
should be handled.
59+
:param target_action_cls: A subclass of `Action`, indicating which kind of action can
60+
generate the event.
61+
"""
62+
if not issubclass(target_event_cls, Event):
63+
raise TypeError("'target_event_cls' must be a subclass of 'Event'")
64+
if (
65+
not isinstance(action_matcher, (target_action_cls, type(None)))
66+
and not callable(action_matcher)
67+
):
68+
raise TypeError(
69+
f"action_matcher must be an '{target_action_cls.__name__}' instance or a callable"
70+
)
71+
self.__target_action_cls = target_action_cls
72+
self.__target_event_cls = target_event_cls
73+
self.__action_matcher = action_matcher
74+
75+
def event_matcher(event):
76+
if not isinstance(event, target_event_cls):
77+
return False
78+
if callable(action_matcher):
79+
return action_matcher(event.action)
80+
if isinstance(action_matcher, target_action_cls):
81+
return event.action is action_matcher
82+
assert action_matcher is None
83+
return True
84+
super().__init__(matcher=event_matcher, **kwargs)
85+
self.__actions_on_event: List[LaunchDescriptionEntity] = []
86+
# TODO(wjwwood) check that it is not only callable, but also a callable that matches
87+
# the correct signature for a handler in this case
88+
if callable(on_event):
89+
# Then on_exit is a function or lambda, so we can just call it, but
90+
# we don't put anything in self.__actions_on_event because we cannot
91+
# know what the function will return.
92+
self.__on_event = on_event
93+
else:
94+
# Otherwise, setup self.__actions_on_event
95+
if isinstance(on_event, collections.abc.Iterable):
96+
for entity in on_event:
97+
if not isinstance(entity, LaunchDescriptionEntity):
98+
raise TypeError(
99+
"expected all items in 'on_event' iterable to be of type "
100+
"'LaunchDescriptionEntity' but got '{}'".format(type(entity)))
101+
self.__actions_on_event = list(on_event) # Outside list is to ensure type is List
102+
else:
103+
self.__actions_on_event = [on_event]
104+
105+
def handle(self, event: Event, context: LaunchContext) -> Optional[SomeActionsType]:
106+
"""Handle the given event."""
107+
super().handle(event, context)
108+
109+
if self.__actions_on_event:
110+
return self.__actions_on_event
111+
return self.__on_event(event, context)
112+
113+
@property
114+
def handler_description(self) -> Text:
115+
"""Return the string description of the handler."""
116+
# TODO(jacobperron): revisit how to describe known actions that are passed in.
117+
# It would be nice if the parent class could output their description
118+
# via the 'entities' property.
119+
if self.__actions_on_event:
120+
return '<actions>'
121+
return '{}'.format(self.__on_event)
122+
123+
@property
124+
def matcher_description(self) -> Text:
125+
"""Return the string description of the matcher."""
126+
if self.__action_matcher is None:
127+
return f'event == {self.__target_event_cls.__name__}'
128+
return (
129+
f'event == {self.__target_event_cls.__name__} and'
130+
f' {self.__target_action_cls.__name__}(event.action)'
131+
)

launch/launch/event_handlers/on_execution_complete.py

+19-67
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,23 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import collections.abc
1615
from typing import Callable
1716
from typing import cast
18-
from typing import List # noqa
1917
from typing import Optional
20-
from typing import Text
2118
from typing import TYPE_CHECKING
2219
from typing import Union
2320

21+
from .on_action_event_base import OnActionEventBase
2422
from ..event import Event
25-
from ..event_handler import EventHandler
2623
from ..events import ExecutionComplete
2724
from ..launch_context import LaunchContext
28-
from ..launch_description_entity import LaunchDescriptionEntity
2925
from ..some_actions_type import SomeActionsType
3026

3127
if TYPE_CHECKING:
32-
from .. import Action # noqa
28+
from .. import Action # noqa: F401
3329

3430

35-
class OnExecutionComplete(EventHandler):
31+
class OnExecutionComplete(OnActionEventBase):
3632
"""
3733
Convenience class for handling an action completion event.
3834
@@ -43,69 +39,25 @@ class OnExecutionComplete(EventHandler):
4339
def __init__(
4440
self,
4541
*,
46-
target_action: Optional['Action'] = None,
47-
on_completion: Union[SomeActionsType, Callable[[int], Optional[SomeActionsType]]],
42+
target_action:
43+
Optional[Union[Callable[['Action'], bool], 'Action']] = None,
44+
on_completion:
45+
Union[
46+
SomeActionsType,
47+
Callable[[ExecutionComplete, LaunchContext], Optional[SomeActionsType]]],
4848
**kwargs
4949
) -> None:
5050
"""Create an OnExecutionComplete event handler."""
51-
from ..action import Action # noqa
52-
if not isinstance(target_action, (Action, type(None))):
53-
raise ValueError("OnExecutionComplete requires an 'Action' as the target")
51+
from ..action import Action # noqa: F811
52+
on_completion = cast(
53+
Union[
54+
SomeActionsType,
55+
Callable[[Event, LaunchContext], Optional[SomeActionsType]]],
56+
on_completion)
5457
super().__init__(
55-
matcher=(
56-
lambda event: (
57-
isinstance(event, ExecutionComplete) and (
58-
target_action is None or
59-
event.action == target_action
60-
)
61-
)
62-
),
63-
entities=None,
58+
action_matcher=target_action,
59+
on_event=on_completion,
60+
target_event_cls=ExecutionComplete,
61+
target_action_cls=Action,
6462
**kwargs,
6563
)
66-
self.__target_action = target_action
67-
# TODO(wjwwood) check that it is not only callable, but also a callable that matches
68-
# the correct signature for a handler in this case
69-
self.__on_completion = on_completion
70-
self.__actions_on_completion = [] # type: List[LaunchDescriptionEntity]
71-
if callable(on_completion):
72-
# Then on_completion is a function or lambda, so we can just call it, but
73-
# we don't put anything in self.__actions_on_completion because we cannot
74-
# know what the function will return.
75-
pass
76-
else:
77-
# Otherwise, setup self.__actions_on_completion
78-
if isinstance(on_completion, collections.abc.Iterable):
79-
for entity in on_completion:
80-
if not isinstance(entity, LaunchDescriptionEntity):
81-
raise ValueError(
82-
"expected all items in 'on_completion' iterable to be of type "
83-
"'LaunchDescriptionEntity' but got '{}'".format(type(entity)))
84-
self.__actions_on_completion = list(on_completion)
85-
else:
86-
self.__actions_on_completion = [on_completion]
87-
# Then return it from a lambda and use that as the self.__on_completion callback.
88-
self.__on_completion = lambda event, context: self.__actions_on_completion
89-
90-
def handle(self, event: Event, context: LaunchContext) -> Optional[SomeActionsType]:
91-
"""Handle the given event."""
92-
return self.__on_completion(cast(ExecutionComplete, event), context)
93-
94-
@property
95-
def handler_description(self) -> Text:
96-
"""Return the string description of the handler."""
97-
# TODO(jacobperron): revisit how to describe known actions that are passed in.
98-
# It would be nice if the parent class could output their description
99-
# via the 'entities' property.
100-
if self.__actions_on_completion:
101-
return '<actions>'
102-
return '{}'.format(self.__on_completion)
103-
104-
@property
105-
def matcher_description(self) -> Text:
106-
"""Return the string description of the matcher."""
107-
if self.__target_action is None:
108-
return 'event == ExecutionComplete'
109-
return 'event == ExecutionComplete and event.action == Action({})'.format(
110-
hex(id(self.__target_action))
111-
)
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2018 Open Source Robotics Foundation, Inc.
1+
# Copyright 2018-2021 Open Source Robotics Foundation, Inc.
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
@@ -14,27 +14,25 @@
1414

1515
"""Module for OnProcessExit class."""
1616

17-
import collections.abc
1817
from typing import Callable
1918
from typing import cast
20-
from typing import List # noqa
2119
from typing import Optional
22-
from typing import Text
2320
from typing import TYPE_CHECKING
2421
from typing import Union
2522

23+
from .on_action_event_base import OnActionEventBase
2624
from ..event import Event
27-
from ..event_handler import BaseEventHandler
2825
from ..events.process import ProcessExited
2926
from ..launch_context import LaunchContext
30-
from ..launch_description_entity import LaunchDescriptionEntity
3127
from ..some_actions_type import SomeActionsType
3228

29+
3330
if TYPE_CHECKING:
31+
from ..actions import Action # noqa: F401
3432
from ..actions import ExecuteProcess # noqa: F401
3533

3634

37-
class OnProcessExit(BaseEventHandler):
35+
class OnProcessExit(OnActionEventBase):
3836
"""
3937
Convenience class for handling a process exited event.
4038
@@ -45,70 +43,29 @@ class OnProcessExit(BaseEventHandler):
4543
def __init__(
4644
self,
4745
*,
48-
target_action: 'ExecuteProcess' = None,
49-
on_exit: Union[SomeActionsType,
50-
Callable[[ProcessExited, LaunchContext], Optional[SomeActionsType]]],
46+
target_action:
47+
Optional[Union[Callable[['ExecuteProcess'], bool], 'ExecuteProcess']] = None,
48+
on_exit:
49+
Union[
50+
SomeActionsType,
51+
Callable[[ProcessExited, LaunchContext], Optional[SomeActionsType]]
52+
],
5153
**kwargs
5254
) -> None:
5355
"""Create an OnProcessExit event handler."""
54-
from ..actions import ExecuteProcess # noqa
55-
if not isinstance(target_action, (ExecuteProcess, type(None))):
56-
raise TypeError("OnProcessExit requires an 'ExecuteProcess' action as the target")
56+
from ..actions import ExecuteProcess # noqa: F811
57+
target_action = cast(
58+
Optional[Union[Callable[['Action'], bool], 'Action']],
59+
target_action)
60+
on_exit = cast(
61+
Union[
62+
SomeActionsType,
63+
Callable[[Event, LaunchContext], Optional[SomeActionsType]]],
64+
on_exit)
5765
super().__init__(
58-
matcher=(
59-
lambda event: (
60-
isinstance(event, ProcessExited) and (
61-
target_action is None or
62-
event.action == target_action
63-
)
64-
)
65-
),
66+
action_matcher=target_action,
67+
on_event=on_exit,
68+
target_event_cls=ProcessExited,
69+
target_action_cls=ExecuteProcess,
6670
**kwargs,
6771
)
68-
self.__target_action = target_action
69-
self.__actions_on_exit = [] # type: List[LaunchDescriptionEntity]
70-
# TODO(wjwwood) check that it is not only callable, but also a callable that matches
71-
# the correct signature for a handler in this case
72-
if callable(on_exit):
73-
# Then on_exit is a function or lambda, so we can just call it, but
74-
# we don't put anything in self.__actions_on_exit because we cannot
75-
# know what the function will return.
76-
self.__on_exit = on_exit
77-
else:
78-
# Otherwise, setup self.__actions_on_exit
79-
if isinstance(on_exit, collections.abc.Iterable):
80-
for entity in on_exit:
81-
if not isinstance(entity, LaunchDescriptionEntity):
82-
raise ValueError(
83-
"expected all items in 'on_exit' iterable to be of type "
84-
"'LaunchDescriptionEntity' but got '{}'".format(type(entity)))
85-
self.__actions_on_exit = list(on_exit) # Outside list is to ensure type is List
86-
else:
87-
self.__actions_on_exit = [on_exit]
88-
89-
def handle(self, event: Event, context: LaunchContext) -> Optional[SomeActionsType]:
90-
"""Handle the given event."""
91-
super().handle(event, context)
92-
93-
if self.__actions_on_exit:
94-
return self.__actions_on_exit
95-
return self.__on_exit(cast(ProcessExited, event), context)
96-
97-
@property
98-
def handler_description(self) -> Text:
99-
"""Return the string description of the handler."""
100-
# TODO(jacobperron): revisit how to describe known actions that are passed in.
101-
# It would be nice if the parent class could output their description
102-
# via the 'entities' property.
103-
if self.__actions_on_exit:
104-
return '<actions>'
105-
return '{}'.format(self.__on_exit)
106-
107-
@property
108-
def matcher_description(self) -> Text:
109-
"""Return the string description of the matcher."""
110-
if self.__target_action is None:
111-
return 'event == ProcessExited'
112-
return 'event == ProcessExited and event.action == ExecuteProcess({})'.format(
113-
hex(id(self.__target_action))
114-
)

0 commit comments

Comments
 (0)