From ac4170e4e2b381b005d5188945382b440753239a Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Thu, 18 Jul 2024 19:20:41 -0400 Subject: [PATCH 1/9] PathSubstitutions Signed-off-by: Michael Carlstrom --- .../python_launch_description_source.py | 4 +- launch/launch/some_substitutions_type.py | 2 + .../launch/substitutions/path_substitution.py | 49 +++++++++++++++++++ launch/launch/utilities/class_tools_impl.py | 16 ++++-- ...normalize_to_list_of_substitutions_impl.py | 20 +++++--- .../test_path_join_substitution copy.py | 25 ++++++++++ .../test_path_join_substitution.py | 11 +++-- 7 files changed, 110 insertions(+), 17 deletions(-) create mode 100644 launch/launch/substitutions/path_substitution.py create mode 100644 launch/test/launch/substitutions/test_path_join_substitution copy.py diff --git a/launch/launch/launch_description_sources/python_launch_description_source.py b/launch/launch/launch_description_sources/python_launch_description_source.py index e5ffc86fb..cac3b536c 100644 --- a/launch/launch/launch_description_sources/python_launch_description_source.py +++ b/launch/launch/launch_description_sources/python_launch_description_source.py @@ -14,6 +14,8 @@ """Module for the PythonLaunchDescriptionSource class.""" +from typing import Text + from .python_launch_file_utilities import get_launch_description_from_python_launch_file from ..launch_description_source import LaunchDescriptionSource from ..some_substitutions_type import SomeSubstitutionsType @@ -46,6 +48,6 @@ def __init__( 'interpreted python launch file' ) - def _get_launch_description(self, location): + def _get_launch_description(self, location: Text): """Get the LaunchDescription from location.""" return get_launch_description_from_python_launch_file(location) diff --git a/launch/launch/some_substitutions_type.py b/launch/launch/some_substitutions_type.py index 9f02ae5eb..67cc3fe71 100644 --- a/launch/launch/some_substitutions_type.py +++ b/launch/launch/some_substitutions_type.py @@ -15,6 +15,7 @@ """Module for SomeSubstitutionsType type.""" import collections.abc +from pathlib import Path from typing import Iterable from typing import Text from typing import Union @@ -25,6 +26,7 @@ Text, Substitution, Iterable[Union[Text, Substitution]], + Path ] SomeSubstitutionsType_types_tuple = ( diff --git a/launch/launch/substitutions/path_substitution.py b/launch/launch/substitutions/path_substitution.py new file mode 100644 index 000000000..e892e2bf6 --- /dev/null +++ b/launch/launch/substitutions/path_substitution.py @@ -0,0 +1,49 @@ +# Copyright 2018 Open Source Robotics Foundation, 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. + +"""Module for the PathSubstitution substitution.""" + +from pathlib import Path +from typing import Text + +from ..launch_context import LaunchContext +from ..substitution import Substitution + + +class PathSubstitution(Substitution): + """Substitution that wraps a single string text.""" + + def __init__(self, *, path: Path) -> None: + """Create a PathSubstitution.""" + super().__init__() + + if not isinstance(path, Path): + raise TypeError( + "PathSubstitution expected Text object got '{}' instead.".format(type(path)) + ) + + self.__text = path + + @property + def path(self) -> Path: + """Getter for text.""" + return self.__text + + def describe(self) -> Text: + """Return a description of this substitution as a string.""" + return "'{}'".format(self.path) + + def perform(self, context: LaunchContext) -> Text: + """Perform the substitution by returning the string itself.""" + return str(self.path) diff --git a/launch/launch/utilities/class_tools_impl.py b/launch/launch/utilities/class_tools_impl.py index 346e2a783..eb2a749d2 100644 --- a/launch/launch/utilities/class_tools_impl.py +++ b/launch/launch/utilities/class_tools_impl.py @@ -15,14 +15,20 @@ """Module for the class tools utility functions.""" import inspect +from typing import Type, TYPE_CHECKING, TypeVar, Union +T = TypeVar('T') -def isclassinstance(obj): +if TYPE_CHECKING: + from typing import TypeGuard + + +def isclassinstance(obj: object) -> bool: """Return True if obj is an instance of a class.""" return hasattr(obj, '__class__') -def is_a(obj, entity_type): +def is_a(obj: object, entity_type: Type[T]) -> 'TypeGuard[T]': """Return True if obj is an instance of the entity_type class.""" if not isclassinstance(obj): raise RuntimeError("obj '{}' is not a class instance".format(obj)) @@ -31,11 +37,11 @@ def is_a(obj, entity_type): return isinstance(obj, entity_type) -def is_a_subclass(obj, entity_type): +def is_a_subclass(obj: Union[object, type], entity_type: Type[T]) -> 'TypeGuard[T]': """Return True if obj is an instance of the entity_type class or one of its subclass types.""" if is_a(obj, entity_type): return True - try: + elif isinstance(obj, type): return issubclass(obj, entity_type) - except TypeError: + else: return False diff --git a/launch/launch/utilities/normalize_to_list_of_substitutions_impl.py b/launch/launch/utilities/normalize_to_list_of_substitutions_impl.py index 160a71c94..bc267b60a 100644 --- a/launch/launch/utilities/normalize_to_list_of_substitutions_impl.py +++ b/launch/launch/utilities/normalize_to_list_of_substitutions_impl.py @@ -14,9 +14,10 @@ """Module for the normalize_to_list_of_substitutions() utility function.""" -from typing import cast +from pathlib import Path from typing import Iterable from typing import List +from typing import Union from .class_tools_impl import is_a_subclass from ..some_substitutions_type import SomeSubstitutionsType @@ -26,19 +27,26 @@ def normalize_to_list_of_substitutions(subs: SomeSubstitutionsType) -> List[Substitution]: """Return a list of Substitutions given a variety of starting inputs.""" # Avoid recursive import - from ..substitutions import TextSubstitution + from ..substitutions import TextSubstitution, PathSubstitution - def normalize(x): + def normalize(x: Union[str, Substitution, Path]) -> Substitution: if isinstance(x, Substitution): return x if isinstance(x, str): return TextSubstitution(text=x) + if isinstance(x, Path): + return PathSubstitution(path=x) raise TypeError( "Failed to normalize given item of type '{}', when only " "'str' or 'launch.Substitution' were expected.".format(type(x))) if isinstance(subs, str): return [TextSubstitution(text=subs)] - if is_a_subclass(subs, Substitution): - return [cast(Substitution, subs)] - return [normalize(y) for y in cast(Iterable, subs)] + elif isinstance(subs, Path): + return [PathSubstitution(path=subs)] + elif is_a_subclass(subs, Substitution): + return [subs] + elif isinstance(subs, Iterable): + return [normalize(y) for y in subs] + + raise TypeError(f'{subs} is not a valid SomeSubstitutionsType.') diff --git a/launch/test/launch/substitutions/test_path_join_substitution copy.py b/launch/test/launch/substitutions/test_path_join_substitution copy.py new file mode 100644 index 000000000..0838c7e7c --- /dev/null +++ b/launch/test/launch/substitutions/test_path_join_substitution copy.py @@ -0,0 +1,25 @@ +# Copyright 2019 Open Source Robotics Foundation, 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. + +"""Tests for the PathJoinSubstitution substitution class.""" + +import os + +from launch.substitutions import PathJoinSubstitution + + +def test_path_join(): + path = ['asd', 'bsd', 'cds'] + sub = PathJoinSubstitution(path) + assert sub.perform(None) == os.path.join(*path) diff --git a/launch/test/launch/substitutions/test_path_join_substitution.py b/launch/test/launch/substitutions/test_path_join_substitution.py index 0838c7e7c..af2d9717f 100644 --- a/launch/test/launch/substitutions/test_path_join_substitution.py +++ b/launch/test/launch/substitutions/test_path_join_substitution.py @@ -12,14 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Tests for the PathJoinSubstitution substitution class.""" +"""Tests for the PathSubstitution substitution class.""" import os +from pathlib import Path -from launch.substitutions import PathJoinSubstitution +from launch.substitutions import PathSubstitution def test_path_join(): - path = ['asd', 'bsd', 'cds'] - sub = PathJoinSubstitution(path) - assert sub.perform(None) == os.path.join(*path) + path = Path('asd') / 'bsd' / 'cds' + sub = PathSubstitution(path) + assert sub.perform(None) == os.path.join('asd', 'bsd', 'cds') From 1851cd5d6cc5a23c4d951de477b47f87cc89ba8e Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Thu, 18 Jul 2024 19:24:51 -0400 Subject: [PATCH 2/9] remove copied file Signed-off-by: Michael Carlstrom --- .../test_path_join_substitution copy.py | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 launch/test/launch/substitutions/test_path_join_substitution copy.py diff --git a/launch/test/launch/substitutions/test_path_join_substitution copy.py b/launch/test/launch/substitutions/test_path_join_substitution copy.py deleted file mode 100644 index 0838c7e7c..000000000 --- a/launch/test/launch/substitutions/test_path_join_substitution copy.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2019 Open Source Robotics Foundation, 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. - -"""Tests for the PathJoinSubstitution substitution class.""" - -import os - -from launch.substitutions import PathJoinSubstitution - - -def test_path_join(): - path = ['asd', 'bsd', 'cds'] - sub = PathJoinSubstitution(path) - assert sub.perform(None) == os.path.join(*path) From 8228f4d71ec0d3fb051b093f3315c3b3f3a31415 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Thu, 18 Jul 2024 19:27:27 -0400 Subject: [PATCH 3/9] fix property name Signed-off-by: Michael Carlstrom --- launch/launch/substitutions/path_substitution.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/launch/launch/substitutions/path_substitution.py b/launch/launch/substitutions/path_substitution.py index e892e2bf6..755727af3 100644 --- a/launch/launch/substitutions/path_substitution.py +++ b/launch/launch/substitutions/path_substitution.py @@ -33,12 +33,12 @@ def __init__(self, *, path: Path) -> None: "PathSubstitution expected Text object got '{}' instead.".format(type(path)) ) - self.__text = path + self.__path = path @property def path(self) -> Path: - """Getter for text.""" - return self.__text + """Getter for path.""" + return self.__path def describe(self) -> Text: """Return a description of this substitution as a string.""" From 85e23ccfdd2f3db20af8c7a2ff9758b88bf1c16b Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Thu, 18 Jul 2024 19:28:45 -0400 Subject: [PATCH 4/9] Update __init__.py Signed-off-by: Michael Carlstrom --- launch/launch/substitutions/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/launch/launch/substitutions/__init__.py b/launch/launch/substitutions/__init__.py index 1622debaa..3f3237230 100644 --- a/launch/launch/substitutions/__init__.py +++ b/launch/launch/substitutions/__init__.py @@ -30,6 +30,7 @@ from .launch_log_dir import LaunchLogDir from .local_substitution import LocalSubstitution from .not_equals_substitution import NotEqualsSubstitution +from .path_substitution import PathSubstitution from .path_join_substitution import PathJoinSubstitution from .python_expression import PythonExpression from .substitution_failure import SubstitutionFailure From 27fac8d15157d5432329bc668c3c7ad1e55a0966 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Thu, 18 Jul 2024 19:37:58 -0400 Subject: [PATCH 5/9] Add back deleted teste Signed-off-by: Michael Carlstrom --- launch/launch/substitutions/__init__.py | 3 ++- .../launch/substitutions/python_expression.py | 2 ++ .../test_path_join_substitution.py | 11 ++++---- .../substitutions/test_path_substitution.py | 26 +++++++++++++++++++ 4 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 launch/test/launch/substitutions/test_path_substitution.py diff --git a/launch/launch/substitutions/__init__.py b/launch/launch/substitutions/__init__.py index 3f3237230..021595110 100644 --- a/launch/launch/substitutions/__init__.py +++ b/launch/launch/substitutions/__init__.py @@ -30,8 +30,8 @@ from .launch_log_dir import LaunchLogDir from .local_substitution import LocalSubstitution from .not_equals_substitution import NotEqualsSubstitution -from .path_substitution import PathSubstitution from .path_join_substitution import PathJoinSubstitution +from .path_substitution import PathSubstitution from .python_expression import PythonExpression from .substitution_failure import SubstitutionFailure from .text_substitution import TextSubstitution @@ -56,6 +56,7 @@ 'NotEqualsSubstitution', 'OrSubstitution', 'PathJoinSubstitution', + 'PathSubstitution', 'PythonExpression', 'SubstitutionFailure', 'TextSubstitution', diff --git a/launch/launch/substitutions/python_expression.py b/launch/launch/substitutions/python_expression.py index 66f02c078..a1f087fa4 100644 --- a/launch/launch/substitutions/python_expression.py +++ b/launch/launch/substitutions/python_expression.py @@ -16,6 +16,7 @@ import collections.abc import importlib +from pathlib import Path from typing import List from typing import Sequence from typing import Text @@ -73,6 +74,7 @@ def parse(cls, data: Sequence[SomeSubstitutionsType]): # Ensure that we got a list! assert not isinstance(data[1], str) assert not isinstance(data[1], Substitution) + assert not isinstance(data[1], Path) # Modules modules = list(data[1]) if len(modules) > 0: diff --git a/launch/test/launch/substitutions/test_path_join_substitution.py b/launch/test/launch/substitutions/test_path_join_substitution.py index af2d9717f..0838c7e7c 100644 --- a/launch/test/launch/substitutions/test_path_join_substitution.py +++ b/launch/test/launch/substitutions/test_path_join_substitution.py @@ -12,15 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Tests for the PathSubstitution substitution class.""" +"""Tests for the PathJoinSubstitution substitution class.""" import os -from pathlib import Path -from launch.substitutions import PathSubstitution +from launch.substitutions import PathJoinSubstitution def test_path_join(): - path = Path('asd') / 'bsd' / 'cds' - sub = PathSubstitution(path) - assert sub.perform(None) == os.path.join('asd', 'bsd', 'cds') + path = ['asd', 'bsd', 'cds'] + sub = PathJoinSubstitution(path) + assert sub.perform(None) == os.path.join(*path) diff --git a/launch/test/launch/substitutions/test_path_substitution.py b/launch/test/launch/substitutions/test_path_substitution.py new file mode 100644 index 000000000..af2d9717f --- /dev/null +++ b/launch/test/launch/substitutions/test_path_substitution.py @@ -0,0 +1,26 @@ +# Copyright 2019 Open Source Robotics Foundation, 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. + +"""Tests for the PathSubstitution substitution class.""" + +import os +from pathlib import Path + +from launch.substitutions import PathSubstitution + + +def test_path_join(): + path = Path('asd') / 'bsd' / 'cds' + sub = PathSubstitution(path) + assert sub.perform(None) == os.path.join('asd', 'bsd', 'cds') From 66966298a54ebd4c024214af26e92da8db894fb1 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Thu, 18 Jul 2024 19:42:53 -0400 Subject: [PATCH 6/9] Use kwarg Signed-off-by: Michael Carlstrom --- launch/test/launch/substitutions/test_path_substitution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launch/test/launch/substitutions/test_path_substitution.py b/launch/test/launch/substitutions/test_path_substitution.py index af2d9717f..e1dc12af4 100644 --- a/launch/test/launch/substitutions/test_path_substitution.py +++ b/launch/test/launch/substitutions/test_path_substitution.py @@ -22,5 +22,5 @@ def test_path_join(): path = Path('asd') / 'bsd' / 'cds' - sub = PathSubstitution(path) + sub = PathSubstitution(path=path) assert sub.perform(None) == os.path.join('asd', 'bsd', 'cds') From 420a50079a55c34a8a34fdf7d82f2dd36296a3a4 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Thu, 18 Jul 2024 21:37:36 -0400 Subject: [PATCH 7/9] Add overloads for is_a_subclass Signed-off-by: Michael Carlstrom --- launch/launch/launch_context.py | 2 +- launch/launch/launch_service.py | 9 +++++---- launch/launch/utilities/class_tools_impl.py | 12 ++++++++++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/launch/launch/launch_context.py b/launch/launch/launch_context.py index ecd29cb19..28afb2d17 100644 --- a/launch/launch/launch_context.py +++ b/launch/launch/launch_context.py @@ -53,7 +53,7 @@ def __init__( self.__noninteractive = noninteractive self._event_queue: asyncio.Queue = asyncio.Queue() - self._event_handlers: collections.deque = collections.deque() + self._event_handlers: collections.deque[BaseEventHandler] = collections.deque() self._completion_futures: List[asyncio.Future] = [] self.__globals: Dict[Text, Any] = {} diff --git a/launch/launch/launch_service.py b/launch/launch/launch_service.py index 2e1719edf..b55ac8d5a 100644 --- a/launch/launch/launch_service.py +++ b/launch/launch/launch_service.py @@ -237,17 +237,18 @@ async def __process_event(self, event: Event) -> None: "processing event: '{}' ✓ '{}'".format(event, event_handler)) self.__context._push_locals() entities = event_handler.handle(event, self.__context) - entities = \ + iterable_entities = \ entities if isinstance(entities, collections.abc.Iterable) else (entities,) - for entity in [e for e in entities if e is not None]: + for entity in [e for e in iterable_entities if e is not None]: from .utilities import is_a_subclass if not is_a_subclass(entity, LaunchDescriptionEntity): raise RuntimeError( "expected a LaunchDescriptionEntity from event_handler, got '{}'" .format(entity) ) - self._entity_future_pairs.extend( - visit_all_entities_and_collect_futures(entity, self.__context)) + else: + self._entity_future_pairs.extend( + visit_all_entities_and_collect_futures(entity, self.__context)) self.__context._pop_locals() else: pass diff --git a/launch/launch/utilities/class_tools_impl.py b/launch/launch/utilities/class_tools_impl.py index eb2a749d2..8198bfb2a 100644 --- a/launch/launch/utilities/class_tools_impl.py +++ b/launch/launch/utilities/class_tools_impl.py @@ -15,7 +15,7 @@ """Module for the class tools utility functions.""" import inspect -from typing import Type, TYPE_CHECKING, TypeVar, Union +from typing import overload, Type, TYPE_CHECKING, TypeVar T = TypeVar('T') @@ -37,7 +37,15 @@ def is_a(obj: object, entity_type: Type[T]) -> 'TypeGuard[T]': return isinstance(obj, entity_type) -def is_a_subclass(obj: Union[object, type], entity_type: Type[T]) -> 'TypeGuard[T]': +@overload +def is_a_subclass(obj: type, entity_type: Type[T]) -> 'TypeGuard[Type[T]]': ... + + +@overload +def is_a_subclass(obj: object, entity_type: Type[T]) -> 'TypeGuard[T]': ... + + +def is_a_subclass(obj, entity_type): """Return True if obj is an instance of the entity_type class or one of its subclass types.""" if is_a(obj, entity_type): return True From 93b9350307d8c0f271bae60998ba8ebdf81ad20a Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 9 Aug 2024 11:31:51 -0400 Subject: [PATCH 8/9] update doc Signed-off-by: Michael Carlstrom --- launch/doc/source/architecture.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/launch/doc/source/architecture.rst b/launch/doc/source/architecture.rst index 112865ce4..474622923 100644 --- a/launch/doc/source/architecture.rst +++ b/launch/doc/source/architecture.rst @@ -122,6 +122,11 @@ There are many possible variations of a substitution, but here are some of the c - This substitution simply returns the given string when evaluated. - It is usually used to wrap literals in the launch description so they can be concatenated with other substitutions. +- :class:`launch.substitutions.PathSubstitution` + + - This substitution simply returns the given Path object in string form. + - It is usually used to support Python Path objects in substitutions. + - :class:`launch.substitutions.PythonExpression` - This substitution will evaluate a python expression and get the result as a string. From b9b513da024c2ac27ad49661a569e2e6c72d21ef Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Tue, 27 Aug 2024 12:06:00 -0400 Subject: [PATCH 9/9] Update path_substitution.py Signed-off-by: Michael Carlstrom --- launch/launch/substitutions/path_substitution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launch/launch/substitutions/path_substitution.py b/launch/launch/substitutions/path_substitution.py index 755727af3..94c191f67 100644 --- a/launch/launch/substitutions/path_substitution.py +++ b/launch/launch/substitutions/path_substitution.py @@ -30,7 +30,7 @@ def __init__(self, *, path: Path) -> None: if not isinstance(path, Path): raise TypeError( - "PathSubstitution expected Text object got '{}' instead.".format(type(path)) + "PathSubstitution expected Path object got '{}' instead.".format(type(path)) ) self.__path = path