Skip to content

Commit 48cd144

Browse files
emersonknappmergify[bot]
authored andcommitted
Allow Path in substitutions, instead of requiring cast to str (#873)
Signed-off-by: Emerson Knapp <[email protected]> (cherry picked from commit 49bcf2d) # Conflicts: # launch/launch/substitutions/python_expression.py # launch_xml/test/launch_xml/test_executable.py
1 parent 8569cf1 commit 48cd144

File tree

17 files changed

+90
-47
lines changed

17 files changed

+90
-47
lines changed

launch/launch/launch_description_sources/python_launch_file_utilities.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
from importlib.machinery import SourceFileLoader
1818
from importlib.util import module_from_spec
1919
from importlib.util import spec_from_loader
20+
from pathlib import Path
2021
from types import ModuleType
2122
from typing import Text
23+
from typing import Union
2224

2325
from ..launch_description import LaunchDescription
2426

@@ -29,17 +31,17 @@ class InvalidPythonLaunchFileError(Exception):
2931
...
3032

3133

32-
def load_python_launch_file_as_module(python_launch_file_path: Text) -> ModuleType:
34+
def load_python_launch_file_as_module(python_launch_file_path: Union[Text, Path]) -> ModuleType:
3335
"""Load a given Python launch file (by path) as a Python module."""
34-
loader = SourceFileLoader('python_launch_file', python_launch_file_path)
36+
loader = SourceFileLoader('python_launch_file', str(python_launch_file_path))
3537
spec = spec_from_loader(loader.name, loader)
3638
mod = module_from_spec(spec)
3739
loader.exec_module(mod)
3840
return mod
3941

4042

4143
def get_launch_description_from_python_launch_file(
42-
python_launch_file_path: Text
44+
python_launch_file_path: Union[Text, Path]
4345
) -> LaunchDescription:
4446
"""
4547
Load a given Python launch file (by path), and return the launch description from it.

launch/launch/some_substitutions_type.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Module for SomeSubstitutionsType type."""
1616

1717
import collections.abc
18+
from pathlib import Path
1819
from typing import Iterable
1920
from typing import Text
2021
from typing import Union
@@ -23,12 +24,14 @@
2324

2425
SomeSubstitutionsType = Union[
2526
Text,
27+
Path,
2628
Substitution,
27-
Iterable[Union[Text, Substitution]],
29+
Iterable[Union[Text, Path, Substitution]],
2830
]
2931

3032
SomeSubstitutionsType_types_tuple = (
3133
str,
34+
Path,
3235
Substitution,
3336
collections.abc.Iterable,
3437
)

launch/launch/substitutions/path_join_substitution.py

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

1515
"""Module for the PathJoinSubstitution substitution."""
1616

17-
import os
17+
from pathlib import Path
1818
from typing import Iterable
1919
from typing import List
2020
from typing import Text
@@ -86,7 +86,7 @@ def perform(self, context: LaunchContext) -> Text:
8686
perform_substitutions(context, component_substitutions)
8787
for component_substitutions in self.substitutions
8888
]
89-
return os.path.join(*path_components)
89+
return str(Path(*path_components))
9090

9191
def __truediv__(self, additional_path: SomeSubstitutionsType) -> 'PathJoinSubstitution':
9292
"""Join path substitutions using the / operator, mimicking pathlib.Path operation."""

launch/launch/substitutions/python_expression.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@
1515
"""Module for the PythonExpression substitution."""
1616

1717
import collections.abc
18+
<<<<<<< HEAD
1819
import math
1920
from typing import Iterable
21+
=======
22+
import importlib
23+
from pathlib import Path
24+
>>>>>>> 49bcf2d (Allow Path in substitutions, instead of requiring cast to str (#873))
2025
from typing import List
2126
from typing import Text
2227

@@ -53,9 +58,36 @@ def __init__(self, expression: SomeSubstitutionsType) -> None:
5358
@classmethod
5459
def parse(cls, data: Iterable[SomeSubstitutionsType]):
5560
"""Parse `PythonExpression` substitution."""
61+
<<<<<<< HEAD
5662
if len(data) != 1:
5763
raise TypeError('eval substitution expects 1 argument')
5864
return cls, {'expression': data[0]}
65+
=======
66+
if len(data) < 1 or len(data) > 2:
67+
raise TypeError('eval substitution expects 1 or 2 arguments')
68+
kwargs = {}
69+
kwargs['expression'] = data[0]
70+
if len(data) == 2:
71+
# We get a text substitution from XML,
72+
# whose contents are comma-separated module names
73+
kwargs['python_modules'] = []
74+
# Check if we got empty list from XML
75+
# Ensure that we got a list!
76+
assert not isinstance(data[1], str)
77+
assert not isinstance(data[1], Path)
78+
assert not isinstance(data[1], Substitution)
79+
# Modules
80+
modules = list(data[1])
81+
if len(modules) > 0:
82+
# XXX: What is going on here: the type annotation says we should get
83+
# a either strings or substitutions, but this says that we're
84+
# getting a substitution always?
85+
# Moreover, `perform` is called with `None`, which is not acceptable
86+
# for any substitution as far as I know (should be an empty launch context?)
87+
modules_str = modules[0].perform(None) # type: ignore
88+
kwargs['python_modules'] = [module.strip() for module in modules_str.split(',')]
89+
return cls, kwargs
90+
>>>>>>> 49bcf2d (Allow Path in substitutions, instead of requiring cast to str (#873))
5991

6092
@property
6193
def expression(self) -> List[Substitution]:

launch/launch/utilities/normalize_to_list_of_substitutions_impl.py

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

1515
"""Module for the normalize_to_list_of_substitutions() utility function."""
1616

17+
from pathlib import Path
1718
from typing import cast
1819
from typing import Iterable
1920
from typing import List
@@ -31,14 +32,14 @@ def normalize_to_list_of_substitutions(subs: SomeSubstitutionsType) -> List[Subs
3132
def normalize(x):
3233
if isinstance(x, Substitution):
3334
return x
34-
if isinstance(x, str):
35-
return TextSubstitution(text=x)
35+
if isinstance(x, (str, Path)):
36+
return TextSubstitution(text=str(x))
3637
raise TypeError(
3738
"Failed to normalize given item of type '{}', when only "
3839
"'str' or 'launch.Substitution' were expected.".format(type(x)))
3940

40-
if isinstance(subs, str):
41-
return [TextSubstitution(text=subs)]
41+
if isinstance(subs, (str, Path)):
42+
return [TextSubstitution(text=str(subs))]
4243
if is_a_subclass(subs, Substitution):
4344
return [cast(Substitution, subs)]
4445
return [normalize(y) for y in cast(Iterable, subs)]

launch/launch/utilities/typing_file_path.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
# limitations under the License.
1414

1515
import os
16+
from pathlib import Path
1617
import typing
1718

1819
# Type of a filesystem path
19-
FilePath = typing.TypeVar('FilePath', str, bytes, os.PathLike)
20+
FilePath = typing.TypeVar('FilePath', str, bytes, os.PathLike, Path)

launch/test/launch/actions/test_include_launch_description.py

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

1515
"""Tests for the IncludeLaunchDescription action class."""
1616

17-
import os
17+
from pathlib import Path
1818

1919
from launch import LaunchContext
2020
from launch import LaunchDescription
@@ -74,7 +74,7 @@ def test_include_launch_description_launch_file_location():
7474
assert lc1.locals.current_launch_file_directory == '<script>'
7575
assert action.get_asyncio_future() is None
7676

77-
this_file = os.path.abspath(__file__)
77+
this_file = Path(__file__).absolute()
7878
ld2 = LaunchDescription()
7979
action2 = IncludeLaunchDescription(LaunchDescriptionSource(ld2, this_file))
8080
assert 'IncludeLaunchDescription' in action2.describe()
@@ -83,7 +83,7 @@ def test_include_launch_description_launch_file_location():
8383
lc2 = LaunchContext()
8484
# Result should only contain the launch description as there are no launch arguments.
8585
assert action2.visit(lc2) == [ld2]
86-
assert lc2.locals.current_launch_file_directory == os.path.dirname(this_file)
86+
assert lc2.locals.current_launch_file_directory == str(this_file.parent)
8787
assert action2.get_asyncio_future() is None
8888

8989

@@ -209,11 +209,8 @@ def test_include_launch_description_launch_arguments():
209209

210210
def test_include_python():
211211
"""Test including Python, with and without explicit PythonLaunchDescriptionSource."""
212-
this_dir = os.path.dirname(os.path.abspath(__file__))
213-
simple_launch_file_path = os.path.join(this_dir,
214-
'..',
215-
'launch_description_source',
216-
'simple.launch.py')
212+
this_dir = Path(__file__).parent
213+
simple_launch_file_path = this_dir.parent / 'launch_description_source' / 'simple.launch.py'
217214

218215
# Explicitly construct with PythonLaunchDescriptionSource
219216
plds = PythonLaunchDescriptionSource(simple_launch_file_path)
@@ -232,4 +229,4 @@ def test_include_python():
232229
assert action.get_asyncio_future() is None
233230
assert len(action.launch_arguments) == 0
234231

235-
assert action.launch_description_source.location == simple_launch_file_path
232+
assert action.launch_description_source.location == str(simple_launch_file_path)

launch/test/launch/launch_description_source/test_python_launch_description_source.py

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

1515
"""Tests for the PythonLaunchDescriptionSource class."""
1616

17-
import os
17+
from pathlib import Path
1818

1919
from launch import LaunchContext
2020
from launch.launch_description_sources import InvalidPythonLaunchFileError
@@ -25,17 +25,17 @@
2525

2626
def test_python_launch_description_source():
2727
"""Test the PythonLaunchDescriptionSource class."""
28-
this_dir = os.path.dirname(os.path.abspath(__file__))
29-
simple_launch_file_path = os.path.join(this_dir, 'simple.launch.py')
28+
this_dir = Path(__file__).parent
29+
simple_launch_file_path = this_dir / 'simple.launch.py'
3030
plds = PythonLaunchDescriptionSource(simple_launch_file_path)
3131
assert 'python launch file' in plds.method
3232
assert 'launch.substitutions.text_substitution.TextSubstitution' in plds.location
3333
ld = plds.get_launch_description(LaunchContext())
34-
assert plds.location == simple_launch_file_path
34+
assert plds.location == str(simple_launch_file_path)
3535
assert 0 == len(ld.entities)
3636

3737
with pytest.raises(InvalidPythonLaunchFileError):
38-
plds = PythonLaunchDescriptionSource(os.path.join(this_dir, 'loadable_python_module.py'))
38+
plds = PythonLaunchDescriptionSource(this_dir / 'loadable_python_module.py')
3939
ld = plds.get_launch_description(LaunchContext())
4040

4141
with pytest.raises(FileNotFoundError):

launch/test/launch/launch_description_source/test_python_launch_file_utilities.py

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

1515
"""Tests for the Python launch file utility functions."""
1616

17-
import os
17+
from pathlib import Path
1818
import sys
1919

2020
from launch.launch_description_sources import get_launch_description_from_python_launch_file
@@ -26,8 +26,8 @@
2626

2727
def test_load_python_launch_file_as_module():
2828
"""Test load_python_launch_file_as_module()."""
29-
this_dir = os.path.dirname(os.path.abspath(__file__))
30-
module = load_python_launch_file_as_module(os.path.join(this_dir, 'loadable_python_module.py'))
29+
this_dir = Path(__file__).parent
30+
module = load_python_launch_file_as_module(this_dir / 'loadable_python_module.py')
3131
assert sys.executable == module.some_function()
3232

3333
with pytest.raises(FileNotFoundError):
@@ -36,13 +36,12 @@ def test_load_python_launch_file_as_module():
3636

3737
def test_get_launch_description_from_python_launch_file():
3838
"""Test get_launch_description_from_python_launch_file()."""
39-
this_dir = os.path.dirname(os.path.abspath(__file__))
40-
ld = get_launch_description_from_python_launch_file(os.path.join(this_dir, 'simple.launch.py'))
39+
this_dir = Path(__file__).parent
40+
ld = get_launch_description_from_python_launch_file(this_dir / 'simple.launch.py')
4141
assert 0 == len(ld.entities)
4242

4343
with pytest.raises(InvalidPythonLaunchFileError):
44-
ld = get_launch_description_from_python_launch_file(
45-
os.path.join(this_dir, 'loadable_python_module.py'))
44+
ld = get_launch_description_from_python_launch_file(this_dir / 'loadable_python_module.py')
4645

4746
with pytest.raises(FileNotFoundError):
4847
ld = get_launch_description_from_python_launch_file('does_not_exist')

launch/test/launch/substitutions/test_command.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ def commands():
2929
this_dir = pathlib.Path(__file__).parent
3030

3131
commands = {
32-
'normal': str(this_dir / 'test_command' / 'normal_command.bash'),
33-
'failing': str(this_dir / 'test_command' / 'failing_command.bash'),
34-
'with_stderr': str(this_dir / 'test_command' / 'command_with_stderr.bash')
32+
'normal': this_dir / 'test_command' / 'normal_command.bash',
33+
'failing': this_dir / 'test_command' / 'failing_command.bash',
34+
'with_stderr': this_dir / 'test_command' / 'command_with_stderr.bash'
3535
}
3636

3737
if os.name == 'nt':
3838
for key, value in commands.items():
39-
commands[key] = value.replace('bash', 'bat')
39+
commands[key] = value.with_suffix('.bat')
4040
return commands
4141

4242

0 commit comments

Comments
 (0)