Skip to content

Commit f6483ea

Browse files
committed
Preload launch extensions so there is no caveat about e.g. launch_ros
Signed-off-by: Emerson Knapp <[email protected]>
1 parent f230abd commit f6483ea

File tree

4 files changed

+87
-117
lines changed

4 files changed

+87
-117
lines changed

launch_frontend_py/__init__.py

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

15-
from typing import Any, List
15+
from typing import Any, Callable, List
1616

1717
from launch.frontend.parser import Parser
1818
from launch.launch_description import LaunchDescription
1919

20-
from . import actions
21-
from .entity import Entity
20+
from .entity import Entity, make_valid_name
21+
22+
__all__ = [
23+
'launch',
24+
]
2225

2326

2427
def launch(actions: List[Entity]) -> LaunchDescription:
28+
"""Entrypoint of the launchfile, produces a LaunchDescription containing the listed entities."""
2529
parser = Parser()
2630
root_entity = Entity('launch', children=actions)
2731
return parser.parse_description(root_entity)
2832

2933

34+
def __make_action_factory(action_name: str) -> Callable[..., Any]:
35+
"""Create a factory function that constructs an Entity of the given action type."""
36+
action_name = make_valid_name(action_name)
37+
38+
def fn(*args, **kwargs):
39+
return Entity(action_name, *args, **kwargs)
40+
41+
fn.__doc__ = f'launch_frontend_py action: {action_name} (dynamically generated)'
42+
fn.__name__ = action_name
43+
fn.__qualname__ = action_name
44+
fn.__module__ = __name__
45+
globals()[action_name] = fn
46+
__all__.append(action_name)
47+
return fn
48+
49+
3050
def __getattr__(name: str) -> Any:
31-
"""Forward attribute access to the dynamic actions module."""
51+
"""
52+
Dynamically create action factory functions on demand.
53+
54+
This is called in `from actions import <name>`, `getattr(actions, <name>)`, or `actions.<name>`
55+
"""
3256
import importlib
57+
from launch.frontend import Parser
58+
from launch.frontend.expose import action_parse_methods
3359

3460
if name in __all__:
61+
# It's already been loaded, just return it
3562
return importlib.import_module(f'.{name}', __name__)
3663

37-
return getattr(actions, name)
64+
# Not loaded here yet, perhaps in an extension that wasn't imported yet
65+
Parser.load_launch_extensions()
3866

67+
if name in action_parse_methods:
68+
return __make_action_factory(name)
69+
elif name.endswith('_'):
70+
# The name has a trailing underscore, which we add to actions with reserved Python names
71+
# Try again without the underscore
72+
__getattr__(name[:-1])
73+
else:
74+
# It's not registered, raise the usual error
75+
msg = f'module {__name__} has no attribute "{name}"'
76+
if base_name != name:
77+
msg += ' (or "{base_name}")'
78+
raise AttributeError(msg)
3979

40-
__all__ = [
41-
'launch',
42-
]
80+
81+
def __preseed_all_actions() -> None:
82+
"""
83+
Pre-seed all available actions into this module.
84+
85+
This preloads all launch extensions and fills __all__ with available actions.
86+
87+
This is not necessary, since __getattr__ loads actions on demand,
88+
but it allows tools and users to see all available actions in __all__.
89+
"""
90+
from launch.frontend.expose import action_parse_methods
91+
from launch.frontend import Parser
92+
Parser.load_launch_extensions()
93+
94+
action_names = list(action_parse_methods.keys())
95+
for action_name in action_names:
96+
__getattr__(action_name)
97+
98+
__preseed_all_actions()

launch_frontend_py/actions.py

Lines changed: 0 additions & 80 deletions
This file was deleted.

launch_frontend_py/entity.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
# limitations under the License.
1414

1515
"""Module for launch_frontend_py Entity class."""
16+
import builtins
17+
import keyword
1618
from collections.abc import Iterable
1719
from typing import (
1820
List,
@@ -30,7 +32,26 @@
3032
is_instance_of,
3133
)
3234

33-
from .util import is_reserved_identifier
35+
36+
def is_reserved_identifier(name: str) -> bool:
37+
"""
38+
Check if a name is a reserved identifier in Python.
39+
40+
Used to avoid naming issues in the launch DSL that overlap with Python reserved words.
41+
"""
42+
return keyword.iskeyword(name) or name in dir(builtins)
43+
44+
45+
def make_valid_name(name: str) -> str:
46+
"""
47+
Make a valid Python identifier for an action or attribute name.
48+
49+
If the name is a reserved identifier in Python, append an underscore to it.
50+
"""
51+
if is_reserved_identifier(name):
52+
return f'{name}_'
53+
return name
54+
3455

3556

3657
class Entity(BaseEntity):
@@ -108,9 +129,7 @@ def get_attr( # type: ignore[override]
108129
See :py:meth:`launch.frontend.Entity.get_attr`.
109130
Does not apply type coercion, only checks if the read value is of the correct type.
110131
"""
111-
# Reserved identifiers are all suffixed with an underscore
112-
if is_reserved_identifier(name):
113-
name += '_'
132+
name = make_valid_name(name)
114133

115134
if name not in self.__attrs:
116135
if not optional:

launch_frontend_py/util.py

Lines changed: 0 additions & 25 deletions
This file was deleted.

0 commit comments

Comments
 (0)