Skip to content

Commit 2b828e2

Browse files
Add AppendEnvironmentVariable action (#543)
* Add AppendEnvironmentVariable action Signed-off-by: Christophe Bedard <[email protected]> * Add tests for AppendEnvironmentVariable and document Signed-off-by: Christophe Bedard <[email protected]> * Fix circular import Signed-off-by: Christophe Bedard <[email protected]> * Add separator option Signed-off-by: Christophe Bedard <[email protected]> * Support substitutions for prepend boolean parameter Signed-off-by: Christophe Bedard <[email protected]> * Properly document AppendEnvironmentVariable constructor parameters Signed-off-by: Christophe Bedard <[email protected]> * Add launch_yaml test for AppendEnvironmentVariable Signed-off-by: Christophe Bedard <[email protected]>
1 parent 7255890 commit 2b828e2

File tree

6 files changed

+368
-0
lines changed

6 files changed

+368
-0
lines changed

launch/doc/source/architecture.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ This is a non-exhaustive list of actions that `launch` may provide:
5858

5959
- This action will set an environment variable by name.
6060

61+
- :class:`launch.actions.AppendEnvironmentVariable`
62+
63+
- This action will set an environment variable by name if it does not exist, otherwise it appends to the existing value using a platform-specific separator.
64+
- There is also an option to prepend instead of appending and to provide a custom separator.
65+
6166
- :class:`launch.actions.GroupAction`
6267

6368
- This action will yield other actions, but can be associated with conditionals (allowing you to use the conditional on the group action rather than on each sub-action individually) and can optionally scope the launch configurations.

launch/launch/actions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""actions Module."""
1616

1717
from .declare_launch_argument import DeclareLaunchArgument
18+
from .append_environment_variable import AppendEnvironmentVariable # noqa: I100
1819
from .emit_event import EmitEvent
1920
from .execute_process import ExecuteProcess
2021
from .group_action import GroupAction
@@ -35,6 +36,7 @@
3536
from .unset_launch_configuration import UnsetLaunchConfiguration
3637

3738
__all__ = [
39+
'AppendEnvironmentVariable',
3840
'DeclareLaunchArgument',
3941
'EmitEvent',
4042
'ExecuteProcess',
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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 the AppendEnvironmentVariable action."""
16+
17+
import os
18+
from typing import List
19+
from typing import Union
20+
21+
from ..action import Action
22+
from ..frontend import Entity
23+
from ..frontend import expose_action
24+
from ..frontend import Parser
25+
from ..launch_context import LaunchContext
26+
from ..some_substitutions_type import SomeSubstitutionsType
27+
from ..substitution import Substitution
28+
from ..utilities import normalize_to_list_of_substitutions
29+
from ..utilities import perform_substitutions
30+
from ..utilities.type_utils import normalize_typed_substitution
31+
from ..utilities.type_utils import NormalizedValueType
32+
from ..utilities.type_utils import perform_typed_substitution
33+
34+
35+
@expose_action('append_env')
36+
class AppendEnvironmentVariable(Action):
37+
"""
38+
Action that appends to an environment variable if it exists and sets it if it does not.
39+
40+
It can optionally prepend instead of appending.
41+
It can also optionally use a custom separator, with the default being a platform-specific
42+
separator, `os.pathsep`.
43+
"""
44+
45+
def __init__(
46+
self,
47+
name: SomeSubstitutionsType,
48+
value: SomeSubstitutionsType,
49+
prepend: Union[bool, SomeSubstitutionsType] = False,
50+
separator: SomeSubstitutionsType = os.pathsep,
51+
**kwargs,
52+
) -> None:
53+
"""
54+
Create an AppendEnvironmentVariable action.
55+
56+
All parameters can be provided as substitutions.
57+
A substitution for the prepend parameter will be coerced to `bool` following YAML rules.
58+
59+
:param name: the name of the environment variable
60+
:param value: the value to set or append
61+
:param prepend: whether the value should be prepended instead
62+
:param separator: the separator to use, defaulting to a platform-specific separator
63+
"""
64+
super().__init__(**kwargs)
65+
self.__name = normalize_to_list_of_substitutions(name)
66+
self.__value = normalize_to_list_of_substitutions(value)
67+
self.__prepend = normalize_typed_substitution(prepend, bool)
68+
self.__separator = normalize_to_list_of_substitutions(separator)
69+
70+
@classmethod
71+
def parse(
72+
cls,
73+
entity: Entity,
74+
parser: Parser,
75+
):
76+
"""Parse an 'append_env' entity."""
77+
_, kwargs = super().parse(entity, parser)
78+
kwargs['name'] = parser.parse_substitution(entity.get_attr('name'))
79+
kwargs['value'] = parser.parse_substitution(entity.get_attr('value'))
80+
prepend = entity.get_attr('prepend', optional=True, data_type=bool, can_be_str=True)
81+
if prepend is not None:
82+
kwargs['prepend'] = parser.parse_if_substitutions(prepend)
83+
separator = entity.get_attr('separator', optional=True)
84+
if separator is not None:
85+
kwargs['separator'] = parser.parse_substitution(separator)
86+
return cls, kwargs
87+
88+
@property
89+
def name(self) -> List[Substitution]:
90+
"""Getter for the name of the environment variable to be set or appended to."""
91+
return self.__name
92+
93+
@property
94+
def value(self) -> List[Substitution]:
95+
"""Getter for the value of the environment variable to be set or appended."""
96+
return self.__value
97+
98+
@property
99+
def prepend(self) -> NormalizedValueType:
100+
"""Getter for the prepend flag."""
101+
return self.__prepend
102+
103+
@property
104+
def separator(self) -> List[Substitution]:
105+
"""Getter for the separator."""
106+
return self.__separator
107+
108+
def execute(self, context: LaunchContext) -> None:
109+
"""Execute the action."""
110+
name = perform_substitutions(context, self.name)
111+
value = perform_substitutions(context, self.value)
112+
prepend = perform_typed_substitution(context, self.prepend, bool)
113+
separator = perform_substitutions(context, self.separator)
114+
if name in os.environ:
115+
os.environ[name] = \
116+
os.environ[name] + separator + value \
117+
if not prepend \
118+
else value + separator + os.environ[name]
119+
else:
120+
os.environ[name] = value
121+
return None
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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+
"""Tests for the AppendEnvironmentVariable action."""
16+
17+
import os
18+
19+
from launch import LaunchContext
20+
from launch.actions import AppendEnvironmentVariable
21+
from launch.substitutions import EnvironmentVariable
22+
from launch.substitutions import TextSubstitution
23+
24+
25+
def test_append_environment_variable_constructor():
26+
"""Test the constructor for the AppendEnvironmentVariable class."""
27+
AppendEnvironmentVariable('name', 'value')
28+
AppendEnvironmentVariable('name', 'value', separator='|')
29+
AppendEnvironmentVariable('name', 'value', prepend=False)
30+
AppendEnvironmentVariable('name', 'value', prepend=True)
31+
AppendEnvironmentVariable('name', 'value', prepend=True, separator='|')
32+
33+
34+
def test_append_environment_variable_execute():
35+
"""Test the execute() of the AppendEnvironmentVariable class."""
36+
lc1 = LaunchContext()
37+
38+
# Sets environment variable if it does not exist
39+
if 'NONEXISTENT_KEY' in os.environ:
40+
del os.environ['NONEXISTENT_KEY']
41+
assert os.environ.get('NONEXISTENT_KEY') is None
42+
AppendEnvironmentVariable('NONEXISTENT_KEY', 'value').visit(lc1)
43+
assert os.environ.get('NONEXISTENT_KEY') == 'value'
44+
# Same result if prepending is enabled
45+
del os.environ['NONEXISTENT_KEY']
46+
AppendEnvironmentVariable('NONEXISTENT_KEY', 'value', prepend=True).visit(lc1)
47+
assert os.environ.get('NONEXISTENT_KEY') == 'value'
48+
49+
# Appends to environment variable if it does exist
50+
AppendEnvironmentVariable('NONEXISTENT_KEY', 'another value').visit(lc1)
51+
assert os.environ.get('NONEXISTENT_KEY') == 'value' + os.pathsep + 'another value'
52+
53+
# Prepends to environment variable if it does exist and option is enabled
54+
AppendEnvironmentVariable('NONEXISTENT_KEY', 'some value', prepend=True).visit(lc1)
55+
assert os.environ.get('NONEXISTENT_KEY') == \
56+
'some value' + os.pathsep + 'value' + os.pathsep + 'another value'
57+
58+
# Can use an optional separator
59+
AppendEnvironmentVariable('NONEXISTENT_KEY', 'other value', separator='|').visit(lc1)
60+
assert os.environ.get('NONEXISTENT_KEY') == \
61+
'some value' + os.pathsep + 'value' + os.pathsep + 'another value' + '|' + 'other value'
62+
63+
# Appends/prepends with substitutions
64+
assert os.environ.get('ANOTHER_NONEXISTENT_KEY') is None
65+
AppendEnvironmentVariable(
66+
'ANOTHER_NONEXISTENT_KEY',
67+
EnvironmentVariable('NONEXISTENT_KEY'),
68+
prepend=TextSubstitution(text='false')).visit(lc1)
69+
assert os.environ.get('ANOTHER_NONEXISTENT_KEY') == \
70+
'some value' + os.pathsep + 'value' + os.pathsep + 'another value' + '|' + 'other value'
71+
72+
os.environ['ANOTHER_NONEXISTENT_KEY'] = 'abc'
73+
os.environ['SOME_SEPARATOR'] = '//'
74+
AppendEnvironmentVariable(
75+
'ANOTHER_NONEXISTENT_KEY',
76+
TextSubstitution(text='def'),
77+
separator=EnvironmentVariable('SOME_SEPARATOR'),
78+
prepend=TextSubstitution(text='yes')).visit(lc1)
79+
assert os.environ.get('ANOTHER_NONEXISTENT_KEY') == 'def' + '//' + 'abc'
80+
81+
# Cleanup environment variables
82+
del os.environ['NONEXISTENT_KEY']
83+
del os.environ['ANOTHER_NONEXISTENT_KEY']
84+
del os.environ['SOME_SEPARATOR']
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
"""Test parsing an append_env action."""
16+
17+
import io
18+
import os
19+
import textwrap
20+
21+
from launch.actions import AppendEnvironmentVariable
22+
from launch.frontend import Parser
23+
24+
25+
def test_append_env():
26+
xml_file = \
27+
"""\
28+
<launch>
29+
<append_env name="my_env_var" value="asd"/>
30+
<append_env name="my_env_var" value="zxc" separator="|"/>
31+
<append_env name="my_other_env_var" value="fgh"/>
32+
<append_env name="my_other_env_var" value="jkl" prepend="false"/>
33+
<append_env name="my_other_env_var" value="qwe" prepend="yes"/>
34+
<append_env name="my_other_env_var" value="rty" prepend="true" separator="|"/>
35+
</launch>
36+
"""
37+
xml_file = textwrap.dedent(xml_file)
38+
root_entity, parser = Parser.load(io.StringIO(xml_file))
39+
ld = parser.parse_description(root_entity)
40+
assert len(ld.entities) == 6
41+
assert isinstance(ld.entities[0], AppendEnvironmentVariable)
42+
assert isinstance(ld.entities[1], AppendEnvironmentVariable)
43+
assert isinstance(ld.entities[2], AppendEnvironmentVariable)
44+
assert isinstance(ld.entities[3], AppendEnvironmentVariable)
45+
assert isinstance(ld.entities[4], AppendEnvironmentVariable)
46+
assert isinstance(ld.entities[5], AppendEnvironmentVariable)
47+
assert 'my_env_var' == ''.join([x.perform(None) for x in ld.entities[0].name])
48+
assert 'my_env_var' == ''.join([x.perform(None) for x in ld.entities[0].name])
49+
assert 'my_other_env_var' == ''.join([x.perform(None) for x in ld.entities[2].name])
50+
assert 'my_other_env_var' == ''.join([x.perform(None) for x in ld.entities[3].name])
51+
assert 'my_other_env_var' == ''.join([x.perform(None) for x in ld.entities[4].name])
52+
assert 'my_other_env_var' == ''.join([x.perform(None) for x in ld.entities[5].name])
53+
assert 'asd' == ''.join([x.perform(None) for x in ld.entities[0].value])
54+
assert 'zxc' == ''.join([x.perform(None) for x in ld.entities[1].value])
55+
assert 'fgh' == ''.join([x.perform(None) for x in ld.entities[2].value])
56+
assert 'jkl' == ''.join([x.perform(None) for x in ld.entities[3].value])
57+
assert 'qwe' == ''.join([x.perform(None) for x in ld.entities[4].value])
58+
assert 'rty' == ''.join([x.perform(None) for x in ld.entities[5].value])
59+
assert not ld.entities[0].prepend
60+
assert not ld.entities[1].prepend
61+
assert not ld.entities[2].prepend
62+
assert not ld.entities[3].prepend
63+
assert ld.entities[4].prepend
64+
assert ld.entities[5].prepend
65+
assert os.pathsep == ''.join([x.perform(None) for x in ld.entities[0].separator])
66+
assert '|' == ''.join([x.perform(None) for x in ld.entities[1].separator])
67+
assert os.pathsep == ''.join([x.perform(None) for x in ld.entities[2].separator])
68+
assert os.pathsep == ''.join([x.perform(None) for x in ld.entities[3].separator])
69+
assert os.pathsep == ''.join([x.perform(None) for x in ld.entities[4].separator])
70+
assert '|' == ''.join([x.perform(None) for x in ld.entities[5].separator])
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
"""Test parsing an append_env action."""
16+
17+
import io
18+
import os
19+
import textwrap
20+
21+
from launch.actions import AppendEnvironmentVariable
22+
from launch.frontend import Parser
23+
24+
25+
def test_append_env():
26+
yaml_file = \
27+
"""\
28+
launch:
29+
- append_env:
30+
name: my_env_var
31+
value: asd
32+
- append_env:
33+
name: my_env_var
34+
value: zxc
35+
separator: "|"
36+
- append_env:
37+
name: my_other_env_var
38+
value: fgh
39+
- append_env:
40+
name: my_other_env_var
41+
value: jkl
42+
prepend: false
43+
- append_env:
44+
name: my_other_env_var
45+
value: qwe
46+
prepend: yes
47+
- append_env:
48+
name: my_other_env_var
49+
value: rty
50+
prepend: true
51+
separator: "|"
52+
"""
53+
yaml_file = textwrap.dedent(yaml_file)
54+
root_entity, parser = Parser.load(io.StringIO(yaml_file))
55+
ld = parser.parse_description(root_entity)
56+
assert len(ld.entities) == 6
57+
assert isinstance(ld.entities[0], AppendEnvironmentVariable)
58+
assert isinstance(ld.entities[1], AppendEnvironmentVariable)
59+
assert isinstance(ld.entities[2], AppendEnvironmentVariable)
60+
assert isinstance(ld.entities[3], AppendEnvironmentVariable)
61+
assert isinstance(ld.entities[4], AppendEnvironmentVariable)
62+
assert isinstance(ld.entities[5], AppendEnvironmentVariable)
63+
assert 'my_env_var' == ''.join([x.perform(None) for x in ld.entities[0].name])
64+
assert 'my_env_var' == ''.join([x.perform(None) for x in ld.entities[0].name])
65+
assert 'my_other_env_var' == ''.join([x.perform(None) for x in ld.entities[2].name])
66+
assert 'my_other_env_var' == ''.join([x.perform(None) for x in ld.entities[3].name])
67+
assert 'my_other_env_var' == ''.join([x.perform(None) for x in ld.entities[4].name])
68+
assert 'my_other_env_var' == ''.join([x.perform(None) for x in ld.entities[5].name])
69+
assert 'asd' == ''.join([x.perform(None) for x in ld.entities[0].value])
70+
assert 'zxc' == ''.join([x.perform(None) for x in ld.entities[1].value])
71+
assert 'fgh' == ''.join([x.perform(None) for x in ld.entities[2].value])
72+
assert 'jkl' == ''.join([x.perform(None) for x in ld.entities[3].value])
73+
assert 'qwe' == ''.join([x.perform(None) for x in ld.entities[4].value])
74+
assert 'rty' == ''.join([x.perform(None) for x in ld.entities[5].value])
75+
assert not ld.entities[0].prepend
76+
assert not ld.entities[1].prepend
77+
assert not ld.entities[2].prepend
78+
assert not ld.entities[3].prepend
79+
assert ld.entities[4].prepend
80+
assert ld.entities[5].prepend
81+
assert os.pathsep == ''.join([x.perform(None) for x in ld.entities[0].separator])
82+
assert '|' == ''.join([x.perform(None) for x in ld.entities[1].separator])
83+
assert os.pathsep == ''.join([x.perform(None) for x in ld.entities[2].separator])
84+
assert os.pathsep == ''.join([x.perform(None) for x in ld.entities[3].separator])
85+
assert os.pathsep == ''.join([x.perform(None) for x in ld.entities[4].separator])
86+
assert '|' == ''.join([x.perform(None) for x in ld.entities[5].separator])

0 commit comments

Comments
 (0)