Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allows substitutions with Python Standard Path Objects with PathSubstitutions #790

Open
wants to merge 9 commits into
base: rolling
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions launch/doc/source/architecture.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion launch/launch/launch_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
9 changes: 5 additions & 4 deletions launch/launch/launch_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions launch/launch/some_substitutions_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,6 +26,7 @@
Text,
Substitution,
Iterable[Union[Text, Substitution]],
Path
]

SomeSubstitutionsType_types_tuple = (
Expand Down
2 changes: 2 additions & 0 deletions launch/launch/substitutions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -55,6 +56,7 @@
'NotEqualsSubstitution',
'OrSubstitution',
'PathJoinSubstitution',
'PathSubstitution',
'PythonExpression',
'SubstitutionFailure',
'TextSubstitution',
Expand Down
49 changes: 49 additions & 0 deletions launch/launch/substitutions/path_substitution.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 2 additions & 0 deletions launch/launch/substitutions/python_expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
22 changes: 18 additions & 4 deletions launch/launch/utilities/class_tools_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.')
26 changes: 26 additions & 0 deletions launch/test/launch/substitutions/test_path_substitution.py
Original file line number Diff line number Diff line change
@@ -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')