Skip to content

Commit 581f81e

Browse files
authored
Handle positional list arg as children (#7)
Allow passing positional args, either as individual items or lists, for children entities to `Entity`
1 parent 826b887 commit 581f81e

File tree

4 files changed

+97
-22
lines changed

4 files changed

+97
-22
lines changed

launch_py/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
def launch(actions: List[Entity]) -> LaunchDescription:
2525
parser = Parser()
26-
root_entity = Entity('launch', {'children': actions})
26+
root_entity = Entity('launch', children=actions)
2727
return parser.parse_description(root_entity)
2828

2929

launch_py/actions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ def make_action_factory(action_name: str, **kwargs) -> Callable[..., Any]:
2424
if is_reserved_identifier(action_name):
2525
action_name += '_'
2626

27-
def fn(**kwargs):
28-
return Entity(action_name, kwargs)
27+
def fn(*args, **kwargs):
28+
return Entity(action_name, *args, **kwargs)
2929

3030
fn.__doc__ = f'launch_py action: {action_name} (dynamically generated)'
3131
fn.__name__ = action_name

launch_py/entity.py

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
"""Module for launch_py Entity class."""
1616
import builtins
17+
from collections.abc import Iterable
1718
import keyword
1819
from typing import (
1920
List,
@@ -43,14 +44,28 @@ class Entity(BaseEntity):
4344
def __init__(
4445
self,
4546
type_name: Text,
46-
kwargs: dict,
47-
*,
48-
parent: Optional['Entity'] = None,
47+
*args: BaseEntity,
48+
**kwargs,
4949
) -> None:
5050
"""Create an Entity."""
51+
if args and kwargs:
52+
raise ValueError(
53+
'Entity cannot take both positional arguments and keyword arguments. '
54+
f'Provided args={args}, kwargs={kwargs}. '
55+
'To provide attributes & children, pass `children` kwarg with type list[Entity]')
56+
57+
if kwargs:
58+
self.__attrs = kwargs
59+
else:
60+
children: list[BaseEntity] = []
61+
for child in args:
62+
if isinstance(child, Iterable):
63+
children.extend(child)
64+
else:
65+
children.append(child)
66+
self.__attrs = {'children': children}
67+
5168
self.__type_name = type_name
52-
self.__kwargs = kwargs
53-
self.__parent = parent
5469
self.__read_keys: Set[Text] = set()
5570

5671
@property
@@ -61,27 +76,21 @@ def type_name(self) -> Text:
6176
@property
6277
def parent(self) -> Optional['Entity']:
6378
"""Get Entity parent."""
64-
return self.__parent
79+
return None
6580

6681
@property
6782
def children(self) -> List[BaseEntity]:
6883
"""Get the Entity's children."""
69-
if not isinstance(self.__kwargs, (dict)):
70-
raise TypeError(
71-
f'Expected a dict, got {type(self.__kwargs)}:'
72-
f'\n---\n{self.__kwargs}\n---'
73-
)
74-
75-
if 'children' not in self.__kwargs:
84+
if 'children' not in self.__attrs:
7685
raise ValueError(
7786
f'Expected entity `{self.__type_name}` to have children entities.')
7887
self.__read_keys.add('children')
7988

80-
children: List[BaseEntity] = self.__kwargs['children']
89+
children: List[BaseEntity] = self.__attrs['children']
8190
return children
8291

8392
def assert_entity_completely_parsed(self):
84-
unparsed_keys = set(self.__kwargs.keys()) - self.__read_keys
93+
unparsed_keys = set(self.__attrs.keys()) - self.__read_keys
8594
if unparsed_keys:
8695
raise ValueError(
8796
f'Unexpected key(s) found in `{self.__type_name}`: {unparsed_keys}'
@@ -104,18 +113,18 @@ def get_attr( # type: ignore[override]
104113
See :py:meth:`launch.frontend.Entity.get_attr`.
105114
Does not apply type coercion, only checks if the read value is of the correct type.
106115
"""
107-
if name not in self.__kwargs:
116+
if name not in self.__attrs:
108117
if not optional:
109118
raise AttributeError(
110119
"Can not find attribute '{}' in Entity '{}'".format(
111120
name, self.type_name))
112121
else:
113122
return None
114123
self.__read_keys.add(name)
115-
data = self.__kwargs[name]
124+
data = self.__attrs[name]
116125
if check_is_list_entity(data_type):
117126
if isinstance(data, list) and isinstance(data[0], dict):
118-
return [Entity(name, child) for child in data]
127+
return [Entity(name, **child) for child in data]
119128
elif isinstance(data, list) and isinstance(data[0], Entity):
120129
return data
121130
raise TypeError(
@@ -133,4 +142,4 @@ def get_attr( # type: ignore[override]
133142

134143
def __repr__(self) -> str:
135144
"""Return a string representation of the Entity."""
136-
return f'Entity(type_name={self.__type_name}, kwargs={self.__kwargs})'
145+
return f'Entity(type_name={self.__type_name}, attrs={self.__attrs})'

test/test_entity.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
15+
from launch_py.actions import group, log
16+
import pytest
17+
18+
19+
def test_kwarg_children():
20+
g = group(
21+
namespace='something',
22+
children=[
23+
log(level='info', message='list child'),
24+
]
25+
)
26+
assert len(g.children) == 1
27+
assert g.get_attr('namespace') == 'something'
28+
29+
30+
def test_list_children():
31+
g = group([
32+
log(level='info', message='list child'),
33+
log(level='info', message='list child 2')
34+
])
35+
assert len(g.children) == 2
36+
37+
38+
def test_positional_children():
39+
g = group(
40+
log(level='info', message='positional child'),
41+
log(level='info', message='positional child 2'),
42+
log(level='info', message='positional child 3'),
43+
)
44+
assert len(g.children) == 3
45+
46+
47+
def test_positional_lists():
48+
g = group(
49+
[
50+
log(level='info', message='positional list 1 child 1'),
51+
log(level='info', message='positional list 1 child 2'),
52+
],
53+
[
54+
log(level='info', message='positional list 2 child 1'),
55+
]
56+
)
57+
assert len(g.children) == 3
58+
59+
60+
def test_bad_arg_combo():
61+
with pytest.raises(ValueError):
62+
g = group(
63+
log(level='info', message='positional child'),
64+
condition='Something'
65+
)
66+
assert g

0 commit comments

Comments
 (0)