Skip to content

Commit efd7757

Browse files
authored
Launch description initial (#2)
Initial functionality pass - Apply linters properly - Edit entity class to actually apply to this frontend's logic - Produce launchdescription using base parser - It can be launched with ros2 launch! at least for one simple example
1 parent 597a844 commit efd7757

File tree

6 files changed

+92
-60
lines changed

6 files changed

+92
-60
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__pycache__/

launch_py/__init__.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,41 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from launch import LaunchDescription
15+
import importlib
16+
from typing import Any, List
17+
1618
from launch.frontend.expose import action_parse_methods
19+
from launch.frontend.parser import Parser
20+
from launch.launch_description import LaunchDescription
21+
22+
from .entity import Entity
23+
24+
25+
def launch(actions: List[Entity]) -> LaunchDescription:
26+
parser = Parser()
27+
root_entity = Entity('launch', {'children': actions})
28+
return parser.parse_description(root_entity)
1729

18-
launch = LaunchDescription
1930

2031
def make_action(action_name):
21-
def impl():
22-
return f'Action: {action_name}'
32+
def impl(**kwargs):
33+
return Entity(action_name, kwargs)
2334
globals()[action_name] = impl
2435
return impl
2536

37+
2638
exposed_actions = [
2739
make_action(key) for key in action_parse_methods.keys()
2840
]
2941

3042
__all__ = [
3143
'launch',
3244
] + exposed_actions
45+
46+
47+
def __getattr__(name: str) -> Any:
48+
# This is a workaround to silence mypy complaint about attributes that are created dynamically
49+
try:
50+
return importlib.import_module(f'.{name}', __name__)
51+
except ModuleNotFoundError as e:
52+
raise AttributeError(f'module {__name__} has no attribute {name}') from e

launch_py/entity.py

Lines changed: 43 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -13,35 +13,39 @@
1313
# limitations under the License.
1414

1515
"""Module for launch_py Entity class."""
16+
from typing import (
17+
List,
18+
Optional,
19+
Set,
20+
Text,
21+
Union,
22+
)
1623

17-
from typing import List
18-
from typing import Optional
19-
from typing import Text
20-
from typing import Union
21-
22-
from launch.frontend import Entity as BaseEntity
24+
from launch.frontend.entity import Entity as BaseEntity
2325
from launch.frontend.type_utils import check_is_list_entity
24-
from launch.utilities.type_utils import AllowedTypesType
25-
from launch.utilities.type_utils import AllowedValueType
26-
from launch.utilities.type_utils import is_instance_of
26+
from launch.utilities.type_utils import (
27+
AllowedTypesType,
28+
AllowedValueType,
29+
is_instance_of,
30+
)
2731

2832

2933
class Entity(BaseEntity):
30-
"""Single item in the intermediate YAML front_end representation."""
34+
"""Single item in the intermediate front_end representation."""
3135

3236
def __init__(
33-
self,
34-
element: dict,
35-
type_name: Text = None,
36-
*,
37-
parent: 'Entity' = None
38-
) -> Text:
37+
self,
38+
type_name: Text,
39+
kwargs: dict,
40+
*,
41+
parent: Optional['Entity'] = None,
42+
) -> None:
3943
"""Create an Entity."""
4044
self.__type_name = type_name
41-
self.__element = element
45+
self.__kwargs = kwargs
4246
self.__parent = parent
43-
self.__read_keys = set()
4447
self.__children_called = False
48+
self.__read_keys: Set[Text] = set()
4549

4650
@property
4751
def type_name(self) -> Text:
@@ -54,48 +58,31 @@ def parent(self) -> Optional['Entity']:
5458
return self.__parent
5559

5660
@property
57-
def children(self) -> List['Entity']:
61+
def children(self) -> List[BaseEntity]:
5862
"""Get the Entity's children."""
5963
self.__children_called = True
60-
if not isinstance(self.__element, (dict, list)):
64+
if not isinstance(self.__kwargs, (dict)):
6165
raise TypeError(
62-
f'Expected a dict or list, got {type(self.element)}:'
63-
f'\n---\n{self.__element}\n---'
66+
f'Expected a dict, got {type(self.__kwargs)}:'
67+
f'\n---\n{self.__kwargs}\n---'
6468
)
65-
if isinstance(self.__element, dict):
66-
if 'children' not in self.__element:
67-
raise ValueError(
68-
f'Expected entity `{self.__type_name}` to have children entities.'
69-
f'That can be a list of subentities or a dictionary with a `children` '
70-
'list element')
71-
self.__read_keys.add('children')
72-
children = self.__element['children']
73-
else:
74-
children = self.__element
75-
entities = []
76-
for child in children:
77-
if len(child) != 1:
78-
raise RuntimeError(
79-
'Subentities must be a dictionary with only one key'
80-
', which is the entity type')
81-
type_name = list(child.keys())[0]
82-
entities.append(Entity(child[type_name], type_name))
83-
return entities
69+
70+
if 'children' not in self.__kwargs:
71+
raise ValueError(
72+
f'Expected entity `{self.__type_name}` to have children entities.')
73+
self.__read_keys.add('children')
74+
75+
children: List[BaseEntity] = self.__kwargs['children']
76+
return children
8477

8578
def assert_entity_completely_parsed(self):
86-
if isinstance(self.__element, list):
87-
if not self.__children_called:
88-
raise ValueError(
89-
f'Unexpected nested entity(ies) found in `{self.__type_name}`: '
90-
f'{self.__element}')
91-
return
92-
unparsed_keys = set(self.__element.keys()) - self.__read_keys
79+
unparsed_keys = set(self.__kwargs.keys()) - self.__read_keys
9380
if unparsed_keys:
9481
raise ValueError(
9582
f'Unexpected key(s) found in `{self.__type_name}`: {unparsed_keys}'
9683
)
9784

98-
def get_attr(
85+
def get_attr( # type: ignore[override]
9986
self,
10087
name: Text,
10188
*,
@@ -113,18 +100,18 @@ def get_attr(
113100
`launch_yaml` does not apply type coercion,
114101
it only checks if the read value is of the correct type.
115102
"""
116-
if name not in self.__element:
103+
if name not in self.__kwargs:
117104
if not optional:
118105
raise AttributeError(
119106
"Can not find attribute '{}' in Entity '{}'".format(
120107
name, self.type_name))
121108
else:
122109
return None
123110
self.__read_keys.add(name)
124-
data = self.__element[name]
111+
data = self.__kwargs[name]
125112
if check_is_list_entity(data_type):
126113
if isinstance(data, list) and isinstance(data[0], dict):
127-
return [Entity(child, name) for child in data]
114+
return [Entity(name, child) for child in data]
128115
raise TypeError(
129116
"Attribute '{}' of Entity '{}' expected to be a list of dictionaries.".format(
130117
name, self.type_name
@@ -137,3 +124,7 @@ def get_attr(
137124
)
138125
)
139126
return data
127+
128+
def __repr__(self) -> str:
129+
"""Return a string representation of the Entity."""
130+
return f'Entity(type_name={self.__type_name}, kwargs={self.__kwargs})'

launch_py/mypy.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[mypy-launch_py]
2+
disable_error_code = attr-defined

package.xml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,9 @@
1414
<depend>launch</depend>
1515

1616
<test_depend>ament_copyright</test_depend>
17-
<test_depend>ament_flake8</test_depend>
18-
<test_depend>ament_pep257</test_depend>
19-
<test_depend>ament_mypy</test_depend>
2017
<test_depend>ament_lint_auto</test_depend>
21-
<test_depend>ament_xmllint</test_depend>
18+
<test_depend>ament_lint_common</test_depend>
19+
<test_depend>ament_cmake_mypy</test_depend>
2220
<test_depend>python3-pytest</test_depend>
2321

2422
<member_of_group>launch_frontend_packages</member_of_group>

test/basic_launch.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2025 Polymath Robotics, 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+
from launch_py import executable, launch
15+
16+
17+
def generate_launch_description():
18+
return launch([
19+
executable(cmd='echo hello launch_py world!', output='screen')
20+
])

0 commit comments

Comments
 (0)