Skip to content

[launch] refactor introspection system #103

@wjwwood

Description

@wjwwood

Some effort was already invested to express intent without side-effects in the new launch api, and each entity in a launch description can express itself and related, or "sub" entities. However, the introspection interfaces of various entities (Actions, Event Handlers, Events, Subsitutions, etc...) need to be refined to avoid impassible and opaque boundaries in the description when introspecting.

I have some ideas of what needs to change already, but ran out of time before the bouncy release. The launch.LaunchDescriptionEntity interfaces for this are the most "mature" and reflect somewhat closely what I'd like to update the other entities (like EventHandler and Substitution) to look like in the future:

def describe(self) -> Text:
"""
Return a description of this entity as a string.
When inherited from, calling this base class's default method is not
required, and in fact it will raise NotImplementedError.
"""
raise NotImplementedError()
def describe_sub_entities(self) -> List['LaunchDescriptionEntity']:
"""
Return a list of sub-entities which need to be described as well.
The list may be empty.
The sub-entities in this list should always be returned when this
entity is visited at runtime.
If there are entities which are only returned under some condition,
use the describe_conditional_sub_entities() method instead.
In the case of multiple layers of inheritance, you may wish to call
other base class's describe_sub_entities() and extend your own list of
sub-entities depending on the behavior of your class, but calling this
default method is not required.
"""
return []
def describe_conditional_sub_entities(self) -> List[Tuple[
Text, # text description of the condition
Iterable['LaunchDescriptionEntity'], # list of conditional sub-entities
]]:
"""
Return a list of condition descriptions and lists of sub-entities.
The list of conditional sub-entities are made up of two item tuples,
where the first item is a text description of the condition, and the
second item is a list of sub-entities which would be visited if the
condition is evaluated to be true during launch.
In the case of multiple layers of inheritance, you may wish to call the
base class's describe_conditional_sub_entities() and extend your own
list of sub-entities depending on the behavior of your class, but
calling this default method is not required.
"""
return []

The above interfaces may also need some work. Additionally, I think that right now the mechanism to "render" an entity during introspection (render to text for printing on the console in the case of launch.LaunchIntrospector) needs to be placed with or along side the entities themselves. Currently this logic is in the LaunchIntrospector, which was only done to get something working rapidly:

def format_entities(entities: List[LaunchDescriptionEntity]) -> List[Text]:
"""Return a list of lines of text that represent of a list of LaunchDescriptionEntity's."""
result = []
for entity in entities:
if is_a(entity, Action):
result.extend(format_action(cast(Action, entity)))
else:
result.append("Unknown entity('{}')".format(entity))
return result
def format_substitutions(substitutions: SomeSubstitutionsType) -> Text:
"""Return a text representation of some set of substitutions."""
normalized_substitutions = normalize_to_list_of_substitutions(substitutions)
result = ''
for sub in normalized_substitutions:
result += ' + '
if is_a(sub, TextSubstitution):
result += "'{}'".format(cast(TextSubstitution, sub).text)
elif is_a(sub, EnvironmentVariable):
result += 'EnvVarSub({})'.format(
format_substitutions(cast(EnvironmentVariable, sub).name))
elif is_a(sub, FindExecutable):
result += 'FindExecSub({})'.format(
format_substitutions(cast(FindExecutable, sub).name))
else:
result += "Substitution('{}')".format(sub)
return result[3:]
def format_event_handler(event_handler: EventHandler) -> List[Text]:
"""Return a text representation of an event handler."""
if hasattr(event_handler, 'describe'):
# TODO(wjwwood): consider supporting mode complex descriptions of branching
description, entities = event_handler.describe() # type: ignore
result = [description]
result.extend(indent(format_entities(entities)))
return result
else:
return ["EventHandler('{}')".format(hex(id(event_handler)))]
def format_action(action: Action) -> List[Text]:
"""Return a text representation of an action."""
if is_a(action, LogInfo):
return ['LogInfo({})'.format(format_substitutions(cast(LogInfo, action).msg))]
elif is_a(action, EmitEvent):
return ["EmitEvent(event='{}')".format(cast(EmitEvent, action).event.name)]
elif is_a(action, ExecuteProcess):
typed_action = cast(ExecuteProcess, action)
msg = 'ExecuteProcess(cmd=[{}], cwd={}, env={}, shell={})'.format(
', '.join([format_substitutions(x) for x in typed_action.cmd]),
typed_action.cwd if typed_action.cwd is None else "'{}'".format(
format_substitutions(typed_action.cwd)
),
typed_action.env if typed_action.env is None else '{' + ', '.join(
['{}: {}'.format(format_substitutions(k), format_substitutions(v))
for k, v in typed_action.env.items()] + '}'),
typed_action.shell,
)
return [msg]
elif is_a(action, RegisterEventHandler):
# Different variable name used to assist with type checking.
typed_action2 = cast(RegisterEventHandler, action)
result = ["RegisterEventHandler('{}'):".format(typed_action2.event_handler)]
result.extend(indent(format_event_handler(typed_action2.event_handler)))
return result
else:
return ["Action('{}')".format(action)]

Lastly, specific entities need to implement these interfaces and handle any special logic to make them useful during introspection. Specifically I noticed:

  • launch.EventHandlers could report what is yielded when an event is handled
  • Update introspection support for some launch.Actions (e.g. the ExecuteProcess action could report more of what it might do in certain situations)

A stretch goal of this refactoring would be to add some notion of "details" when introspecting, e.g. you might just want to see that a process is getting run and not all the various and tedious things it might do in every situation, unless you asked for those details, in which case it would show them. Basically some way for an entity to express that some sub entities and parts of its own description are implementation details and not usually important to express.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions