Source code for manyworlds.scenario

"""Defines the Scenario Class"""
# needed to support "type[]" class type annotations in Python 3.8:
from __future__ import annotations

import re
import igraph as ig  # type: ignore
from typing import Optional, Union, List

from .step import Step, Prerequisite, Action, Assertion


[docs]class Scenario: """A BDD Scenario""" SCENARIO_PATTERN: re.Pattern = re.compile( r""" ^ # start of line Scenario: # "Scenario:" keyword [ ] # space (?P<scenario_name>[^#]*) # scenario name (?:\# (?P<comment>.+))? # optional comment $ # end of line """, re.VERBOSE, ) """ re.Pattern Pattern describing a BDD scenario line ("Scenario: …") followed by an optional comment """ name: str graph: ig.Graph vertex: ig.Vertex steps: List[Step] comment: Optional[str] _validated: bool def __init__( self, name: str, graph: ig.Graph, parent_scenario: Optional["Scenario"] = None, comment: Optional[str] = None, ) -> None: """Constructor method Parameters ---------- name : str The name of the scenario graph : igraph.Graph The graph parent_scenario: Scenario (optional) The parent scenario to connect the new scenario to comment : str, optional """ self.name = name.strip() self.graph = graph self.vertex = graph.add_vertex() self.vertex["scenario"] = self self.steps = [] self._validated = False self.comment = comment.strip() if comment is not None else None if parent_scenario is not None: self.graph.add_edge(parent_scenario.vertex, self.vertex) @property def validated(self) -> bool: """The "validated" property Used to keep track of which scenarios had their assertions written to an output scenario already so that assertions are not run multiple times. Returns ------- bool Whether or not this scenario has been validated """ return self._validated @validated.setter def validated(self, value: bool) -> None: """The validated property setter Parameters ---------- value : bool Whether or not this scenario has been validated """ self._validated = value
[docs] def prerequisites(self) -> List[Step]: """Returns all steps of type Prerequisite Returns ------- List[Prerequisite] List of steps of type Prerequisite """ return self.steps_of_type(Prerequisite)
[docs] def actions(self) -> List[Step]: """Returns all steps of type Action Returns ------- List[Action] List of steps of type Action """ return self.steps_of_type(Action)
[docs] def assertions(self) -> List[Step]: """Returns all steps of type Assertion Returns ---------- list List[Assertion] List of steps of type Assertion """ return self.steps_of_type(Assertion)
[docs] def steps_of_type( self, step_type: Union[type[Prerequisite], type[Action], type[Assertion]] ) -> List[Step]: """Returns all steps of the passed in type Parameters ---------- step_class : {Prerequisite, Action, Assertion} A step subclass Returns ------- List[Step] All steps of the passed in type """ return [st for st in self.steps if type(st) is step_type]
def __str__(self) -> str: """Returns a string representation of the Scenario instance for terminal output. Returns ------- str String representation of the Scenario instance """ return "<Scenario: {} ({} prerequisites, {} actions, {} assertions)>".format( self.name, len(self.prerequisites()), len(self.actions()), len(self.assertions()), ) def __repr__(self) -> str: """Returns a string representation of the Scenario instance for terminal output. Returns ------- str String representation of the Scenario instance """ return self.__str__()
[docs] def ancestors(self) -> List["Scenario"]: """Returns the scenario's ancestors, starting with a root scenario Returns ------- List[Scenario] List of scenarios """ ancestors: List[ig.Vertex] = self.graph.neighborhood( self.vertex, mode="IN", order=1000, mindist=1, ) ancestors.reverse() return [vx["scenario"] for vx in self.graph.vs(ancestors)]
[docs] def parent(self) -> Optional[Scenario]: """Returns the scenario's parent scenario, if one exists Returns ------- Scenario, optional The parent scenario """ parents: List[ig.Vertex] = self.graph.neighborhood( self.vertex, mode="IN", order=1, mindist=1 ) if len(parents) == 1: return self.graph.vs[parents[0]]["scenario"] else: return None
[docs] def children(self) -> List[Scenario]: """Returns the scenario's child scenarios Returns ------- List[Scenario] The child scenarios """ children: List[ig.Vertex] = self.graph.neighborhood( self.vertex, mode="OUT", order=1, mindist=1 ) return [vx["scenario"] for vx in self.graph.vs(children)]
[docs] def siblings(self) -> List[Scenario]: """Returns the scenario's sibling scenarios The scenario's parent's children (including self) Returns ------- List[Scenario] The sibling scenarios """ parent: Optional[Scenario] = self.parent() if parent is not None: return parent.children() else: return [vx["scenario"] for vx in self.graph.vs if vx.indegree() == 0]
# TODO: This duplicates the implementation of # ScenarioForest#root_scenarios() but Scenario does not currently # have access to its feature. Might want to change that
[docs] def path_scenarios(self) -> List["Scenario"]: """Returns the complete scenario path from the root scenario to (and including) self. Returns ------- List[Scenario] List of scenarios. The last scenario is self """ return self.ancestors() + [self]
[docs] def level(self) -> int: """Returns the scenario"s level in the scenario tree. Root scenario = Level 1 Returns ------- int The scenario"s level """ return self.graph.neighborhood_size(self.vertex, mode="IN", order=1000)
[docs] def is_organizational(self) -> bool: """Returns whether the scenario is an "organizational" scenario. "Organizational" scenarios are used for grouping only. They do not have any assertions. Returns ---------- bool Whether the scenario is an "organizational" scenario """ return len(self.assertions()) == 0
[docs] def index(self) -> Optional[int]: """Returns the "index" of the scenario. The scenario"s vertical position in the feature file. Returns ---------- int Index of self """ return self.vertex.index # TODO: start at index 1 (instead of 0)
[docs] def is_closed(self) -> bool: """Returns whether or not the scenario is "closed". A scenario is "closed" if additional child scenarios cannot be added which is the case when there is a "later" (higher index) scenario with a lower or equal indentation level in the feature file. Returns ---------- bool Whether or not the scenario is "closed" """ # Later scenario with lower or equal indentation level: closing_scenario: Optional[Scenario] = next( ( vx for vx in self.graph.vs() if vx.index > self.index() and self.graph.neighborhood_size(vx, mode="IN", order=1000) <= self.level() ), None, ) return closing_scenario is not None