Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 launch_py/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

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


Expand Down
4 changes: 2 additions & 2 deletions launch_py/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ def make_action_factory(action_name: str, **kwargs) -> Callable[..., Any]:
if is_reserved_identifier(action_name):
action_name += '_'

def fn(**kwargs):
return Entity(action_name, kwargs)
def fn(*args, **kwargs):
return Entity(action_name, *args, **kwargs)

fn.__doc__ = f'launch_py action: {action_name} (dynamically generated)'
fn.__name__ = action_name
Expand Down
47 changes: 28 additions & 19 deletions launch_py/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

"""Module for launch_py Entity class."""
import builtins
from collections.abc import Iterable
import keyword
from typing import (
List,
Expand Down Expand Up @@ -43,14 +44,28 @@ class Entity(BaseEntity):
def __init__(
self,
type_name: Text,
kwargs: dict,
*,
parent: Optional['Entity'] = None,
*args: BaseEntity,
**kwargs,
) -> None:
"""Create an Entity."""
if args and kwargs:
raise ValueError(
'Entity cannot take both positional arguments and keyword arguments. '
f'Provided args={args}, kwargs={kwargs}. '
'To provide attributes & children, pass `children` kwarg with type list[Entity]')

if kwargs:
self.__attrs = kwargs
else:
children: list[BaseEntity] = []
for child in args:
if isinstance(child, Iterable):
children.extend(child)
else:
children.append(child)
self.__attrs = {'children': children}

self.__type_name = type_name
self.__kwargs = kwargs
self.__parent = parent
self.__read_keys: Set[Text] = set()

@property
Expand All @@ -61,27 +76,21 @@ def type_name(self) -> Text:
@property
def parent(self) -> Optional['Entity']:
"""Get Entity parent."""
return self.__parent
return None

@property
def children(self) -> List[BaseEntity]:
"""Get the Entity's children."""
if not isinstance(self.__kwargs, (dict)):
raise TypeError(
f'Expected a dict, got {type(self.__kwargs)}:'
f'\n---\n{self.__kwargs}\n---'
)

if 'children' not in self.__kwargs:
if 'children' not in self.__attrs:
raise ValueError(
f'Expected entity `{self.__type_name}` to have children entities.')
self.__read_keys.add('children')

children: List[BaseEntity] = self.__kwargs['children']
children: List[BaseEntity] = self.__attrs['children']
return children

def assert_entity_completely_parsed(self):
unparsed_keys = set(self.__kwargs.keys()) - self.__read_keys
unparsed_keys = set(self.__attrs.keys()) - self.__read_keys
if unparsed_keys:
raise ValueError(
f'Unexpected key(s) found in `{self.__type_name}`: {unparsed_keys}'
Expand All @@ -104,18 +113,18 @@ def get_attr( # type: ignore[override]
See :py:meth:`launch.frontend.Entity.get_attr`.
Does not apply type coercion, only checks if the read value is of the correct type.
"""
if name not in self.__kwargs:
if name not in self.__attrs:
if not optional:
raise AttributeError(
"Can not find attribute '{}' in Entity '{}'".format(
name, self.type_name))
else:
return None
self.__read_keys.add(name)
data = self.__kwargs[name]
data = self.__attrs[name]
if check_is_list_entity(data_type):
if isinstance(data, list) and isinstance(data[0], dict):
return [Entity(name, child) for child in data]
return [Entity(name, **child) for child in data]
elif isinstance(data, list) and isinstance(data[0], Entity):
return data
raise TypeError(
Expand All @@ -133,4 +142,4 @@ def get_attr( # type: ignore[override]

def __repr__(self) -> str:
"""Return a string representation of the Entity."""
return f'Entity(type_name={self.__type_name}, kwargs={self.__kwargs})'
return f'Entity(type_name={self.__type_name}, attrs={self.__attrs})'
66 changes: 66 additions & 0 deletions test/test_entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Copyright 2025 Polymath Robotics, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from launch_py.actions import group, log
import pytest


def test_kwarg_children():
g = group(
namespace='something',
children=[
log(level='info', message='list child'),
]
)
assert len(g.children) == 1
assert g.get_attr('namespace') == 'something'


def test_list_children():
g = group([
log(level='info', message='list child'),
log(level='info', message='list child 2')
])
assert len(g.children) == 2


def test_positional_children():
g = group(
log(level='info', message='positional child'),
log(level='info', message='positional child 2'),
log(level='info', message='positional child 3'),
)
assert len(g.children) == 3


def test_positional_lists():
g = group(
[
log(level='info', message='positional list 1 child 1'),
log(level='info', message='positional list 1 child 2'),
],
[
log(level='info', message='positional list 2 child 1'),
]
)
assert len(g.children) == 3


def test_bad_arg_combo():
with pytest.raises(ValueError):
g = group(
log(level='info', message='positional child'),
condition='Something'
)
assert g