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. 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_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/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/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/__init__.py b/launch/launch/substitutions/__init__.py index 1622debaa..021595110 100644 --- a/launch/launch/substitutions/__init__.py +++ b/launch/launch/substitutions/__init__.py @@ -31,6 +31,7 @@ from .local_substitution import LocalSubstitution from .not_equals_substitution import NotEqualsSubstitution 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 @@ -55,6 +56,7 @@ 'NotEqualsSubstitution', 'OrSubstitution', 'PathJoinSubstitution', + 'PathSubstitution', 'PythonExpression', 'SubstitutionFailure', 'TextSubstitution', diff --git a/launch/launch/substitutions/path_substitution.py b/launch/launch/substitutions/path_substitution.py new file mode 100644 index 000000000..94c191f67 --- /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 Path object got '{}' instead.".format(type(path)) + ) + + self.__path = path + + @property + def path(self) -> Path: + """Getter for path.""" + return self.__path + + 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/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/launch/utilities/class_tools_impl.py b/launch/launch/utilities/class_tools_impl.py index 346e2a783..8198bfb2a 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 overload, Type, TYPE_CHECKING, TypeVar +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,19 @@ def is_a(obj, entity_type): return isinstance(obj, entity_type) +@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 - 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_substitution.py b/launch/test/launch/substitutions/test_path_substitution.py new file mode 100644 index 000000000..e1dc12af4 --- /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=path) + assert sub.perform(None) == os.path.join('asd', 'bsd', 'cds')